En esta página
Inputs, outputs y model signals
Comunicación entre componentes
Angular ofrece tres primitivas para la comunicación padre-hijo:
input()— El padre envia datos al hijo (unidireccional)output()— El hijo emite eventos al padremodel()— Binding bidireccional entre padre e hijo
Input signals
Los input() reemplazan al decorador @Input. Son signals de solo lectura:
import { input } from '@angular/core';
// Input opcional con valor por defecto
readonly título = input('Sin título');
// Input requerido (obligatorio en el template)
readonly userId = input.required<number>();
// Input con transformación
readonly deshabilitado = input(false, {
transform: (valor: string | boolean) => typeof valor === 'string' ? valor !== 'false' : valor,
});
// Input con alias
readonly datos = input<string[]>([], { alias: 'data' });Usar inputs en el template del padre
<!-- String literal (sin corchetes) -->
<app-card título="Bienvenido" />
<!-- Binding a expresión (con corchetes) -->
<app-card [título]="tituloSignal()" [userId]="42" />Output signals
Los output() reemplazan al decorador @Output. Emiten eventos al componente padre:
import { output } from '@angular/core';
// Output simple
readonly click = output<void>();
// Output con datos
readonly seleccionado = output<number>();
// Output con alias
readonly cerrar = output<void>({ alias: 'close' });Emitir un evento
// En el método del componente
this.seleccionado.emit(42);
this.click.emit();Escuchar en el padre
<app-lista (seleccionado)="manejarSeleccion($event)" />Model signals
model() crea un signal que soporta two-way binding. Internamente genera un input y un output con el sufijo Change:
import { model } from '@angular/core';
// model() === input valor + output valorChange
readonly valor = model(0);
// model requerido
readonly selección = model.required<string>();Two-way binding
<!-- El padre puede leer y escribir el model del hijo -->
<app-slider [(valor)]="miValor" />
<!-- Equivalente largo -->
<app-slider [valor]="miValor()" (valorChange)="miValor.set($event)" />Patron contenedor-presentación
Un patrón comun es separar la lógica (contenedor) de la presentación (UI pura):
// Componente presentacional: solo inputs + outputs
@Component({ selector: 'app-producto-card', /* ... */ })
export class ProductoCard {
readonly producto = input.required<Producto>();
readonly agregarAlCarrito = output<number>();
}
// Componente contenedor: maneja estado y lógica
@Component({ selector: 'app-tienda', /* ... */ })
export class Tienda {
readonly productos = signal<Producto[]>([]);
agregarProducto(id: number): void {
// lógica de negocio
}
}Resumen de API
| API | Lectura | Escritura | Two-way | Uso |
|---|---|---|---|---|
input() |
Si | No | No | Recibir datos del padre |
output() |
No | Emit | No | Notificar al padre |
model() |
Si | Si | Si | Binding bidireccional |
Práctica
- Crea un componente con model(): Implementa un componente
RangeSlidercon unmodel()para el valor y usa[(valor)]en el componente padre para lograr two-way binding. - Usa input con transform: Crea un input que reciba un string
"true"o"false"y lo transforme automaticamente a booleano usando la opciontransform. - Patron contenedor-presentacion: Separa un componente existente en un componente contenedor (con logica y estado) y un componente presentacional (solo
input()youtput()).
En la siguiente leccion aprenderemos sobre servicios e inyección de dependencias, la columna vertebral de Angular.
model() vs input() + output()
Usa model() cuando necesites two-way binding ([(valor)]). Es equivalente a tener un input() + un output con nombre valorChange, pero con menos boilerplate.
Inputs con alias
Puedes renombrar un input para el template externo: input({ alias: 'título' }). El nombre en la clase será diferente al nombre en el HTML.
import {
Component, input, output, model,
computed, ChangeDetectionStrategy,
} from '@angular/core';
// ---- Componente hijo: Slider ----
@Component({
selector: 'app-slider',
changeDetection: ChangeDetectionStrategy.OnPush,
template: `
<div class="slider">
<label>{{ etiqueta() }}</label>
<input
type="range"
[min]="min()"
[max]="max()"
[value]="valor()"
(input)="valor.set(+$any($event.target).value)"
/>
<span>{{ valor() }}</span>
</div>
`,
})
export class Slider {
// Input requerido
readonly etiqueta = input.required<string>();
// Inputs con valores por defecto
readonly min = input(0);
readonly max = input(100);
// Model: two-way binding automático
readonly valor = model(50);
}
// ---- Componente padre ----
@Component({
selector: 'app-editor-color',
changeDetection: ChangeDetectionStrategy.OnPush,
imports: [Slider],
template: `
<h2>Editor de color</h2>
<app-slider etiqueta="Rojo" [max]="255" [(valor)]="rojo" />
<app-slider etiqueta="Verde" [max]="255" [(valor)]="verde" />
<app-slider etiqueta="Azul" [max]="255" [(valor)]="azul" />
<div
class="preview"
[style.background-color]="colorRGB()"
>
{{ colorRGB() }}
</div>
<button (click)="copiar.emit(colorRGB())">Copiar color</button>
`,
})
export class EditorColor {
readonly rojo = model(128);
readonly verde = model(128);
readonly azul = model(64);
readonly colorRGB = computed(
() => `rgb(${this.rojo()}, ${this.verde()}, ${this.azul()})`
);
readonly copiar = output<string>();
}
Inicia sesión para guardar tu progreso