Signal forms

Avanzado
Angular
Angular
Actualizado: 04/05/2026

Los formularios basados en signals representan una evolución en la gestión del estado de formularios en Angular. Esta librería experimental aprovecha la reactividad nativa de las signals para ofrecer sincronización automática bidireccional, acceso tipado a campos y validación declarativa basada en esquemas.

Arquitectura de Signal Forms

Signal Forms centraliza los datos del formulario en una única WritableSignal que actúa como fuente de verdad. Cuando el modelo se actualiza programáticamente, el formulario refleja esos cambios automáticamente, y las interacciones del usuario actualizan el modelo de forma transparente.

%%{init: {'theme': 'default'}}%%
flowchart LR
    A[WritableSignal Model] <-->|Sincronización automática| B[FieldTree]
    B <-->|Directiva field| C[Inputs HTML]
    B --> D[FieldState]
    D --> E["value, valid, touched..."]

La diferencia fundamental con Reactive Forms es que Signal Forms almacena los datos en una signal definida por el desarrollador, mientras que Reactive Forms utiliza instancias de FormControl y FormGroup como contenedores de datos.

Configuración inicial

Para utilizar Signal Forms, importa las funciones y directivas desde @angular/forms/signals:

import { Component, signal } from '@angular/core';
import { form, Field, required, email } from '@angular/forms/signals';

El flujo básico consiste en tres pasos: definir un modelo con signal(), crear un FieldTree con form(), y vincular los inputs con la directiva [field].

Creación del modelo y formulario

Define primero la interfaz que representa la estructura de datos del formulario:

interface LoginData {
  email: string;
  password: string;
}

Crea el modelo como una signal escribible y genera el árbol de campos:

@Component({
  selector: 'app-login',
  imports: [Field],
  template: `
    <form>
      <label>
        Email:
        <input type="email" [field]="loginForm.email" />
      </label>
      <label>
        Contraseña:
        <input type="password" [field]="loginForm.password" />
      </label>
    </form>
  `
})
export class LoginComponent {
  // Modelo como WritableSignal
  loginModel = signal<LoginData>({
    email: '',
    password: ''
  });

  // FieldTree que refleja la estructura del modelo
  loginForm = form(this.loginModel);
}

El FieldTree generado permite acceder a cada campo usando notación de punto, como loginForm.email o loginForm.password. Esta estructura se infiere automáticamente del tipo del modelo.

Estado de los campos con FieldState

Cada campo del formulario expone su estado mediante una FieldState que contiene múltiples signals reactivas. Para acceder al estado, invoca el campo como función:

// Accede al FieldState del campo email
const emailState = this.loginForm.email();

// Las signals disponibles en FieldState
emailState.value()      // Valor actual del campo
emailState.valid()      // true si pasa todas las validaciones
emailState.invalid()    // true si tiene errores de validación
emailState.touched()    // true después de focus y blur
emailState.dirty()      // true cuando el usuario modifica el valor
emailState.disabled()   // true si el campo está deshabilitado
emailState.readonly()   // true si el campo es solo lectura
emailState.pending()    // true durante validación asíncrona
emailState.errors()     // Array de objetos de error

Los estados valid() e invalid() pueden ser ambos false simultáneamente cuando existe una validación asíncrona en progreso.

%%{init: {'theme': 'default'}}%%
stateDiagram-v2
    [*] --> Untouched: Campo inicial
    Untouched --> Touched: Usuario hace focus y blur
    Untouched --> Dirty: Usuario modifica valor
    Touched --> Dirty: Usuario modifica valor
    Dirty --> Valid: Pasa validaciones
    Dirty --> Invalid: Falla validaciones
    Invalid --> Valid: Corrige errores

Validación basada en esquemas

Signal Forms utiliza una función de esquema para vincular validadores a rutas de campos. Esta aproximación declarativa difiere de Reactive Forms, donde los validadores se pasan directamente a cada control.

Validadores integrados

Los validadores disponibles incluyen required, email, min, max, minLength, maxLength y pattern:

