Optimizing Element-UI Transfer Component for Large Datasets
When the Element-UI Transfer component handles extensive datasets, performance dgeradation occurs during rendering, searching, selection, and data transfer operations.
A custom component can be created by modifying the original Element-UI Transfer source code. The necessary files are located in the packages directory, specifically transfer-panel.vue, main.vue, and index.js.
In transfer-panel.vue, improve the updateAllChecked method to reduce time complexity from O(n²) to O(n) by using an object for lookups.
updateAllChecked() {
const checkedSet = {};
this.checked.forEach(item => {
checkedSet[item] = true;
});
this.allChecked = this.checkableData.length > 0 &&
this.checked.length > 0 &&
this.checkableData.every(item => checkedSet[item[this.keyProp]]);
}
Modify the checked watcher to optimize single selection handling.
watch: {
checked(newVal, oldVal) {
this.updateAllChecked();
const newSet = {};
newVal.forEach(item => { newSet[item] = true; });
const oldSet = {};
oldVal.forEach(item => { oldSet[item] = true; });
if (this.checkChangeByUser) {
const movedKeys = newVal.concat(oldVal)
.filter(key => newSet[key] || oldSet[key]);
this.$emit('checked-change', newVal, movedKeys);
} else {
this.$emit('checked-change', newVal);
this.checkChangeByUser = true;
}
}
}
In main.vue, refactor the addToRight method to enhance data movement efficiency.
addToRight() {
let currentValue = this.value.slice();
const itemsToMove = [];
const keyProp = this.props.key;
const leftCheckedSet = {};
this.leftChecked.forEach(item => { leftCheckedSet[item] = true; });
const valueSet = {};
this.value.forEach(item => { valueSet[item] = true; });
this.data.forEach(item => {
const itemKey = item[keyProp];
if (leftCheckedSet[itemKey] && !valueSet[itemKey]) {
itemsToMove.push(itemKey);
}
});
currentValue = this.targetOrder === 'unshift' ?
itemsToMove.concat(currentValue) :
currentValue.concat(itemsToMove);
this.$emit('input', currentValue);
this.$emit('change', currentValue, 'right', this.leftChecked);
}
Update the sourceData and targetData computed properties.
computed: {
sourceData() {
const valueSet = {};
this.value.forEach(item => { valueSet[item] = true; });
return this.data.filter(item => !valueSet[item[this.props.key]]);
},
targetData() {
if (this.targetOrder === 'original') {
const valueSet = {};
this.value.forEach(item => { valueSet[item] = true; });
return this.data.filter(item => valueSet[item[this.props.key]]);
} else {
return this.value.reduce((acc, cur) => {
const val = this.dataObj[cur];
if (val) acc.push(val);
return acc;
}, []);
}
}
}
Implement virtual scrolling using vue-virtual-scroll-list. First, install the package with npm install vue-virtual-scroll-list.
Create a TransferCheckboxItem.vue component.
<template>
<el-checkbox
class="el-transfer-panel__item"
:label="source[keyProp]"
:disabled="source[disabledProp]">
<option-content :option="source"></option-content>
</el-checkbox>
</template>
<script>
import ElCheckbox from 'element-ui/packages/checkbox';
export default {
name: 'TransferCheckboxItem',
props: {
index: Number,
source: {
type: Object,
default: () => ({})
},
keyProp: String,
disabledProp: String
},
components: { ElCheckbox, OptionContent: {
props: { option: Object },
render(h) {
const findPanel = vm => {
if (vm.$options.componentName === 'ElTransferPanel') return vm;
return vm.$parent ? findPanel(vm.$parent) : vm;
};
const panel = findPanel(this);
const transfer = panel.$parent || panel;
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>;
}
} }
};
</script>
In transfer-panel.vue, import the components and modify the template.
import TransferCheckboxItem from './TransferCheckboxItem.vue';
import VirtualList from 'vue-virtual-scroll-list';
export default {
components: { 'virtual-list': VirtualList },
data() {
return {
itemComponent: TransferCheckboxItem,
virtualListConfig: {}
};
},
computed: {
virtualScroll() {
return this.$parent.virtualScroll;
},
keyProp() {
this.virtualListConfig.keyProp = this.props.key || 'key';
return this.props.key || 'key';
},
disabledProp() {
this.virtualListConfig.disabledProp = this.props.disabled || 'disabled';
return this.props.disabled || 'disabled';
}
}
};
Replace the checkbox rendering section.
<el-checkbox-group
v-model="checked"
v-show="!hasNoMatch && data.length > 0"
:class="{ 'is-filterable': filterable }"
class="el-transfer-panel__list">
<virtual-list
v-if="virtualScroll"
style="height:100%;overflow-y: auto;"
:data-key="keyProp"
:data-sources="filteredData"
:data-component="itemComponent"
:extra-props="virtualListConfig"
/>
<template v-else>
<el-checkbox
class="el-transfer-panel__item"
:label="item[keyProp]"
:disabled="item[disabledProp]"
:key="item[keyProp]"
v-for="item in filteredData">
<option-content :option="item"></option-content>
</el-checkbox>
</template>
</el-checkbox-group>
Add a virtualScroll prop to main.vue.
props: {
virtualScroll: {
type: Boolean,
default: false
}
}
Enable the feature in the parent component.
<custom-transfer v-model="selectedValues" :data="items" :virtual-scroll="true"></custom-transfer>
These modifications allow the Transfer component to effficiently manage datasets of up to 100,000 items.