Skip to content

Commit

Permalink
enhance(ValidationParser): handle validate props that subpropeties of…
Browse files Browse the repository at this point in the history
… an object

fixes aurelia#283
  • Loading branch information
ericIMT committed Dec 19, 2016
1 parent d749cf5 commit 709ae6a
Show file tree
Hide file tree
Showing 8 changed files with 100 additions and 17 deletions.
2 changes: 1 addition & 1 deletion doc/api.json

Large diffs are not rendered by default.

16 changes: 13 additions & 3 deletions src/implementation/standard-validator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ export class StandardValidator extends Validator {
* Validates the specified property.
* @param object The object to validate.
* @param propertyName The name of the property to validate.
* @param rules Optional. If unspecified, the rules will be looked up using the metadata
* @param rules Optional. If unspecified, the rules will be looked up using the metadata
* for the object created by ValidationRules....on(class/object)
*/
public validateProperty(object: any, propertyName: string, rules?: any): Promise<ValidateResult[]> {
Expand All @@ -38,7 +38,7 @@ export class StandardValidator extends Validator {
/**
* Validates all rules for specified object and it's properties.
* @param object The object to validate.
* @param rules Optional. If unspecified, the rules will be looked up using the metadata
* @param rules Optional. If unspecified, the rules will be looked up using the metadata
* for the object created by ValidationRules....on(class/object)
*/
public validateObject(object: any, rules?: any): Promise<ValidateResult[]> {
Expand Down Expand Up @@ -108,7 +108,17 @@ export class StandardValidator extends Validator {
}

// validate.
const value = rule.property.name === null ? object : object[rule.property.name];
let value = rule.property.name === null ? object : object[rule.property.name];
console.log("standard-validator.ts 109 Property ", rule.property.name);
if (rule.property.name && rule.property.name.indexOf('.') !== -1) {
// if the rule name has a '.', we have a sub property.
// "Object" is the parent containing the field.
// The field is the last part of the propert path
// e.g. finalProp in object.sub1.sub2.finalProp
let parts = rule.property.name.split('.');
value = object[ parts[ parts.length - 1 ]];
}
console.log("standard-validator.ts 118 Property ", rule.property.name);
let promiseOrBoolean = rule.condition(value, object);
if (!(promiseOrBoolean instanceof Promise)) {
promiseOrBoolean = Promise.resolve(promiseOrBoolean);
Expand Down
21 changes: 15 additions & 6 deletions src/implementation/validation-parser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,10 +64,19 @@ export class ValidationParser {
return { name: <string>property, displayName: null };
}
const accessor = this.getAccessorExpression(property.toString());
if (accessor instanceof AccessScope
|| accessor instanceof AccessMember && accessor.object instanceof AccessScope) {
return {
name: accessor.name,
const isSubProp = accessor instanceof AccessMember && accessor.object instanceof AccessScope;
if (accessor instanceof AccessScope || isSubProp) {
let propName = (<any>accessor).name;
if (isSubProp) {
// iterate up the chain until we are in the 1st sub-object of the root object.
let ao = (<any>accessor).object;
while (ao) {
propName = ao.name + '.' + propName;
ao = ao.object;
}
}
return {
name: propName,
displayName: null
};
}
Expand All @@ -88,8 +97,8 @@ export class ValidationParser {
}

private getAccessorExpression(fn: string): Expression {
const classic = /^function\s*\([$_\w\d]+\)\s*\{\s*(?:"use strict";)?\s*return\s+[$_\w\d]+\.([$_\w\d]+)\s*;?\s*\}$/;
const arrow = /^\(?[$_\w\d]+\)?\s*=>\s*[$_\w\d]+\.([$_\w\d]+)$/;
const classic = /^function\s*\([$_\w\d]+\)\s*\{\s*(?:"use strict";)?\s*.*return\s+[$_\w\d]+\.([$_\w\d]+(\.[$_\w\d]+)*)\s*;?\s*\}$/;
const arrow = /^\(?[$_\w\d]+\)?\s*=>\s*(?:\{?.*return\s+)?[$_\w\d]+\.([$_\w\d]+(\.[$_\w\d]+)*);?\s*\}?$/;
const match = classic.exec(fn) || arrow.exec(fn);
if (match === null) {
throw new Error(`Unable to parse accessor function:\n${fn}`);
Expand Down
10 changes: 10 additions & 0 deletions src/property-info.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,12 +30,22 @@ export function getPropertyInfo(expression: Expression, source: any): { object:

let object: null | undefined | Object;
let propertyName: string;
let ruleSrc = null;
if (expression instanceof AccessScope) {
object = source.bindingContext;
propertyName = expression.name;
} else if (expression instanceof AccessMember) {
object = getObject(originalExpression, expression.object, source);
propertyName = expression.name;
if (expression.object) {
// build the path to the property from the object root.
let exp: any = expression.object;
while (exp.object) {
propertyName = exp.name + '.' + propertyName;
exp = exp.object;
}
ruleSrc = <any>getObject(originalExpression, exp, source);
}
} else if (expression instanceof AccessKeyed) {
object = getObject(originalExpression, expression.object, source);
propertyName = expression.key.evaluate(source);
Expand Down
39 changes: 38 additions & 1 deletion src/validation-controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -170,6 +170,27 @@ export class ValidationController {
let { object, propertyName, rules } = instruction;
// if rules were not specified, check the object map.
rules = rules || this.objects.get(object);
if (!rules)
{
for (let [binding, { rulesObj }] of Array.from(this.bindings))
{
const propertyInfo = getPropertyInfo(<Expression>binding.sourceExpression, (<any>binding).source);
if (propertyInfo.propertyName!= propertyName || !propertyInfo || this.objects.has(propertyInfo.object)) {
continue;
}
if (propertyInfo.propertyName.indexOf(".") !== -1)
{
let parentProp ="";
let ittr:any= (<any>binding).sourceExpression.expression;
while(ittr.object)
{
ittr = ittr.object;
parentProp = ittr.name;
}
rules = Rules.get((<any>binding)._observer0._callable0._observer0.obj[parentProp]);
}
}
}
// property specified?
if (instruction.propertyName === undefined) {
// validate the specified object.
Expand All @@ -185,11 +206,27 @@ export class ValidationController {
for (let [object, rules] of Array.from(this.objects)) {
promises.push(this.validator.validateObject(object, rules));
}
for (let [binding, { rules }] of Array.from(this.bindings)) {
for (let [binding, { rulesObj }] of Array.from(this.bindings)) {
const propertyInfo = getPropertyInfo(<Expression>binding.sourceExpression, (<any>binding).source);
if (!propertyInfo || this.objects.has(propertyInfo.object)) {
continue;
}
const propName = propertyInfo.propertyName;
if (propertyInfo.propertyName.indexOf(".") !== -1)
{
let parentProp ="";
let ittr:any= (<any>binding).sourceExpression.expression;
while(ittr.object)
{
ittr = ittr.object;
parentProp = ittr.name;
}
propName = propertyInfo.propertyName.substr(propertyInfo.propertyName.lastIndexOf('.')+1);
rules = Rules.get((<any>binding)._observer0._callable0._observer0.obj[parentProp]);
}
else{
rules = rulesObj;
}
promises.push(this.validator.validateProperty(propertyInfo.object, propertyInfo.propertyName, rules));
}
return Promise.all(promises).then(resultSets => resultSets.reduce((a, b) => a.concat(b), []));
Expand Down
19 changes: 15 additions & 4 deletions test/basic.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ describe('end to end', () => {

let firstName: HTMLInputElement;
let lastName: HTMLInputElement;
let subprop: HTMLInputElement;
let number1: HTMLInputElement;
let number2: HTMLInputElement;
let password: HTMLInputElement;
Expand All @@ -30,6 +31,7 @@ describe('end to end', () => {
viewModel.controller.addRenderer(renderer);
firstName = <HTMLInputElement>component.element.querySelector('#firstName');
lastName = <HTMLInputElement>component.element.querySelector('#lastName');
subprop = <HTMLInputElement>component.element.querySelector('#subProp');
number1 = <HTMLInputElement>component.element.querySelector('#number1');
number2 = <HTMLInputElement>component.element.querySelector('#number2');
password = <HTMLInputElement>component.element.querySelector('#password');
Expand All @@ -41,7 +43,7 @@ describe('end to end', () => {
.then(() => blur(firstName))
// confirm there's an error.
.then(() => expect(viewModel.controller.errors.length).toBe(1))
// make a model change to the firstName field.
// make a model change to the firstName field.
// this should reset the errors for the firstName field.
.then(() => viewModel.firstName = 'test')
// confirm the errors were reset.
Expand All @@ -55,6 +57,15 @@ describe('end to end', () => {
const renderInstruction = calls.argsFor(calls.count() - 1)[0];
expect(renderInstruction.render[0].elements[0]).toBe(lastName);
})

// blur the subprop- this should trigger validation.
.then(() => blur(subprop))
// confirm there's an error.
.then(() => expect(viewModel.controller.errors.length).toBe(2))
// set to a valid value, should reset error
.then(() => viewModel.settings.subprop = 'test')
.then(() => expect(viewModel.controller.errors.length).toBe(1))

// blur the number1 field- this should trigger validation.
.then(() => blur(number1))
// confirm there's an error.
Expand All @@ -73,12 +84,12 @@ describe('end to end', () => {
const renderInstruction = calls.argsFor(calls.count() - 1)[0];
expect(renderInstruction.render[0].elements[0]).toBe(number2);
})
// make a model change to the number1 field.
// make a model change to the number1 field.
// this should reset the errors for the number1 field.
.then(() => viewModel.number1 = 1)
// confirm the error was reset.
.then(() => expect(viewModel.controller.errors.length).toBe(2))
// make a model change to the number2 field.
// make a model change to the number2 field.
// this should reset the errors for the number2 field.
.then(() => viewModel.number2 = 2)
// confirm the error was reset.
Expand Down Expand Up @@ -166,7 +177,7 @@ describe('end to end', () => {
.then(() => blur(firstName))
// confirm there's an error.
.then(() => expect(viewModel.controller.errors.length).toBe(1))
// make a model change to the firstName field.
// make a model change to the firstName field.
// this should reset the errors for the firstName field.
.then(() => viewModel.firstName = 'test')
// confirm the errors were reset.
Expand Down
7 changes: 6 additions & 1 deletion test/resources/registration-form.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,9 @@ import {
<form novalidate autocomplete="off" if.bind="showForm">
<input id="firstName" type="text" value.bind="firstName & validate">
<input id="lastName" type="text" value.bind="lastName & validate">
<input id="subProp" type="text" value.bind="settings.subprop & validate">
<input id="email" type="text" value.bind="email & validate">
<input id="number1" type="text" number-value.bind="number1 & validate">
<input id="number1" type="text" number-value.bind="number1 & validate">
<number-input id="number2" value.bind="number2 & validate"></number-input>
<input id="password" type="text" value.bind="password & validate">
<input id="confirmPassword" type="text" value.bind="confirmPassword & validate">
Expand All @@ -23,6 +24,9 @@ export class RegistrationForm {
public firstName = '';
public lastName = '';
public email = '';
public settings = {
subprop : ''
};
public number1 = 0;
public number2 = 0;
public password = '';
Expand Down Expand Up @@ -52,6 +56,7 @@ ValidationRules.customRule(
ValidationRules
.ensure((f: RegistrationForm) => f.firstName).required()
.ensure(f => f.lastName).required()
.ensure(f => f.settings.subprop).required()
.ensure('email').required().email()
.ensure(f => f.number1).satisfies(value => value > 0)
.ensure(f => f.number2).satisfies(value => value > 0).withMessage('${displayName} gots to be greater than zero.')
Expand Down
3 changes: 2 additions & 1 deletion test/validation-parser.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Expression, AccessScope } from 'aurelia-binding';
import { Expression, AccessScope,AccessMember } from 'aurelia-binding';
import { Container } from 'aurelia-dependency-injection';
import { BindingLanguage } from 'aurelia-templating';
import { TemplatingBindingLanguage } from 'aurelia-templating-binding';
Expand Down Expand Up @@ -29,6 +29,7 @@ describe('Validator', () => {
expect(parse('a =>a.b')).toEqual(new AccessScope('b', 0));
expect(parse('a=> a.b')).toEqual(new AccessScope('b', 0));
expect(parse('a => a.b')).toEqual(new AccessScope('b', 0));
expect(parse('a => a.b.c')).toEqual(new AccessMember(new AccessScope('b',0),'c'));
expect(parse('a => a.bcde')).toEqual(new AccessScope('bcde', 0));
expect(parse('_ => _.b')).toEqual(new AccessScope('b', 0));
expect(parse('$ => $.b')).toEqual(new AccessScope('b', 0));
Expand Down

0 comments on commit 709ae6a

Please sign in to comment.