有的情况下,需要对普通 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')

Logo

前往低代码交流专区

更多推荐