Aurelia validation alpha

We've been working hard on a validation rewrite and today we're excited to announce the Aurelia validation libraries are in alpha! After gathering all your feedback and use-cases we've refactored validation into two separate libraries with robust set of standard behaviors and a simpler, more flexible API surface for easier customization:

  • aurelia-validation - a generic validation library that provides a ValidationController, a validate binding behavior, a Validator interface and more.
  • aurelia-validatejs - a validatejs powered implementation of the Validator interface along with fluent and decorator based APIs for defining rules for your components and data.

Example

Let's put together a simple registration form to demonstrate the new validation APIs.

1) Create a new module that exports a RegistrationForm class:

registration-form.js

export class RegistrationForm {
  firstName = '';
  lastName = '';
  email = '';

  submit() {
    // todo: call server...
  }
}

2) Create a registration form view:

NOTE: I'm using bootstrap markup in this example. Bootstrap is not required. You can use whatever you want.

registration-form.html

<template>
  <form submit.delegate="submit()">

    <div class="form-group">
      <label class="control-label" for="first">First Name</label>
      <input type="text" class="form-control" id="first" placeholder="First Name"
             value.bind="firstName">
    </div>

    <div class="form-group">
      <label class="control-label" for="last">Last Name</label>
      <input type="text" class="form-control" id="last" placeholder="Last Name"
             value.bind="lastName">
    </div>

    <div class="form-group">
      <label class="control-label" for="email">Email</label>
      <input type="text" class="form-control" id="email" placeholder="Email"
             value.bind="email">
    </div>

    <button type="submit" class="btn btn-primary">Submit</button>
  </form>
</template>

3) Install aurelia-validation:
jspm install aurelia-validation

4) Give our view-model an instance of a ValidationController:
import {inject, NewInstance} from 'aurelia-dependency-injection';
import {ValidationController} from 'aurelia-validation';

@inject(NewInstance.of(ValidationController))
export class RegistrationForm {
  constructor(controller) {
    this.controller = controller;
  }

The ValidationController manages a set of bindings and a set of renderers, controlling when to tell the renderers to render or unrender validation errors. The @inject(NewInstance.of(ValidationController)) line of code is important. I'm going to skip discussing it for now and revisit it later in this post.

5) Configure the ValidationController:

The default "trigger" that tells the validation controller to validate bindings is the DOM blur event. All in all there are three standard "validation triggers" to choose from:

  1. blur: Validate the binding when the binding's target element fires a DOM "blur" event.
  2. change: Validate the binding when it updates the model due to a change in the view.
  3. manual: Manual validation. Use the controller's validate() and reset() methods to validate all bindings.

To configure the controller's validation trigger, import the validateTrigger enum: import {validateTrigger} from 'aurelia-validation'; and assign the controller's validateTrigger property:

controller.validateTrigger = validateTrigger.manual;

6) Implement the view-model's submit method.

No matter which validation trigger you chose, you're probably going to want to validate all bindings when the form is submitted. Use the controller's validate() method to do this:

submit() {
  let errors = this.controller.validate();
  ...
}

7) Define validation rules:

At this point your view-model should look like this:

import {inject, NewInstance} from 'aurelia-dependency-injection';
import {ValidationController} from 'aurelia-validation';

@inject(NewInstance.of(ValidationController))
export class RegistrationForm {
  firstName = '';
  lastName = '';
  email = '';

  constructor(controller) {
    this.controller = controller;
  }

  submit() {
    let errors = this.controller.validate();
    // todo: call server...
  }
}

Let's bring in the aurelia-validatejs plugin which has APIs for defining rules and an implementation of the Validator interface that the ValidationController depends on to validate bindings. You can of course build your own implementation of Validator, one is in the works for breeze.

jspm install aurelia-validatejs

Now lets define some rules on our RegistrationForm class. You could use the decorator API:

import {required, email} from 'aurelia-validatejs'
...
...
export class RegistrationForm {
  @required
  firstName = '';

  @required
  lastName = '';

  @required
  @email
  email = '';

Or you can use the fluent API:

import {ValidationRules} from 'aurelia-validatejs'
...
...
export class RegistrationForm {
  ...
}

ValidationRules
  .ensure('firstName').required()
  .ensure('lastName').required()
  .ensure('email').required().email()
  .on(RegistrationForm);

8) Add validation to our bindings

Now that the view-model has been implemented and rules are defined, it's time to add validation to the view.

First, let's use the validate binding behavior to all of the input value bindings on our form to indicate these bindings require validation...

Change value.bind="someProperty" to value.bind="someProperty & validate".

The binding behavior will obey the controller's validation trigger configuration and notify the controller when the binding instance requires validation. In turn, the controller will validate the object/property combination used in the binding and instruct the renderers to render or unrender errors accordingly.

9) Create a ValidationRenderer

We're almost done. One of the last things we need to do is define how validation errors will be rendered. This is done by creating one or more ValidationRenderer implementations. A validation renderer implements a simple API render(error, target) and unrender(error, target) (error is a ValidationError instance and target is the binding's DOM element).

Since we're using bootstrap, we'll create a renderer that adds the has-error css class to the form-group div of fields that have errors. We'll also add a <span class="help-text"> elements to the form-group div, listing each of the field's errors. Here's the BootstrapFormValidationRenderer code and this is what a rendered error will look like:

