Vue3: directives — cheat sheet (built-in and custom) (en)

sucodelarangela

Angela Caldas

Posted on October 1, 2024

Vue3: directives — cheat sheet (built-in and custom) (en)

Ler em pt/br

One of the advantages of Vue is the ease of manipulating the DOM through special attributes called Directives. In addition to having several built-in directives, Vue also allows us to create custom ones.

If you're tired of using querySelector's and addEventListener's, then this article is for you!

Table of contents

Want to get straight to the point?
What are directives?
Creating custom directives
Creating local custom directives
Importing an external custom directive into a component
Declaring custom directives globally
Custom directives with modifiers
Phew! I think we've seen enough...

Want to see a specific directive?

Gif de Wall-E


What are directives?

Directives are nothing more than template-manipulating attributes used in HTML tags. With them, we can dynamically alter our DOM, adding or omitting information and elements (Leonardo Vilarinho in Front-end com Vue.js: Da teoria à prática sem complicações).

Vue has a large number of built-in directives. To better understand how to use them, we'll walk through some practical examples for each one (you can also directly access some examples in Vue’s documentation).

Back to table of contents


v-text

<h1 v-text="title"></h1>
<!-- is the same as -->
<h1>{{ title }}</h1>
Enter fullscreen mode Exit fullscreen mode

The v-text directive is used to insert a textContent into an element and works exactly the same as mustache interpolation ({{ }}). The difference in usage between them is that v-text will replace the entire textContent of the element, while interpolation allows you to replace only parts of the content.

Back to table of contents


v-html

<script setup>
import { ref } from "vue"
const myTitle = ref("<h1>Title</h1>")
</script>

<template>
  <div v-html="myTitle"></div>
</template>

<!--
The code above will render:
<div>
  <h1>Title</h1>
</div>
 -->
Enter fullscreen mode Exit fullscreen mode

The v-html directive is used to inject dynamic HTML into another element (equivalent to using innerHTML in JavaScript). It's a directive that should be used with caution, as it can allow for XSS (Cross-site Scripting) attacks, where malicious code can be injected into your application's DOM.

Back to table of contents


v-show

<script setup>
import { ref } from "vue"
const visible = ref(true)
</script>

<template>
  <div>
    <button @click="visible = !visible">Hide/Show</button>
    <p v-show="visible">Lorem ipsum</p>
  </div>
</template>
Enter fullscreen mode Exit fullscreen mode

The v-show directive dynamically changes the element's display property based on the received value. In the example above, when the visible state is true, the paragraph receives display: block (the default display of a <p> element). Clicking the button changes the visible state to false and assigns display: none to the paragraph.

Back to table of contents


v-if / v-else-if / v-else

<p v-if="type === 'A'">A paragraph</p>
<a v-else-if="type === 'B'" href="#">A link</a>
<ErrorComponent v-else />
Enter fullscreen mode Exit fullscreen mode

Considered as some of the most used directives in Vue, v-if, v-else-if, and v-else, are used for dynamic rendering of components and elements and follow the same logic as if, else if, and else in vanilla JavaScript. In the example, if type is 'A', we render a paragraph; however, if it's 'B', we render an anchor; and in any other case, we render a component called ErrorComponent.

Note: Elements managed by v-if, v-else-if, and v-else are completely destroyed or reconstructed in the DOM according to the condition met, unlike v-show which only changes the element's display.

Back to table of contents


v-for

<script setup>
import { ref } from "vue"
const users = ref([
  { name: 'John' },
  { name: 'Jane' }
])
</script>

<template>
  <p v-for="user in users">
    {{ user.name }}
  </p>
</template>
Enter fullscreen mode Exit fullscreen mode

The v-for directive renders a list of elements or components by iterating over an array or object, similar to a forEach. In our example:

  • we have an array of objects called users;
  • using the v-for directive, we iterate over this array using the variable in expression syntax to access each element of the iteration;
  • each user will be an object from our array. Thus, we will have two users, resulting in two paragraphs;
  • each paragraph will render the value of the name key of each user.

An important point about v-for is that we need to provide a special :key attribute, which must receive a unique value. This value can be derived directly from our user or we can use the index of our array (which is not recommended if you need to manipulate the items in the array, as it can result in errors).

<!-- using an unique value from 'user' -->
<p v-for="user in users" :key="user.name">
  {{ user.name }}
</p>

<!-- using the array index -->
<p v-for="(user, index) in users" :key="index">
  {{ user.name }}
</p>
Enter fullscreen mode Exit fullscreen mode

Back to table of contents


v-on

<button v-on:click="handleClick">Click</button>
<button @click="handleClick">Click</button>

<input type="text" v-on:focus="handleFocus" />
<input type="text" @focus="handleFocus" />

