教你一個vue小技巧,通常人我不說的

本文由雲+社區發表css

1. 需求

最近的項目中,須要實如今vue框架中動態渲染帶提示框的單選/多選文本框,具體的效果以下圖所示,在輸入框聚焦時,前端組件經過接收的kv參數渲染出選項,用戶點擊選項,能夠將選擇的選項的key拼裝到輸入框中,同時容許用戶自由輸入。html

img

因爲項目中使用的element-ui,首選考慮使用組件的input和select組件,然而實際使用中發現框架提供的組件不能很好知足此需求。例如,使用帶輸入建議的input組件,可以實現提示框和單選,但並不能方便地實現多選(重複選擇會覆蓋輸入框內的內容)。前端

img

而使用框架提供的select選擇器的遠程搜索功能,可以實現提示框,也能輕鬆實現單選與多選,但select組件的內容只能經過用戶選擇(文本框內容必須包含於提示選項中),不容許用戶自由輸入文本內容。vue

img

再加上設計稿須要實現三列布局,最終的返回結果須要動態拼裝選項key值,若對現有的element組件進行改形成本太高,所以,嘗試封裝帶提示框的單選/多選文本框組件,記錄下封裝過程當中組件交互方面遇到的問題。react

2. 接口參數設計

組件支持傳入6個參數,分別爲element-ui

  1. size (尺寸,String, medium / small / mini)
  2. value (輸入值,String,可使用sync修飾符實現雙向綁定)
  3. opt (選項列表,Array,kv數組形如{key:1, value:xxx})
  4. seperator (分隔符,String,如','、'|'、'-')
  5. multiple (是否支持多選,Boolean)
  6. 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事件執行,致使提示框隱藏後再顯示,形成閃爍dom

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-sidebarfocusEvent () { 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. 組件應用與改進

帶提示框的單選/多選文本框組件的應用場景較多,典型的場景如封裝企業聯繫人的選擇器,用戶輸入用戶名關鍵詞,提示框顯示相關聯繫人,同時容許用戶自由輸入用戶名。

組件還有很多能夠改進的地方,例如:

  1. 目前的設計經過監聽mousedown來阻止提示框的關閉,很明顯不能兼容移動端,能夠考慮添加touch事件;
  2. 在css佈局方面沒有判斷用戶可見的友好性,在極端狀況下可能會超出屏幕範圍;
  3. 還不支持slot插槽和動態設置class等。

隨着總體項目的迭代能夠逐步完善。

此文已由做者受權騰訊雲+社區發佈

相關文章
相關標籤/搜索