Element Transfer

效果圖

這是 Element-UI 的 Transfer 組件,下面就配合源碼看下具體實現。

Template

<template>
  <div class="el-transfer">
    <transfer-panel
      // 將父組件的props一塊兒傳給自組件
      v-bind="$props"
      // 組件註冊引用
      ref="leftPanel"
      // 須要展現的數據
      :data="sourceData"
      // 標題
      :title="titles[0] || t('el.transfer.titles.0')"
      // 默認勾選項
      :default-checked="leftDefaultChecked"
      // 搜索欄默認的佔位符
      :placeholder="filterPlaceholder || t('el.transfer.filterPlaceholder')"
      // 勾選項發生變化的回調
      @checked-change="onSourceCheckedChange">
      // 底部插槽
      <slot name="left-footer"></slot>
    </transfer-panel>
    <div class="el-transfer__buttons">
      <el-button
        // 按鈕類型
        type="primary"
        :class="['el-transfer__button', hasButtonTexts ? 'is-with-texts' : '']"
        // 點擊回調
        @click.native="addToLeft"
        // 動態綁定disable狀態
        :disabled="rightChecked.length === 0">
        <i class="el-icon-arrow-left"></i>
        // 按鈕名稱
        <span v-if="buttonTexts[0] !== undefined">{{ buttonTexts[0] }}</span>
      </el-button>
      <el-button
        type="primary"
        :class="['el-transfer__button', hasButtonTexts ? 'is-with-texts' : '']"
        @click.native="addToRight"
        :disabled="leftChecked.length === 0">
        <span v-if="buttonTexts[1] !== undefined">{{ buttonTexts[1] }}</span>
        <i class="el-icon-arrow-right"></i>
      </el-button>
    </div>
    <transfer-panel
      v-bind="$props"
      ref="rightPanel"
      :data="targetData"
      :title="titles[1] || t('el.transfer.titles.1')"
      :default-checked="rightDefaultChecked"
      :placeholder="filterPlaceholder || t('el.transfer.filterPlaceholder')"
      @checked-change="onTargetCheckedChange">
      <slot name="right-footer"></slot>
    </transfer-panel>
  </div>
</template>
複製代碼

總體上,能夠劃分爲左中右三塊,左右兩個 TransferPanel 組件承載數據展現。中間兩個 ElButton 是左右移動的操做按鈕。結構清晰。數組

JS 部分

mixins: [Emitter, Locale, Migrating],
複製代碼

mixins 部分混入了三個對象。Locale 是國際化的東西,Migrating 是組件遷移的一些提示信息。須要關注的是 Emitter 部分,代碼以下:bash

// 尋找全部子組件,直到找到名爲componentName的組件,調用其$emit方法
function broadcast(componentName, eventName, params) {
  this.$children.forEach(child => {
    var name = child.$options.componentName;

    if (name === componentName) {
      child.$emit.apply(child, [eventName].concat(params));
    } else {
      broadcast.apply(child, [componentName, eventName].concat([params]));
    }
  });
}
export default {
  // 事件定向傳播
  methods: {
    // 尋找全部父組件,直到找到名爲componentName的組件,調用其$emit方法
    dispatch(componentName, eventName, params) {
      var parent = this.$parent || this.$root;
      var name = parent.$options.componentName;

      while (parent && (!name || name !== componentName)) {
        parent = parent.$parent;

        if (parent) {
          name = parent.$options.componentName;
        }
      }
      if (parent) {
        parent.$emit.apply(parent, [eventName].concat(params));
      }
    },
    broadcast(componentName, eventName, params) {
      broadcast.call(this, componentName, eventName, params);
    }
  }
};
複製代碼

提供了兩個方法: dispatch, broadcast 作事件的定向傳播。app

屬性 props

  • data ,Transfer 的數據源
// array[{ key, label, disabled }]
data: {
type: Array,
default() {
  return [];
}
}
複製代碼

傳入的數組,每一項須要有三個屬性,key : 惟一標識,label : 展現內容,disabled : 是否可勾選,若是不想用這三個屬性名,能夠經過 props 屬性設置別名。函數

  • props , 數據源的字段別名
