Skip to content

Latest commit

 

History

History
562 lines (434 loc) · 15.5 KB

validation.md

File metadata and controls

562 lines (434 loc) · 15.5 KB

Validation

Introduction

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

Key Features

  • Fluent rule configuration
  • Multiple validation triggers
  • Custom validation rules
  • Easy error display
  • Seamless integration with Aurelia's binding system

Installation and Setup

Installation

Install the validation plugin using npm or your chosen package installer:

npm install aurelia-validation

Plugin Configuration

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'));
}

Optional Configuration

You can customize the default validation trigger during plugin registration:

export function configure(aurelia) {
  aurelia.use.plugin('aurelia-validation', config => {
    config.defaultValidationTrigger(validateTrigger.manual);
  });
}

Validation Basics

Defining Rules

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);

Rule Types

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);

Rule Sequencing

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);

Example: Complete Validation Setup

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);
  }
}

Validation Controllers

The ValidationController orchestrates validation execution and error rendering in your application. Controllers manage the validation lifecycle and maintain the current validation state.

Creating a Controller

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();
  }
}

Validation Triggers

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 focus
  • change: Validates when the model value changes
  • changeOrBlur: Validates on both change and blur events
  • changeOrFocusout: Validates on both change and focusout events
  • manual: 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;
  }
}

Validate and Reset Methods

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' });

Displaying Validation Errors

Built-in Error Display

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>

Bootstrap Form Integration

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>

Custom Renderer Implementation

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);
  }
}

Renderer Binding Context

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 %}

Advanced Validation Techniques

Conditional Validation

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);

Custom Rules

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);

Entity Validation

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);

Integration

Working with I18N

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"
  }
}

API Reference

ValidationRules

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

ValidationController

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

ValidateResult

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 %}