【element3-开发日记】通过TDD方式重写Transfer组件

505 阅读3分钟

Tasking

  • 阅读Vue 组合式 API文档

  • 阅读【element3-开发日记】手摸手教你重写 Button 组件

  • 穿梭框

    • Attributes:
    • 通过 <el-transfer v-model="value" :data="data"></el-transfer> 定义一个默认的 Transfer 组件
      • 只有data情况的渲染
      • data和v-model都有的渲染
      • 点击左右按钮列表内容变化
    • 通过 props.targetOrder 控制 Transfer 组件右侧列表元素的排序策略:original(与数据源相同的顺序)/push(新加入的元素排在最后)/unshift (新加入的元素排在最前)
    • 通过 props.filterable 控制 Transfer 组件开启搜索模式
    • 通过 props.filterPlaceHolder 控制 Transfer 组件搜索框占位符
    • 通过 props.filterMethod 控制 Transfer 组件自定义搜索
    • 通过 props.titles 控制 Transfer 组件自定义标题
    • 通过 props.buttonTexts 控制 Transfer 组件按钮自定义文案
    • 通过 props.renderContent 控制 Transfer 组件自定义数据项渲染函数
    • 通过 props.format 控制 Transfer 组件列表顶部勾选状态文案 { noChecked: 'checked/{checked}/{total}', hasChecked: 'checked/{checked}/{total}' }
    • 通过 props.props 控制 Transfer 组件数据源的字段别名
    • 通过 props.leftDefaultChecked 控制 Transfer 组件初始状态下左侧列表的已勾选项的 key 数组
    • 通过 props.rightDefaultChecked 控制 Transfer 组件初始状态下右侧列表的已勾选项的 key 数组
    • slot:
    • left-footer 左侧列表底部内容
    • right-footer右侧列表底部内容
    • Scoped Slot:
    • 自定义数据项的内容,参数为 { option }
    • Methods:
    • clearQuery 清空某个面板的搜索关键词 参数:'left' / 'right',指定需要清空的面板
    • Events:
    • change 右侧列表元素变化时触发 回调参数:当前值、数据移动的方向('left' / 'right')、发生移动的数据 key 数组
    • left-check-change 左侧列表元素被用户选中 / 取消选中时触发 回调参数:当前被选中的元素的 key 数组、选中状态发生变化的元素的 key 数组
    • right-check-change 右侧列表元素被用户选中 / 取消选中时触发 回调参数:当前被选中的元素的 key 数组、选中状态发生变化的元素的 key 数组

单元测试+代码

测试准备

import ElTransfer from '../src1/Transfer.vue'
import { mount } from '@vue/test-utils'
import { toRefs, reactive, nextTick, ref } from 'vue'
const getTestData = () => {
  const data = []
  for (let i = 1; i <= 15; i++) {
    data.push({
      key: i,
      label: `备选项 ${i}`,
      disabled: i % 4 === 0
    })
  }
  return data
}
const createTransfer = (props, opts) => {
  return Object.assign({
    template: `
      <el-transfer :data="testData" ref="transfer" ${props}>
      </el-transfer>
    `,
    components: {
      'el-transfer': ElTransfer
    },
    data() {
      return {
        testData: getTestData()
      }
    }
  }, opts)
}

通过 <el-transfer v-model="value" :data="data"></el-transfer> 定义一个默认的 Transfer 组件

只有data情况的渲染

测试

describe('Transfer', () => {
  it('render data', () => {
    const Comp = createTransfer('', {})
    const wrapper = mount(Comp)

    const transferLeft = wrapper.get('[data-test="transfer-panel-left"]')

    expect(transferLeft.findAll('.el-transfer-panel__item')).toHaveLength(15)
  })
})

代码

<template>
    <div class="el-transfer">
      <div class="el-transfer-panel" data-test="transfer-panel-left">
          <el-checkbox-group>
              <el-checkbox
                  class="el-transfer-panel__item"
                  :label="item.key"
                  :key="item.key"
                  v-for="item in data"
              >
              </el-checkbox>
          </el-checkbox-group>
      </div>
    </div>
