如何使用Select组件封装成级联组件

1,707 阅读1分钟

前言

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