← Back to list

Getting Started with Signal Forms in Angular v21 — Part 1

H ey Devs,

Vetriselvan Panneerselvam · 2025-09-10 20:27 · 218 claps · 5.1 min read paywalled
#angular-21 #signal-forms #signal #angular-tips #angular-forms
Open on Medium ↗
Wiki topics: 🌐 · Web Development

Angular signal form

Angular signal form

Getting Started with Signal Forms in Angular v21 — Part 1

H ey Devs,

The long wait is over! With the upcoming Angular v21, we can now experiment with a new way to handle forms: Signal Forms. This feature is now available for developers to explore ( not ready for production ), so let’s dive in.

👉 Not a Medium member? You can read the full article for free by clicking here 🔗

What are Signal Forms and How Do They Differ from Reactive Forms?

Signal Forms are a new approach to creating forms in Angular that are powered by signals. They are conceptually similar to reactive forms, but with a key difference: they use signals instead of observables to manage state.

One of the most immediate changes you’ll notice is in the template. Instead of the formControlName directive, we now use a new control directive.

What is the purpose of the control directive?

The control directive is the bridge that connects a form field in your model to a UI element (like an <input>) in your template. Its main responsibilities are

  • Two-way data binding: It binds the field’s value to the UI control’s value and vice versa.
  • State synchronization: It binds additional form-related states, such as disabled, required, etc., from the field to the UI control.
  • Event relaying: It relays relevant UI events back to the field. For example, it marks a field as touched on a blur event.

How to Implement a Signal Form

Let’s walk through an implementation using a simple registration form.

First, we need to create a signal that represents our form’s data model.

import { signal } from '@angular/core';
// ...
readonly register = signal({
  firstName: "",
  lastName: "",
  gender: "M",
  dob: "",
  terms: false,
});

Next, we create the form instance using the form function provided by @angular/forms/signals.

import { form } from '@angular/forms/signals';
// ...
readonly registerForm = form(this.register);

What does the form function do?

The form function uses the signal you provide as the single source of truth; it does not maintain its own internal copy of the data. This means that when you update a value on a form field, the original signal model is updated directly.

Note: To keep the code clean, I created a separate readonly signal for the form model. However, you can also define the signal directly inside the form function if you prefer:

readonly registerForm = form(signal({
  firstName: "",
  lastName: "",
  gender: "M",
  dob: "",
  terms: false,
}));

Now, we can use the control directive in our template to bind the form fields to their UI controls.

<label>First Name</label>
<input [control]="registerForm.controls.firstName" type="text" />

As you can see, the process is similar to reactive forms, but we use the [control] directive to establish the binding.

How to Get the Form’s Value

You can get the current value of the form by simply calling the form signal.

console.log(this.registerForm.value());

Implementing Validation

To add field logic, such as validation, you provide a callback function as the second argument to the form method. This function receives a path object that you use to specify which field the validators apply to.

import { form, required, minLength } from '@angular/forms/signals';
// ...
readonly registerForm = form(this.register, (path) => [
  required(path.controls.firstName),
  minLength(path.controls.firstName, 3)
]);

A neat feature is that when you add validators, the corresponding HTML attributes (e.g., required, minlength, maxlength) are automatically added to the bound HTML element in the browser.

Html attributes — angular signal forms

Html attributes — angular signal forms

Displaying Error Messages

Validation errors are available at both the form level and on each individual control. To access the errors for a specific control, you can call its errors() method.

@if (registerForm.controls.firstName.errors(); as errors) {
  @for (error of errors; track error) {
    <small class="error-message">{{ error.message }}</small>
  }
}

The errors() method returns an array of ValidationError objects. Each error object in this array contains the following properties:

{
  /** Identifies the kind of error (e.g., 'required', 'minLength'). */
  readonly kind: string;
  /** The field associated with this error. */
  readonly field: Field<unknown>;
  /** A human-readable error message. */
  readonly message?: string;
}

You might be wondering if it’s possible to configure a custom error message for each validator. The answer is yes! You can define a custom message directly within the validator configuration.

readonly registerForm = form(this.register, (path) => [
  required(path.controls.firstName, { message: "First Name is required." }),
  minLength(path.controls.firstName, 3, { message: "First Name must be at least 3 characters." })
]);

Final Code :

import { CommonModule } from "@angular/common";
import { Component, effect, signal } from "@angular/core";
import { Control, form, minLength, required } from "@angular/forms/signals";

