tutoriales.com

¡Desbloquea la Personalización! 🎨 Creando Directivas Estructurales en Angular

Este tutorial te guiará paso a paso en la creación de directivas estructurales personalizadas en Angular. Descubre cómo manipular la estructura del DOM de tus aplicaciones, añadiendo o eliminando elementos dinámicamente, para crear componentes altamente personalizables y reutilizables. Aumenta la potencia y flexibilidad de tus plantillas de Angular.

Avanzado18 min de lectura11 views
Reportar error

Las directivas son una característica fundamental en Angular, permitiéndonos extender el HTML y darle nuevas funcionalidades. Dentro de estas, las directivas estructurales son especialmente poderosas, ya que tienen la capacidad de modificar la estructura del DOM, añadiendo o eliminando elementos. Seguramente ya estás familiarizado con algunas de ellas, como *ngIf, *ngFor y *ngSwitch.

Pero, ¿qué pasa si necesitas una lógica de renderizado más específica que no cubren las directivas predefinidas? Aquí es donde entra en juego la creación de tus propias directivas estructurales personalizadas. En este tutorial, te sumergirás en el fascinante mundo de la personalización de Angular, aprendiendo a construir directivas estructurales desde cero para desbloquear un nuevo nivel de control sobre la interfaz de usuario de tus aplicaciones.


🎯 ¿Qué Aprenderás en Este Tutorial?

Al finalizar este tutorial, serás capaz de:

  • Entender los fundamentos de las directivas estructurales en Angular.
  • Identificar las diferencias clave entre directivas de atributo y estructurales.
  • Crear una directiva estructural personalizada desde cero.
  • Utilizar TemplateRef y ViewContainerRef para manipular el DOM.
  • Implementar lógica condicional y de iteración personalizada.
  • Aplicar tus nuevas directivas en proyectos reales para mejorar la reusabilidad y el control.
💡 Consejo: Para seguir este tutorial, se recomienda tener un conocimiento básico de Angular (componentes, plantillas, data binding).

📖 Entendiendo las Directivas en Angular

Antes de sumergirnos en la creación de nuestras propias directivas estructurales, recordemos brevemente qué son las directivas en Angular y sus tipos principales.

Una directiva es una clase que añade comportamiento extra a los elementos de tu plantilla. En Angular, existen tres tipos principales:

  1. Directivas de Componente: Son las más comunes. Un componente es una directiva con una plantilla. (@Component).
  2. Directivas de Atributo: Modifican la apariencia o el comportamiento de un elemento, componente o de otra directiva. Ejemplos: NgClass, NgStyle, NgModel. (@Directive).
  3. Directivas Estructurales: Modifican la estructura del DOM añadiendo o eliminando elementos. Ejemplos: *ngIf, *ngFor, *ngSwitch. (@Directive).
¿Cuál es la diferencia entre directivas de atributo y estructurales? Las directivas de atributo (`@Directive`) solo cambian las propiedades del elemento al que están aplicadas (estilos, clases, comportamiento). Las directivas estructurales (`@Directive`) son más potentes porque pueden **añadir o eliminar elementos enteros** del DOM, afectando su estructura. Se distinguen visualmente por el asterisco `*` prefijo, que es solo azúcar sintáctico para una sintaxis más verbosa.

🌟 ¿Por qué un Asterisco *?

El asterisco * en directivas como *ngIf es azúcar sintáctico (syntactic sugar) para una sintaxis más detallada que utiliza las etiquetas <ng-template>. Cuando escribes:

<div *ngIf="mostrar">
  Contenido visible
</div>

Angular lo transforma internamente a:

<ng-template [ngIf]="mostrar">
  <div>
    Contenido visible
  </div>
</ng-template>

Entender esto es crucial, ya que cuando creamos nuestras propias directivas estructurales, estaremos trabajando con esta sintaxis subyacente de <ng-template>, TemplateRef y ViewContainerRef.

PASO 1: AZÚCAR SINTÁCTICO (*) <div *myDirective="value"> ... </div> Transformación de Angular PASO 2: FORMA EXPANDIDA (ng-template) <ng-template [myDirective]="value"> <div> ... </div> </ng-template>

🛠️ Creando Nuestra Primera Directiva Estructural: *ngPermission

Imaginemos que queremos crear una directiva que muestre u oculte elementos basándose en los permisos del usuario. Podríamos llamarla *ngPermission. Si el usuario tiene el permiso especificado, el elemento se muestra; de lo contrario, se oculta.