@Component({
  selector: 'app-registro',
  imports: [Field],
  template: `
    <form>
      <label>
        Email:
        <input type="email" [field]="registroForm.email" />
      </label>
      @if (registroForm.email().touched() && registroForm.email().invalid()) {
        <div class="errores">
          @for (error of registroForm.email().errors(); track error) {
            <p>{{ error.message }}</p>
          }
        </div>
      }

      <label>
        Contraseña:
        <input type="password" [field]="registroForm.password" />
      </label>
      @if (registroForm.password().touched() && registroForm.password().invalid()) {
        <div class="errores">
          @for (error of registroForm.password().errors(); track error) {
            <p>{{ error.message }}</p>
          }
        </div>
      }
    </form>
  `
})
export class RegistroComponent {
  registroModel = signal({
    email: '',
    password: ''
  });

  registroForm = form(this.registroModel, (schemaPath) => {
    // Múltiples validadores por campo
    required(schemaPath.email, { message: 'El email es obligatorio' });
    email(schemaPath.email, { message: 'Introduce un email válido' });
    required(schemaPath.password, { message: 'La contraseña es obligatoria' });
    minLength(schemaPath.password, 8, { message: 'Mínimo 8 caracteres' });
  });
}

Todas las reglas de validación se ejecutan de forma independiente, sin cortocircuito tras el primer error. Esto permite mostrar todos los errores relevantes al usuario simultáneamente.

Objetos anidados y envío del formulario

Signal Forms soporta estructuras complejas con objetos anidados. El FieldTree refleja automáticamente la jerarquía del modelo:

interface Direccion {
  calle: string;
  ciudad: string;
  codigoPostal: string;
}

interface PerfilUsuario {
  nombre: string;
  direccion: Direccion;
}

@Component({
  selector: 'app-perfil',
  imports: [Field],
  template: `
    <form (ngSubmit)="enviarFormulario()">
      <label>
        Nombre:
        <input [field]="perfilForm.nombre" />
      </label>

      <h3>Dirección</h3>
      <label>
        Calle:
        <input [field]="perfilForm.direccion.calle" />
      </label>
      <label>
        Ciudad:
        <input [field]="perfilForm.direccion.ciudad" />
      </label>
      <label>
        Código postal:
        <input [field]="perfilForm.direccion.codigoPostal" />
      </label>

      <button type="submit">Guardar</button>
    </form>
  `
})
export class PerfilComponent {
  perfilModel = signal<PerfilUsuario>({
    nombre: '',
    direccion: {
      calle: '',
      ciudad: '',
      codigoPostal: ''
    }
  });

  perfilForm = form(this.perfilModel, (schemaPath) => {
    required(schemaPath.nombre, { message: 'Nombre obligatorio' });
    required(schemaPath.direccion.calle, { message: 'Calle obligatoria' });
    required(schemaPath.direccion.ciudad, { message: 'Ciudad obligatoria' });
    required(schemaPath.direccion.codigoPostal, { message: 'Código postal obligatorio' });
  });

  enviarFormulario() {
    submit(this.perfilForm, async () => {
      // La callback solo se ejecuta si el formulario es válido
      console.log('Datos enviados:', this.perfilModel());
      
      // Resetear formulario tras envío exitoso
      this.perfilForm().reset();
      this.perfilModel.set({
        nombre: '',
        direccion: { calle: '', ciudad: '', codigoPostal: '' }
      });
    });
  }
}

La función submit() marca automáticamente todos los campos como touched y ejecuta la callback proporcionada únicamente si el formulario completo es válido. Esto simplifica la lógica de envío evitando comprobaciones manuales del estado de validación.

Para acceder al valor actual del modelo completo, simplemente invoca la signal: this.perfilModel(). La sincronización bidireccional garantiza que siempre contenga los datos actualizados del formulario.

Alan Sastre - Autor del tutorial

Alan Sastre

Ingeniero de Software y formador, CEO en CertiDevs

Ingeniero de software especializado en Full Stack y en Inteligencia Artificial. Como CEO de CertiDevs, Angular es una de sus áreas de expertise. Con más de 15 años programando, 6K seguidores en LinkedIn y experiencia como formador, Alan se dedica a crear contenido educativo de calidad para desarrolladores de todos los niveles.

Más tutoriales de Angular

Explora más contenido relacionado con Angular y continúa aprendiendo con nuestros tutoriales gratuitos.

Aprendizajes de esta lección

Comprender la nueva API experimental de signal forms y su función principal form(). Aprender el enfoque model-first basado en signals para gestionar formularios. Diferenciar signal forms de los reactive forms tradicionales en Angular. Conocer patrones y estrategias para migrar formularios existentes a signal forms. Explorar la integración de signal forms con computed signals, effects y otros primitivos reactivos.