Custom Directives

Intro

In addition to the default set of directives shipped in core (like v-model or v-show), Vue also allows you to register your own custom directives. Note that in Vue, the primary form of code reuse and abstraction is components - however, there may be cases where you need some low-level DOM access on plain elements, and this is where custom directives would still be useful. An example would be focusing on an input element, like this one:

See the Pen Custom directives: basic example by Vue (@Vue) on CodePen.

When the page loads, that element gains focus (note: autofocus doesn’t work on mobile Safari). In fact, if you haven’t clicked on anything else since visiting this page, the input above should be focused now. Also, you can click on the Rerun button and input will be focused.

Now let’s build the directive that accomplishes this:

  1. const app = Vue.createApp({})
  2. // Register a global custom directive called `v-focus`
  3. app.directive('focus', {
  4. // When the bound element is mounted into the DOM...
  5. mounted(el) {
  6. // Focus the element
  7. el.focus()
  8. }
  9. })

If you want to register a directive locally instead, components also accept a directives option:

  1. directives: {
  2. focus: {
  3. // directive definition
  4. mounted(el) {
  5. el.focus()
  6. }
  7. }
  8. }

Then in a template, you can use the new v-focus attribute on any element, like this:

  1. <input v-focus />

Hook Functions

A directive definition object can provide several hook functions (all optional):

  • beforeMount: called when the directive is first bound to the element and before parent component is mounted. This is where you can do one-time setup work.

  • mounted: called when the bound element’s parent component is mounted.

  • beforeUpdate: called before the containing component’s VNode is updated

Note

We’ll cover VNodes in more detail later, when we discuss render functions.

  • updated: called after the containing component’s VNode and the VNodes of its children have updated.

  • beforeUnmount: called before the bound element’s parent component is unmounted

  • unmounted: called only once, when the directive is unbound from the element and the parent component is unmounted.

You can check the arguments passed into these hooks (i.e. el, binding, vnode, and prevVnode) in Custom Directive API

Dynamic Directive Arguments

Directive arguments can be dynamic. For example, in v-mydirective:[argument]="value", the argument can be updated based on data properties in our component instance! This makes our custom directives flexible for use throughout our application.

Let’s say you want to make a custom directive that allows you to pin elements to your page using fixed positioning. We could create a custom directive where the value updates the vertical positioning in pixels, like this:

  1. <div id="dynamic-arguments-example" class="demo">
  2. <p>Scroll down the page</p>
  3. <p v-pin="200">Stick me 200px from the top of the page</p>
  4. </div>
  1. const app = Vue.createApp({})
  2. app.directive('pin', {
  3. mounted(el, binding) {
  4. el.style.position = 'fixed'
  5. // binding.value is the value we pass to directive - in this case, it's 200
  6. el.style.top = binding.value + 'px'
  7. }
  8. })
  9. app.mount('#dynamic-arguments-example')

This would pin the element 200px from the top of the page. But what happens if we run into a scenario when we need to pin the element from the left, instead of the top? Here’s where a dynamic argument that can be updated per component instance comes in very handy:

  1. <div id="dynamicexample">
  2. <h3>Scroll down inside this section ↓</h3>
  3. <p v-pin:[direction]="200">I am pinned onto the page at 200px to the left.</p>
  4. </div>
  1. const app = Vue.createApp({
  2. data() {
  3. return {
  4. direction: 'right'
  5. }
  6. }
  7. })
  8. app.directive('pin', {
  9. mounted(el, binding) {
  10. el.style.position = 'fixed'
  11. // binding.arg is an argument we pass to directive
  12. const s = binding.arg || 'top'
  13. el.style[s] = binding.value + 'px'
  14. }
  15. })
  16. app.mount('#dynamic-arguments-example')

Result:

See the Pen Custom directives: dynamic arguments by Vue (@Vue) on CodePen.

Our custom directive is now flexible enough to support a few different use cases. To make it even more dynamic, we can also allow to modify a bound value. Let’s create an additional property pinPadding and bind it to the <input type="range">

  1. <div id="dynamicexample">
  2. <h2>Scroll down the page</h2>
  3. <input type="range" min="0" max="500" v-model="pinPadding">
  4. <p v-pin:[direction]="pinPadding">Stick me 200px from the {{ direction }} of the page</p>
  5. </div>
  1. const app = Vue.createApp({
  2. data() {
  3. return {
  4. direction: 'right',
  5. pinPadding: 200
  6. }
  7. }
  8. })

Now let’s extend our directive logic to recalculate the distance to pin on component update:

  1. app.directive('pin', {
  2. mounted(el, binding) {
  3. el.style.position = 'fixed'
  4. const s = binding.arg || 'top'
  5. el.style[s] = binding.value + 'px'
  6. },
  7. updated(el, binding) {
  8. const s = binding.arg || 'top'
  9. el.style[s] = binding.value + 'px'
  10. }
  11. })

Result:

See the Pen Custom directives: dynamic arguments + dynamic binding by Vue (@Vue) on CodePen.

Function Shorthand

In previous example, you may want the same behavior on mounted and updated, but don’t care about the other hooks. You can do it by passing the callback to directive:

  1. app.directive('pin', (el, binding) => {
  2. el.style.position = 'fixed'
  3. const s = binding.arg || 'top'
  4. el.style[s] = binding.value + 'px'
  5. })

Object Literals

If your directive needs multiple values, you can also pass in a JavaScript object literal. Remember, directives can take any valid JavaScript expression.

  1. <div v-demo="{ color: 'white', text: 'hello!' }"></div>
  1. app.directive('demo', (el, binding) => {
  2. console.log(binding.value.color) // => "white"
  3. console.log(binding.value.text) // => "hello!"
  4. })

Usage on Components

In 3.0, with fragments support, components can potentially have more than one root nodes. This creates an issue when a custom directive is used on a component with multiple root nodes.

To explain the details of how custom directives will work on components in 3.0, we need to first understand how custom directives are compiled in 3.0. For a directive like this:

  1. <div v-demo="test"></div>

Will roughly compile into this:

  1. const vFoo = resolveDirective('demo')
  2. return withDirectives(h('div'), [[vDemo, test]])

Where vDemo will be the directive object written by the user, which contains hooks like mounted and updated.

withDirectives returns a cloned VNode with the user hooks wrapped and injected as VNode lifecycle hooks (see Render Function for more details):

  1. {
  2. onVnodeMounted(vnode) {
  3. // call vDemo.mounted(...)
  4. }
  5. }

As a result, custom directives are fully included as part of a VNode’s data. When a custom directive is used on a component, these onVnodeXXX hooks are passed down to the component as extraneous props and end up in this.$attrs.

This also means it’s possible to directly hook into an element’s lifecycle like this in the template, which can be handy when a custom directive is too involved:

  1. <div @vnodeMounted="myHook" />

This is consistent with the attribute fallthrough behavior. So, the rule for custom directives on a component will be the same as other extraneous attributes: it is up to the child component to decide where and whether to apply it. When the child component uses v-bind="$attrs" on an inner element, it will apply any custom directives used on it as well.