Capítulo 5 - Parte 2: Event Binding (event) y Two-way Binding [(ngModel)]¶
Parte 2 de 4 · Capítulo 5 · PARTE III - Templates y Directivas
La Parte 1 estableció cómo los datos fluyen desde la clase hacia el template. Ahora cerramos el circuito: el event binding captura lo que ocurre en el DOM y lo comunica de vuelta a la clase. Cuando combinamos ambas direcciones en un solo mecanismo obtenemos el two-way binding, la base de los formularios simples en Angular.
Event Binding: (evento)¶
La sintaxis de event binding usa paréntesis () rodeando el nombre del evento DOM que se quiere escuchar. Cuando el evento ocurre, Angular ejecuta la expresión del lado derecho:
import { Component } from '@angular/core';
@Component({
selector: 'app-contador',
standalone: true,
template: `
<p>Contador: {{ cuenta }}</p>
<button (click)="incrementar()">+1</button>
<button (click)="resetear()">Resetear</button>
<!-- La expresión puede ser directa, no solo una llamada a método -->
<button (click)="cuenta = cuenta - 1">-1</button>
`
})
export class ContadorComponent {
cuenta = 0;
incrementar(): void {
this.cuenta++;
}
resetear(): void {
this.cuenta = 0;
}
}
Angular escucha cualquier evento DOM estándar: click, dblclick, mouseover, mouseout, focus, blur, submit, keydown, keyup, scroll, y todos los demás. También puede escuchar eventos personalizados emitidos por componentes hijos (→ Ver Capítulo 3, Parte 3).
El objeto $event¶
Cuando Angular invoca el manejador del evento, pone a disposición el objeto del evento nativo de DOM bajo la variable especial $event. Podemos pasarlo al método o usar sus propiedades directamente en la expresión:
import { Component } from '@angular/core';
@Component({
selector: 'app-buscador',
standalone: true,
template: `
<input
type="text"
(input)="alEscribir($event)"
(focus)="enFoco = true"
(blur)="enFoco = false"
/>
<p *ngIf="enFoco">Campo activo - término: "{{ termino }}"</p>
`
})
export class BuscadorComponent {
termino = '';
enFoco = false;
// $event es de tipo Event; para acceder a value necesitamos castear
alEscribir(evento: Event): void {
const input = evento.target as HTMLInputElement;
this.termino = input.value;
}
}
El cast as HTMLInputElement es necesario porque TypeScript tipifica evento.target como EventTarget | null, que es la interfaz base. En la Parte 3 veremos cómo las template reference variables eliminan la necesidad de este cast.
Filtros de tecla: (keyup.enter), (keydown.escape)¶
Angular extiende la sintaxis de event binding con filtros de tecla que evitan escribir lógica de comparación de event.key dentro del manejador:
import { Component } from '@angular/core';
@Component({
selector: 'app-campo-busqueda',
standalone: true,
template: `
<input
type="text"
[(ngModel)]="termino"
(keyup.enter)="buscar()"
(keydown.escape)="limpiar()"
placeholder="Presiona Enter para buscar..."
/>
<button (click)="buscar()">Buscar</button>
`
})
export class CampoBusquedaComponent {
termino = '';
buscar(): void {
console.log('Buscando:', this.termino);
}
limpiar(): void {
this.termino = '';
}
}
La sintaxis (keyup.enter) equivale a (keyup) con un manejador que comprueba event.key === 'Enter'. Angular soporta también (keyup.shift.enter), (keydown.control.z) y combinaciones con modificadores.
Two-way Binding: [(ngModel)] - la "banana in a box"¶
El nombre coloquial "banana in a box" describe visualmente la sintaxis [()]: los corchetes [] del property binding contienen los paréntesis () del event binding, como una banana dentro de una caja. Conceptualmente, [(ngModel)] es azúcar sintáctica que combina exactamente esas dos operaciones:
Para usar ngModel hay que importar FormsModule:
import { Component } from '@angular/core';
import { FormsModule } from '@angular/forms';
@Component({
selector: 'app-perfil',
standalone: true,
imports: [FormsModule], // FormsModule provee la directiva NgModel
template: `
<label>Nombre:
<input type="text" [(ngModel)]="nombre" />
</label>
<label>Edad:
<input type="number" [(ngModel)]="edad" />
</label>
<p>Hola, {{ nombre }}. Tienes {{ edad }} años.</p>
`
})
export class PerfilComponent {
nombre = 'María';
edad = 28;
}
Cada vez que el usuario escribe en el campo, ngModel captura el evento input, extrae el valor y lo asigna a la propiedad. Cada vez que la propiedad cambia en la clase, ngModel actualiza el campo del formulario. El resultado es sincronización bidireccional continua.
La forma equivalente manual: [value] + (input)¶
Es importante entender que [(ngModel)] no hace nada mágico que no se pueda hacer manualmente. La forma explícita sirve para casos donde FormsModule no está disponible o se prefiere control total:
import { Component } from '@angular/core';
@Component({
selector: 'app-manual',
standalone: true,
template: `
<input
[value]="correo"
(input)="correo = $any($event.target).value"
/>
<p>Correo: {{ correo }}</p>
`
})
export class ManualComponent {
correo = '';
}
$any() es una función especial de los templates de Angular que suprime la verificación de tipos del compilador, equivalente a un cast a any. Se usa con moderación cuando el tipado estricto genera falsos positivos en situaciones como esta.
Two-way Binding con Signal Inputs en Angular 17+¶
Con la llegada de los Signals, Angular introdujo model() como nueva forma de crear propiedades con two-way binding en componentes hijos. Esto reemplaza el patrón @Input() valor + @Output() valorChange:
import { Component, model } from '@angular/core';
// Componente hijo con model signal
@Component({
selector: 'app-calificacion',
standalone: true,
template: `
@for (estrella of estrellas; track estrella) {
<span
[style.color]="estrella <= puntuacion() ? 'gold' : 'gray'"
(click)="puntuacion.set(estrella)"
style="cursor: pointer; font-size: 24px"
>★</span>
}
`
})
export class CalificacionComponent {
// model() crea un Signal que soporta [(modelo)] desde el padre
puntuacion = model<number>(0);
estrellas = [1, 2, 3, 4, 5];
}
import { Component, signal } from '@angular/core';
import { CalificacionComponent } from './calificacion.component';
// Componente padre usando [(puntuacion)]
@Component({
selector: 'app-resena',
standalone: true,
imports: [CalificacionComponent],
template: `
<app-calificacion [(puntuacion)]="miPuntuacion" />
<p>Tu puntuación: {{ miPuntuacion() }}/5</p>
`
})
export class ResenaComponent {
miPuntuacion = signal(3);
}
El signal creado con model() funciona exactamente como [(ngModel)] pero en el nivel de comunicación entre componentes: cuando el componente hijo llama a puntuacion.set(valor), el padre recibe la actualización automáticamente porque [(puntuacion)] es el azúcar sintáctica para [puntuacion]="miPuntuacion" (puntuacionChange)="miPuntuacion.set($event)".
Diagrama: cómo funciona internamente el two-way binding¶
sequenceDiagram
participant U as Usuario (DOM)
participant D as Directiva NgModel
participant C as Clase del componente
U->>D: Evento input (usuario escribe)
D->>C: Actualiza propiedad = $event.target.value
Note over C: Change Detection detecta cambio
C->>D: [ngModel]="propiedad" (valor actualizado)
D->>U: Actualiza el value del input en el DOM
Puntos clave¶
(evento)escucha eventos DOM; la expresión del lado derecho se ejecuta cuando el evento ocurre.$eventcontiene el objeto nativo del evento o el valor emitido por un@Output()de un componente hijo.(keyup.enter),(keydown.escape)y similares filtran por tecla sin necesidad de lógica en el manejador.[(ngModel)]es azúcar sintáctica sobre[value]+(input); requiere importarFormsModule.model()de Angular 17+ reemplaza el patrón@Input+@Outputpara two-way binding entre componentes.
¿Qué sigue?¶
En la Parte 3 estudiamos las template reference variables (#ref) y cómo @ViewChild permite acceder desde la clase TypeScript a elementos e instancias del template.