<!-- events with modifiers -->
<button type="submit" @click.prevent="submit">Send</button>
<input @keyup.enter="handleEnterKey" />
Enter fullscreen mode Exit fullscreen mode

The v-on directive (or simply @ as a shorthand) adds an "event listener" to HTML elements, similar to what we would do with JavaScript's addEventListener.

Any standard JavaScript event can be used with the v-on directive, which also accepts behavior modifiers and/or key modifiers.

In the code block above, we have some examples of events, such as v-on:click (or @click) and v-on:focus (or @focus), and we also show some events with modifiers, like .prevent (referring to event.preventDefault()) and .enter (which identifies the "Enter" key for the keyup event).

Back to table of contents


v-bind

<!-- Dynamic attributes -->
<img v-bind:src="imgSrc" />
<img :src="imgSrc" />
<img :src /> <!-- equivalent to :src="src" -->

<!-- Dynamic classes -->
<div :class="myClasses"></div>
<div :class="{ myClass: isValid }"></div>
<!-- Results in <div class="red"></div> if `isRed` is truthy -->

<!-- Props for child components -->
<ChildComponent :prop="myProp" />
Enter fullscreen mode Exit fullscreen mode

The v-bind directive is used to create/bind dynamic HTML attributes to elements or to pass props to child components. The v-bind directive can be abbreviated to just : as a shorthand.

In our examples, we have:

  • An imgSrc state used as the src attribute of an image, as well as a myClasses state used as a dynamic class;
  • The shortened form :src, for when the variable name is the same as the attribute name;
  • An example of dynamically assigning a "myClass" class if the isValid state is truthy;
  • An example of a myProp prop being passed to a child component.

Back to table of contents


v-model

<script setup>
import { ref } from "vue"
const message = ref("")
</script>

<template>
  <input type="text" v-model="message" />
</template>
Enter fullscreen mode Exit fullscreen mode

The v-model directive creates two-way data bindings, making it easy to synchronize states between inputs, selects, and components.

In the example above, the message state is bound to an <input> via v-model, so when you type in the input, the value of message is automatically updated with what was typed. Similarly, if we have a function that changes the value of message, for example, the input will reflect that change.

We can also use v-model to create this two-way binding from parent to child component:

<!-- passing props as readonly -->
<ChildComponent :msg="message" />

<!-- passing props with two-way data binding -->
<ChildComponent v-model="message" />
<!-- or -->
<ChildComponent v-model:msg="message" />
Enter fullscreen mode Exit fullscreen mode

Back to table of contents


v-slot

<!-- child component with named slots -->
<div>
  <slot name="title" />
  <slot name="message" />
</div>

<!-- parent component -->
<ChildComponent>
  <template v-slot:title>
    <h1>My title</h1>
  </template>
  <template #message>
    <p>Lorem ipsum</p>
  </template>
</ChildComponent>
Enter fullscreen mode Exit fullscreen mode

The v-slot directive is used to define and use slots in components. Slots are a way to pass content to a child component more flexibly than through props and can be named or unnamed, helping you insert elements into the child component in the correct places.

In the example above, we have a ChildComponent consisting of a div that encompasses two named slots: title and message. When using the child component, we pass two elements (h1 and p) to it through templates that use the v-slot directive with the name of the slot we want each element to receive. The v-slot directive can be abbreviated with the # symbol.

Back to table of contents


v-pre

<script setup>
import { ref } from "vue"
const message = ref("A simple message")
</script>

<template>
  <p>{{ message }}</p>
  <p v-pre>{{ message }}</p> 
</template>
Enter fullscreen mode Exit fullscreen mode

The v-pre directive ignores the compilation of the element it's used on, as well as all its child elements, rendering the content that would be dynamic as plain text (the first paragraph will render A simple message while the second paragraph will render {{ message }}).

Back to table of contents


v-once

<div v-once>
  <h1>Comment</h1>
  <p>{{msg}}</p>
</div>
Enter fullscreen mode Exit fullscreen mode

The v-once directive helps with performance by rendering the content of an element only once, making it static thereafter. Above, the paragraph will render the msg state only once and will remain static even if the value of msg changes later.

Back to table of contents


v-memo

<script setup>
import { ref, computed } from "vue"
const count = ref(0)
function calculate() {
  return // Some logic which uses `count`
}
</script>

<template>
  <div v-memo="[count]">
    {{ calculate() }}
  </div>
</template>
Enter fullscreen mode Exit fullscreen mode

The v-memo directive is somewhat similar to v-once, but it limits the re-rendering of the element or component to changes in one or more states, which must be passed as dependencies of the directive. In our example, we have a calculate function whose result should be rendered inside the div. However, this re-rendering should only occur if the value of count is updated, as it is referenced in the v-memo directive as a dependency.

The v-memo directive caches the content and only updates it if one of its dependencies is updated. This is exactly what happens with computed properties.

