There are times when we might need to work on forms that show and validate fields depending on a users selected input. For example an account registration form, with two types of account:
- Individual
- Business
When the user selects individual, show:
- First name
- Last name
When the user selects business, show:
- Company name
- Tax ID
This means that the validation rules will likely change for every change in account type.
In this blog post, we'll see how we can dynamically validate form fields based on a users selected account type, using angular signal forms.
Setting up our form model
Our form model tells us what our form will look like and the data it will contain. We define it like so:
export interface userAccount {
account: {
type: 'individual' | 'business';
individual: {
firstName: string;
lastName: string;
},
business: {
companyName: string;
taxId: string;
registrationNumber: string;
}
}
}
Creating the accountModel
Next, we'll create the accountModel. This holds the initial state of our form.
According to the angular docs, when working with dynamic fields, it is a good idea to initialize every field even if some of them won't be visible initially. Not doing so causes those fields to not exist in the form field tree, making it impossible to reference and interact with them later.
accountModel = signal<userAccount>({
account: {
type: 'individual',
individual: {
firstName: '',
lastName: '',
},
business: {
companyName: '',
taxId: '',
registrationNumber: '',
},
},
});
Defining our form schema
Then, we'll tell the form which sections should be visible.
When the user selects Individual, we want to hide the business fields.
When the user selects Business, we want to hide the individual fields.
accountTypeSchema = schema<userAccount>((user) => {
// Hide individual account when user selects business.
hidden(user.account.individual, {
when: ({ valueOf }) => valueOf(user.account.type) === 'business',
});
// Hide business account when user selects individual.
hidden(user.account.business, {
when: ({ valueOf }) => valueOf(user.account.type) === 'individual',
});
});
Creating our form
Now that we have both our schema and model, we can create the form.
To create the form, we use Angular Signal's form() function, passing both accountModel and accountTypeSchema as arguments.
registrationForm = form<userAccount>(this.accountModel, this.accountTypeSchema);
At this point, our form is ready to be used in the template.
Building the template
Since our schema already knows which part of the form should be hidden based on a users selected input, we just need to reflect that in the template of our application.
Angular signal forms, expects hidden fields to be removed from the DOM. And that's why we wrapped each section with an @if block statement.
Instead of checking the selected account type, we simply check if the section is hidden.
<form>
<h2>Create Account</h2>
<div>
<label>Account Type</label>
<div>
<label>
<input
type="radio"
value="individual"
[formField]="registrationForm.account.type"
/>
Individual
</label>
<label>
<input
type="radio"
value="business"
[formField]="registrationForm.account.type"
/>
Business
</label>
</div>
</div>
@if (!registrationForm.account.individual().hidden()) {
<fieldset>
<legend>Individual Information</legend>
<div>
<label for="firstName">First Name</label>
<input
id="firstName"
type="text"
[formField]="registrationForm.account.individual.firstName"
/>
</div>
<div>
<label for="lastName">Last Name</label>
<input
id="lastName"
type="text"
[formField]="registrationForm.account.individual.lastName"
/>
</div>
</fieldset>
}
@if (!registrationForm.account.business().hidden()) {
<fieldset>
<legend>Business Information</legend>
<div>
<label for="companyName">Company Name</label>
<input
id="companyName"
type="text"
[formField]="registrationForm.account.business.companyName"
/>
</div>
<div>
<label for="taxId">Tax ID</label>
<input
id="taxId"
type="text"
[formField]="registrationForm.account.business.taxId"
/>
</div>
<div>
<label for="registrationNumber">Registration Number</label>
<input
id="registrationNumber"
type="text"
[formField]="registrationForm.account.business.registrationNumber"
/>
</div>
</fieldset>
}
<button type="submit">Create Account</button>
</form>
When you run the application, you can see that switching between individual and business types automatically updates the visible section of the form with no data lost.
🔗 Live Demo & Source Code ⭐
Dynamic Form Validation with Angular Signal Forms
Top comments (0)