Introduction
This article covers the basics of validation with Aurelia's validation plugin. You'll learn how to add validation to your applications using a fluent rule API and minimal changes to your templates.
To get started you'll need to install aurelia-validation
using npm install aurelia-validation
or jspm install aurelia-validation
. Afterwards, add .plugin(PLATFORM.moduleName('aurelia-validation'))
to the configuration in your main.js
to ensure the plugin is loaded at application startup.
Defining Rules
Aurelia Validation's standard rule engine uses a fluent syntax to define a set of rules. There are five parts to the syntax:
- Selecting a property using
.ensure
- Associating rules with the property using
.required
,.matches
, etc - Customizing property rules using
.withMessage
,.when
, etc - Sequencing rules using
.then
- Applying the ruleset to a class or instance using
.on
ensure
To begin defining a ruleset, use the ValidationRules
class. Start by targeting a property using ValidationRules.ensure(...)
. The ensure
method accepts one argument representing the property name. The argument can be a string or a simple property access expression. If you're using TypeScript you'll probably want to use a property access expression because you'll get the benefit of intellisense, refactoring and avoid using "magic strings" that can be a maintenance issue.
ValidationRules
.ensure('firstName')...
ValidationRules
.ensure(p => p.firstName)...
ValidationRules
.ensure('firstName')...
ValidationRules
.ensure((p: Person) => p.firstName)...
displayName
Once you've targetted a property using ensure
you can define the property's display name using .displayName(name: string|ValidationDisplayNameAccessor)
. Display names are used in validation messages. Specifying a display name is optional. If you do not explicitly set the display name the validation engine will attempt to compute the display name for you by splitting the property name on upper-case letters. A firstName
property's display name would be First Name
.
ValidationRules
.ensure('ssn').displayName('SSN')...
Applying Rules
After targeting a property with ensure
and optionally setting its display name you can begin associating rules with the property using the built-in rule methods:
required()
validates the property is not null, undefined or whitespace.matches(regex)
validates the property matches the specified regular expression.email()
validates an email address.minLength(length)
andmaxLength(length)
validate the length of string properties.minItems(count)
andmaxItems(count)
validate the number of items in an array.min(constaint)
,max(constraint)
,range(min, max)
, andbetween(min, max)
validate the value of number properties.equals(expectedValue)
validates the property equals the expected value.satisfies((value: any, object?: any) => boolean|Promise<boolean>)
validates the supplied function returnstrue
or aPromise
that resolves totrue
. The function will be invoked with two arguments:- the property's current value.
- the object the property belongs to.
withMessage
All rules have a standard message that can be overriden on a case-by-case basis using .withMessage(message)
. The message
argument is a string that will be interpreted as an interpolation binding expression and evaluated against the validated object when a validation error occurs. The interpolation binding expression can access any of the object's properties as well as the contextual properties listed below:
$displayName
: the display name of the property.$propertyName
: the name of the property.$value
: the property value (at the moment the validation rule was executed).$object
: the object that owns the property.$config
: an object containing the rule's configuration. For example, the config for aminLength
rule will have alength
property.$getDisplayName
: returns a displayable name of a property given the property name (irrespective of the property's displayName), split on capital letters, first letter ensured to be capitalized.
Here's an example:
ValidationRules
.ensure('ssn').displayName('SSN')
.required().withMessage(`\${$displayName} cannot be blank.`);
.matches(/\d{3}-\d{2}-\d{4}/).withMessage(`"\${$value}" is not a valid \${$displayName}.`);
withMessageKey
Another way to override messages on a case-by-case basis is to use the .withMessageKey(key)
fluent API. Key is a string representing a key in the validationMessages
dictionary. You can add new keys to the dictionary using the following code:
import {validationMessages} from 'aurelia-validation';
validationMessages['invalidAirportCode'] = `\${$displayName} is not a valid airport code.`;
ValidationRules
.ensure('airportCode')
.matches(/^[A-Z]{3}$/).withMessageKey('invalidAirportCode')...
Conditional Validation
You may run into situations where you only want a rule to be evaluated when certain conditions are met. Use the .when(condition: (object) => boolean)
fluent API to define a condition that must be met before the rule is evaluated. when
accepts one argument, a function that takes the object and returns a boolean that indicates whether the rule should (true
) or should not (false
) be evaluated.
ValidationRules
.ensure('email')
.email()
.required()
.when(order => order.shipmentNotifications)
.withMessage('Email is required when shipment notifications have been requested.');
Sequencing Rule Evaluation
Rules are evaluated in parallel. Use the .then()
method to postpone evaluation of a rule until after the preceding rules in the ensure
have been evaluated.
ValidationRules
.ensure('email')
.email()
.required()
.then()
.satisfiesRule('emailNotAlreadyRegistered')
.ensure('username')
.required()
.minLength(3)
.maxLength(50)
.then()
.satisfiesRule('usernameNotInUse');
In the example above, the emailNotAlreadyRegistered
custom rule will only be evaluated when the email
property passes the required()
and email()
validations. Likewise, usernameNotInUse
will be evaluated only when the required()
, minLength(3)
and maxLength(50)
checks pass validation.
Tagging Rules
Use the .tag(tag: string)
method to tag a specific property rule with a name. You can retrieve rules with a specific tag using let someRules = ValidationRules.taggedRules(rules, tag)
. To get only rules that have no tag applied to them you can use let someRules = ValidationRules.untaggedRules(rules)
. This can come in handy when you want to execute a specific rule or subset of rules. The documentation for the ValidationController (below) shows how to validate specific objects/properties/rules. You can also use the subset of rules with the Validator API (also documented below).
on
Once your ruleset has been defined you can apply them to a class using the .on
method. This will ensure the validation engine can locate the rules when evaluating a particular object.
export class Person {
firstName = '';
lastName = '';
}
ValidationRules
.ensure('firstName').required()
.ensure('lastName').required()
.on(Person);
.on
can apply the rules to a plain JavaScript object as well:
let patient = {
firstName: '',
lastName: ''
};
ValidationRules
.ensure('firstName').required()
.ensure('lastName').required()
.on(patient);
There is no requirement to apply the rules directly to an object or class, you can capture the ruleset in a variable or property using .rules
instead:
const personRules = ValidationRules
.ensure('firstName').required()
.ensure('lastName').required()
.rules;
Customizing Messages
The previous section showed how to customize the message of an individual property rule. You can override messages system-wide by replacing a rule's default message in the validationMessages
dictionary:
import {validationMessages} from 'aurelia-validation';
validationMessages['required'] = `\${$displayName} is missing!`;
You can override the ValidationMessageProvider
's getMessage(key: string): Expression
method to enable more dynamic message logic:
import {ValidationMessageProvider} from 'aurelia-validation';
ValidationMessageProvider.prototype.standardGetMessage = ValidationMessageProvider.prototype.getMessage;
ValidationMessageProvider.prototype.getMessage = function(key) {
const translation = i18next.t(key);
return this.parser.parse(translation);
};
You can also override the ValidationMessageProvider
's getDisplayName(propertyName: string, displayName: string): string
method:
import {ValidationMessageProvider} from 'aurelia-validation';
ValidationMessageProvider.prototype.getDisplayName = function(propertyName, displayName) {
if (displayName !== null && displayName !== undefined) {
return displayName;
}
return i18next.t(propertyName);
};
Validation Controller
The ValidationController
orchestrates the UI process of validating properties in response to various triggers and surfacing validation errors via renderers. Typically you'll have one validation controller instance per "form" view model. Depending on the use-case you may have multiple.
Creating a Controller
Validation controllers can be created using the NewInstance
resolver:
import {inject, NewInstance} from 'aurelia-dependency-injection';
import {ValidationController} from 'aurelia-validation';
@inject(NewInstance.of(ValidationController))
export class RegistrationForm {
controller = null;
constructor(controller) {
this.controller = controller;
}
}
Or with the ValidationControllerFactory
:
import {ValidationControllerFactory} from 'aurelia-validation';
@inject(ValidationControllerFactory)
export class RegistrationForm {
controller = null;
constructor(controllerFactory) {
this.controller = controllerFactory.createForCurrentScope();
}
}
import {autoinject} from 'aurelia-dependency-injection';
import {ValidationControllerFactory, ValidationController} from 'aurelia-validation';
@autoinject
export class RegistrationForm {
controller: ValidationController;
constructor(controllerFactory: ValidationControllerFactory) {
this.controller = controllerFactory.createForCurrentScope();
}
}
Both techniques create a new instance of a ValidationController
and register the instance in the component's container enabling other components in the validation library to access the approriate controller instance without needing a lot of boilerplate code or markup.
If you'd like to be completely explicit when wiring up controllers with view models and bindings, or if you need to use multiple controllers in your component, you can use the Factory
resolver or the ValidationControllerFactory
's create
method to create controller instances. Using these approaches will not automatically register the controller instance in the container which will prevent the automatic wire-up of controllers with bindings and renderers and will force you to specify the controller instance in your bindings and add renderers to the controller manually.
Setting the Validate Trigger
Once you've created a controller you can set its validationTrigger
to either blur
, focusout
, change
, changeOrBlur
, changeOrFocusout
, or manual
. The default is blur
which means the validation controller will validate the property accessed in a binding when the binding's associated element "blurs" (loses focus).
When the trigger is change
, each change the binding makes to the model property will trigger validation of the property. Use the throttle
, debounce
and updateTrigger
binding behaviors in conjunction with the change
validate trigger to customize the behavior.
Use the manual
trigger to indicate the controller should not automatically validate properties used in bindings. Errors will only be displayed when you invoke the controller's validate
method and will be cleared when you invoke the controller's reset
method.
import {ValidationControllerFactory, validateTrigger} from 'aurelia-validation';
@inject(ValidationControllerFactory)
export class RegistrationForm {
controller = null;
constructor(validationControllerFactory) {
this.controller = validationControllerFactory.createForCurrentScope();
this.controller.validateTrigger = validateTrigger.manual;
}
}
Changing the default Validate Trigger
You can configure the default validation trigger that every controller uses without having to change each controller yourself. You can access the validation configuration when you register the validation plugin during bootstrapping.
import {validateTrigger} from "aurelia-validation";
export function configure(aurelia) {
aurelia.use.plugin("aurelia-validation", config => {
config.defaultValidationTrigger(validateTrigger.manual);
});
}
import {GlobalValidationConfiguration, validateTrigger} from "aurelia-validation";
export function configure(aurelia) {
aurelia.use.plugin("aurelia-validation", (config: GlobalValidationConfiguration) => {
config.defaultValidationTrigger(validateTrigger.manual);
});
}
validate & reset
You can force the validation controller to run validation by invoking the validate()
method. Validate will run the validation, render the results and return a Promise
that resolves with a ControllerValidateResult
instance. The promise will only reject when there is an unexpected application error. Be sure to catch these rejections like you would any other unexpected application error.
Invoking the validate method with no arguments will validate all bindings and objects registered with the controller. You can supply a validate instruction to limit the validation to a specific object, property and ruleset:
controller.validate();
controller.validate({ object: person });
controller.validate({ object: person, rules: myRules });
controller.validate({ object: person, propertyName: 'firstName' });
controller.validate({ object: person, propertyName: 'firstName', rules: myRules });
controller.validate()
.then(result => {
if (result.valid) {
// validation succeeded
} else {
// validation failed
}
});
Most of the time you will use the ControllerValidateResult
instance's valid
property to determine whether validation passed or failed. Use the results
property to access the ValidateResult
for every rule that was evaluated by the controller.validate(...)
call. Each ValidateResult
has it's own rule
and valid
properties that will tell you whether a particular rule passed or failed, along with message
, object
and propertyName
properties.
The opposite of the validate
method is reset
. Calling reset with no arguments will unrender any previously rendered validation results. You can supply a reset instruction to limit the reset to a specific object or property:
controller.reset();
controller.reset({ object: person });
controller.reset({ object: person, propertyName: 'firstName' });
addError & removeError
You may need to surface validation errors from other sources. Perhaps while attempting to save a change the server returned a business rule error. You can display the server error using the controller's addError(message: string, object: any, propertyName?: string): ValidateResult
method. The method returns a ValidateResult
instance which can be used to unrender the error using removeError(result: ValidateResult)
.
addRenderer & removeRenderer
The validation controller renders errors by sending them to implementations of the ValidationRenderer
interface. The library ships with a built-in renderer that "renders" the errors to an array property for data-binding/templating purposes. This is covered in the
displaying errors
section below. You can create your own
custom renderer
and add it to the controller's set of renderers using the addRenderer(renderer)
method.
Events
The validation controller has a subscribe(callback: (event: ValidateEvent) => void)
method you can use to subscribe to validate and reset events. Callbacks will be invoked whenever the controller's validate and reset methods are called. Callbacks will be passed an instance ValidateEvent
which contains properties you can use to determine the overall validity state as well as the result of the validate or reset invocation. Refer to the API docs for more info.
Validator
Validator
is an interface used by the ValidationController
to do the behind-the-scenes work of validating objects and properties. The aurelia-validation
plugin ships with an implementation of this interface called the StandardValidator
, which knows how to evaluate rules created by aurelia-validation
's fluent API. When you use a Validator
directly to validate a particular object or property, there are no UI side-effects- the validation results are not sent to the the validation renderers.
Creating a Validator
Validators can be injected:
import {inject} from 'aurelia-dependency-injection';
import {Validator} from 'aurelia-validation';
@inject(Validator)
export class RegistrationForm {
validator = null;
constructor(validator) {
this.validator = validator;
}
}
Use the Validator instance's validateObject
and validateProperty
methods to run validation without any render side-effects. These methods return a Promise
that resolves with an array of ValidateResults
.
Validate Binding Behavior
The validate
binding behavior enables quick and easy validation for two-way data-bindings. The behavior registers the binding instance with a controller, enabling the system to validate the binding's associated property when the validate trigger occurs (blur / change). The binding behavior is able to identify the object and property name to validate in all sorts of binding expressions:
<input type="text" value.bind="firstName & validate">
<input type="text" value.bind="person.firstName & validate">
<input type="text" value.bind="person['firstName'] | upperCase & validate">
<input type="text" value.bind="currentEntity[p] & debounce & validate">
validate
accepts a couple of optional arguments enabling you to explicitly specify the rules and controller instance:
<input type="text" value.bind="firstName & validate:personController">
<input type="text" value.bind="firstName & validate:personRules">
<input type="text" value.bind="firstName & validate:personController:personRules">
The validate
binding behavior obeys the associated controller's validateTrigger
(blur
, focusout
, change
, changeOrBlur
, changeOrFocusout
, manual
). If you'd like to use a different validateTrigger
in a particular binding use one of the following binding behaviors in place of & validate
:
& validateOnBlur
: the DOM blur event triggers validation.& validateOnFocusout
: the DOM focusout event triggers validation.& validateOnChange
: data entry that changes the model triggers validation.& validateOnChangeOrBlur
: DOM blur or data entry triggers validation.& validateOnChangeOrFocusout
: DOM focusout or data entry triggers validation.& validateManually
: the binding is not validated automatically when the associated element is blurred or changed by the user.
Note: As of v2.x of this plugin, there has been a change in the behavior of
validateOnchangeOrEVENT
triggers. The change-triggered validation is ineffective till the associated property is validated once, either by manually callingValidationController#validate
or by event-triggered (blur or focusout) validation. This prevents showing 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 to the property. This prevents showing a validation failure message when there is a blur or focusout event without changing the property.
Displaying Errors
The controller exposes properties that are useful for creating error UIs using standard Aurelia templating techniques:
results
: An array of the currentValidateResult
instances. These are the results of validating individual rules.errors
: An array of the currentValidateResult
instances whosevalid
property is false.validating
: a boolean that indicates whether the controller is currently executing validation.
Assuming your view-model had a controller property you could add a simple error summary to your form using a repeat:
<form>
<ul if.bind="controller.errors">
<li repeat.for="error of controller.errors">
${error.message}
</li>
</ul>
...
...
</form>
To build more sophisticated error UIs you might need a list of errors specific to a particular binding or set of bindings. The validation-errors
custom attribute creates an array containing all validation errors relevant to the element the validation-errors
attribute appears on and its descendent elements. Here's an example using bootstrap style form markup:
<form>
<div class="form-group" validation-errors.bind="firstNameErrors"
class.bind="firstNameErrors.length ? 'has-error' : ''">
<label for="firstName">First Name</label>
<input type="text" class="form-control" id="firstName"
placeholder="First Name"
value.bind="firstName & validate">
<span class="help-block" repeat.for="errorInfo of firstNameErrors">
${errorInfo.error.message}
</span>
</div>
<div class="form-group" validation-errors.bind="lastNameErrors"
class.bind="lastNameErrors.length ? 'has-error' : ''">
<label for="lastName">Last Name</label>
<input type="text" class="form-control" id="lastName"
placeholder="Last Name"
value.bind="lastName & validate">
<span class="help-block" repeat.for="errorInfo of lastNameErrors">
${errorInfo.error.message}
</span>
</div>
</form>
This first form-group div uses the validation-errors
custom attribute to create a firstNameErrors
property. When there are items in the array the bootstrap has-error
class is applied to the form-group div. Each error message is displayed below the input using help-block
spans. The same approach is used to display the lastName field's errors.
The validation-errors
custom attribute implements the ValidationRenderer
interface. Instead of doing direct DOM manipulation to display the errors it "renders" the errors to an array property to enable the data-binding and templating scenarios illustrated above. It also automatically adds itself to the controller using addRenderer
when its "bind" lifecycle event occurs and removes itself from the controller using the removeRenderer
method when its "unbind" composition lifecycle event occurs.
Custom Renderers
The templating approaches described in the previous section may require more markup than you wish to include in your templates. If you would prefer use direct DOM manipulation to render validation errors you can implement a custom renderer.
Custom renderers implement a one-method interface: render(instruction: RenderInstruction)
. The RenderInstruction
is an object with two properties: the results to render and the results to unrender. Below is an example implementation for bootstrap forms:
import {
ValidationRenderer,
RenderInstruction,
ValidateResult
} from 'aurelia-validation';
export class BootstrapFormRenderer {
render(instruction: RenderInstruction) {
for (let { result, elements } of instruction.unrender) {
for (let element of elements) {
this.remove(element, result);
}
}
for (let { result, elements } of instruction.render) {
for (let element of elements) {
this.add(element, result);
}
}
}
add(element: Element, result: ValidateResult) {
if (result.valid) {
return;
}
const formGroup = element.closest('.form-group');
if (!formGroup) {
return;
}
// add the has-error class to the enclosing form-group div
formGroup.classList.add('has-error');
// add help-block
const message = document.createElement('span');
message.className = 'help-block validation-message';
message.textContent = result.message;
message.id = `validation-message-${result.id}`;
formGroup.appendChild(message);
}
remove(element: Element, result: ValidateResult) {
if (result.valid) {
return;
}
const formGroup = element.closest('.form-group');
if (!formGroup) {
return;
}
// remove help-block
const message = formGroup.querySelector(`#validation-message-${result.id}`);
if (message) {
formGroup.removeChild(message);
// remove the has-error class from the enclosing form-group div
if (formGroup.querySelectorAll('.help-block.validation-message').length === 0) {
formGroup.classList.remove('has-error');
}
}
}
}
To use a custom renderer you'll need to instantiate it and pass it to your controller via the addRenderer
method. Any of the controller's existing errors will be renderered immediately. You can remove a renderer using the removeRenderer
method. Removing a renderer will unrender any errors that renderer had previously rendered. If you choose to call addRenderer
in your view-model's activate
or bind
methods, make sure to call removeRenderer
in the corresponding deactivate
or unbind
methods.
The renderer example uses Element.closest
. You'll need to
polyfill
this method in Internet Explorer.
Here's another renderer for bootstrap forms that demonstrates "success styling". When a property transitions from indeterminate validity to valid or from invalid to valid, the form control will highlight in green. When a property transitions from indeterminate validity to invalid or from valid to invalid, the form control will highlight in red, just like in the previous bootstrap form renderer example.
export class BootstrapFormRenderer {
render(instruction: RenderInstruction) {
for (let { result, elements } of instruction.unrender) {
for (let element of elements) {
this.remove(element, result);
}
}
for (let { result, elements } of instruction.render) {
for (let element of elements) {
this.add(element, result);
}
}
}
add(element: Element, result: ValidateResult) {
const formGroup = element.closest('.form-group');
if (!formGroup) {
return;
}
if (result.valid) {
if (!formGroup.classList.contains('has-error')) {
formGroup.classList.add('has-success');
}
} else {
// add the has-error class to the enclosing form-group div
formGroup.classList.remove('has-success');
formGroup.classList.add('has-error');
// add help-block
const message = document.createElement('span');
message.className = 'help-block validation-message';
message.textContent = result.message;
message.id = `validation-message-${result.id}`;
formGroup.appendChild(message);
}
}
remove(element: Element, result: ValidateResult) {
const formGroup = element.closest('.form-group');
if (!formGroup) {
return;
}
if (result.valid) {
if (formGroup.classList.contains('has-success')) {
formGroup.classList.remove('has-success');
}
} else {
// remove help-block
const message = formGroup.querySelector(`#validation-message-${result.id}`);
if (message) {
formGroup.removeChild(message);
// remove the has-error class from the enclosing form-group div
if (formGroup.querySelectorAll('.help-block.validation-message').length === 0) {
formGroup.classList.remove('has-error');
}
}
}
}
}
Entity Validation
The examples so far show the controller validating specific properties used in & validate
bindings. The controller can validate whole entities even if some of the properties aren't used in data bindings. Opt in to this "entity" style validation using the controller's addObject(object, rules?)
method. Calling addObject
will add the specified object to the set of objects the controller should validate when its validate
method is called. The rules
parameter is optional. Use it when the rules for the object haven't been specified using the fluent syntax's .on
method. You can remove objects from the controller's list of objects to validate using removeObject(object)
. Calling removeObject
will unrender any errors associated with the object.
You may have rules that are not associated with a single property. The fluent rule syntax has an ensureObject()
method you can use to define rules for the whole object.
export class Shipment {
length = 0;
width = 0;
height = 0;
}
ValidationRules
.ensureObject()
.satisfies(obj => obj.length * obj.width * obj.height <= 50)
.withMessage('Volume cannot be greater than 50 cubic centemeters.')
.on(Shipment);
Custom Rules
The fluent API's satisfies
method enables quick custom rules. If you have a custom rule that you need to use multiple times you can define it using the customRule
method. Once defined, you can apply the rule using satisfiesRule
. Here's how you could define and use a simple date validation rule:
ValidationRules.customRule(
'date',
(value, obj) => value === null || value === undefined || value instanceof Date,
`\${$displayName} must be a Date.`
);
ValidationRules
.ensure('startDate')
.required()
.satisfiesRule('date');
You will often need to pass arguments to your custom rule. Below is an example of an "integer range" rule that accepts "min" and "max" arguments. Notice the last parameter to the customRule
method packages up the expected parameters into a "config" object. The config object is used when computing the validation message when an error occurs, enabling the message expression to access the rule's configuration.
ValidationRules.customRule(
'integerRange',
(value, obj, min, max) => value === null || value === undefined
|| Number.isInteger(value) && value >= min && value <= max,
`\${$displayName} must be an integer between \${$config.min} and \${$config.max}.`,
(min, max) => ({ min, max })
);
ValidationRules
.ensure('volume')
.required()
.satisfiesRule('integerRange', 1, 5000);
You may have noticed the custom rule examples above consider null
and undefined
to be valid. This is intentional- a rule should follow the single responsibility principle and validate only one constraint. It's the .required()
rule's job to validate whether the data is filled in, you shouldn't add required checking logic to your custom rule for two reasons:
- Rule reuse- if our "integerRange" rule also does "required" checks, we can't use it on optional fields.
- Messages Relevance- if our "integerRange" rule also does "required" checks the user will get "range" error messages when we they should have gotten "required" error messages.
When you write a custom rule, the function should return true
when the rule is "satisfied" / "valid" and false
when the rule is "broken" / "invalid". Optionally you can return a Promise
that resolves to true
or false
. The promise should not reject unless there's an unexpected exception. Promise rejection is not used for control flow or to represent "invalid" status.
A common application of a custom rule is to confirm that two password entries match. Here is a example showing how you can do that:
ValidationRules.customRule(
'matchesProperty',
(value, obj, otherPropertyName) =>
value === null
|| value === undefined
|| value === ''
|| obj[otherPropertyName] === null
|| obj[otherPropertyName] === undefined
|| obj[otherPropertyName] === ''
|| value === obj[otherPropertyName],
'${$displayName} must match ${$getDisplayName($config.otherPropertyName)}', otherPropertyName => ({ otherPropertyName })
);
ValidationRules
.ensure(a => a.password)
.required()
.ensure(a => a.confirmPassword)
.required()
.satisfiesRule('matchesProperty', 'password');
<div class="form-group">
<label class="control-label" for="password">Password</label>
<input type="password" class="form-control" id="password" placeholder="Password"
value.bind="password & validate"
change.delegate="confirmPassword = ''">
</div>
<div class="form-group">
<label class="control-label" for="confirmPassword">Confirm Password</label>
<input type="password" class="form-control" id="confirmPassword" placeholder="Confirm Password"
value.bind="confirmPassword & validate">
</div>
Multiple Validation Controllers
If you have two forms that need to be independently validated, it is of course recommended you implement them in separate components. However, it is technically possible to do two or more independant validations in the same component by creating multiple validation controllers.
For instance: imagine you're building an order form for an Italian restaurant. The customer can order pizza or pasta. If the customer makes a mistake in ordering a pizza, they should be able to order a pasta regardless. In that case your view model would look like this:
import {inject, NewInstance} from 'aurelia-framework';
import {ValidationController, ValidationRules} from 'aurelia-validation';
@inject(NewInstance.of(ValidationController), NewInstance.of(ValidationController))
export class ItalianRestaurant {
pizza;
pasta;
pizzaValidationController;
pastaValidationController
constructor(pizzaValidationController, pastaValidationController) {
this.pizzaValidationController = pizzaValidationController;
this.pastaValidationController = pastaValidationController;
const pizzaRules = ValidationRules
.ensure((res: ItalianRestaurant) => res.pizza).required()
.rules;
this.pizzaValidationController.addObject(this, pizzaRules);
const pastaRules = ValidationRules
.ensure((res: ItalianRestaurant) => res.pasta).required()
.rules;
this.pastaValidationController.addObject(this, pastaRules);
}
orderPizza() {
this.pizzaValidationController.validate()
.then(result => {
if (result.valid) {
alert("Ordering this pizza: " + this.pizza);
}
});
}
orderPasta() {
this.pastaValidationController.validate()
.then(result => {
if (result.valid) {
alert("Ordering this pasta: " + this.pasta);
}
});
}
}
In your view you need to take care to associate each input with the correct validation controller:
<template>
<form validation-errors="errors.bind: pizzaErrors; controller.bind: pizzaValidationController"
submit.delegate="orderPizza()">
<label for="pizza">Choose a pizza:</label>
<input id="pizza" value.bind="pizza & validateManually:pizzaValidationController">
<span class="help-block" repeat.for="errorInfo of pizzaErrors">
${errorInfo.error.message}
</span>
<input type="submit" value="Order pizza!">
</form>
<form validation-errors="errors.bind: pastaErrors; controller.bind: pastaValidationController"
submit.delegate="orderPasta()">
<label for="pasta">Choose a pasta:</label>
<input id="pasta" value.bind="pasta & validateManually:pastaValidationController">
<span class="help-block" repeat.for="errorInfo of pastaErrors">
${errorInfo.error.message}
</span>
<input type="submit" value="Order pasta!">
</form>
</template>
In the forms above you can see that each validation-errors
attribute and each validateManually
binding behavior is bound to the appropriate validation controller. This needs to be specified each time, since by default the attribute and the binding behavior will ask the container for a ValidationController
instance not knowing which one it will get.
Integration With Other Libraries
In aurelia-validation
the object and property validation work is handled by the StandardValidator
class which is an implementation of the Validator
interface. The StandardValidator
is responsible for applying the rules created with aurelia-validation's fluent syntax. You may not need any of this machinery if you have your own custom validation engine or if you're using a client-side data management library like
Breeze
which has its own validation logic. You can replace the StandardValidator
with your own implementation when the plugin is installed. Here's an example using breeze:
import {ValidateResult} from 'aurelia-validation';
export class BreezeValidator {
validateObject(object) {
if (object.entityAspect.validateEntity()) {
return [];
}
return object.entityAspect
.getValidationErrors()
.map(({ errorMessage, propertyName, key }) => new ValidateResult(key, object, propertyName, false, errorMessage));
}
validateProperty(object, propertyName) {
if (object.entityAspect.validateProperty(propertyName)) {
return [];
}
return object.entityAspect
.getValidationErrors(propertyName)
.map(({ errorMessage, propertyName, key }) => new ValidateResult(key, object, propertyName, false, errorMessage));
}
}
import {BreezeValidator} from './breeze-validator';
export function configure(aurelia) {
aurelia.use
.standardConfiguration()
.plugin(PLATFORM.moduleName('aurelia-validation'), config => config.customValidator(BreezeValidator))
...
...
}
Integrating with Aurelia-I18N
aurelia-i18n
is Aurelia's official I18N plugin. Check out the project's
readme
for information on how to use aurelia-i18n
in your application.
Integrating aurelia-i18n
with aurelia-validation
is easy. All standard validation messages are supplied by the ValidationMessageProvider
class. To translate the messages, override the getMessage(key)
and getDisplayName(propertyName, displayName)
methods with implementations that use aurelia-i18n
to fetch translated versions of the messages/property-names.
Here's how to override the methods, in your main.js, during application startup:
import {I18N} from 'aurelia-i18n';
import {ValidationMessageProvider} from 'aurelia-validation';
export function configure(aurelia) {
...
...
ValidationMessageProvider.prototype.getMessage = function(key) {
const i18n = aurelia.container.get(I18N);
const translation = i18n.tr(`errorMessages.${key}`);
return this.parser.parse(translation);
};
ValidationMessageProvider.prototype.getDisplayName = function(propertyName, displayName) {
if (displayName !== null && displayName !== undefined) {
return displayName;
}
const i18n = aurelia.container.get(I18N);
return i18n.tr(propertyName);
};
...
...
}
Creating the view and view-model
Once you've overriden the necessary methods in ValidationMessageProvider
you're ready to create a view and view-model. Here's a view for a simple multi-language form with first and last name fields. All static text is translated using the
T binding behavior
. Validation occurs on form submission and a switch language button demonstrates the i18n capabilities.
<template>
<form>
<label>${'firstName' & t}: <br>
<input type="text" value.bind="firstName & validate">
</label>
<label>${'lastName' & t}: <br>
<input type="text" value.bind="lastName & validate">
</label>
<button click.delegate="submit()">${'submit' & t}</button>
</form>
<div>
<h3>${'latestValidationResult' & t}: ${message}</h3>
<ul if.bind="controller.errors.length">
<li repeat.for="error of controller.errors">
${error.message}
</li>
</ul>
</div>
<button click.trigger="switchLanguage()">${'switchLanguage' & t}</button>
<template>
Here's the view model:
import {inject, NewInstance} from 'aurelia-framework';
import {I18N} from 'aurelia-i18n';
import {ValidationRules, ValidationController} from 'aurelia-validation';
@inject(NewInstance.of(ValidationController), I18N)
export class App {
firstName;
lastName;
controller;
i18n;
message = '';
constructor(controller, i18n) {
this.controller = controller;
this.i18n = i18n;
ValidationRules
.ensure((m: App) => m.firstName).required()
.ensure((m: App) => m.lastName).required()
.on(this);
}
submit() {
this.executeValidation();
}
switchLanguage() {
const currentLocale = this.i18n.getLocale();
this.i18n.setLocale(currentLocale === 'en' ? 'de' : 'en')
.then(() => this.executeValidation());
}
executeValidation() {
this.controller.validate()
.then(errors => {
if (errors.length === 0) {
this.message = this.i18n.tr('allGood');
} else {
this.message = this.i18n.tr('youHaveErrors');
}
});
}
}
Translation Files
Last but not least, create translation files that include translations for each propertyName and each validation message key. Below are the German and English files for the example above. Notice the errorMessages
section has the translation for the required
rule. In practice, you would need translations for each rule that you use. Take a look at the
validationMessages
for the full list.
// file: locales/de/translation.json
{
"firstName": "Vorname",
"lastName": "Nachname",
"submit": "Abschicken",
"switchLanguage": "Sprache wechseln",
"youHaveErrors": "Es gibt Fehler!",
"allGood": "Alles in Ordnung!",
"latestValidationResult": "Aktuelles Validierungsergebnis",
"errorMessages": {
"required": "${$displayName} fehlt!"
}
}
// file: locales/en/translation.json
{
"firstName": "First name",
"lastName": "Last name",
"submit": "Submit",
"switchLanguage": "Switch language",
"youHaveErrors": "You have errors!",
"allGood": "All is good!",
"latestValidationResult": "Latest validation result",
"errorMessages": {
"required": "${$displayName} is missing!"
}
}
Server-Side Validation
The fluent rule API and Validator API can be used server-side in a NodeJS application.