本篇文章教大家写一个非常简单的 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 . 同时, 在包裹 input 的 div 上添加 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 上面了. 效果如下:
到这里基本功能都写完了, 可以通过添加 $emit 和 props 来进行数据传递, 让它更加通用些. 但是最后关于点击其它地方让选项部分消失的功能, 我们还可以再完善下, 可以考虑使用 Vue 指令的方式实现.
关于 Vue 指令, 官方文档里有比较清楚的说明, 如果不是特别明白可以点击这里先看看!
关于 Vue 自定义指令, 在这个例子中需要明白以下基本知识点:
它是用来操作 DOM 的, 所以所有 Vue 指令都会挂在 template 里的某个元素上
它有 4 个钩子函数, 一是 bind , 它在指令第一次绑定到元素上调用而且只调用一次, 这个钩子很重要, 我们在这个例子里会用到; 第二个是 inserted , 它在元素插入到父元素的时候调用, 官方文档里给了一个 v-focus 的例子就用到了它; 第三个和第四个分别是 update 和 componentUpdated , 前者是在 vNode 更新时调用, 后者是在更新完成后调用; 最后是 unbind , 在指令和元素解绑时调用.
这 4 个钩子函数可以 都至少可以传 3 个参数 , 第一是 el 就是被绑定指令的元素, 第二个 binding , 它是个对象 , 而且 它的一些属性特别有用 , 它的属性包括 name , expression 和 value 等, 当然不只这三个, 但是我们这个例子要用. 举个例子: 假如我写一个自定义指令 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)
- }
- }
- }
代码如上, 效果如下:
简单总结一下: 这是一个非常简单的小例子, 因为需要操作 DOM, 所以我们选择使用自定义来完成, 当然我们也可以使用其它方法. 只是, 在我们用 Vue 的时候, 如果遇到需要操作 DOM 的时候, 那么可以想想可不可以通过自定义指令来实现呢!
来源: http://www.jb51.net/article/140765.htm