Global Custom Directives

Registering global custom directives with the app instance

Introduction

Custom directives are often registered globally - by their nature, they are quite often used as little helpers in many places throughout your app.

This chapter is also the first that touches on changes in Vue 3 core outside of the "app" API, as there are a couple of changes for custom directives themselves. You can read more about it in detail in the Official Migration Guide on Custom Directives, but we will cover it here by example as well - it should be pretty straightforward.

Then, in Vue 2

In our app, we have a custom directive to handle clicks happening outside of an element, v-click-outside. It's defined and registered globally in a separate file named directives.js and then that file is imported into main.js

directives.js(Vue2)
import Vue from 'vue'

let handlers = new WeakMap()

function handleClick(e, el, binding) {
  const path = e.composedPath && e.composedPath()
  const isClickOutside = path ? path.indexOf(el) < 0 : !el.contains(e.target)

  if (isClickOutside) {
    binding.value()
  }
}

// the actual directive
const clickOutside = {
  bind(el, binding) {
    handlers.set(el, e => {
      handleClick(e, el, binding)
    })
    document.addEventListener('click', handlers.get(el))
  },
  unbind(el) {
    const handler = handlers.get(el)
    if (handler) {
      document.removeEventListener('click', handler)
    }
  },
}

Vue.directive('clickOutside', clickOutside)
main.js(Vue2)
import './directives'

// rest omitted

This allows us to define and register as many directives as we want and still only have one import into main.js, which is nice and clean.

Migration to Vue 3

Now, we have two things to take care of, mostly:

  1. The names of custom components' lifecycle hooks changed, in order to align them with the component hook names.
  2. Similar to the previous chapters, Vue.directive() is now app.directive(), so we have to handle that.

Renaming lifecycle Hook names

The function signatures haven't really changed much (see this), but the lifecycle hooks' names were aligned with those of components. That means less to remember!

x.js(Vue3)
const clickOutside = {
- bind(el, binding) {
+ beforeMount(el, binding) {
    handlers.set(el, e => {
      handleClick(e, el, binding)
    })
    document.addEventListener('click', handlers.get(el))
  },

-  unbind(el) {
+  ounMounted(el) {
    const handler = handlers.get(el)
    if (handler) {
      document.removeEventListener('click', handler)
    }
  },
}
Mapping of old and new Lifecycle hooks
Vue 2Vue 3
bindbeforeMounted
insertedmounted
-/-beforeUpdate (This one is new, see docs)
updatethis one was removed
componentUpdatedupdated
-/-beforeUnmount (this one is new, see docs)
unbindunmounted

Tip: Accessing the component instance

The example in your app doesn't access the component instance, and the general guideline ist that custom directives are best suited for tasks that are not coupled to the instance that uses them.

But sometimes it does make sense. In Vue 2, People used vnode.context to access the instance. In Vue 3, vnodes are context-free, so the instance is now available as binding.instance, which means you don't have to tough the third argument anymore.

beforeMount(el, binding) {
  const vm = binding.instance
}
bind(el, binding, vnode) {
  const vm = vnode.context
}

Registering with app

We can't use Vue.directive() anymore, we have to call app.directive() to register our directives. The most ergonomic way to do that is to do that in a function receiving the app instance, and then export this function from our directives.js file. This effectively turns it into a plugin that we can the pass to app.use() in our main file.

This is following the pattern yo may have rad as a tip in the previous chapter.

directives.js
let handlers = new WeakMap()

function handleClick(e, el, binding) {
  // ...
}

// the actual directive
const clickOutside = {
  beforeMount(el, binding) {
    // ...
  },
  unmounted(el) {
    // ...
  },
}

 export default function directives (app) {
   app.directive('clickOutside',clickOutside)
 }
directives.js
- import Vue from 'vue'
let handlers = new WeakMap()

function handleClick(e, el, binding) {
  // ...
}

// the actual directive
const clickOutside = {
  beforeMount(el, binding) {
    // ...
  },
  unmounted(el) {
    // ...
  },
}

- Vue.directive('clickOutside', clickOutside)
+ export default function directives (app) {
+   app.directive('clickOutside',clickOutside)
+ }
main.js(Vue3)
import { createApp } from 'vue'
import directives from './directives'

// ...

const app = createApp(/* ... */)

app.use(directives)

Further Reading