Custom Directives in Vue JS

Vue JS promotes the use of components as the primary form of code reuse and abstraction. However, there are some instances when you want to manipulate the low-level DOM from within Vue JS components. In these instances, directives come to the rescue!

If you have already been developing apps with Vue JS you must surely be familiar with some of the core directives offered by the Vue JS core team. Here are a few worth mentioning: v-model, v-if, v-for’, etc.

In this article, I will cover everything you need to know to start building your own custom directives in Vue JS.

Dissect a Custom Directive

Custom directives enrich HTML with new reactive functionality that's fully managed by Vue JS.

Let's start with a full dissection of a basic custom directive written for Vue JS.

Consider the following directive:

Vue.directive('focus', {
  // When the bound element is inserted into the DOM...
  inserted: function (el) {
    // Focus the element
    el.focus()
  }
})

This example registers a new global custom directive into the main Vue instance. Later I will discuss the different ways available for registering directives. For now, let’s focus on the directive itself.

A custom directive is defined by a JavaScript literal object implementing a set of functions. These functions are called hooks by Vue JS and are standard to any custom directive. More on hook functions in the coming section.

The inserted() hook function accepts the el input parameter. This parameter represents the HTML element where this custom directive is applied.

Inside the function, the focus() function is called on the element itself.

In summary, when the element with the custom directive is added to its parent node, this function runs and makes the element in the focus state.

How do you apply this custom directive inside a component? Every custom directive should be prefixed by the letter v-. In this case, assuming we are adding this custom directive to an input control, then it follows like this:

<input v-focus>

Hook Functions

All hook functions provided by Vue Js for building custom directives are optional. Hook functions are there to help you customize and provide the needed functionality for the directive at certain stages of the directive life cycle.

There are five available:

  • bind
  • inserted
  • update
  • componentUpdate
  • unbind

bind

This function is called once when the directive is bound to the underlying element. Think of it as a one-time setup hook.

inserted

This is called when the underlying element is inserted into the parent node. This doesn’t mean the element is inserted into the live DOM but rather its context is now known and part of a well-defined tree of nodes. You can read more about VNodes to understand how Vue JS works with them.

update

This function is called after the containing component's VNode has updated, but possibly before its children have updated.

componentUpdate

This is called after the containing component's VNode and the VNodes of its children have updated.

unbind

This function is called only once when the directive is unbound from the element.

Vue JS engine passes the same set of input parameters to all hook functions. Let's look at these parameters.

Binding Function Parameters

Each and every hook function receives the same set of input parameters defined as follows.

el

This parameter represents the element that this custom directive is applied to. It can be any valid HTML element.

binding

This input parameter is an object containing the following properties:

name: The name of the directive without the v- prefix. For instance, using a custom directive as v-focus yields a name of focus.

value: The value passed to the directive. For instance, using the v-slot=”prop” directive yields a value of prop.

oldValue: This field is only available inside update() and componentUpdate() hook functions. It contains the previous value of the directive, before the update.

expression: This field represents the expression of the binding as a string literal. For instance, using the custom directive v-add="1+1" yields an expression of "1+1".

arg: This field represents the argument (if any) that's passed to the directive. There can be only one argument passed. For instance, using the v-slot:default directive yields an argument of default.

modifiers: This field is an object containing modifiers that could change and control the behavior of the directive if they are set. Think of modifiers as flags you set on the directive. If a modifier is set, it will have a value of true, if not set, it won’t even be visible to the directive. For example, using the directive v-on:click.prevent yields a modifier of { prevent: true } object.

vnode

The virtual node produced by Vue's compiler. See the VNode API for full details.

oldVnode

The previous virtual node, only available in the update() and componentUpdated() hooks.

Now that you know all about hooks and the details about their input parameters, let's see how you register a custom directive in your Vue JS app.

Globally Registered Directives

There are two ways to define and register a custom directive. In this section, we will look at how to register a custom directive globally in your app.

To do this, navigate to the main.js file located at the root folder of your application and add the following to register the focus custom directive.

import Vue from "vue";
import App from "./App.vue";

Vue.config.productionTip = false;

// Register a global custom directive called `v-focus`
Vue.directive('focus', {
  // When the bound element is inserted into the DOM...
  inserted: function (el) {
    // Focus the element
    el.focus()
  }
})

new Vue({
  render: h => h(App)
}).$mount("#app");

The `Vue.directive()` function accepts as a first parameter the name of the custom directive (without the `v-` prefix). The second parameter is the custom directive object. In this case, the object contains the `inserted()` hook function only.

That's it! Now you can use the custom directive anywhere inside your components.

Locally Registered Directives

The second way of registering custom directives is local to the component. You can define and register a custom directive to be used inside a single component. In case you want to use the same custom directive somewhere else in your app, you have to redefine it again inside the other component.

This method of registering custom directives is definitely limited and might not be used often, if not at all!

I strongly recommend registering your custom directives as global directives for better performance and easier access across your app.

To register a custom directive locally, navigate to the component where you want to use the custom directive, and add the method below as part of the Vue Options API:

…
directives: {
  focus: {
    // directive definition
    inserted: function (el) {
      el.focus()
    }
  }
}
...

That’s it!

Demo: List Custom Directive

Now that you understand custom directives in Vue JS, let's build a custom directive.

The custom directive I am going to build in this section is the v-list directive.

Using this custom directive as such:

