select 选择器是个比较复杂的组件了,经过不一样的配置能够有多种用法。有必要单独学习学习。
如下是 select 的 template 结构,已去掉了一部分代码便于查看总体结构:html
<template> <div> <!-- 多选 --> <div v-if="multiple" ref="tags"> <!-- collapse tags 多选时是否将选中值按文字的形式展现 --> <span v-if="collapseTags && selected.length"> <el-tag type="info" disable-transitions> <span class="el-select__tags-text">{{ selected[0].currentLabel }}</span> </el-tag> <el-tag v-if="selected.length > 1" type="info" disable-transitions> <span class="el-select__tags-text">+ {{ selected.length - 1 }}</span> </el-tag> </span> <!-- 多选,多个 el-tag 组成 --> <transition-group @after-leave="resetInputHeight" v-if="!collapseTags"> <el-tag v-for="item in selected" :key="getValueKey(item)" type="info" disable-transitions> <span class="el-select__tags-text">{{ item.currentLabel }}</span> </el-tag> </transition-group> <!-- 可输入文本的查询框 --> <input v-model="query" v-if="filterable" ref="input"> </div> <!-- 显示结果框 read-only --> <el-input ref="reference" v-model="selectedLabel"> <!-- 用户显示清空和向下箭头 --> <i slot="suffix"></i> </el-input> <!-- 下拉菜单 --> <transition> <el-select-menu ref="popper" v-show="visible && emptyText !== false"> <el-scrollbar tag="ul" wrap-class="el-select-dropdown__wrap" view-class="el-select-dropdown__list" ref="scrollbar" v-show="options.length > 0 && !loading"> <!-- 默认项(建立条目) --> <el-option :value="query" created v-if="showNewOption"> </el-option> <!-- 插槽,用于放 option 和 option-group --> <slot></slot> </el-scrollbar> <!-- loading 加载中文本 --> <p v-if="emptyText && (!allowCreate || loading || (allowCreate && options.length === 0 ))"> {{ emptyText }} </p> </el-select-menu> </transition> </div> </template>
具体都写在注释中了~从上面内容中能够看到,select 考虑了不少状况,如单选、多选、搜索、下拉框、图标等等。而且使用 slot 插槽来获取开发者传递的 option 和 option-group 组件。
能够发如今 select 中使用了多个外部组件,也就是说 el-select 是由多个组件组装成的一个复杂组件~vue
// components import ElInput from 'element-ui/packages/input'; import ElSelectMenu from './select-dropdown.vue'; import ElOption from './option.vue'; import ElTag from 'element-ui/packages/tag'; import ElScrollbar from 'element-ui/packages/scrollbar';
参照官方文档的内容罗列出 select 的一些功能,后面跟上我对功能实现的理解:element-ui
select
弹出下拉框,点击 option
完成赋值。select
和 option
都有 disabled
选项用于禁用。select
中有内容,鼠标悬浮在 input
上显示删除图标,点击执行删除操做。slot
插槽,默认加了 span
显示内容。能够修改 el-option
标签中内容来自定义模板。select
中添加额外 option
(通常 option
都是经过 slot
插槽传递的),如容许建立条目,则显示这条 option
,option
的内容显示为查询内容。分析下基本功能:点击 input,显示下拉菜单;鼠标选中一项 option,隐藏下拉菜单;input 中显示选中的结果。
因此这里看下显示内容的 input 都有些什么事件:api
@focus="handleFocus" // 处理 焦点 @blur="handleBlur" // 处理 焦点 离开 @keyup.native="debouncedOnInputChange" @keydown.native.down.stop.prevent="navigateOptions('next')" // 向下按键,移动到下一个 option @keydown.native.up.stop.prevent="navigateOptions('prev')" // 向上按键,移动到上一个 option @keydown.native.enter.prevent="selectOption" // 回车按键,选中option @keydown.native.esc.stop.prevent="visible = false" // esc按键,隐藏下拉框 @keydown.native.tab="visible = false" // tab按键,跳转到下一个文本框,隐藏下拉框 @paste.native="debouncedOnInputChange" // @mouseenter.native="inputHovering = true" // mouse enter 事件 @mouseleave.native="inputHovering = false" // mouse leave 事件
从上面的这些事件中能够知道:选中方法为 selectOption
(从英文字面意思都能知道~);显示下拉框经过 visible
属性控制;以及其余按键的一些功能。这里主要主要看看 selectOption
方法。数组
selectOption() { if (!this.visible) { this.toggleMenu(); } else { if (this.options[this.hoverIndex]) { this.handleOptionSelect(this.options[this.hoverIndex]); } } },
逻辑就是,若是下拉框未显示则执行 toggleMenu
方法触发下拉框,若是已显示下拉框则处理选择 option 的过程。看看这个 toggleMenu
方法:ide
toggleMenu() { if (!this.selectDisabled) { this.visible = !this.visible; if (this.visible) { (this.$refs.input || this.$refs.reference).focus(); } } },
其实就是控制下拉菜单的显示和隐藏。若是显示的时候定焦在 input
和 reference
上,它们其实就是单选和多选的 input 框(多选 input 定义了 ref="input"
单选 input 定义了 ref="reference"
)。
至此,下拉菜单的显示与隐藏解决了。而后咱们去找 option 点击事件:学习
// 处理选项选中事件 handleOptionSelect(option) { if (this.multiple) { // 多选 const value = this.value.slice(); const optionIndex = this.getValueIndex(value, option.value); if (optionIndex > -1) { // 已选中,从数组中移除 value.splice(optionIndex, 1); } else if (this.multipleLimit <= 0 || value.length < this.multipleLimit) { // 未选中,传入数组 value.push(option.value); } this.$emit('input', value); this.emitChange(value); if (option.created) { this.query = ''; this.handleQueryChange(''); this.inputLength = 20; } // 查询 if (this.filterable) this.$refs.input.focus(); } else { // 单选 this.$emit('input', option.value); this.emitChange(option.value); this.visible = false; } // 渲染完成后 this.$nextTick(() => { this.scrollToOption(option); this.setSoftFocus(); }); },
处理选中事件考虑了单选和多选两种状况。
若是是多选,检索选中 option 是否在 value
数组中,有则移除、无则添加到 value
数组中。而后 $emit
触发 input
事件,执行 emitChange
方法。若是 option 的 created
为 true,则清空查询内容。
若是是单选,$emit
触发 input
事件将选中值传递给父组件,执行 emitChange
方法,最后隐藏下拉菜单。
最后使用 $nextTick
方法处理下界面。
到这里,选中 option 后下拉菜单消失问题解决,只剩下显示结果到 input 中了。这个显示结果的过程是经过对 visible
属性的监听来完成的(一开始觉得在 emitChange
结果发现那只是触发改变事件的)。动画
visible(val) { // 在下拉菜单隐藏时 if (!val) { // 处理图标 this.handleIconHide(); // 广播下拉菜单销毁事件 this.broadcast('ElSelectDropdown', 'destroyPopper'); // 取消焦点 if (this.$refs.input) { this.$refs.input.blur(); } // 重置过程 this.query = ''; this.previousQuery = null; this.selectedLabel = ''; this.inputLength = 20; this.resetHoverIndex(); this.$nextTick(() => { if (this.$refs.input && this.$refs.input.value === '' && this.selected.length === 0) { this.currentPlaceholder = this.cachedPlaceHolder; } }); // 若是不是多选,进行赋值如今 input 中 if (!this.multiple) { // selected 为当前选中的 option if (this.selected) { if (this.filterable && this.allowCreate && this.createdSelected && this.createdOption) { this.selectedLabel = this.createdLabel; } else { this.selectedLabel = this.selected.currentLabel; } // 查询结果 if (this.filterable) this.query = this.selectedLabel; } } } else { // 下拉菜单显示 // 处理图片显示 this.handleIconShow(); // 广播下拉菜单更新事件 this.broadcast('ElSelectDropdown', 'updatePopper'); // 处理查询事件 if (this.filterable) { this.query = this.remote ? '' : this.selectedLabel; this.handleQueryChange(this.query); if (this.multiple) { this.$refs.input.focus(); } else { if (!this.remote) { this.broadcast('ElOption', 'queryChange', ''); this.broadcast('ElOptionGroup', 'queryChange'); } this.broadcast('ElInput', 'inputSelect'); } } } // 触发 visible-change 事件 this.$emit('visible-change', val); },
从 template 中可知,显示结果的 input 绑定的 v-model
是 selectedLabel
,而 select 是经过获取下拉菜单的显示与隐藏事件来执行结果显示部分的功能的。最终 selectedLabel
得到到了选中的 option 的 label
内容。
这样,从 点击-单选-显示 的流程就实现了。仍是很简单的。ui
关于多选,在刚才讲单选的时候说起了一些了。因此有些代码就不贴出浪费篇幅了。具体逻辑以下:
先点击 input 执行 selectOption
方法显示下拉菜单,而后点击下拉菜单中的 option,执行 handleOptionSelect
方法将 option 的值都传给 value
数组。此时 value
数组改变,触发 watch 中的 value
变化监听方法。this
value(val) { // 多选 if (this.multiple) { this.resetInputHeight(); if (val.length > 0 || (this.$refs.input && this.query !== '')) { this.currentPlaceholder = ''; } else { this.currentPlaceholder = this.cachedPlaceHolder; } if (this.filterable && !this.reserveKeyword) { this.query = ''; this.handleQueryChange(this.query); } } this.setSelected(); // 非多选查询 if (this.filterable && !this.multiple) { this.inputLength = 20; } },
以上代码关键是执行了 setSelected
方法:
// 设置选择项 setSelected() { // 单选 if (!this.multiple) { let option = this.getOption(this.value); // created 是指建立出来的 option,这里指 allow-create 建立的 option 项 if (option.created) { this.createdLabel = option.currentLabel; this.createdSelected = true; } else { this.createdSelected = false; } this.selectedLabel = option.currentLabel; this.selected = option; if (this.filterable) this.query = this.selectedLabel; return; } // 遍历获取 option let result = []; if (Array.isArray(this.value)) { this.value.forEach(value => { result.push(this.getOption(value)); }); } // 赋值 this.selected = result; this.$nextTick(() => { // 重置 input 高度 this.resetInputHeight(); }); },
能够看到若是是多选,那么将 value
数组遍历,获取相应的 option
值,传给 selected
。而多选界面其实就是对于这个 selected
的 v-for 遍历显示。显示的标签使用的是 element 的另一个组件 el-tag
<el-tag v-for="item in selected" :key="getValueKey(item)"> <span class="el-select__tags-text">{{ item.currentLabel }}</span> </el-tag>
这里顺便提一句: option 的 created
参数用于标识是 select
组件中建立的那个用于建立条目的 option
。而从 slot 插槽传入的 option 是不用传 created
参数的。
从 template 中可知,select 有两个 input,一个用于显示结果,一个则用于查询搜索。咱们来看下搜索内容的 input 文本框如何实现搜索功能:
在 input 中有 @input="e => handleQueryChange(e.target.value)"
这么一段代码。因此,handleQueryChange 方法就是关键所在了。
// 处理查询改变 handleQueryChange(val) { if (this.previousQuery === val) return; if ( this.previousQuery === null && (typeof this.filterMethod === 'function' || typeof this.remoteMethod === 'function') ) { this.previousQuery = val; return; } this.previousQuery = val; this.$nextTick(() => { if (this.visible) this.broadcast('ElSelectDropdown', 'updatePopper'); }); this.hoverIndex = -1; if (this.multiple && this.filterable) { const length = this.$refs.input.value.length * 15 + 20; this.inputLength = this.collapseTags ? Math.min(50, length) : length; this.managePlaceholder(); this.resetInputHeight(); } if (this.remote && typeof this.remoteMethod === 'function') { this.hoverIndex = -1; this.remoteMethod(val); } else if (typeof this.filterMethod === 'function') { this.filterMethod(val); this.broadcast('ElOptionGroup', 'queryChange'); } else { this.filteredOptionsCount = this.optionsCount; this.broadcast('ElOption', 'queryChange', val); this.broadcast('ElOptionGroup', 'queryChange'); } if (this.defaultFirstOption && (this.filterable || this.remote) && this.filteredOptionsCount) { this.checkDefaultFirstOption(); } },
其中,remoteMethod
和 filterMethod
方法是自定义的远程查询和本地过滤方法。若是没有自定义的这两个方法,则会触发广播给 option
和 option-group
组件 queryChange
方法。
// option.vue queryChange(query) { let parsedQuery = String(query).replace(/(\^|\(|\)|\[|\]|\$|\*|\+|\.|\?|\\|\{|\}|\|)/g, '\\$1'); // 匹配字符决定是否显示当前option this.visible = new RegExp(parsedQuery, 'i').test(this.currentLabel) || this.created; if (!this.visible) { this.select.filteredOptionsCount--; } }
option 中经过正则匹配决定是否隐藏当前 option 组件,而 option-group 经过获取子组件,判断若是有子组件是可见的则显示,不然隐藏。
// option-group.vue queryChange() { this.visible = this.$children && Array.isArray(this.$children) && this.$children.some(option => option.visible === true); }
因此,其实 option 和 option-group 在搜索的时候只是隐藏掉了不匹配的内容而已。
下拉菜单是经过 transition 来实现过渡动画的。
下拉菜单 el-select-menu
本质上就是一个 div 容器而已。
<div class="el-select-dropdown el-popper" :class="[{ 'is-multiple': $parent.multiple }, popperClass]" :style="{ minWidth: minWidth }"> <slot></slot> </div>
另外,在代码中常常出现的通知下拉菜单显示和隐藏的广播在 el-select-menu
的 mounted
方法中接收使用:
mounted() { this.referenceElm = this.$parent.$refs.reference.$el; this.$parent.popperElm = this.popperElm = this.$el; this.$on('updatePopper', () => { if (this.$parent.visible) this.updatePopper(); }); this.$on('destroyPopper', this.destroyPopper); }
上文中提到过,就是在 select 中默认藏了一条 option,当建立条目时显示这个 option 并显示建立内容。点击这个 option 就能够把建立的内容添加到显示结果的 input 上了。
经过为 select 设置 remote
和 remote-method
属性来获取远程数据。remote-method
方法最终将数据赋值给 option 的 v-model 绑定数组数据将结果显示出来便可。
在显示结果的 input 文本框中有一个 <i>
标签,用于显示图标。
<!-- 用户显示清空和向下箭头 --> <i slot="suffix" :class="['el-select__caret', 'el-input__icon', 'el-icon-' + iconClass]" @click="handleIconClick" ></i>
最终 input 右侧显示什么图标由 iconClass
决定,其中 circle-close
就是圆形查查,即清空按钮~
iconClass() { let criteria = this.clearable && !this.selectDisabled && this.inputHovering && !this.multiple && this.value !== undefined && this.value !== ''; return criteria ? 'circle-close is-show-close' : (this.remote && this.filterable ? '' : 'arrow-up'); },
handleIconClick
方法:
// 处理图标点击事件(删除按钮) handleIconClick(event) { if (this.iconClass.indexOf('circle-close') > -1) { this.deleteSelected(event); } }, // 删除选中 deleteSelected(event) { event.stopPropagation(); this.$emit('input', ''); this.emitChange(''); this.visible = false; this.$emit('clear'); },
最终,清空只是将文本清空掉而且关闭下拉菜单。其实当再次打开 select 的时候,option 仍是选中在以前选中的那个位置,即 HoverIndex
没有变为 -1,不知道算不算 bug。
很简单,使用了 slot 插槽。而且在 slot 中定义了默认显示方式。
<slot> <span>{{ currentLabel }}</span> </slot>
第一次尝试用问题取代主题来写博客,这样看着中心是否是更明确一些?
最后,说下看完 select 组件的感觉:
好吧,说好了一天写出来,结果断断续续花了三天才完成。有点高估本身能力啦~说下以后的Vue实验室博客计划:计划再找两个复杂的 element 组件来学习,最后写一篇总结博客。而后试着本身去建立几个 UI 组件,学以至用。