Aurelia's validation plugin provides a robust and flexible form and data validation system. It offers a fluent, declarative API that makes it easy to define validation rules for your application's models and forms.
The validation system supports:
- Declarative rule definition
- Customizable validation triggers
- Flexible error rendering
- Extensive rule types
- Internationalization support
- Fluent rule configuration
- Multiple validation triggers
- Custom validation rules
- Easy error display
- Seamless integration with Aurelia's binding system
Install the validation plugin using npm or your chosen package installer:
npm install aurelia-validation
In your application's main configuration file (typically main.js
or main.ts
), register the validation plugin:
export function configure(aurelia) {
aurelia.use
.standardConfiguration()
.plugin(PLATFORM.moduleName('aurelia-validation'));
}
You can customize the default validation trigger during plugin registration:
export function configure(aurelia) {
aurelia.use.plugin('aurelia-validation', config => {
config.defaultValidationTrigger(validateTrigger.manual);
});
}
Aurelia's validation uses a fluent API to define validation rules. The primary class for defining rules is ValidationRules
.
Basic Rule Definition
import { ValidationRules } from 'aurelia-validation';
ValidationRules
.ensure('firstName')
.required()
.ensure('email')
.required()
.email()
.on(MyClass);
Aurelia provides several built-in validation rules:
Rule | Description | Example |
---|---|---|
required() |
Ensures the property is not null, undefined, or whitespace | .required() |
email() |
Validates email format | .email() |
minLength(length) |
Validates minimum string length | .minLength(3) |
maxLength(length) |
Validates maximum string length | .maxLength(50) |
matches(regex) |
Validates against a regular expression | .matches(/^\d+$/) |
min(value) |
Validates minimum numeric value | .min(0) |
max(value) |
Validates maximum numeric value | .max(100) |
Custom Display Names
You can set custom display names for more meaningful error messages:
ValidationRules
.ensure('ssn').displayName('Social Security Number')
.required()
.matches(/\d{3}-\d{2}-\d{4}/)
.on(Person);
Rules are evaluated in parallel by default. Use .then()
to create sequential rule evaluation:
ValidationRules
.ensure('email')
.email()
.required()
.then()
.satisfiesRule('emailNotInUse')
.on(User);
Customizing Error Messages
Override default messages using .withMessage()
:
ValidationRules
.ensure('password')
.required().withMessage('Please enter a password')
.minLength(8).withMessage('Password must be at least 8 characters long')
.on(User);
export class RegistrationForm {
firstName = '';
email = '';
password = '';
constructor() {
ValidationRules
.ensure('firstName')
.required().withMessage('First name is required')
.minLength(2).withMessage('First name must be at least 2 characters')
.ensure('email')
.required().withMessage('Email is required')
.email().withMessage('Please enter a valid email address')
.ensure('password')
.required().withMessage('Password is required')
.minLength(8).withMessage('Password must be at least 8 characters')
.on(this);
}
}
The ValidationController orchestrates validation execution and error rendering in your application. Controllers manage the validation lifecycle and maintain the current validation state.
You can create a controller using dependency injection:
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;
}
}
Alternatively, use the ValidationControllerFactory:
import { ValidationControllerFactory } from 'aurelia-validation';
@inject(ValidationControllerFactory)
export class RegistrationForm {
controller = null;
constructor(controllerFactory) {
this.controller = controllerFactory.createForCurrentScope();
}
}
Controllers support different validation triggers that determine when validation occurs:
blur
: Validates when an input loses focus (default)focusout
: Validates when an element or its children lose focuschange
: Validates when the model value changeschangeOrBlur
: Validates on both change and blur eventschangeOrFocusout
: Validates on both change and focusout eventsmanual
: Validation only occurs when explicitly called
Set the trigger in your view-model:
import { validateTrigger } from 'aurelia-validation';
export class RegistrationForm {
constructor(controller) {
this.controller = controller;
this.controller.validateTrigger = validateTrigger.changeOrBlur;
}
}
Trigger validation programmatically using the controller's methods:
class RegistrationForm {
async submit() {
const result = await this.controller.validate();
if (result.valid) {
// proceed with form submission
}
}
reset() {
this.controller.reset();
}
}
You can validate specific objects or properties:
// Validate specific object
controller.validate({ object: person });
// Validate specific property
controller.validate({ object: person, propertyName: 'email' });
The simplest way to display validation errors is using the validation-errors
attribute and controller's errors property:
<form>
<div validation-errors.bind="errors">
<input value.bind="email & validate">
<ul if.bind="errors.length">
<li repeat.for="error of errors">
${error.error.message}
</li>
</ul>
</div>
</form>
Here's an example using Bootstrap form styling:
<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"
value.bind="firstName & validate">
<span class="help-block" repeat.for="error of firstNameErrors">
${error.error.message}
</span>
</div>
</form>
Create custom renderers for more control over error display:
import { ValidationRenderer, RenderInstruction } from 'aurelia-validation';
export class CustomFormRenderer {
render(instruction: RenderInstruction) {
for (let { result, elements } of instruction.unrender) {
for (let element of elements) {
this.removeError(element, result);
}
}
for (let { result, elements } of instruction.render) {
for (let element of elements) {
this.addError(element, result);
}
}
}
addError(element, result) {
if (result.valid) return;
const formGroup = element.closest('.form-group');
if (!formGroup) return;
// Add error class
formGroup.classList.add('has-error');
// Add error message
const message = document.createElement('div');
message.className = 'validation-error';
message.textContent = result.message;
message.id = `validation-message-${result.id}`;
formGroup.appendChild(message);
}
removeError(element, result) {
if (result.valid) return;
const formGroup = element.closest('.form-group');
if (!formGroup) return;
// Remove error class
formGroup.classList.remove('has-error');
// Remove error message
const message = formGroup.querySelector(`#validation-message-${result.id}`);
if (message) {
formGroup.removeChild(message);
}
}
}
Register your custom renderer with the controller:
export class RegistrationForm {
constructor(controller) {
this.controller = controller;
this.renderer = new CustomFormRenderer();
this.controller.addRenderer(this.renderer);
}
detached() {
this.controller.removeRenderer(this.renderer);
}
}
When using validation-errors, you can specify both the errors and the controller:
<div validation-errors="errors.bind: myErrors; controller.bind: myController">
<input value.bind="email & validate:myController">
<span repeat.for="error of myErrors">
${error.error.message}
</span>
</div>
{% hint style="info" %} Always remove custom renderers in the detached lifecycle method to prevent memory leaks and ensure proper cleanup. {% endhint %}
Implement conditional validation using the .when()
method:
ValidationRules
.ensure('alternateEmail')
.email()
.required()
.when(person => person.needsAlternateContact)
.withMessage('Alternate email is required when alternate contact is enabled')
.on(Person);
Create reusable custom validation rules using ValidationRules.customRule()
:
// Define a custom date validation rule
ValidationRules.customRule(
'date',
(value, obj) => value === null || value === undefined || value instanceof Date,
'${$displayName} must be a valid date.'
);
// Define a custom rule with parameters
ValidationRules.customRule(
'numericRange',
(value, obj, min, max) => {
if (value === null || value === undefined) return true;
return Number.isInteger(value) && value >= min && value <= max;
},
'${$displayName} must be between ${$config.min} and ${$config.max}.',
(min, max) => ({ min, max })
);
// Using custom rules
ValidationRules
.ensure('birthDate')
.required()
.satisfiesRule('date')
.ensure('age')
.satisfiesRule('numericRange', 18, 100)
.on(Person);
Validate entire objects using controller's addObject()
method:
export class OrderForm {
constructor(controller) {
this.controller = controller;
this.order = new Order();
// Add object for validation
this.controller.addObject(this.order);
}
// Validate entire object
async submit() {
const result = await this.controller.validate();
if (result.valid) {
await this.submitOrder();
}
}
detached() {
// Clean up
this.controller.removeObject(this.order);
}
}
Define object-level rules using ensureObject()
:
ValidationRules
.ensureObject()
.satisfies(obj => obj.endDate > obj.startDate)
.withMessage('End date must be after start date')
.on(DateRange);
Integrate validation messages with aurelia-i18n:
import { I18N } from 'aurelia-i18n';
import { ValidationMessageProvider } from 'aurelia-validation';
// Configure during app startup
export function configure(aurelia) {
aurelia.use
.standardConfiguration()
.plugin('aurelia-i18n')
.plugin('aurelia-validation');
// Override message provider
ValidationMessageProvider.prototype.getMessage = function(key) {
const i18n = aurelia.container.get(I18N);
const translation = i18n.tr(`validationMessages.${key}`);
return this.parser.parse(translation);
};
ValidationMessageProvider.prototype.getDisplayName = function(propertyName, displayName) {
if (displayName) return displayName;
const i18n = aurelia.container.get(I18N);
return i18n.tr(propertyName);
};
}
Translation files example:
{
"validationMessages": {
"required": "${$displayName} is required",
"email": "${$displayName} must be a valid email",
"minLength": "${$displayName} must be at least ${$config.length} characters"
},
"propertyNames": {
"firstName": "First Name",
"email": "Email Address"
}
}
Core methods for defining validation rules:
Method | Description |
---|---|
ensure(property) |
Targets a property for validation |
ensureObject() |
Targets the entire object for validation |
required() |
Validates presence |
email() |
Validates email format |
minLength(length) |
Validates minimum length |
maxLength(length) |
Validates maximum length |
matches(regex) |
Validates against regular expression |
satisfies(condition) |
Custom validation function |
satisfiesRule(name) |
Uses a custom rule |
when(condition) |
Conditional validation |
withMessage(message) |
Custom error message |
on(target) |
Applies rules to class/object |
Core controller methods:
Method | Description |
---|---|
validate() |
Validates all registered objects/bindings |
reset() |
Clears all validation results |
addObject(object, rules?) |
Registers object for validation |
removeObject(object) |
Removes object from validation |
addRenderer(renderer) |
Adds custom error renderer |
removeRenderer(renderer) |
Removes error renderer |
addError(message, object, propertyName?) |
Adds manual error |
removeError(result) |
Removes specific error |
Properties available in validation results:
Property | Type | Description |
---|---|---|
valid |
boolean | Validation result |
message |
string | Error message |
object |
any | Validated object |
propertyName |
string | Validated property |
rule |
any | Rule that generated the result |
{% hint style="info" %} When implementing custom validation rules, always handle null/undefined values appropriately to maintain consistent validation behavior. {% endhint %}
{% hint style="warning" %} Be careful when using async custom rules, which may impact form submission timing and user experience. {% endhint %}