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-else
are completely destroyed or reconstructed in the DOM according to the condition met, unlikev-show
which 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-for
directive, we iterate over this array using thevariable in expression
syntax to access each element of the iteration; - each
user
will be an object from our array. Thus, we will have twousers
, resulting in two paragraphs; - each paragraph will render the value of the
name
key 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
imgSrc
state used as thesrc
attribute of an image, as well as amyClasses
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.
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-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>
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!