前言
因为工作内容变更的原因,在20年的时候,我的前端技术栈由Angular转移到了Vue。 它们都是现阶段比较火的前端框架,在一开始的时候,需要快速的转换一下组件设计的思想。
到现在也有半年多的时间了,我现在的前端技术栈主要如下:
前端框架:Vue 2
UI框架: Element UI
开发语言: TypeScript
使用这些技术,不断的开发、封装一些项目中的组件,乐此不疲。
需求
在项目开发过程中,有一个表单组件联动的场景:两个Select组件组成,第二个Select的选项内容根据第一个Select选择的结果动态加载。
设计
要设计联动组件,首先想到的是将两个Select组合起来,将两个Select通过事件机制绑定在一起,从而实现联动效果。
联动组件最终会在Form表单中使用,在设计其功能的时候就需要将表单组件基本功能考虑进去,如:disabled,clearable,placeholder,以及v-model双向绑定等等。
实现
export interface Option {
value: string | number;
label: string;
}
export interface SelectProvider {
getFirstOptions(): Promise<Option[]>;
getOptionsByName(name: string, type?: string): Promise<Option[]>;
getOptionsById(id: number | string, type?: string): Promise<Option[]>;
getDefaultOptions(type?: string): Promise<Option[]>;
}
export class DefaultSelectProvider {
emptyOptions: Option[] = [];
getFirstOptions(): Promise<Option[]> {
return Promise.resolve(this.emptyOptions);
}
getOptionsByName(name: string, type?: string): Promise<Option[]> {
return Promise.resolve(this.emptyOptions);
}
getOptionsById(id: number | string, type?: string): Promise<Option[]> {
return Promise.resolve(this.emptyOptions);
}
getDefaultOptions(type?: string): Promise<Option[]> {
return Promise.resolve(this.emptyOptions);
}
}
<template>
<div>
<el-select v-model="firstValue"
:disabled="disabled"
:clearable="clearable" placeholder="请选择" @change="handleFirstChange">
<el-option v-for="item in firstOptions" :key="item.value" :label="item.label" :value="item.value"></el-option>
</el-select>
<el-select
v-model="secondValue"
:disabled="disabled"
:clearable="clearable"
remote
filterable
placeholder="请选择"
style="margin-left: 8px"
@change="handleSecondChange"
:remote-method="handleSecondRemoteMethod"
>
<el-option v-for="item in secondOptions" :key="item.value" :label="item.label" :value="item.value"></el-option>
</el-select>
</div>
</template>
<script lang="ts">
// import ...
@Component
export default class TwoSelect extends Vue {
@Prop() value: any;
@Prop() first!: string;
@Prop() second!: string;
@Prop() clearable!: boolean;
@Prop() disabled!: boolean;
@Prop({ type: Object, default: new DefaultSelectProvider() }) selectProvider!: SelectProvider;
firstOptions: Option[] = [];
secondOptions: Option[] = [];
@Watch('value', { immediate: true, deep: true })
onValueChanged(val) {
if (val) {
this.firstValue = val[this.firstField];
this.secondValue = val[this.secondField];
} else {
this.firstValue = '';
this.secondValue = '';
}
}
firstField = this.first || 'first';
secondField = this.second || 'second';
firstValue = '';
secondValue = '';
created() {
this.selectProvider.getFirstOptions().then(res => this.firstOptions = res);
this.initSecondOptions();
}
initSecondOptions() {
if (!this.secondOptions || this.secondOptions.length === 0) {
this.selectProvider.getOptionsByValue(this.secondValue, this.firstValue).then(res => (this.secondOptions = res));
}
}
handleFirstChange(val) {
this.secondValue = '';
this.$emit('firstChange', val);
const ms: any = null;
this.selectProvider.getDefaultOptions(val).then(res => {
this.secondOptions = res;
});
}
emitChange() {
const value = {};
value[this.firstField] = this.firstValue;
value[this.secondField] = this.secondValue;
this.$emit('change', value);
}
handleSecondChange(val) {
this.emitChange();
}
clearValue() {
this.firstValue = '';
this.secondValue = '';
}
handleSecondRemoteMethod(query) {
// TODO: optimize with throttle-debounce
if (query !== '') {
this.selectProvider.getOptionsByLabel(query, this.firstValue).then(res => this.secondOptions = res);
}
}
}
</script>