Paso 1: Generar la Directiva

Primero, vamos a generar la directiva usando Angular CLI. Abre tu terminal en el directorio de tu proyecto Angular y ejecuta:

ng generate directive permission
# O su forma corta:
ng g d permission

Esto creará un archivo src/app/permission.directive.ts (y su archivo de prueba permission.directive.spec.ts), y lo declarará automáticamente en tu AppModule.

📌 Nota: Si estás trabajando en un módulo específico, asegúrate de generar la directiva dentro de ese módulo y declararla en él.

Paso 2: Importaciones Necesarias

Para crear una directiva estructural, necesitaremos inyectar dos servicios clave en el constructor:

  • TemplateRef<any>: Representa la plantilla incrustada (el contenido del elemento al que se aplica la directiva). Es lo que se encuentra dentro del *ngIf o *ngFor.
  • ViewContainerRef: Representa el contenedor donde una o más vistas pueden ser adjuntadas. Es el lugar donde Angular inserta la plantilla.

Modifica permission.directive.ts para incluir estas inyecciones:

import { Directive, Input, TemplateRef, ViewContainerRef } from '@angular/core';

@Directive({
  selector: '[appPermission]'
})
export class PermissionDirective {

  constructor(
    private templateRef: TemplateRef<any>,
    private viewContainer: ViewContainerRef
  ) { }

}

Paso 3: Definir el Input de la Directiva

Nuestra directiva *ngPermission necesitará saber qué permiso debe verificar. Para ello, usaremos un @Input(). El nombre del input debe coincidir con el nombre de la directiva (sin el prefijo app), para que podamos usar la sintaxis *appPermission="'admin'".

import { Directive, Input, TemplateRef, ViewContainerRef } from '@angular/core';

@Directive({
  selector: '[appPermission]'
})
export class PermissionDirective {
  private hasView = false;

  constructor(
    private templateRef: TemplateRef<any>,
    private viewContainer: ViewContainerRef
  ) { }

  @Input() set appPermission(permissionName: string) {
    // Aquí simularemos la lógica de permisos.
    // En una aplicación real, obtendrías los permisos del usuario de un servicio.
    const userPermissions = ['admin', 'edit_posts']; // Simulamos permisos del usuario actual

    if (userPermissions.includes(permissionName) && !this.hasView) {
      // Si el usuario tiene el permiso y la vista no ha sido creada, la creamos.
      this.viewContainer.createEmbeddedView(this.templateRef);
      this.hasView = true;
    } else if (!userPermissions.includes(permissionName) && this.hasView) {
      // Si el usuario NO tiene el permiso y la vista existe, la limpiamos.
      this.viewContainer.clear();
      this.hasView = false;
    }
  }

}

Analicemos el código:

  • private hasView = false;: Una bandera para saber si ya hemos renderizado la vista.
  • @Input() set appPermission(permissionName: string): Este es un setter de input. Se ejecutará cada vez que el valor de appPermission cambie. Es importante que el nombre del input sea appPermission (sin *) para que Angular lo reconozca como el valor de la directiva estructural.
  • userPermissions: Un array simulado de permisos que el usuario actual posee. En una aplicación real, esto provendría de un servicio de autenticación/autorización.
  • this.viewContainer.createEmbeddedView(this.templateRef): Este es el método mágico. Le dice a Angular que tome la plantilla (templateRef) y la inserte en el DOM en la posición de esta directiva (viewContainer).
  • this.viewContainer.clear(): Elimina todas las vistas de este contenedor, borrando el elemento del DOM.
🔥 Importante: La lógica de permisos en el ejemplo es simplificada. En un entorno de producción, los permisos se gestionarían a través de un servicio de autenticación y autorización.

Paso 4: Usando la Directiva en tu Componente

Ahora, vamos a utilizar nuestra nueva directiva en la plantilla de AppComponent (o cualquier otro componente).

<!-- src/app/app.component.html -->

<h1>Tutorial de Directivas Estructurales Personalizadas</h1>

<p>Contenido siempre visible.</p>

<div *appPermission="'admin'">
  <p>Este contenido solo es visible para **administradores**.</p>
  <button>Panel de Administración</button>
</div>

<div *appPermission="'edit_posts'">
  <p>Puedes **editar posts**.</p>
  <input type="text" placeholder="Título del Post">