</template>
<script>
import ElCheckboxGroup from '../../checkbox-group'
import ElCheckbox from '../../checkbox'
export default {
    name: 'ElTransfer',
    components: {
        TransferPanel
    },
    props: {
        data: {
            type: Array,
            default() {
                return []
            }
        },
        modelValue: {
            type: Array,
            default() {
                return []
            }
        }
    }
}
</script>

data和v-model都有的渲染

测试

describe('Transfer', () => {
  it('default target list', () => {
    const Comp = createTransfer('v-model="value"', {
      setup() {
        const state = reactive({
          value: [1, 4]
        })

        return toRefs(state)
      }
    })
    const wrapper = mount(Comp)

    const transferLeft = wrapper.get('[data-test="transfer-panel-left"]')
    const transferRight = wrapper.get('[data-test="transfer-panel-right"]')

    expect(transferLeft.findAll('.el-transfer-panel__item')).toHaveLength(13)
    expect(transferRight.findAll('.el-transfer-panel__item')).toHaveLength(2)
  })
})

代码

<template>
    <div class="el-transfer">
      <div class="el-transfer-panel" data-test="transfer-panel-left">
          <el-checkbox-group>
              <el-checkbox
                  class="el-transfer-panel__item"
                  :label="item.key"
                  :key="item.key"
                  v-for="item in sourceData"
              >
              </el-checkbox>
          </el-checkbox-group>
      </div>
      <div class="el-transfer-panel" data-test="transfer-panel-right">
          <el-checkbox-group>
              <el-checkbox
                  class="el-transfer-panel__item"
                  :label="item.key"
                  :key="item.key"
                  v-for="item in targetData"
              >
              </el-checkbox>
          </el-checkbox-group>
      </div>
</template>
<script>
import TransferPanel from './TransferPanel.vue'
export default {
    name: 'ElTransfer',
    components: {
        TransferPanel
    },
    props: {
        data: {
            type: Array,
            default() {
                return []
            }
        },
        modelValue: {
            type: Array,
            default() {
                return []
            }
        }
    },
    setup(props, { emit, slots }) {

        const { sourceData, targetData } = useTransferData(props)

        return {
            sourceData,
            targetData
        }
    }
}

const useTransferData = (props) => {
    let sourceData = []
    let targetData = []
    let count = 0
    const { data, modelValue } = props
    const len = modelValue.length

    if (modelValue.length > 0) {
        for (let i = 0; i < data.length; i++) {
            const item = data[i]
            if (count < len && modelValue.indexOf(item.key) > -1) {
                targetData.push(item)
                count++
                continue
            }

            sourceData.push(item)
        }
    } else {
        sourceData = data
    }

    return {
        sourceData,
        targetData
    }
}
</script>

点击左右按钮列表内容变化

测试

describe('Transfer', () => {
 it('transfer', async () => {
    const Comp = createTransfer('v-model="value"', {
      setup() {
        const state = reactive({
          value: [1, 4]
        })

        return toRefs(state)
      }
    })
    const wrapper = mount(Comp)

    const transferLeft = wrapper.get('[data-test="transfer-panel-left"]')
    const transferRight = wrapper.get('[data-test="transfer-panel-right"]')
    const buttonLeft = wrapper.get('[data-test="transfer__button-left"]')
    const buttonRight = wrapper.get('[data-test="transfer__button-right"]')
	
    // 初始左13 右2
    expect(transferLeft.findAll('.el-transfer-panel__item')).toHaveLength(13)
    expect(transferRight.findAll('.el-transfer-panel__item')).toHaveLength(2)
    
    // 左1选中,点击左按钮 左12 右3
    transferLeft.findAll('.el-transfer-panel__item')[0].trigger('click')
    await nextTick()
    buttonRight.trigger('click')
    await nextTick()
    expect(transferLeft.findAll('.el-transfer-panel__item')).toHaveLength(12)
    expect(transferRight.findAll('.el-transfer-panel__item')).toHaveLength(3)

    // 右1 右2选中,点击右按钮 左14 右1
    transferRight.findAll('.el-transfer-panel__item')[0].trigger('click')
    transferRight.findAll('.el-transfer-panel__item')[1].trigger('click')
    await nextTick()
    buttonLeft.trigger('click')
    await nextTick()
    expect(transferLeft.findAll('.el-transfer-panel__item')).toHaveLength(14)
    expect(transferRight.findAll('.el-transfer-panel__item')).toHaveLength(1)
  })
})

