前言
因为工作内容变更的原因,在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>