Introduction
Essentials

Forms with signals

Signal Forms is built on Angular signals to provide a reactive, type-safe way to manage form state.

How it works

1. Create a signal model

When you create a form, you start by creating a signal that holds the state of your form:

const loginModel = signal({  email: '',  password: '',});

2. Pass the data model to form()

Then, you pass your form model into the form() function to create a form tree that mirrors and enhances your model's structure:

form(loginModel);// Access fields directly by property nameloginForm.emailloginForm.password

3. Bind inputs with [field] directive

Next, you bind your HTML inputs to the form using the [field] directive, which creates two-way binding between them:

<input type="email" [field]="loginForm.email" /><input type="password" [field]="loginForm.password" />

As a result, user changes (such as typing in the field) automatically updates the form, and any programmatic changes update the displayed value as well:

// Update the value programmaticallyloginForm.email().value.set('alice@wonderland.com');// The model signal is also updatedconsole.log(loginModel().email); // 'alice@wonderland.com'

NOTE: The [field] directive also syncs field state for attributes like required, disabled, and readonly when appropriate. You can read more in the upcoming in-depth guide.

4. Read form field values with value()

Finally, you can access field values as reactive signals by calling the field as a function and accessing its value():

<!-- Render form value that updates automatically as user types --><p>Email: {{ loginForm.email().value() }}</p>
// Get the current valueconst currentEmail = loginForm.email().value();

Here's a complete example:

app/app.component.ts

import {Component, signal} from '@angular/core';import {form, Field} from '@angular/forms/signals';/** * @title Signal Forms - Login Example */@Component({  selector: 'app-root',  templateUrl: 'app.component.html',  styleUrl: 'app.component.css',  imports: [Field],})export class App {  loginModel = signal({    email: '',    password: '',  });  loginForm = form(this.loginModel);}

app/app.component.html

<form>  <label>    Email:    <input type="email" [field]="loginForm.email" />  </label>  <label>    Password:    <input type="password" [field]="loginForm.password" />  </label>  <p>Hello {{ loginForm.email().value() }}!</p>  <p>Password length: {{ loginForm.password().value().length }}</p></form>

app/app.component.css

