本文由云+社区发表

1. 需求

最近的项目中,需要实现在vue框架中动态渲染带提示框的单选/多选文本框,具体的效果如下图所示,在输入框聚焦时,前端组件通过接收的kv参数渲染出选项,用户点击选项,可以将选择的选项的key拼装到输入框中,同时允许用户自由输入。

由于项目中使用的element-ui,首选考虑使用组件的input和select组件,然而实际使用中发现框架提供的组件不能很好满足此需求。例如,使用带输入建议的input组件,能够实现提示框和单选,但并不能方便地实现多选(重复选择会覆盖输入框内的内容)。

而使用框架提供的select选择器的远程搜索功能,能够实现提示框,也能轻松实现单选与多选,但select组件的内容只能通过用户选择(文本框内容必须包含于提示选项中),不允许用户自由输入文本内容。

再加上设计稿需要实现三列布局,最终的返回结果需要动态拼装选项key值,若对现有的element组件进行改造成本过高,因此,尝试封装带提示框的单选/多选文本框组件,记录下封装过程中组件交互方面遇到的问题。

2. 接口参数设计

组件支持传入6个参数,分别为

  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事件执行,导致提示框隐藏后再显示,造成闪烁

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-sidebar>focusEvent () { 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等。

随着整体项目的迭代可以逐步完善。

此文已由作者授权腾讯云+社区发布


教你一个vue小技巧,一般人我不说的的更多相关文章

  1. vue小技巧之偷懒的文件路径——减少不必要的代码

    众所周知,我们写vue项目的时候都会创建很多个文件,尤其是一些中大型项目,会有很深的文件夹,当你去引入的时候,要写很长的路径比如我要引入一个css文件, 必须得 import  '../../../s ...

  2. 不是吧?30秒 就能学会一个python小技巧?!

    大家好鸭!我是小熊猫 很多学习Python的朋友在项目实战中会遇到不少功能实现上的问题,有些问题并不是很难的问题,或者已经有了很好的方法来解决.当然,孰能生巧,当我们代码熟练了,自然就能总结一些好用的 ...

  3. Vue小技巧-懒加载

    Vue懒加载包括图片懒加载与路由懒加载 1.图片懒加载: 首先安装 vue-lazyload包 然后导入并加载事先下载好的加载图片 import VueLazyLoad from 'vue-lazyl ...

  4. Vue小技巧,如何导入普通JS文件

    最近在开发一个展示3D模型的WEB程序,在工程中使用了VUE和ThreeJS库.Three.js本身是支持CommonJS的,但我们还用到了OBJLoader模块,此模块不支持CommonJS,改成C ...

  5. 11中javascrip教程教不到的小技巧

    1.过滤唯一值 Set对象类型是在ES6中引入的,配合展开操作...一起,我们可以使用它来创建一个新数组,该数组只有唯一的值. 1 const array = [1, 1, 2, 3, 5, 5, 1 ...

  6. dos界面下执行java文件将错误输出到一个文本小技巧

    如果dos下执行java出现错误,把错误记录到一个文档 正确时如图,输出结果为hello,我把String的s改为小写,出现错误,用2>命令输出到error.txt在当前目录就出现了error. ...

  7. [每天一个Linux小技巧] gdb 下一次运行多个命令

    一般gdb运行的时候,我们仅仅能输入一个命令. 如: (gdb) c (gdb) bt 假设想运行多个命令怎么办? 能否像bash那样, 使用; 如 ls; ls 结论是不行. 但能够通过gdb 内建 ...

  8. 一个简单小技巧实现手机访问.html文件网页效果

    注册登录Github不解释 settings设置往下拉 选择一个主题上传代码文件到code 打开这个文件选择此时的网址 在网址前面加上 这段代码 http://htmlpreview.github.i ...

  9. 一个word小技巧

    最近在进行word格式重拍的时候发现了一个有些恶心的事,怎么去匹配文档里面所有的中文呢? 后来通过网络搜索发现了答案,在word中的查找和替换中有一个选项,可以使用通配符进行匹配. 当我们使用 ([一 ...

随机推荐

  1. 冒泡排序&&选择排序

    package cn.lijun.demo;//冒泡排序public class Test5 { public static void main(String[] args) { int[] arr ...

  2. JAVA的环境变量配置

    开发JAVA程序需要先准备开发环境,安装好操作系统后首先需要去下载JDK并安装. JDk(Java Development Kit )是Java开发工具包,如果您要开发基于Java的应用首先需要下载并 ...

  3. 2019.03.26 bzoj4448: [Scoi2015]情报传递(归并排序+树链剖分)

    传送门 题意简述: 给一棵nnn个点的树,树上每个点表示一个情报员,一共有mmm天,每天会派发以下两种任务中的一个任务: 1.搜集情报:指派T号情报员搜集情报 2.传递情报:将一条情报从X号情报员传递 ...

  4. apt-get update 出现错误“ AppStream cache update completed, but some metadata was ignored due to errors. ”

    只需要 执行 sudo rm /var/lib/dpkg/lock;     之后再次执行:sudo apt-get update

  5. ABAP 图形练习(GFW_PRES_SHOW and GRAPH_2D)

    创建屏幕0100(元素清单中含定制控制CONTAINER和OK_CODE) 创建GUI状态100(功能键含BACK和EXIT用于返回和退出 ) 代码 *&------------------- ...

  6. navicat连接mysql出现2059错误

    最近在学习django的时候需要用到数据库,于是便下载了navicat准备和mysql配套使用,但是在连接的时候确出现了如下问题: 网上查询过后,发现这个错误出现的原因是在mysql8之前的版本中加密 ...

  7. 《NoSQL精粹》读后感

    <NoSQL精粹>作者Pramod J. Sadalaga.Martin Flower著,译者爱飞翔. 本书以关系型数据库开头,讲解了关系型数据库的优缺点,然后引入了NoSQL数据库,并且 ...

  8. Codeforces831C Jury Marks

    C. Jury Marks time limit per test 2 seconds memory limit per test 256 megabytes input standard input ...

  9. 在CentOS 7上安装和使用GlusterFS

    GlusterFS aggregates various storage servers over Ethernet or Infiniband RDMA interconnect into one ...

  10. Il laser che è chiaramente visibile

    Prima di quel tempo ho ottenuto questo potente puntatore laser 500mW, non so davvero come questo dis ...