A closer look at registering the Aurelia Validation plugin
Like all Aurelia plugins, you'll usually configure them inside your file. Importing the configuration object from the validation package and passing it into the register method provided by Aurelia is all you need to do to register it.
In the previous step here, we already went over this. Make sure you have the plugin and relevant adapters installed before continuing.
import { ValidationHtmlConfiguration } from '@aurelia/validation-html';
import Aurelia from 'aurelia';
Aurelia
.register(ValidationHtmlConfiguration)
.app(component)
.start();
This sets up the plugin with the required dependency registrations. You can also customize the validation plugin via the customize method, which provides an options object we can use to configure default settings.
The following options are available for customization.
From @aurelia/validation
ValidatorType: Custom implementation of IValidator. Defaults to StandardValidator.
MessageProviderType: Custom implementation of IValidationMessageProvider. Defaults to ValidationMessageProvider.
ValidationControllerFactoryType: Custom implementation of the factory for IValidationController; Defaults to ValidationControllerFactory.
CustomMessages: Custom validation messages.
From @aurelia/validation-html
HydratorType: Custom implementation of IValidationHydrator. Defaults to ModelValidationHydrator.
DefaultTrigger: Default validation trigger. Defaults to blur.
UseSubscriberCustomAttribute: Use the validation-errors custom attribute. Defaults to true.
SubscriberCustomElementTemplate: Custom template for validation-container custom element. Defaults to the default template of the custom element.
These options are explained in detail in their respective sections. Note that the categorization of the options is done with the intent of clarifying the origin package of each option. However, as the @aurelia/validation-html wraps @aurelia/validation all the customization options are available when the @aurelia/validation-html package is registered.
The @aurelia/validation-i18n package is skipped intentionally for now, as discussed in detail later.
Migration Guide & Breaking Changes
Creating and customing Aurelia Validation rules to ensure data is validated.
This section outlines the breaking changes introduced by @aurelia/validation* as compared to the predecessor aurelia-validation. However, it is recommended that you read the documentation, as many new features have been added.
A list of differences
Functionality is now in three different packages
Instead of a single validation package, the functionalities are arranged in three different packages. These are @aurelia/validation (provides core functionalities), @aurelia/validation-html (provides integration with the view), and @aurelia/validation-i18n (provides localization support for validation in view).
Rules are defined differently using ValidationRules
Usage of ValidationRules in terms of defining rules is a bit different. The example below shows the difference. Refer to the Defining rules section for the details.
Named registration of custom rules is no longer supported
Named registration of reusable custom rules is no longer supported in favor of simply using an instance of the rule implementation. The example below shows the difference. Refer to the Customizing rules section for the details.
// aurelia-validation
ValidationRules.customRule(
'customRule',
// rule body
// rule config
);
ValidationRules
.ensure('property')
.satisfiesRule('customRule' ...);
// @aurelia/validation
class CustomRule extends BaseValidationRule { // of implements IValidationRule
// rule config
public execute() {
// rule body
}
}
validationRules
.on(obj)
.ensure('property')
.satisfiesRule(new CustomRule(...));
The validator interface only has one method
The validator interface has been changed to have only one validate method equipped with validation instructions. Refer to the Validator and validate instruction section for the details.
Validation controller factory usage changed
The usage of the validation controller factory is changed. Instead of using controllerFactory.createForCurrentScope(); , you need to use the argument decorator @newInstanceForScope(IValidationController) syntax. Refer to the Injecting a controller instance section for the details.
Validation renderer has been removed
No validation renderer in favor of ValidationResultsSubscriber. Refer to the addSubscriber and removeSubscriber section for the details.
Tagging Rules
Creating groups of tagged rules to allow for re-use of Aurelia Validation rules.
Tagging rules involves marking a rule or ruleset for a property or an object with a string identifier, namely a tag. Later, the tag can be used to execute a specific set of rules selectively. Note that every set of rules defined on an object has a tag. When the tag is omitted, a default tag for the ruleset is used for the objects.
Tags can be defined both with objects and properties. Refer to the following examples.
/* tagged rule definition */
// default tag
validationRules
.on(person)
.ensure('name')
// specific tags on object
validationRules
.on(person, 'ruleset1')
//...
.on(person, 'ruleset2')
//...
// specific tag on property rules
validationRules
.on(person)
.ensure('name')
.minLength(42)
.tag('ruleset1')
.minLength(84)
.tag('ruleset2')
/* executing tagged rules */
// default tag
validator.validate(new ValidateInstruction(person));
validator.validate(new ValidateInstruction(person, 'name'));
// specific object tag
validator.validate(new ValidateInstruction(person, undefined, 'ruleset1'));
// specific property tag
validator.validate(new ValidateInstruction(person, 'name', undefined, 'ruleset1'));
Architecture
Familiarize yourself with the Aurelia Validation plugin and how it all pieces together.
Overview
There are three different interrelated packages for validation. The relation between the packages are depicted in the following diagram.
@aurelia/validation: Provides the core validation functionality. Hosts the validator, out-of-the-box rule implementations, and the validation message provider.
@aurelia/validation-html: Provides the view-specific functionalities such as validation controller, validate binding behavior, and subscribers. It wraps the @aurelia/validation package so that you do not need to register both packages.
@aurelia/validation-i18n: Provides localized implementation of the validation message provider and validation controller. Wraps the @aurelia/validation-html package.
The rest of the document assumes that validation is view is more common scenario. For that reason, the demos are mostly integrated with view.
How does it work
The validationRules (IValidationRules instance) allows defining validation rules on a class or object/instance. The defined rules are stored as metadata in a global registry.
The instance of PropertyRule instance hold the collection of rules defined for a property. In simplified terms it can be described by the diagram below.
The validator (IValidator instance) allows you to execute a validate instruction, which instructs which object and property needs to be validated. The validator gets the matching rules from the RulesRegistry (see the diagram above), and executes those.
The last piece of the puzzle is to getting the rules executed on demand. For this the validation controller (IValidationController instance) is used along with the validate binding behavior (more on these later). The binding behavior registers the property binding with the validation controller, and on configured event, instructs the controller to validate the binding. The validation controller eventually ends up invoking the IValidator#validate with certain instruction which triggers the workflow shown in the last diagram. The following diagram shows a simplified version of this.
The following sections describe the API in more detail, which will help understanding the concepts further.
Displaying Errors
How to display validation errors in your UI.
The validation controller maintains the active list of validation results which can be iterated to display the errors in UI.
<ul>
<li repeat.for="result of validationController.results">
<template if.bind="!result.valid">${result}</template>
</li>
</ul>
There are also some out-of-the-box components that can be used to display the errors. These are discussed in the following sections.
validation-errors custom attribute
This custom attribute can be used to bind the errors for children the target elements.
<div validation-errors.from-view="nameErrors"> <!--binds all errors for name to the "nameErrors" property-->
<input value.bind="person.name & validate">
<div>
<span repeat.for="error of nameErrors">${error.result.message}</span>
</div>
</div>
<div validation-errors.from-view="ageErrors"> <!--binds all errors for age to the "ageErrors" property-->
<input value.bind="person.age & validate">
<div>
<span repeat.for="error of ageErrors">${error.result.message}</span>
</div>
</div>
Note that this in itself does not show any error, unless errors are iterated to be bound with the view. An example can be seen below.
A point to note is that multiple validation targets can also be used for a single validation-errors custom attribute, and the errors for multiple targets will be captured the same way.
<div validation-errors.from-view="errors"> <!--binds all errors for name, and age to the "errors" property-->
<input value.bind="person.name & validate">
<input value.bind="person.age & validate">
<div>
<span repeat.for="error of errors">${error.result.message}</span>
</div>
</div>
The usage of this custom element can be deactivated by using UseSubscriberCustomAttribute configuration option.
This is useful if you have a custom attribute of the same name, and want to use that over this out-of-the-box custom attribute.
validation-container custom element
The validation-containercustom element also has similar goal of capturing the validation errors for the children target elements. Additionally, it provides a template to display the errors as well. This helps in reducing the boilerplate created by the validation-errors custom attribute. For example, using this custom element, displaying the errors reduces to the following.
There are couple of important points to note about the examples shown above. The first validation target shown in the example uses the default template of the custom element. This custom element template is based on two slots as shown below.
The results of using the default template may not also suite your app or esthetics. However content can be injected into the slot from Light DOM (in case you are unfamiliar with the concepts, you are encouraged to give this excellent article a read) as shown in the example. Although traditionally the default slot is meant for the validation target(s), it can also be used to inject the error template, as shown in the example above.
It is quite understandable that the CSS-containment of the Shadow DOM can come in the way of styling the custom element as per your need. It can be argued that this can be facilitated using CSS variables extensively. However, there is a far easy alternative to reach the same goal is offered by facilitating the customization of the whole template. To this end, use the SubscriberCustomElementTemplate configuration option.
There is another aspect of this configuration option. When a null, undefined, or '' (empty string) is used as the value for this configuration option, it deactivates the usage of this custom element. This is in sense similar to the UseSubscriberCustomAttribute configuration option.
ValidationResultPresenterService
Unlike the previous two approaches, this is a standalone service that manipulates the DOM directly. That it adds elements to DOM for every new errors and removes elements from DOM that are associated with old errors.
To use this, you need to instantiate it and register it with the validation controller.
import { IValidationController, IValidationResultPresenterService } from '@aurelia/validation';
export class MyApp {
public constructor(
@newInstanceForScope(IValidationController) private readonly validationController: IValidationController,
@IValidationResultPresenterService private readonly presenter: IValidationResultPresenterService;
) {
this.validationController.addSubscriber(this.presenter);
}
}
The error rendering process can be completely overridden in the child classes. The use methods for overriding are described below.
add: this adds a new error to DOM. Override this if you want to completely change the process of adding new errors.
remove: this removes an old error from the DOM. Override this if you want to completely change the process of removing old errors.
getValidationMessageContainer: As the name suggests it provides container element with respect to current target. The default behavior is to look for an element with the attribute validation-result-container that is contained by the parent element of the current target element. If there is none found a div is created with the attribute and appended to the parent element.
showResults: This is the method that appends the errors to the container. By default a span with the error message is added for every errors whereas the valid results are skipped.
To avoid direct DOM manipulation, it is highly encouraged to use the previously mentioned custom attribute, and custom element.
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
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 } from '@aurelia/kernel';
import { IValidationRules } from '@aurelia/validation';
import { IValidationController } from '@aurelia/validation-html';
export class MyApp {
public constructor(
@newInstanceForScope(IValidationController) readonly validationController: IValidationController,
@IValidationRules readonly validationRules: IValidationRules
) {}
}
@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 } from '@aurelia/kernel';
import { IValidationRules } from '@aurelia/validation';
import { IValidationController } from '@aurelia/validation-html';
export class MyApp {
public constructor(
@newInstanceForScope(IValidationController)
private validationController: IValidationController,
@IValidationRules validationRules: 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.
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.
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.
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).
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 } from '@aurelia/kernel';
import { IValidationRules } from '@aurelia/validation';
import { IValidationController } from '@aurelia/validation-html';
export class MyApp {
public constructor(
@newInstanceForScope(IValidationController)
private validationController: IValidationController,
@IValidationRules validationRules: 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.
I18n Internationalization
Display validation errors in other languages.
If you are already using the aurelia/i18n plugin, you would also naturally want localization support for validation. The package provides out-of-the-box localization support.
The plugin has a dependency on @aurelia/i18n package. It assumes that the @aurelia/i18n package is correctly registered/configured and uses the I18N services to provide the translations.
To add localization support, you must first register the plugin.
import Aurelia from 'aurelia';
import { ValidationI18nConfiguration } from '@aurelia/validation-i18n'; // <-- get the configuration
import { I18nConfiguration } from '@aurelia/i18n';
import { MyApp } from './my-app';
import * as en from "./locales/en.json";
import * as de from "./locales/de.json";
Aurelia
.register(
I18nConfiguration.customize((options) => { // <-- take care of I18N configuration as you see fit
options.initOptions = {
resources: {
en: { translation: en },
de: { translation: de },
}
};
}),
ValidationI18nConfiguration // <-- register the configuration
)
.app(MyApp)
.start();
Note that the @aurelia/validation-i18n wraps the @aurelia/validation plugin. Stated differently, it customizes the @aurelia/validation plugin with custom implementations of the following:
Validation controller factory ( see the ValidationControllerFactoryType customization option): This ensures that the validation controller reacts to locale change.
Message provider (see the MessageProviderType customization option): This ensures that localized error message templates and property names are used to create the error messages. With this place, the evaluation expr in withMessageKey(expr) is used as an I18N key, and the value is looked up in the I18N resources. This also happens for the display names of the properties, where the property name is used as the I18N key.
Check the demo to see this in action.
All the configuration options of the @aurelia/validation plugin are also available from @aurelia/validation-i18n. This means it even allows you to provide your implementation of a message provider or validation controller factory! Apart from that, it has two additional configuration options that dictate how the value of the I18N keys is looked up.
DefaultNamespace
By default, the value of the keys is searched in translation namespace. Using this configuration option that can be changed. This is useful if you want to separate the validation resources from your regular 18N resources. See the example below.
DefaultKeyPrefix
Instead of using a separate namespace, a key prefix can be used for the keys related to validation resources. This is shown in the example below.
Naturally, the DefaultNamespace and the DefaultKeyPrefix can be used together.
It should not come as any surprise that localization also works for model-based rules when appropriate messageKey is specified in the rule definition.
Validation Controller
So far, the functionalities of the @aurelia/validation have been discussed. The part regarding the integration with a view has been kept out of the discussion so far. This section starts addressing that.
The validation controller is the implementation of IValidationController interface. It acts as a bridge between the validator and the other related components, such as view, binding, and subscribers. The capabilities of the validation controller are discussed below.
Injecting a controller instance
An instance of the validation controller can be injected using the @newInstanceForScope(IValidationController), and the @IValidationController decorator. The @newInstanceForScope(IValidationController) decorator creates a new instance of the validation controller and registers the instance with the dependency injection container. This same instance can later be made available to the child components using the @IValidationController decorator.
// parent-ce.ts
import { customElement } from '@aurelia/runtime';
import { newInstanceForScope } from '@aurelia/kernel';
import { IValidationController } from '@aurelia/validation-html';
@customElement({name:'parent-ce', template:`<child-ce></child-ce>`})
export class ParentCe {
public constructor(
// new instance of validation controller; let us name it c1
@newInstanceForScope(IValidationController) private controller: IValidationController
) { }
}
// child-ce.ts
import { IValidationController } from '@aurelia/validation';
export class Parent {
public constructor(
// the c1 instance is injected here
@IValidationController private controller: IValidationController
) { }
}
The design decision is made keeping the following frequent use case in mind. The manual/final validation happens in the "root"/"parent" component/custom element. The child components, such as other custom elements, define the necessary validation rules at the custom element level, as well as uses the validate binding behavior to mark the validation targets in the view/markup. This helps show the validation messages near the validation targets.
Creating a new instance of the validation controller and registering the instance with the dependency injection container makes the same instance available to the child components level. The instance can then be used for registering the validation targets (see validate binding behavior), which makes it possible to execute all the validation rules defined in the children with a single instance of the controller.
A new instance of validation controller can always be injected using the @newInstanceOf(IValidationController) decorator. See this action in the demo below.
validate and reset
The validate method can be used to explicitly/manually perform the validation. The usage examples are as follows.
// validate all registered objects and bindings.
await validationController.validate();
// validate specific instruction
await validationController.validate(new ValidateInstruction(person));
await validationController.validate(new ValidateInstruction(person, 'name'));
This method is in essence similar to the validate method in validator. However, there are some differences. If the method is called with an instruction, the instruction is executed. Otherwise all the registered objects, as well as the registered bindings are validated. After the validation, all the registered subscribers are notified of the change. Refer the visual representation of the workflow to understand it better. To know more about ValidateInstruction refer this.
The reset method on the other hand removes the errors from the validation controller. It also has an optional argument of type ValidateInstruction which when provided instructs the controller to remove errors for specific object, and/or properties. Note that other properties of the instruction object has no effect on resetting the errors.
revalidateErrors
With the revalidateErrors method, verifying whether the current errors are still there is possible. It does not validate all objects and bindings, as it is done in validate method. It is useful when you don't want to get a new set of errors and rather check on the current status of the existing set of errors.
await validationController.revalidateErrors();
addObject and removeObject
The method addObject registers an object explicitly to the validation controller. The validation controller automatically validates the object every time the validate method is called. This is useful when you can validate some object in your view model that does not have any direct reference to the view.
The object can be unregistered by calling the removeObject method. This also removes the associated errors of the object.
Note that the errors added by the addError method, never gets revalidated when revalidateErrors is called. If the error needs to be removed, it must be done using removeError method.
addSubscriber and removeSubscriber
The subscribers can be added or removed using addSubscriber and removeSubscriber methods respectively. Whenever the validation controller performs validation or resets errors, the registered subscribers are notified of the change in validation results. To unsubscribe from the validation results notification, the subscriber needs to be removed.
The subscriber interface is rather simple, consisting of only one method.
The notification event data looks loosely like the following.
class ValidationEvent {
public kind: 'validate' | 'reset';
public addedResults: ValidationResultTarget[];
public removedResults: ValidationResultTarget[];
}
class ValidationResultTarget {
public result: ValidationResult;
public targets: Element[];
}
class ValidationResult<TRule extends BaseValidationRule = BaseValidationRule> {
public valid: boolean;
public message: string | undefined;
public propertyName: string | undefined;
public object: any;
public rule: TRule | undefined;
public propertyRule: PropertyRule | undefined;
// `true` if the validation result is added manually.
public isManual: boolean = false;
}
What the subscribers do with the event data depends on the subscribers. An obvious use case is to present the errors to the end users. In fact, the out-of-the-box subscribers are used for that purpose only. Below is one example of how you can create a custom subscriber.
Validation
This guide explains how to validate the user input for your app using the validation plugin. The plugin gives you enough flexibility to write your own rules rather than being tied to any external validation library.
Here's what you'll learn...
How to register and customize the plugin
How to define the validation rules
How to validate the data
How to apply model-based validation rules
Note If you have already used the aurelia-validation plugin previously and are migrating your existing Aurelia app to Aurelia vNext then jump straight to the migration guide.
Install and register the plugin
You need to install two packages to use Aurelia validation. The core validation plugin @aurelia/validation and an adapter. Currently, only one adapter is available for validating HTML-based applications.
Installation
npm i @aurelia/validation @aurelia/validation-html
Register
import { ValidationHtmlConfiguration } from '@aurelia/validation-html';
import Aurelia from 'aurelia';
Aurelia
.register(ValidationHtmlConfiguration)
.app(component)
.start();
Quick rundown
To use the validation plugin, all you have to do is inject the validation controller as well as the validation rules object to register validation rules.
import { newInstanceForScope } from '@aurelia/kernel';
import { IValidationRules } from '@aurelia/validation';
import { IValidationController } from '@aurelia/validation-html';
export class AwesomeComponent {
private person: Person; // Let us assume that we want to validate instance of Person class
public constructor(
@newInstanceForScope(IValidationController) private validationController: IValidationController,
@IValidationRules validationRules: IValidationRules
) {
this.person = new Person();
validationRules
.on(this.person)
.ensure('name')
.required()
.ensure('age')
.required()
.min(42);
}
public async submit() {
const result = await this.validationController.validate();
if(result.valid) {
// Yay!! make that fetch now
}
}
}
import { inject, resolve, newInstanceForScope } from '@aurelia/kernel'
import { IValidationRules } from '@aurelia/validation';
import { IValidationController } from '@aurelia/validation-html';
@inject(IValidationRules)
export class AwesomeComponent {
validationController = resolve(newInstanceForScope(IValidationController));
constructor(validationRules) {
this.person = new Person();
validationRules
.on(this.person)
.ensure('name')
.required()
.ensure('age')
.required()
.min(42);
}
async submit() {
const result = await this.validationController.validate();
if(result.valid) {
// Yay!! make that fetch now
}
}
}
Inside our HTML, we use the validate binding behavior to signal to Aurelia that we want to validate these bindings. You might notice that both name and age appear in our view model above where we set some rules up.
@newInstanceForScope(IValidationController) injects a new instance of validation controller which is made available to the children of awesome-component. More on validation controller later.
Demo
A playable demo can be seen below if you want to see the validation plugin in action. You can try adding new properties and playing around with the code to learn how to use the validation plugin.
Validate Binding Behavior
The validate binding behavior, as the name suggests, adds the validation behavior to a property binding. In other words, it "mark"s the associated property binding as a target for validation by registering the binding to the validation controller.
This is how the validation controller comes to know of the bindings that need to be validated when validationController.validate() method is called.
You must have noticed plenty examples of the validate binding behavior in the demos so far. For completeness, this can be used as follows.
Note that the binding behavior has three optional arguments: trigger, validation controller, and rules.
Validation trigger
This dictates when the validation is performed. The valid values are as follows.
manual: Use the validation controller's validate() method to validate all bindings.
blur: Validate the binding when the binding's target element fires a DOM "blur" event.
focusout: Validate the binding when the target element fires a DOM "focusout" event. This is useful when the actual input is wrapped in a custom element, and the validate binding behavior is used on the custom element. In that case, the blur trigger does not work as the blur event does not bubble. See the difference in action below.
change: Validate the binding when the source property is updated (usually triggered by some change in view).
changeOrBlur: Validate the binding when the binding's target element fires a DOM "blur" event and when the source property is updated.
changeOrFocusout: Validate the binding when the binding's target element fires a DOM "focusout" event and when the source property is updated.
There is an important point to note about the changeOrEVENT triggers. The change-triggered validation is ineffective till the associated property is validated once, either by manually calling ValidationController#validate or by event-triggered (blur or focusout) validation. This prevents showing a validation failure message immediately in case of an incomplete input, which might be the case if validation is triggered for every change.
Note the distinction made between incomplete and invalid input. The event-triggered validation is ineffective until the property is dirty, i.e. any changes were made. This prevents showing a validation failure message when there is a blur or focusout event without changing the property. This behavior delays bugging the user and "reward"s eagerly.
The examples above show an explicit usage of trigger. However, this is an optional value; when used, it overrides the default trigger configured. The default trigger is used for that instance when the value is omitted. The default validation trigger is focusout, although it can be changed using the DefaultTrigger registration customization option.
The binding behavior, by default, registers the binding to the closest (in terms of dependency injection container) available instance of the validation controller. Note that the validation controller instance can be made available for the scope using the @newInstanceForScope decorator (refer Injecting a controller instance for more details). If no instance of validation controller is available, it throws an error.
However, an instance of validation can be explicitly bound to the binding behavior, using the positional argument. This is useful when you use multiple instances of validation controllers to perform a different validation set.
In the example below, there are two injected controllers and the property person.age the validationController2 is used. Playing with the example, you can see that the person.age does not get validated by the scoped validation controller instance.
Performing validation on data models using Aurelia Validation.
It is a commonly known best practice to perform data validation both on the server and the client. Validating the data on the server reduces the coupling between the client and the server as then the service does not have to depend on the data quality, solely on the client.
On the other hand, client-side validation is equally important to ensure a better user experience so that the client can quickly provide feedback to the end users without making a round trip to the server. For this reason, it is often the case that the validation rules are defined on the server, and the client ends up duplicating those definitions.
With the support of model-based validation, @aurelia/validation plugin tries to reduce duplication. We assume the server can communicate the validation rules with the client in JSON data. The plugin uses an implementation of IValidationHydrator to adapt the JSON data to Aurelia validation rules. Let us see an example of this.
The first argument to the method can be a class or an object instance. The second argument must be an array of ModelBasedRule instances. This registers the rules for the target class or object instance. After this, the normal validation works as expected without any further changes.
The ModelBasedRule is a simple class that describes the ruleset definition or the JSON data that describes the validation rules.
export class ModelBasedRule {
public constructor(
public ruleset: any,
public tag: string = '__default'
) { }
}
The constructor of the class, as shown above, takes 2 arguments. The first is the ruleset. The second is an optional object tag (refer to the validate instruction). The ruleset, although typically a plain javascript object, can take any shape that is supported by the implementation of IValidationHydrator.
Default model-based ruleset schema
The out-of-the-box implementation of IValidationHydrator supports plain javascript objects with a specific schema. The expected schema is explained below.
{
"propertyName": {
// rule definition for this property
"displayName": "Optional display name for the property",
"rules": [ // <-- the rules needs to be an array
// the rules to be validated on parallel needs to go in one object
{
"ruleKey1": { // <-- for the out-of-the-box rule keys see later
// common properties
"messageKey": "optional message key",
"tag": "optional tag",
"when": "boolean > expression"|function() { return boolean; }, // see later
// rule specific properties, see later
},
"ruleKey2": { /*... */ }
},
/**
* multiple items in the `rules` array means that the subsequent set of rules won't be validated
* till the preceding rules are successfully validated.
* It has same effect of sequencing rules using `.then`
*/
{
"ruleKey11" : { /*... */ },
"ruleKey22" : { /*... */ },
}
]
},
"navigationProperty": {
"subProperty": {
"subSubProperty": { /* rule definition */ } // <-- rules for navigationProperty.subProperty.subSubProperty
}
}
}
The default implementation also supports defining all the out-of-the-box rules.
Rule
Key
Rule-specific properties
Required
required
None. Example: { required: { } }
Regex
regex
pattern: object describing a RegExp. Example: { regex: { pattern: { source: 'foo\\d', flag: 'gi' } } } is equivalent to /foo\d/gi
min: numeric; lower boundary, optional. max: numeric; upper boundary, optional. isInclusive: boolean; whether it is an inclusive or exclusive range, defaults to exclusive. Note that either of the min or max is required. Examples: { range: { isInclusive: true, min: 42 } }, { range: { max: 42 } }, { range: { min: 42, max: 84 } }.
Specifying a conditional rule by using a string value representing a boolean expression is also possible.
{ ruleKey: { when: "$object.age > 18" } }
Loosely speaking, the expression in when will be hydrated to this function expression: ($object) => $object.age > 18. Alternatively, if the ruleset is not a plain JSON but rather a javascript object, a function can be used as well.
Note that the second use case, as stated above, probably needs a completely new implementation of this interface, which is in its own merit out-of-the-scope of this documentation. To that end, you can easily subclass the default implementation to support your custom rule. Refer to the example and the demo below.
import { ModelValidationHydrator } from "@aurelia/validation";
export class CustomModelValidationHydrator extends ModelValidationHydrator {
protected hydrateRule(ruleName: string, ruleConfig: any): IValidationRule {
switch (ruleName) {
case 'customRule1': //<- handle custom rule hydration
return this.hydrateCustomRule1(ruleConfig);
// here goes more cases for other custom rules
default:
return super.hydrateRule(ruleName, ruleConfig);
}
}
private hydrateCustomRule1(ruleConfig: any) {
const rule = new CustomRule1(ruleConfig.ruleProperty1, ruleConfig.rulePropertyN);
this.setCommonRuleProperties(ruleConfig, rule);
return rule;
}
}
import Aurelia from 'aurelia';
import { ValidationConfiguration } from '@aurelia/validation';
import { MyApp } from './my-app';
import { CustomModelValidationHydrator } './custom-model-validation-hydrator';
Aurelia
.register(
ValidationConfiguration.customize((options) => {
options.HydratorType = CustomModelValidationHydrator; // <-- register the hydrator
})
)
.app(MyApp)
.start();
Defining & Customizing Rules
Creating and customing Aurelia Validation rules to ensure data is validated.
Let us also consider the following Person class, and we want to define validation rules for this class or instance of this class.
export interface Address {
line1: string;
line2?: string;
city: string;
pin: number;
}
export class Person {
public constructor(
public name: string,
public age: number,
public email: string,
public pets: string[],
public address: Address,
) { }
}
To define rules use the IValidationRules fluent API. In order to do that you need to use the @IValidationRules constructor parameter decorator which will inject an transient instance of IValidationRules object. This is shown in the following example.
import { IValidationRules } from '@aurelia/validation';
export class AwesomeComponent {
public constructor(
@IValidationRules validationRules: IValidationRules
) { }
}
The fluent API syntax has following parts.
Start applying ruleset on a target using .on. The target can be an object instance or class.
Select a property from the target using .ensure.
Associate rules with the property using .required, .matches etc.
Customize the property rules using .wthMessage, .when etc.
Specify validation target using .on
Be it is an object instance or class, both can be used as validation target using .on.
const person: Person = new Person(...);
validationRules
.on(person);
validationRules
.on(Person);
Specifying the target serves two purposes. Firstly, this initiates an empty collection of rules (ruleset) for the target. Secondly, this helps providing the typing information from the target to the subsequent methods which in turn provides with intellisense for the property names (see next section).
Specifying target property for validation using .ensure
The .ensure method can be use used select a property of the target for validation. This adds an instance of PropertyRule to the ruleset for the object. The property can be defined using a string or an arrow function expression.
validationRules
.on(person)
.ensure('name') // string literal
//...
.ensure((p) => p.age) // arrow function expression
//...
.ensure("address.line1") // nested property using string literal
//...
.ensure((p) => address.line2) // nested property using an arrow function expression
//...
With TypeScript support, intellisense is available for both the variants.
Associating validation rules with property
After selecting a property with .ensure the next step is to associate rules. The rules can be built-in or custom. Irrespective of what kind of rule it is, at the low-level it is nothing but an instance of the rule class. For example, the "required" validation is implemented by the RequiredRule class. This will be more clear when you will define custom validation rules. However, let us take a look at the built-in rules first.
required
Considers the property to be valid if the value is not null, and not undefined. In case of string, it must not be empty.
This instantiates a RequiredRule for the property.
Note that this is the only built-in rule that considers null, undefined, or empty string as invalid value. The other built-in rules purposefully consider null, undefined, or empty string as valid value. This is done to ensure single responsibility for the built-in rules.
matches
Considers the string property to be valid if the value matches the given pattern described by a regular expression.
validationRules
.on(person)
.ensure('name')
.matches(/foo/); // name is valid if it contains the string 'foo'
This instantiates a RegexRule for the property.
email
This also instantiates a RegexRule for the property, but with a specific regex for matching emails.
validationRules
.on(person)
.ensure('email')
.email(); // person's email need to be valid
minLength
Considers the string property to be valid if the value is at least of the specified length. Under the hood, it instantiates a LengthRule with minimum length constraint.
validationRules
.on(person)
.ensure('name')
.minLength(42); // name must be at least 42 characters long
maxLength
Considers the string property to be valid if the value is at most of the specified length. Under the hood, it instantiates a LengthRule with maximum length constraint.
validationRules
.on(person)
.ensure('name')
.maxLength(42); // name must be at most 42 characters long
minItems
Considers the collection (array) property to be valid if the array has at least the number of items specified by the constraint. Under the hood, it instantiates a SizeRule with minimum size constraint.
validationRules
.on(person)
.ensure('pets')
.minItems(42); // a person should have at least 42 pets
maxItems
Considers the collection (array) property to be valid if the array has at most the number of items specified by the constraint. Under the hood, it instantiates a SizeRule with maximum size constraint.
validationRules
.on(person)
.ensure('pets')
.maxItems(42); // a person should have at most 42 pets
min
Considers the numeric property to be valid if the value is greater than or equal to the given lower bound. Under the hood, it instantiates a RangeRule with [min,] interval (if your unfamiliar with the interval notation, you can refer this.
validationRules
.on(person)
.ensure('age')
.min(42); // a person should be at least 42 years old
max
Considers the numeric property to be valid if the value is less than or equal to the given upper bound. Under the hood, it instantiates a RangeRule with [,max] interval (if your unfamiliar with the interval notation, you can refer this.
validationRules
.on(person)
.ensure('age')
.max(42); // a person should be at most 42 years old
range
Considers the numeric property to be valid if the value is greater than or equal to the given lower bound and less than or equal to the given upper bound. Under the hood, it instantiates a RangeRule with [min,max] interval (if your unfamiliar with the interval notation, you can refer this.
validationRules
.on(person)
.ensure('age')
.range(42, 84); // a person's age should be between 42 and 84 or equal to these values
between
Considers the numeric property to be valid if the value is strictly greater than the given lower bound and strictly less than the given upper bound. If the value matches any of the boundary value, it is considered invalid. Under the hood, it instantiates a RangeRule with (min,max) interval (if your unfamiliar with the interval notation, you can refer this.
validationRules
.on(person)
.ensure('age')
.between(42, 84); // a person's age should be between 42 and 84, but cannot be equal to any these values
equals
Considers the property to be valid if the value is strictly equal to the expected value. Under the hood, it instantiates a EqualsRule.
validationRules
.on(person)
.ensure('name')
.equals('John Doe'); // Only people named 'John Doe' are valid
Have you noticed that the same rule implementation is "alias"ed for multiple validation rules? You will get know another aspect of aliasing rules in the customizing validation messages section.
Custom rules
There are two ways custom rules can be defined.
satisfies
This is the easiest way of defining a custom rule using an arrow function that has loosely the following signature.
The value of the first argument provides the value being validated, whereas the value of the second argument provides the containing object. You can use it as follows.
// Let us assume that we do not want to accept "John Doe"s as the input for name
const testNames = [ "John Doe", "Max Mustermann" ];
validationRules
.on(person)
.ensure('name')
.satisfies((name) => !testNames.includes(name));
This is useful for the rules that are used only in one place. For example, in one of view-model you need to apply a very specific rule that is not needed elsewhere. However, if you want to reuse you rule, then you need to use the satisfiesRule.
satisfiesRule
This lets reuse a rule implementation. For this we need to remember two things. Firstly, as mentioned before at the start of this section any rule can be applied on a property just by instantiating the rule, and associating with the property. Secondly, every rule needs to be a subtype of BaseValidationRule, as discussed in before.
The method satisfiesRule accepts such an instance of a rule implementation and associates it with the property. It can be used as follows.
You must have noticed that the API for the built rules instantiates a rule implementation. For example, the following two are synonymous.
validationRules
.on(person)
.ensure('name')
.required();
// same can be done by this as well
import { RequiredRule } from "@aurelia/validation";
validationRules
.on(person)
.ensure('name')
.satisfiesRule(new RequiredRule());
Let us look at one last example before moving to next section. The following example implements a integer range rule by inheriting the RangeRule.
import { RangeRule } from "@aurelia/validation";
class IntegerRangeRule extends RangeRule {
public execute(value: any, object?: IValidateable): boolean {
return value === null
|| value === undefined
|| (Number.isInteger(Number(value))
&& (this.isInclusive
? value >= this.min && value <= this.max
: value > this.min && value < this.max
));
}
}
//...
validationRules
.on(person)
.ensure(age)
.satisfiesRule(new IntegerRangeRule(true, { min:42, max: 84 })); // the age must between 42 and 84 (inclusive) and must be an integer.
In the light of using rule instance, note that the the lambda in satisfies is actually wrapped in an instance of anonymous subclass of BaseValidationRule.
Defining rules for multiple objects
Rules on multiple objects can be defined by simply using the API in sequence for multiple objects. An example is shown below.
Note that there is no limitation on how many times on is applied on an object or in what order. The following is a perfectly valid rule definition, although such definition can be difficult to understand.
In this section you will learn about how to customize the rule definition. A common use-case of that will be to customize the validation messages, as in most of the cases, the default validation messages might not fit the real-life requirements. Apart from that this section also discusses further ways to change the chaining of the rules.
Customize the display name
By default, the display name of the property is computed by splitting on capital letters and capitalizing the first letter of the property name. For example, the property named age will appear as "Age" in the validation messages for age, or firstName becomes "First Name", etc. It is quite evident that this is limiting in a sense. However, the display name of the property can easily be changed using .displayName.
validationRules
.on(person)
.ensure("address.line1")
.displayName("First line of address")
.required(); // results in validation message: "First line of address is required."
Note that instead of a literal string, a function can also be used to customize the display name. The function signature is () => string; The following example shows a use case for this where the attempt for a guessing game is validated, and the attempt count is increased with each attempt and shown in the validation message. Note that some parts of the example is not yet discussed, but those will be addressed in respective sections.
Customize the validation message
Apart from customizing the display name, you can in fact customize the whole message. Messages can be customized on a per-instance basis or globally. Let us first consider the per-instance based customization.
For this, we need to use the withMessage method. The example below shows how it can be used to define different messages for different rule instances.
validationRules
.on(person)
.ensure("address.line1")
.required()
.withMessage("Enter the address line1 to continue.")
.maxLength(7)
.withMessage("The address line1 is too long.");
The above examples shows usage of string literal as custom message. A message template can also be used instead. The expressions supported in the template are as follows.
$object: The object being validated. Note that any property of the object can thus be accessed in the template.
$value: The value being validated.
$displayName: The display name of the property.
$propertyName: The name of the property.
$rule: The associated rule instance. This is useful to access the properties of the rule instance. For example, you have a custom validation rule, and you want to access the value of some of your rule property in the validation message, this property is what you need to use. This is used in the messages of RangeRule to access the "min", and "max" values of the rule instance.
$getDisplayName: It is a function that returns the display name of another given property. This is useful if you want to create a message associating another property.
Let us look at the following example, to understand these better.
Apart from this the messages the can be customized globally. You must have noted that same rule implementations are aliased quite frequently. The same concept is used here as well.
The messages can be customized globally during registering the plugin, using the CustomMessages property of the configuration object.
import { RequiredRule, RangeRule, } from '@aurelia/validation';
import { ValidationHtmlConfiguration } from '@aurelia/validation-html';
import Aurelia from 'aurelia';
const customMessages: ICustomMessage[] = [
{
rule: RequiredRule,
aliases: [
{ name: 'required', defaultMessage: `\${$displayName} is non-optional.` }
],
},
{
rule: RangeRule,
aliases: [
{ name: 'min', defaultMessage: `\${$displayName} should be at least \${$rule.min}.` },
{ name: 'max', defaultMessage: `\${$displayName} should be at most \${$rule.max}.` },
{ name: 'range', defaultMessage: `\${$displayName} should be between or equal to \${$rule.min} and \${$rule.max}.` },
{ name: 'between', defaultMessage: `\${$displayName} should be between but not equal to \${$rule.min} and \${$rule.max}.` },
],
},
// ...
];
Aurelia
.register(
ValidationHtmlConfiguration
.customize((options) => {
options.CustomMessages = customMessages;
})
)
.app(component)
.start();
You are encouraged to play with the following demo; define more rules, change the custom messages, etc. to see it in action.
Following is the complete list of default messages for the out of the box validation rules.
RequiredRule
Alias
Message template
required
${$displayName} is invalid.
RegexRule
Alias
Message template
matches
${$displayName} is not correctly formatted.
email
${$displayName} is not a valid email.
LengthRule
Alias
Message template
minLength
${$displayName} must be at least ${$rule.length} character${$rule.length === 1 ? '' : 's'}.
maxLength
${$displayName} cannot be longer than ${$rule.length} character${$rule.length === 1 ? '' : 's'}.
SizeRule
Alias
Message template
minItems
${$displayName} must contain at least ${$rule.count} item${$rule.count === 1 ? '' : 's'}.
maxItems
${$displayName} cannot contain more than ${$rule.count} item${$rule.count === 1 ? '' : 's'}.
RangeRule
Alias
Message template
min
${$displayName} must be at least ${$rule.min}.
max
${$displayName} must be at most ${$rule.max}.
range
${$displayName} must be between or equal to ${$rule.min} and ${$rule.max}.
between
${$displayName} must be between but not equal to ${$rule.min} and ${$rule.max}.
EqualsRule
Alias
Message template
equals
${$displayName} must be ${$rule.expectedValue}.
Note that a new key can also be added to any out of the box rule, and can be referred from the code using withMessageKey.
Then you can refer the second message by using .withMessageKey('key2'). Refer the demo below, to see this in action.
Conditional Rule
Often you would want to execute a rule conditionally. This can be done using the .when method. This method takes a function, with signature (object: any) => boolean, as input which is later evaluated during rule evaluation to decide whether or not to execute this rule. The object in the argument of the function is the object being validated.
validationRules
.on(person)
.ensure("guardianName")
.required()
.when((p) => p.age < 18 ); // guardianName is required for a minor
Sequencing Rules
When multiple rules are defined on a property, the rules are all executed on parallel. Using then, the rules can be chained and executed serially. This means that if the first rule fails, the second rule is not executed. A common example is shown below.
Assuming there is an implementation of UniqueRule that validates the data against the records, existing in data store/backend service. Such rules can be expensive in nature, and thus it makes sense to execute those when all other preconditions are validated. In the above example if either of the required or email rules fails, the UniqueRule will never be executed. Verify this in the demo shown below.
Validating object
So far we have seen how to define validation rules on properties. Validation rules can also be applied on an object using ensureObject, and validate the object as a whole.
In all the demos so far we have seen usage of validation controller. Under the hood, validation controller uses the IValidator API to perform the validation.
It has only a single method called validate, which accepts an instruction that describes what needs to be be validated.
The plugin ships a standard implementation of the IValidator interface. This can be injected to manually perform the validation on objects. Note that validator is the core component that executes the validation rules without any connection with the view. This is the main difference between validator and validation controller. The following example shows how to use it.
export class AwesomeComponent {
private person: Person;
private errors: string[];
public constructor(
@IValidator private validator: IValidator,
@IValidationRules validationRules: IValidationRules
) {
this.person = new Person();
validationRules
.on(this.person)
.ensure("name")
.required();
}
public async submit(){
const result = await this.validator.validate(new ValidateInstruction(this.person));
console.log(result);
this.errors = result.filter((r) => !r.valid).map((r) => r.message);
}
}
An important aspect of the demo above is that it shows how to use @aurelia/validation without the @aurelia/validation-html.
Let us now focus on the ValidateInstruction, which basically instructs the validator, on what to validate. The instruction can be manipulated using the following optional class properties.
object: The object to validate.
propertyName: The property name to validate.
rules: The specific rules to execute.
objectTag: When present instructs to validate only specific ruleset defined for a object. Tagging is discussed in detail in the respective section
propertyTag: When present instructs to validate only specific ruleset for a property. Tagging is discussed in detail in the respective section
Some of the useful combinations are as follows.
object
propertyName
rules
objectTag
propertyTag
Details
✔
The default ruleset defined on the instance or the class are used for validation.
✔
✔
Only the rules defined for the particular property are used for validation.
✔
✔
-
Only the specified rules are used for validation.
✔
✔
✔
-
Only the specified rules that are associated with the property are used for validation.
✔
✔
Only the tagged ruleset for the object is used for validation.
✔
✔
✔
Only the rules for the property in the tagged ruleset are used for validation.
✔
✔
✔
✔
Only the tagged rules for the property in the tagged ruleset for the object are validated.
Note that in the presence of rules the objectTag is ignored. However, we strongly encourage the usage of tags for executing specific set of rules. You can find more details on tagging in Tagging rules section. Note that the validate instruction is also respected by validation controller.