Apariencia
📝 Clase 10 — Formularios (Template-driven vs Reactivos)
📅 2026-07-25 · 🗂️ Proyecto:
app-routing(destino en este repo:02-Ejercicios/app-routing) · 🎯 Las dos formas de manejar formularios en Angular: Template-driven (ngModel) y Reactivos (FormGroup/FormControl).⬅️ Viene después de Clase 9 (Routing avanzado), misma sesión de clase — el profe pasó de Routing a Formularios usando el mismo proyecto
app-routing.
Angular tiene dos formas de construir formularios, con filosofías distintas: Template-driven (la lógica vive sobre todo en el HTML, con ngModel) y Reactivos (la lógica vive en la clase TS, con FormGroup/FormControl). Se armó un ejemplo de cada uno con los mismos campos (Nombre, Email) para compararlos lado a lado.
🎯 Qué aprendí
- Formularios Template-driven:
[(ngModel)],FormsModule, validadores como atributos HTML, y#miFormulario="ngForm"como referencia de plantilla. - Formularios Reactivos:
FormGroup/FormControlexplícitos, y la forma más corta conFormBuilder(fb.group({...})). - Los
Validatorsde Angular (required,minLength,email) como funciones TS, y el error real de escribirlos sueltos en vez de agrupados en un array. - Cuándo conviene cada enfoque (formularios simples vs. grandes/dinámicos).
🗺️ Diagrama: dónde vive la lógica en cada enfoque

