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 組件,學以至用。