Outcome-focused validation scenarios that show how to wire Aurelia's validation controller, rules, and presenters for real forms.
These recipes start with the result you want and walk through the minimal steps to get there with @aurelia/validation + @aurelia/validation-html.
1. Instant inline validation feedback
Goal: Highlight invalid inputs as soon as a user blurs a field, with errors listed next to each control.
Steps
Register the HTML adapter as usual:
Aurelia.register(ValidationHtmlConfiguration);
In your form component, define rules for the model:
import{IValidationRules}from'@aurelia/validation';import{IValidationController}from'@aurelia/validation-html';import{newInstanceForScope,resolve}from'@aurelia/kernel';exportclassSignupForm{ person ={ name:'', email:''}; controller =resolve(newInstanceForScope(IValidationController));constructor(privaterules=resolve(IValidationRules)){this.rules.on(this.person).ensure('name').required().ensure('email').required().email();}}
Use the validate binding behavior with a trigger (changeOrBlur fires on blur and subsequent edits) and capture errors with the validation-errors attribute:
<formsubmit.trigger="controller.validate()"><divvalidation-errors.from-view="nameErrors"><label> Name<inputvalue.bind="person.name & validate:changeOrBlur"></label><ulif.bind="nameErrors?.length"><lirepeat.for="error of nameErrors">${error.result.message}</li></ul></div><divvalidation-errors.from-view="emailErrors"><label> Email<inputvalue.bind="person.email & validate:changeOrBlur"></label><pclass="error"repeat.for="error of emailErrors">${error.result.message}</p></div></form>
Checklist
Typing and blurring a field shows validation messages immediately.
Submitting runs controller.validate() and prevents submission when result.valid is false.
Removing the validation-errors attribute stops error collections, confirming the bindings are wired correctly.
2. Step-by-step wizard gating
Goal: Prevent users from advancing to the next wizard step until the current step’s model is valid, while keeping previous steps untouched.
Steps
Model each step separately and register them with the controller:
Bind each step’s inputs to the respective object and call goTo('payment') on the “Continue” button.
Use controller.reset({ object: this.payment }) when users move backward so stale errors disappear.
Checklist
Clicking “Continue” on shipping does nothing until the required fields pass validation.
Moving back to shipping clears payment errors (thanks to controller.reset).
Each step validates only its own object, so partial progress is preserved.
3. Merge API validation errors
Goal: Display server-side validation failures next to the relevant controls after submitting the form.
Steps
Submit the form through the controller so client rules run first:
Store references to server errors (the return value from addError) and call controller.removeError(error) after the next successful submission or when the user edits that field.
Checklist
When the API returns { errors: [{ property: 'email', message: 'Email already used' }] }, the message appears under the email input without reloading.
Editing the field and re-validating removes the manual error via removeError.
Server-only properties (like paymentToken) can still be surfaced by passing a propertyName even if there is no binding.
4. Validate only changed fields on large forms
Goal: Skip expensive validation when only one field changes by using validation triggers strategically.
Steps
Leave the global configuration at the default focusout trigger.
Override the behavior per binding using & validate:manual for fields that should only validate on submit.
Call controller.validate({ object: this.model, propertyName: 'taxId' }) inside a watcher or setter when you do want to validate a single property.
Checklist
Fields marked with validate:manual don’t show inline errors until submit.
Calling validate({ propertyName }) updates only the specified property’s errors.
The controller’s results array stays small even on big forms.