- Template-driven — el estado queda repartido: propiedades sueltas en la clase, validadores como atributos en el HTML, y una referencia de plantilla (
ngForm) para leer el estado completo del formulario. - Reactivos — el estado queda centralizado: un solo objeto
FormGrouparmado en la clase (con sus validadores como funciones TS), y el HTML solo se conecta a ese objeto ya armado ([formGroup],formControlName).
🅰️ Template-driven Forms (ngModel)
ts
// contacto-form.ts
import { Component } from '@angular/core';
import { FormsModule, NgForm } from '@angular/forms';
@Component({
selector: 'app-contacto-form',
imports: [FormsModule],
templateUrl: './contacto-form.html',
styleUrl: './contacto-form.css',
})
export class ContactoForm {
nombre = '';
email = '';
edad = '';
enviar(formulario: NgForm): void {
if (formulario.valid) {
console.log(this.nombre, this.email, this.edad);
}
}
}html
<!-- contacto-form.html -->
<h1>Formulario Básico</h1>
<form #miFormulario="ngForm" (ngSubmit)="enviar(miFormulario)">
<label for="nombre">Nombre</label>
<input id="nombre" name="nombre" autocomplete="name" [(ngModel)]="nombre" required minlength="3" maxlength="20"> <br> <br>
<label for="email">Email</label>
<input id="email" name="email" autocomplete="email" [(ngModel)]="email" required email> <br> <br>
<label for="edad">Edad</label>
<input id="edad" name="edad" autocomplete="off" [(ngModel)]="edad" required pattern="^[0-9]+$"> <br> <br>
<button type="submit" [disabled]="miFormulario.invalid">Enviar</button>
</form>FormsModule— hay que importarlo en el componente (imports: [FormsModule]) para poder usarngModel/ngFormen su template; sin esto, Angular ni reconoce esas directivas.[(ngModel)]="nombre"— two-way binding: la propiedadnombrede la clase y el valor del<input>quedan sincronizados en ambos sentidos (escribes en el input → cambianombre; cambiasnombredesde código → se actualiza el input). Necesitaname="nombre"en el<input>para que Angular pueda registrarlo dentro delngForm(#miFormulario="ngForm").- Validadores como atributos HTML —
required,minlength="3",maxlength="20",email,pattern="^[0-9]+$"son los validadores nativos de HTML5, que Angular reconoce automáticamente (víaFormsModule) y usa para marcar el formulario válido/inválido — no hay que escribir lógica de validación en la clase. #miFormulario="ngForm"— una referencia de plantilla (template reference variable) al formulario completo (todos sus controles y su estado de validez), que se puede pasar como argumento aenviar(miFormulario)o leer directo en el HTML (miFormulario.invalid).(ngSubmit)="enviar(miFormulario)"— evento que Angular dispara al enviar el formulario (en vez de(submit)nativo, que recargaría la página);ngSubmitya viene conpreventDefault()incluido.- La lógica del formulario vive "repartida": el estado (
nombre,email,edad) está en la clase, pero la validación está declarada en el HTML — por eso se llama template-driven ("dirigido por la plantilla").
🅱️ Formularios Reactivos (FormGroup / FormControl)
Primera versión, con FormGroup/FormControl explícitos (con new):
ts
// contacto-reactivo.ts (versión 1 — explícita)
import { Component } from '@angular/core';
import { ReactiveFormsModule, FormGroup, FormControl, Validators } from '@angular/forms';
@Component({
selector: 'app-contacto-reactivo',
imports: [ReactiveFormsModule],
templateUrl: './contacto-reactivo.html',
styleUrl: './contacto-reactivo.css',
})
export class ContactoReactivo {
formulario = new FormGroup({
nombre: new FormControl('', [Validators.required, Validators.minLength(5)]),
email: new FormControl('', [Validators.required, Validators.email]),
});
enviar(): void {
if (this.formulario.valid) {
console.log('Datos enviados', this.formulario.value);
} else {
console.log('Formulario invalido, revisa los campos');
}
}
}html
<!-- contacto-reactivo.html -->
<h1>Formulario Reactivo</h1>
<form [formGroup]="formulario" (ngSubmit)="enviar()">
<label for="nombre">Nombre</label>
<input id="nombre" formControlName="nombre" /> <br><br>
<label for="email">Email</label>
<input id="email" formControlName="email" /> <br><br>
<button type="submit" [disabled]="formulario.invalid">Enviar</button>
</form>⭐ Versión 2 — FormBuilder (la forma más corta de armar el FormGroup)
El profe mostró después una forma más corta de escribir lo mismo, con el servicio FormBuilder:
ts
// contacto-reactivo.ts (versión 2 — con FormBuilder)
import { Component, inject } from '@angular/core';
import { FormBuilder, ReactiveFormsModule, Validators } from '@angular/forms';
@Component({
selector: 'app-contacto-reactivo',
imports: [ReactiveFormsModule],
templateUrl: './contacto-reactivo.html',
styleUrl: './contacto-reactivo.css',
})
export class ContactoReactivo {
private fb = inject(FormBuilder);
formulario = this.fb.group({
nombre: ['', [Validators.required, Validators.minLength(3)]],
email: ['', [Validators.required, Validators.email]],
});
enviar(): void {
if (this.formulario.valid) {
console.log(this.formulario.value);
}
}
}FormBuilder— un servicio inyectable (inject(FormBuilder)) con métodos de fábrica que armanFormGroup/FormControlsin escribirnewen cada campo — mismo resultado que la versión 1, con menos código repetido.this.fb.group({ ... })— reemplaza anew FormGroup({ ... }). Cada campo se escribe como un array corto en vez denew FormControl(...):nombre: ['', [Validators.required, Validators.minLength(3)]]es equivalente anombre: new FormControl('', [Validators.required, Validators.minLength(3)]).- Forma del array de cada campo:
[valorInicial, validadorEsSincrono(s), validadorAsíncrono(s)]— hasta 3 posiciones: el valor inicial, un validador (o array de validadores) síncronos, y opcionalmente un validador (o array) asíncrono.
⚠️ Error real detectado en la captura: el campo
tsemail: ['', Validators.required, Validators.email](sin el
[ ]interno alrededor de los validadores). Con la forma corta deFormBuilder, el segundo elemento del array es el validador síncrono y el tercero es el validador asíncrono — al escribirValidators.requiredyValidators.emailcomo dos elementos sueltos (posiciones 2 y 3),Validators.emailtermina en la posición de validador asíncrono, no de validador síncrono normal. El camponombre:tsemail: ['', [Validators.required, Validators.email]] // 👈 corregido
🧪 Tip de entrevista: ¿Cuál es la forma del array corto de
FormBuilder.group()? → Hasta 3 posiciones:[valorInicial, validadorSíncrono(s), validadorAsíncrono(s)]. Si se necesita más de un validador síncrono, hay que agruparlos en un array en la segunda posición ([Validators.required, Validators.email]) — ponerlos como elementos sueltos del array principal los desplaza a posiciones equivocadas (la tercera posición es para validadores asíncronos, no para un segundo validador síncrono).
ReactiveFormsModule(noFormsModule) — el módulo que habilita[formGroup]/formControlNameen el template.FormGroup— agrupa variosFormControlbajo un solo objeto; representa todo el formulario (o una sección de él). Su.valueda un objeto con todos los campos:{ nombre: '...', email: '...' }.FormControl— representa un solo campo. El primer argumento es el valor inicial (''), el segundo es un array de validadores (Validators.required,Validators.minLength(3),Validators.email, etc.) — las mismas reglas que antes, pero como funciones de TypeScript en vez de atributos HTML.[formGroup]="formulario"— conecta el<form>completo con el objetoFormGroupde la clase (property binding, con corchetes).formControlName="nombre"— conecta un<input>con elFormControlque tiene ese nombre dentro delFormGroup(debe coincidir exactamente con la clave usada ennew FormGroup({ nombre: ... })).formulario.invalid— la validez ahora se consulta directo en el objetoFormGroupde la clase (this.formulario.valid/.invalid), sin necesitar una referencia de plantilla como#miFormulario="ngForm".- Toda la lógica del formulario vive en la clase TS (la estructura, los valores iniciales y los validadores se arman ahí) — el HTML solo se conecta a esa estructura ya armada. Por eso se llama reactivo: el formulario es un objeto reactivo de RxJS (
FormGroup/FormControlexponenvalueChanges,statusChanges, etc., aunque no se usó todavía en este ejemplo) que vive independiente del template. enviar()conelse(versión 1) — aunque el botón ya queda deshabilitado con[disabled]="formulario.invalid"(no se debería poder hacer submit inválido desde el mouse), elif/elseenenviar()es una segunda capa de seguridad: cubre el caso de enviar el formulario por otro medio (p. ej. presionando Enter) y deja un mensaje claro en consola en vez de fallar en silencio. La versión 2 (FormBuilder) quedó más simple, solo con elif— el botón deshabilitado ya alcanza para ese ejemplo.
🆚 Comparación: Template-driven vs Reactivos
Template-driven (ngModel) | Reactivos (FormGroup) | |
|---|---|---|
| Módulo a importar | FormsModule | ReactiveFormsModule |
| Dónde vive la estructura del formulario | Repartida: propiedades sueltas en la clase + ngModel/validadores en el HTML | Centralizada: un objeto FormGroup armado en la clase |
| Validadores | Atributos HTML (required, minlength, pattern...) | Funciones TS (Validators.required, Validators.minLength(3)...) |
| Acceso a los valores | Propiedades sueltas (this.nombre, this.email) | Un solo objeto (this.formulario.value) |
| Testear sin el DOM | Difícil (la lógica depende del template) | Fácil (FormGroup es un objeto TS normal, se puede probar sin renderizar nada) |
| Bueno para | Formularios simples, pocos campos | Formularios grandes, dinámicos, o que necesiten lógica compleja (campos que aparecen/desaparecen, validadores cruzados entre campos, etc.) |
🧪 Tip de entrevista: ¿Cuándo usar Template-driven vs Reactivos? → Template-driven para formularios simples y pequeños, donde la rapidez de escribir importa más que la flexibilidad; Reactivos para formularios grandes o dinámicos, donde se necesita testear la lógica de validación sin depender del DOM, o armar/cambiar controles desde código (agregar un campo dinámicamente, validadores que dependen de otro campo, etc.).
🧪 Tip de entrevista: ¿Qué hace
Validators.email/Validators.minLength(3)? → Son funciones validadoras que Angular corre contra el valor del control; si el valor no cumple la regla, el validador agrega un error al control (p. ej.{ email: true }) y elFormGroup/FormControlcompleto quedainvalidhasta que se corrija.
✅ Verificado corriendo
Ambos formularios se probaron en 02-Ejercicios/app-routing (/formulario y /formulario-reactivo), llenando datos reales y enviando:
Datos enviados {nombre: 'gvbv', email: 'styp@gmail.com'}- El botón "Enviar" arranca deshabilitado (
[disabled]="formulario.invalid"/[disabled]="miFormulario.invalid") y se habilita solo cuando todos los validadores pasan (nombre con mínimo 3 caracteres, email con formato válido). - Al enviar con datos válidos, aparece
console.log('Datos enviados', ...)con el objeto{ nombre, email }; con datos inválidos (forzando el submit), aparece'Formulario invalido, revisa los campos'.
🏋️ Ejercicios con solución
Ejercicio 1 — Agregar un campo "Teléfono" al formulario Template-driven
Agrega un campo telefono a ContactoForm (contacto-form.ts/.html), con [(ngModel)], required y pattern="^[0-9]{9}$" (9 dígitos), siguiendo el mismo patrón que nombre/email/edad.
Ver solución
ts
// contacto-form.ts
export class ContactoForm {
nombre = '';
email = '';
edad = '';
telefono = '';
enviar(formulario: NgForm): void {
if (formulario.valid) {
console.log(this.nombre, this.email, this.edad, this.telefono);
}
}
}html
<!-- contacto-form.html -->
<label for="telefono">Teléfono</label>
<input id="telefono" name="telefono" autocomplete="tel" [(ngModel)]="telefono" required pattern="^[0-9]{9}$"> <br> <br>Ejercicio 2 — El mismo campo, pero en el formulario Reactivo
Agrega el mismo campo telefono a ContactoReactivo (versión FormBuilder), con Validators.required y Validators.pattern(/^[0-9]{9}$/).
Ver solución
ts
// contacto-reactivo.ts (versión FormBuilder)
formulario = this.fb.group({
nombre: ['', [Validators.required, Validators.minLength(3)]],
email: ['', [Validators.required, Validators.email]],
telefono: ['', [Validators.required, Validators.pattern(/^[0-9]{9}$/)]],
});html
<!-- contacto-reactivo.html -->
<label for="telefono">Teléfono</label>
<input id="telefono" formControlName="telefono" /> <br><br>❓ Preguntas y respuestas
1. ¿Cuál es la diferencia principal entre Template-driven y Reactivos?
Dónde vive la lógica del formulario: repartida entre HTML y clase (Template-driven) vs centralizada en un objeto
FormGroupde la clase (Reactivos).
2. ¿Qué error real se documentó con FormBuilder y por qué pasaba?
Escribir
email: ['', Validators.required, Validators.email]sin agrupar los validadores en un array — la 3ª posición del array corto es para el validador asíncrono, así queValidators.emailterminaba en el lugar equivocado.
3. ¿Por qué los Reactivos son más fáciles de testear que los Template-driven?
Porque
FormGroup/FormControlson objetos TypeScript normales — se pueden crear y validar en un test sin necesidad de renderizar el DOM.
4. ¿Qué hace #miFormulario="ngForm" y para qué se usa?
Es una referencia de plantilla al formulario completo (todos sus controles y su estado de validez); se usa para pasarlo a
enviar(miFormulario)o leermiFormulario.invaliddirecto en el HTML.
📎 Apuntes relacionados
- routing-avanzado.md — Clase 9, mismo proyecto
app-routing, donde vive el código de estos dos formularios (rutas/formularioy/formulario-reactivo). 02-Conceptos.md— pendiente resumir aquí Template-driven vs Reactivos en cuanto se vea más código (validadores custom,valueChanges, etc.).
➡️ Siguiente
Esperando más capturas de la clase para completar (validadores personalizados, mostrar mensajes de error por campo, valueChanges, etc.).