代码

这个阶段我代码中抽离了新的组件

TanrsferPanel.vue

<template>
    <div class="el-transfer-panel">
        <el-checkbox-group v-model="checked" @change="checkedChangeHandler">
            <el-checkbox
                class="el-transfer-panel__item"
                :label="item.key"
                :key="item.key"
                v-for="item in data"
            >
            </el-checkbox>
        </el-checkbox-group>
    </div>
</template>
<script>
import ElCheckboxGroup from '../../checkbox-group'
import ElCheckbox from '../../checkbox'
import { reactive, toRefs } from 'vue'
export default {
    name: 'ElTransferPanel',
    emits: ['checked-change'],
    components: {
        ElCheckboxGroup,
        ElCheckbox
    },
    props: {
        data: {
            type: Array,
            default() {
                return []
            }
        }
    },
    setup(props, { emit, slots }) {
        const state = reactive({
            checked: []
        })

        const checkedChangeHandler = (val) => {
            emit('checked-change', val)
        }

        return {
            ...toRefs(state),
            checkedChangeHandler
        }
    }
}
</script>

Transfer.vue

<template>
    <div class="el-transfer">
        <transfer-panel
            :data="sourceData"
            @checked-change="onSourceCheckedChange"
            data-test="transfer-panel-left"
        >
        </transfer-panel>
        <div class="el-transfer__buttons">
            <button
                class="el-transfer__button"
                @click="addToLeft"
                data-test="transfer__button-left"
            >
            </button>
            <button
                class="el-transfer__button"
                @click="addToRight"
                data-test="transfer__button-right"
            >
            </button>
        </div>
        <transfer-panel
            :data="targetData"
            @checked-change="onTargetCheckedChange"
            data-test="transfer-panel-right"
        >
        </transfer-panel>
    </div>
</template>
<script>
import { reactive, toRefs, computed } from 'vue'
import TransferPanel from './TransferPanel.vue'
import { props } from '../../../src/components/Avatar/src/props'
export default {
    name: 'ElTransfer',

    emits: [
        'update:modelValue'
    ],
    components: {
        TransferPanel
    },
    props: {
        data: {
            type: Array,
            default() {
                return []
            }
        },
        modelValue: {
            type: Array,
            default() {
                return []
            }
        }
    },
    setup(props, { emit, slots }) {
        const {
            sourceData,
            targetData
        } = useTransferData(props)

        const {
            onSourceCheckedChange,
            onTargetCheckedChange,
            addToLeft,
            addToRight
        } = useTransfercheckedChange(props, emit)


        return {
            sourceData,
            targetData,
            onSourceCheckedChange,
            onTargetCheckedChange,
            addToLeft,
            addToRight
        }
    }
}

const useTransferData = (props) => {

    const sourceData = computed(() => {
        const { data, modelValue } = props

        if (modelValue.length === 0) {
            return data.slice()
        }

        return data.filter(item => modelValue.indexOf(item.key) === -1)
    })

    const targetData = computed(() => {
        const { data, modelValue } = props

        if (modelValue.length === 0) {
            return []
        }

        return data.filter(item => modelValue.indexOf(item.key) > -1)
    })

    return {
        sourceData,
        targetData
    }
}

const useTransfercheckedChange = (props, emit) => {
    let sourceChecked = []
    let targetChecked = []

    const onSourceCheckedChange = (val) => {
        sourceChecked = val
    }

    const onTargetCheckedChange = (val) => {
        targetChecked = val
    }

    const addToLeft = () => {
        if (targetChecked.length === 0) {
            return
        }
        const { data, modelValue } = props
        const currentValue = modelValue.slice()

        for (let i = 0; i < targetChecked.length; i++) {
            if (currentValue.indexOf(targetChecked[i]) > -1) {
                currentValue.splice(i, 1)
            }
        }

        emit('update:modelValue', currentValue)
    }

    const addToRight = () => {
        if (sourceChecked.length === 0) {
            return
        }
        const { data, modelValue } = props
        const arr = Array.from(new Set([].concat(modelValue, sourceChecked)))
        emit('update:modelValue', arr)
    }

    return {
        onSourceCheckedChange,
        onTargetCheckedChange,
        addToLeft,
        addToRight
    }
}
</script>