This directive is used for micro-optimizations of rendering, used more commonly in more complex components. However, if your component's logic is following best practices, the need to use v-memo becomes almost nonexistent.

For example, if calculate were a computed property, we wouldn't need v-memo, as computed properties do exactly what the directive does: they cache values and only update them again when dependencies change:

<script setup>
// omitted code
const calculate = computed(() => {
  return // Some logic which uses `count`
})
</script>

<template>
  <div>{{ calculate }}</div>
</template>
Enter fullscreen mode Exit fullscreen mode

Back to table of contents


v-cloak

<!DOCTYPE html>
<html lang="en">
<head>
  <!-- omitted code -->
  <style>
    [v-cloak] {
      display: none;
    }
  </style>
</head>

<body>
  <div id="app">
    <p v-cloak>{{ message }}</p>
  </div>

  <script src="https://unpkg.com/vue@3"></script>
  <script>
    const app = Vue.createApp({
      data() {
        return {
          message: 'Hello, World!'
        }
      }
    })

    app.mount('#app')
  </script>
</body>
</html>
Enter fullscreen mode Exit fullscreen mode

The v-cloak directive prevents uncompiled content from being rendered on the screen until Vue finishes its initialization (which typically happens when creating a Vue application directly in an HTML file via CDN). In the example above, the v-cloak directive will hide the paragraph until the message state is initialized and the component is fully mounted.

Back to table of contents


Creating custom directives

Custom directives allow you to bind Vue states to HTML elements, manipulating them according to your application's business rules. This way, you'll have greater control over the application's layout.

These directives are defined as an object that contains lifecycle hooks (the same ones we use in components), and each hook receives the element on which the directive will be used. The Vue documentation offers very easy-to-understand examples.

Creating local custom directives

<template>
  <input v-focus />
</template>

<!-- Options API -->
<script>
export default {
  directives: {
    focus: {
      mounted: (el) => el.focus()
    }
  }
}
</script>

<!-- Composition API -->
<script setup>
const vFocus = { mounted: (el) => el.focus() }
</script>
Enter fullscreen mode Exit fullscreen mode

Here we have a local custom directive called v-focus that automatically focuses on an input when the component is mounted. With the Options API, we need to declare our directive inside the directives object, but in the Composition API, we simply create a variable (which must start with 'v').


Importing an external custom directive into a component

Imagine that you need to use the v-focus directive in multiple components. This would generate a lot of repeated code in your application, as you would have to redeclare the directive in every component where you intend to use it, right?

Neo vs Smith clones

Let's finish those repeated codes off!

To avoid this repetition, we can extract the logic of our new directive to a file in the directives folder:

// src/directives/v-focus.js
export const vFocus = {
  mounted: (el) => el.focus()
};
Enter fullscreen mode Exit fullscreen mode

Now, just import the directive into the desired component and use it:

<template>
  <input v-focus />
</template>

<!-- Options API -->
<script>
import { vFocus } from '@/directives/v-focus.js';

export default {
  directives: {
    focus: vFocus
  }
}
</script>

<!-- Composition API -->
<script setup>
import { vFocus } from '@/directives/v-focus.js';
</script>
Enter fullscreen mode Exit fullscreen mode

Declaring custom directives globally

If you need to use a particular custom directive very often, a more suitable solution might be to declare it globally in your main.js or main.ts file:

// src/main.js
import { createApp } from 'vue'
import App from './App.vue'
import { vFocus } from '@/directives/v-focus.js'

const app = createApp(App)

// You can import the directive from an external file:
app.directive('focus', vFocus)

// Or you can declare ir directly like this:
app.directive('focus', {
  mounted: (el) => el.focus()
})

app.mount('#app')
Enter fullscreen mode Exit fullscreen mode

Back to table of contents


Custom directives with modifiers

So far, we've learned how to create simple custom directives. But what if you want a more complex directive that has action modifiers, like @click.prevent?

A directive can have up to four types of attributes that can be used in its declaration, with the most important (and our focus in this article) being the following:

  • el: The element on which the directive is being used (as we saw in vFocus); and
  • binding: Object containing various properties that we can use in our directives, such as value (the value passed in the directive) and modifiers, which is what we will use to create our modifiers.

For example, if we have the directive <div v-example:foo.bar="one">, our binding object would be:

{
  arg: 'foo',
  modifiers: { bar: true },
  value: 'one',
  oldValue: /* any previous value from the directive */
}
Enter fullscreen mode Exit fullscreen mode

Let's see how to create a directive to format text in uppercase, lowercase, or capitalized letters.

1. We create the initial structure of the vFormat directive, which will execute actions when the element is mounted in the component. Note that we are using el and binding as parameters of our hook:

