前言
Select组件作为常见的用户界面元素,广泛应用于各种软件和网页中,用于提供用户从预定义选项中进行选择的功能。本文将简单介绍Select组件的基本开发。
需求分析
基本需求
先来分析一下Select组件都需要什么功能:
- 点击选项框展开下拉菜单
- 选中菜单中的某一项之后关闭菜单
- 并且在选中后需要将选中的项填充到选项框中
分析组件
以上就是一个Select组件需要具备的基本功能,基于这几个功能,我们可以想到,Select同样也有触发区域和展示区域,原生的select组件是分为select
和option
两个部分,Element Plus也是分为了el-select
和el-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组件结合之后进行二次开发,剩下的就是在原有的属性和方法上进行拓展。不过本组件也仅有基本和常用功能,更多功能如多选、自定义其他功能可以自行分析和开发。