Directivas personalizadas

Avanzado
Angular
Angular
Actualizado: 24/09/2025

Creando directivas de atributo personalizadas

Las directivas de atributo personalizadas nos permiten encapsular lógica de manipulación del DOM que se puede reutilizar en cualquier elemento HTML. A diferencia de los componentes que crean nuevos elementos, las directivas extienden el comportamiento de elementos existentes.

Anatomía básica de una directiva

Una directiva personalizada se define usando el decorador @Directive y se estructura como una clase que encapsula la funcionalidad deseada:

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

@Directive({
  selector: '[appHighlight]',
  standalone: true
})
export class HighlightDirective {
  constructor(private elementRef: ElementRef) {
    // Lógica de la directiva
  }
}

El selector define cómo aplicaremos la directiva en nuestros templates. Al usar corchetes [appHighlight], indicamos que es una directiva de atributo que se aplicará escribiendo appHighlight como atributo en cualquier elemento HTML.

ElementRef para acceso al elemento

ElementRef nos proporciona acceso directo al elemento del DOM donde se aplica la directiva. A través de su propiedad nativeElement, podemos obtener referencia al elemento HTML real:

@Directive({
  selector: '[appHighlight]',
  standalone: true
})
export class HighlightDirective {
  constructor(private elementRef: ElementRef) {
    // Acceso directo al elemento HTML
    const element = this.elementRef.nativeElement;
    element.style.backgroundColor = 'yellow';
  }
}

Esta aproximación funciona, pero no es la práctica recomendada para aplicaciones que necesiten funcionar en diferentes entornos (server-side rendering, web workers, etc.).

Renderer2 para manipulación segura del DOM

Renderer2 es el servicio recomendado para manipular el DOM de forma segura. Proporciona una API abstracta que funciona correctamente en todos los entornos donde Angular puede ejecutarse:

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

@Directive({
  selector: '[appHighlight]',
  standalone: true
})
export class HighlightDirective {
  constructor(
    private elementRef: ElementRef,
    private renderer: Renderer2
  ) {
    this.renderer.setStyle(
      this.elementRef.nativeElement,
      'backgroundColor',
      'yellow'
    );
  }
}

Los métodos más comunes de Renderer2 incluyen:

  • setStyle(element, styleName, value) - Aplica estilos CSS
  • addClass(element, className) - Añade clases CSS
  • removeClass(element, className) - Elimina clases CSS
  • setAttribute(element, name, value) - Establece atributos HTML
  • removeAttribute(element, name) - Elimina atributos HTML

Ejemplo práctico: Directiva de resaltado

Vamos a crear una directiva de resaltado que cambie el color de fondo al pasar el ratón sobre el elemento:

import { Directive, ElementRef, Renderer2, HostListener } from '@angular/core';

@Directive({
  selector: '[appHighlight]',
  standalone: true
})
export class HighlightDirective {
  constructor(
    private elementRef: ElementRef,
    private renderer: Renderer2
  ) {}

  @HostListener('mouseenter') onMouseEnter() {
    this.renderer.setStyle(
      this.elementRef.nativeElement,
      'backgroundColor',
      '#f0f0f0'
    );
  }

  @HostListener('mouseleave') onMouseLeave() {
    this.renderer.removeStyle(
      this.elementRef.nativeElement,
      'backgroundColor'
    );
  }
}

Para usar esta directiva en nuestros templates:

<p appHighlight>
  Este párrafo cambiará de color al pasar el ratón
</p>

<button appHighlight>
  Este botón también responderá al hover
</button>

Registro de directivas en componentes

Al usar componentes standalone, debemos importar nuestras directivas en el array imports del componente donde las queremos utilizar:

import { Component } from '@angular/core';
import { HighlightDirective } from './highlight.directive';

@Component({
  selector: 'app-example',
  standalone: true,
  imports: [HighlightDirective],
  template: `
    <div appHighlight>
      Contenido con directiva aplicada
    </div>
  `
})
export class ExampleComponent {}

Consideraciones importantes

Nomenclatura: Es recomendable usar un prefijo personalizado (como app) para evitar conflictos con directivas nativas o de terceros. Esto hace que nuestras directivas sean fácilmente identificables.

Standalone por defecto: En Angular 20+, todas las directivas deben ser standalone. Esto simplifica su uso y reutilización entre diferentes componentes sin necesidad de módulos adicionales.

Inyección de dependencias: Las directivas pueden inyectar cualquier servicio disponible, lo que las hace muy flexibles para implementar funcionalidad compleja de forma reutilizable.

Las directivas de atributo nos permiten crear comportamientos reutilizables que se pueden aplicar a cualquier elemento HTML, manteniendo nuestro código DRY y facilitando el mantenimiento de funcionalidades comunes en toda la aplicación.

Directivas con input/output y ElementRef

