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?
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).
v-text
<h1 v-text="title"></h1>
<!-- is the same as -->
<h1>{{ title }}</h1>
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.
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>
-->
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.
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>
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.
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 />
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, andv-elseare completely destroyed or reconstructed in the DOM according to the condition met, unlikev-showwhich only changes the element'sdisplay.
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>
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-fordirective, we iterate over this array using thevariable in expressionsyntax to access each element of the iteration; - each
userwill be an object from our array. Thus, we will have twousers, resulting in two paragraphs; - each paragraph will render the value of the
namekey of eachuser.
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>
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" />
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).
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" />
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
imgSrcstate used as thesrcattribute of an image, as well as amyClassesstate 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
isValidstate is truthy; - An example of a
myPropprop being passed to a child component.
v-model
<script setup>
import { ref } from "vue"
const message = ref("")
</script>
<template>
<input type="text" v-model="message" />
</template>
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" />
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>
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.
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>
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 }}).
v-once
<div v-once>
<h1>Comment</h1>
<p>{{msg}}</p>
</div>
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.
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>
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-memodirective 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>
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>
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.
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>
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?
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()
};
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>
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')
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 invFocus); and -
binding: Object containing various properties that we can use in our directives, such asvalue(the value passed in the directive) andmodifiers, 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 */
}
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) => {},
}
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];
},
}
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) + " ";
}
},
};
},
}
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();
}
},
}
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>
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()
}
}
}
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>
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')
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!


Top comments (1)
nice article!