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组件结合之后进行二次开发,剩下的就是在原有的属性和方法上进行拓展。不过本组件也仅有基本和常用功能,更多功能如多选、自定义其他功能可以自行分析和开发。

相关推荐
桂月二二4 小时前
探索前端开发中的 Web Vitals —— 提升用户体验的关键技术
前端·ux
沈梦研5 小时前
【Vscode】Vscode不能执行vue脚本的原因及解决方法
ide·vue.js·vscode
hunter2062065 小时前
ubuntu向一个pc主机通过web发送数据,pc端通过工具直接查看收到的数据
linux·前端·ubuntu
qzhqbb5 小时前
web服务器 网站部署的架构
服务器·前端·架构
刻刻帝的海角5 小时前
CSS 颜色
前端·css
轻口味6 小时前
Vue.js 组件之间的通信模式
vue.js
九酒6 小时前
从UI稿到代码优化,看Trae AI 编辑器如何帮助开发者提效
前端·trae
浪浪山小白兔7 小时前
HTML5 新表单属性详解
前端·html·html5
lee5767 小时前
npm run dev 时直接打开Chrome浏览器
前端·chrome·npm
2401_897579657 小时前
AI赋能Flutter开发:ScriptEcho助你高效构建跨端应用
前端·人工智能·flutter