Las directivas se vuelven verdaderamente útiles cuando pueden recibir parámetros y comunicarse con el componente padre. Esto las convierte en herramientas reutilizables y configurables que se adaptan a diferentes escenarios.

Directivas con inputs para configuración

Podemos hacer que nuestras directivas reciban datos usando input() o el tradicional @Input. Esto nos permite personalizar el comportamiento según las necesidades:

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

@Directive({
  selector: '[appHighlight]',
  standalone: true
})
export class ConfigurableHighlightDirective {
  color = input<string>('yellow');
  intensity = input<number>(0.3);

  constructor(
    private elementRef: ElementRef,
    private renderer: Renderer2
  ) {}

  ngOnInit() {
    this.applyHighlight();
  }

  private applyHighlight() {
    const element = this.elementRef.nativeElement;
    const backgroundColor = this.adjustColorIntensity(this.color(), this.intensity());
    
    this.renderer.setStyle(element, 'backgroundColor', backgroundColor);
    this.renderer.setStyle(element, 'transition', 'background-color 0.3s ease');
  }

  private adjustColorIntensity(color: string, intensity: number): string {
    // Lógica para ajustar la intensidad del color
    return `${color}${Math.round(intensity * 255).toString(16)}`;
  }
}

Uso en el template:

<p appHighlight color="blue" [intensity]="0.5">
  Texto con resaltado azul personalizado
</p>

<div appHighlight color="red" [intensity]="0.2">
  Contenido con resaltado rojo suave
</div>

Directivas con outputs para comunicación

Las directivas también pueden emitir eventos usando output() o @Output, permitiendo que el componente padre reaccione a las acciones del usuario:

import { Directive, ElementRef, Renderer2, output, HostListener } from '@angular/core';

@Directive({
  selector: '[appClickTracker]',
  standalone: true
})
export class ClickTrackerDirective {
  clicked = output<{ element: string, timestamp: Date }>();
  doubleClicked = output<{ element: string, timestamp: Date }>();

  private clickCount = 0;
  private clickTimer?: any;

  constructor(private elementRef: ElementRef) {}

  @HostListener('click', ['$event'])
  onClick(event: MouseEvent) {
    this.clickCount++;
    
    if (this.clickCount === 1) {
      this.clickTimer = setTimeout(() => {
        // Single click
        this.emitClickEvent();
        this.resetClick();
      }, 300);
    } else if (this.clickCount === 2) {
      // Double click
      clearTimeout(this.clickTimer);
      this.emitDoubleClickEvent();
      this.resetClick();
    }
  }

  private emitClickEvent() {
    this.clicked.emit({
      element: this.elementRef.nativeElement.tagName,
      timestamp: new Date()
    });
  }

  private emitDoubleClickEvent() {
    this.doubleClicked.emit({
      element: this.elementRef.nativeElement.tagName,
      timestamp: new Date()
    });
  }

  private resetClick() {
    this.clickCount = 0;
  }
}

Uso con manejo de eventos:

<button 
  appClickTracker 
  (clicked)="onSingleClick($event)"
  (doubleClicked)="onDoubleClick($event)">
  Botón con seguimiento de clicks
</button>
export class ExampleComponent {
  onSingleClick(data: { element: string, timestamp: Date }) {
    console.log(`Click simple en ${data.element} a las ${data.timestamp}`);
  }

  onDoubleClick(data: { element: string, timestamp: Date }) {
    console.log(`Doble click en ${data.element} a las ${data.timestamp}`);
  }
}

Ejemplo práctico: Directiva de tooltip

Una directiva que combina inputs para configuración y outputs para eventos es especialmente útil para casos como tooltips:

import { Directive, ElementRef, Renderer2, input, output, HostListener } from '@angular/core';

@Directive({
  selector: '[appTooltip]',
  standalone: true
})
export class TooltipDirective {
  text = input.required<string>();
  position = input<'top' | 'bottom' | 'left' | 'right'>('top');
  delay = input<number>(500);
  
  shown = output<string>();
  hidden = output<string>();

  private tooltipElement?: HTMLElement;
  private showTimer?: any;

  constructor(
    private elementRef: ElementRef,
    private renderer: Renderer2
  ) {}

  @HostListener('mouseenter')
  onMouseEnter() {
    this.showTimer = setTimeout(() => {
      this.showTooltip();
    }, this.delay());
  }

  @HostListener('mouseleave')
  onMouseLeave() {
    if (this.showTimer) {
      clearTimeout(this.showTimer);
    }
    this.hideTooltip();
  }

