本文由雲+社區發表
1. 需求
最近的項目中,需要實現在vue框架中動態渲染帶提示框的單選/多選文本框,具體的效果如下圖所示,在輸入框聚焦時,前端組件通過接收的kv參數渲染出選項,用戶點擊選項,可以將選擇的選項的key拼裝到輸入框中,同時允許用戶自由輸入。
由於項目中使用的element-ui,首選考慮使用組件的input和select組件,然而實際使用中發現框架提供的組件不能很好滿足此需求。例如,使用帶輸入建議的input組件,能夠實現提示框和單選,但並不能方便地實現多選(重復選擇會覆蓋輸入框內的內容)。
而使用框架提供的select選擇器的遠程搜索功能,能夠實現提示框,也能輕松實現單選與多選,但select組件的內容只能通過用戶選擇(文本框內容必須包含於提示選項中),不允許用戶自由輸入文本內容。
再加上設計稿需要實現三列布局,最終的返回結果需要動態拼裝選項key值,若對現有的element組件進行改造成本過高,因此,嘗試封裝帶提示框的單選/多選文本框組件,記錄下封裝過程中組件交互方面遇到的問題。
2. 接口參數設計
組件支持傳入6個參數,分別為
- size (尺寸,String, medium / small / mini)
- value (輸入值,String,可以使用sync修飾符實現雙向綁定)
- opt (選項列表,Array,kv數組形如{key:1, value:xxx})
- seperator (分隔符,String,如','、'|'、'-')
- multiple (是否支持多選,Boolean)
- placeholder (提示,String)
調用方式如下:
<cs-select
size="mini" // 尺寸
:value.sync="value" // value
:opt="optParams.kv" // 選項
seperator="," // 分隔符
:multiple="true">
</cs-select>
3. 提示框顯示隱藏交互實現
細化上述需求,需要在用戶點擊輸入框(獲取焦點)時,顯示提示框,在用戶點擊空白區域時隱藏提示框,點擊組件自身時不做任何操作。組件的模板結構如下,通過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>
3.1 嘗試方案1: click事件主動聚焦
根據上述需求,毫無疑問聯想到可以為選項綁定click事件,調用el-input的focus()
方法進行主動聚焦,實現如下,此處使用了vue的ref,通過$ref來查找dom元素。
clickEvent () {
this.show = true // 設置提示框顯示
this.$refs.input.$el.querySelector('input').focus() // 設置主動聚焦
}
問題:實際開發過程中發現,每次點擊提示選項后,提示框會閃爍一次,原因在於js的事件機制,blur
事件先於click
事件執行,導致提示框隱藏后再顯示,造成閃爍。
3.2 嘗試方案2: blur事件添加延時器 + 開關變量
由於方案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
...
}
問題:實際開發過程中發現,大多數情況下,提示框能夠顯示與隱藏,但是當操作較快時,會偶爾出現提示框不能關閉或提前關閉的情況,分析原因在於,延時器期間任何對開關的操作可能導致組件開關狀態變化,致使狀態紊亂。
3.3 嘗試方案3: 不使用blur,關閉方法改為事件委托,動態綁定class
如果關閉不使用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:綁定事件過多會帶來性能隱患甚至導致意想不到的問題發生。
3.4 嘗試方案4: onfocus + onblur + mousedown + 開關
由於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
,導致自動聚焦失敗。
3.5 實現方案
在方案4的基礎上,使用nextTick
異步更新隊列能夠解決dom渲染時序問題,具體實現針對方案4稍作修改即可。
$nextTick
: 在vue官方深入響應式原理中說明了 vue 實現響應式並不是數據發生變化之后 DOM 立即變化,而是在下次 DOM 更新循環結束之后執行延遲回調,在修改數據之后使用 \(nextTick,則可以在回調中獲取更新后的 DOM,官方示例:<https://cn.vuejs.org/v2/guide/reactivity.html#search-query-sidebar>focusEvent () { this.show = true this.canClose = true // 聚焦時打開開關 }, blurEvent () { if (this.canClose) { this.show = false // 只有開關打開時才執行關閉 } }, mousedownEvent (key) { this.canClose = false // 點擊提示選項,關閉開關 this.show = true this.\)nextTick(() => { this.\(refs.input.\)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
}
}
5. 組件應用與改進
帶提示框的單選/多選文本框組件的應用場景較多,典型的場景如封裝企業聯系人的選擇器,用戶輸入用戶名關鍵詞,提示框顯示相關聯系人,同時允許用戶自由輸入用戶名。
組件還有不少可以改進的地方,例如:
- 目前的設計通過監聽mousedown來阻止提示框的關閉,很明顯不能兼容移動端,可以考慮添加touch事件;
- 在css布局方面沒有判斷用戶可見的友好性,在極端情況下可能會超出屏幕范圍;
- 還不支持slot插槽和動態設置class等。
隨着整體項目的迭代可以逐步完善。
此文已由作者授權騰訊雲+社區發布