DEV Community

margaret ogbor
margaret ogbor

Posted on

Cross Field Validation With Angular Signal Forms

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
}
Enter fullscreen mode Exit fullscreen mode

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,
  });
Enter fullscreen mode Exit fullscreen mode

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);
      },
    });
  });
Enter fullscreen mode Exit fullscreen mode

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 kind property tells the kind of validation error
  • The message property is text that can be conveyed to user
  • Returning null means 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>
Enter fullscreen mode Exit fullscreen mode

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)