Desarrollo robusto de componentes personalizados en Angular: comunicación, reutilización y aislamiento de estilos

Fundamentos del desarrollo avanzado de componentes

Los componentes personalizados son la piedra angular de una aplicación Angular, y su diseño determina la mantenibilidad y escalabilidad del proyecto. Este artículo explora patrones avanzados de comunicación, estrategias de reutilización y aislamiento de estilos, aprovechando la API reactiva basada en señales de las versiones más recientes de Angular.

1. Modelos de comunicación entre componentes

Angular ofrece múltiples mecanismos para el flujo de datos entre componentes, cuya elección depende de la relación jerárquica y la naturaleza de los datos.

1.1 Entradas reactivas con la función input()

Las señales de entrada (input()) reemplazan al decorador @Input() tradicional, proporcionando una sintaxis más concisa y capacidades reactivas mejoradas, como valores por defecto, validación de campos requeridos y transformación de datos.

// product-detail.component.ts
import { Component, input, computed } from '@angular/core';

interface Product {
  sku: string;
  title: string;
}

@Component({
  selector: 'app-product-detail',
  standalone: true,
  template: `
    <div class="product">
      <h3>{{ productName() }}</h3>
      <p>SKU: {{ productId() }}</p>
      <p>Precio formateado: {{ formattedCost() }}</p>
    </div>
  `
})
export class ProductDetailComponent {
  // Entrada con valor por defecto
  productName = input<string>('Producto sin nombre');
  
  // Entrada obligatoria
  productId = input.required<string>();
  
  // Entrada con transformación
  baseCost = input<number>(0, {
    transform: (v: number | string) => {
      const parsed = Number(v);
      return isNaN(parsed) ? 0 : parseFloat(parsed.toFixed(2));
    }
  });

  // Señal derivada
  formattedCost = computed(() => `€${this.baseCost()}`);
}

1.2 Eventos con la función output()

La función output() simplifica la emisión de eventos, reemplazando a EventEmitter. Para la comunicación bidireccional, se utiliza el atajo model().

// feedback-stars.component.ts
import { Component, input, output } from '@angular/core';

@Component({
  selector: 'app-feedback-stars',
  standalone: true,
  template: `
    <div class="star-container">
      @for (star of starSequence; track star) {
        <button (click)="handleSelection(star)" 
                [class.highlighted]="star <= currentRating()">
          ★
        </button>
      }
    </div>
  `
})
export class FeedbackStarsComponent {
  currentRating = input<number>(0);
  ratingChanged = output<number>();
  starSequence = [1, 2, 3, 4, 5];

  handleSelection(star: number) {
    this.ratingChanged.emit(star);
  }
}

// Para bidireccionalidad con model()
// counter-display.component.ts
import { Component, model } from '@angular/core';

@Component({
  selector: 'app-counter-display',
  standalone: true,
  template: `
    <button (click)="value.update(v => v - 1)">-</button>
    <span>{{ value() }}</span>
    <button (click)="value.update(v => v + 1)">+</button>
  `
})
export class CounterDisplayComponent {
  value = model<number>(0);
}

1.3 Inyección de dependencias para compartir estado

La inyección de dependencias permite compartir datos entre componentes no relacionados jerárquicamente, evitando el problema del "prop drilling".

// language.service.ts (servicio singleton)
import { Injectable, signal } from '@angular/core';

@Injectable({ providedIn: 'root' })
export class LanguageService {
  private _currentLang = signal('es');
  
  get activeLanguage() {
    return this._currentLang();
  }
  
  setLanguage(lang: string) {
    this._currentLang.set(lang);
  }
}

// Un componente inyecta y modifica
// settings-panel.component.ts
import { Component, inject } from '@angular/core';
import { LanguageService } from './language.service';

@Component({ ... })
export class SettingsPanelComponent {
  languageService = inject(LanguageService);

  switchToEnglish() {
    this.languageService.setLanguage('en');
  }
}

2. Estrategias de reutilización de componentes

2.1 Proyección de contenido con ng-content

La proyección de contenido permite a un componente actuar como un contenedor flexible para contenido dinámico inyectado por su padre.

