๐ Angular Signal Forms: improved form submission
Did you know that Signal Forms submission got a lot more convenient in Angular 21.2?
๐ Angular Signal Forms: improved form submission

Did you know that Signal Forms submission got a lot more convenient in Angular 21.2?
Letโs look at a simple LoginForm component using Angular Signal Forms (still experimental package):
import { Component, signal } from '@angular/core';
import { email, form, FormField, required, submit } from '@angular/forms/signals';
interface LoginData {
email: string;
password: string;
}
@Component({
selector: 'app-login-form',
imports: [FormField],
template: `
<form (submit)="login($event)" novalidate>
<div class="form-control">
<label for="email">Email</label>
<input type="email" id="email" [formField]="loginForm.email" />
</div>
<div class="form-control">
<label for="password">Password</label>
<input type="password" id="password" [formField]="loginForm.password" />
</div>
<div class="action-btns">
<button type="submit">Login</button>
</div>
</form>
`,
})
export class LoginForm {
private readonly loginModel = signal<LoginData>({ email: '', password: '' });
protected readonly loginForm = form(this.loginModel, (schemaPath) => {
required(schemaPath.email);
email(schemaPath.email);
required(schemaPath.password);
});
protected login(event: SubmitEvent) {
event.preventDefault();
if (this.loginForm().invalid()) {
const firstError = this.loginForm().errorSummary()[0];
firstError.fieldTree().focusBoundControl();
return;
}
submit(this.loginForm, async (form) => {
console.log('Logging in with data:', form().value());
});
}
}
It works, but thereโs some boilerplate you need to remember:
- add the
novalidateattribute to prevent built-in browser validation - call
event.preventDefault()in the submit handler to prevent page reload - check if the form is invalid and handle that case
Angular 21.2 improves all of this. First, you can define actions for both the valid and invalid scenarios in a single place โ as the second argument of submit:
import { Component, signal } from '@angular/core';
import { email, form, FormField, required, submit } from '@angular/forms/signals';
interface LoginData {
email: string;
password: string;
}
@Component({
selector: 'app-login-form',
imports: [FormField],
template: `...`,
})
export class LoginForm {
private readonly loginModel = signal<LoginData>({ email: '', password: '' });
protected readonly loginForm = form(this.loginModel, (schemaPath) => {
required(schemaPath.email);
email(schemaPath.email);
required(schemaPath.password);
});
protected login(event: SubmitEvent) {
event.preventDefault();
submit(this.loginForm, {
// when form is valid
action: async (form) => {
console.log('Logging in with data:', form().value());
},
// when form is invalid
onInvalid: (form) => {
const firstError = form().errorSummary()[0];
firstError.fieldTree().focusBoundControl();
},
});
}
}
Even better, you can define the submission logic directly when defining the form:
import { Component, signal } from '@angular/core';
import { email, form, FormField, required, submit } from '@angular/forms/signals';
interface LoginData {
email: string;
password: string;
}
@Component({
selector: 'app-login-form',
imports: [FormField],
template: `...`,
})
export class LoginForm {
private readonly loginModel = signal<LoginData>({ email: '', password: '' });
protected readonly loginForm = form(
this.loginModel,
(schemaPath) => {
required(schemaPath.email);
email(schemaPath.email);
required(schemaPath.password);
},
{
// submission logic
submission: {
action: async (form) => {
console.log('Logging in with data:', form().value());
},
onInvalid: (form) => {
const firstError = form().errorSummary()[0];
firstError.fieldTree().focusBoundControl();
},
},
},
);
protected login(event: SubmitEvent) {
event.preventDefault();
submit(this.loginForm);
}
}
Finally, the new FormRoot directive handles calling event.preventDefault() and adding novalidate for you. It listens to the submit event and invokes the actions defined in the submission property of the form definition:
import { Component, signal } from '@angular/core';
import { email, form, FormField, FormRoot, required, submit } from '@angular/forms/signals';
interface LoginData {
email: string;
password: string;
}
@Component({
selector: 'app-login-form',
imports: [FormField, FormRoot],
template: `
<!-- FormRoot directive -->
<form [formRoot]="loginForm">
<div class="form-control">
<label for="email">Email</label>
<input type="email" id="email" [formField]="loginForm.email" />
</div>
<div class="form-control">
<label for="password">Password</label>
<input type="password" id="password" [formField]="loginForm.password" />
</div>
<div class="action-btns">
<button type="submit">Login</button>
</div>
</form>
`,
})
export class LoginForm {
private readonly loginModel = signal<LoginData>({ email: '', password: '' });
protected readonly loginForm = form(
this.loginModel,
(schemaPath) => {
required(schemaPath.email);
email(schemaPath.email);
required(schemaPath.password);
},
{
submission: {
action: async (form) => {
console.log('Logging in with data:', form().value());
},
onInvalid: (form) => {
const firstError = form().errorSummary()[0];
firstError.fieldTree().focusBoundControl();
},
},
},
);
}
Angular 21.2 brings some really nice improvements to Signal Forms submission โ a solid DX boost in a minor update.
I hope you liked the โgolden nuggetโ ๐ , thanks for reading! ๐
๋ฉํ๋ฐ์ดํฐ
- post_id
- a44c7e6f2b8d
- slug
- angular-signal-forms-improved-form-submission-a44c7e6f2b8d
- url
- https://medium.com/javascript-everyday/angular-signal-forms-improved-form-submission-a44c7e6f2b8d
- canonical_url
- https://medium.com/javascript-everyday/angular-signal-forms-improved-form-submission-a44c7e6f2b8d
- author_url
- https://medium.com/@wojtrawi
- status
- ok
- fetched_at
- 2026-06-14 11:28:49