本文由雲+社區發表css
最近的項目中,須要實如今vue框架中動態渲染帶提示框的單選/多選文本框,具體的效果以下圖所示,在輸入框聚焦時,前端組件經過接收的kv參數渲染出選項,用戶點擊選項,能夠將選擇的選項的key拼裝到輸入框中,同時容許用戶自由輸入。html
因爲項目中使用的element-ui,首選考慮使用組件的input和select組件,然而實際使用中發現框架提供的組件不能很好知足此需求。例如,使用帶輸入建議的input組件,可以實現提示框和單選,但並不能方便地實現多選(重複選擇會覆蓋輸入框內的內容)。前端
而使用框架提供的select選擇器的遠程搜索功能,可以實現提示框,也能輕鬆實現單選與多選,但select組件的內容只能經過用戶選擇(文本框內容必須包含於提示選項中),不容許用戶自由輸入文本內容。vue
再加上設計稿須要實現三列布局,最終的返回結果須要動態拼裝選項key值,若對現有的element組件進行改形成本太高,所以,嘗試封裝帶提示框的單選/多選文本框組件,記錄下封裝過程當中組件交互方面遇到的問題。react
組件支持傳入6個參數,分別爲element-ui
調用方式以下:設計模式
<cs-select
size="mini" // 尺寸
:value.sync="value" // value
:opt="optParams.kv" // 選項
seperator="," // 分隔符
:multiple="true">
</cs-select> 複製代碼
細化上述需求,須要在用戶點擊輸入框(獲取焦點)時,顯示提示框,在用戶點擊空白區域時隱藏提示框,點擊組件自身時不作任何操做。組件的模板結構以下,經過show變量控制提示框的顯示與隱藏,在組件的輸入框綁定聚焦和失焦事件: @focus="onfocus"
和 @blur="onblur"
,在focus時設置this.show爲true,blur時爲false,因爲點擊了輸入框外的選項元素必然致使輸入框失焦從而自動關閉,全部問題的關鍵在於如何實現點擊提示選項而不隱藏提示框。數組
<template>
<div>
<!-- 輸入框 -->
<el-input
@focus="onfocus
@blur="onblur>
</el-input>
<!-- 提示框 -->
<div v-if="show && opt.length > 0">
<el-row>
<el-col :span="8" v-for="(item, index) in opt" :key="index">
{{item.value}}
</el-col>
</el-row>
</div>
</div>
</template>
複製代碼
根據上述需求,毫無疑問聯想到能夠爲選項綁定click事件,調用el-input的focus()
方法進行主動聚焦,實現以下,此處使用了vue的ref,經過$ref來查找dom元素。框架
clickEvent () {
this.show = true // 設置提示框顯示
this.$refs.input.$el.querySelector('input').focus() // 設置主動聚焦
}
複製代碼
問題:實際開發過程當中發現,每次點擊提示選項後,提示框會閃爍一次,緣由在於js的事件機制,blur
事件先於click
事件執行,致使提示框隱藏後再顯示,形成閃爍。dom
因爲方案1blur
事件先於click
事件執行,所以考慮使用settimeout
延時器來改變執行時間,實現以下。
blurEvent () {
setTimeout(() => {
this.show = false
}, 200)
}
複製代碼
**問題:**實際開發過程當中發現,延時器延時執行關閉操做,致使輸入框獲取焦點後,主動關閉了提示框,再也不自動打開,不知足需求,所以考慮使用開關變量canClose
判斷當前是否須要執行關閉,實現以下。
focusEvent () {
this.show = true
this.canClose = true // 聚焦時打開開關
},
blurEvent () {
if (this.canClose) {
setTimeout(() => {
this.show = false // 只有開關打開時才執行關閉
}, 200)
}
},
clickEvent (key) {
this.canClose = false // 點擊提示選項,關閉開關
this.show = true
...
}
複製代碼
問題:實際開發過程當中發現,大多數狀況下,提示框可以顯示與隱藏,可是當操做較快時,會偶爾出現提示框不能關閉或提早關閉的狀況,分析緣由在於,延時器期間任何對開關的操做可能致使組件開關狀態變化,導致狀態紊亂。
若是關閉不使用blur,而是經過點擊事件觸發,則不會存在上述時序問題,所以考慮在全局使用事件委託,監聽用戶的點擊事件,經過判斷節點特殊class實現提示框關閉,實現以下。
$('body').on('click', (event) => {
this.show = false
})
$('body').on('click', className, (event) => {
this.show = true
})
複製代碼
問題1:事件委託,使用固定的class,當同時渲染多個組件時,沒法實現單獨管理提示框的開關,所以沒法渲染多組件,所以class使用動態綁定,每一個組件使用不一樣的class,實現以下。
**問題2:**阻止冒泡,若是組件的父容器阻止了冒泡,則沒法觸發body上綁定的關閉方法,須要針對父容器單獨處理。
let randId = Math.round(Math.random()*100000)
this.className = `cs-select-${randId}`
// 單獨處理父容器,在父容器上綁定關閉事件
...
複製代碼
改造後的組件表面看起來已經基本可用,實際存在諸多問題:
**問題1:**組件中對父組件綁定了事件,違反了設計模式的迪米特法則,增長了組件間的耦合,不利於後期維護。
**問題2:**上述操做只考慮了點擊事件的關閉,忽略了其餘可能關閉的狀況,如使用tab
按鍵切換輸入框時也須要能正常顯示隱藏提示框。
**問題3:**綁定事件過多會帶來性能隱患甚至致使意想不到的問題發生。
因爲focus事件先於click事件執行,致使了上述方案1和方案2問題的產生,經過查閱資料可知,mousedown
事件先於focus事件執行,所以,使用onfocus + onblur + mousedown + 開關可以很好解決上述執行時序問題,具體實現以下。
focusEvent () {
this.show = true
this.canClose = true // 聚焦時打開開關
},
blurEvent () {
if (this.canClose) {
this.show = false // 只有開關打開時才執行關閉
}
},
mousedownEvent (key) {
this.canClose = false // 點擊提示選項,關閉開關
this.show = true
this.$refs.input.$el.querySelector('input').focus()
...
}
複製代碼
**問題:**實際開發中發現,因爲組件是動態渲染的,mousedownEvent事件中沒法直接獲取到當前對象的dom元素this.$refs.xxx
,致使自動聚焦失敗。
在方案4的基礎上,使用nextTick
異步更新隊列可以解決dom渲染時序問題,具體實現針對方案4稍做修改便可。
$nextTick
: 在vue官方深刻響應式原理中說明了 vue 實現響應式並非數據發生變化以後 DOM 當即變化,而是在下次 DOM 更新循環結束以後執行延遲迴調,在修改數據以後使用nextTick(() => { this.
el.querySelector('input').focus() }) ... }4. 組件數據雙向綁定
爲了方便組件內數據的處理,傳入組件的輸入值value會首先被split分解爲key數組,而後添加watcher觀察器,監聽輸入值的變化,更新提示框的選中狀態,並經過$emit
方法同步到父組件中,實現數據的雙向綁定,輸入值的watch以下所示:
watch: {
inputVal: {
handler () {
let selectArray = this.inputFilter()
this.inputVal = selectArray.join(this.seperator)
// 更新選中狀態
this.updateActive()
// 同步數據
this.$emit('update:value', this.inputVal) // 可改成v-model
},
immediate: true
}
}
複製代碼
帶提示框的單選/多選文本框組件的應用場景較多,典型的場景如封裝企業聯繫人的選擇器,用戶輸入用戶名關鍵詞,提示框顯示相關聯繫人,同時容許用戶自由輸入用戶名。
組件還有很多能夠改進的地方,例如:
隨着總體項目的迭代能夠逐步完善。
此文已由做者受權騰訊雲+社區發佈