@Component({
  selector: "app-signal-form",
  standalone: true,
  imports: [Control , CommonModule],
  template: `
    <div class="form-header">
      <h1> Registration Form using signals form </h1>
    </div>
    <form autocomplete="off" (submit)="submit()" >
      <div class="form-container">
        <div class="form-field">
          <label>First Name</label>
          <input  [control]="registerForm.firstName" type="text" />
          @if(registerForm.firstName().errors()) {
            @for(error of registerForm.firstName().errors(); track error) {
              <!-- @if(error.kind === 'required'){
                <small class="error-message"> First Name is required </small>
              } -->

              <small class="error-message">{{ error.message }}</small>
            }
          }
        </div>
        <div class="form-field">
          <label>Last Name</label>
          <input [control]="registerForm.lastName" type="text" />
        </div>
        <div class="form-field">
          <label>Gender</label>
          <select [control]="registerForm.gender">
            <option value="M">Male</option>
            <option value="F">Female</option>
            <option value="O">Other</option>
          </select>
        </div>
        <div class="form-field">
          <label>Date of Birth</label>
          <input [control]="registerForm.dob" type="date" />
        </div>
        <div class="form-field">
          <label>
            <input [control]="registerForm.terms" type="checkbox" />
            Accept Terms
          </label>
        </div>
      </div>
      <div class="button-container">
        <button type="button" (click)="clear()">Clear</button>
        <button [disabled]="!registerForm().valid()" type="submit">Register</button>
      </div>
    </form>
    {{
      registerForm().value() | json
    }}
  `,
  styles: `
  .form-header {
    display: flex;
    justify-content: center;
    align-items: center;
    margin-bottom: 16px;
  }
  .form-container {
    display: flex;
    justify-content: center;
    align-items: center;
    flex-wrap: wrap;
    gap: 16px;
  }
  .form-field {
    display: flex;
    flex-direction: column;
    gap: 5px;
    width: 450px;
  }
  form {
    display: flex;
    flex-direction: column;
    flex-wrap: wrap;
    gap: 10px;
  }
  label {
    font-size: 14px;
    font-weight: 600;
  }
  input, select {
    padding: 10px;
    border: 1px solid #ccc;
    border-radius: 5px;
  }
  input.invalid {
    border: 1.5px solid #e74c3c;
    background: #fff6f6;
  }
  .error-message {
    color: #e74c3c;
    font-size: 13px;
    margin-top: 2px;
  }
  button {
    padding: 10px;
    border: 1px solid #ccc;
    border-radius: 5px;
    background-color: #007bff;
    color: white;
    width: 100px;
  }
  button:disabled {
    background-color: #ccc;
    color: #000;
  }
  .button-container {
    display: flex;
    justify-content: center;
    align-items: center;
    margin-top: 16px;
    gap: 16px;
  }
  `,
})
export class SignalForm {
  readonly register = signal({
    firstName: "",
    lastName: "",
    gender: "M",
    dob: "",
    terms: false,
  });

  readonly registerForm = form(this.register, (path) => {
    required(path.firstName, { message: "First Name is required"  }),
    minLength(path.firstName, 3 ,  { message : "First Name must be at least 3 characters long" })
  });

  submit() {
    console.log(this.registerForm().value());
  }

  clear() {}

  constructor(){
    effect(() => {
      console.log(this.registerForm.firstName().value());
    });

    effect(() => {
      console.log(this.registerForm.firstName().valid());
    });
  }
}

Angular signal form

Angular signal form

What’s Next?

In this post, we’ve covered the basic implementation of Signal Forms and how to add validation. In a future post, we will explore more advanced topics like implementing schemas, handling form submission, and updating form values programmatically. Stay tuned!

Thanks for reading! If this was helpful, consider clapping 👏 and following for more Angular tips. Got questions or suggestions? Drop them in the comments below!

✍️ Author: **Vetriselvan Panneerselvam**

👨‍💻 Frontend Developer | 💡 Code Enthusiast | 📚 Lifelong Learner | ✍️ Tech Blogger | 🌍 Freelance Developer


메타데이터
post_id
13212078820d
slug
getting-started-with-signal-forms-in-angular-v21-part-1-13212078820d
url
https://medium.com/@vetriselvan_11/getting-started-with-signal-forms-in-angular-v21-part-1-13212078820d
canonical_url
https://medium.com/@vetriselvan_11/getting-started-with-signal-forms-in-angular-v21-part-1-13212078820d
author_url
https://medium.com/@vetriselvan_11
status
ok
fetched_at
2026-06-10 13:10:15