Image de l'article 'Custom Directives with Vue 3 and TypeScript'

Custom Directives with Vue 3 and TypeScript

Sometimes, it’s useful to create custom directives with Vue.js. Let’s see how you can do that using TypeScript.

Starting point

I learn every day to use and improve my usage of TypeScript.

Today, I’ll share the case where I converted from JavaScript to TypeScript for a custom directive in Vue 3.

The JavaScript code was the following:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
const ClickOutsideDirective = {
  mounted(el, binding) {
    el.__clickOutsideHandler__ = (event) => {
      if (!(el === event.target || el.contains(event.target))) {
        binding.value(event);
      }
    };
    document.body.addEventListener("click", el.__clickOutsideHandler__);
  },
  unmounted(el) {
    document.body.removeEventListener("click", el.__clickOutsideHandler__);
  },
};
export default (app) => {
  app.directive("click-outside", ClickOutsideDirective);
};

How to tackle the challenge

This first thing to deal with is the two arguments of the mounted hook and the argument of the unmounted hook.

It isn’t well documented in the official documentation, but if you dig into the Vue.js code (or find the Stackoverflow threads 😁), you can find more about the interface.

You need to type the custom directive object with <Directive<T, V>> where:

  • T is the type of the DOM element where the directive is used
  • V is the type of the value passed to the directive in the template.

So in my case, T is HTMLElement and V is a Function.

It took me a moment to understand the type V. But I found it by pressing F12 on value in the line binding.value(event);.

You can then see the definition of the DirectiveBinding interface and value has the generic type V.

But how do you know the type based on a different use case?

Just look at what you pass as the value to your custom directive. In my example, I’m passing a function:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
<!-- closeDropdown is a method defined in the component -->
<a @click.prevent="toggleMenu" v-click-outside="closeDropdown" href="#"
  >Toggle
</a>

<!-- or an anonymous function -->

<a
  @click.prevent="toggleMenu"
  v-click-outside="() => menuOpened = false"
  href="#"
  >Toggle
</a>

Next, the best practice of building a custom directive is to define a custom event listener to the DOM element. Therefore you need to define a custom interface (within the custom directive file since you’ll use it only there):

1
2
3
interface ClickOutsideDirectiveHTMLElement extends HTMLElement {
  __clickOutsideHandler__: EventListener;
}

You have one property typed with EventListener that is the name of the custom event listener. You must make sure the interface extends the type T, in our case a HTMLElement.

Next, you simply type:

  • The event argument in the definition of the custom event listener as Event.
  • The [event.target](https://developer.mozilla.org/en-US/docs/Web/API/Event/target) must be casted to a Node where you check if the element contains the target.
  • The app argument on the final export.

Final code

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
import { App } from "vue";

interface ClickOutsideDirectiveHTMLElement extends HTMLElement {
  __clickOutsideHandler__: EventListener;
}

import { Directive } from "vue";

const ClickOutsideDirective = <
  Directive<ClickOutsideDirectiveHTMLElement, Function>
>{
  mounted(el, binding) {
    el.__clickOutsideHandler__ = (event: Event) => {
      if (!(el === event.target || el.contains(event.target as Node))) {
        binding.value(event);
      }
    };
    document.body.addEventListener("click", el.__clickOutsideHandler__);
  },
  unmounted(el) {
    document.body.removeEventListener("click", el.__clickOutsideHandler__);
  },
};
export default (app: App) => {
  app.directive("click-outside", ClickOutsideDirective);
};

Now, you’re ready. Go make your own Vue 3 directives with TypeScript!

License GPLv3 | Terms
Built with Hugo
Theme Stack designed by Jimmy