The Power of Custom Directives in Vue

Avatar of Sarah Drasner
Sarah Drasner on

When you’re initially learning a JavaScript framework, it feels a little like being a kid in a candy store. You take in everything available to you, and right off the bat, there are things that will make your life as a developer easier. Inevitably though, we all reach a point working with a framework where we have a use-case that the framework doesn’t cover very well.

The beautiful thing about Vue is that it’s incredibly feature-rich. But even if you have an edge case not covered by the framework, it’s got your back there as well, because you can quite easily create a custom directive.

What are directives?

I’ve written a post here on directives in my guide on Vue.js, but let’s do a refresher.

Directives are tiny commands that you can attach to DOM elements. They are prefixed with v- to let the library know you’re using a special bit of markup and to keep syntax consistent. They are typically useful if you need low-level access to an HTML element to control a bit of behavior.

Some directives you might already be familiar with if you’ve worked with Vue (or Angular, for that matter) are v-if, v-else, v-show, etc. We’ll go into some of the foundations, but if you’d rather read through the examples instead, you could scroll down the page a bit and probably still understand the concepts.

Here are some ways to use a directive, along with an example counterpart. These examples are not prescriptive, they’re just use-cases. The word example here is in place of the actual directive.

v-example – this will instantiate a directive, but doesn’t accept any arguments. Without passing a value, this would not be very flexible, but you could still hang some piece of functionality off of the DOM element.

v-example="value" – this will pass a value into the directive, and the directive figures out what to do based off of that value.

<div v-if="stateExample">I will show up if stateExample is true</div>

v-example="'string'" – this will let you use a string as an expression.

<p v-html="'<strong>this is an example of a string in some text</strong>'"></p>

v-example:arg="value" – this allows us to pass in an argument to the directive. In the example below, we’re binding to a class, and we’d style it with an object, stored separately.

<div v-bind:class="someClassObject"></div>

v-example:arg.modifier="value" – this allows us to use a modifier. The example below allows us to call preventDefault() on the click event.

<button v-on:submit.prevent="onSubmit"></button>

Understanding Custom Directives

Now that we see all the ways we can use directives, let’s break down how we would implement them with a custom directive we author ourselves. A nice example of something you might use a custom directive for is a scroll event, so let’s see how we’d write that.

At it’s very base, this is how we would create a global directive. (but it doesn’t do anything – yet!) – it just creates the directive.

Vue.directive('tack');

On the element itself, it would look like:

<p v-tack>This element has a directive on it</p>

There are a few hooks available to us, and each one has the option of a few arguments. The hooks are as follows:

  • bind – This occurs once the directive is attached to the element.
  • inserted – This hook occurs once the element is inserted into the parent DOM.
  • update – This hook is called when the element updates, but children haven’t been updated yet.
  • componentUpdated – This hook is called once the component and the children have been updated.
  • unbind – This hook is called once the directive is removed.
directives hooks diagram

Personally, I find bind and update the most useful of the five.

Each of these have el, binding, and vnode arguments available to them, with the exception of update and componentUpdated, which also expose oldVnode, to differentiate between the older value passed and the newer value.

el, as you might expect, is the element the binding sits on. binding is an object which contains arguments that are passed into the hooks. There are many available arguments, including name, value, oldValue, expression, arg, and modifiers. vnode has a more unusual use-case, it’s available in case you need to refer directly to the node in the virtual DOM. Both binding and vnode should be treated as read-only.

Building a Custom Directive

Now that we’ve broken that down, we can start to look at how we’d use a custom directive in action. Let’s build off of that first example with what we just covered to make it more useful:

Vue.directive('tack', {
 bind(el, binding, vnode) {
    el.style.position = 'fixed'
  }
});

And on the HTML itself:

<p v-tack>I will now be tacked onto the page</p>

This is OK, but it’s not very flexible until we pass a value into it and update it or reuse it on the fly. Let’s decide how far from the top we’d like to fix the element to:

Vue.directive('tack', {
  bind(el, binding, vnode) {
    el.style.position = 'fixed'
    el.style.top = binding.value + 'px'
  }
});
<div id="app">
  <p>Scroll down the page</p>
  <p v-tack="70">Stick me 70px from the top of the page</p>
</div>

See the Pen.

Let’s say I wanted to now differentiate between whether or not we’re offseting the 70px from the top or the left. We could do that by passing an argument:

<p v-tack:left="70">I'll now be offset from the left instead of the top</p>
Vue.directive('tack', {
  bind(el, binding, vnode) {
    el.style.position = 'fixed';
    const s = (binding.arg == 'left' ? 'left' : 'top');
    el.style[s] = binding.value + 'px';
  }
});

See the Pen.

You can use more than one value, as well. You could do so the same way you would for regular directives:

<p v-tack="{ top: '40', left: '100' }">Stick me 40px from the top of the page and 100px from the left of the page</p>

And then the directive would be rewritten to work with both:

Vue.directive('tack', {
  bind(el, binding, vnode) {
    el.style.position = 'fixed';
    el.style.top = binding.value.top + 'px';
    el.style.left = binding.value.left + 'px';
  }
}); 

See the Pen.

We can also write something more complex where we can create and modify methods based on our custom directives. Here, we’ll do a waypoints-like example, where we can create an animation that fires off a particular scroll event with a small amount of code:

Vue.directive('scroll', {
  inserted: function(el, binding) {
    let f = function(evt) {
      if (binding.value(evt, el)) {
        window.removeEventListener('scroll', f);
      }
    };
    window.addEventListener('scroll', f);
  },
});

// main app
new Vue({
  el: '#app',
  methods: {
   handleScroll: function(evt, el) {
    if (window.scrollY > 50) {
      TweenMax.to(el, 1.5, {
        y: -10,
        opacity: 1,
        ease: Sine.easeOut
      })
    }
    return window.scrollY > 100;
    }
  }
});
<div class="box" v-scroll="handleScroll">
  <p>Lorem ipsum dolor sit amet, consectetur adipisicing elit. A atque amet harum aut ab veritatis earum porro praesentium ut corporis. Quasi provident dolorem officia iure fugiat, eius mollitia sequi quisquam.</p>
</div>

See the Pen.

In these Pens, we’re keeping everything simple so that you can see it easily. In an actual app, you can build out really nice custom and flexible customized directives available for your whole team.

In an actual build process, I would place the directive code in the `main.js` file that lives at the root of the `src` directory (if you’re using something like the Vue-cli build) so that `App.vue` and all of the subsequent .vue files in the components directory will have access to it. There are other ways of working with it as well, but I’ve found this to be the most flexible implementation for the whole app.

If you’d like to learn more about the Vue framework, check out our guide.