Vue 遞歸組件實現樹結構封裝 開源

<!-- checkbox.vue -->
<template>
  <label>
    <span>
      <input
        type="checkbox"
        :disabled="disabled"
        :checked="currentValue"
        @change="change">
    </span>
    <slot></slot>
  </label>
</template>
<script>
// 定義兩個 props:`trueValue` 和 `falseValue`,它們允許用戶指定 `value` 用什麼值來判斷是否選中。
// 因爲實際開發中,數據庫中並不直接保存 true / false,而是 1 / 0 或其它字符串
 import Emitter from '@/mixins/emitter.js';
  export default {
    name: 'iCheckbox',
    mixins: [ Emitter ],
    props: {
      disabled: {
        type: Boolean,
        default: false
      },
      value: {
        type: [String, Number, Boolean],
        default: false
      },
      trueValue: {
        type: [String, Number, Boolean],
        default: true
      },
      falseValue: {
        type: [String, Number, Boolean],
        default: false
      }
    },
    data () {
      return {
        currentValue: this.value
      };
    },
    watch: {
      value (val) {
        if (val === this.trueValue || val === this.falseValue) {
          this.updateModel();
        } else {
          throw 'Value should be trueValue or falseValue.';
        }
      }
    },
    methods: {
      change (event) {
        if (this.disabled) {
          return false;
        }

        const checked = event.target.checked;
        this.currentValue = checked;

        const value = checked ? this.trueValue : this.falseValue;
        //  `input`,用於實現 v-model 語法糖
        this.$emit('input', value);
        //  `on-change`,當選中 / 取消選中時觸發,用於通知父級狀態發生了變化
        this.$emit('on-change', value);
        this.dispatch('iFormItem', 'on-form-change', value);
      },
      updateModel () {
        this.currentValue = this.value === this.trueValue;
      }
    }
  }
</script>

<!-- src/components/tree/node.vue node.vue 是一個遞歸組件 -->
<template>
  <ul class="tree-ul">
    <li class="tree-li">
      <span class="tree-expand" @click="handleExpand">
        <span v-if="data.children && data.children.length && !data.expand">+</span>
        <span v-if="data.children && data.children.length && data.expand">-</span>
      </span>
      <i-checkbox
        v-if="showCheckbox"
        :value="data.checked"
        @input="handleCheck"
      ></i-checkbox>
      <span>{{ data.title }}</span>
      <!-- 遞歸組件有兩個必要條件:name 特性和終結條件 當 `data.children` 不存在或爲空數組時,遞歸也就停止了。 -->
      <!-- data.expand 是否展開子節點 -->
      <tree-node
        v-if="data.expand"
        v-for="(item, index) in data.children"
        :key="index"
        :data="item"
        :show-checkbox="showCheckbox"
      ></tree-node>
    </li>
  </ul>
</template>
<script>
  import iCheckbox from './checkbox.vue';
  import { findComponentUpward } from '@/libs/assist.js';
  export default {
    name: 'TreeNode',
    components: { iCheckbox },
    props: {
      data: {
        type: Object,
        default () {
          return {};
        }
      },
      showCheckbox: {
        type: Boolean,
        default: false
      }
    },
    data () {
      return {
        // 通過 `findComponentUpward` 向上找到了 Tree 的實例
        tree: findComponentUpward(this, 'Tree')
      }
    },
    methods: {
      handleExpand () {
        // 設置當前節點 是否展開 收起
        this.$set(this.data, 'expand', !this.data.expand);

        if (this.tree) {
          this.tree.emitEvent('on-toggle-expand', this.data);
        }
      },
      // 設置當前節點 是否 選中 checkBox
      handleCheck (checked) {
        this.updateTreeDown(this.data, checked);

        if (this.tree) {
          this.tree.emitEvent('on-check-change', this.data);
          console.log(this.tree);
        }
      },
      updateTreeDown (data, checked) {
        this.$set(data, 'checked', checked);

        if (data.children && data.children.length) {
          data.children.forEach(item => {
            this.updateTreeDown(item, checked);
          });
        }
      }
    },
    watch: {
      // data.children 當前的節點有兩個”身份“,它既是下屬節點的父節點,同時也是上級節點的子節點
    'data.children': {
      // handler 執行的函數
        handler (data) {
          if (data) {
            // checkedAll 最終返回結果就是當前子節點是否都被選中
            const checkedAll = !data.some(item => !item.checked);
            this.$set(this.data, 'checked', checkedAll);
          }
        },
        deep: true  // 深度監聽
      }
    }
  }
</script>
<style>
  .tree-ul, .tree-li{
    list-style: none;
    padding-left: 10px;
  }
  .tree-expand{
    cursor: pointer;
  }
</style>

<template>
  <div>
    <tree-node
      v-for="(item, index) in cloneData"
      :key="index"
      :data="item"
      :show-checkbox="showCheckbox"
    ></tree-node>
  </div>
</template>
<script>
  import TreeNode from './node.vue';  // 遞歸組件
  import { deepCopy } from '@/libs/assist.js';

  export default {
    name: 'Tree',
    components: { TreeNode },
    props: {
      data: {
        type: Array,
        default () {
          return [];
        }
      },
      showCheckbox: {
        type: Boolean,
        default: false
      }
    },
    data () {
      return {
        cloneData: []
      }
    },
    created () {
      this.rebuildData();
    },
    watch: {
      data () {
        this.rebuildData();
      }
    },
    methods: {
      rebuildData () {
        this.cloneData = deepCopy(this.data);
      },
      emitEvent (eventName, data) {
        this.$emit(eventName, data, this.cloneData);
      }
    }
  }
</script>
發佈了102 篇原創文章 · 獲贊 46 · 訪問量 39萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章