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 aValidationController
, avalidate
binding behavior, aValidator
interface and more.aurelia-validatejs
- a validatejs powered implementation of theValidator
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:
blur
: Validate the binding when the binding's target element fires a DOM "blur" event.change
: Validate the binding when it updates the model due to a change in the view.manual
: Manual validation. Use the controller'svalidate()
andreset()
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:
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:
- Edit registration-form.js... uncomment the code to change the validation trigger to "manual" or "change"
- 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:
-
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.
-
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:
- If you're using
validateTrigger.blur
(the default), you'll want to make sure your custom element publishes DOMblur
events. - Your custom element should expose a
focus
method on it's DOM element if you plan on building a validation summary that callsfocus()
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!