form {  display: flex;  flex-direction: column;  gap: 1rem;  max-width: 400px;  padding: 1rem;  font-family: Inter, system-ui, -apple-system, sans-serif;}label {  display: flex;  flex-direction: column;  gap: 0.25rem;}input {  padding: 0.5rem;  border: 1px solid #ccc;  border-radius: 4px;  font-size: 1rem;  font-family: inherit;}p {  margin: 0.5rem 0;  color: #666;}

Basic usage

The [field] directive works with all standard HTML input types. Here are the most common patterns:

Text inputs

Text inputs work with various type attributes and textareas:

<!-- Text and email --><input type="text" [field]="form.name" /><input type="email" [field]="form.email" />

Numbers

Number inputs automatically convert between strings and numbers:

<!-- Number - automatically converts to number type --><input type="number" [field]="form.age" />

Date and time

Date inputs store values as YYYY-MM-DD strings, and time inputs use HH:mm format:

<!-- Date and time - stores as ISO format strings --><input type="date" [field]="form.eventDate" /><input type="time" [field]="form.eventTime" />

If you need to convert date strings to Date objects, you can do so by passing the field value into Date():

const dateObject = new Date(form.eventDate().value());

Multiline text

Textareas work the same way as text inputs:

<!-- Textarea --><textarea [field]="form.message" rows="4"></textarea>

Checkboxes

Checkboxes bind to boolean values:

<!-- Single checkbox --><label>  <input type="checkbox" [field]="form.agreeToTerms" />  I agree to the terms</label>

Multiple checkboxes

For multiple options, create a separate boolean field for each:

<label>  <input type="checkbox" [field]="form.emailNotifications" />  Email notifications</label><label>  <input type="checkbox" [field]="form.smsNotifications" />  SMS notifications</label>

Radio buttons

Radio buttons work similarly to checkboxes. As long as the radio buttons use the same [field] value, Signal Forms will automatically bind the same name attribute to all of them:

<label>  <input type="radio" value="free" [field]="form.plan" />  Free</label><label>  <input type="radio" value="premium" [field]="form.plan" />  Premium</label>

When a user selects a radio button, the form field stores the value from that radio button's value attribute. For example, selecting "Premium" sets form.plan().value() to "premium".

Select dropdowns

Select elements work with both static and dynamic options:

<!-- Static options --><select [field]="form.country">  <option value="">Select a country</option>  <option value="us">United States</option>  <option value="ca">Canada</option></select><!-- Dynamic options with @for --><select [field]="form.productId">  <option value="">Select a product</option>  @for (product of products; track product.id) {    <option [value]="product.id">{{ product.name }}</option>  }</select>

NOTE: Multiple select (<select multiple>) is not supported by the [field] directive at this time.

Validation and state

Signal Forms provides built-in validators that you can apply to your form fields. To add validation, pass a schema function as the second argument to form(). This function receives a field path (sometimes abbreviated as p) that allows you to reference the model and its subsequent fields:

const loginForm = form(loginModel, (p) => {  required(p.email);  email(p.email);});

Common validators include:

You can also customize error messages by passing an options object as the second argument to the validator:

required(p.email, { message: 'Email is required' });email(p.email, { message: 'Please enter a valid email address' });

Each form field exposes its validation state through signals. For example, you can check field.valid() to see if validation passes, field.touched() to see if the user has interacted with it, and field.errors() to get the list of validation errors.

Here's a complete example:

app/app.component.ts

import {Component, signal} from '@angular/core';import {form, Field, required, email, submit} from '@angular/forms/signals';/** * @title Login Form with Validation */@Component({  selector: 'app-root',  templateUrl: 'app.component.html',  styleUrl: 'app.component.css',  imports: [Field],})export class App {  loginModel = signal({    email: '',    password: '',  });  loginForm = form(this.loginModel, (p) => {    required(p.email, {message: 'Email is required'});    email(p.email, {message: 'Enter a valid email address'});    required(p.password, {message: 'Password is required'});  });  onSubmit(event: Event) {    event.preventDefault();    submit(this.loginForm, async () => {      // Perform login logic here      const credentials = this.loginModel();      console.log('Logging in with:', credentials);      // e.g., await this.authService.login(credentials);    });  }}

app/app.component.html

<form (submit)="onSubmit($event)">  <div>    <label>      Email:      <input type="email" [field]="loginForm.email" />    </label>    @if (loginForm.email().touched() && loginForm.email().invalid()) {      <ul class="error-list">        @for (error of loginForm.email().errors(); track $index) {          <li>{{ error.message }}</li>        }      </ul>    }  </div>  <div>    <label>      Password:      <input type="password" [field]="loginForm.password" />    </label>    @if (loginForm.password().touched() && loginForm.password().invalid()) {      <div class="error">        @for (error of loginForm.password().errors(); track $index) {          <p>{{ error.message }}</p>        }      </div>    }  </div>  <button type="submit">Log In</button></form>

app/app.component.css

form {  display: flex;  flex-direction: column;  gap: 1rem;  max-width: 400px;  padding: 1rem;  font-family:    Inter,    system-ui,    -apple-system,    sans-serif;}div {  display: flex;  flex-direction: column;  gap: 0.25rem;}label {  display: flex;  flex-direction: column;  gap: 0.25rem;  font-weight: 500;}input {  padding: 0.5rem;  border: 1px solid #ccc;  border-radius: 4px;  font-size: 1rem;  font-family: inherit;}input:focus {  outline: none;  border-color: #4285f4;}button {  padding: 0.75rem 1.5rem;  background-color: #4285f4;  color: white;  border: none;  border-radius: 4px;  font-size: 1rem;  font-family: inherit;  cursor: pointer;  transition: background-color 0.2s;}button:hover {  background-color: #357ae8;}button:active {  background-color: #2a65c8;}.error-list {  color: red;  font-size: 0.875rem;  margin: 0.25rem 0 0 0;  padding-left: 0;  list-style-position: inside;}.error-list p {  margin: 0;}

Field State Signals

Each field provides these state signals:

State Description
valid() Returns true if the field passes all validation rules
touched() Returns true if the user has focused and blurred the field
dirty() Returns true if the user has changed the value
disabled() Returns true if the field is disabled
pending() Returns true if async validation is in progress
errors() Returns an array of validation errors with kind and message properties

TIP: Show errors only after touched() is true to avoid displaying validation messages before the user has interacted with a field.