No results found

    Angular 22 Signal Forms: Complete Guide

     Angular 22 Signal Forms: Complete Guide (Goodbye FormGroup/FormControl)

    Introduction:

    If you've built forms in Angular for more than a year, you know the drill. Template-driven forms are quick but loosely typed. Reactive Forms Angular 22 Signal Forms are powerful but come loaded with ceremony — FormControl, FormGroup, valueChanges subscriptions you have to remember to tear down, and a ControlValueAccessor every time you want a custom input to behave.

    Angular 22 Signal Forms: Complete Guide


    With Angular 22, released June 3, 2026, that ceremony finally has an alternative. Angular 22 Signal Forms — built on top of Angular's Signals reactivity model — are now stable and production-ready, along with the accompanying Submission API. No more experimental flags, no more "wait for it to stabilize." It's here.

    This guide walks through what Signal Forms actually are, how they compare to the old FormGroup/FormControl approach, a complete working example, and — since a form is only half a feature — how to validate the same data on a Node/Express and a .NET backend so the client and server never disagree.


    1. What Are Angular 22 Signal Forms?

    Signal Forms live in a separate entry point, @angular/forms/signals, distinct from the classic @angular/forms package you already know. Instead of constructing a parallel FormGroup tree that mirrors your data model, you hand a plain writable Signal to a form() function, and it hands you back a FieldTree — a deeply nested Signal structure where every property of your model has its own reactive field, complete with value(), valid(), dirty(), touched(), and errors().
    In short: your data signal is the single source of truth, and the form stays in sync with it in both directions, with no manual subscriptions and no boilerplate ControlValueAccessor classes for custom inputs.

    2. Why Angular Needed a Third Forms API

    Reactive Forms were designed in a pre-Signals era of Angular — before zoneless change detection, before Angular had one unified reactivity model. They still work, but every valueChanges subscription is a subscription you own and must clean up, and every patchValue() call is a push-based mutation the rest of your Signals-based code has to work around.

    To be clear: Signal Forms don't replace Reactive Forms or Template-Driven Forms. The Angular team has said explicitly that this is a third option, best suited to apps already built around Signals end to end — it sits alongside the existing two APIs rather than deprecating them.

    3. Core Building Blocks: form(), FieldTree, and Schemas
    Everything you need ships from one entry point:

    import {
    form,
    FormField,
    required,
    validate,
    email,
    min,
    max,
    minLength,
    maxLength,
    pattern,
    applyEach,
    submit,
    } from '@angular/forms/signals';

    The mental model has three parts:
    • A signal holds your raw data (signal({ email: '', age: 0 })).
    • form() wraps that signal in a schema callback and returns a FieldTree.
    • [field] (via the FormField directive) binds a template input to one node of that tree.
    4. Angular 22 Signal Forms Example: A Registration Form

    Here's a complete, realistic example — a user registration form with required fields, an email validator, and a submit handler.
    import { Component, signal, inject } from '@angular/core';
    import {
    form,
    FormField,
    required,
    email,
    minLength,
    submit,
    } from '@angular/forms/signals';

    import { RegistrationService } from './registration.service';
    @Component({
    selector: 'app-registration-form',
    standalone: true,
    imports: [FormField],
    template: `
    <form (submit)="onSubmit($event)">
    <label>
    Full name
    <input type="text" [field]="registrationForm.fullName" />
    </label>
    @if (registrationForm.fullName().touched() && registrationForm.fullName().invalid()) {
    <p class="error">Name is required.</p>
    }
    <label>
    Email
    <input type="email" [field]="registrationForm.email" />
    </label>
    @if (registrationForm.email().touched() && registrationForm.email().invalid()) {
    <p class="error">Enter a valid email address.</p>
    }
    <label>
    Password
    <input type="password" [field]="registrationForm.password" />
    </label>
    <button type="submit" [disabled]="registrationForm().invalid()">
    Create account
    </button>
    </form>

    ,
    })

    export class RegistrationFormComponent {
    private registrationService = inject(RegistrationService);
    formModel = signal({
    fullName: '',
    email: '',
    password: '',
    });
    registrationForm = form(this.formModel, (schema) => {
    required(schema.fullName);
    required(schema.email);
    email(schema.email);
    required(schema.password);
    minLength(schema.password, 8);
    });
    onSubmit(event: Event) {
    event.preventDefault();
    submit(this.registrationForm, async () => {
    await this.registrationService.register(this.formModel());
    });
    }
    }
    Notice what's absent: no FormBuilder, no FormGroup, no manual valueChanges.subscribe(), no unsubscribe logic in ngOnDestroy. Validity, touched state, and values are all just signals you read with ().

    5. Validation: Built-in Validators and Custom Rules

    Angular 22 Signal Forms ships the validators you'd expect out of the box: required, email, min, max, minLength, maxLength, and pattern. For anything more specific, use validate() inside the schema callback:

    import { validate } from '@angular/forms/signals';

    registrationForm = form(this.formModel, (schema) => {
    required(schema.password);
    minLength(schema.password, 8);
    validate(schema.password, (ctx) => {
    const value = ctx.value();
    const hasNumber = /\d/.test(value);
    return hasNumber
    ? undefined
    : { kind: 'password-needs-number', message: 'Include at least one number.' };
    });
    });

    A couple of details worth internalizing, because they trip people up coming from Reactive Forms:
    • Custom validator functions read the field's own value with ctx.value(), and can read other fields with ctx.valueOf(schema.otherField) — both are signals.
    • Return undefined when the field is valid — not null. This is a real behavioral difference from Reactive Forms' validator functions, and it's the single most common migration bug.
    • field().errors() returns an array of ValidationError objects, each with a kind string and an optional message.
    For array fields (repeated form groups — think "add another phone number"), applyEach() lets you apply a schema to every item in a signal array without manually mapping over FormArray controls.

    6. Handling Submission with the Submission API

    The manual if (form().valid()) pattern shown above works fine for simple cases. But Angular 22 also ships a dedicated submit() function that handles the lifecycle for you: it marks every field touched (so validation errors surface immediately on a failed attempt), refuses to run while the form is already invalid, and won't fire twice if a submission is already in flight.

    import { submit } from '@angular/forms/signals';

    onSubmit() {
    submit(this.registrationForm, async () => {
    // Runs only if the form is valid at the moment of submission
    await this.registrationService.register(this.formModel());
    });
    }

    Two things to watch for:
    • The callback passed to submit() must be async — a common early mistake is passing a plain synchronous function, which throws.
    • There's also a more declarative pattern: bind submission logic directly when you call form(), and a plain <button type="submit"> inside an ordinary <form> is enough to trigger the whole flow — Signal Forms sets novalidate, calls preventDefault(), and handles the rest.
    7. Migrating from FormGroup/FormControl

    If you're evaluating whether to migrate an existing Reactive Forms codebase, here's the shape of the change, side by side:

    The framework team has been explicit that this is not a forced migration. There's no deprecation notice on Reactive Forms. Treat Signal Forms as the default choice for new forms in Signals-first components, and migrate existing complex forms opportunistically rather than all at once.

    8. Full-Stack Validation: Node/Express Backend

    Client-side validation is a UX layer, not a security boundary — the same rules need to be enforced server-side. Here's a matching Express validator using zod, which keeps the shape identical to the Angular schema above:

    // registration.schema.js

    const { z } = require('zod');
    const registrationSchema = z.object({
    fullName: z.string().min(1, 'Name is required'),
    email: z.string().email('Enter a valid email address'),
    password: z
    .string()
    .min(8, 'Password must be at least 8 characters')
    .regex(/\d/, 'Include at least one number'),
    });
    module.exports = { registrationSchema };
    // registration.route.js
    const express = require('express');
    const { registrationSchema } = require('./registration.schema');
    const router = express.Router();
    router.post('/api/register', async (req, res) => {
    const parseResult = registrationSchema.safeParse(req.body);
    if (!parseResult.success) {
    return res.status(400).json({ errors: parseResult.error.flatten().fieldErrors });
    }
    const { fullName, email, password } = parseResult.data;
    // proceed to hash password, create user, etc.
    res.status(201).json({ success: true });
    });

    module.exports = router;

    Because the Zod schema field names mirror the Signal Forms model (fullName, email, password), the error object your Express route returns can be mapped straight back onto the same fields in the Angular form if you want to surface server-side errors (e.g., "email already taken") inline.

    9. Full-Stack Validation: .NET Backend

    If your API is ASP.NET Core instead, the equivalent is a request DTO with data annotations, validated automatically by model binding:

    // RegistrationRequest.cs

    using System.ComponentModel.DataAnnotations;
    public class RegistrationRequest
    {
    [Required(ErrorMessage = "Name is required")]
    public string FullName { get; set; } = string.Empty;
    [Required, EmailAddress(ErrorMessage = "Enter a valid email address")]
    public string Email { get; set; } = string.Empty;
    [Required, MinLength(8, ErrorMessage = "Password must be at least 8 characters")]
    [RegularExpression(@".*\d.*", ErrorMessage = "Include at least one number")]
    public string Password { get; set; } = string.Empty;
    }
    // RegistrationController.cs
    [ApiController]
    [Route("api/[controller]")]
    public class RegistrationController : ControllerBase
    {
    [HttpPost]
    public IActionResult Register([FromBody] RegistrationRequest request)
    {
    if (!ModelState.IsValid)
    {
    return BadRequest(ModelState);
    }
    // proceed to hash password, create user, etc.
    return StatusCode(201, new { success = true });
    }
    }


    ASP.NET Core validates the DTO against ModelState automatically before your action body even runs, so the [ApiController] attribute alone gives you the 400 response with field-level errors — the same 
    Angular 22 Signal Forms pairing you'd get from the Express/Zod setup.

    In both backends, the rule of thumb is the same:
    Signal Forms handles the fast, reactive, client-side feedback loop; your API is still the last line of defense, and duplicating the rule names (not the logic) between the two keeps error messages consistent end to end.
    10. Should You Migrate Existing Forms Today?
    A reasonable middle path, and the one most teams are taking right now:
    • Upgrade to Angular 22 to stay on a supported version and pick up the OnPush-by-default and Resource API changes regardless of what you do with forms.
    • Use Signal Forms for all new forms, especially in components already written around Signals.
    • Leave large, working Reactive Forms flows alone for now — migrate them opportunistically when you're touching that code anyway, not as a dedicated sprint.
    Reactive FormsSignal Forms
    new FormGroup({ email: new FormControl('') })signal({ email: '' }) wrapped in form()
    Validators.required in the control constructorrequired(schema.email) in the schema callback
    form.valueChanges.subscribe(...)Read field().value() directly, no subscription
    [formGroup] / formControlName directives[field]="form.email"
    Return null from a valid custom validatorReturn undefined from a valid custom validator
    ControlValueAccessor for custom inputsBind [field] directly, no accessor class needed

     FAQ

    Are Angular 22 Signal Forms stable? Yes. Signal Forms, along with the Submission API, left experimental status and shipped as stable in Angular 22 (released June 3, 2026).

    Do Signal Forms replace Reactive Forms? No. The Angular team has stated Signal Forms is a third forms option alongside Template-Driven and Reactive Forms, not a replacement — Reactive Forms is not deprecated.

    What package do Signal Forms come from? @angular/forms/signals, a separate entry point from the classic @angular/forms.

    What's the most common migration bug? Returning null from a custom validator instead of undefined — Signal Forms treats undefined as "no error," and forgetting this is the single most common source of validators that silently never pass.



    Post a Comment

    Previous Next

    نموذج الاتصال