Vue.js Custom Directives
Introduction
While Vue.js provides several built-in directives like v-if
, v-for
, and v-model
that cover most common use cases, sometimes you need special functionality that interacts directly with the DOM in ways not covered by these standard tools. Vue's custom directives system allows you to register your own directives to perform low-level DOM manipulations.
Custom directives are perfect when you need to:
- Access and manipulate DOM elements directly
- Create reusable DOM manipulation logic
- Integrate with third-party libraries that manipulate the DOM
- Implement behaviors not easily achievable with standard Vue features
Understanding Custom Directives
Basic Syntax
A Vue.js custom directive has the following registration syntax:
// Global registration
app.directive('my-directive', {
// Directive hooks go here
})
// Local registration in a component
export default {
directives: {
'my-directive': {
// Directive hooks go here
}
}
}
And you use it in your template like this:
<div v-my-directive></div>
Directive Hooks
Custom directives have several lifecycle hooks that are called at different stages:
created
: Called before the element's attributes or event listeners are appliedbeforeMount
: Called when the directive is first bound to the elementmounted
: Called after the element is inserted into the parent DOMbeforeUpdate
: Called before the containing component's VNode is updatedupdated
: Called after the containing component's VNode and the children's VNodes have updatedbeforeUnmount
: Called before the element is removed from the DOMunmounted
: Called when the directive is unbound from the element
Each hook receives the following arguments:
el
: The element the directive is bound tobinding
: An object containing various properties related to the directivevnode
: The virtual node representing the elementprevVnode
: The previous virtual node (only available inbeforeUpdate
andupdated
)
Creating Your First Custom Directive
Let's start with a simple example - a directive that automatically focuses an input element when it appears on the page:
// Global registration
app.directive('focus', {
mounted(el) {
el.focus()
}
})
<!-- Usage in a template -->
<input v-focus type="text" placeholder="I'll be focused automatically">
This directive will automatically focus the input element when it's inserted into the DOM.
Passing Values to Custom Directives
You can pass values to your directives using arguments, modifiers, and values:
app.directive('color', {
mounted(el, binding) {
// binding.value contains the value passed to the directive
el.style.color = binding.value || 'blue'
},
updated(el, binding) {
el.style.color = binding.value || 'blue'
}
})
<p v-color="'red'">This text will be red</p>
<p v-color="textColor">This text will use the textColor value from data</p>
The Binding Object
The binding object provides several properties that give you information about how the directive was used:
value
: The value passed to the directiveoldValue
: The previous value (only available inbeforeUpdate
andupdated
)arg
: Any arguments passed to the directivemodifiers
: An object containing any modifiers usedinstance
: The component instance where the directive is useddir
: The directive definition object
Here's an example using these properties:
app.directive('highlight', {
mounted(el, binding) {
// Extract the properties from binding
const { value, arg, modifiers } = binding
// Default color is yellow
let color = 'yellow'
// If an argument is provided, use it as the color
if (arg) {
color = arg
}
// Apply background or text highlighting based on modifiers
if (modifiers.background) {
el.style.backgroundColor = value || color
} else {
el.style.color = value || color
}
// If bold modifier is present, make text bold
if (modifiers.bold) {
el.style.fontWeight = 'bold'
}
}
})
Usage examples:
<!-- Basic usage with default color (yellow) -->
<p v-highlight>Highlighted text</p>
<!-- With a color argument -->
<p v-highlight:red>Red highlighted text</p>
<!-- With a dynamic value -->
<p v-highlight="color">Dynamic color</p>
<!-- With modifiers -->
<p v-highlight.background>Yellow background</p>
<p v-highlight:blue.bold.background>Bold text with blue background</p>
Function Shorthand
For simple directives that only need the mounted
and updated
hooks with the same behavior, you can use the function shorthand:
app.directive('simple', (el, binding) => {
// This will be called for both mounted and updated
el.textContent = binding.value
})
<span v-simple="message"></span>
Practical Examples
Example 1: Clickable Outside Directive
This directive detects clicks outside of an element, which is useful for closing dropdowns or modals:
app.directive('click-outside', {
mounted(el, binding) {
el._clickOutsideHandler = (event) => {
// Check if the click was outside the element
if (!(el === event.target || el.contains(event.target))) {
// Call the provided method
binding.value(event)
}
}
document.addEventListener('click', el._clickOutsideHandler)
},
unmounted(el) {
// Clean up event listener when element is removed
document.removeEventListener('click', el._clickOutsideHandler)
delete el._clickOutsideHandler
}
})
Usage:
<template>
<div class="dropdown">
<button @click="isOpen = !isOpen">Toggle Dropdown</button>
<div v-if="isOpen" v-click-outside="closeDropdown" class="dropdown-menu">
<a href="#">Option 1</a>
<a href="#">Option 2</a>
<a href="#">Option 3</a>
</div>
</div>
</template>
<script>
export default {
data() {
return {
isOpen: false
}
},
methods: {
closeDropdown() {
this.isOpen = false
}
}
}
</script>
Example 2: Tooltip Directive
Here's a custom directive that creates a simple tooltip:
app.directive('tooltip', {
mounted(el, binding) {
// Create the tooltip element
const tooltip = document.createElement('div')
tooltip.className = 'tooltip'
tooltip.textContent = binding.value
tooltip.style.display = 'none'
// Position style
tooltip.style.position = 'absolute'
tooltip.style.padding = '6px 12px'
tooltip.style.backgroundColor = 'rgba(0, 0, 0, 0.7)'
tooltip.style.color = 'white'
tooltip.style.borderRadius = '4px'
tooltip.style.fontSize = '14px'
tooltip.style.zIndex = '9999'
// Add tooltip to the document body
document.body.appendChild(tooltip)
// Store tooltip reference
el._tooltip = tooltip
// Show the tooltip on mouseenter
el.addEventListener('mouseenter', () => {
const rect = el.getBoundingClientRect()
tooltip.style.display = 'block'
// Position the tooltip
if (binding.arg === 'top') {
tooltip.style.top = `${rect.top - tooltip.offsetHeight - 5}px`
tooltip.style.left = `${rect.left + rect.width / 2 - tooltip.offsetWidth / 2}px`
} else {
tooltip.style.top = `${rect.bottom + 5}px`
tooltip.style.left = `${rect.left + rect.width / 2 - tooltip.offsetWidth / 2}px`
}
})
// Hide the tooltip on mouseleave
el.addEventListener('mouseleave', () => {
tooltip.style.display = 'none'
})
},
updated(el, binding) {
// Update tooltip text if it changes
el._tooltip.textContent = binding.value
},
unmounted(el) {
// Remove the tooltip when element is removed
document.body.removeChild(el._tooltip)
delete el._tooltip
}
})
Usage:
<button v-tooltip="'Delete this item'">Delete</button>
<button v-tooltip:top="'Edit this item'">Edit</button>
Local Registration of Custom Directives
You can register directives locally within a specific component:
<template>
<div>
<input v-local-focus>
</div>
</template>
<script>
export default {
directives: {
'local-focus': {
mounted(el) {
el.focus()
}
}
}
}
</script>
This is useful when the directive is only needed in a specific component and doesn't need to be available globally.
Best Practices for Custom Directives
-
Do one thing well: Each directive should have a single, well-defined purpose
-
Clean up after yourself: Always remove event listeners and clean up any DOM manipulations in the
unmounted
hook -
Use computed properties or methods when possible: Custom directives are for DOM manipulation - if you're just transforming data, consider other Vue features
-
Name consistently: Prefix your directives with a common name (like
v-app-*
) to avoid conflicts with future Vue directives -
Document your directives: Add clear comments explaining what your directive does, what arguments and modifiers it accepts
Directive Lifecycle Flow
Summary
Custom directives in Vue.js provide a powerful way to create reusable DOM manipulation logic. They allow you to extend Vue's capabilities beyond the standard features, letting you interact directly with the DOM when necessary. With the various hooks and binding properties available, you can create complex behaviors that respond to changing data or element states.
Key points to remember:
- Use custom directives when you need direct DOM access
- Directives have lifecycle hooks that let you control when your code runs
- You can pass values, arguments, and modifiers to directives
- Always clean up resources in the unmounted hook
- Register directives globally or locally depending on your needs
Exercises
-
Create a
v-longpress
directive that triggers a function when a user holds down on an element for more than 1 second. -
Build a
v-animate
directive that applies a CSS animation when an element enters the viewport. -
Implement a
v-copy
directive that copies text to the clipboard when clicked and shows a temporary success message. -
Create a
v-resize
directive that watches for element size changes and calls a callback function. -
Make a
v-scroll-lock
directive that prevents page scrolling when a modal is open.
Additional Resources
If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)