It is not difficult to write a custom directive. Vue provides the method directive so that we can easily register and use a custom directive. So let’s see how to implement and use a global custom directive step by step

Take an animation instruction to load loading

<template>
   <div v-loading="loading"></div>
<template>

<script>
import { ref } from 'vue'
export default {
  props: {
    handleLeave : {
      type: Function,
      default: () => null
    }
  },
    setup() {
        let loading = ref(false)
        
        return { loading }
    }
}
</script>
Copy the code

If loading is true, loading animation is displayed; if loading is false, loading animation is removed

Next we need to implement a component that can render animations, which we’ll call Load.vue

<template> <transition name="fade" @after-leave="handleAfterLeave"> <div v-show="visible" class="loading-mask"> <div class="loading-spinner"> <svg class="circular" viewBox="25 25 50 50"> <circle class="path" cx="50" cy="50" r="20" fill="none"/> </svg> </div> </div> </transition> </template> <script> import { ref } from 'vue' export default { name:'Loading', setup(props, { emit }) { let visible = ref(false) return { visible, show: () => visible.value = true, close: () => visible.value = false, handleAfterLeave: () => { props.handleLeave() } } }, } </script> <style lang="scss"> .loading-mask { position: absolute; z-index: 2000; Background - color: hsla (% 0, 0, 100%, 9); margin: 0; top: 0; right: 0; bottom: 0; left: 0; transition: opacity .3s; } .loading-spinner { top: 50%; margin-top: -21px; width: 100%; text-align: center; position: absolute; .circular { height: 42px; width: 42px; animation: loading-rotate 2s linear infinite; }. Path {animation: loading-dash 1.5s ease-in-out infinite; Stroke - dasharray: 90150; stroke-dashoffset: 0; stroke-width: 2; stroke: #409eff; stroke-linecap: round; }} @keyframes loading-dash {to{transform:rotate(1turn)}} @keyframes loading-dash {0% {stroke-dasharray: 1200; stroke-dashoffset: 0; } 50% {stroke-dasharray: 90,150; stroke-dashoffset: -40px; } 100% {stroke-dasharray: 90,150; stroke-dashoffset: -120px; } } </style>Copy the code

This is a very simple loading component. In addition to the mask layer and SVG animation, it receives a closed callback and exposes some component methods

Because this loading component is not a simple logical processing, but also needs to invoke vue’s rendering capability, we actually need to implement a global component rendering function. I wrote an article about vue3 global API components before, and I will directly show the code here

import { createVNode, render } from 'vue'
import Loading from './loading.vue'

const loadingDirective = {}
const container = document.createElement('div')
loadingDirective.install = app => {

  const createLoading = () => {

    const vLoading = createVNode(Loading)
    render(vLoading, container)
    
    document.body.appendChild(container)
  }

}

export default loadingDirective

Copy the code

In loading. Js we declare a createLoading method to implement the rendering function. The install method is exposed because we want to register the global singleton with the use method

app.directive('loading', { mounted: function(el, binding) { createLoading(el, binding) const instance = el.instance !! binding.value && instance.show() }, updated: function(el, binding) { const instance = el.instance if (binding.oldValue ! == binding.value) { !! binding.value ? instance.show() : instance.close() } } })Copy the code

In VUe2, the instruction method is exposed in the Vue global object, while in 3, it is the application instance object. Therefore, we can complete the instruction registration by the APP instance passed in through the install method, which also avoids the situation of mutual contamination when creating multiple singletons

There are 7 instruction hook functions in 3, which correspond to the life cycle of the component one by one, which is more conducive to understanding the meaning of each stage. The explanation is not expanded here, and the interested parties can consult the official document by themselves. Here we mainly use two hooks, mounted and updated

In Mounted, we initialize the loading component, obtain the component instance, and check whether the show method is triggered according to the binding value. Updated is the logic executed repeatedly. This method is triggered by the update of the component VNode bound to the instruction and its child vNode. Here, the binding value is verified to trigger the corresponding component instance method each time the method is fired

Because we can pass el (the element object to which the directive is bound) in the hook function, we can simplify our rendering logic without having to create an additional container and call appendChild, which can be extended as needed. By adding instance to the EL to point to the loading component instance, we can call the relevant method, and avoid declaring additional variables to generate closures. So, our load.js can be rewritten as

import { createVNode, render } from 'vue' import Loading from './index.vue' const loadingDirective = {} loadingDirective.install = app => { const createLoading = (el, binding) => { const vm = binding.instance const leaveExr = el.getAttribute('loading-leave') const handleLeave = Object.prototype.toString.call(vm[leaveExr]).silce(8,-1) === 'Function' ? vm[leaveExr] : () => null const vLoading = createVNode(Loading, { handleLeave }) render(vLoading, el) el.instance = vLoading.component.ctx } app.directive('loading', { mounted: function(el, binding) { createLoading(el, binding) const instance = el.instance !! binding.value && instance.show() }, updated: function(el, binding) { const instance = el.instance if (binding.oldValue ! == binding.value) { !! binding.value ? instance.show() : instance.close() } } }) } export default loadingDirectiveCopy the code

How to pass parameters to the instruction and add instace property to the EL object pointing to the loading component instance object

El is the mounted element to which the directive is bound. Binding. instance refers to a component instance that uses the directive. The createVnode method is used to pass in a loading component. The vNode object returned by this method exposes the component instance context CTX, which is used to access the loading component

The use method can be invoked on the application instance to register the directive after the entry file is instantiated

import { createApp } from 'vue'
import App from './App.vue'
import vLoading from './loading.js'

const app = createApp()

app.use(vLoading)
app.mount('#app')
Copy the code

Finally we can pass in a method on top of the original use example

<template> <div v-loading="loading" loading-leave="handleLeave"></div> <template> <script> import { ref } from 'vue' Export default {setup() {let loading = ref(false) const handleLeave = () => console.log(' the instruction is off ') return {loading, handleLeave } } } </script>Copy the code

The core of the directive method is to implement the desired function based on the parameters provided by each hook

In Vue3, passing parameters to components is not as free as in 2. All parameters are handled to props objects, which can be retrieved from the first parameter of the setup method as a starting point for the composite API