面向Vue新人:使用Vue自定義指令來完善一個Select組件

本篇文章教你們寫一個很是簡單的Select組件,想必不少人都寫過Select,畢竟它太經常使用了,可是本篇文章的示例使用到了Vue的自定義指令,若是你對Vue自定義指令不怎麼熟悉的話,本篇文章或許會讓您有所收穫!html

完成的效果圖以下:vue

1、首先,咱們簡單佈局一下:

<template>
  <div class="select">
    <div class="inner">
      <div class="inputWrapper">
        <input type="text" readonly placeholder="請選擇菜品">
        <span class="iconfont icon-zhankaishangxia"></span>
      </div>
      <ul class="options">
        <li v-for="(item, index) in options" :key="index">{{item.value}}</li>
      </ul>
    </div>
  </div>
</template> 

......

data() {
    return {
        options: [
            {
              value: '西紅柿雞蛋'
            },
            {
              value: '青椒抱雞蛋'
            },
            {
              value: '回鍋肉'
            },
            {
              value: '宮保雞丁'
            },
            {
              value: '地三鮮'
            }
        ],
    }
}
複製代碼

效果是這樣:git

下面可供選擇的options用的是絕對定位;同時input設置了readonly,使input變的不可輸入,總體佈局很簡單。github

2、開始添加功能

接下來,咱們要添加兩個功能:express

  • 點擊上面的input框,能夠切換顯示下面的options
  • 選擇options裏的某個選項後讓它展現在input裏,同時讓選項部分消失

這兩項目功能都挺簡單,先來完成第一個,點擊input框切換顯示options,藉助v-show就好。bash

<div class="inputWrapper" @click="showOptions = !showOptions">
    <input type="text" readonly placeholder="請選擇菜品">
    <span class="iconfont icon-zhankaishangxia"></span>
</div>
<ul class="options" v-show="showOptions" v-show="showOptions">  //添加v-show
    <li v-for="(item, index) in options" :key="index">{{item.value}}</li>
</ul>

.......
data() {
    showOptions: false
}
複製代碼

如上所示,在選項裏添加v-show="showOptions"並將showOptions初始化爲false。同時,在包裹inputdiv上添加click事件來回切換showOptions的布爾值。app

效果以下:ide

第二個,點擊下面的選項,將被選擇的展現到input裏,同時讓options消失,也不難。函數

<div class="inputWrapper" @click="showOptions = !showOptions">
    <input type="text" readonly placeholder="請選擇菜品" :value="selected"> //這裏用value綁定一個data值selected
    <span class="iconfont icon-zhankaishangxia"></span>
</div>
<ul class="options" v-show="showOptions">
    <li v-for="(item, index) in options" :key="index" @click="choose(item.value)">{{item.value}}</li>
</ul>

......

data() {
    return {
        ......
        showOptions: false
        selected: ''
    }
},
methods: {
    choose(value) {
        this.showOptions = false
        if (value !== this.selected) {
            this.selected = value
        }
    }
}
複製代碼

邏輯很簡單,在input裏用value綁定一個data值,點擊選擇某個選項後,將選項的內容賦給這個data值便可,同時,隱藏整個選項內容。佈局

效果以下:

從上面的效果圖中能夠看到,已經能夠正常選擇了,可是有一個問題,就是它選項內容展現的時候,咱們但願點擊其它空白的地方也可讓選擇內容隱藏,可是上面的代碼並無解決這個問題,接下來咱們來用兩種辦法來解決它。

三、常規的DOM操做 VS Vue自定義指令

其實,實現這個功能並不難,只是要想解決它就須要操做DOM

<div class="inputWrapper" @click.stop="showOptions = !showOptions"> //注意這裏的stop修飾器
    <input type="text" readonly placeholder="請選擇菜品" :value="selected">
    <span class="iconfont icon-zhankaishangxia"></span>
</div>
<ul class="options" v-show="showOptions">
    <li v-for="(item, index) in options" :key="index" @click.stop="choose(item.value)">{{item.value}}</li>  //還有這裏的stop修飾器
</ul>

...
data() {
    return {
        ......
        showOptions: false
    }
}
mounted() {
    let that = this
    document.addEventListener('click', function() {
        that.showOptions = false
    })
}
複製代碼

上面的代碼有兩點:一個是在mounted後面給整個document添加了點擊事件,這樣在點擊時候就能夠將options隱藏,可是,咱們在點擊輸入框部分和選項內容時,咱們不但願它觸發,而是讓它走咱們以前寫好的邏輯,因此給兩個click事件都添加了stop修飾器來阻止冒泡,這樣,點擊到它們的時候就不會冒泡到document上面了。效果以下:

到這裏基本功能都寫完了,能夠經過添加$emitprops來進行數據傳遞,讓它更加通用些。可是最後關於點擊其它地方讓選項部分消失的功能,咱們還能夠再完善下,能夠考慮使用Vue指令的方式實現。

關於Vue指令,官方文檔裏有比較清楚的說明,若是不是特別明白能夠點擊這裏先看看!

