[组件库] Select

27 阅读2分钟

实际使用

  1. 覆盖 select panel 的 border color
  2. 覆盖 select panel 的 scroll bar 样式
  3. 显示小三角形
  4. select panel 中的 search icon 的位置 和样式
  5. select option 内容超出并省略的内容,则显示省略号,如果内容很长,隐藏起来,如何配置 total 提示

TODO

  1. 提供清空所有选中项的快捷 icon ❎
  2. 忽略过滤条件 大小写 - 涉及正则表达式的书写,可以作为面试点
  3. 高亮过滤内容

实现

  1. Select 控制整体样式和事件

    • 下拉框的展开和收起
    • 统计所有Option 的数量
    • 统计选择的所有的Option 的数量(响应子组件的点击事件,判断用户点击该选项,是选中还是取消选中)
    • 缓存所有的Option对象,用于显示用户选择的 Option 的 Label
    • 广播过滤条件
  2. Option 负责单个选项

    • 内容展示
    • OptionsCount++
    • SelectedOptionsCount++
    • 向 Select 组件传递点击事件
    • 判断当前Option是否选中, 当用户点击时候,向上 dispatch 点击事件以及被点击的当前 Option
      1. 需要根据 Select 组件的 Value (CheckedOptions) 来判断
      2. 无法根据用户的「点击行为」直接得出是选中还是取消的。
      3. 用户点击 Option,Select 接收 「点击的 Option」,根据如果 Option 已经出现在 SelectedOptions 中,则意味着 取消选中,反之则为选中
      4. 用于初始化用户传入的数据
    • 接收 Select 传入的过滤条件。判断当前选项是否 visible
      1. 如果匹配,则设置 visible,且更新可见 Option 数量,进而更新可见区域高度

实现关键点

  1. dispatch 和 broadcast(在Vue 源码中也有出现,如 keep-alive 组件)

    • Select 通知 Option 进行过滤: this.broadcast('GrOption', 'queryChange', query)
    • Option 通知 Select 当前项被点击: this.dispatch('GrSelect', 'onSelectOptionClick', this)
    function broadcast (componentName, eventName, params) {
      this.$children.forEach(child => {
        const name = child.$options.name
    
        if (name === componentName) {
          child.$emit.apply(child, [eventName].concat(params))
        } else {
          // todo 如果 params 是空数组,接收到的会是 undefined
          broadcast.apply(child, [componentName, eventName].concat([params]))
        }
      })
    }
    export default {
      methods: {
        dispatch (componentName, eventName, params) {
          let parent = this.$parent || this.$root
          let name = parent.$options.name
    
          // 经常在 Vue 源码中见到
          // commonly used in Vue source code
          while (parent && (!name || name !== componentName)) {
            parent = parent.$parent
    
            if (parent) {
              name = parent.$options.name
            }
          }
          if (parent) {
            parent.$emit.apply(parent, [eventName].concat(params))
          }
        },
        broadcast (componentName, eventName, params) {
          broadcast.call(this, componentName, eventName, params)
        }
      }
    }
    
  2. Select 展开之后,过滤框自动获得焦点 StackOverFlow 中的解释

    this.$nextTick(() => {
      this.$refs.input.focus()
    })
    
  3. innerSelectedOptionsValue 需要这个值的原因:

    <xx-select
      :value="[1,2,3]"
      @on-change="onChange">
        <xx-option :value="1">Number1</xx-option>
        <xx-option :value="2">Number2</xx-option>
    </xx-select>
    
    export default {
      data() {
        return {
            options: [1,2]
        }
      },
      methods: {
        onChange(val) {
          this.options = val // 这边如果这样写, 则会在 option 内部重新触发 判断 当前 option 是否 active/selected
          // Element-UI 中流程如下:
          // value => this.select.value => xx-option inject => computed optionSelected => this.contains;
          // 但是按照 Element-UI 中的代码,发现如果不这样写(this.options = val)的话,则在点击新的 option的时候,不会触发 option 的 active/selected 效果
          // 因为在 select.vue 的 handleOptionSelect 中,只是将 value 做了copy 并且 将增/删 之后的备份 value emit 出去
          // 因此 value 并没有修改(这是正确的做法),但是这样也就导致了 value 没有变化,导致 option.vue 中的 itemSelected 中的 this.select.value.contains  也不会有变化。
          // 这样就会有问题,因此需要将用户 toggle option 之后在 select.vue 中 emit 出去的修改之后的 copy 的 value 赋值给 innerValue,
          // 然后在 option.vue 中使用这个 innerValue 来做 contains 判断。
          //  在 provider 和 inject 中使用,用于判断该 option 是否被选中
        }
      }
    }