props: {
    type: Object,
    default() {
      return {
        label: 'label',
        key: 'key',
        disabled: 'disabled'
      };
    }
  }
複製代碼
  • titles , 容許自定義標題列表
// ['列表 1', '列表 2']
titles: {
    type: Array,
    default() {
      return [];
    }
  }
複製代碼
  • buttonTexts , 自定義 el-button 文案
// ['到左邊', '到右邊']
buttonTexts: {
    type: Array,
    default() {
      return [];
    }
  }
複製代碼
  • filterPlaceholder , 搜索框佔位符
filterPlaceholder: {
    type: String,
    default: ''
  }
複製代碼
  • filterMethod , 自定義搜索方法
filterMethod: Function
複製代碼
  • leftDefaultChecked / rightDefaultChecked ,初始狀態下左側/右側列表的已勾選項的 key 數組
leftDefaultChecked: {
    type: Array,
    default() {
      return [];
    }
  }
複製代碼
  • renderContent , 自定義的數據渲染函數
renderContent: Function,
複製代碼
  • value , 目標列表的 key 數組
value: {
    type: Array,
    default() {
      return [];
    }
  }
複製代碼
  • format , 列表頂部勾選狀態文案
// object{noChecked, hasChecked}
  format: {
    type: Object,
    default() {
      return {};
    }
  }
複製代碼
  • filterable , 是否可搜索,默認爲 false
filterable: Boolean
複製代碼
  • targetOrder , 右側列表元素的排序策略:若爲 original,則保持與數據源相同的順序;若爲 push,則新加入的元素排在最後;若爲 unshift,則新加入的元素排在最前
targetOrder: {
    type: String,
    default: 'original'
  }
複製代碼

計算屬性 computed

  • dataObj , data 數組轉爲對象
// [{key:1,label:'數據1',disabled:false}] => {1:{key:1,label:'數據1',disabled:false}
dataObj() {
    const key = this.props.key;
    return this.data.reduce((o, cur) => (o[cur[key]] = cur) && o, {});
}
複製代碼
  • sourceData , leftPanel 數據源
// 篩選在 data 中 ,可是不在 value 中的數據
sourceData() {
    return this.data.filter(item => this.value.indexOf(item[this.props.key]) === -1);
  }
複製代碼
  • targetData , rightPanel 數據源
targetData() {
    // 目標源排序順序爲 original,按照數據在 data 數組的前後順序
    if (this.targetOrder === 'original') {
      return this.data.filter(item => this.value.indexOf(item[this.props.key]) > -1);
    // 不然按照 value 數組中 key 的前後順序
    } else {
      return this.value.reduce((arr, cur) => {
        const val = this.dataObj[cur];
        if (val) {
          arr.push(val);
        }
        return arr;
      }, []);
    }
  }
複製代碼
  • hasButtonTexts , 是否傳入可用的按鈕文案
// 當傳入的 button-text 有兩項的時候返回 true 
hasButtonTexts() {
    return this.buttonTexts.length === 2;
}
複製代碼

方法 methods

methods 中 leftPanel 和 rightPanel 的部分是對稱的,因此只選取 rightPanel 部分展現:ui

// val : 當前選中項的 key 數組
// movedKeys: 選中狀態發生變化的 key 數組
onTargetCheckedChange(val, movedKeys) {
    this.rightChecked = val;
    if (movedKeys === undefined) return;
    this.$emit('right-check-change', val, movedKeys);
},
addToLeft() {
    // rightPanel 中數據項的 key 數組
    let currentValue = this.value.slice();
    // 從 currentValue 中刪除選中的項
    this.rightChecked.forEach(item => {
      const index = currentValue.indexOf(item);
      if (index > -1) {
        currentValue.splice(index, 1);
      }
    });
    // currentValue: 當前 rightPanel 中存在數據的 key 數組
    // rightChecked: 選中移動的數據項的 key 數組
    this.$emit('input', currentValue);
    this.$emit('change', currentValue, 'left', this.rightChecked);
  },