</div>

<div *appPermission="'view_reports'">
  <p>Solo usuarios con permiso 'view_reports' ven esto.</p>
  <span class="badge yellow">Permiso Requerido</span>
</div>

<hr>

<p>Contenido después de las directivas.</p>

Al ejecutar la aplicación (ng serve), verás que solo los elementos con los permisos 'admin' y 'edit_posts' son visibles, ya que esos son los permisos que hemos simulado que tiene el usuario.

Directiva Creada y Funcional!

🔄 Extendiendo la Directiva: Añadiendo un else

Las directivas *ngIf tienen una cláusula else (ej: *ngIf="condición; else elseBlock"). Podemos replicar esta funcionalidad en nuestra directiva *appPermission.

Paso 1: Añadir un Input else

Para el else, necesitaremos un segundo TemplateRef que apunte al bloque de else. El nombre del input debe ser appPermissionElse para que Angular lo reconozca. Angular usa el patrón directiveName + Property para los inputs adicionales.

import { Directive, Input, TemplateRef, ViewContainerRef } from '@angular/core';

@Directive({
  selector: '[appPermission]'
})
export class PermissionDirective {
  private hasView = false;
  private _elseTemplate: TemplateRef<any> | null = null;

  constructor(
    private templateRef: TemplateRef<any>,
    private viewContainer: ViewContainerRef
  ) { }

  @Input() set appPermission(permissionName: string) {
    const userPermissions = ['admin', 'edit_posts']; // Simulamos permisos del usuario actual
    const hasPermission = userPermissions.includes(permissionName);

    if (hasPermission && !this.hasView) {
      this.viewContainer.createEmbeddedView(this.templateRef);
      this.hasView = true;
    } else if (!hasPermission && this.hasView) {
      this.viewContainer.clear();
      this.hasView = false;
    } else if (!hasPermission && !this.hasView && this._elseTemplate) {
      // Si NO tiene permiso, NO tiene vista y hay un 'elseTemplate', creamos la vista 'else'
      this.viewContainer.clear(); // Asegurarse de que no haya nada previo
      this.viewContainer.createEmbeddedView(this._elseTemplate);
      this.hasView = true; // Ahora 'hasView' se refiere a si se ha renderizado CUALQUIER vista
    } else if (hasPermission && !this.hasView && this._elseTemplate) {
        // Si ahora tiene permiso, y teníamos el elseTemplate visible, lo limpiamos
        this.viewContainer.clear();
        this.viewContainer.createEmbeddedView(this.templateRef);
        this.hasView = true;
    }
  }

  @Input() set appPermissionElse(templateRef: TemplateRef<any> | null) {
    this._elseTemplate = templateRef;
    // Si el permiso no se cumple y hay un elseTemplate, y la vista no ha sido creada (o fue limpiada)
    // necesitamos reevaluar la condición para mostrar el elseTemplate si aplica.
    // Esto es un poco más complejo porque el setter del elseTemplate se puede ejecutar antes del setter del permission.
    // Para simplificar, podríamos llamar al setter de appPermission nuevamente si es necesario.
  }

  // Un método para reevaluar la visibilidad, útil si el 'else' llega después o los permisos cambian dinámicamente
  private updateView(permissionName: string) {
    const userPermissions = ['admin', 'edit_posts'];
    const hasPermission = userPermissions.includes(permissionName);

    this.viewContainer.clear(); // Limpiar siempre antes de renderizar para evitar duplicados
    this.hasView = false; // Resetear la bandera

    if (hasPermission) {
      this.viewContainer.createEmbeddedView(this.templateRef);
      this.hasView = true;
    } else if (this._elseTemplate) {
      this.viewContainer.createEmbeddedView(this._elseTemplate);
      this.hasView = true;
    }
  }

  // Mejorar el setter principal para que llame a updateView
  @Input() set appPermission(permissionName: string) {
    // Guardar el nombre del permiso para usarlo en updateView si el 'else' cambia
    // Ojo: en un caso real, la lógica de permisos puede ser más compleja y reactiva.
    this.updateView(permissionName);
  }

}

En este ejemplo, la lógica de actualización se ha movido a updateView para manejar mejor los cambios dinámicos. El appPermissionElse input simplemente almacena la referencia a la plantilla else.

Paso 5: Usando la Cláusula else

