阅读 5630

面向Vue新人:使用Vue自定义指令来完善一个Select组件

本篇文章教大家写一个非常简单的Select组件,想必很多人都写过Select,毕竟它太常用了,但是本篇文章的示例使用到了Vue的自定义指令,如果你对Vue自定义指令不怎么熟悉的话,本篇文章或许会让您有所收获!

完成的效果图如下:

一、首先,我们简单布局一下:

<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: '地三鲜'
            }
        ],
    }
}
复制代码

效果是这样:

下面可供选择的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的布尔值。

效果如下:

第二个,点击下面的选项,将被选择的展示到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值即可,同时,隐藏整个选项内容。

效果如下:

从上面的效果图中可以看到,已经可以正常选择了,但是有一个问题,就是它选项内容展示的时候,我们希望点击其它空白的地方也可以让选择内容隐藏,但是上面的代码并没有解决这个问题,接下来我们来用两种办法来解决它。

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上面了。效果如下:

到这里基本功能都写完了,可以通过添加$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)
        }        
    }
}

复制代码

代码如上,效果如下:

四、补充和修改

看了有朋友在下面的评论,在多个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的时候,那么可以想想可不可以通过自定义指令来实现呢。这是我在掘金上的第七篇文章,感谢您的阅读!

文章分类
前端
文章标签