使用场景
在一些form表单填写内容的时候,要限制输入的内容必须是数值、浮点型,本来el-input-number就可以实现,但是它本身带那个数值控制操作,等一系列感觉不舒服的地方。如果只是使用el-input该多好,只要监听一下输入的内容就好了。于是就可以考虑使用注册指令的方式,限制输入的内容。
因为只是限制数值和浮点型,数值挺好的,限制浮点类型最好是可以传入参数,限制具体多少位数。
版本环境
vue的版本是^2.6.12,elementui的版本是^2.15.6,使用到Vue.directive()方法,浏览器的requestAnimationFrame方法、cancelAnimationFrame方法,文档对象document的execCommand方法。
文件位置
与index.js同级的文件夹中。src/directive/input.js
引入方式
在入口文件内引用,
import App from './App'
import "@/directive/input.js"
// 省略其他
new Vue({
el: '#app',
router,
store,
render: h => h(App)
})
复制功能实现
Vue.directive("copy", {
bind(el) {
el.$value = el.innerText
el.handler = () => {
const textarea = document.createElement("textarea")
textarea.readOnly = "readonly"
textarea.style.position = "absolute"
textarea.style.left = "-9999px"
textarea.value = el.innerText.trim()
document.body.appendChild(textarea)
textarea.select()
const result = document.execCommand("Copy")
if (result) {
Message.success("复制成功!")
}
document.body.removeChild(textarea)
}
el.addEventListener("click", el.handler)
},
unbind(el) {
el.removeEventListener("click", el.handler)
}
})
注册指令过程中有几个钩子函数
实现原理就是为指令绑定的组件设置一个点击事件,在销毁的时候注销掉那个点击事件。
在dom文档流中添加一个textarea文本域,然后设置一系列的样式使其不会出现在视图窗口中,为它赋值,将其添加到文档流中,然后选中整个文本域的内容。然后调用文档流的document.execCommand("Copy")方法,将文本内容复制出来。
这个复制内容功能做的比较简单,因为复制出来的内容是取的元素的innerText的,所以最好是在那种span标签使用这个指令。
使用方式
<div class="page-head-text">
<span>订单编号:</span>
<el-tooltip content="复制单号" type="light">
<span v-copy class="pointer" style="color: #1890ff">
{{ form.orderSn }}
</span>
</el-tooltip>
</div>
限制整数,浮点数的输入
因为是限制输入类型,因此对于绑定的元素需要进行判断,判断是否是一个Input类型。如果不是的话,就直接通过querySelector去寻找input标签。
需要给指令传递参数,用来区分是想进行整数限制,还是浮点数限制。需要注意的是,这个参数和赋值是两个概念。
v-filter:int 其中的int是一个参数,让我们知道这个输入框限制的是整数。
v-filter:decimals 其中的decimals也是一个参数,让我们知道这个输入框限制的是浮点数。如果我们想设置这个浮点数最多两位呢?在组件里面要怎么用?
这就是参数和赋值的区别。以限制两位浮点数为例
v-filter:decimals="2"
在钩子函数的四个参数之一binding对象中,value对应的是2,arg对应的是decimals
Vue.directive("filter", {
bind(el, binding) {
if (el.tagName.toLowerCase() !== "input") {
el = el instanceof HTMLInputElement ? el : el.querySelector("input")
}
switch (binding.arg) {
case "int":
intFilter(el)
break
case "decimals":
let decimalsFilterO = new DecimalsBinding()
decimalsFilterO.decimalsFilter(el, binding)
break
}
},
unbind(el, binding) {
// 解除事件监听
switch (binding.arg) {
case "int":
el.removeEventListener("input", intFilterEx, true)
break
case "decimals":
el.removeEventListener("input", decimalsFilterEx, true)
break
default:
break
}
}
})
整数限制
其中intFilter实现
const intFilter = function (el) {
el.addEventListener("input", intFilterEx, true)
}
const intFilterEx = e => {
e.target.removeEventListener("input", intFilterEx, true)
let stop = requestAnimationFrame(() => {
let num = e.target.value.replace(/\D/g, "").replace(".", "")
e.target.value = ""
e.target.value = num.replace(/^0([0-9])/g, "$1") || 0
e.target.dispatchEvent(new Event("input"))
e.target.addEventListener("input", intFilterEx, true)
window.cancelAnimationFrame(stop)
}, 1)
}
requestAnimationFrame是浏览器的一个事件,在mdn中有详细的解释
https://developer.mozilla.org/en-US/docs/Web/API/Window/requestAnimationFrame
浮点数限制
function decimalsFilterEx(e) {
e.target.removeEventListener("input", this.decimalsFilterEx, true)
let re = new RegExp(`^\\D*([0-9]\\d*\\.?\\d{0,${this.decimalsBinding.value}})?.*$`)
let stop = requestAnimationFrame(() => {
e.target.value = e.target.value.replace(re, "$1").replace(/^0([0-9])/g, "$1")
e.target.dispatchEvent(new Event("input"))
e.target.addEventListener("input", this.decimalsFilterEx, true)
window.cancelAnimationFrame(stop)
}, 1)
}
class DecimalsBinding {
decimalsBinding = {}
decimalsFilter = function (el, binding) {
this.decimalsBinding = binding
el.addEventListener("input", this.decimalsFilterEx, true)
}
decimalsFilterEx = decimalsFilterEx.bind(this)
}
实现方式
第一步,创建一个对象,DecimalsBinding对象有三个属性。decimalsBinding用来存储钩子函数中的binding值信息;decimalsFilter用来设置对象存储的binging值,以及为el添加input事件监听;decimalsFilterEx是事件的执行内容。
这里有一些绕,因为在整个input.js文件中,定义了一个decimalsFilterEx函数方法。而且DecimalsBinding里面也有一个同名的属性。
在new DecimalsBinding()创建实例的时候,对象的decimalsFilterEx与input.js文件中的decimalsFilterEx是同一个,因为bind方法改变了外围的decimalsFilterEx的this指向。此时decimalsFilterEx里面的this指向的是实体类
第二步 decimalsFilter为输入框绑定input事件,在这个事件中,先移除原先绑定的监听事件,设定正则校验,拿到输入框的值,使用replace替换里面的值。
然后重新绑定input事件
requestAnimationFrame是一个异步方法,像setTimeOut一样,会返回一个ID值,用来清除这个执行。
requestAnimationFrame的刷新频率和浏览器的渲染频率一样。因此几乎是无感限制。其他监听方法会有几帧显示输入的内容,然后立马消失。这种方法会让用户体验更好一点。
最后就是cancelAnimationFrame方法,清除这个渲染。因为是嵌套设置,如果不清除的话,就像是在setInterval里面再次调用setInterval一样。
完整文件代码
import { Message } from "element-ui"
import Vue from "vue"
const intFilter = function (el) {
el.addEventListener("input", intFilterEx, true)
}
const intFilterEx = e => {
e.target.removeEventListener("input", intFilterEx, true)
let stop = requestAnimationFrame(() => {
let num = e.target.value.replace(/\D/g, "").replace(".", "")
e.target.value = ""
e.target.value = num.replace(/^0([0-9])/g, "$1") || 0
e.target.dispatchEvent(new Event("input"))
e.target.addEventListener("input", intFilterEx, true)
window.cancelAnimationFrame(stop)
}, 1)
}
function decimalsFilterEx(e) {
e.target.removeEventListener("input", this.decimalsFilterEx, true)
let re = new RegExp(`^\\D*([0-9]\\d*\\.?\\d{0,${this.decimalsBinding.value}})?.*$`)
let stop = requestAnimationFrame(() => {
e.target.value = e.target.value.replace(re, "$1").replace(/^0([0-9])/g, "$1")
e.target.dispatchEvent(new Event("input"))
e.target.addEventListener("input", this.decimalsFilterEx, true)
window.cancelAnimationFrame(stop)
}, 1)
}
class DecimalsBinding {
decimalsBinding = {}
decimalsFilter = function (el, binding) {
this.decimalsBinding = binding
el.addEventListener("input", this.decimalsFilterEx, true)
}
decimalsFilterEx = decimalsFilterEx.bind(this)
}
Vue.directive("copy", {
bind(el) {
el.$value = el.innerText
el.handler = () => {
const textarea = document.createElement("textarea")
textarea.readOnly = "readonly"
textarea.style.position = "absolute"
textarea.style.left = "-9999px"
textarea.value = el.innerText.trim()
document.body.appendChild(textarea)
textarea.select()
const result = document.execCommand("Copy")
if (result) {
Message.success("复制成功!")
}
document.body.removeChild(textarea)
}
el.addEventListener("click", el.handler)
},
unbind(el) {
el.removeEventListener("click", el.handler)
}
})
Vue.directive("filter", {
bind(el, binding) {
if (el.tagName.toLowerCase() !== "input") {
el = el instanceof HTMLInputElement ? el : el.querySelector("input")
}
switch (binding.arg) {
case "int":
intFilter(el)
break
case "decimals":
let decimalsFilterO = new DecimalsBinding()
decimalsFilterO.decimalsFilter(el, binding)
break
}
},
unbind(el, binding) {
// 解除事件监听
switch (binding.arg) {
case "int":
el.removeEventListener("input", intFilterEx, true)
break
case "decimals":
el.removeEventListener("input", decimalsFilterEx, true)
break
default:
break
}
}
})
export default Vue