refactor(transfer): transfer component refactoring completed

This commit is contained in:
Ryan 2020-08-30 20:33:21 +08:00 committed by jeremywu
parent 4e29aab1cf
commit 3daf1cd6ab
11 changed files with 739 additions and 0 deletions

View File

@ -30,6 +30,7 @@ import ElTabs from '@element-plus/tabs'
import ElTooltip from '@element-plus/tooltip'
import ElSlider from '@element-plus/slider'
import ElInput from '@element-plus/input'
import ElTransfer from '@element-plus/transfer'
export {
ElAlert,
@ -62,6 +63,7 @@ export {
ElTooltip,
ElSlider,
ElInput,
ElTransfer,
}
export default function install(app: App): void {
@ -96,4 +98,5 @@ export default function install(app: App): void {
ElTooltip(app)
ElSlider(app)
ElInput(app)
ElTransfer(app)
}

View File

@ -0,0 +1,5 @@
import { App } from 'vue'
import Transfer from './src/index.vue'
export default (app: App): void => {
app.component(Transfer.name, Transfer)
}

View File

@ -0,0 +1,12 @@
{
"name": "@element-plus/transfer",
"version": "0.0.0",
"main": "dist/index.js",
"license": "MIT",
"peerDependencies": {
"vue": "^3.0.0-rc.6"
},
"devDependencies": {
"@vue/test-utils": "^2.0.0-beta.0"
}
}

View File

@ -0,0 +1,235 @@
<template>
<div class="el-transfer">
<transfer-panel
ref="leftPanel"
:data="sourceData"
:render-content="renderContent"
:placeholder="panelFilterPlaceholder"
:title="leftPanelTitle"
:filterable="filterable"
:format="format"
:filter-method="filterMethod"
:default-checked="leftDefaultChecked"
:props="props"
@checked-change="onSourceCheckedChange"
>
<template #default>
<slot name="left-footer"></slot>
</template>
</transfer-panel>
<div class="el-transfer__buttons">
<el-button
type="primary"
:class="['el-transfer__button', hasButtonTexts ? 'is-with-texts' : '']"
:disabled="rightChecked.length === 0"
@click="addToLeft"
>
<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' : '']"
:disabled="leftChecked.length === 0"
@click="addToRight"
>
<span v-if="buttonTexts[1] !== undefined">{{ buttonTexts[1] }}</span>
<i class="el-icon-arrow-right"></i>
</el-button>
</div>
<transfer-panel
ref="rightPanel"
:data="targetData"
:render-content="renderContent"
:placeholder="panelFilterPlaceholder"
:filterable="filterable"
:format="format"
:filter-method="filterMethod"
:title="rightPanelTitle"
:default-checked="rightDefaultChecked"
:props="props"
@checked-change="onTargetCheckedChange"
>
<template #default>
<slot name="right-footer"></slot>
</template>
</transfer-panel>
</div>
</template>
<script lang="ts">
import { computed, defineComponent, PropType, provide, reactive, ref, toRefs, VNode, watch } from 'vue'
import { t } from '@element-plus/locale'
import ElButton from '@element-plus/button/src/button.vue'
import TransferPanel from './transfer-panel.vue'
import { useComputedData } from './useComputedData'
import { useCheckedChange } from './useCheckedChange'
import { useMove } from './useMove'
import { Key } from './transfer'
import { UPDATE_MODEL_EVENT } from '@element-plus/utils/constants'
export const CHANGE_EVENT = 'change'
export const LEFT_CHECK_CHANGE_EVENT = 'left-check-change'
export const RIGHT_CHECK_CHANGE_EVENT = 'right-check-change'
export default defineComponent({
name: 'ElTransfer',
components: {
TransferPanel,
ElButton,
},
props: {
data: {
type: Array,
default() {
return []
},
},
titles: {
type: Array,
default() {
return []
},
},
buttonTexts: {
type: Array,
default() {
return []
},
},
filterPlaceholder: {
type: String,
default: '',
},
filterMethod: {
type: Function as PropType<(query: string, item: Record<string, any>) => boolean>,
},
leftDefaultChecked: {
type: Array,
default() {
return []
},
},
rightDefaultChecked: {
type: Array,
default() {
return []
},
},
renderContent: {
type: Function as PropType<(h, option) => VNode>,
},
modelValue: {
type: Array,
default() {
return []
},
},
format: {
type: Object,
default() {
return {}
},
},
filterable: {
type: Boolean,
default: false,
},
props: {
type: Object,
default() {
return {
label: 'label',
key: 'key',
disabled: 'disabled',
}
},
},
targetOrder: {
type: String,
default: 'original',
validator: (val: string) => {
return ['original', 'push', 'unshift'].includes(val)
},
},
},
emits: [
UPDATE_MODEL_EVENT,
CHANGE_EVENT,
LEFT_CHECK_CHANGE_EVENT,
RIGHT_CHECK_CHANGE_EVENT,
],
setup(props, { emit, slots }) {
const initData = reactive({
leftChecked: [],
rightChecked: [],
})
const {
propsKey,
sourceData,
targetData,
} = useComputedData(props)
const {
onSourceCheckedChange,
onTargetCheckedChange,
} = useCheckedChange(initData, emit)
const { addToLeft,
addToRight,
} = useMove(props, initData, propsKey, emit)
const leftPanel = ref(null)
const rightPanel = ref(null)
const clearQuery = (which: 'left' | 'right') => {
if (which === 'left') {
leftPanel.value.query = ''
} else if (which === 'right') {
rightPanel.value.query = ''
}
}
const hasButtonTexts = computed(() => props.buttonTexts.length === 2)
const leftPanelTitle = computed(() => props.titles[0] || t('el.transfer.titles.0'))
const rightPanelTitle = computed(() => props.titles[1] || t('el.transfer.titles.1'))
const panelFilterPlaceholder = computed(() => props.filterPlaceholder || t('el.transfer.filterPlaceholder'))
watch(() => props.modelValue, (val: Key[]) => emit(UPDATE_MODEL_EVENT, val))
provide('defaultScopedSlots', computed(() => slots.default))
const {
leftChecked,
rightChecked,
} = toRefs(initData)
return {
sourceData,
targetData,
onSourceCheckedChange,
onTargetCheckedChange,
addToLeft,
addToRight,
leftChecked,
rightChecked,
hasButtonTexts,
leftPanelTitle,
rightPanelTitle,
panelFilterPlaceholder,
clearQuery,
}
},
})
</script>

View File

@ -0,0 +1,26 @@
<script lang="ts">
import { ComputedRef, defineComponent, h, inject, Slot } from 'vue'
export default defineComponent({
name: 'OptionContent',
props: {
option: Object,
renderContent: Function,
labelProp: String,
keyProp: String,
},
setup() {
const defaultScopedSlots: ComputedRef<undefined | Slot> = inject('defaultScopedSlots')
return {
defaultScopedSlots,
}
},
render() {
return this.renderContent
? this.renderContent(h, this.option)
: this.defaultScopedSlots
? this.defaultScopedSlots({ option: this.option })
: h('span', this.option[this.labelProp] || this.option[this.keyProp])
},
})
</script>

View File

@ -0,0 +1,177 @@
<template>
<div class="el-transfer-panel">
<p class="el-transfer-panel__header">
<el-checkbox
v-model="allChecked"
:indeterminate="isIndeterminate"
@change="handleAllCheckedChange"
>
{{ title }}
<span>{{ checkedSummary }}</span>
</el-checkbox>
</p>
<div
:class="['el-transfer-panel__body', hasFooter ? 'is-with-footer' : '']"
>
<el-input
v-if="filterable"
v-model="query"
class="el-transfer-panel__filter"
size="small"
:placeholder="placeholder"
@mouseenter="inputHover = true"
@mouseleave="inputHover = false"
>
<template #prefix>
<i :class="['el-input__icon', 'el-icon-' + inputIcon]" @click="clearQuery"></i>
</template>
</el-input>
<el-checkbox-group
v-show="!hasNoMatch && data.length > 0"
v-model="checked"
:class="{ 'is-filterable': filterable }"
class="el-transfer-panel__list"
>
<el-checkbox
v-for="item in filteredData"
:key="item[keyProp]"
class="el-transfer-panel__item"
:label="item[keyProp]"
:disabled="item[disabledProp]"
>
<option-content
:option="item"
:render-content="renderContent"
:label-prop="labelProp"
:key-prop="keyProp"
/>
</el-checkbox>
</el-checkbox-group>
<p v-show="hasNoMatch" class="el-transfer-panel__empty">
{{ t('el.transfer.noMatch') }}
</p>
<p
v-show="data.length === 0 && !hasNoMatch"
class="el-transfer-panel__empty"
>
{{ t('el.transfer.noData') }}
</p>
</div>
<p v-if="hasFooter" class="el-transfer-panel__footer">
<slot></slot>
</p>
</div>
</template>
<script lang="ts">
import { computed, defineComponent, reactive, toRefs } from 'vue'
import { t } from '@element-plus/locale'
import ElCheckboxGroup from '@element-plus/checkbox/src/checkbox-group.vue'
import ElCheckbox from '@element-plus/checkbox/src/checkbox.vue'
import ElInput from '@element-plus/input/src/index.vue'
import OptionContent from './option-content.vue'
import { useCheck } from './useCheck'
export const CHECKED_CHANGE_EVENT = 'checked-change'
export default defineComponent({
name: 'ElTransferPanel',
components: {
ElCheckboxGroup,
ElCheckbox,
ElInput,
OptionContent,
},
props: {
data: {
type: Array,
default() {
return []
},
},
renderContent: Function,
placeholder: String,
title: String,
filterable: Boolean,
format: Object,
filterMethod: Function,
defaultChecked: Array,
props: Object,
},
emits: [CHECKED_CHANGE_EVENT],
setup(props, { emit, slots }) {
const initData = reactive({
checked: [],
allChecked: false,
query: '',
inputHover: false,
checkChangeByUser: true,
})
const {
labelProp,
keyProp,
disabledProp,
filteredData,
checkedSummary,
isIndeterminate,
handleAllCheckedChange,
} = useCheck(props, initData, emit)
const hasNoMatch = computed(() => initData.query.length > 0 && filteredData.value.length === 0)
const inputIcon = computed(() => {
return initData.query.length > 0 && initData.inputHover
? 'circle-close'
: 'search'
})
const hasFooter = computed(() => !!slots.default()[0].children.length)
const clearQuery = () => {
if (inputIcon.value === 'circle-close') {
initData.query = ''
}
}
const {
checked,
allChecked,
query,
inputHover,
checkChangeByUser,
} = toRefs(initData)
return {
labelProp,
keyProp,
disabledProp,
filteredData,
checkedSummary,
isIndeterminate,
handleAllCheckedChange,
checked,
allChecked,
query,
inputHover,
checkChangeByUser,
hasNoMatch,
inputIcon,
hasFooter,
clearQuery,
t,
}
},
})
</script>

61
packages/transfer/src/transfer.d.ts vendored Normal file
View File

@ -0,0 +1,61 @@
import { VNode } from 'vue'
export declare type Key = string | number
export declare type DataItem = {
key: Key
label: string
disabled: boolean
}
export declare type Format = {
noChecked: string
hasChecked: string
}
export declare type Props = {
label: string
key: string
disabled: string
}
export declare interface TransferProps {
data: DataItem[]
titles: [string, string]
buttonTexts: [string, string]
filterPlaceholder: string
filterMethod: (query: string, item: DataItem) => boolean
leftDefaultChecked: Key[]
rightDefaultChecked: Key[]
renderContent: (h, option) => VNode
modelValue: Key[]
format: Format
filterable: boolean
props: Props
targetOrder: 'original' | 'push' | 'unshift'
}
export declare interface TransferInitData {
leftChecked: Key[]
rightChecked: Key[]
}
export declare interface TransferPanelProps {
data: DataItem[]
renderContent: (h, option) => VNode
placeholder: string
title: string
filterable: boolean
format: Format
filterMethod: (query: string, item: DataItem) => boolean
defaultChecked: Key[]
props: Props
}
export declare interface TransferPanelInitData {
checked: Key[]
allChecked: boolean
query: string
inputHover: boolean
checkChangeByUser: boolean
}

View File

@ -0,0 +1,119 @@
import { computed, watch } from 'vue'
import { TransferPanelProps, TransferPanelInitData, Key } from './transfer'
import { CHECKED_CHANGE_EVENT } from './transfer-panel.vue'
export const useCheck = (props: TransferPanelProps, initData: TransferPanelInitData, emit) => {
const labelProp = computed(() => props.props.label || 'label')
const keyProp = computed(() => props.props.key || 'key')
const disabledProp = computed(() => props.props.disabled || 'disabled')
const filteredData = computed(() => {
return props.data.filter(item => {
if (typeof props.filterMethod === 'function') {
return props.filterMethod(initData.query, item)
} else {
const label = item[labelProp.value] || item[keyProp.value].toString()
return label.toLowerCase().includes(initData.query.toLowerCase())
}
})
})
const checkableData = computed(() => filteredData.value.filter(item => !item[disabledProp.value]))
const checkedSummary = computed(() => {
const checkedLength = initData.checked.length
const dataLength = props.data.length
const { noChecked, hasChecked } = props.format
if (noChecked && hasChecked) {
return checkedLength > 0
? hasChecked
.replace(/\${checked}/g, checkedLength.toString())
.replace(/\${total}/g, dataLength.toString())
: noChecked.replace(/\${total}/g, dataLength.toString())
} else {
return `${ checkedLength }/${ dataLength }`
}
})
const isIndeterminate = computed(() => {
const checkedLength = initData.checked.length
return checkedLength > 0 && checkedLength < checkableData.value.length
})
const updateAllChecked = () => {
const checkableDataKeys = checkableData.value.map(item => item[keyProp.value])
initData.allChecked = checkableDataKeys.length > 0 && checkableDataKeys.every(item => initData.checked.includes(item))
}
const handleAllCheckedChange = (value: Key[]) => {
initData.checked = value ? checkableData.value.map(item => item[keyProp.value]) : []
}
watch(() => initData.checked, (val, oldVal) => {
updateAllChecked()
if (initData.checkChangeByUser) {
const movedKeys = val
.concat(oldVal)
.filter(v => !val.includes(v) || !oldVal.includes(v))
emit(CHECKED_CHANGE_EVENT, val, movedKeys)
} else {
emit(CHECKED_CHANGE_EVENT, val)
initData.checkChangeByUser = true
}
})
watch(checkableData, () => {
updateAllChecked()
})
watch(() => props.data, () => {
const checked = []
const filteredDataKeys = filteredData.value.map(item => item[keyProp.value])
initData.checked.forEach(item => {
if (filteredDataKeys.includes(item)) {
checked.push(item)
}
})
initData.checkChangeByUser = false
initData.checked = checked
})
watch(() => props.defaultChecked, (val, oldVal) => {
if (oldVal && val.length === oldVal.length && val.every(item => oldVal.includes(item))) return
const checked = []
const checkableDataKeys = checkableData.value.map(item => item[keyProp.value])
val.forEach(item => {
if (checkableDataKeys.includes(item)) {
checked.push(item)
}
})
initData.checkChangeByUser = false
initData.checked = checked
}, {
immediate: true,
})
return {
labelProp,
keyProp,
disabledProp,
filteredData,
checkableData,
checkedSummary,
isIndeterminate,
updateAllChecked,
handleAllCheckedChange,
}
}

View File

@ -0,0 +1,22 @@
import { TransferInitData, Key } from './transfer'
import { LEFT_CHECK_CHANGE_EVENT, RIGHT_CHECK_CHANGE_EVENT } from './index.vue'
export const useCheckedChange = (initData: TransferInitData, emit) => {
const onSourceCheckedChange = (val: Key[], movedKeys: Key[]) => {
initData.leftChecked = val
if (movedKeys === undefined) return
emit(LEFT_CHECK_CHANGE_EVENT, val, movedKeys)
}
const onTargetCheckedChange = (val: Key[], movedKeys: Key[]) => {
initData.rightChecked = val
if (movedKeys === undefined) return
emit(RIGHT_CHECK_CHANGE_EVENT, val, movedKeys)
}
return {
onSourceCheckedChange,
onTargetCheckedChange,
}
}

View File

@ -0,0 +1,34 @@
import { computed } from 'vue'
import { TransferProps } from './transfer'
export const useComputedData = (props: TransferProps) => {
const propsKey = computed(() => props.props.key)
const dataObj = computed(() => {
return props.data.reduce((o, cur) => (o[cur[propsKey.value]] = cur) && o, {})
})
const sourceData = computed(() => {
return props.data.filter(item => !props.modelValue.includes(item[propsKey.value]))
})
const targetData = computed(() => {
if (props.targetOrder === 'original') {
return props.data.filter(item => props.modelValue.includes(item[propsKey.value]))
} else {
return props.modelValue.reduce((arr, cur) => {
const val = dataObj.value[cur]
if (val) {
arr.push(val)
}
return arr
}, [])
}
})
return {
propsKey,
sourceData,
targetData,
}
}

View File

@ -0,0 +1,45 @@
import { ComputedRef } from 'vue'
import { UPDATE_MODEL_EVENT } from '../../utils/constants'
import { CHANGE_EVENT } from './index.vue'
import { TransferProps, TransferInitData, DataItem, Key } from './transfer'
export const useMove = (props: TransferProps, initData: TransferInitData, propsKey: ComputedRef<string>, emit) => {
const _emit = (value, type: 'left' | 'right', checked: Key[]) => {
emit(UPDATE_MODEL_EVENT, value)
emit(CHANGE_EVENT, value, type, checked)
}
const addToLeft = () => {
const currentValue = props.modelValue.slice()
initData.rightChecked.forEach(item => {
const index = currentValue.indexOf(item)
if (index > -1) {
currentValue.splice(index, 1)
}
})
_emit(currentValue, 'left', initData.rightChecked)
}
const addToRight = () => {
let currentValue = props.modelValue.slice()
const itemsToBeMoved = props.data
.filter((item: DataItem) => {
const itemKey = item[propsKey.value]
return initData.leftChecked.includes(itemKey) && !props.modelValue.includes(itemKey)
})
.map(item => item[propsKey.value])
currentValue = props.targetOrder === 'unshift'
? itemsToBeMoved.concat(currentValue)
: currentValue.concat(itemsToBeMoved)
_emit(currentValue, 'right', initData.leftChecked)
}
return {
addToLeft,
addToRight,
}
}