Saltar a contenido

8. Formularios: capturar y validar información

Del campo escrito por el usuario a un modelo listo para enviar.

Los seis pasos

flowchart LR
    P1["1 · Definir controles<br/>nombre y correo con FormControl"] --> P2["2 · Asignar validadores<br/>required, email, longitud"]
    P2 --> P3["3 · Vincular el HTML<br/>formGroup y formControlName"]
    P3 --> P4["4 · Mostrar retroalimentación<br/>errores solo si el control fue tocado"]
    P4 --> P5["5 · Validar al enviar<br/>si es inválido, marcar y detener"]
    P5 --> P6["6 · Construir NewStudent<br/>getRawValue() → servicio"]
Paso Qué se hace
1. Definir controles Crear nombre y correo con FormControl, agrupados en un FormGroup
2. Asignar validadores required, email y reglas de longitud
3. Vincular el HTML formGroup y formControlName conectan vista y estado
4. Mostrar retroalimentación Errores visibles solo cuando el control fue tocado
5. Validar al enviar Si el formulario es inválido, marcar controles y detener
6. Construir NewStudent Tomar getRawValue() tipado y delegarlo al servicio

Estados que debemos comprender

Estado Significado
valid Todos los controles cumplen sus reglas
invalid Existe al menos un control con error
touched El usuario ya interactuó con el control

Formularios reactivos: ideas clave

  • FormControl representa un campo y su estado.
  • FormGroup reúne controles como un solo formulario.
  • Validators declaran reglas reutilizables.
  • La validación del cliente mejora la experiencia, pero el backend debe volver a validar.
  • Al enviar, getRawValue() produce un NewStudent que el componente delega al servicio.

8.1 Formulario reactivo tipado: ejemplo completo

Importar → construir → validar → convertir en modelo → delegar al servicio.

src/app/models/student.model.ts
export type NewStudent = Omit<Student, 'id'>;   // el backend generará el identificador
src/app/students/student-form/student-form.component.ts
import { Component, inject } from '@angular/core';
import {
  ReactiveFormsModule, FormBuilder, Validators
} from '@angular/forms';
import { NewStudent } from '../../models/student.model';
import { StudentService } from '../../services/student.service';

@Component({
  selector: 'app-student-form',
  standalone: true,
  imports: [ReactiveFormsModule],
  templateUrl: './student-form.component.html'
})
export class StudentFormComponent {
  private fb = inject(FormBuilder);
  private service = inject(StudentService);

  // Pasos 1 y 2: controles y validadores
  form = this.fb.nonNullable.group({
    name: ['', Validators.required],
    email: ['', [
      Validators.required, Validators.email
    ]],
    active: [true]
  });

  submit(): void {
    // Paso 5: validar al enviar
    if (this.form.invalid) {
      this.form.markAllAsTouched();
      return;
    }
    // Paso 6: construir el modelo y delegar al servicio
    const student: NewStudent =
      this.form.getRawValue();
    this.service.create(student).subscribe();
  }
}
src/app/students/student-form/student-form.component.html
<!-- Paso 3: vincular el HTML -->
<form [formGroup]="form" (ngSubmit)="submit()">

  <label>
    Nombre
    <input formControlName="name">
  </label>
  <!-- Paso 4: retroalimentación solo si el control fue tocado -->
  @if (form.controls.name.touched && form.controls.name.hasError('required')) {
    <small class="error">El nombre es obligatorio</small>
  }

  <label>
    Correo
    <input type="email" formControlName="email">
  </label>
  @if (form.controls.email.touched && form.controls.email.hasError('required')) {
    <small class="error">El correo es obligatorio</small>
  }
  @if (form.controls.email.touched && form.controls.email.hasError('email')) {
    <small class="error">El correo no tiene un formato válido</small>
  }

  <label>
    <input type="checkbox" formControlName="active"> Activo
  </label>

  <button type="submit">Guardar</button>
</form>

Detalles que conviene notar:

  • nonNullable hace que cada control tenga un tipo preciso (string, boolean) en lugar de string | null; por eso getRawValue() encaja directamente en NewStudent.
  • (ngSubmit) se dispara al enviar (botón submit o Enter) y evita la recarga de la página.
  • markAllAsTouched() hace visibles todos los errores aunque el usuario no haya pasado por cada campo.

Validadores más usados

Validador Error que produce
Validators.required required
Validators.email email
Validators.minLength(n) / maxLength(n) minlength / maxlength
Validators.min(n) / max(n) min / max
Validators.pattern(regex) pattern

¿Y después de guardar?

El ejemplo hace subscribe() sin manejar el resultado. Lo habitual es navegar al detalle si todo salió bien y mostrar un mensaje si falló; ese manejo es el tema de Excepciones:

this.service.create(student).subscribe({
  next: created => this.router.navigate(['/students', created.id]),
  error: () => this.message.set('No fue posible guardar el estudiante')
});

Validar en ambos lados

La validación del frontend mejora la experiencia. La validación del backend (@Valid sobre el DTO) garantiza la integridad: cualquiera puede enviar una petición a la API sin pasar por el formulario.

Idea clave

El componente valida y construye el modelo; el servicio se encarga de enviarlo a la API.

Siguiente: Excepciones.