En esta página

Directivas y pipes personalizados

10 min lectura TextoCap. 4 — Integración

Directivas en Angular

Las directivas extienden el comportamiento de elementos HTML. Hay dos tipos principales:

  • Directivas de atributo — Modifican la apariencia o comportamiento de un elemento
  • Directivas estructurales — Modifican la estructura del DOM (agregar/quitar elementos)

Directivas de atributo

Crean comportamiento reutilizable que se aplica a elementos existentes:

import { Directive, ElementRef, inject, input } from '@angular/core';

@Directive({
  selector: '[appTooltip]',
  host: {
    '(mouseenter)': 'mostrar()',
    '(mouseleave)': 'ocultar()',
    '[attr.aria-label]': 'appTooltip()',
  },
})
export class TooltipDirective {
  readonly appTooltip = input.required<string>();

  mostrar(): void { /* lógica del tooltip */ }
  ocultar(): void { /* ocultar tooltip */ }
}

Uso en el template:

<button [appTooltip]="'Guardar cambios'">Guardar</button>

Directivas estructurales

Modifican la estructura del DOM. Usan TemplateRef y ViewContainerRef:

import { Directive, TemplateRef, ViewContainerRef, inject, input, effect } from '@angular/core';

@Directive({ selector: '[appRepetir]' })
export class RepetirDirective {
  private readonly template = inject(TemplateRef);
  private readonly container = inject(ViewContainerRef);

  readonly appRepetir = input.required<number>();

  constructor() {
    effect(() => {
      this.container.clear();
      for (let i = 0; i < this.appRepetir(); i++) {
        this.container.createEmbeddedView(this.template);
      }
    });
  }
}

Pipes: transformar datos en templates

Los pipes transforman datos para la presentación. Se usan con el operador | en templates.

Pipes integrados de Angular

Pipe Uso Ejemplo
date Formatear fechas {{ fecha | date:'short' }}
currency Formatear moneda {{ precio | currency:'USD' }}
uppercase Mayusculas {{ texto | uppercase }}
lowercase Minusculas {{ texto | lowercase }}
titlecase Capitalizar {{ texto | titlecase }}
number Formatear números {{ valor | number:'1.2-2' }}
percent Porcentaje {{ ratio | percent }}
json Serializar a JSON {{ objeto | json }}
slice Extraer porcion {{ lista | slice:0:5 }}
async Resolver Observable/Promise {{ obs$ | async }}

Crear un pipe personalizado

import { Pipe, PipeTransform } from '@angular/core';

@Pipe({ name: 'iniciales' })
export class InicialesPipe implements PipeTransform {
  transform(nombreCompleto: string): string {
    if (!nombreCompleto) return '';
    return nombreCompleto
      .split(' ')
      .map(palabra => palabra[0])
      .join('')
      .toUpperCase();
  }
}

Uso:

<span class="avatar">{{ usuario.nombre | iniciales }}</span>
<!-- "Carlos Morales" -> "CM" -->

Pipes con parametros

@Pipe({ name: 'filtrar' })
export class FiltrarPipe implements PipeTransform {
  transform<T>(lista: T[], campo: keyof T, valor: T[keyof T]): T[] {
    return lista.filter(item => item[campo] === valor);
  }
}
@for (activo of usuarios | filtrar:'estado':'activo'; track activo.id) {
  <span>{{ activo.nombre }}</span>
}

Importar directivas y pipes

En Angular 21, importa directivas y pipes directamente en el componente:

@Component({
  imports: [HighlightDirective, TiempoRelativoPipe],
  template: `
    <p appHighlight>{{ fecha | tiempoRelativo }}</p>
  `,
})

Práctica

  1. Crea una directiva de atributo: Implementa una directiva appTooltip que muestre un texto flotante al hacer hover sobre un elemento. Usa la propiedad host del decorador en lugar de @HostListener.
  2. Crea un pipe personalizado: Implementa un pipe tiempoTranscurrido que reciba una fecha y devuelva un string como "hace 5 min", "hace 2 h" o "hace 3 dias".
  3. Crea un pipe con parametros: Implementa un pipe resaltar que reciba un texto y un termino de busqueda, y devuelva el texto con el termino envuelto en una etiqueta <mark>.

En la siguiente leccion aprenderemos sobre Server-Side Rendering y pre-rendering para mejorar el SEO y rendimiento.

Pipes puros vs impuros
Los pipes puros (pure: true, por defecto) solo se recalculan cuando la referencia del input cambia. Los impuros (pure: false) se recalculan en cada ciclo de detección de cambios. Prefiere pipes puros.
host sobre HostListener
En Angular 21, usa la propiedad host del decorador @Directive en lugar de @HostListener y @HostBinding. Es más declarativo y funciona mejor con el compilador AOT.
import {
  Directive, Pipe, PipeTransform,
  ElementRef, TemplateRef, ViewContainerRef,
  inject, input, effect,
} from '@angular/core';
import { AuthService } from './auth.service';

// --- Directiva de atributo: highlight ---
@Directive({
  selector: '[appHighlight]',
  host: {
    '(mouseenter)': 'activar()',
    '(mouseleave)': 'desactivar()',
    '[style.transition]': '"background-color 0.3s ease"',
  },
})
export class HighlightDirective {
  private readonly el = inject(ElementRef);

  readonly appHighlight = input('var(--brand-amber)');

  activar(): void {
    this.el.nativeElement.style.backgroundColor = this.appHighlight();
  }

  desactivar(): void {
    this.el.nativeElement.style.backgroundColor = '';
  }
}

// --- Directiva estructural: permisos ---
@Directive({
  selector: '[appSiRol]',
})
export class SiRolDirective {
  private readonly templateRef = inject(TemplateRef);
  private readonly viewContainer = inject(ViewContainerRef);

  readonly appSiRol = input.required<string>();

  constructor() {
    const auth = inject(AuthService);
    effect(() => {
      const rolRequerido = this.appSiRol();
      const usuario = auth.usuario();
      if (usuario?.rol === rolRequerido) {
        this.viewContainer.createEmbeddedView(this.templateRef);
      } else {
        this.viewContainer.clear();
      }
    });
  }
}

// --- Pipe: tiempo relativo ---
@Pipe({
  name: 'tiempoRelativo',
  pure: true,
})
export class TiempoRelativoPipe implements PipeTransform {
  transform(fecha: string | Date): string {
    const ahora = Date.now();
    const pasado = new Date(fecha).getTime();
    const diff = ahora - pasado;

    const minutos = Math.floor(diff / 60000);
    const horas = Math.floor(diff / 3600000);
    const dias = Math.floor(diff / 86400000);

    if (minutos < 1) return 'Justo ahora';
    if (minutos < 60) return `Hace ${minutos} min`;
    if (horas < 24) return `Hace ${horas} h`;
    if (dias < 30) return `Hace ${dias} dias`;
    return new Date(fecha).toLocaleDateString('es');
  }
}

// --- Pipe: truncar texto ---
@Pipe({ name: 'truncar' })
export class TruncarPipe implements PipeTransform {
  transform(texto: string, longitud = 100): string {
    if (!texto || texto.length <= longitud) return texto;
    return texto.substring(0, longitud).trimEnd() + '...';
  }
}