Angular 22: Reemplace ControlValueAccessor con Signal Forms
Descubre cómo Signal Forms en Angular 22 optimiza el desarrollo de formularios, permitiendo crear controles personalizados con menos código.

El problema que resuelven Signal Forms
Hay un momento específico que todo desarrollador de Angular conoce. Necesitas un componente de entrada personalizado: un selector de fechas, un widget de calificación, un campo de texto enriquecido. Y sabes lo que viene: array de providers, forwardRef, cuatro métodos de interfaz, callbacks manuales de onChange y onTouched. Cada vez. Para cada control.
Signal Forms no simplifica esto. Lo reemplaza por completo.
El segundo punto de dolor es la gestión del estado. En Reactive Forms, el estado del formulario reside en FormControl y FormGroup — basado en Observables, imperativo para actualizar, frágil de tipar correctamente con estructuras anidadas. Observar un cambio en un campo significa suscribirse a valueChanges, gestionar la suscripción y cancelar la suscripción al destruir. Signal Forms convierte cada pieza de estado en una signal: legible, componible y limpiada automáticamente.
⚠️ Importante:
ControlValueAccessorestá no obsoleto en Angular 22. Sigue funcionando y los controles existentes aún se compilan. Pero para cada nuevo control, Signal Forms es el camino a seguir. Angular 22 garantiza la compatibilidad total entre ambos mundos.
No tienes que migrar todo. Solo tienes que dejar de escribir de la forma antigua para todo lo nuevo.
La nueva API: tres primitivas
Aprende estas tres y habrás aprendido Signal Forms.
form()
El punto de entrada. Pasa una signal (o linkedSignal) que contenga tu modelo y una función de validación. Devuelve un FieldTree tipado: un espejo de la forma de tu modelo donde cada hoja es un campo reactivo con valor, validez, estado dirty, touched y pending.
FormValueControl<T>
La interfaz que implementa tu componente personalizado en lugar de ControlValueAccessor. Requisito mínimo: exponer una signal value = model<T>(). La directiva conecta todo lo demás: sincroniza la signal de valor del campo, propaga touched/dirty y maneja el estado disabled.
SignalFormControl
Un puente para la migración incremental. Inserta un control basado en signals en un FormGroup existente sin reescribir el formulario padre. Los errores de validación y el estado se propagan en ambos sentidos: de Signal Form al árbol de Reactive Forms y viceversa.
[formField] directiva
La directiva de enlace de plantilla. Aplícala a cualquier elemento junto con ngModel y conectará el estado del campo del formulario. Establece clases CSS (.ng-valid, .ng-dirty, .ng-touched), gestiona atributos ARIA y mantiene el valor sincronizado automáticamente.
Construyendo un formulario con validación
La función form() toma tu signal de modelo y una función de esquema. Los validadores se ejecutan sobre el valor del campo: ya no hay que pasar arrays de validadores esperando que los tipos coincidan.
import { Component, inject, signal } from '@angular/core';import { form, required, minLength, pattern } from '@angular/forms/signals';import { UserService } from './user.service';interface RegisterModel { username: string; email: string; password: string;}@Component({ selector: 'app-register', standalone: true, imports: [SignalFormsModule], template: ` <form (ngSubmit)="submit()"> <label>Username <input [formField]="f.username" /> @if (f.username().errors()?.required) { <span class="error">Required</span> } @if (f.username().errors()?.minLength) { <span class="error">Min 3 characters</span> } @if (f.username().pending()) { <span class="info">Checking availability…</span> } </label> <label>Email <input type="email" [formField]="f.email" /> @if (f.email().errors()?.pattern) { <span class="error">Invalid email format</span> } </label> <label>Password <input type="password" [formField]="f.password" /> </label> <button type="submit" [disabled]="!f.valid()">Register</button> </form> `,})export class RegisterComponent { private userSvc = inject(UserService); // Model as a writable signal private model = signal<RegisterModel>({ username: '', email: '', password: '', }); // form() generates the FieldTree — typed to RegisterModel f = form(this.model, (field) => { required(field.username); minLength(field.username, 3); // Async validator — checks username availability field.username.addAsyncValidator(async (value) => { if (!value) return null; const taken = await this.userSvc.isUsernameTaken(value); return taken ? { usernameTaken: true } : null; }, { debounce: 400 }); // built-in debounce — no more timer juggling required(field.email); pattern(field.email, /^[^\s@]+@[^\s@]+\.[^\s@]+$/); required(field.password); minLength(field.password, 8); }); submit() { if (!this.f.valid()) return; console.log(this.model()); // typed RegisterModel — no .getRawValue() }}✅ Perspectiva clave: La signal de modelo es la fuente de verdad.
form()no posee los datos: tu signal sí. Leerthis.model()te da el valor tipado actual, sin.getRawValue(), sin.value, sin extracción manual. Signal Forms son una lente sobre tus datos, no un contenedor para ellos.
Controles personalizados: antes y después
Esta es la mayor mejora en la calidad de vida de toda la versión.
Implementación con múltiples métodos: writeValue, registerOnChange, registerOnTouched, setDisabledState
Uso de variables privadas para gestionar callbacks onChange y onTouched
Código extenso (~40 líneas) y boilerplate para manejar estado y eventos
Necesidad de definir providers y usar forwardRef para integración
Complejidad mayor para controles personalizados
Reducción significativa de código y complejidad
Uso directo de signals para manejar el estado del control
Menos métodos y lógica explícita para callbacks
Mejora en la calidad de vida del desarrollador
Facilita la creación y mantenimiento de controles personalizados
Antes — ControlValueAccessor (~40 líneas)
@Component({ selector: 'app-rating', providers: [{ provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => RatingComponent), multi: true, }],})export class RatingComponent implements ControlValueAccessor { protected value = 0; protected disabled = false; private onChange: (v: number) => void = () => {}; private onTouched: () => void = () => {}; writeValue(v: number) { this.value = v ?? 0; } registerOnChange(fn: (v: number) => void) { this.onChange = fn; } registerOnTouched(fn: () => void) { this.onTouched = fn; } setDisabledState(d: boolean) { this.disabled = d; } select(star: number) { this.value = star; this.onChange(star); this.onTouched(); }}Después — FormValueControl (3 líneas)
import { model, Component } from '@angular/core';import { FormValueControl } from '@angular/forms/signals';@Component({ selector: 'app-rating', standalone: true,})export class RatingComponent implements FormValueControl<number> { // This single signal is the entire API value = model(0); // Optional inputs — Signal Forms wires them automatically // disabled?: InputSignal<boolean>; // touched?: InputSignal<boolean>; // name?: InputSignal<string>; select(star: number) { this.value.set(star); // No manual onChange/onTouched calls needed // The directive handles all propagation }}// Usage in template — identical to before:// <app-rating [formField]="f.rating" />El contrato del componente pasó de 4 métodos obligatorios + un array de providers + una llamada forwardRef a una signal. Eso es todo. Todo lo demás (sincronización, seguimiento de dirty, propagación de touched, estado disabled) es manejado automáticamente por la directiva [formField].
Editando datos del servidor: linkedSignal + httpResource
El patrón más potente en Signal Forms: cargar datos mediante httpResource, vincularlos al formulario mediante linkedSignal, y el formulario se actualiza automáticamente cada vez que el recurso se recarga.
import { Component, inject, input } from '@angular/core';import { httpResource } from '@angular/common/http';import { linkedSignal } from '@angular/core';import { form, required, minLength } from '@angular/forms/signals';interface UserProfile { username: string; bio: string; website: string;}@Component({ selector: 'app-edit-profile', standalone: true, template: ` @if (userResource.isLoading()) { <p>Loading profile...</p> } @else { <form (ngSubmit)="save()"> <input [formField]="f.username" placeholder="Username" /> <textarea [formField]="f.bio" placeholder="Bio"></textarea> <input [formField]="f.website" placeholder="Website" /> <button [disabled]="!f.valid() || !f.dirty()">Save changes</button> <button type="button" (click)="reset()">Discard</button> </form> } `,})export class EditProfileComponent { userId = input.required<number>(); // 1. Fetch from server — refetches when userId changes userResource = httpResource<UserProfile>( () => `/api/users/${this.userId()}` ); // 2. Bridge resource → writable signal via linkedSignal // Updates automatically when userResource reloads editableProfile = linkedSignal( () => this.userResource.value() ?? { username: '', bio: '', website: '' } ); // 3. Form observes editableProfile — resets when profile reloads f = form(this.editableProfile, (field) => { required(field.username); minLength(field.username, 3); required(field.bio); }); save() { if (!this.f.valid()) return; // this.editableProfile() holds the current form values console.log('Saving:', this.editableProfile()); } reset() { // Force linkedSignal to re-read from the resource this.editableProfile.set(this.userResource.value()!); }}Este patrón elimina toda una categoría de código de los formularios de edición: no más ngOnInitPatrón de edición de datos del servidor con Signal FormspatchValue(), no más suscripción a parámetros de ruta para activar una recarga, no más lógica de reinicio manual. El grafo reactivo se encarga de todo.
Validación entre campos
La validación entre campos solía requerir un validador personalizado a nivel de FormGroup con una API incómoda. En Signal Forms, un validador en un campo puede leer directamente la signal de cualquier campo hermano.
interface PasswordForm { password: string; confirm: string;}f = form(signal<PasswordForm>({ password: '', confirm: '' }), (field) => { required(field.password); minLength(field.password, 8); required(field.confirm); // Cross-field rule: reads the sibling field's signal directly field.confirm.addValidator((confirmValue) => { const pwd = field.password.value(); // sibling signal — no workarounds return confirmValue !== pwd ? { passwordMismatch: true } : null; });});// In the template:// @if (f.confirm().errors()?.passwordMismatch) {// <span>Passwords don't match</span>// }Reactive Forms vs Signal Forms
Criterio | Reactive Forms | Signal Forms (Angular 22) |
|---|---|---|
Modelo de estado | Basado en Observable: suscribirse, cancelar suscripción, gestionar ciclo de vida | Basado en Signal: leer, derivar, limpieza automática ✅ |
Controles personalizados |
|
|
Formularios anidados |
| La forma del modelo se refleja naturalmente — |
Validación asíncrona | Devolver |
|
Reglas entre campos | Validador personalizado a nivel de | Leer la signal de cualquier hermano directamente dentro del esquema ✅ |
Enlace de datos del servidor | Manual |
|
Leer el valor |
|
|
Compatibilidad | Independiente | Compatible con CVA y Reactive Forms mediante |
Cuándo mantener | Formularios existentes que funcionan: no migres lo que no está roto | Todos los formularios nuevos a partir de hoy |
Migrando sin un gran cambio
La migración correcta es incremental. Angular 22 garantiza que ControlValueAccessor y FormValueControl son mutuamente compatibles: puedes tener ambos en el mismo formulario sin necesidad de soluciones alternativas.
Evolución de Signal Forms
Versión | Estado |
|---|---|
v21 | Experimental: API inestable entre versiones menores. No recomendado para producción. |
v22 | Estable: API pública. |
El manual de migración de cuatro pasos
Paso 1: migrar primero los controles personalizados. Reemplaza ControlValueAccessor con FormValueControl un control a la vez. Angular 22 hace que los componentes FormValueControl sean totalmente compatibles con los padres FormGroup existentes: no se necesitan cambios en el padre.
Replace ControlValueAccessor with FormValueControl one control at a time without changing parent FormGroups.
Develop new forms using Signal Forms to maximize migration ROI; leave working forms unchanged.
Insert signal-based controls into existing FormGroups to enable two-way validation error propagation.
Apply ngNoCva directive when mixing legacy and Signal Forms APIs to avoid conflicts.
Una guía concisa para migrar incrementalmente formularios de Angular usando Signal Forms.
Paso 2: escribir todos los formularios nuevos con Signal Forms. No toques los formularios que funcionan. El ROI de la migración está en el nuevo desarrollo (donde Signal Forms elimina la configuración por completo), no en reescribir formularios que ya funcionan.
Paso 3: usar SignalFormControl como puente. Inserta un control basado en signals dentro de un FormGroup existente. Los errores de validación se propagan en ambos sentidos: sin doble contabilidad.
import { SignalFormControl } from '@angular/forms/signals';import { FormGroup } from '@angular/forms';// Drop a signal-based control into an existing FormGroup — no rewrite neededconst legacyForm = new FormGroup({ firstName: new FormControl(''), lastName: new FormControl(''), // Signal-based control coexists naturally nickname: new SignalFormControl('', (field) => { required(field); minLength(field, 2); }),});// legacyForm.getRawValue()// → { firstName: '', lastName: '', nickname: '' }// nickname behaves exactly like a regular FormControl from the parent's perspectivePaso 4: añadir ngNoCva al mezclar ambas API en el mismo elemento. Cuando [formField] se usa en una entrada que también está dentro de un [formGroup], ambas API intentan controlar el mismo elemento. ngNoCva le dice a Angular que omita el CVA y deje que Signal Forms tome el control.
⚠️ La directiva
ngNoCva: solo necesitas esto al mezclar[formField]con[formGroup],formControlName, ongModelen el mismo elemento. Si estás usando[formField]de forma independiente o en un componenteFormValueControl, no hay conflicto.
Cuándo usar Signal Forms (y cuándo no)
✅ Opta por Signal Forms cuando
Escribas cualquier formulario nuevo desde cero: esta es la opción predeterminada ahora
Construyas controles de formulario personalizados: reemplaza CVA por completo
Los datos del formulario provienen de una API:
linkedSignal+httpResourcese componen naturalmenteSe requiera validación asíncrona: debouncing integrado, sin configuración de Observable
Exista validación entre campos: lee las signals de los hermanos directamente en el esquema
Quieres un
.value()tipado sin.getRawValue()o coerción de nulosEstás en una aplicación zoneless: las Signals son la primitiva correcta de principio a fin
Escribir cualquier formulario nuevo desde cero (opción predeterminada)
Construir controles de formulario personalizados (reemplaza totalmente CVA)
Datos de formulario obtenidos de una API (natural con linkedSignal + httpResource)
Validación asíncrona necesaria (debouncing integrado, sin configuración de Observable)
Validación entre campos soportada mediante signals de hermanos en el esquema
Formularios existentes que funcionan sin errores
Lógica compleja de FormGroup con gestión de estado personalizada
Librerías CVA de terceros aún no actualizadas
El equipo necesita tiempo para ganar intuición antes de migrar
Paneles de administración simples o pantallas de configuración sin problemas
→ Mantén Reactive Forms cuando
El formulario ya funciona y no hay errores que corregir
Lógica de
FormGroupmuy personalizada con gestión de estado complejaLibrerías CVA de terceros que aún no se han actualizado
Tu equipo necesita tiempo para desarrollar intuición: no apresures una migración masiva
Paneles de administración y pantallas de configuración con lógica simple y sin problemas
En conclusión
Signal Forms no es una nueva forma de pensar sobre los formularios. Son los mismos conceptos (validación, estado, enlace, envío) construidos sobre la primitiva correcta. Las Signals siempre iban a ser la base de la reactividad de Angular; los formularios eran solo la última gran superficie en ponerse al día.
El impacto práctico es inmediato en dos lugares:
Controles personalizados: si tienes un sistema de diseño con 10 entradas personalizadas, cada una puede perder 30-40 líneas de boilerplate la próxima vez que la toques.
Integración formulario-API: el patrón httpResource + linkedSignal elimina toda una categoría de llamadas manuales a patchValue() y gestión de suscripciones que plagaban los formularios de edición en cada base de código de Angular.
La historia de la migración es genuinamente buena. Angular 22 no te obliga: ControlValueAccessor sigue funcionando, FormGroup sigue funcionando, y las dos API pueden coexistir en el mismo formulario. El momento adecuado para cambiar es el próximo formulario nuevo que escribas, no un sprint de migración programado.
La guía de decisión de una frase: ¿Formulario nuevo? Usa Signal Forms:
form(),model(),FormValueControl. ¿Formulario existente que funciona? Déjalo.¿Formulario existente que vas a tocar de todos modos? Migra primero los controles personalizados, luego considera el modelo de formulario si vale la pena el diff.
Los mejores formularios de Angular no se sienten como formularios de Angular. Se sienten como datos.
Fuentes
angular.dev/guide/forms/signals/overview — Guía oficial de Signal Forms de Angular:
form(),[formField], validadores, validación asíncrona, formularios anidados.angular.dev/essentials/signal-forms — Descripción general de elementos esenciales, interfaz
FormValueControl, controles personalizados sin ControlValueAccessor.angular.dev/guide/forms/signals/migration — Guía oficial de migración desde Reactive Forms, puente
compatForm, estrategia incremental.Santosh Yadav — santoshyadav.dev (16 de junio de 2026) — "Angular 22: The Signals Are Strong" —
ngNoCva,SignalFormControl,OnPushcomo nuevo predeterminado,linkedSignalopción de conjunto personalizado.Angular.love (26 de mayo de 2026) — "Signal Forms in Angular 21: Complete Guide" —
compatForm, modelo de validación, comparación CVA→FormValueControl.ANGULARarchitects.io (junio de 2026) — "All About Angular's New Signal Forms" — validación personalizada, esquemas, formularios anidados, form arrays, debouncing.
Brian Treese — briantree.se (26 de marzo de 2026) — "Angular 22: Mix Signal Forms and Reactive Forms Seamlessly" —
FormValueControlmigración, estrategia incremental.LogRocket Blog (27 de marzo de 2026) — "Signal Forms: Angular's best quality of life update in years."
Rajat / Substack (junio de 2026) — "Signal Forms Are Finally Production-Ready in Angular 22" —
form()API,httpResource+linkedSignalpatrón, validación asíncrona.Angular_with_Awais / Medium (mayo de 2026) — "Angular v22 Signal Forms: Build Custom Controls Without ControlValueAccessor."
Los ejemplos de código reflejan la API estable de Signal Forms de Angular 22 lanzada el 3 de junio de 2026. ControlValueAccessor no está obsoleto y sigue funcionando en Angular 22. Consulta siempre angular.dev/guide/forms/signals para obtener la API estable actual.


