Skip to content

📝 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/FormControl explícitos, y la forma más corta con FormBuilder (fb.group({...})).
  • Los Validators de 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

Diagrama comparando formularios Template-driven y Reactivos en Angular: en Template-driven el HTML tiene los validadores y sincroniza con propiedades sueltas de la clase vía ngModel, leído con una referencia de plantilla ngForm; en Reactivos la clase arma un FormGroup centralizado con Validators, y el HTML solo se conecta a ese objeto ya armado vía formGroup y formControlName

  • 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 FormGroup armado 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 usar ngModel/ngForm en su template; sin esto, Angular ni reconoce esas directivas.
  • [(ngModel)]="nombre"two-way binding: la propiedad nombre de la clase y el valor del <input> quedan sincronizados en ambos sentidos (escribes en el input → cambia nombre; cambias nombre desde código → se actualiza el input). Necesita name="nombre" en el <input> para que Angular pueda registrarlo dentro del ngForm (#miFormulario="ngForm").
  • Validadores como atributos HTMLrequired, minlength="3", maxlength="20", email, pattern="^[0-9]+$" son los validadores nativos de HTML5, que Angular reconoce automáticamente (vía FormsModule) 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 a enviar(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); ngSubmit ya viene con preventDefault() 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 arman FormGroup/FormControl sin escribir new en cada campo — mismo resultado que la versión 1, con menos código repetido.
  • this.fb.group({ ... }) — reemplaza a new FormGroup({ ... }). Cada campo se escribe como un array corto en vez de new FormControl(...): nombre: ['', [Validators.required, Validators.minLength(3)]] es equivalente a nombre: 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 email se escribió como

ts
email: ['', Validators.required, Validators.email]

(sin el [ ] interno alrededor de los validadores). Con la forma corta de FormBuilder, el segundo elemento del array es el validador síncrono y el tercero es el validador asíncrono — al escribir Validators.required y Validators.email como dos elementos sueltos (posiciones 2 y 3), Validators.email termina en la posición de validador asíncrono, no de validador síncrono normal. El campo email dejaría de validarse como se espera (o generaría un error de tipos, según la versión de Angular). Corrección: envolver ambos validadores en un array, como se hizo con nombre:

ts
email: ['', [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 (no FormsModule) — el módulo que habilita [formGroup]/formControlName en el template.
  • FormGroup — agrupa varios FormControl bajo un solo objeto; representa todo el formulario (o una sección de él). Su .value da 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 objeto FormGroup de la clase (property binding, con corchetes).
  • formControlName="nombre" — conecta un <input> con el FormControl que tiene ese nombre dentro del FormGroup (debe coincidir exactamente con la clave usada en new FormGroup({ nombre: ... })).
  • formulario.invalid — la validez ahora se consulta directo en el objeto FormGroup de 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/FormControl exponen valueChanges, statusChanges, etc., aunque no se usó todavía en este ejemplo) que vive independiente del template.
  • enviar() con else (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), el if/else en enviar() 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 el if — el botón deshabilitado ya alcanza para ese ejemplo.

🆚 Comparación: Template-driven vs Reactivos

Template-driven (ngModel)Reactivos (FormGroup)
Módulo a importarFormsModuleReactiveFormsModule
Dónde vive la estructura del formularioRepartida: propiedades sueltas en la clase + ngModel/validadores en el HTMLCentralizada: un objeto FormGroup armado en la clase
ValidadoresAtributos HTML (required, minlength, pattern...)Funciones TS (Validators.required, Validators.minLength(3)...)
Acceso a los valoresPropiedades sueltas (this.nombre, this.email)Un solo objeto (this.formulario.value)
Testear sin el DOMDifícil (la lógica depende del template)Fácil (FormGroup es un objeto TS normal, se puede probar sin renderizar nada)
Bueno paraFormularios simples, pocos camposFormularios 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 el FormGroup/FormControl completo queda invalid hasta que se corrija.

✅ Verificado corriendo

Ambos formularios se probaron en 02-Ejercicios/app-routing (/formulario y /formulario-reactivo), llenando datos reales y enviando:

DevTools · Console
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 FormGroup de 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í que Validators.email terminaba en el lugar equivocado.

3. ¿Por qué los Reactivos son más fáciles de testear que los Template-driven?

Porque FormGroup/FormControl son 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 leer miFormulario.invalid directo en el HTML.

📎 Apuntes relacionados

  • routing-avanzado.md — Clase 9, mismo proyecto app-routing, donde vive el código de estos dos formularios (rutas /formulario y /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.).