Validation Tutorial

Learn how to use the Aurelia Validation package with this comprehensive tutorial.

Introduction

Aurelia provides a powerful validation library that allows you to add validation to your applications. If you are new to Aurelia, we recommend visiting the Getting Started section first to familiarize yourself with the framework.

This tutorial aims to teach you all the basics of validation. Enabling it, validation rules, conditional validation and multiple objects.

While building our form application, we will cover the following:

  • How to configure validation

  • Working with built-in validation rules

  • Conditional logic (if this, then that)

  • Highlighting input fields using a custom renderer and displaying errors

  • Working with multiple objects

A working demo and code for the following tutorial can also be found here.

Installation

To do this tutorial, you'll need a working Aurelia application. We highly recommend following the Quick Start guide to do this. However, for this tutorial, we have a starter Aurelia 2 application ready to go that we recommend. It will allow you to follow along and live code.

Because the validation packages do not come with Aurelia out-of-the-box, you will need to install them as detailed in the Validation section. The linked code environment already has these dependencies added for you.

Enable and configure the plugin

We need to tell Aurelia we want to use the validation package by registering it in main.ts via the register method.

import { ValidationHtmlConfiguration } from '@aurelia/validation-html';
import Aurelia from 'aurelia';

import { MyApp } from './my-app';

Aurelia
  .register(ValidationHtmlConfiguration)
  .app(MyApp)
  .start();

The ValidationHtmlConfiguration object will configure our Aurelia application to use HTML validation, and that's all we need to do to start using it.

Create a form

Because we will be using the validation package on a form (the most common scenario with validation), we will create one in my-app.html

my-app.html
<form>
  <div><input type="text" placeholder="First Name" value.bind="firstName & validate"></div>
  <br>
  <div><input type="text" placeholder="Last Name" value.bind="lastName & validate"></div>
  <br>
  <div><input type="number" placeholder="Age value.bind="age & validate"></div>
  <br>
  <div><input type="email" placeholder="Email Address" value.bind="email & validate"></div>
  <br>
  <div><input type="url" placeholder="http://google.com" value.bind="website & validate"></div>
  <br>
  <button type="button" click.trigger="add()">Add</button>
</form>

We have added a few form input elements and bound their values to class properties inside our view model. One thing to point out here is & validate which is a binding behavior that tells the validation library we want to validate these bindable values.

Add the validation plugin

We now need to include the validation plugin in our component. We'll inject it using Dependency Injection and then create rules and implement validation logic.

my-app.ts
import { newInstanceForScope, resolve } from '@aurelia/kernel';
import { IValidationRules } from '@aurelia/validation';
import { IValidationController } from '@aurelia/validation-html';

export class MyApp {
  public constructor(
    readonly validationController: IValidationController = resolve(newInstanceForScope(IValidationController)),
    readonly validationRules: IValidationRules = resolve(IValidationRules)
  ) {}
}

resolve(newInstanceForScope(IValidationController)) injects a new instance of validation controller, which is made available to the children of my-app

IValidationRules is what we will use to register our validation rules that we validate against in our views.

Create validation rules

Now we have our validation plugin added to the component, let's write validation rules.

my-app.ts

import { newInstanceForScope, resolve } from '@aurelia/kernel';
import { IValidationRules } from '@aurelia/validation';
import { IValidationController } from '@aurelia/validation-html';

export class MyApp {
  public constructor(
    private validationController: IValidationController = resolve(newInstanceForScope(IValidationController)),
    validationRules: IValidationRules = resolve(IValidationRules),
  ) {
    validationRules
      .on(this)
      .ensure('firstName')
      .required()
      .ensure('lastName')
      .required()
      .ensure('age')
      .required()
      .min(18)
      .max(110)
      .ensure('email')
      .required()
      .email()
      .ensure('website')
      .required();
  }
  public async add() {
    const result = await this.validationController.validate();
    if (result.valid) {
    } else {
      console.log(result);
    }
  }
}

By calling the validationRules API, we first must tell the plugin what we are validating. In this instance, its properties on our component class hence the on(this) part. If you wanted to validate an object called user, you would write on(this.user).

Running what we have so far will successfully validate our inputs but will provide no visual feedback.

Display error messages

We need a way to tell the user there are errors. We can get error messages from the validation controller and display them with very little code.

my-app.html
<form>
  <ul>
    <li repeat.for="error of validationController.results" if.bind="!error.valid">${error}</li>
  </ul>

  <div><input type="text" placeholder="First Name" value.bind="firstName & validate"></div>
  <br>
  <div><input type="text" placeholder="Last Name" value.bind="lastName & validate"></div>
  <br>
  <div><input type="number" placeholder="Age" value.bind="age & validate"></div>
  <br>
  <div><input type="email" placeholder="Email Address" value.bind="email & validate"></div>
  <br>
  <div><input type="url" placeholder="http://google.com" value.bind="website & validate"></div>
  <br>
  <button type="button" click.trigger="add()">Add</button>
</form>

Clicking the add button will now display some error messages. Try filling out the fields and see the error messages appear and disappear.

Turning input fields red on error

A common UI pattern when adding validation is turning the input fields red to highlight error states. While the Validation plugin provides a few ways to do this, we will leverage the validation-errors attribute.

