VueはTreeコンポーネントを実装します

序文

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状態を設定して、次のレベルが開いているか閉じているかを制御します。

getClassNameis_open対応するクラス名は、三角形のアイコンが下または右に表示されているかどうかを示すためにレンダリングすることができます。

ではTree.vuename属性を設定し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トリガーインターフェイス全体を更新して再レンダリングします。

外部定義のデータ読み込み機能

template2つの新しい属性がテンプレートlazyとに追加されますload

lazy子コンポーネントに渡される指定されたデータは非同期でレンダリングさloadれ、属性に対応する関数は、loadNode使用するためにTreeコンポーネントに渡されるデータを取得する関数です。

<template>
  <Tree
    :data="data"
    :load="loadNode"
    :lazy="true"
  />
</template>

loadNode設計時に設定した関数は、2つのパラメータnodeオブジェクトと1つの関数を返しresolveます。nodeオブジェクトには2つのプロパティlayerが含まれていますchildren

layerlabelラベルのタブをクリックすると、いくつかのステージのレベルに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新しいドキュメント二つの性質loadingloadedロードの状況を示すために使用されている。場合はloadingtrueで、テンプレートがレンダリングされます加载中...言葉を。

受信lazyが真になると、外部で定義されたデータ読み込み関数を実行してthis.load非同期データが取得されますthis.load。2つのパラメータdataresolve関数が受け入れられます。

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.loadresolve関数パラメータとして渡され、非同期データが読み込まresolveれる関数が実行さます。loadedステータスがに更新されtrue、にloading更新されfalseます。次に、祖先から渡されたthis.updateData関数が実行され、非同期の戻り結果が渡されます。datathis.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はまた、のようないくつかのイベントリスナー関数、含まれdragstartdropdragoverなどを。

  • dragstartイベントは、マウスがdom要素上で押され、ドラッグされようとしているときにトリガーされます。そのイベントオブジェクトは、パラメーター値を設定するための関数のe呼び出しをサポートしe.dataTransfer.setDataます。これは、保持されたdom要素にバインドされたイベントです。
  • dragoverこれは、マウスが特定のdom要素を押し下げた後のドラッグプロセス中にトリガーされる機能です。
  • dropこのイベントは、マウスがdom要素を別のdom要素にドラッグして解放するとトリガーされます。これは、別のdom要素にバインドされたリスナーイベントであり、そのイベントオブジェクトee.dataTransfer.getData関数を介してdragstart内部的に設定されたパラメーター値を取得できます

ツリーコンポーネントのすべてのノードはすべてバインドされdragstartdropイベントが発生します。ノード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つのイベント関数dragstartdropバインドdragoverます。

Tree.vue

<template>
...
<div
      class="box"
      @click="toggle()"
      @dragstart="startDrag"
      @drop="dragEnd"
      @dragover="dragOver"
      draggable="true"
>
...
</template>

startDragイベントストレージ配列インデックスはthis.layere.dataTransfer.setData参照データ型の保存をサポートしていないため、JSON.stringify変換して使用します。

dragOverイベントe.preventDefault()呼び出す必要がありますdragEndそうしないと、関数はトリガーされません。

dragEndこの関数は、2つのノードのデータを取得し、祖先のメソッドdragDataupdatetree_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>

おすすめ

転載: blog.csdn.net/brokenkay/article/details/111771038