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

limingru發表於2018-05-23

本篇文章教大家寫一個非常簡單的Select元件,想必很多人都寫過Select,畢竟它太常用了,但是本篇文章的示例使用到了Vue的自定義指令,如果你對Vue自定義指令不怎麼熟悉的話,本篇文章或許會讓您有所收穫!

完成的效果圖如下:

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

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

<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: '地三鮮'
            }
        ],
    }
}
複製程式碼

效果是這樣:

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

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

二、開始新增功能

接下來,我們要新增兩個功能:

  • 點選上面的input框,可以切換顯示下面的options
  • 選擇options裡的某個選項後讓它展示在input裡,同時讓選項部分消失

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

<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的布林值。

效果如下:

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

第二個,點選下面的選項,將被選擇的展示到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值即可,同時,隱藏整個選項內容。

效果如下:

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

從上面的效果圖中可以看到,已經可以正常選擇了,但是有一個問題,就是它選項內容展示的時候,我們希望點選其它空白的地方也可以讓選擇內容隱藏,但是上面的程式碼並沒有解決這個問題,接下來我們來用兩種辦法來解決它。

3、常規的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上面了。效果如下:

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

到這裡基本功能都寫完了,可以通過新增$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出的東西都是些啥。

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

從上面的圖片可以看出當指令和元素繫結的時候即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)
        }        
    }
}

複製程式碼

程式碼如上,效果如下:

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

四、補充和修改

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

面向Vue新人:使用Vue自定義指令來完善一個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>
複製程式碼

效果:

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

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

簡單總結一下:這是一個非常簡單的小例子,因為需要操作DOM,所以我們選擇使用自定義來完成,當然我們也可以使用其它方法。只是,在我們用Vue的時候,如果遇到需要操作DOM的時候,那麼可以想想可不可以通過自定義指令來實現呢。這是我在掘金上的第七篇文章,感謝您的閱讀!

相關文章