my-app.html
<form>
  <ul>
    <li repeat.for="error of validationController.results" if.bind="!error.valid">${error}</li>
  </ul>
  <div validation-errors.from-view="firstNameErrors">
  <input class="${firstNameErrors.length ? 'error' : ''}" type="text" placeholder="First Name" value.bind="firstName & validate"></div>
  <br>
  <div validation-errors.from-view="lastNameErrors">
  <input class="${lastNameErrors.length ? 'error' : ''}" type="text" placeholder="Last Name" value.bind="lastName & validate"></div>
  <br>
  <div validation-errors.from-view="ageErrors">
  <input class="${ageErrors.length ? 'error' : ''}" type="number" placeholder="Age" value.bind="age & validate"></div>
  <br>
  <div validation-errors.from-view="emailErrors">
  <input class="${emailErrors.length ? 'error' : ''}" type="email" placeholder="Email Address" value.bind="email & validate"></div>
  <br>
  <div validation-errors.from-view="websiteErrors">
  <input class="${websiteErrors.length ? 'error' : ''}" type="url" placeholder="http://google.com" value.bind="website & validate"></div>
  <br>
  <button type="button" click.trigger="add()">Add</button>
</form>

We place the validation-errors attribute on the surrounding DIV element, then use the pattern of propertyNameErrors where propertyName our property's name is and Errors is the suffix that Aurelia sees as an error pointer.

Inside my-app.css add a CSS class called error

my-app.css
.error {
  border: 1px solid #ff0000;
  color: #ff0000;
}

Conditional validation logic

We could end things here. Our form has validation, and it shows error messages and styling. However, we are going to implement some conditional logic. Validation rules get called when certain values meet criteria (like radio button selections).

my-app.html
<form>
  <ul>
    <li repeat.for="error of validationController.results" if.bind="!error.valid">${error}</li>
  </ul>

  <div validation-errors.from-view="firstNameErrors">
  <input class="${firstNameErrors.length ? 'error' : ''}" type="text" placeholder="First Name" value.bind="firstName & validate"></div>
  <br>
  <div validation-errors.from-view="lastNameErrors">
  <input class="${lastNameErrors.length ? 'error' : ''}" type="text" placeholder="Last Name" value.bind="lastName & validate"></div>
  <br>
  <div validation-errors.from-view="ageErrors">
  <input class="${ageErrors.length ? 'error' : ''}" type="number" placeholder="Age" value.bind="age & validate"></div>
  <br>
  <div validation-errors.from-view="typeErrors">
    <label class="${typeErrors.length ? 'error' : ''}">
      <input type="radio" name="type" checked.bind="type" value="user" />
      User
    </label>
    <label class="${typeErrors.length ? 'error' : ''}">
      <input type="radio" name="type" checked.bind="type" value="customer" />
      Customer
    </label>
    <input type="hidden" value.bind="type & validate" />
  </div>
  <br>
  <div validation-errors.from-view="emailErrors">
  <input class="${emailErrors.length ? 'error' : ''}" type="email" placeholder="Email Address" value.bind="email & validate"></div>
  <br>
  <div validation-errors.from-view="websiteErrors" if.bind="type == 'user'">
  <input class="${websiteErrors.length ? 'error' : ''}" type="url" placeholder="http://google.com" value.bind="website & validate"></div>
  <br>
  <button type="button" click.trigger="add()">Add</button>
</form>

We have highlighted the changes to our form. We have added in a DIV with two radio inputs and a hidden input. The hidden input allows us to validate the value or the validator will see each radio input as a separate thing to validate.

We also add an if.bind to our website field as we are making the URL only mandatory for new users, not new customers.

my-app.ts

import { newInstanceForScope, resolve } from '@aurelia/kernel';
import { IValidationRules } from '@aurelia/validation';
import { IValidationController } from '@aurelia/validation-html';

export class MyApp {
  public constructor(
    private validationController: IValidationController = resolve(newInstanceForScope(IValidationController)),
    validationRules: IValidationRules = resolve(IValidationRules),
  ) {
    validationRules
      .on(this)
      .ensure('firstName')
      .required()
      .ensure('lastName')
      .required()
      .ensure('age')
      .required()
      .min(18)
      .max(110)
      .ensure('type')
      .required()
      .ensure('email')
      .required()
      .email()
      .ensure('website')
      .required()
        .when(obj => obj.type === 'user')
        .withMessage(`Website is required when creating a new user.`)
  }

  public async add() {
    const result = await this.validationController.validate();
    if (result.valid) {
    } else {
      console.log(result);
    }
  }
}

We make a slight change to our website rules by using .when to introduce a condition to our validation. The obj The object itself is returned, allowing us to inspect other object property values.

  • When the type value is user

  • Make the website property mandatory

  • Use withMessage to create a custom validation message

Do something on successful validation

We already have the code for this part, but we must talk about it. In our my-app.ts file, we created a method called add which calls our validation controller.

my-app.ts
public async add() {
  const result = await this.validationController.validate();

  if (result.valid) {
    // Make an API call or do something here when validation passes
  } else {
    console.log(result);
  }
}

Calling the validate method on the validation controller, which returns a promise, allows us to check the valid property to determine if validation was successful or not. The valid value will be true if all validation rules pass and false if one or more do not. In the case of a form where we create users, we would probably call an API or something to save.

The cool thing about this is that we also get back all the validation rules (the ones that pass and fail) on the results property. This allows us to do things in our code without relying on the view. An example might be to display a toast notification with an error message for one or more errors.

Conclusion

While we only scratched the surface of what the validator can do, we have covered all of the essential aspects of validation. We highly recommend reading over the validation documentation to better understand the validation library.

The code above can be found in a working demo application here.

Last updated