Signal-based forms are one of the coolest new additions to Angular’s ecosystem.
Let’s walk through what the sample login form above is doing and why it’s nice to work with.
1. Modeling the form with signals
First, we define a simple data model:
type LoginData = {
email: string;
password: string;
};
Then we create a signal that holds that model:
loginModel = signal<LoginData>({
email: '',
password: '',
});
This signal is our single source of truth for the form’s state. Instead of separate controls, we bind the whole object and let the form() helper take care of wiring it up.
2. Creating a signal form
loginForm = form(this.loginModel, (login) => {
required(login.email, { message: 'Email is required' });
email(login.email, { message: 'Enter a valid email address' });
required(login.password, { message: 'Password is required' });
pattern(
login.password,
/^((?=\S*?[A-Z])(?=\S*?[a-z])(?=\S*?[0-9]).{6,})\S$/,
{
message:
'Password must be at least 6 characters long and include at least one uppercase letter, one lowercase letter, and one number, with no spaces.',
}
);
});
What’s going on here?
form(this.loginModel, …) creates a signal form bound to the loginModel signal.
Inside the callback, login exposes field-level signals like login.email and login.password.
-
We attach validation rules using helpers like:
- required(field, { message })
- email(field, { message })
- pattern(field, regex, { message })
Each field now knows:
Its current value (as a signal)
Its validation status
Its list of errors (if any)
3. Binding to the template with and [field]
In the template, we don’t deal with [(ngModel)] or formControlName. Instead, we use the Field standalone component and bind the field signal:
<input type="email" [field]="loginForm.email" />
That’s enough for Angular to:
Set the input’s value
Update the field when the user types
Trigger validation
To show validation errors, we just read from the field’s errors() signal:
@for (err of loginForm.email().errors(); track err.kind) {
<p style="color: red; font-size: smaller">*{{ err.message }}</p>
}
Same for the password field.
And because everything is signal-based, we can easily display live values:
<p>Hello {{ loginForm.email().value() }}!</p>
<p>Password length: {{ loginForm.password().value().length }}</p>
No async pipes, no manual subscription management—just reading signal values.
4. Reacting to form changes with RxJS
Signals are great, but sometimes you still want an RxJS stream—for example, to debounce user input before saving or logging.
That’s what this part does:
constructor() {
toObservable(this.loginForm().value)
.pipe(
debounceTime(500),
distinctUntilChanged(),
takeUntilDestroyed(this.destroyRef)
)
.subscribe((value) => {
console.log(value);
});
}
Breakdown:
this.loginForm().value is a signal representing the full form value.
toObservable(...) converts that signal into an Observable.
-
We apply RxJS operators:
- debounceTime(500) – wait 500ms after the last change.
- distinctUntilChanged() – only react when the value actually changes.
- takeUntilDestroyed(this.destroyRef) – automatically clean up when the component is destroyed.
Finally, we subscribe and log the result.
This gives you the best of both worlds:
Signals for template binding and simple reactivity.
RxJS for more advanced reactive pipelines.
5. Why signal forms feel so good
A few nice properties of this approach:
Type-safe by default: The form is built from a typed model (LoginData).
No manual subscriptions for validation or display — signals handle it.
Template stays clean: [field] is easier to reason about than mixing ngModel, FormControl, and validators.
Easy interoperability with existing RxJS-based code via toObservable.
Wrap-up
This small login example shows how signal-based forms can simplify form handling in Angular:
Define a typed model.
Wrap it with form() and attach validation.
Bind fields with [field].
Use signals directly in the template.
Convert to observables when you need RxJS power.
Example
Happy coding!
I hope you found it helpful. Thanks for reading!
Let's get connected! You can find me on:
Top comments (0)