關於Vue自定義指令,在這個例子中須要明白如下基本知識點:

  • 它是用來操做DOM的,因此全部Vue指令都會掛在template裏的某個元素上
  • 它有4個鉤子函數,一是bind,它在指令第一次綁定到元素上調用並且只調用一次,這個鉤子很重要,咱們在這個例子裏會用到;第二個是inserted,它在元素插入到父元素的時候調用,官方文檔裏給了一個v-focus的例子就用到了它;第三個和第四個分別是updatecomponentUpdated,前者是在vNode更新時調用,後者是在更新完成後調用;最後是unbind,在指令和元素解綁時調用。
  • 這4個鉤子函數能夠都至少能夠傳3個參數,第一是el就是被綁定指令的元素,第二個binding它是個對象,並且它的一些屬性特別有用,它的屬性包括nameexpressionvalue等,固然不僅這三個,可是咱們這個例子要用。舉個例子: 假如我寫一個自定義指令v-example="test",而這個test是我在methods裏寫的一個方法,那麼就能夠經過binding.name拿到example字符串,能夠經過binding.value拿到test函數自己而且執行。若是這裏不明白不要緊接下來咱們會說到。

若是仔細觀察,它們很是像Vue自己的生命週期鉤子函數,只是它們是做用在指令上與元素的上的。從bind最開始綁定到最後unbind解綁完成了一個完整的週期。

好了,咱們把以前mounted寫的DOM操做相關的東西都刪掉,開始動手寫一個自定義指令。

<ul class="options" v-show="showOptions" v-clickOut="test"> //這裏使用了下面的自定義指令,並將一個test方法傳遞進去了
    <li v-for="(item, index) in options" :key="index" @click.stop="choose(item.value)">{{item.value}}</li>
</ul>

...
methods: {
    ......
    test() {             //test函數,它做爲參數傳遞給了指令
        console.log('這是一個測試函數')
    }
}, 
directives: {              //這裏是自定義指令
    clickOut: {             // 這裏是自定義的v-clickOut指令
        bind: function(el, binding) {        // bind鉤子函數,當它與元素綁定的時候就會執行
            console.log('el===>', el)
            console.log('binding.name===>', binding.name)
            console.log('binding.expression===>', binding.expression)
            console.log('binding.value===>', binding.value)
        }
    }
}
複製代碼

上面的代碼都有清楚的註釋說明,咱們自定義了一個clickOut的指令,而且把它掛到了一個元素上,並且給它傳了一個test方法,咱們來看看console.log出的東西都是些啥。

從上面的圖片能夠看出當指令和元素綁定的時候即bind的時候,它會執行bind函數得到不少有用的東西,上面咱們講了bind函數裏有幾個重要的參數,從打印出的結果裏咱們很是清楚地看到,el就是指令綁定的元素自己,binding是一個對象,它得到了不少有用的東西,包括傳遞進來的函數。

明白了它的基本構造,咱們就來繼續完善這個指令。

<ul class="options" v-show="showOptions" v-clickOut="test">
    <li v-for="(item, index) in options" :key="index" @click.stop="choose(item.value)">{{item.value}}</li>
</ul>
...

methods: {
    test() {
        this.showOptions = false   
    }
},
directives: {
    clickOut: {
      bind: function(el, binding) {
        document.addEventListener('click', function(e) {
          if (el.contains(e.target)) return false
          if (binding.expression) {
            binding.value()
          }
        })
      }
    }
複製代碼

看下上面改寫過的代碼作了些啥?說下邏輯:當咱們自定的v-clickOut與選項部分的ul元素綁定的時候,咱們監聽document的click事件,若是點擊的元素是被指令綁定的元素的子元素或是被綁定元素自己,那就什麼都不作;若是不是,那就執行傳遞進來的test函數。而test函數執行的結果就是把選項部分隱藏。

邏輯很清楚。

固然咱們能夠繼續完善它。咱們給document.addEventListener了,也能夠在合適的時候removeEventListener,這個合適的時候就是unbind鉤子函數。

因此咱們能夠完善下:

......

directives : {
    clickOut: {
        bind: function(el, binding) {
            function handler(e) {
              if (el.contains(el.target)) return false
              if (binding.expression) {
                binding.value()
              }
            }
            el.handler = handler
            document.addEventListener('click', el.handler)
        },
        unbind: function(el) {
            document.removeEventListener('click', el.handler)
        }        
    }
}

複製代碼

代碼如上,效果以下:

4、補充和修改

看了有朋友在下面的評論,在多個Select時發現仍是有一點問題:

當有多個Select的時候,只有點擊了空白的其它地方的時候,展開的選項才都會消失,當其中一個選項展開再去選擇另外一個的時候,以前展開的選項不會被收起。產生這個問題的緣由是點擊input部分的時候被阻止冒泡了。只要把它去掉同時把指定指令綁定的元素擴大到整個select而不是僅是以前的options部分就好了,由於在指令裏有了攔截。

<template>
  <div class="select">
    <div class="inner" v-clickOut="test">  //指令放到了這裏
      <div class="inputWrapper" @click="showOptions = !showOptions">  //這裏阻止冒泡被去掉了
        <input type="text" readonly placeholder="請選擇菜品" :value="selected">
        <span class="iconfont icon-zhankaishangxia"></span>
      </div>
      <ul class="options" v-show="showOptions">
        <li v-for="(item, index) in options" :key="index" @click="choose(item.value)">{{item.value}}</li>  // 這裏的點擊阻止冒泡也被去掉了
      </ul>
    </div>
  </div>
</template>
複製代碼

效果:

最後把github地址放上:修改過的代碼地址

簡單總結一下:這是一個很是簡單的小例子,由於須要操做DOM,因此咱們選擇使用自定義來完成,固然咱們也可使用其它方法。只是,在咱們用Vue的時候,若是遇到須要操做DOM的時候,那麼能夠想一想可不能夠經過自定義指令來實現呢。這是我在掘金上的第七篇文章,感謝您的閱讀!

相關文章
相關標籤/搜索