Element Plus 组件库实现:8. Select组件

前言

Select组件作为常见的用户界面元素,广泛应用于各种软件和网页中,用于提供用户从预定义选项中进行选择的功能。本文将简单介绍Select组件的基本开发。

需求分析

基本需求

先来分析一下Select组件都需要什么功能:

  • 点击选项框展开下拉菜单
  • 选中菜单中的某一项之后关闭菜单
  • 并且在选中后需要将选中的项填充到选项框中

分析组件

以上就是一个Select组件需要具备的基本功能,基于这几个功能,我们可以想到,Select同样也有触发区域和展示区域,原生的select组件是分为selectoption两个部分,Element Plus也是分为了el-selectel-option两个。我们这里还是从简处理,只有一个Select组件,当然,一个Select组件完成基本功能也不是那么容易就实现的。

确定组件

我们知道,Select组件在选中选项时候需要将选项填充,所以触发区域可以使用Input组件,说到触发区域和显示区域,就不得不说起之前多次用到的Tooltip组件,前边已经介绍了Dropdown组件就是基于Tooltip组件进行二次开发,那么这里我们同样可以使用Tooltip组件来开发一个Select组件。

其他需求

一个Select只有上述基本功能就太单调了,而且这些功能原生的select也可以实现,那么再为我们的Select添加一些其他有意思的功能吧:

  • 填充的选项可清空
  • 选项节点可自定义
  • 选项可筛选
  • 支持远程搜索
  • 支持键盘事件

确定方案

  • 属性
ts 复制代码
import type { VNode } from 'vue'
// 自定义渲染选项节点
export type RenderLabelFunc = (option: SelectOption) => VNode
// 支持筛选方法
export type CustomFilterFunc = (value: string) => SelectOption[]
// 支持远程搜索方法
export type CustomFilterRemoteFunc = (value: string) => Promise<SelectOption[]>
// 每个选项需要的属性
export interface SelectOption {
  label: string
  value: string
  disabled?: boolean
}

export interface SelectsProps {
  // v-model
  modelValue: string
  // 选项
  options?: SelectOption[]
  //   表单属性
  placeholder?: string
  disabled?: boolean
  // 可清除已选中的选项
  clearable?: boolean
  // 自定义渲染节点
  renderLabel?: RenderLabelFunc
  // 是否开启筛选
  filterable?: boolean
  filterMethod?: CustomFilterFunc
  
  // 是否开启远程搜索
  remote?: boolean
  remoteMethod?: CustomFilterRemoteFunc
}
// 用于处理选中选项的状态
export interface SelectState {
  inputValue: string
  selectedOption: null | SelectOption
  mouseHover: boolean
  loading: boolean
  // 用于按键盘上下键的时候高亮选项
  highlightIndex: number
}
  • 事件
ts 复制代码
export interface SelectEmits {
   // 选项改变时
  (e: 'change', value: string): void
  // 与modelValue结合支持v-model
  (e: 'update:modelValue', value: string): void
  // 菜单展示隐藏
  (e: 'visible-change', value: boolean): void
  // 清除选项
  (e: 'clear'): void
}
  • 组件
html 复制代码
<template>
    <div class="yv-select">
        <Tooltip>
            <Input>
                <template #suffix>
                    <!-- 清除图标 -->
                    <Icon icon="circle-xmark"></Icon>
                    <!-- 展开箭头 -->
                    <Icon icon="angle-down"></Icon>
                </template>
            </Input>
            <template #content>
                <!-- 等待数据加载中展示 -->
                <div class="yv-select__loading">
                    <Icon icon="spinner" spin></Icon>
                </div>
                <!-- 无数据时展示 -->
                <div>
                    no Data
                </div>
                  <!-- 展示数据,渲染选项 -->
                <ul class="yv-select__menu">
                    <template >
                        <li >
                          <!-- 支持默认和用户自定义节点 -->
                            <RenderVnode />
                        </li>
                    </template>
                </ul>
            </template>
        </Tooltip>
    </div>
</template>

代码实现

接下来就来看看这些功能都是怎么实现的吧:

基本功能

  • 点击展开/隐藏菜单列表
ts 复制代码
const controlDropdown = (show: boolean) => {
    if (show) {
        // filter模式
        // 之前选择过对应的值
        if (props.filterable && selectState.selectedOption) {
            selectState.inputValue = ''
        }
        // 进行一次默认选项的生成
        if (props.filterable) {
            // generateFilterOptions方法用于过滤,下文将展示
            generateFilterOptions(selectState.inputValue)
        }
        // 使用Tooltip组件的方法实现展开/隐藏
        tooltipRef.value.show()
    } else {
        tooltipRef.value.hide()
         // blur 时候将之前的值回显到 input 中,实现复用
        if (props.filterable) {
            selectState.inputValue = selectState.selectedOption ? selectState.selectedOption.label : ''
        }
        // 取消高亮
        selectState.highlightIndex = -1
    }
    isDropdownShow.value = show
    emits('visible-change', show)
}
// 绑定到节点上的方法
const toggleDropdown = () => {
    if (props.disabled) return
    if (isDropdownShow.value) {
        controlDropdown(false)
    } else {
        controlDropdown(true)
    }
}
  • 选中选项之后填充选项框,关闭展示选项列表
