tutoriales.com

Aprovechando las Directivas Personalizadas en Vue 3: Creando Comportamientos Reutilizables y Reactivos

Este tutorial te guiará a través del proceso de creación y uso de directivas personalizadas en Vue 3. Descubrirás cómo encapsular comportamientos DOM reutilizables y reactivos, mejorando la modularidad y el mantenimiento de tu código. Exploraremos desde las bases hasta ejemplos prácticos y el uso de modificadores.

Intermedio12 min de lectura24 views
Reportar error

Las directivas personalizadas en Vue 3 son una herramienta poderosa que te permite extender las capacidades del DOM al aplicar comportamientos reactivos directamente a tus elementos HTML. Piensa en ellas como atributos especiales que puedes añadir a cualquier etiqueta, <input>, <button>, <div>, para dotarlos de una lógica específica sin la necesidad de un componente completo.

Aunque las directivas v-if, v-for, v-bind (o :) y v-on (o @) son las más conocidas, Vue nos ofrece la flexibilidad de definir nuestras propias directivas para abstraer manipulaciones del DOM de bajo nivel o añadir funcionalidades interactivas que se pueden reutilizar fácilmente en diferentes partes de nuestra aplicación.

Este tutorial te sumergirá en el mundo de las directivas personalizadas, explicando su estructura, ciclo de vida, cómo registrarlas global o localmente, y cómo interactuar con ellas a través de argumentos y modificadores. Prepárate para llevar la reactividad de tus aplicaciones Vue al siguiente nivel. ✨


📖 ¿Qué Son las Directivas Personalizadas y Por Qué Usarlas?

En esencia, una directiva personalizada es una forma de aplicar efectos secundarios al DOM cuando el valor de una expresión reactiva cambia. Nos permiten manipular elementos del DOM de bajo nivel de forma declarativa. Esto significa que, en lugar de escribir código imperativo para cambiar estilos o añadir event listeners en la lógica de tus componentes, puedes encapsular esa lógica en una directiva y aplicarla directamente en tu plantilla HTML.

🎯 Beneficios Clave:

  • Reusabilidad: Define la lógica una vez y úsala en múltiples componentes sin duplicar código.
  • Abstracción: Separa la lógica de manipulación del DOM de la lógica del componente, haciendo el código más limpio y fácil de mantener.
  • Modularidad: Divide tu aplicación en piezas más pequeñas y manejables.
  • Reactividad: Las directivas personalizadas son reactivas, lo que significa que pueden reaccionar a los cambios en los datos de tu componente.
💡 Consejo: Usa directivas personalizadas para interacciones DOM simples y repetitivas. Para lógicas más complejas que involucren estado o renderizado condicional de sub-componentes, es preferible crear un componente Vue tradicional.

🛠️ Creando Tu Primera Directiva Personalizada

Vamos a empezar con un ejemplo sencillo: una directiva que cambia el color de fondo de un elemento.

📝 Estructura Básica de una Directiva

Una directiva es un objeto con una serie de hooks (ganchos) de ciclo de vida, similares a los de los componentes Vue.

const myCustomDirective = {
  // Se llama antes de que los atributos del elemento o los event listeners sean aplicados.
  created(el, binding, vnode, prevVnode) {},
  // Se llama antes de que el componente sea montado.
  beforeMount(el, binding, vnode, prevVnode) {},
  // Se llama cuando el elemento es insertado en el DOM.
  mounted(el, binding, vnode, prevVnode) {
    // Aquí es donde normalmente interactuamos con el DOM
  },
  // Se llama antes de que el componente que contiene el elemento montado sea actualizado.
  beforeUpdate(el, binding, vnode, prevVnode) {},
  // Se llama después de que el componente que contiene el elemento montado haya sido actualizado.
  updated(el, binding, vnode, prevVnode) {},
  // Se llama antes de que el elemento padre sea desmontado.
  beforeUnmount(el, binding, vnode, prevVnode) {},
  // Se llama cuando el elemento padre ha sido desmontado (limpieza de event listeners).
  unmounted(el, binding, vnode, prevVnode) {}
};

