Skip to content

Instantly share code, notes, and snippets.

@plwalters
Last active January 3, 2017 04:28
Show Gist options
  • Save plwalters/1f2e763d9e6f81aac0204de7959446f1 to your computer and use it in GitHub Desktop.
Save plwalters/1f2e763d9e6f81aac0204de7959446f1 to your computer and use it in GitHub Desktop.
Aurelia Validation Demo
<template>
<require from="./registration-form"></require>
<registration-form></registration-form>
</template>
export class App {
}
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');
}
}
}
}
<template>
<h2>Case One: Validation messages are never rendered from rules defined by ensureObject.satisfies</h2>
<ol>
<li>Enter anything in the First Name field that does not contain 'abc'.</li>
<li>Click the 'Validate' button.</li>
<li><span style=color:red>Observe that the First Name form group is not highlighed in red and does not contain the error message that refers to it.</span> My request here is that the ValidationRenderer be informed about elements associated with properties referenced by ensureObject.satisfies. I suggested we might accomplish this by having ensureObject.satisfies accept a list of names of properties upon which it depends.</li>
</ol>
<div style="width:100%" with.bind="instance">
<!--<div class="form-group">
<ul><li repeat.for="error of controller.errors">${error.message}</li></ul>
</div>-->
<div class="form-group">
<label class="control-label" for="first">First Name</label>
<input type="text" class="form-control" id="first" placeholder="First Name"
value.bind="firstName & validate">
</div>
<div class="form-group">
<label class="control-label" for="last">Last Name</label>
<input type="text" class="form-control" id="last" placeholder="Last Name"
value.bind="lastName & validate">
</div>
<div class="form-group">
<label class="control-label" for="email">Email</label>
<input type="email" class="form-control" id="email" placeholder="Email"
value.bind="email & validate">
</div>
</div>
<button type="button" class="btn btn-info" style="margin-top:10px;" click.delegate="submitInstance()">Validate</button>
</template>
import {inject} from 'aurelia-dependency-injection';
import {BindingEngine} from "aurelia-framework";
import {
ValidationControllerFactory,
ValidationController,
ValidationRules
//, validateTrigger
} from 'aurelia-validation';
import {BootstrapFormRenderer} from './bootstrap-form-renderer';
@inject(ValidationControllerFactory, BindingEngine)
export class case1 {
controller = null;
instance = { firstName: '', lastName: '', email: '' };
constructor(controllerFactory, bindingEngine ) {
this.controller = controllerFactory.createForCurrentScope();
this.controller.addRenderer(new BootstrapFormRenderer());
this.controller.validateTrigger = 0; // validateTrigger.manual;
this.rules = ValidationRules
.ensure("firstName")
.displayName("First Name")
.required()
.ensure("lastName")
.displayName("Last Name")
.required()
.ensure("email")
.displayName("Email")
.required()
.email()
.ensureObject()
.satisfies((value, instance) => {
return (!instance["firstName"]) || (instance["firstName"].indexOf("abc") !== -1);
})
.withMessage("First Name must contain 'abc'")
.rules;
this.controller.addObject(this.instance, this.rules);
}
submitInstance() {
try {
this.controller.validate({ object: this.instance })
.then((errors) => {
console.log(errors);
});
} catch (ex) {
console.log(ex);
}
}
}
<template>
<h2>Case Two: Non-Manual Triggers Don't Evaluate Object-level Rules</h2>
<ol>
<li>In this case, the validate trigger is set to "blur". In this case, we want to save the entire instance whenever a single property is changed.</li>
<li>Enter anything in the First Name field that does not contain 'abc'.</li>
<li>Tab out of the input field.</li>
<li><span style=color:red>Notice that nothing happened, even though the entry is invalid. </span> Since the 'abc' rule is not known to relate to the FirstName property, only the rules known to apply to the FirstName property were evaluated. So again we need the requested change from Case One. Then the "validate" value converter should be confirmed to validate against all rules that apply to the property, including those defined by ensureObject.satifies.</li>
</ol>
<div style="width:100%" with.bind="instance">
<!--<div class="form-group">
<ul><li repeat.for="error of controller.errors">${error.message}</li></ul>
</div>-->
<div class="form-group">
<label class="control-label" for="first">First Name</label>
<input type="text" class="form-control" id="first" placeholder="First Name"
value.bind="firstName & validate">
</div>
<div class="form-group">
<label class="control-label" for="last">Last Name</label>
<input type="text" class="form-control" id="last" placeholder="Last Name"
value.bind="lastName & validate">
</div>
<div class="form-group">
<label class="control-label" for="email">Email</label>
<input type="email" class="form-control" id="email" placeholder="Email"
value.bind="email & validate">
</div>
</div>
</template>
import {inject} from 'aurelia-dependency-injection';
import {BindingEngine} from "aurelia-framework";
import {
ValidationControllerFactory,
ValidationController,
ValidationRules
//, validateTrigger
} from 'aurelia-validation';
import {BootstrapFormRenderer} from './bootstrap-form-renderer';
@inject(ValidationControllerFactory, BindingEngine)
export class case2 {
controller = null;
instance = { firstName: '', lastName: '', email: '' };
constructor(controllerFactory, bindingEngine ) {
this.controller = controllerFactory.createForCurrentScope();
this.controller.addRenderer(new BootstrapFormRenderer());
this.controller.validateTrigger = 1; // validateTrigger.blur;
this.rules = ValidationRules
.ensure("firstName")
.displayName("First Name")
.required()
.ensure("lastName")
.displayName("Last Name")
.required()
.ensure("email")
.displayName("Email")
.required()
.email()
.ensureObject()
.satisfies((value, instance) => {
return (!instance["firstName"]) || (instance["firstName"].indexOf("abc") !== -1);
})
.withMessage("First Name must contain 'abc'")
.rules;
this.controller.addObject(this.instance, this.rules);
}
submitInstance() {
try {
this.controller.validate({ object: this.instance })
.then((errors) => {
console.log(errors);
});
} catch (ex) {
console.log(ex);
}
}
}
<template>
<hr/>
<h2>Multiple Rows</h2>
<table class="table" style="width:100%">
<thead>
<tr>
<th>renderer errors</th>
<th>controller.errors</th>
<th>First Name</th>
<th>Last Name</th>
<th>Email</th>
<th></th>
</tr>
</thead>
<tbody repeat.for="instance of instances" >
<tr validation-errors.bind="instance.errors">
<td>
<ul><li repeat.for="errorInfo of instance.errors">${errorInfo.error.message}</li></ul>
</td>
<td>
<div class="form-group" style="margin-bottom: 0px">
<input type="text" placeholder="First Name" class="form-control"
value.bind="instance.firstName & validate">
</div>
</td>
<td>
<div class="form-group" style="margin-bottom: 0px">
<input type="text" placeholder="Last Name" class="form-control"
value.bind="instance.lastName & validate">
</div>
</td>
<td>
<div class="form-group" style="margin-bottom: 0px">
<input type="email" placeholder="Email" class="form-control"
value.bind="instance.email & validate">
</div>
</td>
<td style="padding-top: 15px">
<button type="button" class="btn btn-xs btn-info" click.delegate="submitInstance(instance)">Save</button>
</td>
</tr>
</tbody>
</table>
</template>
import {inject} from 'aurelia-dependency-injection';
import {BindingEngine} from "aurelia-framework";
import {
ValidationControllerFactory,
ValidationController,
ValidationRules
//, validateTrigger
} from 'aurelia-validation';
import {BootstrapFormRenderer} from './bootstrap-form-renderer';
@inject(ValidationControllerFactory, BindingEngine)
export class case2old {
controller = null;
instances = null;
constructor(controllerFactory, bindingEngine ) {
this.controller = controllerFactory.createForCurrentScope();
this.controller.addRenderer(new BootstrapFormRenderer());
this.controller.validateTrigger = 0; // validateTrigger.manual;
this.instances = new Array(
{ firstName: '', lastName: '', email: '', controllerErrors: [] },
{ firstName: '', lastName: '', email: '', controllerErrors: [] },
{ firstName: '', lastName: '', email: '', controllerErrors: [] });
this.rules = ValidationRules
.ensure("firstName")
.displayName("First Name")
.required()
.ensure("lastName")
.displayName("Last Name")
.required()
.ensure("email")
.displayName("Email")
.required()
.email()
.ensureObject()
.satisfies((value, instance) => {
return (!instance["firstName"]) || (instance["firstName"].indexOf("abc") !== -1);
})
.withMessage("First Name must contain 'abc'")
.rules;
var _this = this;
for (let instance of this.instances) {
this.controller.addObject(instance, this.rules);
/*********** HOOP **************/
// hoop to calculate object-specific set of ValidationErrors
// Object.defineProperty(instance, "controllerErrors", { get: function() { return _this.filterInstanceErrors(instance); } });
// bindingEngine.propertyObserver(instance, "controllerErrors")
}
}
submitInstance(instance: IPerson) {
try {
this.controller.validate({ object: instance })
.then((errors) => {
console.log(errors);
});
} catch (ex) {
console.log(ex);
}
}
filterInstanceErrors(instance) {
return (!this.controller.errors) ? [] : this.controller.errors.filter(function(e) { return e.object === instance });
}
}
<template>
<hr/>
<h2>Case Two</h2>
<ol>
<li>In this case, the validate trigger is set to "blur" rather than manual because, by definition for this case, we want to save each row whenever a single property is changed.</li>
<li>Note that I have applied the "validate" value converter to each input.</li>
<li>On any row, enter anything in the First Name field that does not contain 'abc'.</li>
<li>Tab out of the input field.</li>
<li><span style=color:red>Notice that nothing happened, even though the entry is invalid. </span> In this case, only the rules known to apply to the FirstName property were evaluated. The 'abc' rule is not known to apply to the FirstName property. So first we need the requested change from Case One. What I am requesting in this scenario is for the entire instance to be evaluated by virtue of the trigger and the presence of the validate value converter.</li>
<li>I originally suggested that the new custom attribute I requested in Case Two, "object-validation-errors", when present would instruct the 'validate" value converter to trigger whole-instance, rather than single-property, validation. But I think that is incorrect. I think it makes more sense that a new parameter of the 'validate' value converter control this.</li>
</ol>
<table class="table" style="width:100%">
<thead>
<tr>
<th>First Name</th>
<th>Last Name</th>
<th>Email</th>
</tr>
</thead>
<tbody repeat.for="instance of instances" >
<!--<td>
<ul><li repeat.for="errorInfo of instance.errors">${errorInfo.error.message}</li></ul>
</td>-->
<td>
<div class="form-group" style="margin-bottom: 0px">
<input type="text" placeholder="First Name" class="form-control"
value.bind="instance.firstName & validate">
</div>
</td>
<td>
<div class="form-group" style="margin-bottom: 0px">
<input type="text" placeholder="Last Name" class="form-control"
value.bind="instance.lastName & validate">
</div>
</td>
<td>
<div class="form-group" style="margin-bottom: 0px">
<input type="email" placeholder="Email" class="form-control"
value.bind="instance.email & validate">
</div>
</td>
</tr>
</tbody>
</table>
</template>
import {inject} from 'aurelia-dependency-injection';
import {BindingEngine} from "aurelia-framework";
import {
ValidationControllerFactory,
ValidationController,
ValidationRules
//, validateTrigger
} from 'aurelia-validation';
import {BootstrapFormRenderer} from './bootstrap-form-renderer';
@inject(ValidationControllerFactory, BindingEngine)
export class case2 {
controller = null;
constructor(controllerFactory, bindingEngine ) {
this.controller = controllerFactory.createForCurrentScope();
this.controller.addRenderer(new BootstrapFormRenderer());
this.controller.validateTrigger = 1; // validateTrigger.blur;
this.instances = new Array(
{ firstName: '', lastName: '', email: '', controllerErrors: [] },
{ firstName: '', lastName: '', email: '', controllerErrors: [] },
{ firstName: '', lastName: '', email: '', controllerErrors: [] });
this.rules = ValidationRules
.ensure("firstName")
.displayName("First Name")
.required()
.ensure("lastName")
.displayName("Last Name")
.required()
.ensure("email")
.displayName("Email")
.required()
.email()
.ensureObject()
.satisfies((value, instance) => {
return (!instance["firstName"]) || (instance["firstName"].indexOf("abc") !== -1);
})
.withMessage("First Name must contain 'abc'")
.rules;
var _this = this;
for (let instance of this.instances) {
this.controller.addObject(instance, this.rules);
// hoop to caluculate object-specific set of ValidationErrors
Object.defineProperty(instance, "controllerErrors", { get: function() { return _this.filterInstanceErrors(instance); } });
bindingEngine.propertyObserver(instance, "controllerErrors")
}
}
submitInstance(instance: IPerson) {
try {
this.controller.validate({ object: instance })
.then((errors) => {
console.log(errors);
});
} catch (ex) {
console.log(ex);
}
}
filterInstanceErrors(instance) {
return (!this.controller.errors) ? [] : this.controller.errors.filter(function(e) { return e.object === instance });
}
}
import {inject} from 'aurelia-dependency-injection';
import {BindingEngine} from "aurelia-framework";
import {
ValidationControllerFactory,
ValidationController,
ValidationRules
//, validateTrigger
} from 'aurelia-validation';
import {BootstrapFormRenderer} from './bootstrap-form-renderer';
@inject(ValidationControllerFactory, BindingEngine)
export class case2 {
controller = null;
constructor(controllerFactory, bindingEngine ) {
this.controller = controllerFactory.createForCurrentScope();
this.controller.addRenderer(new BootstrapFormRenderer());
this.controller.validateTrigger = 1; // validateTrigger.blur;
this.instances = new Array(
{ firstName: '', lastName: '', email: '', controllerErrors: [] },
{ firstName: '', lastName: '', email: '', controllerErrors: [] },
{ firstName: '', lastName: '', email: '', controllerErrors: [] });
this.rules = ValidationRules
.ensure("firstName")
.displayName("First Name")
.required()
.ensure("lastName")
.displayName("Last Name")
.required()
.ensure("email")
.displayName("Email")
.required()
.email()
.ensureObject()
.satisfies((value, instance) => {
return (!instance["firstName"]) || (instance["firstName"].indexOf("abc") !== -1);
})
.withMessage("First Name must contain 'abc'")
.rules;
var _this = this;
for (let instance of this.instances) {
this.controller.addObject(instance, this.rules);
// hoop to caluculate object-specific set of ValidationErrors
Object.defineProperty(instance, "controllerErrors", { get: function() { return _this.filterInstanceErrors(instance); } });
bindingEngine.propertyObserver(instance, "controllerErrors")
}
}
submitInstance(instance: IPerson) {
try {
this.controller.validate({ object: instance })
.then((errors) => {
console.log(errors);
});
} catch (ex) {
console.log(ex);
}
}
filterInstanceErrors(instance) {
return (!this.controller.errors) ? [] : this.controller.errors.filter(function(e) { return e.object === instance });
}
}
<!doctype html>
<html>
<head>
<title>Aurelia</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<link href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.6/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-1q8mTJOASx8j1Au+a5WDVnPi2lkFfwwEAa8hDDdjZlpLegxhjVME1fgjWPGmkzs7" crossorigin="anonymous">
<style>
registration-form {
display: inline-block;
width: 98%;
margin-left: 10px;
margin-right: 10px;
}
</style>
</head>
<body aurelia-app="main">
<h1>Loading...</h1>
<script src="https://jdanyow.github.io/rjs-bundle/node_modules/requirejs/require.js"></script>
<script src="https://jdanyow.github.io/rjs-bundle/config.js"></script>
<script src="https://jdanyow.github.io/rjs-bundle/bundles/aurelia.js"></script>
<script src="https://jdanyow.github.io/rjs-bundle/bundles/babel.js"></script>
<script>
require(['aurelia-bootstrapper']);
</script>
</body>
</html>
export function configure(aurelia) {
aurelia.use
.standardConfiguration()
.developmentLogging()
.plugin('aurelia-validation');
aurelia.start().then(() => aurelia.setRoot());
}
<template>
<require from="./case1"></require>
<require from="./case2"></require>
<h1>See Case One and Case Two, Below</h1>
<case1></case1>
<case2></case2>
</template>
import {inject} from 'aurelia-dependency-injection';
import {BindingEngine} from "aurelia-framework";
import {
ValidationControllerFactory,
ValidationController,
ValidationRules
//, validateTrigger
} from 'aurelia-validation';
import {BootstrapFormRenderer} from './bootstrap-form-renderer';
@inject(ValidationControllerFactory, BindingEngine)
export class RegistrationForm {
controller = null;
constructor(controllerFactory, bindingEngine ) {
this.controller = controllerFactory.createForCurrentScope();
this.controller.addRenderer(new BootstrapFormRenderer());
this.controller.validateTrigger = 0; // validateTrigger.manual;
this.instances = new Array(
{ firstName: '', lastName: '', email: '', controllerErrors: [] },
{ firstName: '', lastName: '', email: '', controllerErrors: [] },
{ firstName: '', lastName: '', email: '', controllerErrors: [] });
this.rules = ValidationRules
.ensure("firstName")
.displayName("First Name")
.required()
.ensure("lastName")
.displayName("Last Name")
.required()
.ensure("email")
.displayName("Email")
.required()
.email()
.ensureObject()
.satisfies((value, instance) => {
return (!instance["firstName"]) || (instance["firstName"].indexOf("abc") !== -1);
})
.withMessage("First Name must contain 'abc'")
.rules;
var _this = this;
for (let instance of this.instances) {
this.controller.addObject(instance, this.rules);
// hoop to caluculate object-specific set of ValidationErrors
Object.defineProperty(instance, "controllerErrors", { get: function() { return _this.filterInstanceErrors(instance); } });
bindingEngine.propertyObserver(instance, "controllerErrors")
}
}
submitInstance(instance: IPerson) {
try {
this.controller.validate({ object: instance })
.then((errors) => {
console.log(errors);
});
} catch (ex) {
console.log(ex);
}
}
filterInstanceErrors(instance) {
return (!this.controller.errors) ? [] : this.controller.errors.filter(function(e) { return e.object === instance });
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment