@HostBinding para propiedades del host
flowchart LR
A["@HostBinding 'class.active'"] --> B[Propiedad isActive]
B --> C[Si true → class active]
D[host { '[class.x]': 'expr' } API moderna] --> A
E["@HostListener 'click'"] --> F[Método handler]
G["host { '(click)': 'fn()' }"] --> E
H[Tree-shaking mejor con host {}] --> D
El decorador @HostBinding permite modificar dinámicamente propiedades, atributos, clases CSS y estilos del elemento host donde se aplica un componente o directiva. Esta funcionalidad es esencial para crear componentes y directivas que puedan adaptar su apariencia y comportamiento según su estado interno.
Nota Angular 21: A partir de Angular 15, se recomienda usar la propiedad
hosten los metadatos del decorador@Componento@Directivecomo alternativa preferida a los decoradores@HostBindingy@HostListener. La propiedadhostofrece mejor tree-shaking y una sintaxis más declarativa. Los decoradores siguen funcionando, pero el enfoque moderno es usarhost: { '[class.active]': 'isActive', '(click)': 'onClick()' }directamente en los metadatos.
Conceptos fundamentales
El elemento host es el elemento DOM al que se aplica un componente o directiva. Con @HostBinding podemos vincular propiedades de nuestra clase TypeScript directamente a propiedades del elemento host, creando una conexión reactiva entre el estado interno y la presentación visual.
import { Component, HostBinding } from '@angular/core';
@Component({
selector: 'app-status-badge',
template: `<span>{{ status }}</span>`,
standalone: true
})
export class StatusBadgeComponent {
status = 'active';
// Vincula la propiedad className del host al valor calculado
@HostBinding('class')
get cssClass() {
return `status-badge status-${this.status}`;
}
}
Binding de clases CSS
Una de las aplicaciones más comunes de @HostBinding es la gestión dinámica de clases CSS. Podemos vincular clases individuales o múltiples clases según el estado del componente.
Binding de clase individual:
@Component({
selector: 'app-button',
template: `<ng-content></ng-content>`,
standalone: true
})
export class ButtonComponent {
@Input() disabled = false;
@Input() variant: 'primary' | 'secondary' = 'primary';
// Vincula una clase específica
@HostBinding('class.disabled')
get isDisabled() {
return this.disabled;
}
@HostBinding('class.btn-primary')
get isPrimary() {
return this.variant === 'primary';
}
@HostBinding('class.btn-secondary')
get isSecondary() {
return this.variant === 'secondary';
}
}
Binding de múltiples clases:
@Component({
selector: 'app-card',
template: `<ng-content></ng-content>`,
standalone: true
})
export class CardComponent {
@Input() elevated = false;
@Input() size: 'small' | 'medium' | 'large' = 'medium';
@HostBinding('class')
get hostClasses() {
const classes = ['card', `card-${this.size}`];
if (this.elevated) {
classes.push('card-elevated');
}
return classes.join(' ');
}
}
Binding de estilos CSS
@HostBinding también permite modificar estilos directamente en el elemento host, útil para valores dinámicos o propiedades calculadas.
@Component({
selector: 'app-progress-bar',
template: `
<div class="progress-label">{{ label }}</div>
<div class="progress-fill"></div>
`,
standalone: true
})
export class ProgressBarComponent {
@Input() progress = 0;
@Input() color = '#007bff';
@Input() label = '';
// Binding de estilo individual
@HostBinding('style.width.%')
get widthPercentage() {
return Math.min(Math.max(this.progress, 0), 100);
}
@HostBinding('style.background-color')
get backgroundColor() {
return this.color;
}
// Binding de múltiples estilos
@HostBinding('style')
get hostStyles() {
return {
'border-radius': '4px',
'height': '20px',
'transition': 'width 0.3s ease'
};
}
}
Binding de atributos HTML
Los atributos HTML también pueden ser vinculados dinámicamente, especialmente útil para atributos de accesibilidad o propiedades específicas del elemento.
@Component({
selector: 'app-tooltip',
template: `<ng-content></ng-content>`,
standalone: true
})
export class TooltipComponent {
@Input() tooltipText = '';
@Input() position: 'top' | 'bottom' | 'left' | 'right' = 'top';
@HostBinding('attr.title')
get title() {
return this.tooltipText;
}
@HostBinding('attr.data-position')
get dataPosition() {
return this.position;
}
@HostBinding('attr.role')
get role() {
return 'tooltip';
}
}
Combinando @HostBinding con signals
En Angular 21, podemos integrar signals con @HostBinding para crear bindings completamente reactivos.
@Component({
selector: 'app-theme-switcher',
template: `
<button (click)="toggleTheme()">
{{ theme() === 'dark' ? '🌙' : '☀️' }}
</button>
`,
standalone: true
})
export class ThemeSwitcherComponent {
theme = signal<'light' | 'dark'>('light');
@HostBinding('class.theme-dark')
get isDarkTheme() {
return this.theme() === 'dark';
}
@HostBinding('class.theme-light')
get isLightTheme() {
return this.theme() === 'light';
}
toggleTheme() {
this.theme.update(current => current === 'dark' ? 'light' : 'dark');
}
}
Casos de uso prácticos
Componente de notificación con estados:
@Component({
selector: 'app-notification',
template: `
<div class="notification-content">
<span class="notification-icon">{{ getIcon() }}</span>
<span class="notification-message">
<ng-content></ng-content>
</span>
</div>
`,
standalone: true
})
export class NotificationComponent {
@Input() type: 'success' | 'warning' | 'error' | 'info' = 'info';
@Input() dismissible = false;
@HostBinding('class')
get notificationClasses() {
const classes = ['notification', `notification-${this.type}`];
if (this.dismissible) {
classes.push('notification-dismissible');
}
return classes.join(' ');
}
@HostBinding('attr.role')
get ariaRole() {
return this.type === 'error' ? 'alert' : 'status';
}
private getIcon() {
const icons = {
success: '✅',
warning: '⚠️',
error: '❌',
info: 'ℹ️'
};
return icons[this.type];
}
}
Esta aproximación permite crear componentes altamente reutilizables donde el elemento host se adapta automáticamente según el estado interno, manteniendo una separación clara entre la lógica del componente y su presentación visual.
Alternativa moderna con la propiedad host
En Angular 21, la forma preferida es usar la propiedad host en los metadatos del decorador. El ejemplo anterior del componente de notificación se puede reescribir así:
@Component({
selector: 'app-notification',
template: `
<div class="notification-content">
<span class="notification-icon">{{ getIcon() }}</span>
<span class="notification-message">
<ng-content></ng-content>
</span>
</div>
`,
host: {
'[class]': 'notificationClasses',
'[attr.role]': 'ariaRole'
}
})
export class NotificationModernComponent {
type = input<'success' | 'warning' | 'error' | 'info'>('info');
dismissible = input(false);
get notificationClasses() {
const classes = ['notification', `notification-${this.type()}`];
if (this.dismissible()) {
classes.push('notification-dismissible');
}
return classes.join(' ');
}
get ariaRole() {
return this.type() === 'error' ? 'alert' : 'status';
}
private getIcon() {
const icons: Record<string, string> = {
success: '✅', warning: '⚠️', error: '❌', info: 'ℹ️'
};
return icons[this.type()];
}
}
Este enfoque con host y signal inputs (input()) es la forma idiomática en Angular 21.
@HostListener para eventos del host
El decorador @HostListener permite escuchar y responder a eventos DOM del elemento host donde se aplica un componente o directiva. Esta funcionalidad es fundamental para crear componentes interactivos que reaccionen a la interacción del usuario de manera directa y eficiente.
Conceptos fundamentales
@HostListener establece una conexión directa entre eventos DOM del elemento host y métodos de nuestra clase TypeScript. A diferencia de los event bindings en templates, @HostListener actúa sobre el propio elemento host, permitiendo crear comportamientos que se integran naturalmente con el elemento contenedor.
import { Component, HostListener } from '@angular/core';
@Component({
selector: 'app-interactive-card',
template: `
<div class="card-content">
<h3>{{ title }}</h3>
<p>Hover over this card!</p>
</div>
`,
standalone: true
})
export class InteractiveCardComponent {
title = 'Interactive Card';
// Escucha eventos de mouse en el host element
@HostListener('mouseenter')
onMouseEnter() {
console.log('Mouse entered the card');
}
@HostListener('mouseleave')
onMouseLeave() {
console.log('Mouse left the card');
}
}
Eventos de teclado
Los eventos de teclado son especialmente útiles para crear componentes accesibles y con navegación avanzada. @HostListener puede capturar eventos como keydown, keyup y keypress.
@Component({
selector: 'app-modal',
template: `
<div class="modal-backdrop">
<div class="modal-content">
<h2>{{ title }}</h2>
<button (click)="close()">Close</button>
<ng-content></ng-content>
</div>
</div>
`,
standalone: true
})
export class ModalComponent {
@Input() title = 'Modal';
@Output() closed = new EventEmitter<void>();
// Escucha la tecla Escape para cerrar el modal
@HostListener('keydown.escape')
onEscapePressed() {
this.close();
}
// Navegación con flechas
@HostListener('keydown.arrowup', ['$event'])
@HostListener('keydown.arrowdown', ['$event'])
onArrowNavigation(event: KeyboardEvent) {
event.preventDefault();
// Lógica de navegación por elementos focusables
this.navigateFocusableElements(event.key === 'ArrowUp' ? -1 : 1);
}
close() {
this.closed.emit();
}
private navigateFocusableElements(direction: number) {
// Implementación de navegación por teclado
const focusableElements = this.getFocusableElements();
const currentIndex = focusableElements.indexOf(document.activeElement as HTMLElement);
const nextIndex = (currentIndex + direction + focusableElements.length) % focusableElements.length;
focusableElements[nextIndex]?.focus();
}
private getFocusableElements(): HTMLElement[] {
const selector = 'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])';
return Array.from(document.querySelectorAll(selector)) as HTMLElement[];
}
}
Eventos de mouse y touch
Los eventos de mouse permiten crear interacciones sofisticadas y experiencias de usuario enriquecidas.
@Component({
selector: 'app-draggable',
template: `
<div class="draggable-content">
<span class="drag-handle">⋮⋮</span>
<ng-content></ng-content>
</div>
`,
standalone: true
})
export class DraggableComponent {
private isDragging = false;
private startX = 0;
private startY = 0;
@HostListener('mousedown', ['$event'])
onMouseDown(event: MouseEvent) {
if ((event.target as HTMLElement).classList.contains('drag-handle')) {
this.isDragging = true;
this.startX = event.clientX;
this.startY = event.clientY;
event.preventDefault();
}
}
@HostListener('document:mousemove', ['$event'])
onMouseMove(event: MouseEvent) {
if (this.isDragging) {
const deltaX = event.clientX - this.startX;
const deltaY = event.clientY - this.startY;
// Aplicar transformación
(event.currentTarget as HTMLElement).style.transform =
`translate(${deltaX}px, ${deltaY}px)`;
}
}
@HostListener('document:mouseup')
onMouseUp() {
this.isDragging = false;
}
// Soporte para dispositivos táctiles
@HostListener('touchstart', ['$event'])
onTouchStart(event: TouchEvent) {
const touch = event.touches[0];
this.startX = touch.clientX;
this.startY = touch.clientY;
this.isDragging = true;
}
}
Gestión de foco y accesibilidad
@HostListener es especialmente útil para la accesibilidad, permitiendo crear componentes que responden correctamente a la navegación por teclado.
@Component({
selector: 'app-dropdown',
template: `
<button class="dropdown-trigger"
(click)="toggle()"
[attr.aria-expanded]="isOpen()">
{{ placeholder }}
</button>
@if (isOpen()) {
<ul class="dropdown-menu" role="listbox">
@for (option of options; track option.id) {
<li role="option"
[class.selected]="selectedOption()?.id === option.id"
(click)="selectOption(option)">
{{ option.label }}
</li>
}
</ul>
}
`,
standalone: true
})
export class DropdownComponent {
@Input() options: Array<{id: string, label: string}> = [];
@Input() placeholder = 'Select an option';
isOpen = signal(false);
selectedOption = signal<{id: string, label: string} | null>(null);
// Cierra el dropdown al hacer click fuera
@HostListener('document:click', ['$event'])
onDocumentClick(event: Event) {
const target = event.target as HTMLElement;
if (!target.closest('app-dropdown')) {
this.isOpen.set(false);
}
}
// Manejo de navegación por teclado
@HostListener('keydown.enter')
@HostListener('keydown.space')
onEnterOrSpace() {
if (!this.isOpen()) {
this.isOpen.set(true);
}
}
@HostListener('keydown.escape')
onEscape() {
if (this.isOpen()) {
this.isOpen.set(false);
}
}
@HostListener('keydown.tab')
onTab() {
this.isOpen.set(false);
}
toggle() {
this.isOpen.update(current => !current);
}
selectOption(option: {id: string, label: string}) {
this.selectedOption.set(option);
this.isOpen.set(false);
}
}
Combinando con signals y estado reactivo
En Angular 21, podemos combinar @HostListener con signals para crear componentes completamente reactivos.
@Component({
selector: 'app-focus-tracker',
template: `
<div class="focus-indicator">
Focus count: {{ focusCount() }}
<br>
Currently focused: {{ isFocused() ? 'Yes' : 'No' }}
</div>
<ng-content></ng-content>
`,
standalone: true
})
export class FocusTrackerComponent {
isFocused = signal(false);
focusCount = signal(0);
@HostListener('focus')
onFocus() {
this.isFocused.set(true);
this.focusCount.update(count => count + 1);
}
@HostListener('blur')
onBlur() {
this.isFocused.set(false);
}
// Efecto reactivo basado en el estado de foco
focusEffect = effect(() => {
if (this.isFocused()) {
console.log(`Element focused ${this.focusCount()} times`);
}
});
}
Directivas host con @HostListener
Las host directives de Angular 15+ pueden aprovechar @HostListener para crear comportamientos composables y reutilizables.
@Directive({
selector: '[appClickOutside]',
standalone: true
})
export class ClickOutsideDirective {
@Output() clickOutside = new EventEmitter<void>();
@HostListener('document:click', ['$event'])
onDocumentClick(event: Event) {
const target = event.target as HTMLElement;
const hostElement = this.elementRef.nativeElement;
if (!hostElement.contains(target)) {
this.clickOutside.emit();
}
}
constructor(private elementRef: ElementRef) {}
}
// Uso como host directive
@Component({
selector: 'app-sidebar',
template: `<nav class="sidebar-content"><ng-content></ng-content></nav>`,
hostDirectives: [
{
directive: ClickOutsideDirective,
outputs: ['clickOutside']
}
],
standalone: true
})
export class SidebarComponent {
isOpen = signal(true);
@HostListener('clickOutside')
onClickOutside() {
this.isOpen.set(false);
}
}
Mejores prácticas
Gestión de memoria y performance:
@Component({
selector: 'app-scroll-tracker',
template: `<div>Scroll position: {{ scrollY() }}px</div>`,
standalone: true
})
export class ScrollTrackerComponent implements OnDestroy {
scrollY = signal(0);
private destroyed = signal(false);
@HostListener('window:scroll', ['$event'])
onWindowScroll() {
// Throttle para mejorar performance
if (!this.destroyed()) {
requestAnimationFrame(() => {
this.scrollY.set(window.scrollY);
});
}
}
ngOnDestroy() {
this.destroyed.set(true);
}
}
@HostListener proporciona una interfaz limpia y declarativa para manejar eventos del elemento host, permitiendo crear componentes que responden naturalmente a la interacción del usuario mientras mantienen un código organizado y fácil de mantener.
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 el uso de @HostBinding para modificar propiedades, clases, estilos y atributos del elemento host.
- Aprender a utilizar @HostListener para escuchar y manejar eventos DOM del elemento host.
- Integrar signals de Angular 20+ con @HostBinding y @HostListener para lograr reactividad avanzada.
- Aplicar @HostBinding y @HostListener en casos prácticos para mejorar la reutilización y accesibilidad de componentes.
- Conocer buenas prácticas para la gestión de memoria y rendimiento al usar estos decoradores.