当选项过多时, 使用下拉菜单展示并选择内容.
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 实例 https://fatge.xyz/blog/juejin-example-3#/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 实现
来源: https://juejin.im/post/5c47d524e51d457d105d0e80