VueJS - Directives
VueJS - Custom Directives
Section titled “VueJS - Custom Directives”Directives are special tokens in the markup (v- prefix) that tell the library to do something to a DOM element. While Vue has many built-in directives (v-if, v-for, v-bind, v-model, v-on, etc.), you can also register your own custom directives for reusable DOM manipulation logic.
Custom directives are primarily intended for direct, low-level DOM manipulation when you need to encapsulate logic that doesn’t cleanly fit into components or composables.
Creating Custom Directives
Section titled “Creating Custom Directives”You can register custom directives globally using app.directive() or locally within a component’s directives option or directly within <script setup>.
Directive Hook Functions
Section titled “Directive Hook Functions”A directive definition object can provide several hook functions (all optional):
created(el, binding, vnode, prevVnode): Called before the element’s attributes or event listeners are applied.beforeMount(el, binding, vnode, prevVnode): Called right before the element is inserted into the DOM.mounted(el, binding, vnode, prevVnode): Called when the element is inserted into the DOM. Often used for setup that requires the element to be present (e.g., adding non-Vue event listeners, initializing a third-party library).beforeUpdate(el, binding, vnode, prevVnode): Called before the element itself is updated (e.g., due to reactive data changes).updated(el, binding, vnode, prevVnode): Called after the element and its children have updated.beforeUnmount(el, binding, vnode, prevVnode): Called right before the element is removed from the DOM. Ideal for cleanup (e.g., removing event listeners, destroying library instances).unmounted(el, binding, vnode, prevVnode): Called when the element has been removed from the DOM.
Hook Arguments
Section titled “Hook Arguments”el: The DOM element the directive is bound to. You can directly manipulate this.binding: An object containing properties like:-
value: The value passed to the directive (e.g.,v-my-directive="theValue").
-
oldValue: The previous value (only inbeforeUpdateandupdated).
-
arg: The argument passed (e.g.,v-my-directive:foo).
-
modifiers: An object containing modifiers (e.g.,v-my-directive.bar.baz).
-
instance: The instance of the component where the directive is used.
-
dir: The directive definition object.
vnode: The underlying virtual node representing the element.prevVnode: The previous virtual node (only inbeforeUpdateandupdated).
Example: Custom Focus Directive
Section titled “Example: Custom Focus Directive”Let’s create a simple v-focus directive that automatically focuses an input element when it’s mounted.
<!DOCTYPE html><html lang="en"><head> <meta charset="UTF-8"> <title>Vue 3 Custom Directive Example</title> <script src="https://unpkg.com/vue@3"></script></head><body> <div id="app"> <p>Click the button to mount the input field:</p> <button @click="showInput = !showInput">Toggle Input</button> <br><br> <input type="text" v-if="showInput" v-focus placeholder="I should have focus when I appear"> </div>
<script> const { createApp, ref } = Vue;
const app = createApp({ setup() { const showInput = ref(false); return { showInput }; } });
// Register the custom directive globally app.directive('focus', { // Use 'mounted' hook, as the element needs to be in the DOM to be focused mounted(el) { console.log('v-focus mounted on:', el); el.focus(); // Directly manipulate the DOM element } // Note: For components that might re-render, you might also need 'updated' // if the element could potentially lose focus and needs it reapplied. });
app.mount('#app'); </script></body></html>In this example:
- We define a global directive named
focususingapp.directive(). - The
mountedhook is used because focusing an element requires it to be present in the DOM. - Inside
mounted, we simply callel.focus()to focus the input element. - When the input is conditionally rendered using
v-ifand appears on the page, themountedhook ofv-focusruns, and the input gets focus.
Example: Directive with Value and Argument
Section titled “Example: Directive with Value and Argument”Let’s create a v-pin directive that positions an element fixed and pins it to a side of the screen based on an argument, with an optional top offset value.
<!DOCTYPE html><html lang="en"><head> <meta charset="UTF-8"> <title>Vue 3 Directive with Value/Arg</title> <script src="https://unpkg.com/vue@3"></script> <style> body { height: 200vh; } /* Ensure scrolling */ .pinned-element { background-color: lightcoral; padding: 15px; border-radius: 5px; color: white; } </style></head><body> <div id="app"> <p>Scroll down to see the pinned elements.</p>
<!-- Pin to top, 20px offset --> <div v-pin:top="20" class="pinned-element"> Pinned to Top (20px offset) </div>
<!-- Pin to bottom, default 0px offset --> <div v-pin:bottom class="pinned-element" style="background-color: lightseagreen;"> Pinned to Bottom (default offset) </div>
</div>
<script> const { createApp } = Vue;
const app = createApp({});
app.directive('pin', { mounted(el, binding) { console.log('v-pin mounted. Binding:', binding); el.style.position = 'fixed';
// Get the argument (e.g., 'top' or 'bottom') const side = binding.arg || 'top'; // Default to 'top' if no arg
// Get the value (the offset) const offset = typeof binding.value === 'number' ? binding.value : 0; // Default to 0 if no value or not a number
// Apply style based on argument and value el.style[side] = offset + 'px';
// Set opposite side based on the primary side to avoid width issues if (side === 'top' || side === 'bottom') { el.style.left = '10px'; // Add some horizontal positioning el.style.right = '10px'; } else { // Could extend for left/right pinning too el.style.top = '10px'; } }, // Optional: Handle updates if the value/argument could change reactively updated(el, binding) { if (binding.value !== binding.oldValue || binding.arg !== binding.oldArg) { console.log('v-pin updated. Binding:', binding); const side = binding.arg || 'top'; const offset = typeof binding.value === 'number' ? binding.value : 0; el.style[side] = offset + 'px'; } } });
app.mount('#app'); </script></body></html>Here:
- The
v-pindirective uses thebindingobject. binding.arggives us the argument ('top'or'bottom').binding.valuegives us the value passed (20for the first div,undefinedfor the second).- We apply
position: fixedand set the appropriate style (toporbottom) based on the argument and value. - An
updatedhook is added to handle potential reactive changes to the offset value or argument (though not demonstrated with reactive data in this simple example).
Local Directives (Section titled “Local Directives ()”
You can easily define directives locally within a component using <script setup>. Any camelCase variable starting with v is treated as a custom directive.
<script setup>import { ref } from 'vue';
// Directive definition (must start with 'v')const vHighlight = { mounted: (el, binding) => { el.style.backgroundColor = binding.value || 'yellow'; // Default to yellow }, updated: (el, binding) => { if (binding.value !== binding.oldValue) { el.style.backgroundColor = binding.value || 'yellow'; } }};
const highlightColor = ref('lightgreen');</script>
<template> <p v-highlight="highlightColor">This text will be highlighted.</p> <p v-highlight>This uses the default highlight color.</p> <button @click="highlightColor = 'lightblue'">Change Highlight</button></template>This approach keeps the directive logic colocated with the component that uses it, improving organization for directives not intended for global reuse.
A Note on Filters (Removed in Vue 3)
Section titled “A Note on Filters (Removed in Vue 3)”Vue 2 had a feature called ‘Filters’ used for common text formatting within template expressions (e.g., {{ message | capitalize }}). Filters have been removed from Vue 3.
The recommended replacements for filters in Vue 3 are:
- Computed Properties: For transformations based on reactive data.
- Methods: For more complex formatting or formatting needed directly within event handlers or other methods.
Example: Replacing a ‘capitalize’ Filter
Section titled “Example: Replacing a ‘capitalize’ Filter”<script setup>import { ref, computed } from 'vue';
const message = ref('hello vue 3');
// Option 1: Computed Propertyconst capitalizedMessage = computed(() => { if (!message.value) return ''; const firstChar = message.value.charAt(0).toUpperCase(); const rest = message.value.slice(1); return firstChar + rest;});
// Option 2: Methodfunction formatAsCurrency(value) { if (typeof value !== 'number') return value; return '$' + value.toFixed(2);}
const price = ref(29.99);</script>
<template> <input v-model="message" />
<p>Original: {{ message }}</p>
<!-- Using computed property --> <p>Capitalized (Computed): {{ capitalizedMessage }}</p>
<!-- Using method --> <p>Price: {{ formatAsCurrency(price) }}</p></template>Using computed properties or methods provides the same functionality as filters but integrates more naturally with standard JavaScript and Vue’s reactivity system.
Further Reading: Vue’s official documentation on Custom Directives: https://vuejs.org/guide/reusability/custom-directives.html