last name is required

10) Register the validation renderer with the component's controller.

Now that we have a render implementation we need to tell the controller to use the renderer. The aurelia-validate library ships with a validation-renderer custom attribute you can use for this purpose:

<form submit.delegate="submit()"
      validation-renderer="bootstrap-form">

You give the attribute the name of a renderer registration and it will resolve the renderer from the container and register it with the nearest controller instance.

At this point the view looks like this:

<template>
  <form submit.delegate="submit()"
        validation-renderer="bootstrap-form">

    <div class="form-group">
      <label class="control-label" for="first">First Name</label>
      <input type="text" class="form-control" id="first" placeholder="First Name"
             value.bind="firstName & validate">
    </div>

    <div class="form-group">
      <label class="control-label" for="last">Last Name</label>
      <input type="text" class="form-control" id="last" placeholder="Last Name"
             value.bind="lastName & validate">
    </div>

    <div class="form-group">
      <label class="control-label" for="email">Email</label>
      <input type="email" class="form-control" id="email" placeholder="Email"
             value.bind="email & validate">
    </div>

    <button type="submit" class="btn btn-primary">Submit</button>
  </form>
</template>

Try It

Here's a live demo that includes an example "validation summary" component using aurelia-validation.

Exercises for the reader:

  1. Edit registration-form.js... uncomment the code to change the validation trigger to "manual" or "change"
  2. In registration-form.js, remove the validation decorators on the class properties and uncomment the fluent rule definitions at the bottom of the file.

Further Reading

We'll be providing more in-depth information in the docs however there's a few more things I want to cover in this post:

Fluent rule definition

The sample above demonstrates the fluent API and applying the rules to a class definition using the .on(RegistrationForm) method. You can also use the .on() method with POJOs:

let person = {
  firstName: '',
  lastName: '',
  email: ''
};

ValidationRules
  .ensure('firstName').required()
  .ensure('lastName').required()
  .ensure('email').required().email()
  .on(person);  // <-- define rules on the instance

There's no requirement to use the .on() method however. You can capture the ruleset in a property and pass it into the validate decorator as a parameter if you prefer:

export class RegistrationForm {
  rules = ValidationRules
    .ensure('firstName').required()
    .ensure('lastName').required()
    .ensure('email').required().email();
    
  person = {
    firstName: '',
    lastName: '',
    email: ''
  };
}
<!-- pass ruleset to decorator via parameter: -->
<input value.bind="person.firstName & validate:rules">

The validation-errors attribute

You may want to data-bind to the current set of "broken rules". The aurelia-validation library ships with a custom attribute called validation-errors that will populate the property it's bound to with the current set of validation errors.

Here's how you could use the validation-errors attribute to display the list of errors with links that will focus the input element that has the error.

<template>
  <form validation-errors.bind="myErrors">
    <ul>
      <li repeat.for="errorInfo of myErrors">
        ${errorInfo.error.message}
        <a href="#" click.delegate="errorInfo.target.focus()"> Fix it</a>
      </li>
    </ul>
    ...
</template>

What is NewInstance.of(ValidationController) ???

NewInstance.of is a dependency-injection resolver that ships with aurelia-dependency-injection. Resolvers tell the container how to resolve a particular key. In this case it's telling the container to always retrieve a new instance of a ValidationController. This does a couple things:

  1. It ensures the registration form gets it's own instance of a ValidationController rather than sharing one with another component. Most of the time this is the behavior you'll want.

  2. When Aurelia instantiates components it uses a child container of the outer component. This is important because it means behind your component hierarchy is a hierarchy of container instances. Our validation controller instance is installed in that hierarchy, making it easy for downstream renderer instances and validate binding behaviors to locate the relevant validation controller.

My form inputs are custom elements... is that supported?

Yes. The validate binding behavior works with custom elements however there are a couple best practices:

  1. If you're using validateTrigger.blur (the default), you'll want to make sure your custom element publishes DOM blur events.
  2. Your custom element should expose a focus method on it's DOM element if you plan on building a validation summary that calls focus() on the validation error's target element.

Here's an example of a widget that might appear in a form:

<widget id="first" label="First Name"
        value.bind="firstName & validate">
</widget>

A widget implementation that would work well with the validation system would look like this:

<template>
  <div class="form-group">
    <label class="control-label" for="${id}">${label}</label>
    <input type="text" class="form-control"
           id="${id}" ref="input" placeholder="${label}"
           value.two-way="value"
           blur.trigger="blur()">
  </div>
</template>
import {bindable, inject, DOM} from 'aurelia-framework';

@inject(Element)
export class Widget {
  @bindable id;
  @bindable label;
  @bindable value;

  constructor(element) {
    this.element = element;

    // ensure the element exposes a "focus" method
    element.focus = () => this.input.focus();
  }

  blur() {
    // forward "blur" events to the custom element
    const event = DOM.createCustomEvent('blur');
    this.element.dispatchEvent(event);
  }
}

I don't like the way all this works. It's not meeting my requirements.

Let us know! File a github issue. Worst case scenario is you'll need to implement your own validate binding behavior, which isn't hard to do.

Next Steps

We're going to be working on more docs, localization/i18n and cleaning up the list of issues in the validation repositories. Keep sending your feedback!