Кастомные директивы
Что это
Директива — v-xxx для работы с DOM. Встроенные: v-if, v-for, v-model. Можно делать свои.
Когда нужны
Когда есть прямая работа с DOM:
- Фокус
- Слушатели
- Сторонние библиотеки
- Анимации
Не злоупотребляй. 90% задач — компоненты и composables.
Простая директива
// main.js
const app = createApp(App);
app.directive('focus', {
mounted(el) {
el.focus();
},
});
app.mount('#app');
Использование:
<input v-focus>
Хуки директивы
created— до вставки в DOMbeforeMount— перед вставкойmounted— после вставкиbeforeUpdate— перед обновлениемupdated— после обновленияbeforeUnmount— перед удалениемunmounted— после удаления
Обычно используют: mounted, updated, unmounted.
Аргументы хуков
app.directive('demo', {
mounted(el, binding, vnode, prevVnode) {
// el — DOM-элемент
// binding — объект с value, arg, modifiers
// vnode — виртуальный узел
// prevVnode — предыдущий (в updated)
},
});
binding объект
<div v-demo:foo.bar="baz">
binding.value; // 'baz'
binding.arg; // 'foo'
binding.modifiers; // { bar: true }
binding.instance; // экземпляр компонента
Пример — v-focus
app.directive('focus', {
mounted(el) {
el.focus();
},
});
Использование:
<input v-focus placeholder="Автофокус">
v-click-outside
app.directive('click-outside', {
mounted(el, binding) {
el._handler = (e) => {
if (!el.contains(e.target)) {
binding.value(e);
}
};
document.addEventListener('click', el._handler);
},
unmounted(el) {
document.removeEventListener('click', el._handler);
},
});
Использование:
<div v-click-outside="close">
<button @click="open = !open">Меню</button>
<ul v-if="open">...</ul>
</div>
v-debounce
app.directive('debounce', {
mounted(el, binding) {
const delay = binding.arg ? Number(binding.arg) : 300;
let timeout;
el._handler = () => {
clearTimeout(timeout);
timeout = setTimeout(() => binding.value(), delay);
};
el.addEventListener('input', el._handler);
},
unmounted(el) {
el.removeEventListener('input', el._handler);
},
});
Использование:
<input v-debounce:500="search">
Директива с модификаторами
app.directive('style', {
mounted(el, binding) {
el.style.color = binding.value;
if (binding.modifiers.bold) {
el.style.fontWeight = 'bold';
}
if (binding.modifiers.italic) {
el.style.fontStyle = 'italic';
}
},
updated(el, binding) {
el.style.color = binding.value;
},
});
Использование:
<p v-style.bold.italic="'red'">Красный жирный курсив</p>
Директива с объектом
app.directive('tooltip', {
mounted(el, binding) {
const opts = typeof binding.value === 'string'
? { text: binding.value }
: binding.value;
el._tooltip = document.createElement('div');
el._tooltip.className = 'tooltip';
el._tooltip.textContent = opts.text;
el._tooltip.style.position = 'absolute';
document.body.appendChild(el._tooltip);
el._show = () => {
el._tooltip.style.display = 'block';
};
el._hide = () => {
el._tooltip.style.display = 'none';
};
el.addEventListener('mouseenter', el._show);
el.addEventListener('mouseleave', el._hide);
},
unmounted(el) {
el.removeEventListener('mouseenter', el._show);
el.removeEventListener('mouseleave', el._hide);
el._tooltip?.remove();
},
});
Использование:
<button v-tooltip="'Кнопка сохранения'">Сохранить</button>
<button v-tooltip="{ text: 'Удалить', color: 'red' }">Удалить</button>
Локальные директивы
В <script setup> — с префиксом v:
<script setup>
import { ref } from 'vue';
const vFocus = {
mounted(el) {
el.focus();
},
};
</script>
<template>
<input v-focus>
</template>
Правило: переменная начинается с v → Vue считает её директивой.
Локальная с параметрами
<script setup>
const vColor = {
mounted(el, binding) {
el.style.color = binding.value;
},
updated(el, binding) {
el.style.color = binding.value;
},
};
</script>
<template>
<p v-color="'red'">Красный</p>
</template>
Регистрация глобально
main.js:
const app = createApp(App);
app.directive('focus', {
mounted(el) { el.focus(); },
});
app.mount('#app');
В отдельном файле — directives/index.js:
import focus from './focus';
import clickOutside from './clickOutside';
export default {
install(app) {
app.directive('focus', focus);
app.directive('click-outside', clickOutside);
},
};
main.js:
import directives from './directives';
app.use(directives);
Async директивы
app.directive('chart', {
async mounted(el, binding) {
const { Chart } = await import('chart.js/auto');
el._chart = new Chart(el, binding.value);
},
unmounted(el) {
el._chart?.destroy();
},
});
Библиотека грузится только когда нужна.
v-lazy — ленивая загрузка
app.directive('lazy', {
mounted(el, binding) {
const observer = new IntersectionObserver(([entry]) => {
if (entry.isIntersecting) {
el.src = binding.value;
observer.unobserve(el);
}
});
observer.observe(el);
el._observer = observer;
},
updated(el, binding) {
if (binding.value !== binding.oldValue) {
el.src = binding.value;
}
},
unmounted(el) {
el._observer?.disconnect();
},
});
Использование:
<img v-lazy="user.avatar" alt="Аватар">
Утечки памяти
⚠️ Обязательно очищай в unmounted:
removeEventListenerclearIntervalclearTimeoutobserver.disconnect()- DOM-элементы, созданные директивой
Плохо:
mounted(el) {
document.addEventListener('scroll', () => {}); // не убрать
}
Хорошо:
mounted(el) {
el._handler = () => {};
document.addEventListener('scroll', el._handler);
},
unmounted(el) {
document.removeEventListener('scroll', el._handler);
}
Использование vnode
mounted(el, binding, vnode) {
// vnode.props
// vnode.children
// vnode.component
}
Редко нужно.
Полный пример — v-tooltip
// directives/tooltip.js
import './tooltip.css';
export default {
mounted(el, binding) {
const { text, position = 'top' } = typeof binding.value === 'string'
? { text: binding.value }
: binding.value;
const tooltip = document.createElement('div');
tooltip.className = `custom-tooltip custom-tooltip--${position}`;
tooltip.textContent = text;
document.body.appendChild(tooltip);
const show = () => {
const rect = el.getBoundingClientRect();
tooltip.style.top = `${rect.top - 40}px`;
tooltip.style.left = `${rect.left + rect.width / 2}px`;
tooltip.classList.add('visible');
};
const hide = () => tooltip.classList.remove('visible');
el._show = show;
el._hide = hide;
el._tooltip = tooltip;
el.addEventListener('mouseenter', show);
el.addEventListener('mouseleave', hide);
},
updated(el, binding) {
const text = typeof binding.value === 'string'
? binding.value
: binding.value.text;
el._tooltip.textContent = text;
},
unmounted(el) {
el.removeEventListener('mouseenter', el._show);
el.removeEventListener('mouseleave', el._hide);
el._tooltip?.remove();
},
};
Практическое правило
Используй директивы только для:
- Работы с DOM напрямую
- Обёрток над библиотеками (Chart.js, Select2)
- Глобальных эффектов (focus, tooltip, lazy)
Всё остальное — компоненты или composables.
Итог
app.directiveили локально черезvXxx- Хуки:
mounted,updated,unmounted binding.value,arg,modifiers- Очистка в
unmounted— обязательна - Не злоупотребляй — большинство задач — компоненты
Дальше — плагины и миксины.