Ahora podemos usar nuestra directiva con un bloque else.

<!-- src/app/app.component.html -->

<h1>Tutorial de Directivas Estructurales Personalizadas</h1>

<p>Contenido siempre visible.</p>

<div *appPermission="'admin'; else noAdminAccess">
  <p>Este contenido solo es visible para **administradores**.</p>
  <button>Panel de Administración</button>
</div>

<ng-template #noAdminAccess>
  <p class="warning">⚠️ **Acceso denegado**: No tienes permisos de administrador.</p>
</ng-template>

<hr>

<div *appPermission="'view_dashboard'; else noDashboardAccess">
  <p>Bienvenido al **Dashboard**.</p>
  <span class="badge green">Vista Dashboard</span>
</div>

<ng-template #noDashboardAccess>
  <p class="warning">⚠️ **Acceso denegado**: No puedes ver el dashboard.</p>
  <button disabled>Solicitar Acceso</button>
</ng-template>

<hr>

<p>Contenido después de las directivas.</p>

Ahora, cuando el usuario no tenga el permiso 'admin', se mostrará el contenido del <ng-template #noAdminAccess>. Lo mismo ocurrirá para el permiso 'view_dashboard'. Esto demuestra la flexibilidad y potencia de las directivas estructurales.


🚀 Caso Avanzado: Directiva Estructural con Contexto (*appForRange)

Las directivas como *ngFor no solo renderizan elementos, sino que también proporcionan un contexto a la plantilla (como let item of items, let i = index). Podemos lograr esto también.

Crearemos una directiva *appForRange que itere sobre un rango de números y exponga el número actual a la plantilla.

Paso 1: Generar la Directiva for-range

ng g d for-range

Paso 2: Implementar la Lógica

Esta directiva será un poco más compleja, ya que necesitará una interfaz para el contexto que se pasa a la plantilla y un Input para definir el rango.

import { Directive, Input, TemplateRef, ViewContainerRef, OnChanges, SimpleChanges } from '@angular/core';

// 1. Definir la interfaz de contexto que nuestra directiva pasará a la plantilla
export interface ForRangeContext {
  $implicit: number; // El valor principal, como 'item' en ngFor
  index: number;
  first: boolean;
  last: boolean;
  even: boolean;
  odd: boolean;
}

@Directive({
  selector: '[appForRange]'
})
export class ForRangeDirective implements OnChanges {
  @Input() appForRangeOf = 0; // El valor superior del rango (ej: 5 para 0,1,2,3,4)

  constructor(
    private templateRef: TemplateRef<ForRangeContext>, // Tipar templateRef con nuestro contexto
    private viewContainer: ViewContainerRef
  ) { }

  ngOnChanges(changes: SimpleChanges): void {
    if (changes['appForRangeOf']) {
      this.updateView();
    }
  }

  private updateView(): void {
    this.viewContainer.clear(); // Limpiar vistas existentes antes de recrearlas

    for (let i = 0; i < this.appForRangeOf; i++) {
      const context: ForRangeContext = {
        $implicit: i,
        index: i,
        first: i === 0,
        last: i === this.appForRangeOf - 1,
        even: i % 2 === 0,
        odd: i % 2 !== 0
      };
      this.viewContainer.createEmbeddedView(this.templateRef, context);
    }
  }
}

Explicación:

  • ForRangeContext: Define qué propiedades estarán disponibles en la plantilla. $implicit es el valor por defecto que se usa cuando escribes let item sin especificar let item =. Aquí, será el número del rango.
  • @Input() appForRangeOf: El input que define hasta dónde llega el rango (ej. *appForRange="5").
  • ngOnChanges: Implementado para que la vista se actualice si el input appForRangeOf cambia dinámicamente.
  • this.viewContainer.createEmbeddedView(this.templateRef, context): Aquí pasamos el objeto context como segundo argumento. Esto hace que las propiedades definidas en ForRangeContext estén disponibles para la plantilla.

Paso 3: Usando *appForRange con Contexto

Ahora podemos usarla en AppComponent:

<!-- src/app/app.component.html -->

<h1>Iteración con Directiva Personalizada `*appForRange`</h1>

<div *appForRange="10; let num; let i = index; let isFirst = first; let isLast = last">
  <p [style.background-color]="isEven ? '#e0ffe0' : 'transparent'">
    Número: {{ num }} (Índice: {{ i }}) - 
    <span *ngIf="isFirst" class="badge blue">Primero</span>
    <span *ngIf="isLast" class="badge red">Último</span>
  </p>
