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 CSSaddClass(element, className)
- Añade clases CSSremoveClass(element, className)
- Elimina clases CSSsetAttribute(element, name, value)
- Establece atributos HTMLremoveAttribute(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
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.