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.