</div>

<hr>

<p>Lista de tareas (ejemplo simple):</p>
<ul>
  <li *appForRange="5; let taskId = $implicit">
    Tarea #{{ taskId + 1 }} - Completada: <input type="checkbox">
  </li>
</ul>

En este ejemplo, let num mapea a $implicit, y las otras variables (i, isFirst, isLast) mapean a las propiedades index, first, last respectivamente del objeto context. Esto te permite crear directivas de iteración extremadamente flexibles.


🧠 Cuándo Usar Directivas Estructurales Personalizadas

Las directivas estructurales son herramientas poderosas, pero como todo, deben usarse con criterio. Aquí hay algunos escenarios donde brillan:

EscenarioDescripciónEjemploProsContras
---------------
Renderizado Condicional ComplejoCuando *ngIf no es suficiente y necesitas lógica personalizada para mostrar/ocultar secciones basadas en múltiples criterios, permisos, etc.*appPermission, *appFeatureFlagReutilizabilidad, encapsulación de lógica, limpieza del HTMLSobrecarga si la lógica es trivial, complejidad si se abusa
Iteración PersonalizadaNecesitas iterar sobre algo que no es un array estándar o quieres exponer un contexto diferente.*appForRange, *appRepeatNTimesFlexibilidad para iteraciones no convencionalesLa lógica de *ngFor es muy robusta, difícil de superar
---------------
Integración con ServiciosLa visibilidad o estructura depende de un estado global o un servicio.*appAuthShow (muestra si el usuario está autenticado), *appLoading (muestra contenido mientras se carga)Centralización de la lógica de UI con serviciosPuede acoplar la directiva a un servicio específico
Variantes de PlantillasCrear directivas que eligen diferentes <ng-template>s basándose en condiciones.Un *ngChoice que renderiza una de varias plantillas según un valor.Alta flexibilidad en la presentación condicionalMayor complejidad en la gestión de múltiples TemplateRef
⚠️ Advertencia: Evita reinventar la rueda si `*ngIf`, `*ngFor` o `*ngSwitch` ya cubren tus necesidades. Las directivas personalizadas añaden complejidad y un ligero overhead de rendimiento si no se usan adecuadamente.

✅ Buenas Prácticas y Consideraciones

  • Nombres Descriptivos: Usa prefijos (ej. app) y nombres claros que indiquen el propósito de la directiva (ej. appPermission, no solo perm).
  • Una Directiva, Una Responsabilidad: Intenta que cada directiva haga una cosa bien. Si la lógica es demasiado compleja, quizás necesites dividirla o considerar un componente.
  • Manejo de Cambios: Si tu directiva depende de @Input()s que pueden cambiar, implementa OnChanges para asegurarte de que la vista se actualice correctamente. Siempre limpia el viewContainer antes de recrear las vistas.
  • Rendimiento: Manipular el DOM puede ser costoso. Asegúrate de limpiar las vistas (viewContainer.clear()) cuando el contenido ya no sea necesario para evitar fugas de memoria y mantener el rendimiento.
  • Testabilidad: Escribe pruebas unitarias para tus directivas. Verifica que la lógica de mostrar/ocultar o iterar funcione como se espera para diferentes inputs y estados.

wrap-up: ¡Has Desbloqueado el Poder de las Directivas Estructurales! 🎉

¡Felicidades! Has completado una inmersión profunda en la creación de directivas estructurales personalizadas en Angular. Ahora tienes las herramientas y el conocimiento para:

  • Entender el funcionamiento interno de las directivas estructurales de Angular.
  • Crear directivas que añadan o eliminen elementos del DOM de forma dinámica.
  • Exponer un contexto personalizado a tus plantillas, haciendo tus directivas más potentes y versátiles.
  • Saber cuándo y cómo aplicar estas poderosas herramientas para mejorar la reusabilidad y el control en tus aplicaciones Angular.

El desarrollo de directivas estructurales abre un abanico de posibilidades para construir interfaces de usuario altamente dinámicas y reutilizables. Sigue experimentando, creando y personalizando tus aplicaciones Angular para llevarlas al siguiente nivel. ¡El poder de la personalización está en tus manos!

Tutoriales relacionados

Comentarios (0)

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