vue directives自定义指令的使用
directiveCom.vue:<template><div>数量:<input :value="myNum" v-check-num="{key:'myNum',maxval:'1000',minval:'100'}" /><div v-show="show">测试指令</div><button @click="toggle"&
有的情况下,需要对普通 DOM 元素进行底层操作,这时候就会用到自定义指令。
自定义指令使用情景:
1.按钮级别权限的控制。
2.按钮的波纹动态效果。
3.一键copy的功能。
4.输入框自动聚焦。
5.下拉菜单,点击下拉菜单以外的地方时隐藏下拉菜单。
6.时间转换,比如朋友圈发布动态后的相对时间,比如刚刚、两分钟前等等。
7.输入框的最小值和最大值限制。
一:自定义指令有全局注册指令和局部注册指令两种方式:
全局注册指令:
Vue.directive('focus',{
bind:function(){},
inserted:function(){},
update:function(){},
componentUpdated:function(){},
unbind:function(){}
});
局部注册指令:在.vue文件中使用directives属性:
directives:{
focus:{
bind:function(){},
inserted:function(){},
update:function(){},
componentUpdated:function(){},
unbind:function(){}
}
}
注册指令成功后,直接在dom元素上使用v-focus。
<input v-focus/>
二:注册指令的使用。
下面以局部注册指令来举例子。
自定义指令有4个钩子函数,v-check-num="{key:'myNum',maxval:1000,minval:100}"
钩子函数需要用到的参数解析:
1.el:指令所绑定的元素。
2.binding:绑定对象。属性包含
name(指令名),不包括v-,此例子中为check-num。
value(计算后的指令所绑定的值),此例子中为{key:'myNum',maxval:1000,minval:100}。
oldValue(指令所绑定的前一个值,仅在update和componentUpdated中可用)。
express(绑定的值的字符串形式),此例子中为'{key:'myNum',maxval:1000,minval:100}'。
arg(传递给指令的参数),v-check-num:a中的arg为a。
modifiers(修饰符对象),v-my-directive.foo.bar中的modifiers为{foo:true,bar:true}。
3.vnode:编译生成的虚拟节点。属性有context为虚拟节点的上下文。
注意:el可读可写,其他参数只读。如果需要在钩子函数之间共享数据,可通过dataset来实现。
4.oldVnode:上一个虚拟节点。
directiveCom.vue:
<template>
<div>
<p>checkNum指令</p>
<label>数量:</label>
<input :value="myNum" v-check-num="{key:'myNum',maxval:1000,minval:100}" v-if="show" />
<button @click="toggle">切换一下show</button>
</div>
</template>
<script>
export default {
name: "directiveCom",
data: () => {
return {
show: true,
myNum: "" //奖品总数
};
},
methods: {
toggle() {
this.show = !this.show;
}
},
directives: {
checkNum: {
//只调用一次,第一次绑定指令到元素上时调用,可在此生命周期内做一些初始化的操作
bind: function() {
console.log("bind");
},
//被绑定元素插入父节点时调用
inserted: function() {
console.log("inserted");
},
//被绑定元素所在的模板被更新时即可调用
update: function() {
console.log("update");
},
//被绑定元素所在的模板完成一次更新周期时调用
componentUpdated: function() {
console.log("componentUpdated");
},
//指令与元素解绑的时候调用
unbind: function() {
console.log("unbind");
}
}
}
};
</script>
<style>
</style>
在App.vue里面引用上面的指令组件:
<template>
<div id="app">
<directive-com/>
</div>
</template>
<script>
import directiveCom from './components/directiveCom'
export default {
name: "App",
components:{
directiveCom
}
};
</script>
<style>
#app {
font-family: Avenir, Helvetica, Arial, sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
text-align: center;
color: #2c3e50;
margin-top: 60px;
}
</style>
效果图:
页面渲染时,触发了bind和inserted函数。
点击按钮,效果图:
此时,改变了directiveCom组件里的dom元素的css样式,触发了update和componentUpdated函数。
将directiveCom中的v-show改为v-if:
<template>
<div>
<p>checkNum指令</p>
<label>数量:</label>
<input :value="myNum" v-check-num="{key:'myNum',maxval:1000,minval:100}" v-if="show" />
<button @click="toggle">切换一下show</button>
</div>
</template>
<script>
export default {
name: "directiveCom",
data: () => {
return {
show: true,
myNum: "" //奖品总数
};
},
methods: {
toggle() {
this.show = !this.show;
}
},
directives: {
checkNum: {
//只调用一次,第一次绑定指令到元素上时调用,可在此生命周期内做一些初始化的操作
bind: function() {
console.log("bind");
},
//被绑定元素插入父节点时调用
inserted: function() {
console.log("inserted");
},
//被绑定元素所在的模板被更新时即可调用
update: function() {
console.log("update");
},
//被绑定元素所在的模板完成一次更新周期时调用
componentUpdated: function() {
console.log("componentUpdated");
},
//指令与元素解绑的时候调用
unbind: function() {
console.log("upbind");
}
}
}
};
</script>
<style>
</style>
点击按钮,效果如下:
v-if是dom组件的的销毁和创建,指令与元素解绑,此时触发了unbind函数。
三:进阶:使用自定义指令来设置输入框的最小值和最大值规则。规则:如果输入的值<最小值,那么默认为最小值;如果输入的值>最大值,那么默认为最大值;如果输入的是非数字,则清空输入框,默认为空。
directiveCom组件:
<template>
<div>
<p>checkNum指令</p>
<label>数量:</label>
<input :value="myNum" v-check-num="{key:'myNum',maxval:1000,minval:100}" v-if="show" />
<button @click="toggle">切换一下show</button>
</div>
</template>
<script>
export default {
name: "directiveCom",
data: () => {
return {
show: true,
myNum: "" //奖品总数
};
},
methods: {
toggle() {
this.show = !this.show;
}
},
directives: {
checkNum: {
//只调用一次,第一次绑定指令到元素上时调用,可在此生命周期内做一些初始化的操作
bind: function(el, binding, vnode) {
el.handler = function() {
if (!Number(el.value)) {
el.value = "";
return false;
}
const value = Number(el.value);
if (binding.value.maxval && value > binding.value.maxval) {
el.value = binding.value.maxval;
}
if (binding.value.minval && value < binding.value.minval) {
el.value = binding.value.minval;
}
//将el.val的值赋值给myNum
vnode["context"][binding.value.key] = el.value;
};
el.addEventListener("change", el.handler);
console.log("bind");
},
//被绑定元素插入父节点时调用
inserted: function() {
console.log("inserted");
},
//被绑定元素所在的模板被更新时即可调用
update: function() {
console.log("update");
},
//被绑定元素所在的模板完成一次更新周期时调用
componentUpdated: function() {
console.log("componentUpdated");
},
//指令与元素解绑的时候调用
unbind: function() {
console.log("unbind");
}
}
}
};
</script>
<style>
</style>
四:用自定义指令来实现几种需求场景:
git链接:https://github.com/xiaoli0510/vue-directive
directive.vue:
<template>
<div>
<div>
<p>1.validBtn指令:按钮级别权限的控制</p>
<button v-valid-btn="'viewBtn'">查看按钮</button>
<br />
<!-- 通过ajax获取的用户按钮权限数组里面没有editBtn,所以编辑按钮被移除 -->
<button v-valid-btn="'editBtn'">编辑按钮</button>
</div>
<div>
<p>2.waves指令:按钮的波纹动态效果</p>
<button v-waves>点击有波纹效果的按钮</button>
</div>
<div>
<p>3.copy指令:一键copy的功能</p>
<button v-copy="'copyText'">点击一键复制</button>
</div>
<div>
<p>4.focus指令:输入框自动聚焦</p>
<input type="text" v-focus />
</div>
<div>
<p>5.clickoutside指令:下拉菜单,点击下拉菜单以外的地方时隐藏下拉菜单</p>
<div class="main" v-clickoutside="handleClose">
<button @click="showDrop =!showDrop">点击显示下拉菜单</button>
<div class="dropdown" v-show="showDrop">
<p>我是下拉框的内容,点击外部区域可以关闭</p>
</div>
</div>
</div>
<div>
<p>7.time指令:时间转换,比如朋友圈发布动态后的相对时间,比如刚刚、两分钟前等等</p>
<div v-time="timeNow"></div>
</div>
<div>
<p>8.checkNum指令:输入框的最小值和最大值限制</p>
<label>数量:</label>
<input :value="myNum" v-check-num="{key:'myNum',maxval:1000,minval:100}" />
</div>
</div>
</template>
<script>
export default {
name: "directiveCom",
data: () => {
return {
showDrop: false,
myNum: "",
timeNow: new Date().getTime()
};
},
methods: {
handleClose() {
this.showDrop = false;
}
},
directives: {
//输入框的最小值和最大值限制
checkNum: {
//指令第一次绑定在元素上时调用,只调用一次
bind: function(el, binding, vnode) {
el._handler_ = function() {
let value = el.value;
if (!Number(value)) {
el.value = "";
return false;
}
value = Number(value);
if (binding.value.maxval && value > binding.value.maxval) {
el.value = binding.value.maxval;
}
if (binding.value.minval && value < binding.value.minval) {
el.value = binding.value.minval;
}
vnode["context"][binding.value.key] = el.value;
};
el.addEventListener("change", el._handler_);
},
//被绑定的元素插入父节点时调用
inserted: function() {},
//被绑定的元素所在的模板发送更新时,比如style或者内容改变的时候调用
update: function() {},
//所在的模板完成一次更新后调用
componentUpdated: function() {},
//指令与元素解绑的时候调用
unbind: function(el) {
delete el._handler_;
}
},
//按钮级别权限的控制
validBtn: {
inserted: function(el, binding) {
el.handler = function(value) {
//此处模拟的是ajax获取的用户权限按钮
const btnArr = ["viewBtn"];
for (var item of btnArr) {
if (item === value) {
return true;
}
}
//如果用户没有此按钮权限,则移除此按钮
el.parentNode.removeChild(el);
return false;
};
el.handler(binding.value);
},
unbind: function(el) {
delete el.handler;
}
},
focus: {
inserted: function(el) {
el.focus();
}
},
clickoutside: {
bind: function(el, binding) {
function documentHandler(e) {
//如果点击的是当前的下拉框元素 则不做处理
if (el.contains(e.target)) {
return false;
}
//如果v-clickoutside后面有表达式 则执行后面的函数 此例子中执行handleClose函数
if (binding.expression) {
binding.value(e);
}
}
el.__vueClickOutside__ = documentHandler;
document.addEventListener("click", documentHandler);
},
unbind: function(el) {
document.removeEventListener("click", el.__vueClickOutside__);
delete el.__vueClickOutside__;
}
}
}
};
</script>
<style>
button {
margin-bottom: 10px;
}
</style>
先上效果图:
checkNum、validBtn、focus、clickoutside是局部注册指令。
waves、copy、time是全局注册指定。
time.js:
var Time = {
//获取当前时间戳
getUnix: function () {
var date = new Date();
return date.getTime();
},
//获取今天0点0分0秒的时间戳
getTodayUnix: function () {
var date = new Date();
date.setHours(0);
date.setMinutes(0);
date.setSeconds(0);
date.setMilliseconds(0);
return date.getTime();
},
//获取今年1月1日0点0分0秒的时间戳
getYearUnix: function () {
var date = new Date();
date.setMonth(0);
date.setDate(1);
date.setHours(0);
date.setMinutes(0);
date.setSeconds(0);
date.setMilliseconds(0);
return date.getTime();
},
//获取标准年月日
getLastDate: function (time) {
var date = new Date(time);
var month = date.getMonth() + 1 < 10 ? '0' + (date.getMonth() + 1) : date.getMonth() + 1;
var day = date.getDate() < 10 ? '0' + date.getDate() : date.getDate();
return date.getFullYear() + '-' + month + '-' + day;
},
//转换时间
getFormatTime: function (timestamp) {
var now = this.getUnix(); //当前时间戳
var today = this.getTodayUnix(); //今天0点时间戳
//var year = this.getYearUnix(); //今年0点时间戳
var timer = (now - timestamp) / 1000; //转换为妙级别时间戳
var tip = '';
if (timer <= 0) {
tip = '刚刚';
} else if (Math.floor(timer / 60) <= 0) {
tip = '刚刚';
} else if (timer < 3600) {
tip = Math.floor(timer / 60) + '分钟前';
} else if (timer >= 3600 && (timestamp - today >= 0)) {
tip = Math.floor(timer / 3600) + '小时前';
} else if (timer / 86400 <= 31) {
tip = Math.ceil(timer / 86400) + '天前';
} else {
tip = this.getLastDate(timestamp);
}
return tip;
}
};
export default {
bind: function (el, binding) {
el.innerHTML = Time.getFormatTime(binding.value);
el._timeout_ = setInterval(function () {
el.innerHTML = Time.getFormatTime(binding.value);
}, 1000);
},
unbind:function(el){
clearInterval(el._timeout_);
delete el._timeout_;
}
}
copy.js:
let listenAction
export default {
inserted(el, binding) {
const params = binding.value || {}
const stickyTop = params.stickyTop || 0
const zIndex = params.zIndex || 1000
const elStyle = el.style
elStyle.position = '-webkit-sticky'
elStyle.position = 'sticky'
// if the browser support css sticky(Currently Safari, Firefox and Chrome Canary)
// if (~elStyle.position.indexOf('sticky')) {
// elStyle.top = `${stickyTop}px`;
// elStyle.zIndex = zIndex;
// return
// }
const elHeight = el.getBoundingClientRect().height
const elWidth = el.getBoundingClientRect().width
elStyle.cssText = `top: ${stickyTop}px; z-index: ${zIndex}`
const parentElm = el.parentNode || document.documentElement
const placeholder = document.createElement('div')
placeholder.style.display = 'none'
placeholder.style.width = `${elWidth}px`
placeholder.style.height = `${elHeight}px`
parentElm.insertBefore(placeholder, el)
let active = false
const getScroll = (target, top) => {
const prop = top ? 'pageYOffset' : 'pageXOffset'
const method = top ? 'scrollTop' : 'scrollLeft'
let ret = target[prop]
if (typeof ret !== 'number') {
ret = window.document.documentElement[method]
}
return ret
}
const sticky = () => {
if (active) {
return
}
if (!elStyle.height) {
elStyle.height = `${el.offsetHeight}px`
}
elStyle.position = 'fixed'
elStyle.width = `${elWidth}px`
placeholder.style.display = 'inline-block'
active = true
}
const reset = () => {
if (!active) {
return
}
elStyle.position = ''
placeholder.style.display = 'none'
active = false
}
const check = () => {
const scrollTop = getScroll(window, true)
const offsetTop = el.getBoundingClientRect().top
if (offsetTop < stickyTop) {
sticky()
} else {
if (scrollTop < elHeight + stickyTop) {
reset()
}
}
}
listenAction = () => {
check()
}
window.addEventListener('scroll', listenAction)
},
unbind() {
window.removeEventListener('scroll', listenAction)
}
}
waves.js:
import './waves.css'
const context = '@@wavesContext'
function handleClick(el, binding) {
function handle(e) {
const customOpts = Object.assign({}, binding.value)
const opts = Object.assign({
ele: el, // 波纹作用元素
type: 'hit', // hit 点击位置扩散 center中心点扩展
color: 'rgba(0, 0, 0, 0.15)' // 波纹颜色
},
customOpts
)
const target = opts.ele
if (target) {
target.style.position = 'relative'
target.style.overflow = 'hidden'
const rect = target.getBoundingClientRect()
let ripple = target.querySelector('.waves-ripple')
if (!ripple) {
ripple = document.createElement('span')
ripple.className = 'waves-ripple'
ripple.style.height = ripple.style.width = Math.max(rect.width, rect.height) + 'px'
target.appendChild(ripple)
} else {
ripple.className = 'waves-ripple'
}
switch (opts.type) {
case 'center':
ripple.style.top = rect.height / 2 - ripple.offsetHeight / 2 + 'px'
ripple.style.left = rect.width / 2 - ripple.offsetWidth / 2 + 'px'
break
default:
ripple.style.top =
(e.pageY - rect.top - ripple.offsetHeight / 2 - document.documentElement.scrollTop ||
document.body.scrollTop) + 'px'
ripple.style.left =
(e.pageX - rect.left - ripple.offsetWidth / 2 - document.documentElement.scrollLeft ||
document.body.scrollLeft) + 'px'
}
ripple.style.backgroundColor = opts.color
ripple.className = 'waves-ripple z-active'
return false
}
}
if (!el[context]) {
el[context] = {
removeHandle: handle
}
} else {
el[context].removeHandle = handle
}
return handle
}
export default {
bind(el, binding) {
el.addEventListener('click', handleClick(el, binding), false)
},
update(el, binding) {
el.removeEventListener('click', el[context].removeHandle, false)
el.addEventListener('click', handleClick(el, binding), false)
},
unbind(el) {
el.removeEventListener('click', el[context].removeHandle, false)
el[context] = null
delete el[context]
}
}
waves.css:
.waves-ripple {
position: absolute;
border-radius: 100%;
background-color: rgba(0, 0, 0, 0.15);
background-clip: padding-box;
pointer-events: none;
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
-webkit-transform: scale(0);
-ms-transform: scale(0);
transform: scale(0);
opacity: 1;
}
.waves-ripple.z-active {
opacity: 0;
-webkit-transform: scale(2);
-ms-transform: scale(2);
transform: scale(2);
-webkit-transition: opacity 1.2s ease-out, -webkit-transform 0.6s ease-out;
transition: opacity 1.2s ease-out, -webkit-transform 0.6s ease-out;
transition: opacity 1.2s ease-out, transform 0.6s ease-out;
transition: opacity 1.2s ease-out, transform 0.6s ease-out, -webkit-transform 0.6s ease-out;
}
在main.js中引入js文件,再进行全局注册:
main.js:
import Vue from 'vue'
import App from './App.vue'
Vue.config.productionTip = false
import waves from './directive/waves/waves.js'
Vue.directive('waves', waves)
import copy from './directive/copy.js'
Vue.directive('copy', copy)
import time from './directive/time.js'
Vue.directive('time', time)
new Vue({
render: h => h(App),
}).$mount('#app')
更多推荐
所有评论(0)