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:
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:
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:
Common validators include:
required()- Ensures the field has a valueemail()- Validates email formatmin()/max()- Validates number rangesminLength()/maxLength()- Validates string or collection lengthpattern()- Validates against a regex pattern
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.