Rorix Technologies Logo
Engineering13 min read

Getting Started with Angular 21 Signal Forms

Angular21Signal FormSignals
Getting Started with Angular 21 Signal Forms

1. Introduction: Why Signal Forms?

  • Traditional Reactive Forms vs Signal Forms

  • Why Angular introduced Signal Forms

  • Benefits: better reactivity, less boilerplate, fine-grained updates

Angular 21 introduces Signal Forms, a new way to build forms using Angular Signals. Signal Forms provide a more reactive, declarative, and performant approach compared to traditional Reactive Forms.

In this blog, I’ll walk through a practical example that demonstrates validations, dynamic validators, array-like behavior using applyEach(), form submission using signals, and building custom input components


2. Creating a Basic Signal Form

Key Points:

  • form() creation

  • Initial values

We start by defining a signal-based form using the form() API. Each field is represented as a signal control, which automatically reacts to value and validation changes.

interface PersonalForm {
firstName: string;
lastName: string;
email: string;
phone: string;
hobbies: string[];
address: string;
isAddressRequired: boolean;
}
 
export class Home {
readonly personalFormModel = signal<PersonalForm>({
firstName: '',
lastName: '',
});
 
readonly personalForm = form(this.personalFormModel, (schemaPath) => {
required(schemaPath.firstName, { message: 'First name is required' });
required(schemaPath.lastName, { message: 'Last name is required' });
});
}

 


3. Validations in Signal Forms

3.1 Built-in Validations (Email, Regex, Required)

Signal Forms support all common validations such as required, email, and pattern (regex) validations. These validators run automatically whenever the control value changes.

readonly personalFormModel = signal<PersonalForm>({
email: '',
phone: '',
});
 
readonly personalForm = form(this.personalFormModel, (schemaPath) => {
required(schemaPath.email, { message: 'Email is required' });
email(schemaPath.email, { message: 'Invalid email format' });
required(schemaPath.phone, { message: 'Phone number is required' });
pattern(schemaPath.phone, /^\+?[0-9]{1,3}?[-.\s]?[0-9]{7,14}$/, {
message: 'Invalid phone number format',
});
});

3.2 Conditional Validators (Add / Remove Dynamically)

One powerful feature of Signal Forms is the ability to dynamically add or remove validators.

For example, when a checkbox is checked, an additional field becomes required. When unchecked, the validation is removed automatically.

Key Points:

  • No setValidators()

  • No updateValueAndValidity()

  • Signals handle reactivity

readonly personalFormModel = signal<PersonalForm>({
isAddressRequired: false,
address: '',
});
 
readonly personalForm = form(this.personalFormModel, (schemaPath) => {
required(schemaPath.address, {
message: 'Address is required',
when: ({ valueOf }) => valueOf(schemaPath.isAddressRequired),
});
});

4. Implementing Form Array–like Behavior Using applyEach()

This is a highlight section because many devs struggle with FormArray.

 

Key Points:

  • Problem with traditional FormArray

  • How applyEach() solves it

Instead of using FormArray, Signal Forms provide applyEach() to handle repeated form controls.

applyEach() allows us to apply the same validation and structure to every item in a dynamic list, making the code cleaner and easier to manage.

readonly personalFormModel = signal<PersonalForm>({
hobbies: [''],
});
 
readonly personalForm = form(this.personalFormModel, (schemaPath) => {
applyEach(schemaPath.hobbies, (itemSchema) => {
required(itemSchema, { message: 'Hobby is required' });
});
});

5. Submitting the Form Using submit()

This is new and important.

 

Key Points:

  • How submit() ensures validations run

  • Cleaner submit logic

  • No manual markAllAsTouched()

Angular 21 introduces the submit() API for Signal Forms. When the submit button is clicked, submit() validates the entire form and provides access to the final form value in a reactive way.

save() {
submit(this.personalForm, async () => {
// This event is triggered only if the form is valid
alert('Form submitted successfully!');
});
}

6. Creating Custom Input Components Using Form Value Controls

This shows real-world usage.

Signal Forms make it easy to create reusable custom input components. By passing a form value control into the component, we can fully integrate validation, value updates, and error handling without additional boilerplate.

