En esta página

Formularios reactivos con validación

15 min lectura TextoCap. 3 — Arquitectura

Formularios reactivos

Los formularios reactivos (Reactive Forms) definen la estructura del formulario en TypeScript, dando control total sobre validación, estado y transformaciones.

Configurar Reactive Forms

Importa ReactiveFormsModule en el componente:

import { ReactiveFormsModule } from '@angular/forms';

@Component({
  imports: [ReactiveFormsModule],
  // ...
})

FormBuilder

FormBuilder es un servicio que simplifica la creación de controles:

import { inject } from '@angular/core';
import { FormBuilder, Validators } from '@angular/forms';

const fb = inject(FormBuilder);

const formulario = fb.nonNullable.group({
  nombre: ['', Validators.required],
  edad: [0, [Validators.required, Validators.min(18)]],
});

Tipos de controles

Tipo Uso Ejemplo
FormControl Un solo valor Input de texto, checkbox
FormGroup Grupo de controles Formulario completo
FormArray Lista dinámica Agregar/quitar campos

FormArray ejemplo

readonly formulario = this.fb.nonNullable.group({
  título: [''],
  etiquetas: this.fb.array([
    this.fb.control('angular'),
    this.fb.control('typescript'),
  ]),
});

agregarEtiqueta(valor: string): void {
  const etiquetas = this.formulario.controls.etiquetas;
  etiquetas.push(this.fb.control(valor));
}

Validadores integrados

Angular incluye validadores comunes:

import { Validators } from '@angular/forms';

fb.control('', [
  Validators.required,        // Campo obligatorio
  Validators.minLength(3),    // Longitud mínima
  Validators.maxLength(50),   // Longitud máxima
  Validators.email,           // Formato email
  Validators.min(0),          // Valor mínimo
  Validators.max(100),        // Valor máximo
  Validators.pattern(/^\d+$/), // Expresión regular
]);

Validadores personalizados

Crea funciones que reciben un AbstractControl y devuelven errores o null:

function emailCorporativo(control: AbstractControl): ValidationErrors | null {
  const email: string = control.value;
  if (!email) return null;
  return email.endsWith('@empresa.com')
    ? null
    : { emailNoCorporativo: true };
}

Mostrar errores en el template

@if (formulario.controls.email.touched
     && formulario.controls.email.errors; as errores) {
  @if (errores['required']) {
    <span class="error">Campo obligatorio</span>
  } @else if (errores['email']) {
    <span class="error">Formato de email invalido</span>
  }
}

Estado del formulario

Propiedad Descripcion
.valid Todos los controles son validos
.invalid Al menos un control es invalido
.touched El usuario interactuo con el control
.dirty El valor fue modificado
.pristine El valor no ha sido modificado

Práctica

  1. Crea un formulario de contacto: Usa FormBuilder para crear un formulario con campos nombre, email y mensaje. Agrega Validators.required y Validators.email donde corresponda.
  2. Implementa un validador personalizado: Crea una funcion validadora que verifique que un campo de texto no contenga palabras prohibidas. Asociala a un control del formulario.
  3. Muestra errores condicionalmente: Usa @if con .touched y .errors para mostrar mensajes de error especificos debajo de cada campo solo despues de que el usuario interactue con ellos.

En la siguiente leccion aprenderemos a conectar con APIs usando HttpClient.

nonNullable
Usa fb.nonNullable.group() para que los controles no acepten null. Así getRawValue() devuelve tipos estrictos sin null, mejorando la inferencia de TypeScript.
No mezcles enfoques
No combines Template-driven forms (ngModel) con Reactive forms (formGroup) en el mismo formulario. Elige uno u otro. Angular 21 recomienda Reactive forms para formularios complejos.
import {
  Component, inject, signal,
  ChangeDetectionStrategy,
} from '@angular/core';
import {
  ReactiveFormsModule, FormBuilder,
  Validators, AbstractControl, ValidationErrors,
} from '@angular/forms';

// Validador personalizado
function passwordSegura(control: AbstractControl): ValidationErrors | null {
  const valor: string = control.value;
  if (!valor) return null;

  const tieneNumero = /\d/.test(valor);
  const tieneMayuscula = /[A-Z]/.test(valor);
  const tieneLongitud = valor.length >= 8;

  if (tieneNumero && tieneMayuscula && tieneLongitud) return null;
  return {
    passwordDebil: {
      requiereNumero: !tieneNumero,
      requiereMayuscula: !tieneMayuscula,
      requiereLongitud: !tieneLongitud,
    },
  };
}

@Component({
  selector: 'app-registro',
  changeDetection: ChangeDetectionStrategy.OnPush,
  imports: [ReactiveFormsModule],
  templateUrl: './registro.html',
})
export class Registro {
  private readonly fb = inject(FormBuilder);
  readonly enviado = signal(false);

  readonly formulario = this.fb.nonNullable.group({
    nombre: ['', [Validators.required, Validators.minLength(3)]],
    email: ['', [Validators.required, Validators.email]],
    password: ['', [Validators.required, passwordSegura]],
    aceptaTerminos: [false, Validators.requiredTrue],
  });

  enviar(): void {
    if (this.formulario.invalid) {
      this.formulario.markAllAsTouched();
      return;
    }
    const datos = this.formulario.getRawValue();
    console.log('Registro:', datos);
    this.enviado.set(true);
  }
}