refactor(components): [transfer] switch to script-setup syntax (#8343)

* refactor(components): [transfer] switch to script-setup syntax

* chore: improve code

* chore: improve type

* fix: lost reactivity

* chore: fix data error

* chore: simplify code
This commit is contained in:
zz 2022-06-27 22:08:03 +08:00 committed by GitHub
parent 598f3ab5cd
commit a894fbf990
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
16 changed files with 503 additions and 524 deletions

View File

@ -309,7 +309,7 @@ const useEvent = (
}
}
type CheckboxValueType = string | number | boolean
export type CheckboxValueType = string | number | boolean
export const checkboxEmits = {
[UPDATE_MODEL_EVENT]: (val: CheckboxValueType) =>

View File

@ -2,7 +2,7 @@
import { nextTick } from 'vue'
import { mount } from '@vue/test-utils'
import { describe, expect, it } from 'vitest'
import Transfer from '../src/index.vue'
import Transfer from '../src/transfer.vue'
describe('Transfer', () => {
const getTestData = () => {

View File

@ -1,15 +1,8 @@
import Transfer from './src/index.vue'
import { withInstall } from '@element-plus/utils'
import type { App } from 'vue'
import type { SFCWithInstall } from '@element-plus/utils'
import Transfer from './src/transfer.vue'
Transfer.install = (app: App): void => {
app.component(Transfer.name, Transfer)
}
const _Transfer = Transfer as SFCWithInstall<typeof Transfer>
export default _Transfer
export const ElTransfer = _Transfer
export const ElTransfer = withInstall(Transfer)
export default ElTransfer
export * from './src/transfer'

View File

@ -0,0 +1,5 @@
export * from './use-check'
export * from './use-checked-change'
export * from './use-computed-data'
export * from './use-move'
export * from './use-props-alias'

View File

@ -1,54 +1,40 @@
// @ts-nocheck
import { computed, getCurrentInstance, watch } from 'vue'
import { computed, watch } from 'vue'
import { isFunction } from '@element-plus/utils'
import { CHECKED_CHANGE_EVENT } from '../transfer-panel'
import { usePropsAlias } from './use-props-alias'
import type { ExtractPropTypes } from 'vue'
import type { Key, TransferPanelState } from './transfer'
export const CHECKED_CHANGE_EVENT = 'checked-change'
export const useCheckProps = {
data: {
type: Array,
default() {
return []
},
},
optionRender: Function,
placeholder: String,
title: String,
filterable: Boolean,
format: Object,
filterMethod: Function,
defaultChecked: Array,
props: Object,
}
import type { SetupContext } from 'vue'
import type { CheckboxValueType } from '@element-plus/components/checkbox'
import type { TransferKey } from '../transfer'
import type {
TransferPanelEmits,
TransferPanelProps,
TransferPanelState,
} from '../transfer-panel'
export const useCheck = (
props: ExtractPropTypes<typeof useCheckProps>,
panelState: TransferPanelState
props: TransferPanelProps,
panelState: TransferPanelState,
emit: SetupContext<TransferPanelEmits>['emit']
) => {
const { emit } = getCurrentInstance()
const labelProp = computed(() => props.props.label || 'label')
const keyProp = computed(() => props.props.key || 'key')
const disabledProp = computed(() => props.props.disabled || 'disabled')
const propsAlias = usePropsAlias(props)
const filteredData = computed(() => {
return props.data.filter((item) => {
if (typeof props.filterMethod === 'function') {
if (isFunction(props.filterMethod)) {
return props.filterMethod(panelState.query, item)
} else {
const label = item[labelProp.value] || item[keyProp.value].toString()
const label = String(
item[propsAlias.value.label] || item[propsAlias.value.key]
)
return label.toLowerCase().includes(panelState.query.toLowerCase())
}
})
})
const checkableData = computed(() => {
return filteredData.value.filter((item) => !item[disabledProp.value])
})
const checkableData = computed(() =>
filteredData.value.filter((item) => !item[propsAlias.value.disabled])
)
const checkedSummary = computed(() => {
const checkedLength = panelState.checked.length
@ -73,16 +59,16 @@ export const useCheck = (
const updateAllChecked = () => {
const checkableDataKeys = checkableData.value.map(
(item) => item[keyProp.value]
(item) => item[propsAlias.value.key]
)
panelState.allChecked =
checkableDataKeys.length > 0 &&
checkableDataKeys.every((item) => panelState.checked.includes(item))
}
const handleAllCheckedChange = (value: Key[]) => {
const handleAllCheckedChange = (value: CheckboxValueType) => {
panelState.checked = value
? checkableData.value.map((item) => item[keyProp.value])
? checkableData.value.map((item) => item[propsAlias.value.key])
: []
}
@ -110,9 +96,9 @@ export const useCheck = (
watch(
() => props.data,
() => {
const checked = []
const checked: TransferKey[] = []
const filteredDataKeys = filteredData.value.map(
(item) => item[keyProp.value]
(item) => item[propsAlias.value.key]
)
panelState.checked.forEach((item) => {
if (filteredDataKeys.includes(item)) {
@ -134,9 +120,9 @@ export const useCheck = (
)
return
const checked = []
const checked: TransferKey[] = []
const checkableDataKeys = checkableData.value.map(
(item) => item[keyProp.value]
(item) => item[propsAlias.value.key]
)
val.forEach((item) => {
@ -153,9 +139,6 @@ export const useCheck = (
)
return {
labelProp,
keyProp,
disabledProp,
filteredData,
checkableData,
checkedSummary,

View File

@ -0,0 +1,36 @@
import { LEFT_CHECK_CHANGE_EVENT, RIGHT_CHECK_CHANGE_EVENT } from '../transfer'
import type { SetupContext } from 'vue'
import type {
TransferCheckedState,
TransferEmits,
TransferKey,
} from '../transfer'
export const useCheckedChange = (
checkedState: TransferCheckedState,
emit: SetupContext<TransferEmits>['emit']
) => {
const onSourceCheckedChange = (
val: TransferKey[],
movedKeys?: TransferKey[]
) => {
checkedState.leftChecked = val
if (!movedKeys) return
emit(LEFT_CHECK_CHANGE_EVENT, val, movedKeys)
}
const onTargetCheckedChange = (
val: TransferKey[],
movedKeys?: TransferKey[]
) => {
checkedState.rightChecked = val
if (!movedKeys) return
emit(RIGHT_CHECK_CHANGE_EVENT, val, movedKeys)
}
return {
onSourceCheckedChange,
onTargetCheckedChange,
}
}

View File

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

View File

@ -1,23 +1,30 @@
// @ts-nocheck
import { CHANGE_EVENT, UPDATE_MODEL_EVENT } from '@element-plus/constants'
import { usePropsAlias } from './use-props-alias'
import type { ComputedRef } from 'vue'
import type { SetupContext } from 'vue'
import type {
DataItem,
Key,
TransferCheckedState,
TransferDataItem,
TransferDirection,
TransferEmits,
TransferKey,
TransferProps,
} from './transfer'
} from '../transfer'
export const useMove = (
props: TransferProps,
checkedState: TransferCheckedState,
propsKey: ComputedRef<string>,
emit
emit: SetupContext<TransferEmits>['emit']
) => {
const _emit = (value, type: 'left' | 'right', checked: Key[]) => {
const propsAlias = usePropsAlias(props)
const _emit = (
value: TransferKey[],
direction: TransferDirection,
movedKeys: TransferKey[]
) => {
emit(UPDATE_MODEL_EVENT, value)
emit(CHANGE_EVENT, value, type, checked)
emit(CHANGE_EVENT, value, direction, movedKeys)
}
const addToLeft = () => {
@ -36,14 +43,14 @@ export const useMove = (
let currentValue = props.modelValue.slice()
const itemsToBeMoved = props.data
.filter((item: DataItem) => {
const itemKey = item[propsKey.value]
.filter((item: TransferDataItem) => {
const itemKey = item[propsAlias.value.key]
return (
checkedState.leftChecked.includes(itemKey) &&
!props.modelValue.includes(itemKey)
)
})
.map((item) => item[propsKey.value])
.map((item) => item[propsAlias.value.key])
currentValue =
props.targetOrder === 'unshift'
@ -52,8 +59,8 @@ export const useMove = (
if (props.targetOrder === 'original') {
currentValue = props.data
.filter((item) => currentValue.includes(item[propsKey.value]))
.map((item) => item[propsKey.value])
.filter((item) => currentValue.includes(item[propsAlias.value.key]))
.map((item) => item[propsAlias.value.key])
}
_emit(currentValue, 'right', checkedState.leftChecked)

View File

@ -0,0 +1,16 @@
import { computed } from 'vue'
import type { TransferPropsAlias } from '../transfer'
export const usePropsAlias = (props: { props: TransferPropsAlias }) => {
const initProps: Required<TransferPropsAlias> = {
label: 'label',
key: 'key',
disabled: 'disabled',
}
return computed(() => ({
...initProps,
...props.props,
}))
}

View File

@ -1,257 +0,0 @@
<template>
<div :class="ns.b()">
<transfer-panel
ref="leftPanel"
:data="sourceData"
:option-render="optionRender"
:placeholder="panelFilterPlaceholder"
:title="leftPanelTitle"
:filterable="filterable"
:format="format"
:filter-method="filterMethod"
:default-checked="leftDefaultChecked"
:props="props"
@checked-change="onSourceCheckedChange"
>
<slot name="left-footer" />
</transfer-panel>
<div :class="ns.e('buttons')">
<el-button
type="primary"
:class="[ns.e('button'), ns.is('with-texts', hasButtonTexts)]"
:disabled="rightChecked.length === 0"
@click="addToLeft"
>
<el-icon><arrow-left /></el-icon>
<span v-if="buttonTexts[0] !== undefined">{{ buttonTexts[0] }}</span>
</el-button>
<el-button
type="primary"
:class="[ns.e('button'), ns.is('with-texts', hasButtonTexts)]"
:disabled="leftChecked.length === 0"
@click="addToRight"
>
<span v-if="buttonTexts[1] !== undefined">{{ buttonTexts[1] }}</span>
<el-icon><arrow-right /></el-icon>
</el-button>
</div>
<transfer-panel
ref="rightPanel"
:data="targetData"
:option-render="optionRender"
:placeholder="panelFilterPlaceholder"
:filterable="filterable"
:format="format"
:filter-method="filterMethod"
:title="rightPanelTitle"
:default-checked="rightDefaultChecked"
:props="props"
@checked-change="onTargetCheckedChange"
>
<slot name="right-footer" />
</transfer-panel>
</div>
</template>
<script lang="ts">
// @ts-nocheck
import {
computed,
defineComponent,
h,
inject,
reactive,
ref,
toRefs,
watch,
} from 'vue'
import ElButton from '@element-plus/components/button'
import ElIcon from '@element-plus/components/icon'
import { UPDATE_MODEL_EVENT } from '@element-plus/constants'
import { useLocale, useNamespace } from '@element-plus/hooks'
import { formItemContextKey } from '@element-plus/tokens'
import { ArrowLeft, ArrowRight } from '@element-plus/icons-vue'
import { debugWarn } from '@element-plus/utils'
import TransferPanel from './transfer-panel.vue'
import { useComputedData } from './useComputedData'
import {
LEFT_CHECK_CHANGE_EVENT,
RIGHT_CHECK_CHANGE_EVENT,
useCheckedChange,
} from './useCheckedChange'
import { useMove } from './useMove'
import { CHANGE_EVENT } from './transfer'
import type { PropType, VNode } from 'vue'
import type { FormItemContext } from '@element-plus/tokens'
import type { DataItem, Format, Key, Props, TargetOrder } from './transfer'
type TransferType = InstanceType<typeof TransferPanel>
export default defineComponent({
name: 'ElTransfer',
components: {
TransferPanel,
ElButton,
ElIcon,
ArrowLeft,
ArrowRight,
},
props: {
data: {
type: Array as PropType<DataItem[]>,
default: () => [],
},
titles: {
type: Array as PropType<any> as PropType<[string, string]>,
default: () => [],
},
buttonTexts: {
type: Array as PropType<any> as PropType<[string, string]>,
default: () => [],
},
filterPlaceholder: {
type: String,
default: '',
},
filterMethod: Function as PropType<
(query: string, item: DataItem) => boolean
>,
leftDefaultChecked: {
type: Array as PropType<Key[]>,
default: () => [],
},
rightDefaultChecked: {
type: Array as PropType<Key[]>,
default: () => [],
},
renderContent: Function as PropType<(h, option) => VNode>,
modelValue: {
type: Array as PropType<Key[]>,
default: () => [],
},
format: {
type: Object as PropType<Format>,
default: () => ({}),
},
filterable: {
type: Boolean,
default: false,
},
props: {
type: Object as PropType<Props>,
default: () => ({
label: 'label',
key: 'key',
disabled: 'disabled',
}),
},
targetOrder: {
type: String as PropType<TargetOrder>,
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 { t } = useLocale()
const ns = useNamespace('transfer')
const elFormItem = inject(formItemContextKey, {} as FormItemContext)
const checkedState = reactive({
leftChecked: [],
rightChecked: [],
})
const { propsKey, sourceData, targetData } = useComputedData(props)
const { onSourceCheckedChange, onTargetCheckedChange } = useCheckedChange(
checkedState,
emit
)
const { addToLeft, addToRight } = useMove(
props,
checkedState,
propsKey,
emit
)
const leftPanel = ref<TransferType>()
const rightPanel = ref<TransferType>()
const clearQuery = (which: 'left' | 'right') => {
switch (which) {
case 'left':
leftPanel.value!.query = ''
break
case 'right':
rightPanel.value!.query = ''
break
}
}
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,
() => {
elFormItem.validate?.('change').catch((err) => debugWarn(err))
}
)
const optionRender = computed(() => (option) => {
if (props.renderContent) return props.renderContent(h, option)
if (slots.default) return slots.default({ option })
return h('span', option[props.props.label] || option[props.props.key])
})
return {
ns,
sourceData,
targetData,
onSourceCheckedChange,
onTargetCheckedChange,
addToLeft,
addToRight,
...toRefs(checkedState),
hasButtonTexts,
leftPanelTitle,
rightPanelTitle,
panelFilterPlaceholder,
clearQuery,
leftPanel,
rightPanel,
optionRender,
}
},
})
</script>

View File

@ -0,0 +1,40 @@
import { buildProps, definePropType } from '@element-plus/utils'
import { transferCheckedChangeFn, transferProps } from './transfer'
import type { ExtractPropTypes, VNode } from 'vue'
import type { TransferDataItem, TransferKey } from './transfer'
import type TransferPanel from './transfer-panel.vue'
export interface TransferPanelState {
checked: TransferKey[]
allChecked: boolean
query: string
inputHover: boolean
checkChangeByUser: boolean
}
export const CHECKED_CHANGE_EVENT = 'checked-change'
export const transferPanelProps = buildProps({
data: transferProps.data,
optionRender: {
type: definePropType<(option: TransferDataItem) => VNode | VNode[]>(
Function
),
},
placeholder: String,
title: String,
filterable: Boolean,
format: transferProps.format,
filterMethod: transferProps.filterMethod,
defaultChecked: transferProps.leftDefaultChecked,
props: transferProps.props,
} as const)
export type TransferPanelProps = ExtractPropTypes<typeof transferPanelProps>
export const transferPanelEmits = {
[CHECKED_CHANGE_EVENT]: transferCheckedChangeFn,
}
export type TransferPanelEmits = typeof transferPanelEmits
export type TransferPanelInstance = InstanceType<typeof TransferPanel>

View File

@ -18,30 +18,27 @@
:class="ns.be('panel', 'filter')"
size="default"
:placeholder="placeholder"
:prefix-icon="SearchIcon"
:prefix-icon="Search"
clearable
@mouseenter="inputHover = true"
@mouseleave="inputHover = false"
/>
<el-checkbox-group
v-show="!hasNoMatch && data.length > 0"
v-show="!hasNoMatch && !isEmpty(data)"
v-model="checked"
:class="[ns.is('filterable', filterable), ns.be('panel', 'list')]"
>
<el-checkbox
v-for="item in filteredData"
:key="item[keyProp]"
:key="item[propsAlias.key]"
:class="ns.be('panel', 'item')"
:label="item[keyProp]"
:disabled="item[disabledProp]"
:label="item[propsAlias.key]"
:disabled="item[propsAlias.disabled]"
>
<option-content :option="optionRender(item)" />
<option-content :option="optionRender?.(item)" />
</el-checkbox>
</el-checkbox-group>
<p
v-show="hasNoMatch || data.length === 0"
:class="ns.be('panel', 'empty')"
>
<p v-show="hasNoMatch || isEmpty(data)" :class="ns.be('panel', 'empty')">
{{ hasNoMatch ? t('el.transfer.noMatch') : t('el.transfer.noData') }}
</p>
</div>
@ -51,82 +48,59 @@
</div>
</template>
<script lang="ts">
// @ts-nocheck
import { computed, defineComponent, reactive, toRefs } from 'vue'
<script lang="ts" setup>
import { computed, reactive, toRefs, useSlots } from 'vue'
import { isEmpty } from '@element-plus/utils'
import { useLocale, useNamespace } from '@element-plus/hooks'
import { ElCheckbox, ElCheckboxGroup } from '@element-plus/components/checkbox'
import ElInput from '@element-plus/components/input'
import { ElInput } from '@element-plus/components/input'
import { Search } from '@element-plus/icons-vue'
import { CHECKED_CHANGE_EVENT, useCheck, useCheckProps } from './useCheck'
import { transferPanelEmits, transferPanelProps } from './transfer-panel'
import { useCheck, usePropsAlias } from './composables'
export default defineComponent({
import type { VNode } from 'vue'
import type { TransferPanelState } from './transfer-panel'
defineOptions({
name: 'ElTransferPanel',
})
components: {
ElCheckboxGroup,
ElCheckbox,
ElInput,
OptionContent: ({ option }) => option,
},
const props = defineProps(transferPanelProps)
const emit = defineEmits(transferPanelEmits)
const slots = useSlots()
props: useCheckProps,
const OptionContent = ({ option }: { option: VNode | VNode[] }) => option
emits: [CHECKED_CHANGE_EVENT],
const { t } = useLocale()
const ns = useNamespace('transfer')
setup(props, { slots }) {
const { t } = useLocale()
const ns = useNamespace('transfer')
const panelState = reactive<TransferPanelState>({
checked: [],
allChecked: false,
query: '',
inputHover: false,
checkChangeByUser: true,
})
const panelState = reactive({
checked: [],
allChecked: false,
query: '',
inputHover: false,
checkChangeByUser: true,
})
const propsAlias = usePropsAlias(props)
const {
labelProp,
keyProp,
disabledProp,
filteredData,
checkedSummary,
isIndeterminate,
handleAllCheckedChange,
} = useCheck(props, panelState)
const {
filteredData,
checkedSummary,
isIndeterminate,
handleAllCheckedChange,
} = useCheck(props, panelState, emit)
const hasNoMatch = computed(() => {
return panelState.query.length > 0 && filteredData.value.length === 0
})
const hasNoMatch = computed(
() => !isEmpty(panelState.query) && isEmpty(filteredData.value)
)
const hasFooter = computed(() => !!slots.default()[0].children.length)
const hasFooter = computed(() => !isEmpty(slots.default!()[0].children))
const { checked, allChecked, query, inputHover, checkChangeByUser } =
toRefs(panelState)
const { checked, allChecked, query, inputHover } = toRefs(panelState)
return {
ns,
labelProp,
keyProp,
disabledProp,
filteredData,
checkedSummary,
isIndeterminate,
handleAllCheckedChange,
checked,
allChecked,
query,
inputHover,
checkChangeByUser,
hasNoMatch,
SearchIcon: Search,
hasFooter,
t,
}
},
defineExpose({
/** @description filter keyword */
query,
})
</script>

View File

@ -1,68 +1,113 @@
// @ts-nocheck
import { CHANGE_EVENT } from '@element-plus/constants'
import { isNil } from 'lodash-unified'
import {
buildProps,
definePropType,
isArray,
mutable,
} from '@element-plus/utils'
import { CHANGE_EVENT, UPDATE_MODEL_EVENT } from '@element-plus/constants'
import type { VNode } from 'vue'
import type { ExtractPropTypes, h as H, VNode } from 'vue'
import type Transfer from './transfer.vue'
export { CHANGE_EVENT }
export type TransferKey = string | number
export type TransferDirection = 'left' | 'right'
export type Key = string | number
export type TransferDataItem = Record<string, any>
export type DataItem = {
key: Key
label: string
disabled: boolean
export interface TransferFormat {
noChecked?: string
hasChecked?: string
}
export type Format = {
noChecked: string
hasChecked: string
}
export type Props = {
label: string
key: string
disabled: string
}
export type TargetOrder = 'original' | 'push' | 'unshift'
export 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: TargetOrder
export interface TransferPropsAlias {
label?: string
key?: string
disabled?: string
}
export interface TransferCheckedState {
leftChecked: Key[]
rightChecked: Key[]
leftChecked: TransferKey[]
rightChecked: TransferKey[]
}
export interface TransferPanelProps {
data: DataItem[]
optionRender: ({ option: VNode }) => VNode
placeholder: string
title: string
filterable: boolean
format: Format
filterMethod: (query: string, item: DataItem) => boolean
defaultChecked: Key[]
props: Props
}
export const LEFT_CHECK_CHANGE_EVENT = 'left-check-change'
export const RIGHT_CHECK_CHANGE_EVENT = 'right-check-change'
export interface TransferPanelState {
checked: Key[]
allChecked: boolean
query: string
inputHover: boolean
checkChangeByUser: boolean
export const transferProps = buildProps({
data: {
type: definePropType<TransferDataItem[]>(Array),
default: () => [],
},
titles: {
type: definePropType<[string, string]>(Array),
default: () => [],
},
buttonTexts: {
type: definePropType<[string, string]>(Array),
default: () => [],
},
filterPlaceholder: String,
filterMethod: {
type: definePropType<(query: string, item: TransferDataItem) => boolean>(
Function
),
},
leftDefaultChecked: {
type: definePropType<TransferKey[]>(Array),
default: () => [],
},
rightDefaultChecked: {
type: definePropType<TransferKey[]>(Array),
default: () => [],
},
renderContent: {
type: definePropType<
(h: typeof H, option: TransferDataItem) => VNode | VNode[]
>(Function),
},
modelValue: {
type: definePropType<TransferKey[]>(Array),
default: () => [],
},
format: {
type: definePropType<TransferFormat>(Object),
default: () => ({}),
},
filterable: Boolean,
props: {
type: definePropType<TransferPropsAlias>(Object),
default: () =>
mutable({
label: 'label',
key: 'key',
disabled: 'disabled',
} as const),
},
targetOrder: {
type: String,
values: ['original', 'push', 'unshift'],
default: 'original',
},
} as const)
export type TransferProps = ExtractPropTypes<typeof transferProps>
export const transferCheckedChangeFn = (
value: TransferKey[],
movedKeys?: TransferKey[]
) => [value, movedKeys].every(isArray) || (isArray(value) && isNil(movedKeys))
export const transferEmits = {
[CHANGE_EVENT]: (
value: TransferKey[],
direction: TransferDirection,
movedKeys: TransferKey[]
) =>
[value, movedKeys].every(isArray) && ['left', 'right'].includes(direction),
[UPDATE_MODEL_EVENT]: (value: TransferKey[]) => isArray(value),
[LEFT_CHECK_CHANGE_EVENT]: transferCheckedChangeFn,
[RIGHT_CHECK_CHANGE_EVENT]: transferCheckedChangeFn,
}
export type TransferEmits = typeof transferEmits
export type TransferInstance = InstanceType<typeof Transfer>

View File

@ -0,0 +1,161 @@
<template>
<div :class="ns.b()">
<transfer-panel
ref="leftPanel"
:data="sourceData"
:option-render="optionRender"
:placeholder="panelFilterPlaceholder"
:title="leftPanelTitle"
:filterable="filterable"
:format="format"
:filter-method="filterMethod"
:default-checked="leftDefaultChecked"
:props="props.props"
@checked-change="onSourceCheckedChange"
>
<slot name="left-footer" />
</transfer-panel>
<div :class="ns.e('buttons')">
<el-button
type="primary"
:class="[ns.e('button'), ns.is('with-texts', hasButtonTexts)]"
:disabled="isEmpty(checkedState.rightChecked)"
@click="addToLeft"
>
<el-icon><arrow-left /></el-icon>
<span v-if="!isUndefined(buttonTexts[0])">{{ buttonTexts[0] }}</span>
</el-button>
<el-button
type="primary"
:class="[ns.e('button'), ns.is('with-texts', hasButtonTexts)]"
:disabled="isEmpty(checkedState.leftChecked)"
@click="addToRight"
>
<span v-if="!isUndefined(buttonTexts[1])">{{ buttonTexts[1] }}</span>
<el-icon><arrow-right /></el-icon>
</el-button>
</div>
<transfer-panel
ref="rightPanel"
:data="targetData"
:option-render="optionRender"
:placeholder="panelFilterPlaceholder"
:filterable="filterable"
:format="format"
:filter-method="filterMethod"
:title="rightPanelTitle"
:default-checked="rightDefaultChecked"
:props="props.props"
@checked-change="onTargetCheckedChange"
>
<slot name="right-footer" />
</transfer-panel>
</div>
</template>
<script lang="ts" setup>
import { computed, h, reactive, ref, useSlots, watch } from 'vue'
import { debugWarn, isEmpty, isUndefined } from '@element-plus/utils'
import { useFormItem, useLocale, useNamespace } from '@element-plus/hooks'
import { ElButton } from '@element-plus/components/button'
import { ElIcon } from '@element-plus/components/icon'
import { ArrowLeft, ArrowRight } from '@element-plus/icons-vue'
import { transferEmits, transferProps } from './transfer'
import {
useCheckedChange,
useComputedData,
useMove,
usePropsAlias,
} from './composables'
import TransferPanel from './transfer-panel.vue'
import type {
TransferCheckedState,
TransferDataItem,
TransferDirection,
} from './transfer'
import type { TransferPanelInstance } from './transfer-panel'
defineOptions({
name: 'ElTransfer',
})
const props = defineProps(transferProps)
const emit = defineEmits(transferEmits)
const slots = useSlots()
const { t } = useLocale()
const ns = useNamespace('transfer')
const { formItem } = useFormItem()
const checkedState = reactive<TransferCheckedState>({
leftChecked: [],
rightChecked: [],
})
const propsAlias = usePropsAlias(props)
const { sourceData, targetData } = useComputedData(props)
const { onSourceCheckedChange, onTargetCheckedChange } = useCheckedChange(
checkedState,
emit
)
const { addToLeft, addToRight } = useMove(props, checkedState, emit)
const leftPanel = ref<TransferPanelInstance>()
const rightPanel = ref<TransferPanelInstance>()
const clearQuery = (which: TransferDirection) => {
switch (which) {
case 'left':
leftPanel.value!.query = ''
break
case 'right':
rightPanel.value!.query = ''
break
}
}
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,
() => {
formItem?.validate?.('change').catch((err) => debugWarn(err))
}
)
const optionRender = computed(() => (option: TransferDataItem) => {
if (props.renderContent) return props.renderContent(h, option)
if (slots.default) return slots.default({ option })
return h(
'span',
option[propsAlias.value.label] || option[propsAlias.value.key]
)
})
defineExpose({
/** @description clear the filter keyword of a certain panel */
clearQuery,
/** @description left panel ref */
leftPanel,
/** @description left panel ref */
rightPanel,
})
</script>

View File

@ -1,23 +0,0 @@
// @ts-nocheck
import type { Key, TransferCheckedState } from './transfer'
export const LEFT_CHECK_CHANGE_EVENT = 'left-check-change'
export const RIGHT_CHECK_CHANGE_EVENT = 'right-check-change'
export const useCheckedChange = (checkedState: TransferCheckedState, emit) => {
const onSourceCheckedChange = (val: Key[], movedKeys: Key[]) => {
checkedState.leftChecked = val
if (movedKeys === undefined) return
emit(LEFT_CHECK_CHANGE_EVENT, val, movedKeys)
}
const onTargetCheckedChange = (val: Key[], movedKeys: Key[]) => {
checkedState.rightChecked = val
if (movedKeys === undefined) return
emit(RIGHT_CHECK_CHANGE_EVENT, val, movedKeys)
}
return {
onSourceCheckedChange,
onTargetCheckedChange,
}
}

View File

@ -1,43 +0,0 @@
// @ts-nocheck
import { computed } from 'vue'
import type { 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,
}
}