Key Points:

  • Reusability

  • Cleaner templates

  • Better separation of concerns

export class CustomInput implements FormValueControl<any> {
inputType = input.required<'text' | 'email' | 'textarea' | 'tel' | 'checkbox'>();
placeholder = input<string>('');
id = input<string>('');
 
value = model<string | number | boolean>('');
 
toggle(event: any) {
this.value.set(event.target.checked);
}
 
changeAddress(event: any) {
this.value.set(event.target.value);
}
}

7. Error Handling and UI Feedback

Key Points:

  • Reading errors()

  • Showing messages

  • Touched / dirty states

<form novalidate>
<app-custom-input inputType="text" id="firstName" placeholder="Enter your first name" [field]="personalForm.firstName"></app-custom-input>
@if ((personalForm.firstName().touched() || personalForm.firstName().dirty()) && personalForm.firstName().invalid()) {
@let error = personalForm.firstName().errors()[0].message;
<p class="text-red-500 text-xs italic mb-4">{{ error }}</p>
}
 
<app-custom-input inputType="text" id="lastName" placeholder="Enter your last name" [field]="personalForm.lastName"></app-custom-input>
@if ((personalForm.lastName().touched() || personalForm.lastName().dirty()) && personalForm.lastName().invalid()) {
@let error = personalForm.lastName().errors()[0].message;
<p class="text-red-500 text-xs italic mb-4">{{ error }}</p>
}
 
<app-custom-input inputType="email" id="email" placeholder="Enter your email" [field]="personalForm.email"></app-custom-input>
@if ((personalForm.email().touched() || personalForm.email().dirty()) && personalForm.email().invalid()) {
@for (error of personalForm.email().errors(); track error.kind) {
<p class="text-red-500 text-xs italic mb-4">{{ error.message }}</p>
}
}
 
<app-custom-input inputType="tel" id="phone" placeholder="Enter your phone number" [field]="personalForm.phone"></app-custom-input>
@if ((personalForm.phone().touched() || personalForm.phone().dirty()) && personalForm.phone().invalid()) {
@for (error of personalForm.phone().errors(); track error.kind) {
<p class="text-red-500 text-xs italic mb-4">{{ error.message }}</p>
}
}
 
<app-custom-input inputType="checkbox" id="required-address" placeholder="Set address as required" [field]="personalForm.isAddressRequired"></app-custom-input>
 
<app-custom-input inputType="textarea" id="address" placeholder="Enter your address" [field]="personalForm.address"></app-custom-input>
@if ((personalForm.address().touched() || personalForm.address().dirty()) && personalForm.address().invalid()) {
@for (error of personalForm.address().errors(); track error.kind) {
<p class="text-red-500 text-xs italic mb-4">{{ error.message }}</p>
}
}
 
@for (item of personalForm.hobbies; track $index) {
<div class="flex align-middle">
<app-custom-input inputType="text" id="hobby" placeholder="Enter your hobby" [field]="item"></app-custom-input>
@if($index == 0) {
<button type="button" (click)="addHobby()" class="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded ml-2 mb-4">Add</button>
} @else {
<button type="button" (click)="removeHobby($index)" class="bg-red-500 hover:bg-red-700 text-white font-bold py-2 px-4 rounded ml-2 mb-4">Remove</button>
}
</div>
@if (item().invalid() && item().touched() || item().dirty()) {
@for (error of item().errors(); track error.kind) {
<p class="text-red-500 text-xs italic mb-4">{{ error.message }}</p>
}
}
}
 
<div class="flex justify-between">
<button type="button" (click)="save()" class="bg-green-500 hover:bg-green-700 text-white font-bold py-2 px-4 rounded">Submit</button>
</div>
</form>

8. Final Thoughts

Angular 21 Signal Forms provide a modern, reactive alternative to traditional forms. With built-in validations, conditional logic, applyEach() for dynamic lists, simplified submission, and easy custom input integration, they significantly reduce boilerplate while improving readability and maintainability.

If you’re starting a new Angular application or refactoring an existing one, Signal Forms are definitely worth exploring.

9. GitHub Repository

https://github.com/Rorix-Internal-Apps/Signal-form

Ready to Transform Your Warehouse?

Get a free, detailed estimate for your custom WMS solution