@HostBinding y @HostListener

Avanzado
Angular
Angular
Actualizado: 24/09/2025

@HostBinding para propiedades del host

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.

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 20+, 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.

@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 20+, 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 - 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 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.