For many forms, a single field validation is enough. But what happens when the validation of one field is dependent on the value of another field?
When working with Angular, cross field validation allows you handle these kind of scenarios.
Take a flight booking form for example, you may want to ensure that your users do not accidentally select an invalid date range such as choosing a departure date that comes after a return date. Or you might want to prevent them from selecting same airport for departure and arrival.
These are situations where validating individual fields is not enough. Instead, you need to validate the relationship between multiple fields.
This is where cross validation becomes a useful tool.
The Flight Booking Example
To demonstrate this, we'll create a simple flight booking form.
The form will let users select a departure airport, an arrival airport, a departure date, and an optional return date.
We'll then implement two validation rules:
- The departure and arrival airports cannot be the same.
- The return date must always be after the departure date.
Creating our form model
Our form model tells us what our form will look like:
export interface Booking {
departure: string;
arrival: string;
departureDate: Date;
returnDate: Date;
returning: boolean
}
Defining the Form Model
We define our form model using Angular signal. Creating an initial state for the form by assigning a default value to each field.
private bookingModel = signal<Booking>({
departure: '',
departureDate: new Date(),
arrival: '',
returnDate: new Date(),
returning: false,
});
Creating Our form
With our form model ready, we can now create our form using the form() function.
To validate the date fields, Angular provides us with the validate() function. The validate() callback gives us access to value() which returns the value of the current field and valueOf() which returns the value of another field in the form .
In the code snippet below, we're validating the returnDate field while also reading the value of departureDate. This allows us compare and determine if both fields are valid before submission.
bookingForm = form(this.bookingModel, (schema) => {
validate(schema.returnDate, ({ value, valueOf }) => {
const returnDate = value();
const departureDate = valueOf(schema.departureDate);
if (!returnDate || !departureDate) {
return null;
}
if (returnDate <= departureDate) {
return {
kind: 'invalidDateRange',
message: 'Return date must be after the departure date',
};
}
return null;
});
hidden(schema.returnDate, {
when: ({ valueOf }) => {
return !valueOf(schema.returning);
},
});
});
How the Validation Works
Before comparing the dates, we check if both fields have values. If either field is empty, we return null because there isn't a date range to validate yet.
Once both date fields have values, we compare them. If the return date is the same or comes before departure date, we return a validation error object, else we return null so users know the validation has passed.
The error validation object contains the following properties:
- The
kindproperty tells the kind of validation error - The
messageproperty is text that can be conveyed to user - Returning
nullmeans that the value is returned and Angular sees the field as valid
Building The Template
With our form ready, it is time to build the template. We'll create the form fields and bind them to the fieldTree using the [formField] directive.
In order to display the error messages, we'll loop through the returnDate error array and render each error message accordingly.
<form (submit)="submitForm($event)">
<div class="departure">
<div class="airport">
<label for="from">From:</label>
<select id="from" [formField]="bookingForm.departure">
<option value="">Select departure</option>
@for (airport of departureAirport(); track airport.id) {
<option [value]="airport.id"> {{ airport.airport }} ({{airport.code}})</option>
}
</select>
</div>
<div class="date">
<label for="departureDate">Date:</label>
<input type="date" id="departureDate" [formField]="bookingForm.departureDate">
</div>
</div>
<div class="arrival">
<div class="destination">
<div class="airport">
<label for="to">To:</label>
<select id="to" [formField]="bookingForm.arrival">
<option value="">Select destination</option>
@for (airport of arrivalAirport(); track airport.id) {
<option [value]="airport.id"> {{ airport.airport }} ({{airport.code}})</option>
}
</select>
</div>
</div>
</div>
<div class="date">
<div class="returning">
<input type="checkbox" id="returning" [formField]="bookingForm.returning">
Return?
</div>
@if (!bookingForm.returnDate().hidden()) {
<input type="date" id="return Date" [formField]="bookingForm.returnDate">
@for (error of bookingForm.returnDate().errors(); track $index) {
<p class="error">{{error.message}}</p>
}
}
</div>
<button
type="submit"
[disabled]="bookingForm().invalid()">
Search Flights
</button>
</form>
Here's how the working solution behaves:
In conclusion, we've ensured that our users can't select an invalid date range. We also explored how Angular Signal Forms simplifies cross-field validation with validate() and valueOf().
🔗 Live Demo & Source Code ⭐
Cross Field Validation Demo

Top comments (0)