// generic-panel.component.ts
import { Component, input } from '@angular/core';

@Component({
  selector: 'app-generic-panel',
  standalone: true,
  template: `
    <section class="panel">
      <header class="panel-header">
        <h2>{{ panelTitle() }}</h2>
      </header>
      <div class="panel-body">
        <ng-content></ng-content>
      </div>
      <footer class="panel-footer">
        <ng-content select="[panel-footer]"></ng-content>
      </footer>
    </section>
  `
})
export class GenericPanelComponent {
  panelTitle = input('Panel');
}

Los componentes padres pueden llenar múltiples ranuras de contenido de manera declarativa.

<app-generic-panel panelTitle="Mi Panel">
  <p>Este es el contenido principal del cuerpo.</p>
  <div panel-footer>
    <button>Acción del pie de página</button>
  </div>
</app-generic-panel>

2.2 Composición de directivas

En lugar de la herencia de componenets, que puede crear acoplamiento, se prefiere el uso de directivas para añadir comportamientos reutilizables de forma desacoplada.

// click-outside.directive.ts
import { Directive, ElementRef, Output, EventEmitter, HostListener } from '@angular/core';

@Directive({
  selector: '[appClickOutside]',
  standalone: true
})
export class ClickOutsideDirective {
  @Output() clickOutside = new EventEmitter<void>();

  constructor(private elementRef: ElementRef) {}

  @HostListener('document:click', ['$event.target'])
  public onClick(targetElement: HTMLElement) {
    const isInside = this.elementRef.nativeElement.contains(targetElement);
    if (!isInside) {
      this.clickOutside.emit();
    }
  }
}

// Uso en un menú desplegable
// dropdown-menu.component.ts
@Component({
  selector: 'app-dropdown-menu',
  standalone: true,
  imports: [ClickOutsideDirective],
  template: `
    <div appClickOutside (clickOutside)="closeMenu()">
      <button (click)="toggle()">Abrir Menú</button>
      @if (isOpen) {
        <div class="menu-content">
          <ng-content></ng-content>
        </div>
      }
    </div>
  `
})
export class DropdownMenuComponent {
  isOpen = false;
  toggle() { this.isOpen = !this.isOpen; }
  closeMenu() { this.isOpen = false; }
}

3. Aislamiento y personalización de estilos

3.1 Estrategias de encapsulamiento

Angular proporciona diferentes modos de encapsulamiento de estilos a través de ViewEncapsulation, siendo Emulated (el predeterminado) el más común, que añade atributos únicos a los selectores CSS para simular el aislamiento del Shadow DOM.

3.2 Variables CSS para temas y personalización

Las variables CSS (Custom Properties) ofrecen una forma elegante de permitir la personalización de componentes sin comprometre su encapsulamiento.

// alert-banner.component.ts
import { Component, input } from '@angular/core';

@Component({
  selector: 'app-alert-banner',
  standalone: true,
  template: `
    <div class="alert" [class]="'alert-' + type()">
      {{ message() }}
    </div>
  `,
  styles: [`
    .alert {
      padding: var(--alert-padding, 1rem);
      border-radius: var(--alert-radius, 4px);
      border: 1px solid;
      background-color: var(--alert-bg);
      color: var(--alert-color);
    }
    .alert-success {
      --alert-bg: var(--color-success-light, #d4edda);
      --alert-color: var(--color-success-dark, #155724);
    }
    .alert-danger {
      --alert-bg: var(--color-danger-light, #f8d7da);
      --alert-color: var(--color-danger-dark, #721c24);
    }
  `]
})
export class AlertBannerComponent {
  message = input<string>('');
  type = input<'success' | 'danger'>('success');
}

Los componentes padres pueden anular estas variables CSS para personalizar la apariencia:

<!-- En la hoja de estilos del padre -->
:host {
  --alert-padding: 1.5rem;
  --alert-radius: 8px;
  --color-success-light: #e6f4ea;
}

Para una aplicación completa de temas, se pueden definir y cambiar variables a nivel global usando un servicio y estableciéndolas en el elemento raíz del documento.

Etiquetas: angular componentes Señales Inyección de Dependencias CSS Variables

Publicado el 6-14 04:21