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
FormControlyFormGroupcomo 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()einvalid()pueden ser ambosfalsesimultá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
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.