  private showTooltip() {
    if (this.tooltipElement) return;

    // Crear elemento tooltip
    this.tooltipElement = this.renderer.createElement('div');
    this.renderer.setStyle(this.tooltipElement, 'position', 'absolute');
    this.renderer.setStyle(this.tooltipElement, 'background', '#333');
    this.renderer.setStyle(this.tooltipElement, 'color', 'white');
    this.renderer.setStyle(this.tooltipElement, 'padding', '8px 12px');
    this.renderer.setStyle(this.tooltipElement, 'border-radius', '4px');
    this.renderer.setStyle(this.tooltipElement, 'font-size', '14px');
    this.renderer.setStyle(this.tooltipElement, 'z-index', '1000');
    this.renderer.setStyle(this.tooltipElement, 'white-space', 'nowrap');

    const textNode = this.renderer.createText(this.text());
    this.renderer.appendChild(this.tooltipElement, textNode);
    this.renderer.appendChild(document.body, this.tooltipElement);

    this.positionTooltip();
    this.shown.emit(this.text());
  }

  private hideTooltip() {
    if (this.tooltipElement) {
      this.renderer.removeChild(document.body, this.tooltipElement);
      this.tooltipElement = undefined;
      this.hidden.emit(this.text());
    }
  }

  private positionTooltip() {
    if (!this.tooltipElement) return;

    const hostRect = this.elementRef.nativeElement.getBoundingClientRect();
    const tooltipRect = this.tooltipElement.getBoundingClientRect();

    let top: number, left: number;

    switch (this.position()) {
      case 'top':
        top = hostRect.top - tooltipRect.height - 8;
        left = hostRect.left + (hostRect.width - tooltipRect.width) / 2;
        break;
      case 'bottom':
        top = hostRect.bottom + 8;
        left = hostRect.left + (hostRect.width - tooltipRect.width) / 2;
        break;
      case 'left':
        top = hostRect.top + (hostRect.height - tooltipRect.height) / 2;
        left = hostRect.left - tooltipRect.width - 8;
        break;
      case 'right':
        top = hostRect.top + (hostRect.height - tooltipRect.height) / 2;
        left = hostRect.right + 8;
        break;
    }

    this.renderer.setStyle(this.tooltipElement, 'top', `${top}px`);
    this.renderer.setStyle(this.tooltipElement, 'left', `${left}px`);
  }
}

Implementación en el template:

<button 
  appTooltip 
  text="Esta acción guardará los cambios"
  position="top"
  [delay]="300"
  (shown)="onTooltipShown($event)"
  (hidden)="onTooltipHidden($event)">
  Guardar
</button>

<span 
  appTooltip 
  text="Usuario activo desde hace 2 días"
  position="right">
  👤 Juan Pérez
</span>

Directivas reactivas con signals

Las directivas pueden reaccionar automáticamente a cambios en sus inputs cuando usamos signals combinados con effect():

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

@Directive({
  selector: '[appDynamicStyle]',
  standalone: true
})
export class DynamicStyleDirective {
  backgroundColor = input<string>('transparent');
  textColor = input<string>('inherit');
  fontSize = input<number>(16);

  constructor(
    private elementRef: ElementRef,
    private renderer: Renderer2
  ) {
    // Reacciona automáticamente a cambios en los inputs
    effect(() => {
      this.updateStyles();
    });
  }

  private updateStyles() {
    const element = this.elementRef.nativeElement;
    
    this.renderer.setStyle(element, 'backgroundColor', this.backgroundColor());
    this.renderer.setStyle(element, 'color', this.textColor());
    this.renderer.setStyle(element, 'fontSize', `${this.fontSize()}px`);
    this.renderer.setStyle(element, 'transition', 'all 0.3s ease');
  }
}

Casos de uso comunes

Las directivas con inputs y outputs son ideales para:

  • Widgets reutilizables: Tooltips, dropdowns, modales
  • Validación visual: Resaltado de errores, indicadores de estado
  • Interacciones avanzadas: Drag & drop, resize, scroll tracking
  • Integración con librerías: Wrappers para componentes de terceros
  • Funcionalidades transversales: Logging, analytics, accessibility

La combinación de ElementRef para acceso al DOM, inputs para configuración y outputs para comunicación convierte a las directivas en herramientas extremadamente versátiles para crear funcionalidad reutilizable que se puede aplicar a cualquier elemento HTML de forma declarativa.

Alan Sastre - Autor del tutorial

Alan Sastre

Ingeniero de Software y formador, CEO en CertiDevs

Ingeniero de software especializado en Full Stack y en Inteligencia Artificial. Como CEO de CertiDevs, Angular es una de sus áreas de expertise. Con más de 15 años programando, 6K seguidores en LinkedIn y experiencia como formador, Alan se dedica a crear contenido educativo de calidad para desarrolladores de todos los niveles.

Más tutoriales de Angular

Explora más contenido relacionado con Angular y continúa aprendiendo con nuestros tutoriales gratuitos.

Aprendizajes de esta lección

  • Comprender la estructura básica y creación de directivas de atributo en Angular.
  • Aprender a manipular el DOM de forma segura usando Renderer2.
  • Configurar directivas con inputs para personalizar su comportamiento.
  • Implementar outputs para comunicar eventos desde la directiva al componente padre.
  • Aplicar directivas standalone y gestionar su importación en componentes.