Custom directives in Vue 3 provide a powerful mechanism for direct DOM manpiulation and reusable behavior encapsulation. When registered globally, these directives become available across all components without additional imports. The following implementations demonstrate common patterns for permissions, performence optimization, and UI enhancements.
Permission Control
This directive toggles element visibility based on access rights. It compares the required permission against a predefined list of user capabilities.
directives/permission.js
import { ref, watchEffect } from 'vue';
const checkAccess = (requiredRole) => {
// Mock user roles retrieved from authentication service
const currentRoles = ['admin', 'editor'];
return currentRoles.includes(requiredRole);
};
export default {
mounted(element, binding) {
const requiredPermission = binding.value;
const isVisible = ref(false);
watchEffect(() => {
isVisible.value = checkAccess(requiredPermission);
});
element.style.display = isVisible.value ? '' : 'none';
}
};
components/AccessControl.vue
<template>
<div>
<button v-permission="'admin'">Admin Panel</button>
<button v-permission="'editor'">Edit Content</button>
<button v-permission="'guest'">Guest Area</button>
</div>
</template>
<script setup>
// Component logic
</script>
Input Debouncing
Reduce the frequency of event execution, particularly useful for search inputs or API calls triggered by user typing.
directives/debounce.js
export default {
mounted(element, binding) {
const config = binding.value;
if (typeof config.handler !== 'function' || !config.type) return;
let timeoutId = null;
const execute = function(...args) {
if (timeoutId) clearTimeout(timeoutId);
timeoutId = setTimeout(() => {
config.handler.apply(this, args);
timeoutId = null;
}, config.wait || 400);
};
element.addEventListener(config.type, execute);
element._debounceHandler = execute;
},
beforeUnmount(element, binding) {
if (element._debounceHandler) {
element.removeEventListener(binding.value.type, element._debounceHandler);
}
}
};
components/SearchInput.vue
<template>
<input
v-model="query"
v-debounce="searchConfig"
placeholder="Type to search..."
/>
</template>
<script setup>
import { ref } from 'vue';
const query = ref('');
const searchConfig = {
type: 'input',
wait: 600,
handler: (event) => {
console.log('Searching for:', event.target.value);
}
};
</script>
Event Throttling
Unlike debouncing, throttling ensures a function executes at a steady interval. This is ideal for scroll events or window resizing.
directives/throttle.js
export default {
mounted(element, binding) {
const { handler, type, interval } = binding.value;
if (typeof handler !== 'function' || !type) return;
let lastExecution = 0;
let timeoutId = null;
const throttledFn = function(...args) {
const now = Date.now();
const remaining = interval - (now - lastExecution);
if (remaining <= 0) {
if (timeoutId) {
clearTimeout(timeoutId);
timeoutId = null;
}
lastExecution = now;
handler.apply(this, args);
} else if (!timeoutId) {
timeoutId = setTimeout(() => {
lastExecution = Date.now();
handler.apply(this, args);
timeoutId = null;
}, remaining);
}
};
element.addEventListener(type, throttledFn);
element._throttleHandler = throttledFn;
},
beforeUnmount(element, binding) {
if (element._throttleHandler) {
element.removeEventListener(binding.value.type, element._throttleHandler);
}
}
};
components/ThrottleButton.vue
<template>
<button v-throttle="clickConfig">Submit</button>
</template>
<script setup>
const clickConfig = {
type: 'click',
interval: 2000,
handler: () => {
console.log('Action executed');
}
};
</script>
Element Resize Observation
Monitor changes in an element's dimensions to trigger layout adjustments or data fetching.
directives/resize.js
export default {
mounted(element, binding) {
const callback = binding.value;
const observer = new ResizeObserver((entries) => {
for (let entry of entries) {
const { width, height } = entry.contentRect;
callback({ width, height });
}
});
observer.observe(element);
element._resizeObserver = observer;
},
beforeUnmount(element) {
if (element._resizeObserver) {
element._resizeObserver.disconnect();
}
}
};
components/ResizableBox.vue
<template>
<div v-resize="onResize" style="width: 50%; height: 200px; background: #eee;">
Resize this window
</div>
</template>
<script setup>
const onResize = (size) => {
console.log(`New dimensions: ${size.width}x${size.height}`);
};
</script>
Infinite Scroll Loading
Detect when a user scrolls near the bottom of a container to load additional data.
directives/load-more.js
export default {
mounted(element, binding) {
const { action, offset = 150 } = binding.value;
const onScroll = () => {
const { scrollTop, scrollHeight, clientHeight } = element;
if (scrollHeight - scrollTop - clientHeight <= offset) {
action();
}
};
element.addEventListener('scroll', onScroll);
element._scrollHandler = onScroll;
},
beforeUnmount(element) {
if (element._scrollHandler) {
element.removeEventListener('scroll', element._scrollHandler);
}
}
};
components/ListContainer.vue
<template>
<div
v-load-more="scrollConfig"
style="height: 300px; overflow-y: auto; border: 1px solid #ddd;"
>
<div v-for="n in 20" :key="n">Item {{ n }}</div>
</div>
</template>
<script setup>
const scrollConfig = {
offset: 100,
action: () => {
console.log('Loading more items...');
}
};
</script>
Image Lazy Loading
Defer loading images until they enter the viewport to improve initial page load performance.
directives/lazy-img.js
export default {
mounted(element, binding) {
const observer = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
const img = element;
img.src = binding.value;
observer.unobserve(img);
}
});
}, { rootMargin: '50px' });
observer.observe(element);
element._lazyObserver = observer;
},
beforeUnmount(element) {
if (element._lazyObserver) {
element._lazyObserver.disconnect();
}
}
};
components/Gallery.vue
<template>
<img
v-lazy-img="imageUrl"
alt="Content"
style="width: 100%; height: auto;"
/>
</template>
<script setup>
const imageUrl = 'https://example.com/image.jpg';
</script>
Input Validation Mask
Restrict user input based on a regular expression pattern, such as allowing only numbers or specific characters.
directives/input-mask.js
export default {
mounted(element, binding) {
const pattern = binding.value;
const onInput = (e) => {
const value = e.target.value;
const valid = pattern.test(value);
if (!valid) {
e.target.value = value.slice(0, -1);
}
};
element.addEventListener('input', onInput);
element._maskHandler = onInput;
},
beforeUnmount(element) {
if (element._maskHandler) {
element.removeEventListener('input', element._maskHandler);
}
}
};
components/NumberInput.vue
<template>
<input
type="text"
v-input-mask="/^[0-9]*$/"
placeholder="Numbers only"
/>
</template>
Draggable Elements
Enable any DOM element to be moved freely within its container using mouse events.
directives/drag.js
export default {
mounted(element) {
let startX = 0;
let startY = 0;
let initialLeft = 0;
let initialTop = 0;
const onMouseDown = (e) => {
startX = e.clientX;
startY = e.clientY;
const rect = element.getBoundingClientRect();
initialLeft = rect.left;
initialTop = rect.top;
document.addEventListener('mousemove', onMouseMove);
document.addEventListener('mouseup', onMouseUp);
};
const onMouseMove = (e) => {
const dx = e.clientX - startX;
const dy = e.clientY - startY;
element.style.left = `${initialLeft + dx}px`;
element.style.top = `${initialTop + dy}px`;
};
const onMouseUp = () => {
document.removeEventListener('mousemove', onMouseMove);
document.removeEventListener('mouseup', onMouseUp);
};
element.style.position = 'absolute';
element.style.cursor = 'move';
element.addEventListener('mousedown', onMouseDown);
element._dragHandler = onMouseDown;
},
beforeUnmount(element) {
// Cleanup if necessary
}
};
components/DraggableCard.vue
<template>
<div v-drag style="width: 100px; height: 100px; background: blue;">
Drag
</div>
</template>
Dynamic Watermarking
Generate a background watermark using Canvas to protect content or indicate document status.
directives/watermark.js
const createWatermark = (text, container, color, fontSize) => {
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d');
canvas.width = 300;
canvas.height = 200;
ctx.translate(canvas.width / 2, canvas.height / 2);
ctx.rotate((-25 * Math.PI) / 180);
ctx.font = `${fontSize || 14}px sans-serif`;
ctx.fillStyle = color || 'rgba(0, 0, 0, 0.1)';
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
ctx.fillText(text, 0, 0);
container.style.backgroundImage = `url(${canvas.toDataURL()})`;
container.style.backgroundRepeat = 'repeat';
};
export default {
mounted(element, binding) {
const { text, color, fontSize } = binding.value;
createWatermark(text, element, color, fontSize);
}
};
components/SecureDoc.vue
<template>
<div
v-watermark="wmConfig"
style="width: 100%; height: 400px; border: 1px solid #ccc;"
>
Confidential Content
</div>
</template>
<script setup>
const wmConfig = {
text: 'CONFIDENTIAL',
color: 'rgba(200, 0, 0, 0.2)',
fontSize: 20
};
</script>
Global registration in main.js ensures thece directives are accessible throughout the application lifecycle without individual component imports.