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
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)
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:
- The names of custom components' lifecycle hooks changed, in order to align them with the component hook names.
- Similar to the previous chapters,
Vue.directive()
is nowapp.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!
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 2 | Vue 3 |
---|---|
bind | beforeMounted |
inserted | mounted |
-/- | beforeUpdate (This one is new, see docs) |
update | this one was removed |
componentUpdated | updated |
-/- | beforeUnmount (this one is new, see docs) |
unbind | unmounted |
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
}
app
Registering with 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.
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)
}
- 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)
+ }
import { createApp } from 'vue'
import directives from './directives'
// ...
const app = createApp(/* ... */)
app.use(directives)