clearQuery(which) {
    // 清除 leftPanel 搜索欄的搜索條件
    if (which === 'left') {
      this.$refs.leftPanel.query = '';
    // 清除 rightPanel 搜索欄的搜索條件
    } else if (which === 'right') {
      this.$refs.rightPanel.query = '';
    }
}                                  
複製代碼

Transfer 組件部分就這些內容,主要是控制傳入 TransferPanel 的 data ,以及向外發射 change ,check-change 事件。this

ElTransferPanel

template

<template>
  <div class="el-transfer-panel">
    <p class="el-transfer-panel__header">
      // 全選框
      <el-checkbox
        // 綁定值初始爲 false
        v-model="allChecked"
        // 勾選回調
        @change="handleAllCheckedChange"
        // 設置 indeterminate 狀態,只負責樣式控制
        :indeterminate="isIndeterminate">
        // 展現文本
        {{ title }}
        // 勾選總結文本
        <span>{{ checkedSummary }}</span>
      </el-checkbox>
    </p>
    
    <div :class="['el-transfer-panel__body', hasFooter ? 'is-with-footer' : '']">
      // 搜索欄
      <el-input
        class="el-transfer-panel__filter"
        // 綁定值,默認爲‘’
        v-model="query"
        // 尺寸
        size="small"
        // 佔位符
        :placeholder="placeholder"
        // mouseenter 事件
        @mouseenter.native="inputHover = true"
        // mouseleave 事件
        @mouseleave.native="inputHover = false"
        // 設置 filterable 才展現
        v-if="filterable">
        // prefix 插槽, 點擊清除搜索欄條件
        <i slot="prefix"
          :class="['el-input__icon', 'el-icon-' + inputIcon]"
          @click="clearQuery"
        ></i>
      </el-input>
      // 多選框組
      <el-checkbox-group
        // 綁定值,默認爲 []
        v-model="checked"
        // 當有匹配數據而且數據源有內容的時候展現
        v-show="!hasNoMatch && data.length > 0"
        // 根據 filterable 動態綁定 class
        :class="{ 'is-filterable': filterable }"
        class="el-transfer-panel__list">
        // v-for 列表渲染,數據源爲 filteredData
        <el-checkbox
          class="el-transfer-panel__item"
          // 選中狀態的值
          :label="item[keyProp]"
          // 是否禁用
          :disabled="item[disabledProp]"
          :key="item[keyProp]"
          v-for="item in filteredData">
          // option-content 組件
          <option-content :option="item"></option-content>
        </el-checkbox>
      </el-checkbox-group>
      // 沒有匹配數據時的展現內容
      <p
        class="el-transfer-panel__empty"
        v-show="hasNoMatch">{{ t('el.transfer.noMatch') }}</p>
      // 有匹配項而且數據項爲空時展現內容
      <p
        class="el-transfer-panel__empty"
        v-show="data.length === 0 && !hasNoMatch">{{ t('el.transfer.noData') }}</p>
    </div>
    // 底部插槽,當設置footer時展現
    <p class="el-transfer-panel__footer" v-if="hasFooter">
      <slot></slot>
    </p>
  </div>
</template>
複製代碼

JS

引入的組件中,須要關注下 option-content ,它是 render 函數直接定義的spa

OptionContent: {
    props: {
      option: Object
    },
    render(h) {
      // 獲取名爲 ElTransferPanel 的父組件
      const getParent = vm => {
        if (vm.$options.componentName === 'ElTransferPanel') {
          return vm;
        } else if (vm.$parent) {
          return getParent(vm.$parent);
        } else {
          return vm;
        }
      };
      const panel = getParent(this);
      // 獲取 transfer 組件
      const transfer = panel.$parent || panel;
      // 若是設置了自定義數據項渲染函數,則調用自定義的渲染函數
      // 若是沒有定義 render-content 方法,則檢查 Transfer 組件是否設置了 slot-scope 插槽內容
      // 若是設置了,則用 slot-scope 內容渲染
      // 不然用默認的 span 標籤渲染
      // 意味着數據項的渲染能夠經過 render-content 或者 slot-scoped 自定義
      return panel.renderContent
        ? panel.renderContent(h, this.option)
        : transfer.$scopedSlots.default
          ? transfer.$scopedSlots.default({ option: this.option })
          : <span>{ this.option[panel.labelProp] || this.option[panel.keyProp] }</span>;
    }
 }
