Capítulo 3 - Parte 3: Comunicación con @Input y @Output¶
Parte 3 de 4 · Capítulo 3 · PARTE II - Componentes: El Alma de Angular
La reutilización real de un componente exige que pueda adaptarse a diferentes contextos. Un componente de contador que siempre empieza en cero y solo muestra su valor interno tiene utilidad limitada. Uno que acepta un valor inicial, un máximo y que notifica al padre cuando cambia de valor, puede reutilizarse en formularios, configuradores de cantidad en un carrito, reproductores de media y decenas de contextos más. @Input() y @Output() son el mecanismo de Angular para construir ese canal de comunicación entre componentes padre e hijo.
@Input: el flujo de datos hacia abajo¶
El decorador @Input() marca una propiedad de la clase como un "puerto de entrada" que el componente padre puede configurar desde su template. El flujo siempre es unidireccional: de padre a hijo. El hijo no puede (ni debe) modificar directamente el valor que recibió.
// archivo: etiqueta-estado.component.ts
import { Component, Input } from '@angular/core';
import { NgClass } from '@angular/common';
type EstadoPedido = 'pendiente' | 'enviado' | 'entregado' | 'cancelado';
@Component({
selector: 'app-etiqueta-estado',
standalone: true,
imports: [NgClass],
template: `
<span class="etiqueta" [ngClass]="estado">
{{ estado | uppercase }}
</span>
`,
styles: [`
.etiqueta { padding: 2px 10px; border-radius: 12px; font-size: 0.8rem; }
.pendiente { background: #fff9c4; color: #f57f17; }
.enviado { background: #e3f2fd; color: #1565c0; }
.entregado { background: #e8f5e9; color: #2e7d32; }
.cancelado { background: #fce4ec; color: #b71c1c; }
`]
})
export class EtiquetaEstadoComponent {
// El padre puede pasar cualquier valor de EstadoPedido
@Input() estado: EstadoPedido = 'pendiente';
}
El componente padre lo usa así:
<!-- En el template del componente padre -->
<app-etiqueta-estado [estado]="pedido.estado" />
<app-etiqueta-estado estado="enviado" /> <!-- También funciona con valor literal -->
Alias y transformaciones de @Input¶
@Input() acepta opciones adicionales. La más útil en Angular 16+ es transform, que permite transformar el valor entrante antes de asignarlo a la propiedad. El caso de uso más frecuente es convertir el atributo HTML disabled (que llega como string vacío "" o como la cadena "true") a un booleano real:
import { Component, Input, booleanAttribute, numberAttribute } from '@angular/core';
@Component({
selector: 'app-boton-accion',
standalone: true,
template: `
<button [disabled]="deshabilitado" [style.font-size.px]="tamano">
<ng-content />
</button>
`
})
export class BotonAccionComponent {
// booleanAttribute convierte "" o "true" -> true, "false" o ausencia -> false
@Input({ transform: booleanAttribute }) deshabilitado: boolean = false;
// numberAttribute convierte "16" -> 16
@Input({ transform: numberAttribute }) tamano: number = 14;
// alias: el padre usa [etiqueta] pero internamente es "texto"
@Input({ alias: 'etiqueta' }) texto: string = 'Aceptar';
}
Con estas transformaciones, el padre puede escribir <app-boton-accion deshabilitado tamano="18"> en lugar de tener que pasar [deshabilitado]="true". Es la misma ergonomía que tienen los elementos HTML nativos.
@Output: el flujo de eventos hacia arriba¶
Mientras @Input() lleva datos del padre al hijo, @Output() permite al hijo notificar al padre que algo ocurrió. El decorador @Output() marca una propiedad de tipo EventEmitter<T>, que funciona como un Observable especial diseñado para emitir eventos dentro del template.
// archivo: campo-busqueda.component.ts
import { Component, Input, Output, EventEmitter } from '@angular/core';
import { FormsModule } from '@angular/forms';
@Component({
selector: 'app-campo-busqueda',
standalone: true,
imports: [FormsModule],
template: `
<div class="busqueda">
<input
type="search"
[placeholder]="placeholder"
[(ngModel)]="termino"
(input)="alEscribir()"
/>
<button (click)="limpiar()">✕</button>
</div>
`
})
export class CampoBusquedaComponent {
@Input() placeholder: string = 'Buscar...';
// El padre escuchará este evento con (buscar)="handler($event)"
@Output() buscar = new EventEmitter<string>();
// Este evento no emite datos: solo notifica que ocurrió la acción
@Output() limpiar$ = new EventEmitter<void>();
termino: string = '';
alEscribir(): void {
this.buscar.emit(this.termino); // Emite el string al padre
}
limpiar(): void {
this.termino = '';
this.buscar.emit(''); // Notifica al padre que se limpió
this.limpiar$.emit(); // Evento adicional sin datos
}
}
El padre escucha el evento con la sintaxis de event binding:
<app-campo-busqueda
placeholder="Buscar productos..."
(buscar)="filtrarProductos($event)"
(limpiar$)="reiniciarFiltros()"
/>
$event contiene el valor que el hijo pasó a emit(). En este caso es un string, pero puede ser cualquier tipo: un objeto, un número, o void si el evento no transporta datos.
Ejemplo bidireccional completo: componente de contador¶
Integremos ambos decoradores en un ejemplo real que muestra el patrón padre-hijo completo. El componente ContadorComponent recibe un valor inicial y un máximo desde el padre, y emite cambios hacia arriba cada vez que el usuario interactúa.
// archivo: contador.component.ts
import { Component, Input, Output, EventEmitter, OnInit, numberAttribute } from '@angular/core';
@Component({
selector: 'app-contador',
standalone: true,
template: `
<div class="contador">
<button (click)="decrementar()" [disabled]="valor <= minimo">−</button>
<span class="valor">{{ valor }}</span>
<button (click)="incrementar()" [disabled]="valor >= maximo">+</button>
</div>
`,
styles: [`
.contador { display: flex; gap: 12px; align-items: center; }
.valor { min-width: 2ch; text-align: center; font-size: 1.4rem; font-weight: bold; }
button { width: 32px; height: 32px; border-radius: 50%; cursor: pointer; }
`]
})
export class ContadorComponent implements OnInit {
@Input({ transform: numberAttribute }) valorInicial: number = 0;
@Input({ transform: numberAttribute }) maximo: number = 10;
@Input({ transform: numberAttribute }) minimo: number = 0;
// Emite el nuevo valor cada vez que cambia
@Output() valorCambiado = new EventEmitter<number>();
valor: number = 0;
ngOnInit(): void {
// Inicializamos el valor interno a partir del @Input
this.valor = this.valorInicial;
}
incrementar(): void {
if (this.valor < this.maximo) {
this.valor++;
this.valorCambiado.emit(this.valor);
}
}
decrementar(): void {
if (this.valor > this.minimo) {
this.valor--;
this.valorCambiado.emit(this.valor);
}
}
}
El componente padre que lo utiliza:
// archivo: configurador-pedido.component.ts
import { Component } from '@angular/core';
import { ContadorComponent } from './contador.component';
@Component({
selector: 'app-configurador-pedido',
standalone: true,
imports: [ContadorComponent],
template: `
<h3>Configurar pedido</h3>
<label>Cantidad:</label>
<app-contador
[valorInicial]="cantidad"
[maximo]="stockDisponible"
[minimo]="1"
(valorCambiado)="alCambiarCantidad($event)"
/>
<p>Total: {{ cantidad }} × {{ precioPorUnidad | currency }} = {{ total | currency }}</p>
`
})
export class ConfiguradorPedidoComponent {
cantidad: number = 1;
stockDisponible: number = 20;
precioPorUnidad: number = 49.99;
get total(): number {
return this.cantidad * this.precioPorUnidad;
}
alCambiarCantidad(nuevaCantidad: number): void {
this.cantidad = nuevaCantidad;
// Aquí podría también actualizar el carrito, persistir en localStorage, etc.
}
}
El patrón es claro: el padre es el dueño del estado (cantidad). El hijo muestra ese estado y notifica cambios, pero nunca modifica la propiedad del padre directamente. Esta separación hace que ambos componentes sean independientes y predecibles: ContadorComponent no sabe nada sobre pedidos o precios, solo sabe contar dentro de los límites que le dan.
Puntos clave¶
@Input()define propiedades que el componente padre puede configurar desde el template; el flujo de datos es siempre de padre a hijo.@Output()conEventEmitter<T>permite al hijo notificar eventos al padre; el padre los escucha con(nombreEvento)="handler($event)".- Las opciones
transform(booleanAttribute,numberAttribute) convierten valores de atributos HTML a los tipos correctos automáticamente. - El patrón recomendado es que el padre sea el dueño del estado y el hijo solo lo muestre y emita cambios, nunca mutando el valor recibido directamente.
- El tipo genérico de
EventEmitter<T>documenta qué tipo de dato transporta el evento; usaEventEmitter<void>cuando el evento no necesita datos.
¿Qué sigue?¶
En la Parte 4 cerramos el capítulo estudiando cómo Angular aísla los estilos de cada componente, qué opciones ofrece ViewEncapsulation y cómo aprovechar las variables CSS personalizadas para crear un sistema de theming robusto.