Select 组件主要特点在于:当选项过多时,使用下拉菜单展示并选择内容。
- 数据双向绑定,下拉列表变动时,选中项如何回显;
- 单选、多选的区分,以及对应处理。
1. 实例
代码
<fat-select v-model="inputValue">
<fat-option
v-for="item in options"
:key="item.value"
:label="item.label"
:value="item.value"
>{{ item.label }}</fat-option>
</fat-select>
复制代码
实例地址:Select 实例
代码地址:Github UI-Library
2. 原理
Select 组件的基本结构如下
主要可分为两个部分:
- 显示框:用来展示已经选中项,包含取消按钮;
- 下拉框:包含已选中的高亮项,禁用项,默认选择选项等,具备点击选中,再次点击取消的操作;
- 确保每个下拉项唯一,即使存在相同 label 的情况。
fat-select 显示框:
<template>
<div
:class="['select-wrapper', { 'is-disabled': disabled }]"
tabindex="0"
@click.stop="isOpen = !disabled && !isOpen"
@blur="handleBlur"
>
<div class="select-top-part">
<template v-if="!selectItems.length">
<span class="placeholder">{{ placeholder }}</span>
</template>
<template v-else>
<div>{{ selectItems[0].label }}</div>
</template>
</div>
<!-- 下拉框 -->
<div class="select-bottom-part" v-show="isOpen">
<slot></slot>
</div>
</div>
</template>
<script>
export default {
props: {
placeholder: { type: String, default: "请选择" },
optionKey: { type: String, default: "value" },
value: { type: [String, Object, Number, Array] }
},
model: {
prop: "value",
event: "input"
},
data() {
return {
isOpen: false,
selectValue: [],
selectItems: []
};
},
provide() {
return {
fatSelect: this
};
},
watch: {
value: {
handler(value) {
const { multiple } = this;
const init = value ? value : multiple ? [] : "";
this.selectValue = multiple ? [...init] : init;
},
immediate: true
},
selectValue: {
handler(value) {
this.selectItems = [];
}
}
},
methods: {
handleDelete(item) {
const { value } = item;
this.selectValue = this.selectValue.filter(item => item !== value);
this.$emit("input", this.selectValue);
this.$emit("change", this.selectValue);
},
handleBlur(event) {
this.isOpen = false;
this.$emit('blur', event);
}
...
}
};
</script>
复制代码
利用 tabIndex
属性使得最外层的 div
能够触发 blur
事件,如果失焦就收起下拉框。
<div
:class="['select-wrapper', { 'is-disabled': disabled }]"
tabindex="0"
@click.stop="isOpen = !disabled && !isOpen"
@blur="handleBlur"
>
...
<!-- 下拉框 -->
<div class="select-bottom-part" v-show="isOpen">
<slot></slot>
</div>
</div>
handleBlur(event) {
this.isOpen = false;
this.$emit('blur', event);
}
复制代码
组件实现数据双向绑定,当 v-model
对应的值变动时,Select 组件的值也会发生改变,但是显示框内所呈现的是选中项的 label
属性,所以将选中值 selectValue
和选中项 selectItems
进行区分。
同时配置 v-model
相关属性,同时监测 watch
相关 value
具体如下
model: {
prop: "value",
event: "input"
},
watch: {
value: {
handler(value) {
const { multiple } = this;
const init = value ? value : multiple ? [] : "";
this.selectValue = multiple ? [...init] : init;
},
immediate: true
}
}
复制代码
同时利用 provide
向其所有下拉框注入一个依赖,用于访问 selectValue
和 selectItems
等prop和data。
provide() {
return {
fatSelect: this
};
}
复制代码
默认 optionKey: { type: String, default: "value" }
作为下拉项的唯一标识,默认值为 value ,也可自定义。
fat-option 下拉框:
利用插槽将下拉框插入 Select 组件中,其具体定义如下
<template>
<div
:class="['select-option-wrapper', { 'is-selected': isSelect }, { 'is-disabled': disabled }]"
@click.stop="handleClick"
>
<slot></slot>
</div>
</template>
<script>
export default {
props: {
value: { type: [Object, String, Number], required: true },
label: { type: String },
disabled: { type: Boolean, defa: false }
},
inject: ["fatSelect"],
computed: {
isSelect() {
const {
fatSelect: { optionKey, selectItems }
} = this;
const key = this[optionKey] || this.$attrs[optionKey];
return selectItems.find(item => item.key === key);
}
},
watch: {
["fatSelect.selectValue"]: {
handler(newValue) {
const {
value,
label,
fatSelect: { optionKey, multiple, selectValue }
} = this;
const key = this[optionKey] || this.$attrs[optionKey];
if (
newValue === value ||
(Array.isArray(newValue) && newValue.find(item => item === value))
) {
if (!multiple) {
this.fatSelect.selectItems = [
{
key,
label,
value
}
];
} else {
this.fatSelect.selectItems.push({
key,
label,
value
});
}
}
},
immediate: true
}
},
methods: {
...
}
};
</script>
复制代码
利用 inject: ["fatSelect"]
将上述 provide
的 Select 组件注入到当前选项中,
通过 this.fatSelect
来访问父组件的 selectItems
来判断,当前选项是否为选中项。
isSelect() {
const {
fatSelect: { optionKey, selectItems }
} = this;
const key = this[optionKey] || this.$attrs[optionKey];
return selectItems.find(item => item.key === key);
}
复制代码
同时watch fatSelect.selectValue
也就是选中值,之前说过该组件实现数据的双向绑定,当Select组件 v-model
绑定的值变动时,需要同步到下拉项。
["fatSelect.selectValue"]: {
handler(newValue) {
const {
value,
label,
fatSelect: { optionKey, multiple, selectValue }
} = this;
const key = this[optionKey] || this.$attrs[optionKey];
if (
newValue === value ||
(Array.isArray(newValue) && newValue.find(item => item === value))
) {
if (!multiple) {
this.fatSelect.selectItems = [
{
key,
label,
value
}
];
} else {
this.fatSelect.selectItems.push({
key,
label,
value
});
}
}
},
immediate: true
}
复制代码
如果对应的 fatSelect.selectValue
变动时,要判断当前选项的 optionKey
是否在 selectValue
中,如果存在,就将
this.fatSelect.selectItems = [
{
key,
label,
value
}
];
复制代码
3. 结论
忽略了 Select 组件的一些细节,例如多选、单选的逻辑,重点展示了下该组件的设计逻辑,以及数据绑定的实现方式,总结了一些实际业务中碰到的问题。
往期文章:
- 从零实现Vue的组件库(零)- 基本结构以及构建工具
- 从零实现Vue的组件库(一)- Toast 实现
- 从零实现Vue的组件库(二)- Slider 实现
- 从零实现Vue的组件库(三)- Tabs 实现
- 从零实现Vue的组件库(四)- File-Reader 实现
- 从零实现Vue的组件库(五)- Breadcrumb 实现
- 从零实现Vue的组件库(六)- Hover-Tip 实现
- 从零实现Vue的组件库(七)- Message-Box 实现
- 从零实现Vue的组件库(八)- Input 实现
- 从零实现Vue的组件库(九)- InputNumber 实现
- 从零实现Vue的组件库(十)- Select 实现
原创声明: 该文章为原创文章,转载请注明出处。