A simple, real-world Vue.js directive
Tags: javascript, vue, angular
Published: 2018-05-16 12:43:34

---
title: A simple, real-world Vue.js directive
published: true
tags: JavaScript, vuejs, angularjs
canonical_url: https://codewithhugo.com/a-simple-real-world-vuejs-directive/
cover_image: https://codewithhugo.com/img/frank-mckenna-unsplash.jpg
---

[VueJS](https://vuejs.org/) is "The Progressive JavaScript Framework". It takes inspiration from all prior art in the view library and frontend framework world, including AngularJS, React, Angular, Ember, Knockout and Polymer.
In Vue (and Angular/AngularJS), a directive is a way to wrap functionality that usually applies to DOM elements. The example in Vue's documentation is a `focus` directive.

When running VueJS inside of AngularJS, an issue occured whereby the AngularJS router would try to resolve
normal anchors' `href` on click.
The `href`s weren't AngularJS URLs so it would fall back to the default page.
One solution could have leveraged components to update `window.location` directly, but here's a nifty directive to do the same:

```html
<a v-href="'/my-url'">Go</a>
```

That's a pretty cool API, and it's probably more idiomatic Vue than:

```html
<MyAnchor href="/my-url">
  Go
</MyAnchor>
```

There were a couple of gotchas:

* [Local vs global directive registration 🌐](https://codewithhugo.com/a-simple-real-world-vuejs-directive#local-global)
* [A minimal directive API 👌](https://codewithhugo.com/a-simple-real-world-vuejs-directive#minimal-directive)
* [Vue directive hooks and `removeEventListener` 🆓](https://codewithhugo.com/a-simple-real-world-vuejs-directive#hooks-and-remove)
* [Handling clicks properly 🖱](https://codewithhugo.com/a-simple-real-world-vuejs-directive#handling-clicks)
* [Parting thoughts 📚](https://codewithhugo.com/a-simple-real-world-vuejs-directive#parting-thoughts)


[Subscribe](https://buttondown.email/hugo) to get the latest posts right in your inbox (before anyone else).

# Local vs global directive registration 🌐

A global Vue directive can be defined like so:

```js
Vue.directive("non-angular-link", {
  // directive definition
});
```

It can also be defined locally as follows:

```js
Vue.component("my-component", {
  directives: {
    "non-angular-link": nonAngularLinkDirective
  }
});
```

Where `nonAngularLinkDirective` would be a JavaScript object that defines the directive, eg.

```js
const nonAngularLinkDirective = {
  bind(el, binding) {},
  unbind(el) {}
};
```

This allows for flexibility if using a bundler like webpack and single file components:

```js
// non-angular-link-directive.js
export const nonAngularLinkDirective = {
  // directive definition
};
```

```html
// MyComponent.vue
<template>
  <a
    href="/my-url"
    v-non-angular-link
  >
    Go
  </a>
</template>

<script>
import { nonAngularDirective } from './non-angular-link.directive';
export default {
  directives: {
    'non-angular-link': nonAngularLinkDirective
  }
};
</script>
```

# A minimal directive API 👌

A full `MyAnchor` single file component would look like the following:

```html
// MyAnchor.vue
<template>
  <a
    @click="goToUrl($event)"
    :href="href"
  >
    <slot />
  </a>
</template>
<script>
export default {
  props: {
    href: {
      type: String,
      required: true
    }
  },
  methods: {
    goToUrl(e) {
      e.preventDefault();
      window.location.assign(this.href);
    }
  }
});
</script>
```

This is quite verbose and leverages a global DOM object... not ideal.
Here’s something similar using a directive:

```js
// non-angular-link-directive.js
export const nonAngularLinkDirective = {
  bind(el) {
    el.addEventListener("click", event => {
      event.preventDefault();
      window.location.assign(event.target.href);
    });
  }
};
```

This directive has to be used like so `<a href="/my-url" v-non-angular-link>Go</a>`, which isn’t the nicest API.
By leveraging the second parameter passed to `bind` we can write it so that it can be used like `<a v-href="'/my-url'">Go</a>`
(for more information about `el` and `binding`, see https://vuejs.org/v2/guide/custom-directive.html#Directive-Hook-Arguments):

```js
// non-angular-link-directive.js
export const nonAngularLinkDirective = {
  bind(el, binding) {
    el.href = binding.value;
    el.addEventListener("click", event => {
      event.preventDefault();
      window.location.assign(event.target.href);
    });
  }
};
```

We can now use it like using a local directive definition:

```html
// MyComponent.vue
<template>
  <a v-href="'/my-url'">Go</a>
</template>
<script>
import { nonAngularLinkDirective } from './non-angular-link.directive';
export default {
  directives: {
    href: nonAngularLinkDirective
  }
};
</script>
```

# Vue directive hooks and `removeEventListener` 🆓

For the full list of directive hooks, see https://vuejs.org/v2/guide/custom-directive.html#Hook-Functions.

As a good practice, the event listener should be removed when it’s not required any more.
This can be done in `unbind` much in the same way as it was added, there’s a catch though,
the arguments passed to `removeEventListener` have to be the same as the ones passed to `addEventListener`:

```js
// non-angular-link-directive.js
const handleClick = event => {
  event.preventDefault();
  window.location.assign(event.target.href);
};
export const nonAngularLinkDirective = {
  bind(el, binding) {
    el.href = binding.value;
    el.addEventListener("click", handleClick);
  },
  unbind(el) {
    el.removeEventListener("click", handleClick);
  }
};
```

This will now remove the listener when the component where the directive is used is destroyed/un-mounts
and leaves us with no hanging listeners.

# Handling clicks properly 🖱

An edge case happens when an anchor contains an image: the `target` of the event is not the `anchor`,
but the `img`… which doesn’t have a `href` attribute.

To deal with this, with a little knowledge of how `addEventListener` calls the passed handler,
we can refactor the `handleClick` function.

```js
// non-angular-link-directive.js
function handleClick(event) {
  // The `this` context is the element
  // on which the event listener is defined.
  event.preventDefault();
  window.location.assign(this.href);
}

// rest stays the same
```

By using a named functions and the `this` allows the event listener to bind
`this` to the element on which it’s attached as opposed to the lexical `this` of an arrow function.

## Parting thoughts 📚

We use `window.location.assign` so as to allow to test easily. With Jest and `@vue/test-utils` a test at the component level should look like this:

```js
import { shallowMount } from '@vue/test-utils';
import MyComponent from './MyComponent.vue';

test('It should call window.location.assign with the right urls', () => {
  // Stub window.location.assign
  window.location.assign = jest.fn();

  const myComponent = shallowMount(MyComponent);

  myComponent.findAll('a').wrappers.forEach((anchor) => {
    const mockEvent = {
      preventDefault: jest.fn()
    };
    anchor.trigger('click', mockEvent);
    expect(mockEvent.preventDefault).toHaveBeenCalled();
    expect(window.location.assign).toHaveBeenCalledWith(
      anchor.attributes().href
    );
});
```

Directives allow you to contain pieces of code that interact with the DOM. This code needs to be generic enough to be used with the limited information available to a directive.

By coding against the DOM, we leverage the browser APIs instead of re-inventing them.

[Subscribe](https://buttondown.email/hugo) to get the latest posts right in your inbox (before anyone else).

Cover photo by [frank mckenna](https://unsplash.com/photos/tjX_sniNzgQ?utm_source=unsplash&utm_medium=referral&utm_content=creditCopyText) on [Unsplash](https://unsplash.com/search/photos/contain?utm_source=unsplash&utm_medium=referral&utm_content=creditCopyText)