// src/directives/vFormat.js
export const vFormatText = {
  mounted: (el, binding) => {},
}
Enter fullscreen mode Exit fullscreen mode

2. We'll create a modifier variable that will identify the modifier used in the directive. When we use a modifier in a directive, they are saved in a modifiers object within the binding object. So, if we use v-format.uppercase, binding.modifiers will be { uppercase: true } and the value of the modifier variable will be uppercase:

// src/directives/vFormat.js
export const vFormatText = {
  mounted: (el, binding) => {
    const modifier = Object.keys(binding.modifiers)[0];
  },
}
Enter fullscreen mode Exit fullscreen mode

3. Now we'll create the actions variable, which contains the text formatting functions for our directive. We'll capture the innerText of the element the directive will be used on and format it to uppercase, lowercase, or capitalized:

// src/directives/vFormat.js
export const vFormatText = {
  mounted: (el, binding) => {
    const modifier = Object.keys(binding.modifiers)[0];

    const actions = {
      uppercase() {
        el.innerHTML = el.innerHTML.toUpperCase();
      },
      lowercase() {
        el.innerHTML = el.innerHTML.toLowerCase();
      },
      capitalized() {
        const txt = el.innerHTML.split(" ");
        el.innerHTML = "";

        for (let i = 0; i < txt.length; i++) {
          el.innerHTML +=
            txt[i].substring(0, 1).toUpperCase() + txt[i].substring(1) + " ";
        }
      },
    };
  },
}
Enter fullscreen mode Exit fullscreen mode

4. Finally, let's identify the modifier and execute the function that corresponds to it:

// src/directives/vFormat.js
export const vFormatText = {
  mounted: (el, binding) => {
    const modifier = Object.keys(binding.modifiers)[0];

    const actions = {
      uppercase() {
        el.innerHTML = el.innerHTML.toUpperCase();
      },
      lowercase() {
        el.innerHTML = el.innerHTML.toLowerCase();
      },
      capitalized() {
        const txt = el.innerHTML.split(" ");
        el.innerHTML = "";

        for (let i = 0; i < txt.length; i++) {
          el.innerHTML +=
            txt[i].substring(0, 1).toUpperCase() + txt[i].substring(1) + " ";
        }
      },
    };

    if (modifier in actions) {
      const action = actions[modifier];
      action();
    }
  },
}
Enter fullscreen mode Exit fullscreen mode

Great! Our v-format directive is complete and ready to be used in a component. Let's see an example:

<script setup>
import { vFormatar } from "@/directives/vFormatar.js"
</script>

<template>
  <p v-format.uppercase>My text</p> <!-- "MY TEXT" -->
  <p v-format.lowercase>My text</p> <!-- "my text" -->
  <p v-format.capitalized>My text</p> <!-- "My Text" -->
</template>
Enter fullscreen mode Exit fullscreen mode

How about seeing how this directive would be with the Options API?

// src/directives/vFormat.js
export default {
  mounted: function(el, binding) {
    const modifier = Object.keys(binding.modifiers)[0]
    const actions = {
      uppercase() {
        el.innerHTML = el.innerHTML.toUpperCase()
      },
      lowercase() {
        el.innerHTML = el.innerHTML.toLowerCase()
      },
      capitalized() {
        let txt = el.innerHTML.split(' ')
        el.innerHTML = ''

        for (let i = 0; i < txt.length; i++) {
          el.innerHTML += txt[i].substring(0, 1).toUpperCase() + txt[i].substring(1) + ' '
        }
      },
    }

    if(modifier in actions) {
      const action = actions[modifier]
      action()
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

Using the directive in a component:

<script>
import vFormat from "@/directives/vFormat"

export default {
  directives: { format: vFormat }
}
</script>

<template>
  <p v-format.uppercase>My text</p> <!-- "MY TEXT" -->
  <p v-format.lowercase>My text</p> <!-- "my text" -->
  <p v-format.capitalized>My text</p> <!-- "My Text" -->
</template>
Enter fullscreen mode Exit fullscreen mode

And always remember that you can also register the directive globally in the main.js file:

import vFormat from "./directives/vFormat";

const app = createApp(App)
app.directive('format', vFormat)
app.mount('#app')
Enter fullscreen mode Exit fullscreen mode

Back to table of contents


Phew! I think we've seen enough...

Creating custom directives which are more complet may seem a bit confusing at first, but nothing that practice can't solve!

Knowing how to use Vue's built-in directives will be essential for your Vue application to always have great performance when dealing with component rendering and DOM manipulation, as well as being great for your developer experience, making your work easier and your code more elegant.

However, never say never, my young Padawan. In more complex situations, you may realize that a querySelector can still be a lifesaver from time to time!

I hope this article is helpful. See you next time!

💖 💪 🙅 🚩
sucodelarangela
Angela Caldas

Posted on October 1, 2024

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related