複製代碼

組件傳入的 option 是 item ,item 來自 filteredData,3d

filteredData() {
    // data 爲 數據源, leftPanel 對應 sourceData
    return this.data.filter(item => {
      // 若是自定義了搜索方法,則調用自定義的方法
      if (typeof this.filterMethod === 'function') {
        return this.filterMethod(this.query, item);
      // 默認搜索規則是數據項的 label 中是否包含輸入的條件 
      } else {
        const label = item[this.labelProp] || item[this.keyProp].toString();
        return label.toLowerCase().indexOf(this.query.toLowerCase()) > -1;
      }
    });
}
複製代碼

watch

// 選擇項發生變化
  // val 當前選中的元素的 key 數組
  // oldVal 前一狀態選中的元素的 key 數組
  checked(val, oldVal) {
    // 更新全新狀態
    this.updateAllChecked();
    // 若是改變是用戶點擊形成的
    if (this.checkChangeByUser) {
      // 選中狀態發生變化的元素的 key 數組
      const movedKeys = val.concat(oldVal)
        .filter(v => val.indexOf(v) === -1 || oldVal.indexOf(v) === -1);
      this.$emit('checked-change', val, movedKeys);
    } else {
      this.$emit('checked-change', val);
      this.checkChangeByUser = true;
    }
  },
  // 數據源發生變化
  data() {
    const checked = [];
    const filteredDataKeys = this.filteredData.map(item => item[this.keyProp]);
    this.checked.forEach(item => {
      if (filteredDataKeys.indexOf(item) > -1) {
        checked.push(item);
      }
    });
    // 標記這次勾選狀態改變不是由用戶形成的
    this.checkChangeByUser = false;
    // 從新設置勾選的元素項
    this.checked = checked;
  },
  // 可勾選的數據改變
  checkableData() {
    this.updateAllChecked();
  },
  // 默認選中的數據改變
  defaultChecked: {
    // 設置該回調將會在偵聽開始以後被當即調用
    immediate: true,
    handler(val, oldVal) {
      // 存在舊數據,且舊數據和當前數據包含項一致,返回,不進行後續賦值操做
      if (oldVal && val.length === oldVal.length &&
        val.every(item => oldVal.indexOf(item) > -1)) return;
      const checked = [];
      const checkableDataKeys = this.checkableData.map(item => item[this.keyProp]);
      val.forEach(item => {
        if (checkableDataKeys.indexOf(item) > -1) {
          checked.push(item);
        }
      });
      this.checkChangeByUser = false;
      this.checked = checked;
    }
  }
複製代碼

Transferpanel 組件的 computed 比較簡單,主要的 filteredData 在上面已經提過,下面看他的 methodscode

methods

// 更新全選狀態
updateAllChecked() {
    // 全部可勾選數據項的 key 數組
    const checkableDataKeys = this.checkableData.map(item => item[this.keyProp]);
    // 全部可勾選的項都在已勾選數組中,則標記爲全勾選狀態
    this.allChecked = checkableDataKeys.length > 0 &&
      checkableDataKeys.every(item => this.checked.indexOf(item) > -1);
},
// 勾選全選框的回調
handleAllCheckedChange(value) {
    // 若是是選中,則將全部可勾選數據項的 key 放入 checked 數組
    // 若是是取消選中,則清空 checked 數組
    this.checked = value
      ? this.checkableData.map(item => item[this.keyProp])
      : [];
},
// 清空搜索欄
clearQuery() {
    // 若是搜索欄輸入了內容
    if (this.inputIcon === 'circle-close') {
      this.query = '';
    }
}
       
複製代碼
相關文章
相關標籤/搜索