In Vue, directives are special attributes prefixed with v- that apply reactive behavior directly to the DOM. While Vue provides built-in directives like v-model and v-show, developers can also define custom directives to encapsulate reusable DOM manipulation logic.
Custom directives can be registered globally or locally:
// Global registration
Vue.directive('focus', {
inserted(el) {
el.focus();
}
});
// Local registration inside a component
export default {
directives: {
focus: {
inserted(el) {
el.focus();
}
}
}
};
These directives support several lifecycle hooks:
bind: Called once when the directive is first bound to the element.inserted: Called when the bound element has been inserted into its parent node.update: Called when the component’s VNode is updated (before children).componentUpdated: Called after the component’s VNode and its children have updated.unbind: Called once when the directive is unbound from the element.
Each hook receives the following arguments:
el: The element the directive is bound to.binding: An object containing:name: Directive name (withoutv-).value: The value passed to the directive.oldValue: Previous value (available inupdateandcomponentUpdated).expression: String representation of the binding expression.arg: Optional argument passed to the directive (e.g.,v-my:arg).modifiers: Object of modifier flags (e.g.,{ foo: true }for.foo).
vnode: The virtual node produced by Vue’s compiler.oldVnode: Previous virtual node (inupdateandcomponentUpdated).
Avoid mutating any argument except
el. For cross-hook state, use the element’sdataset.
Example usage:
<div v-demo="{ color: 'white', text: 'hello!' }"></div>
Vue.directive('demo', (el, binding) => {
console.log(binding.value.color); // 'white'
console.log(binding.value.text); // 'hello!'
});
Common Use Cases
Debounced Click Handler
Prevent rapid clicks using a throttle-like directive:
Vue.directive('throttle', {
bind(el, binding) {
const delay = binding.value || 2000;
let timer = null;
const handleClick = (e) => {
if (timer) {
e.stopImmediatePropagation();
return;
}
timer = setTimeout(() => {
timer = null;
}, delay);
};
el.addEventListener('click', handleClick, true);
el._throttleHandler = handleClick;
},
unbind(el) {
el.removeEventListener('click', el._throttleHandler, true);
}
});
Use in template:
<button v-throttle="1500" @click="submit">Submit</button>
Image Lazy Loading
Load images only when they enter the viewport:
const LazyLoad = {
install(Vue, { default: placeholder }) {
Vue.directive('lazy', {
bind(el, { value }) {
el.dataset.src = value;
el.src = placeholder;
},
inserted(el) {
if ('IntersectionObserver' in window) {
const observer = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
const img = entry.target;
img.src = img.dataset.src;
img.removeAttribute('data-src');
observer.unobserve(img);
}
});
});
observer.observe(el);
} else {
// Fallback scroll-based loading
const load = () => {
const rect = el.getBoundingClientRect();
const inView = rect.top < window.innerHeight && rect.bottom >= 0;
if (inView && el.dataset.src) {
el.src = el.dataset.src;
el.removeAttribute('data-src');
}
};
load();
window.addEventListener('scroll', load);
el._lazyScrollHandler = load;
}
},
unbind(el) {
if (el._lazyScrollHandler) {
window.removeEventListener('scroll', el._lazyScrollHandler);
}
}
});
}
};
Vue.use(LazyLoad, { default: '/loading.gif' });
Template usage:
<img v-lazy="/real-image.jpg" />
One-Click Copy
Copy text to clipboard on click:
const copyDirective = {
bind(el, { value }) {
el._copyValue = value;
el._copyHandler = () => {
if (!el._copyValue) return;
const textarea = document.createElement('textarea');
textarea.value = el._copyValue;
textarea.readOnly = true;
textarea.style.cssText = 'position:absolute;left:-9999px;';
document.body.appendChild(textarea);
textarea.select();
const success = document.execCommand('copy');
document.body.removeChild(textarea);
if (success) alert('Copied!');
};
el.addEventListener('click', el._copyHandler);
},
componentUpdated(el, { value }) {
el._copyValue = value;
},
unbind(el) {
el.removeEventListener('click', el._copyHandler);
}
};
Vue.directive('copy', copyDirective);
Usage:
<span v-copy="'Hello world!'">Copy this</span>
Other practical applications include drag-and-drop handlers, dynamic watermarking, and fine-grained permission controls based on user roles.