<div class="list" v-list:ul.square.inside="items"/>

Yields the following HTML being generated inside the DOM:
<ul style="list-style-type: square; list-style-position: inside;">
   <li>Item #1</li>
   <li>Item #2</li>
   <li>Item #3</li>
</ul>

Given a variable named `items` defined as an array of strings, yields the app showing in __Figure 1__:
figure1
Figure 1: The List custom directive in action

Figure 2 below shows the details of using this custom directive:

figure2
Figure 2: Using the custom directive

The diagram above is self-explanatory!

Let's sift through the code and define this custom directive.

You can play with the custom directive on codesandbox.io.

Add a new \directives\List.directive.js file and add the following code:

const directive = {
  bind(el, binding, vnode) {
    if (!Array.isArray(binding.value)) {
      return;
    }

    // validate value, arguments, and modifiers
    const { items, listType, listStyleType, listStylePosition } = validate(
      binding
    );

    render(el, { items, listType, listStyleType, listStylePosition });
  }
};

export default directive;

This code snippet defines an object called `directive`. Then, this object is exported as the default export of this code file.

The custom directive at hand makes use of the bind() hook function to implement the functionality of this directive.

First of all, it checks if the binding.value is bound to an array variable. If not, it returns and nothing happens.

The next step is to validate the argument and modifiers. This is done in a separate local utility function called validate. We will get into this very shortly.

The validate() function not only validates the different parts of the custom directive but also sets some default values in order to appease the rendering process.

Finally, it's time to render the list, whether a ul or ol list.

Let's have a look at the validate() method.

const validate = binding => {
  let results = {
    items: [],
    listType: "ul",
    listStyleType: "disc",
    listStylePosition: "outside"
  };

  // grab items
  results["items"] = [...binding.value];

  // grab argument
  const arg = binding.arg;
  const validArgs = ["ul", "ol"];

  if (arg && validArgs.includes(arg)) {
    results["listType"] = arg;
  }

  // grab modifiers
  const modifiers = binding.modifiers;
  const validModifiers = [
    "disc",
    "circle",
    "square",
    "decimal",
    "decimal-leading-zero",
    "lower-roman",
    "upper-roman",
    "lower-greek",
    "lower-latin",
    "upper-latin",
    "armenian",
    "georgian",
    "lower-alpha",
    "upper-alpha",
    "none",
    "inside",
    "outside"
  ];

  if (modifiers) {
    for (const [key, value] of Object.entries(modifiers)) {
      if (value) {
        // modifier included
        if (key && validModifiers.includes(key)) {
          if (key === "inside" || key === "outside") {
            results["listStylePosition"] = key;
          } else {
            results["listStyleType"] = key;
          }
        }
      }
    }
  }

  return results;
};

The method prepares a well-defined result object containing the following properties:
  • items: This property represents the binding.value of the directive. Whatever array variable you bind to the directive, it is being captured inside the items property.
  • listType: This property represents the type of list to render. Whether it is a ul element or ol element. It represents the binding.arg property defined on the custom directive.
  • listStyleType: This property represents the list-style-type CSS property defined on an ul or ol element. It represents one of the modifiers that this custom directive accepts. The code validates this modifier based on a known list of values that the list-style-type property accepts.
  • listStylePosition: This property represents the list-style-position CSS property defined on a ul or ol element. It represents one of the modifiers that this custom directive accepts. The code validates this modifier based on a known list of values that the list-style-position property accepts.

The properties above are defined with a default value representing the real default value behind list-style-type and list-style-position respectively. If these modifiers are incorrect, the default values take precedence.

Let's have a look at the render() method:

const render = (el, { items, listType, listStyleType, listStylePosition }) => {
  if (!el) return;

  // clear container
  el.innerHTML = "";

  // add the list
  const list = document.createElement(listType);
  el.appendChild(list);

  // configure list
  list.style.listStyleType = listStyleType;
  list.style.listStylePosition = listStylePosition;

  items.forEach(item => {
    const li = document.createElement("li");

    list.appendChild(li);

    li.textContent = item;
  });
};

This method starts by clearing the parent container, the `el` element.

It then creates a new HTML element, whether a new ul or ol element. It appends the list into the parent container el element.

After that, it sets the listStyleType and listStylePosition properties on the newly created list element.

It then iterates over the items stored inside the binding.value array. For each array item, it creates a new li element, appends it to the list element created above, and sets it's textContent property to the value of the array item.

To use this custom directive, switch back to the \main.js file and register this directive globally as follows:

...
import ListDirective from "./directives/List.directive";

Vue.directive("list", ListDirective);
...

That's all!

Navigate to App.vue file and add the directive as follows:

<template>
  <div id="app">
    <h2>Awesome List</h2>
    <div class="list" v-list:ul.square.inside="items"/>
  </div>
</template>

<script>
export default {
  name: "App",
  data() {
    return {
      items: ["Item #1", "Item #2", "Item #3"]
    };
  }
};
</script>

Attach the directive to a `
` element. Then set the `items` variable to an array of strings.

Running the app yields the same app shown above in Figure 1.

This custom directive can be made much more complicated. However, I opted for a simplified implementation to illustrate the ideas behind building a custom directive in Vue JS.

You can play with the custom directive on codesandbox.io.

Conclusion

Despite the fact that Vue JS pushes for coding components rather than custom directives, there are some instances when you need to manipulate the DOM reactively using custom directives.

You might also like