⚙️ Parámetros de los Hooks

Cada hook recibe los siguientes argumentos:

  1. el: El elemento DOM al que se aplica la directiva. Este es el principal elemento con el que interactuarás.
  2. binding: Un objeto que contiene propiedades sobre el binding de la directiva:
    • value: El valor pasado a la directiva (e.g., v-my-directive="valor").
    • oldValue: El valor anterior (solo disponible en beforeUpdate y updated).
    • arg: El argumento pasado a la directiva (e.g., v-my-directive:foo).
    • modifiers: Un objeto que contiene los modificadores (e.g., v-my-directive.bar.baz).
    • instance: La instancia del componente que usa la directiva.
    • dir: La definición de la directiva.
  3. vnode: El virtual node del elemento al que se ha aplicado la directiva.
  4. prevVnode: El virtual node anterior (solo disponible en beforeUpdate y updated).
📌 Nota: Los hooks `created` y `beforeMount` se usan con menos frecuencia para manipulación directa del DOM, ya que el elemento no está completamente insertado o procesado. `mounted` es el lugar más común para iniciar interacciones DOM.

🧑‍💻 Ejemplo Básico: v-highlight

Crearemos una directiva que resaltará un elemento con un color de fondo.

1. Definir la Directiva

// src/directives/highlight.js
export const highlightDirective = {
  mounted(el, binding) {
    el.style.backgroundColor = binding.value || 'yellow';
  },
  updated(el, binding) {
    // Si el valor cambia, actualizamos el color
    el.style.backgroundColor = binding.value || 'yellow';
  }
};

Aquí, estamos usando el hook mounted para aplicar el estilo cuando el elemento se inserta en el DOM. También incluimos updated para asegurarnos de que el color se actualice si el valor de la directiva cambia.

2. Registrar la Directiva

Las directivas se pueden registrar global o localmente.

Registro Global (main.js)

Para que la directiva esté disponible en cualquier componente de tu aplicación, la registras globalmente en main.js.

// src/main.js
import { createApp } from 'vue';
import App from './App.vue';
import { highlightDirective } from './directives/highlight.js';

const app = createApp(App);

app.directive('highlight', highlightDirective);

app.mount('#app');

El primer argumento de app.directive() es el nombre de la directiva (sin el prefijo v-), y el segundo es el objeto de la directiva.

Registro Local (en un componente)

Si solo necesitas la directiva en un componente específico, puedes registrarla localmente:

<!-- src/components/MyComponent.vue -->
<template>
  <p v-highlight="'lightblue'">Este texto tiene un fondo azul claro.</p>
  <p v-highlight>Este texto tiene un fondo amarillo por defecto.</p>
</template>

<script setup>
import { highlightDirective } from '@/directives/highlight.js';

const vHighlight = highlightDirective; // Renombrar para usar en el template
</script>

<!-- O si usas Options API -->
<script>
import { highlightDirective } from '@/directives/highlight.js';

