Listado de la etiqueta: forms

Signal Forms: cuando los formularios reactivos finalmente tienen sentido

Los formularios reactivos de Angular siempre me parecieron demasiado verbosos. Crear un FormGroup, definir los FormControl, suscribirse a valueChanges, acordarse de hacer unsubscribe… Signal Forms simplifica todo eso. El formulario es sincrónico, tipado y reactivo sin esfuerzo.

El problema con FormGroup y FormControl

// ❌ FormGroup clásico: verboso y con memory leaks potenciales
import { FormBuilder, Validators } from '@angular/forms';

@Component({ ... })
export class LoginClasico implements OnDestroy {
  private destroy$ = new Subject<void>();

  form = this.fb.group({
    email: ['', [Validators.required, Validators.email]],
    password: ['', [Validators.required, Validators.minLength(8)]]
  });

  constructor(private fb: FormBuilder) {
    this.form.valueChanges
      .pipe(takeUntil(this.destroy$))
      .subscribe(val => console.log(val));
  }

  ngOnDestroy() { this.destroy$.next(); this.destroy$.complete(); }
}

Signal Forms: la forma moderna

// ✅ Signal Forms: sincrónico, tipado, sin suscripciones
import { Component } from '@angular/core';
import { form, field } from '@angular/forms/signals';
import { Validators } from '@angular/forms';

@Component({
  selector: 'app-login',
  template: `
    <form (ngSubmit)="onSubmit()">
      <div>
        <label>Email</label>
        <input type="email" [formField]="loginForm.controls.email" />
        @if (loginForm.controls.email.invalid() && loginForm.controls.email.touched()) {
          <span class="error">Email inválido</span>
        }
      </div>
      <div>
        <label>Password</label>
        <input type="password" [formField]="loginForm.controls.password" />
      </div>
      <button type="submit" [disabled]="!loginForm.valid()">Ingresar</button>
    </form>
  `
})
export class LoginComponent {
  loginForm = form({
    email: field('', [Validators.required, Validators.email]),
    password: field('', [Validators.required, Validators.minLength(8)])
  });

  onSubmit() {
    if (this.loginForm.valid()) {
      const { email, password } = this.loginForm.value();
      console.log('Login con:', email);
    }
  }
}

Formularios dinámicos con Signal Forms

import { Component, computed } from '@angular/core';
import { form, field } from '@angular/forms/signals';
import { Validators } from '@angular/forms';

@Component({
  selector: 'app-registro',
  template: `
    <form>
      <input type="text" [formField]="registroForm.controls.nombre" placeholder="Nombre" />
      <input type="email" [formField]="registroForm.controls.email" placeholder="Email" />

      <label>
        <input type="checkbox" [formField]="registroForm.controls.esEmpresa" />
        Registro como empresa
      </label>

      @if (registroForm.controls.esEmpresa.value()) {
        <input type="text" [formField]="registroForm.controls.razonSocial" placeholder="Razón Social" />
        <input type="text" [formField]="registroForm.controls.cuit" placeholder="CUIT" />
      }

      <p>Completado: {{ camposCompletados() }}/{{ totalCampos() }}</p>
      <button [disabled]="!registroForm.valid()">Registrarse</button>
    </form>
  `
})
export class RegistroComponent {
  registroForm = form({
    nombre: field('', Validators.required),
    email: field('', [Validators.required, Validators.email]),
    esEmpresa: field(false),
    razonSocial: field(''),
    cuit: field('')
  });

  camposCompletados = computed(() =>
    Object.values(this.registroForm.controls)
      .filter(c => c.value() !== '' && c.value() !== false).length
  );

  totalCampos = computed(() =>
    this.registroForm.controls.esEmpresa.value() ? 5 : 3
  );
}

Migración gradual: FormGroup y Signal Forms coexisten

No hay que reescribir todo de una vez. Angular 21 permite tener ambos sistemas en paralelo. Mi estrategia: nuevos formularios con Signal Forms, los existentes se migran cuando hay un motivo concreto (bug, refactor, mejora de rendimiento).

Lo que más me gusta de Signal Forms

No tener que pensar en el ciclo de vida de las suscripciones. Con FormGroup siempre había ese riesgo latente de un memory leak por un valueChanges sin unsubscribe. Con Signal Forms, el formulario es un valor reactivo sincrónico: lo leo con loginForm.value(), verifico con loginForm.valid(), y Angular maneja el resto. Menos código, menos bugs.


Artículo anterior: Adiós Zone.js: cómo Angular 21 cambió la detección de cambios para siempre | Serie Angular 20 → 21.2 | Próximo: Novedades del template en Angular 21: regex, spread, instanceof y más →