ts 复制代码
// 处理点击某个选择项
const optionSelect = async (e: SelectOption) => {
    if (e.disabled) return
    // 保存选中选项的状态,填充选项框
    selectState.inputValue = e.label
    selectState.selectedOption = e
    // 派发相应事件
    emits('change', e.value)
    emits('update:modelValue', e.value)
    controlDropdown(false)
    await nextTick()
    // 选中之后让选项框保持选中状态
    inputRef.value.ref.focus()
}

其他功能

  • 填充的选项可清空
ts 复制代码
// 展示清除图标
const showClearIcon = computed(() => {
    // 展示条件
    // 1. hover上去
    // 2. props.clearable为true
    // 3. 必须要有选择过选项
    // 4. Input的值不能为空
    return props.clearable && selectState.mouseHover && selectState.selectedOption && selectState.inputValue.trim() !== ''
})
// 点击清除图标
const onClear = () => {
    selectState.selectedOption = null
    selectState.inputValue = ''
    emits('change', '')
    emits('clear')
    emits('update:modelValue', '')
}
  • 选项节点可自定义

这里使用 <RenderVnode :v-node="renderLabel ? renderLabel(item) : item.label" />即可,支持用户自定义节点。

  • 支持筛选和远程搜索
ts 复制代码
// 可筛选
const filteredOptions = ref(props.options)
watch(() => props.options, (newVal) => {
    filteredOptions.value = newVal
})
// 定义用于筛选的函数
const generateFilterOptions = async (searchValue: string) => {
    if (!props.filterable) return

    if (props.filterMethod && isFunction(props.filterMethod)) {
        filteredOptions.value = props.filterMethod(searchValue)
    } else if (props.remote && props.remoteMethod && isFunction(props.remoteMethod)) {
        selectState.loading = true
        try {
            filteredOptions.value = await props.remoteMethod(searchValue)
            // 当远程搜索模式的时候需要手动完成展示
            // 注意要在结果生成以后再展示,否则生成的下拉菜单上面箭头图标位置不对
            // if(remote)
        } catch (error) {
            console.error(error)
            filteredOptions.value = []
        } finally {
            selectState.loading = false
        }
    } else {
        filteredOptions.value = props.options.filter(option => option.label.includes(searchValue))
    }
    selectState.highlightIndex = -1
}
// 开始筛选,绑定到input事件
const onFilter = () => {
    generateFilterOptions(selectState.inputValue)
}
  • 支持键盘事件
ts 复制代码
// 添加键盘事件
const handleKeyDown = (e: KeyboardEvent) => {
    switch (e.key) {
        // 回车键
        case 'Enter':
            if (!isDropdownShow.value) {
                controlDropdown(true)
            } else {
                if (selectState.highlightIndex > -1 && filteredOptions.value[selectState.highlightIndex]) {
                    optionSelect(filteredOptions.value[selectState.highlightIndex])
                }
            }
            break;
            // esc键
        case 'Escape':
            if (isDropdownShow.value) {
                controlDropdown(false)

            }
            break
            // 向上箭头
        case 'ArrowUp':
            e.preventDefault()
            if (filteredOptions.value.length > 0) {
                // 找不到或第一项
                if (selectState.highlightIndex === -1 || selectState.highlightIndex === 0) {
                    selectState.highlightIndex = filteredOptions.value.length - 1
                } else {
                    selectState.highlightIndex--
                }
            }
            break
            // 向下箭头
        case 'ArrowDown':
            e.preventDefault()
            if (filteredOptions.value.length > 0) {
                // 找不到或最后一项
                if (selectState.highlightIndex === -1 || selectState.highlightIndex === (filteredOptions.value.length - 1)) {
                    selectState.highlightIndex = 0
                } else {
                    selectState.highlightIndex++
                }
            }
            break
        default:
            break;
    }
}

总结

以上就是开发一个基本Select组件的简介,思路就是Select组件可以将Input和Tooltip组件结合之后进行二次开发,剩下的就是在原有的属性和方法上进行拓展。不过本组件也仅有基本和常用功能,更多功能如多选、自定义其他功能可以自行分析和开发。

相关推荐
J总裁的小芒果10 分钟前
Vue3 el-table 默认选中 传入的数组
前端·javascript·elementui·typescript
Lei_zhen9612 分钟前
记录一次electron-builder报错ENOENT: no such file or directory, rename xxxx的问题
前端·javascript·electron
咖喱鱼蛋15 分钟前
Electron一些概念理解
前端·javascript·electron
yqcoder16 分钟前
Vue3 + Vite + Electron + TS 项目构建
前端·javascript·vue.js
鑫宝Code33 分钟前
【React】React Router:深入理解前端路由的工作原理
前端·react.js·前端框架
Mr_Xuhhh2 小时前
重生之我在学环境变量
linux·运维·服务器·前端·chrome·算法
永乐春秋3 小时前
WEB攻防-通用漏洞&文件上传&js验证&mime&user.ini&语言特性
前端
鸽鸽程序猿3 小时前
【前端】CSS
前端·css
ggdpzhk3 小时前
VUE:基于MVVN的前端js框架
前端·javascript·vue.js
学不会•5 小时前
css数据不固定情况下,循环加不同背景颜色
前端·javascript·html