export default {
  directives: {
    highlight: highlightDirective
  }
};
</script>
🔥 Importante: Cuando se usa `

Ahora puedes usar v-highlight en cualquier elemento de tu plantilla:

<template>
  <div>
    <h1 v-highlight="'#ADD8E6'">Título Resaltado</h1>
    <p v-highlight>Este párrafo está resaltado con el color por defecto.</p>
    <button @click="toggleHighlight">Alternar Resaltado</button>
    <p v-highlight="isHighlighted ? 'lightgreen' : null">Texto con resaltado condicional</p>
  </div>
</template>

<script setup>
import { ref } from 'vue';

const isHighlighted = ref(true);
const toggleHighlight = () => {
  isHighlighted.value = !isHighlighted.value;
};
</script>
mounted Actualización de valor updated Desmontaje de componente unmounted Reactividad

💡 Directivas con Argumentos y Modificadores

Las directivas se vuelven mucho más potentes cuando les añadimos argumentos y modificadores, permitiéndonos una mayor flexibilidad y configuración.

🏷️ Argumentos (v-directiva:argumento)

Un argumento se utiliza para indicar a la directiva cómo debe comportarse o en qué propiedad específica debe actuar. Se pasa después de dos puntos (:).

Ejemplo: Una directiva v-color que toma un argumento para especificar la propiedad CSS a modificar (background-color, color, border-color, etc.).

// src/directives/color.js
export const colorDirective = {
  mounted(el, binding) {
    const property = binding.arg || 'color'; // Por defecto, cambia el color del texto
    el.style[property] = binding.value || 'black';
  },
  updated(el, binding) {
    const property = binding.arg || 'color';
    el.style[property] = binding.value || 'black';
  }
};

Uso en el componente:

<template>
  <div>
    <p v-color:background-color="'red'">Fondo rojo</p>
    <p v-color:color="'blue'">Texto azul</p>
    <p v-color:border-color="'green'">Borde verde</p>
  </div>
</template>

<script setup>
import { colorDirective } from '@/directives/color.js';
const vColor = colorDirective;
</script>

🔗 Modificadores (v-directiva.modificador)

Los modificadores son sufijos especiales que indican que una directiva debe comportarse de una manera particular. Se añaden con un punto (.).

Ejemplo: Una directiva v-focus que puede tener un modificador .select para seleccionar el texto después de enfocarse.

// src/directives/focus.js
export const focusDirective = {
  mounted(el, binding) {
    el.focus();
    if (binding.modifiers.select && el.tagName === 'INPUT') {
      el.select();
    }
  }
};

Uso en el componente:

<template>
  <div>
    <input type="text" v-focus />
    <input type="text" v-focus.select value="Seleccioname" />
    <textarea v-focus.select>Este texto se seleccionará</textarea>
  </div>
</template>

<script setup>
import { focusDirective } from '@/directives/focus.js';
const vFocus = focusDirective;
</script>
⚠️ Advertencia: Ten cuidado con el uso de modificadores que esperan un tipo específico de elemento. Siempre es buena práctica añadir comprobaciones, como `el.tagName === 'INPUT'`, para evitar errores en elementos no compatibles.

🌐 Registro Global vs. Local: ¿Cuándo Usar Cuál?

La elección entre registro global y local depende de la frecuencia y el alcance de uso de tu directiva.

CaracterísticaRegistro Global (main.js)Registro Local (componente)
---------
AlcanceDisponible en todos los componentes de la aplicación.Disponible solo en el componente donde se registra.
ReusabilidadAlta, ideal para directivas de uso común (ej. v-focus, v-tooltip).Media, para directivas específicas de un contexto.
---------
Tamaño del BundlePuede incrementar ligeramente el tamaño si hay muchas directivas no usadas.Generalmente más eficiente, solo carga lo que se usa en el componente.
MantenimientoFácil de olvidar que está disponible globalmente, posible sobrecarga.Más claro el árbol de dependencias, más fácil de depurar.
80%
¿Cuándo elegir registro global? Opta por el registro global si tu directiva es un *utility* transversal que usarás en el 80% o más de tus componentes. Ejemplos incluyen una directiva para formatear texto, una para gestionar el *autofocus* de forma consistente, o para mostrar un *loader* genérico.
¿Cuándo elegir registro local? El registro local es preferible para directivas que son muy específicas de un contexto o un grupo reducido de componentes. Esto ayuda a mantener el *bundle* de tu aplicación más pequeño y a evitar colisiones de nombres.

🔄 Limpieza de Event Listeners y Observadores

Cuando una directiva añade event listeners o crea observadores (como IntersectionObserver o ResizeObserver) al elemento DOM, es crucial limpiarlos cuando el elemento es desmontado para evitar memory leaks (fugas de memoria).

Esto se hace en el hook unmounted de la directiva.

Ejemplo: Una directiva v-click-outside que cierra un menú cuando se hace clic fuera de él.

// src/directives/click-outside.js
export const clickOutsideDirective = {
  mounted(el, binding) {
    // Definir el manejador del evento
    el.__ClickOutsideHandler__ = (event) => {
      // Si el clic no fue dentro del elemento y el elemento existe en el DOM
      if (!(el === event.target || el.contains(event.target))) {
        binding.value(event); // Ejecutar la función pasada como valor
      }
    };
    document.addEventListener('click', el.__ClickOutsideHandler__);
  },
  unmounted(el) {
    // Remover el event listener para evitar fugas de memoria
    document.removeEventListener('click', el.__ClickOutsideHandler__);
    delete el.__ClickOutsideHandler__; // Limpiar la referencia
  }
};

Uso en el componente:

<template>
  <div>
    <button @click="showMenu = !showMenu">Toggle Menú</button>
    <div v-if="showMenu" v-click-outside="onClickOutside" class="menu">
      <ul>
        <li>Opción 1</li>
        <li>Opción 2</li>
      </ul>
    </div>
  </div>
</template>

<script setup>
import { ref } from 'vue';
import { clickOutsideDirective } from '@/directives/click-outside.js';

const showMenu = ref(false);

const onClickOutside = () => {
  console.log('Clic fuera del menú!');
  showMenu.value = false;
};

const vClickOutside = clickOutsideDirective;
</script>

<style scoped>
.menu {
  border: 1px solid #ccc;
  padding: 10px;
  margin-top: 10px;
  background-color: white;
}
</style>
⚠️ Advertencia: Es fundamental limpiar cualquier recurso (event listeners, timers, etc.) que tu directiva haya creado. No hacerlo es una causa común de *memory leaks* en aplicaciones SPA.

🚀 Ejemplos Avanzados y Casos de Uso

Las directivas personalizadas pueden resolver problemas interesantes.

📊 v-tooltip: Mostrar un Tooltip Simple

Crearemos una directiva para mostrar un tooltip básico al pasar el ratón por encima.

// src/directives/tooltip.js
export const tooltipDirective = {
  mounted(el, binding) {
    el.dataset.tooltip = binding.value; // Guardamos el texto del tooltip
    el.style.position = 'relative'; // Necesario para posicionar el pseudo-elemento

    // Estilos CSS para el tooltip usando pseudo-elementos (en tu CSS global)
    // .tooltip-container[data-tooltip]:hover::after { content: attr(data-tooltip); ... }
  },
  updated(el, binding) {
    el.dataset.tooltip = binding.value;
  }
};

Para que este ejemplo funcione, necesitarías un CSS similar a esto (idealmente en un archivo .css global o <style>):

/* Estilos para v-tooltip, por ejemplo en src/assets/main.css */
[v-tooltip] {
  position: relative;
  cursor: help;
}

[v-tooltip]:hover::after {
  content: attr(data-tooltip);
  position: absolute;
  left: 50%;
  transform: translateX(-50%);
  bottom: 100%; /* Aparece encima del elemento */
  background-color: #333;
  color: white;
  padding: 5px 8px;
  border-radius: 4px;
  white-space: nowrap;
  font-size: 0.85em;
  z-index: 1000;
  opacity: 0;
  visibility: hidden;
  transition: opacity 0.3s ease, visibility 0.3s ease;
}

[v-tooltip]:hover::before {
  content: '';
  position: absolute;
  left: 50%;
  transform: translateX(-50%);
  bottom: calc(100% - 2px); /* Flecha del tooltip */
  border-width: 5px;
  border-style: solid;
  border-color: #333 transparent transparent transparent;
  opacity: 0;
  visibility: hidden;
  transition: opacity 0.3s ease, visibility 0.3s ease;
}

[v-tooltip]:hover::after,
[v-tooltip]:hover::before {
  opacity: 1;
  visibility: visible;
}

Uso:

<template>
  <div>
    <p v-tooltip="'Este es un tooltip informativo'">Pasa el ratón por aquí</p>
    <button v-tooltip="'Haz clic para enviar'">Enviar</button>
  </div>
</template>

<script setup>
import { tooltipDirective } from '@/directives/tooltip.js';
const vTooltip = tooltipDirective;
</script>

🔢 v-autonumeric: Formateo de Números en Input (requiere librería externa)

Para un formateo más complejo, a menudo integramos directivas con librerías externas. autonumeric es una excelente opción.

Primero, instala la librería:

npm install autonumeric

// src/directives/autonumeric.js
import AutoNumeric from 'autonumeric';

export const autonumericDirective = {
  mounted(el, binding) {
    // Inicializar AutoNumeric en el elemento
    el.__AutoNumeric__ = new AutoNumeric(el, binding.value || {});
  },
  beforeUpdate(el, binding) {
    // Actualizar las opciones si cambian
    if (binding.value !== binding.oldValue) {
      el.__AutoNumeric__.update(binding.value);
    }
  },
  unmounted(el) {
    // Limpiar AutoNumeric cuando el elemento es desmontado
    if (el.__AutoNumeric__) {
      el.__AutoNumeric__.remove();
      delete el.__AutoNumeric__;
    }
  }
};

Uso:

<template>
  <div>
    <label>Valor de Moneda:</label>
    <input type="text" v-autonumeric="currencyOptions" v-model="amount" />
    <p>Valor sin formatear: {{ amount }}</p>
  </div>
</template>

<script setup>
import { ref } from 'vue';
import { autonumericDirective } from '@/directives/autonumeric.js';

const amount = ref(''); // Se actualizará con el valor numérico sin formatear
const currencyOptions = {
  digitGroupSeparator: '.', // Separador de miles
  decimalCharacter: ',',  // Separador de decimales
  decimalPlaces: 2,
  currencySymbol: '€ ',
  currencySymbolPlacement: AutoNumeric.options.currencySymbolPlacement.prefix,
  minimumValue: '0'
};

const vAutonumeric = autonumericDirective;
</script>
📌 Nota: Cuando se usan librerías externas que manipulan el valor del `input` directamente, es común que la directiva no interactúe directamente con `v-model` para la sincronización. En este caso, AutoNumeric gestiona el valor y tú podrías escuchar los eventos `change` o `input` para actualizar `amount` con el valor real (sin formato).
Componente Vue Plantilla / Lógica Librería Externa GSAP, Chart.js, etc. Directiva v-custom Ganchos del Ciclo de Vida DOM Real Elemento del Navegador Declara Importa Manipula

🔑 Consideraciones y Mejores Prácticas

Al trabajar con directivas personalizadas, ten en cuenta lo siguiente:

  • Manipulación DOM directa: Usa el con responsabilidad. Evita insertar contenido HTML complejo directamente a través de el.innerHTML si tienes alternativas más reactivas o componentizadas.
  • Separación de preocupaciones: Mantén la lógica del componente separada de la manipulación del DOM. Ese es el propósito principal de las directivas.
  • Nombres descriptivos: Prefija tus directivas con v- (aunque Vue lo añade automáticamente en el template), y usa nombres claros y descriptivos.
  • Pruebas: Asegúrate de probar tus directivas, especialmente si manejan lógica compleja o interacciones con el DOM.
  • Alternativas: Antes de crear una directiva, considera si un componente, un composable o una propiedad calculada (computed property) podrían ser una solución mejor o más simple. Las directivas son ideales para la interacción DOM de bajo nivel y reutilizable.
💡 Consejo: Si tu directiva crece mucho en complejidad, es una señal de que quizás debas refactorizarla en un componente Vue. Los componentes son más adecuados para manejar estado interno y lógica de interfaz de usuario compleja.

🏁 Conclusión

Las directivas personalizadas en Vue 3 son una herramienta excepcionalmente útil para añadir funcionalidades reactivas y reutilizables directamente a tus elementos DOM. Permiten encapsular manipulaciones del DOM de bajo nivel, mejorando la modularidad de tu código y simplificando tus componentes. Al entender sus hooks de ciclo de vida, cómo usar argumentos y modificadores, y la importancia de la limpieza de recursos, puedes crear directivas potentes y eficientes que enriquecen tus aplicaciones Vue.

Empieza a experimentar con ellas y verás cómo pueden optimizar tu forma de interactuar con el DOM en Vue. ¡Feliz codificación! 🚀

Tutoriales relacionados

Comentarios (0)

Aún no hay comentarios. ¡Sé el primero!