序文
Treeコンポーネントは、州、市、郡の表示など、実際のアプリケーションで非常に広く使用されています。通常、所属を含む一部のデータは、Treeコンポーネントを使用して表示できます。Treeコンポーネントを実装するために理解する必要があることについて詳しく学びましょう。実践的なデモを通じて。原理と実装の詳細。この記事で実装される機能には、次の3つのポイントが含まれます。
- ネストされたデータを表示できるTreeコンポーネントの基本バージョンを実装する
- ツリーコンポーネントのラベルをクリックすると、その次のレベルのデータが非同期読み込みをサポートします
- ツリーコンポーネントのノードはドラッグアンドドロップをサポートします
デモの最終的なレンダリングは次のとおりです。
ツリーの基本バージョン
Treeコンポーネントの基本バージョンを実装するのは非常に簡単であり、原則はコンポーネントのネストの使用を習得することです。
外線通話
まず、Treeコンポーネントを外部から呼び出すためのテンプレートを次のように設定します。Treeコンポーネントは、データ属性を渡すだけで、データデータを対応するツリー構造にレンダリングできます。
<template>
<Tree
:data="data"
/>
</template>
<script>
import Tree from "../../components/Tree/index.vue";
export default {
data() {
return {
data: [
{
label: "一级",
children: [
{
label: "二级1",
children: [
{
label: "三级1",
},
],
}
],
}
],
};
},
components: {
Tree,
}
}
</script>
ツリーコンポーネントの実装
Treeコンポーネントには、2つのファイルが含まれています。1つはで、もう1つはindex.vue
ですTree.vue
。
index.vue
index.vue
内容は次のとおりです。ツリーコンポーネントと外界との架け橋となります。この中間層では多くの拡張機能を処理できTree.vue
、データ表示に関連するロジックのみが必要です。
<template>
<div class="tree">
<template v-for="(item, index) in data_source">
<Tree :item="item" :key="index" class="first-layer" />
</template>
</div>
</template>
<script>
import Tree from "./Tree";
import { deepClone } from "../util/tool.js"; //深度克隆函数
export default {
props: {
data: Object | Array,
},
data() {
return {
tree_data: deepClone(this.data),
};
},
computed: {
data_source() {
if (Array.isArray(this.tree_data)) {
return this.tree_data;
} else {
return [this.tree_data];
}
},
},
components: {
Tree,
}
}
</script>
上記のコードdata
は、外部から渡されたデータのディープクローン割り当てを行いますtree_data
。データを変更するコンポーネントの内部にいくつかの関数がある場合、外部から渡されたデータにtree_data
影響を与えることなく、直接操作できます。
Treeコンポーネントが配列レンダリングとオブジェクトレンダリングをサポートするために、新しい計算プロパティがdata_source
追加され、すべてのデータソースが配列に変換されてから、レンダリングTree
コンポーネントがトラバースされます。
Tree.vue
Tree.vue
このファイルには、ツリー構造データをレンダリングするための特定のコードが含まれています。そのテンプレートコンテンツは、label
対応するタイトルコンテンツをレンダリングする部分と、子をレンダリングする部分の2つの部分に分かれています。
コンポーネントis_open
に状態を設定して、次のレベルが開いているか閉じているかを制御します。
getClassName
is_open
対応するクラス名は、三角形のアイコンが下または右に表示されているかどうかを示すためにレンダリングすることができます。
ではTree.vue
name属性を設定しTree
、その後は、テンプレート内の巣に自分自身を呼び出すことができます。横断することによりitem.children
、データはそれぞれのレベルにコピーするTree
コンポーネント、木構造データをレンダリングの目的を達成するために。
<template>
<div class="tree">
<div
class="box"
@click="toggle()"
>
<div :class="['lt', getClassName]"></div>
<div class="label lt">{
{ item.label }}</div>
</div>
<div class="drop-list" v-show="is_open">
<template v-for="(child, index) in item.children">
<Tree :item="child" :key="index" />
</template>
</div>
</div>
</template>
<script>
export default {
name: "Tree",
props: {
item: Object
},
data() {
return {
is_open: false, //是否打开下一级
};
},
computed: {
getClassName(){
return this.is_open ? "down" : "right";
}
},
methods:{
toggle() {
this.is_open = !this.is_open;
},
}
};
</script>
レンダリング結果は次のとおりです。
非同期読み込み
上記の基本ツリーコンポーネントは、基本データレンダリングのみをサポートします。外部データを最初に準備してから、対応するデータをレンダリングするために外部データにスローする必要があります。
データがサーバー側に保持されていると仮定すると、クリックされたlabel
ときにサーバーに要求されることを願っています。待機期間中は、Treeコンポーネントがクリックさloading...
れたレベルで表示された単語が表示され、データがクリックされるまで待機します。子をレンダリングする前に完全に返されます。次の図に示すように。
ツリーコンポーネント自体はデータを要求する場所を認識できないため、データを要求するためのロジックはカスタムコンテンツに属し、ユーザーが作成する必要があります。ロジックのこの部分を関数にカプセル化して、に渡すのが最善です。 Treeコンポーネント。Treeコンポーネントlabel
をクリックすると、Treeコンポーネントは非同期リクエストが必要であることを検出し、渡された関数を直接呼び出します。リクエストが成功すると、データを独自のtree_data
データソースに追加し、ページを再表示します。手順は次のように分かれています。
- 外部で定義されたデータ読み込み関数がTreeコンポーネントに渡されます。
- Treeコンポーネントを
label
クリックすると、データ読み込み機能がトリガーされloading...
、ステータスがに更新され、応答結果を待ちます。 - 応答データが戻ったら、
tree_data
トリガーインターフェイス全体を更新して再レンダリングします。
外部定義のデータ読み込み機能
template
2つの新しい属性がテンプレートlazy
とに追加されますload
。
lazy
子コンポーネントに渡される指定されたデータは非同期でレンダリングさload
れ、属性に対応する関数は、loadNode
使用するためにTreeコンポーネントに渡されるデータを取得する関数です。
<template>
<Tree
:data="data"
:load="loadNode"
:lazy="true"
/>
</template>
loadNode
設計時に設定した関数は、2つのパラメータnode
オブジェクトと1つの関数を返しresolve
ます。node
オブジェクトには2つのプロパティlayer
とが含まれていますchildren
。
layer
label
ラベルのタブをクリックすると、いくつかのステージのレベルにchildren
あり、データdata
の最初のステージについて次のコードに示されているデータの1つです。children
ユーザーは最初のステージをクリックするlabel
と、最初のステージのchildren
データはnode
オブジェクトを渡すことができます。取得しました。
resolve
関数の実行により、最終結果がTreeコンポーネントに渡されます。次のコードは、ユーザーが最初のレベルのラベルをクリックすると、そこにdata
定義されている初期データが直接返され、他のレベルのラベルをクリックすると、タイマーの非同期操作が実行さresolve
れます。パッケージ化されたデータをツリーコンポーネントに渡してレンダリングします。
外部调用文件
data(){
return {
label:"一级数据"
children:[{
label:"二级数据"
}]
}
},
methods: {
loadNode(node, resolve) {
const { layer, children } = node;
if (layer <= 1) {
resolve(children);
} else {
setTimeout(() => {
resolve([
{
title: `第${layer}层`,
},
]);
}, 1500);
}
},
}
読み込み機能を処理するツリーコンポーネント
でTree.vue
新しいドキュメント二つの性質loading
とloaded
ロードの状況を示すために使用されている。場合はloading
trueで、テンプレートがレンダリングされます加载中...
言葉を。
受信lazy
が真になると、外部で定義されたデータ読み込み関数を実行してthis.load
非同期データが取得されますthis.load
。2つのパラメータdata
とresolve
関数が受け入れられます。
Tree.vue
ファイル
props: {
item: Object
},
data() {
return {
is_open: false, //是否打开下一级
loading: false, //是否加载中
loaded: false, //是否加载完毕
};
},
methods:{
toggle(){ //点击label时触发
if(this.lazy){ //异步请求数据
if (this.loading) {
//正在加载中
return false;
}
this.loading = true;
const resolve = (data) => {
this.is_open = !this.is_open;
this.loading = false;
this.loaded = true;
this.updateData({ data, layer: this.layer });
};
const data = { ...this.item, layer: this.layer.length };
this.load(data, resolve);//执行数据加载函数
}else{
...
}
}
}
const data = { ...this.item, layer: this.layer.length };
this.item
現在のレベルのデータが格納されます。現在のレベルthis.layer
のインデックス配列が格納され、その配列の長さは対応するレベル数になります。this.layer
詳細な説明は次のとおりです。
データソースデータが次のとおりであると想定します。ユーザーが2-2级
ラベルをクリックすると、this.layer
値はになり[0,1]
ます。this.layer
このレベルのデータがデータソースにあることを追跡できるインデックスコレクションを介して。
data = [{
label:"1级",
children:[{
label:"2-1级"
},{
label:"2-2级"
}}]
}]
this.load
resolve
関数はパラメータとして渡され、非同期データが読み込まresolve
れると関数が実行されます。loaded
ステータスがに更新されtrue
、にloading
更新されfalse
ます。次に、祖先から渡されたthis.updateData
関数が実行され、非同期の戻り結果が渡されます。data
。this.updateData
実行は、ツリー成分データのルート・レベルを更新しtree_data
、それによりコンポーネントツリーを再レンダリング。
tree_dataを更新します
updateData
この関数は、子から渡された非同期応答データdata
とインデックス配列を取得しますlayer
。これら2つのパラメーターを使用してdata
、ルートノードのデータソースを更新できます。
getTarget
関数の機能は、インデックス配列に従って配列に対応する最後のレベルのオブジェクトを見つけることである。例えばlayer
、値は[0,0]
、とresult
の値であります
[
{
label:"第一级",
children:[{
label:"第二级"
}}]
}
]
getTarget(layer,result)
実行結果はそのオブジェクトに返さlabel
れ"第二级"
ます。このオブジェクトresult
のデータが操作されると、それに応じてデータが変更されます。
index.vue
ファイル
methods:{
updateData(data) {
const { data: list, layer } = data;
let result = [...this.data_source];
const tmp = this.getTarget(layer, result);//根据索引数组和数据源找到那一级的数据
tmp.children = list;
this.tree_data = result;
}
}
getTarget
関数を使用してそのレベルのデータを検索しtmp
、にchildren
更新してlist
、にresult
再割り当てしtree_data
ます。このようにして、非同期で要求されたデータがデータソースに追加されます。
ノードドラッグ
ノードのドラッグアンドドロップは、HTML5のドラッグアンドドロップAPIを使用して簡単に実装できます。ノードをdom要素に追加draggable="true"
すると、要素のドラッグが許可されることを意味します。
HTML5のドラッグ&ドロップAPIはまた、のようないくつかのイベントリスナー関数、含まれdragstart
、drop
、dragover
などを。
dragstart
イベントは、マウスがdom要素上で押され、ドラッグされようとしているときにトリガーされます。そのイベントオブジェクトは、パラメーター値を設定するための関数のe
呼び出しをサポートしe.dataTransfer.setData
ます。これは、保持されたdom要素にバインドされたイベントです。dragover
これは、マウスが特定のdom要素を押し下げた後のドラッグプロセス中にトリガーされる機能です。drop
このイベントは、マウスがdom要素を別のdom要素にドラッグして解放するとトリガーされます。これは、別のdom要素にバインドされたリスナーイベントであり、そのイベントオブジェクトe
はe.dataTransfer.getData
関数を介してdragstart
内部的に設定されたパラメーター値を取得できます。
ツリーコンポーネントのすべてのノードはすべてバインドされdragstart
、drop
イベントが発生します。ノード1が別のノード2に移動されると、ノード1のdragstart
すべてのデータ情報を関数を介してキャプチャしてe.dataTransfer.setData
保存できます。
ノード2は、その上のノード1のリリースをリッスンし、drop
イベントがトリガーされます。drop
イベント内では、現在のノード(つまり、ノード2)e.dataTransfer.getData
のデータ情報を取得できます。また、ノード2のデータ情報も取得できます。ノード1。
ノード1とノード2のデータ情報を同時に取得tree_data
する場合は、ルートデータソース上の別のデータオブジェクトの下にデータオブジェクトを移動する必要があることを明確に知ることと同じです。 domノードは運用tree_data
上の問題に変わります。
ドラッグイベントをバインドします
まずdraggable="true"
、テンプレートの各domノードに属性を設定して、すべてのノードがドラッグアンドドロップをサポートするようにします。同時に、3つのイベント関数dragstart
とdrop
をバインドしdragover
ます。
Tree.vue
<template>
...
<div
class="box"
@click="toggle()"
@dragstart="startDrag"
@drop="dragEnd"
@dragover="dragOver"
draggable="true"
>
...
</template>
startDrag
イベントストレージ配列インデックスはthis.layer
、e.dataTransfer.setData
参照データ型の保存をサポートしていないため、JSON.stringify
変換して使用します。
dragOver
イベントe.preventDefault()
で呼び出す必要がありますdragEnd
。そうしないと、関数はトリガーされません。
dragEnd
この関数は、2つのノードのデータを取得し、祖先のメソッドdragData
updatetree_data
の呼び出しを開始します。ここdragData
での祖先メソッドは、provide,inject
メカニズムを介して子孫に渡されます。これは、最後のコード全体で確認できます。
methods: {
dragOver(e) {
e.preventDefault();
},
startDrag(e) {
e.dataTransfer.setData("data", JSON.stringify(this.layer));
},
dragEnd(e) {
e.preventDefault();
const old_layer = JSON.parse(e.dataTransfer.getData("data"));
this.dragData(old_layer, this.layer, this);
}
}
Tree_dataを更新します
dragData
実行プロセスは、ドラッグされたノードのデータオブジェクトを新しいノードのデータオブジェクトのchildren
配列に追加することです。
をthis.getTarget
実行している2つのノードのデータオブジェクトを見つけることによりnew_obj.children.unshift(old_obj);
、古いデータオブジェクトが新しいオブジェクトのchildren
配列に追加されます。さらに、元の位置にある古いデータオブジェクトを削除する必要があります。そうしないと、古いデータのコピーが2つ作成されます。オブジェクト。
元の位置にある古いデータオブジェクトを削除する場合は、親の子配列の下にある親データオブジェクトとそのインデックス値を見つける必要があります。見つけたら、splice
元の位置にある古いデータオブジェクトを使用して削除できます。最後に、変更したデータをに割り当てますtree_data
。
index.vue
ファイル
methods: {
dragData(old_layer, new_layer, elem) {
let result = [...this.data_source];
const old_obj = this.getTarget(old_layer, result);
const new_obj = this.getTarget(new_layer, result);
//找到被拖拽数据对象的父级数据对象
const old_obj_parent = this.getTarget(
old_layer.slice(0, old_layer.length - 1),
result
);
const index = old_layer[old_layer.length - 1]; //获取倒数第二个索引
if (!new_obj.children) {
new_obj.children = [];
}
if (Array.isArray(old_obj_parent)) {
old_obj_parent.splice(index, 1);
} else {
old_obj_parent.children.splice(index, 1); //删掉原来位置的被拖拽数据
}
new_obj.children.unshift(old_obj); //将被拖拽的数据加到目标处
this.tree_data = result;
}
...
}
完全なコード
index.vue
<template>
<div class="tree">
<template v-for="(item, index) in data_source">
<Tree :item="item" :key="index" :layer="[index]" class="first-layer" />
</template>
</div>
</template>
<script>
import Tree from "./Tree";
export default {
props: {
data: Object | Array,
label: {
type: String,
default: "label",
},
children: {
type: String,
default: "children",
},
lazy: {
type: Boolean,
default: false,
},
load: {
type: Function,
default: () => {},
},
},
provide() {
return {
label: this.label,
children: this.children,
lazy: this.lazy,
load: this.load,
updateData: this.updateData,
dragData: this.dragData,
};
},
data() {
return {
tree_data: this.data,
};
},
computed: {
data_source() {
if (Array.isArray(this.tree_data)) {
return this.tree_data;
} else {
return [this.tree_data];
}
},
},
components: {
Tree,
},
methods: {
dragData(old_layer, new_layer, elem) {
//数据拖拽
const flag = old_layer.every((item, index) => {
return item === new_layer[index];
});
if (flag) {
//不能将元素拖拽给自己的子元素
return false;
}
let result = [...this.data_source];
const old_obj = this.getTarget(old_layer, result);
const new_obj = this.getTarget(new_layer, result);
const old_obj_parent = this.getTarget(
old_layer.slice(0, old_layer.length - 1),
result
);
const index = old_layer[old_layer.length - 1]; //获取倒数第二个索引
if (!new_obj[this.children]) {
new_obj[this.children] = [];
}
if (Array.isArray(old_obj_parent)) {
old_obj_parent.splice(index, 1);
} else {
old_obj_parent[this.children].splice(index, 1); //原来位置的被拖拽数据删掉x
}
new_obj[this.children].unshift(old_obj); //将被拖拽的数据加到目标处
this.tree_data = Array.isArray(this.tree_data) ? result : result[0];
this.$nextTick(() => {
!elem.is_open && elem.toggle(); //如果是关闭状态拖拽过去打开
});
},
getTarget(layer, result) {
if (layer.length == 0) {
return result;
}
let data_obj;
Array.from(Array(layer.length)).reduce((cur, prev, index) => {
if (!cur) return null;
if (index == 0) {
data_obj = cur[layer[index]];
} else {
data_obj = cur[this.children][layer[index]];
}
return data_obj;
}, result);
return data_obj;
},
updateData(data) {
const { data: list, layer } = data;
let result = [...this.data_source];
const tmp = this.getTarget(layer, result);
tmp[this.children] = list;
this.tree_data = Array.isArray(this.tree_data) ? result : result[0];
},
},
};
</script>
<style lang="scss" scoped>
.first-layer {
margin-bottom: 20px;
}
</style>
Tree.vue
<template>
<div class="tree">
<div
class="box"
@click="toggle()"
@dragstart="startDrag"
@drop="dragEnd"
@dragover="dragOver"
draggable="true"
>
<div :class="['lt', getClassName()]"></div>
<div class="label lt">{
{ item[label] }}</div>
<div class="lt load" v-if="loading_status">loading...</div>
</div>
<div class="drop-list" v-show="show_next">
<template v-for="(child, index) in item[children]">
<Tree :item="child" :key="index" :layer="[...layer, index]" />
</template>
</div>
</div>
</template>
<script>
export default {
name: "Tree",
props: {
item: Object,
layer: Array,
},
inject: ["label", "children", "lazy", "load", "updateData", "dragData"],
data() {
return {
is_open: false, //是否打开下一级
loading: false, //是否加载中
loaded: false, //是否加载完毕
};
},
computed: {
show_next() {
//是否显示下一级
if (
this.is_open === true &&
(this.loaded === true || this.lazy === false)
) {
return true;
} else {
return false;
}
},
loading_status() {
//控制loading...显示图标
if (!this.lazy) {
return false;
} else {
if (this.loading === true) {
return true;
} else {
return false;
}
}
},
},
methods: {
getClassName() {
if (this.item[this.children] && this.item[this.children].length > 0) {
return this.is_open ? "down" : "right";
} else {
return "gap";
}
},
dragOver(e) {
e.preventDefault();
},
startDrag(e) {
e.dataTransfer.setData("data", JSON.stringify(this.layer));
},
dragEnd(e) {
e.preventDefault();
const old_layer = JSON.parse(e.dataTransfer.getData("data"));
this.dragData(old_layer, this.layer, this);
},
toggle() {
if (this.lazy) {
if (this.loaded) {
//已经加载完毕
this.is_open = !this.is_open;
return false;
}
if (this.loading) {
//正在加载中
return false;
}
this.loading = true;
const resolve = (data) => {
this.is_open = !this.is_open;
this.loading = false;
this.loaded = true;
this.updateData({ data, layer: this.layer });
};
const data = { ...this.item, layer: this.layer.length };
this.load(data, resolve);
} else {
this.is_open = !this.is_open;
}
},
},
};
</script>
<style lang="scss" scoped>
.lt {
float: left;
}
.load {
font-size: 12px;
margin-left: 5px;
margin-top: 4px;
}
.gap {
margin-left: 10px;
width: 1px;
height: 1px;
}
.box::before {
width: 0;
height: 0;
content: "";
display: block;
clear: both;
cursor: pointer;
}
@mixin triangle() {
border-color: #57af1a #fff #fff #fff;
border-style: solid;
border-width: 4px 4px 0 4px;
height: 0;
width: 0;
}
.label {
font-size: 14px;
margin-left: 5px;
}
.down {
@include triangle();
margin-top: 8px;
}
.right {
@include triangle();
transform: rotate(-90deg);
margin-top: 8px;
}
.drop-list {
margin-left: 10px;
}
</style>
Treeコンポーネント(テストファイル)を外部から呼び出す
<template>
<Tree
:data="data"
label="title"
children="childrens"
:load="loadNode"
:lazy="true"
/>
</template>
<script>
import Tree from "../../components/Tree/index.vue";
export default {
data() {
return {
data: [
{
title: "一级",
childrens: [
{
title: "二级1",
childrens: [
{
title: "三级1",
},
],
},
{
title: "二级2",
childrens: [
{
title: "三级2",
},
],
},
],
},
{
title: "一级2",
childrens: [
{
title: "二级2",
},
],
},
],
};
},
components: {
Tree,
},
methods: {
loadNode(node, resolve) {
const { layer, childrens } = node;
if (childrens && childrens.length > 0) {
resolve(childrens);
} else {
setTimeout(() => {
resolve([
{
title: `第${layer}层`,
},
]);
}, 1500);
}
},
},
};
</script>
<style>
</style>