From f4663748edb3b985adff468d8b0c9836ce1b759a Mon Sep 17 00:00:00 2001 From: Eric Joyce Date: Mon, 19 Sep 2016 08:58:21 -0400 Subject: [PATCH] enhance(ValidationParser): handle validate props that subpropeties of an object fixes #283 --- .../implementation/standard-validator.d.ts | 2 +- dist/amd/implementation/standard-validator.js | 7 + dist/amd/implementation/validation-parser.js | 29 ++- dist/amd/property-info.d.ts | 1 + dist/amd/property-info.js | 12 +- dist/amd/validation-controller.js | 24 ++- .../implementation/standard-validator.d.ts | 2 +- .../implementation/standard-validator.js | 7 + .../implementation/validation-parser.js | 29 ++- dist/commonjs/property-info.d.ts | 1 + dist/commonjs/property-info.js | 12 +- dist/commonjs/validation-controller.js | 23 +- .../implementation/standard-validator.d.ts | 2 +- .../implementation/standard-validator.js | 9 +- .../implementation/validation-parser.js | 29 ++- dist/es2015/property-info.d.ts | 1 + dist/es2015/property-info.js | 12 +- dist/es2015/validation-controller.js | 11 +- .../implementation/standard-validator.d.ts | 2 +- .../implementation/standard-validator.js | 7 + .../implementation/validation-parser.js | 29 ++- dist/native-modules/property-info.d.ts | 1 + dist/native-modules/property-info.js | 12 +- dist/native-modules/validation-controller.js | 9 +- .../implementation/standard-validator.d.ts | 2 +- .../implementation/standard-validator.js | 7 + .../implementation/validation-parser.js | 29 ++- dist/system/property-info.d.ts | 1 + dist/system/property-info.js | 12 +- dist/system/validation-controller.js | 29 ++- package.json | 199 +++++++++--------- src/implementation/standard-validator.ts | 24 ++- src/implementation/validation-parser.ts | 36 ++-- src/property-info.ts | 14 +- src/validation-controller.ts | 39 ++-- test/basic.ts | 28 ++- test/resources/registration-form.ts | 10 +- 37 files changed, 450 insertions(+), 253 deletions(-) diff --git a/dist/amd/implementation/standard-validator.d.ts b/dist/amd/implementation/standard-validator.d.ts index d17f819b..3594b7a1 100644 --- a/dist/amd/implementation/standard-validator.d.ts +++ b/dist/amd/implementation/standard-validator.d.ts @@ -7,7 +7,7 @@ import { ValidationMessageProvider } from './validation-messages'; * Responsible for validating objects and properties. */ export declare class StandardValidator extends Validator { - static inject: (typeof ValidationMessageProvider | typeof ViewResources)[]; + static inject: (typeof ViewResources | typeof ValidationMessageProvider)[]; private messageProvider; private lookupFunctions; private getDisplayName; diff --git a/dist/amd/implementation/standard-validator.js b/dist/amd/implementation/standard-validator.js index dbb409f3..b33d3ffc 100644 --- a/dist/amd/implementation/standard-validator.js +++ b/dist/amd/implementation/standard-validator.js @@ -65,6 +65,13 @@ define(["require", "exports", 'aurelia-templating', '../validator', '../validati } // validate. var value = rule.property.name === null ? object : object[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 object containing the field. get the last propertyy in the chain + //to get the field name. Use thi to get the correct value. + var parts = rule.property.name.split('.'); + value = object[parts[parts.length - 1]]; + } var promiseOrBoolean = rule.condition(value, object); if (promiseOrBoolean instanceof Promise) { promises.push(promiseOrBoolean.then(function (isValid) { diff --git a/dist/amd/implementation/validation-parser.js b/dist/amd/implementation/validation-parser.js index 82fc7d03..45250e1a 100644 --- a/dist/amd/implementation/validation-parser.js +++ b/dist/amd/implementation/validation-parser.js @@ -35,26 +35,33 @@ define(["require", "exports", 'aurelia-binding', 'aurelia-templating', './util', return expression; }; ValidationParser.prototype.getAccessorExpression = function (fn) { - var classic = /^function\s*\([$_\w\d]+\)\s*\{\s*(?:"use strict";)?\s*return\s+[$_\w\d]+\.([$_\w\d]+)\s*;?\s*\}$/; - var arrow = /^[$_\w\d]+\s*=>\s*[$_\w\d]+\.([$_\w\d]+)$/; + var classic = /^function\s*\([$_\w\d]+\)\s*\{\s*(?:"use strict";)?\s*.*return\s+[$_\w\d]+((\.[$_\w\d]+)+)\s*;?\s*\}$/; + var arrow = /^\(?[$_\w\d]+\)?\s*=>\s*(?:\{?.*return\s+)?[$_\w\d]+((\.[$_\w\d]+)+);?\s*\}?$/; var match = classic.exec(fn) || arrow.exec(fn); if (match === null) { throw new Error("Unable to parse accessor function:\n" + fn); } - return this.parser.parse(match[1]); + var name = match[1][0] == "." ? match[1].substr(1) : match[1]; + return this.parser.parse(name); }; ValidationParser.prototype.parseProperty = function (property) { - var accessor; if (util_1.isString(property)) { - accessor = this.parser.parse(property); + return { name: property, displayName: null }; } - else { - accessor = this.getAccessorExpression(property.toString()); - } - if (accessor instanceof aurelia_binding_1.AccessScope - || accessor instanceof aurelia_binding_1.AccessMember && accessor.object instanceof aurelia_binding_1.AccessScope) { + var accessor = this.getAccessorExpression(property.toString()); + var isSubProp = accessor instanceof aurelia_binding_1.AccessMember && accessor.object instanceof aurelia_binding_1.AccessScope; + if (accessor instanceof aurelia_binding_1.AccessScope || isSubProp) { + var propName = accessor.name; + if (isSubProp) { + //iterate up the chain until we are in the 1st sub-object of the root object. + var ao = accessor.object; + while (ao) { + propName = ao.name + '.' + propName; + ao = ao.object; + } + } return { - name: accessor.name, + name: propName, displayName: null }; } diff --git a/dist/amd/property-info.d.ts b/dist/amd/property-info.d.ts index ea68d9a0..469635ce 100644 --- a/dist/amd/property-info.d.ts +++ b/dist/amd/property-info.d.ts @@ -7,4 +7,5 @@ import { Expression } from 'aurelia-binding'; export declare function getPropertyInfo(expression: Expression, source: any): { object: any; propertyName: string; + ruleSrc: null; }; diff --git a/dist/amd/property-info.js b/dist/amd/property-info.js index 911ae7a1..91cb1f5f 100644 --- a/dist/amd/property-info.js +++ b/dist/amd/property-info.js @@ -25,6 +25,7 @@ define(["require", "exports", 'aurelia-binding'], function (require, exports, au } var object; var propertyName; + var ruleSrc = null; if (expression instanceof aurelia_binding_1.AccessScope) { object = source.bindingContext; propertyName = expression.name; @@ -32,6 +33,15 @@ define(["require", "exports", 'aurelia-binding'], function (require, exports, au else if (expression instanceof aurelia_binding_1.AccessMember) { object = getObject(originalExpression, expression.object, source); propertyName = expression.name; + if (expression.object) { + //build the path to the property from the object root. + var exp = expression.object; + while (exp.object) { + propertyName = exp.name + '.' + propertyName; + exp = exp.object; + } + ruleSrc = getObject(originalExpression, exp, source); + } } else if (expression instanceof aurelia_binding_1.AccessKeyed) { object = getObject(originalExpression, expression.object, source); @@ -40,7 +50,7 @@ define(["require", "exports", 'aurelia-binding'], function (require, exports, au else { throw new Error("Expression '" + originalExpression + "' is not compatible with the validate binding-behavior."); } - return { object: object, propertyName: propertyName }; + return { object: object, propertyName: propertyName, ruleSrc: ruleSrc }; } exports.getPropertyInfo = getPropertyInfo; }); diff --git a/dist/amd/validation-controller.js b/dist/amd/validation-controller.js index a950fc56..e15494f0 100644 --- a/dist/amd/validation-controller.js +++ b/dist/amd/validation-controller.js @@ -1,4 +1,4 @@ -define(["require", "exports", './validator', './validate-trigger', './property-info', './validation-error'], function (require, exports, validator_1, validate_trigger_1, property_info_1, validation_error_1) { +define(["require", "exports", './validator', './validate-trigger', './property-info', './validation-error', './implementation/rules'], function (require, exports, validator_1, validate_trigger_1, property_info_1, validation_error_1, rules_1) { "use strict"; /** * Orchestrates validation. @@ -112,7 +112,7 @@ define(["require", "exports", './validator', './validate-trigger', './property-i */ ValidationController.prototype.getInstructionPredicate = function (instruction) { if (instruction) { - var object_1 = instruction.object, propertyName_1 = instruction.propertyName, rules_1 = instruction.rules; + var object_1 = instruction.object, propertyName_1 = instruction.propertyName, rules_2 = instruction.rules; var predicate_1; if (instruction.propertyName) { predicate_1 = function (x) { return x.object === object_1 && x.propertyName === propertyName_1; }; @@ -121,8 +121,8 @@ define(["require", "exports", './validator', './validate-trigger', './property-i predicate_1 = function (x) { return x.object === object_1; }; } // todo: move to Validator interface: - if (rules_1 && rules_1.indexOf) { - return function (x) { return predicate_1(x) && rules_1.indexOf(x.rule) !== -1; }; + if (rules_2 && rules_2.indexOf) { + return function (x) { return predicate_1(x) && rules_2.indexOf(x.rule) !== -1; }; } return predicate_1; } @@ -139,17 +139,17 @@ define(["require", "exports", './validator', './validate-trigger', './property-i // Get a function that will process the validation instruction. var execute; if (instruction) { - var object_2 = instruction.object, propertyName_2 = instruction.propertyName, rules_2 = instruction.rules; + var object_2 = instruction.object, propertyName_2 = instruction.propertyName, rules_3 = instruction.rules; // if rules were not specified, check the object map. - rules_2 = rules_2 || this.objects.get(object_2); + rules_3 = rules_3 || this.objects.get(object_2); // property specified? if (instruction.propertyName === undefined) { // validate the specified object. - execute = function () { return _this.validator.validateObject(object_2, rules_2); }; + execute = function () { return _this.validator.validateObject(object_2, rules_3); }; } else { // validate the specified property. - execute = function () { return _this.validator.validateProperty(object_2, propertyName_2, rules_2); }; + execute = function () { return _this.validator.validateProperty(object_2, propertyName_2, rules_3); }; } } else { @@ -241,7 +241,7 @@ define(["require", "exports", './validator', './validate-trigger', './property-i this_1.errors.splice(this_1.errors.indexOf(oldError), 1); } else { - // there is a corresponding new error... + // there is a corresponding new error... var newError = newErrors.splice(newErrorIndex, 1)[0]; // get the elements that are associated with the new error. var elements_1 = this_1.getAssociatedElements(newError); @@ -279,9 +279,13 @@ define(["require", "exports", './validator', './validate-trigger', './property-i if (!binding.isBound) { return; } - var _a = property_info_1.getPropertyInfo(binding.sourceExpression, binding.source), object = _a.object, propertyName = _a.propertyName; + var _a = property_info_1.getPropertyInfo(binding.sourceExpression, binding.source), object = _a.object, propertyName = _a.propertyName, ruleSrc = _a.ruleSrc; var registeredBinding = this.bindings.get(binding); var rules = registeredBinding ? registeredBinding.rules : undefined; + if (!rules && ruleSrc) { + //if we got ruleSrc back we need to get the rules for the subprop which are located in the root of the model + rules = rules_1.Rules.get(ruleSrc); + } this.validate({ object: object, propertyName: propertyName, rules: rules }); }; /** diff --git a/dist/commonjs/implementation/standard-validator.d.ts b/dist/commonjs/implementation/standard-validator.d.ts index d17f819b..3594b7a1 100644 --- a/dist/commonjs/implementation/standard-validator.d.ts +++ b/dist/commonjs/implementation/standard-validator.d.ts @@ -7,7 +7,7 @@ import { ValidationMessageProvider } from './validation-messages'; * Responsible for validating objects and properties. */ export declare class StandardValidator extends Validator { - static inject: (typeof ValidationMessageProvider | typeof ViewResources)[]; + static inject: (typeof ViewResources | typeof ValidationMessageProvider)[]; private messageProvider; private lookupFunctions; private getDisplayName; diff --git a/dist/commonjs/implementation/standard-validator.js b/dist/commonjs/implementation/standard-validator.js index ef860752..e807a481 100644 --- a/dist/commonjs/implementation/standard-validator.js +++ b/dist/commonjs/implementation/standard-validator.js @@ -69,6 +69,13 @@ var StandardValidator = (function (_super) { } // validate. var value = rule.property.name === null ? object : object[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 object containing the field. get the last propertyy in the chain + //to get the field name. Use thi to get the correct value. + var parts = rule.property.name.split('.'); + value = object[parts[parts.length - 1]]; + } var promiseOrBoolean = rule.condition(value, object); if (promiseOrBoolean instanceof Promise) { promises.push(promiseOrBoolean.then(function (isValid) { diff --git a/dist/commonjs/implementation/validation-parser.js b/dist/commonjs/implementation/validation-parser.js index b02edd02..fd2839ad 100644 --- a/dist/commonjs/implementation/validation-parser.js +++ b/dist/commonjs/implementation/validation-parser.js @@ -38,26 +38,33 @@ var ValidationParser = (function () { return expression; }; ValidationParser.prototype.getAccessorExpression = function (fn) { - var classic = /^function\s*\([$_\w\d]+\)\s*\{\s*(?:"use strict";)?\s*return\s+[$_\w\d]+\.([$_\w\d]+)\s*;?\s*\}$/; - var arrow = /^[$_\w\d]+\s*=>\s*[$_\w\d]+\.([$_\w\d]+)$/; + var classic = /^function\s*\([$_\w\d]+\)\s*\{\s*(?:"use strict";)?\s*.*return\s+[$_\w\d]+((\.[$_\w\d]+)+)\s*;?\s*\}$/; + var arrow = /^\(?[$_\w\d]+\)?\s*=>\s*(?:\{?.*return\s+)?[$_\w\d]+((\.[$_\w\d]+)+);?\s*\}?$/; var match = classic.exec(fn) || arrow.exec(fn); if (match === null) { throw new Error("Unable to parse accessor function:\n" + fn); } - return this.parser.parse(match[1]); + var name = match[1][0] == "." ? match[1].substr(1) : match[1]; + return this.parser.parse(name); }; ValidationParser.prototype.parseProperty = function (property) { - var accessor; if (util_1.isString(property)) { - accessor = this.parser.parse(property); + return { name: property, displayName: null }; } - else { - accessor = this.getAccessorExpression(property.toString()); - } - if (accessor instanceof aurelia_binding_1.AccessScope - || accessor instanceof aurelia_binding_1.AccessMember && accessor.object instanceof aurelia_binding_1.AccessScope) { + var accessor = this.getAccessorExpression(property.toString()); + var isSubProp = accessor instanceof aurelia_binding_1.AccessMember && accessor.object instanceof aurelia_binding_1.AccessScope; + if (accessor instanceof aurelia_binding_1.AccessScope || isSubProp) { + var propName = accessor.name; + if (isSubProp) { + //iterate up the chain until we are in the 1st sub-object of the root object. + var ao = accessor.object; + while (ao) { + propName = ao.name + '.' + propName; + ao = ao.object; + } + } return { - name: accessor.name, + name: propName, displayName: null }; } diff --git a/dist/commonjs/property-info.d.ts b/dist/commonjs/property-info.d.ts index ea68d9a0..469635ce 100644 --- a/dist/commonjs/property-info.d.ts +++ b/dist/commonjs/property-info.d.ts @@ -7,4 +7,5 @@ import { Expression } from 'aurelia-binding'; export declare function getPropertyInfo(expression: Expression, source: any): { object: any; propertyName: string; + ruleSrc: null; }; diff --git a/dist/commonjs/property-info.js b/dist/commonjs/property-info.js index 9a8030de..701b6659 100644 --- a/dist/commonjs/property-info.js +++ b/dist/commonjs/property-info.js @@ -25,6 +25,7 @@ function getPropertyInfo(expression, source) { } var object; var propertyName; + var ruleSrc = null; if (expression instanceof aurelia_binding_1.AccessScope) { object = source.bindingContext; propertyName = expression.name; @@ -32,6 +33,15 @@ function getPropertyInfo(expression, source) { else if (expression instanceof aurelia_binding_1.AccessMember) { object = getObject(originalExpression, expression.object, source); propertyName = expression.name; + if (expression.object) { + //build the path to the property from the object root. + var exp = expression.object; + while (exp.object) { + propertyName = exp.name + '.' + propertyName; + exp = exp.object; + } + ruleSrc = getObject(originalExpression, exp, source); + } } else if (expression instanceof aurelia_binding_1.AccessKeyed) { object = getObject(originalExpression, expression.object, source); @@ -40,6 +50,6 @@ function getPropertyInfo(expression, source) { else { throw new Error("Expression '" + originalExpression + "' is not compatible with the validate binding-behavior."); } - return { object: object, propertyName: propertyName }; + return { object: object, propertyName: propertyName, ruleSrc: ruleSrc }; } exports.getPropertyInfo = getPropertyInfo; diff --git a/dist/commonjs/validation-controller.js b/dist/commonjs/validation-controller.js index a85eeb6e..554c0471 100644 --- a/dist/commonjs/validation-controller.js +++ b/dist/commonjs/validation-controller.js @@ -3,6 +3,7 @@ var validator_1 = require('./validator'); var validate_trigger_1 = require('./validate-trigger'); var property_info_1 = require('./property-info'); var validation_error_1 = require('./validation-error'); +var rules_1 = require('./implementation/rules'); /** * Orchestrates validation. * Manages a set of bindings, renderers and objects. @@ -115,7 +116,7 @@ var ValidationController = (function () { */ ValidationController.prototype.getInstructionPredicate = function (instruction) { if (instruction) { - var object_1 = instruction.object, propertyName_1 = instruction.propertyName, rules_1 = instruction.rules; + var object_1 = instruction.object, propertyName_1 = instruction.propertyName, rules_2 = instruction.rules; var predicate_1; if (instruction.propertyName) { predicate_1 = function (x) { return x.object === object_1 && x.propertyName === propertyName_1; }; @@ -124,8 +125,8 @@ var ValidationController = (function () { predicate_1 = function (x) { return x.object === object_1; }; } // todo: move to Validator interface: - if (rules_1 && rules_1.indexOf) { - return function (x) { return predicate_1(x) && rules_1.indexOf(x.rule) !== -1; }; + if (rules_2 && rules_2.indexOf) { + return function (x) { return predicate_1(x) && rules_2.indexOf(x.rule) !== -1; }; } return predicate_1; } @@ -142,17 +143,17 @@ var ValidationController = (function () { // Get a function that will process the validation instruction. var execute; if (instruction) { - var object_2 = instruction.object, propertyName_2 = instruction.propertyName, rules_2 = instruction.rules; + var object_2 = instruction.object, propertyName_2 = instruction.propertyName, rules_3 = instruction.rules; // if rules were not specified, check the object map. - rules_2 = rules_2 || this.objects.get(object_2); + rules_3 = rules_3 || this.objects.get(object_2); // property specified? if (instruction.propertyName === undefined) { // validate the specified object. - execute = function () { return _this.validator.validateObject(object_2, rules_2); }; + execute = function () { return _this.validator.validateObject(object_2, rules_3); }; } else { // validate the specified property. - execute = function () { return _this.validator.validateProperty(object_2, propertyName_2, rules_2); }; + execute = function () { return _this.validator.validateProperty(object_2, propertyName_2, rules_3); }; } } else { @@ -244,7 +245,7 @@ var ValidationController = (function () { this_1.errors.splice(this_1.errors.indexOf(oldError), 1); } else { - // there is a corresponding new error... + // there is a corresponding new error... var newError = newErrors.splice(newErrorIndex, 1)[0]; // get the elements that are associated with the new error. var elements_1 = this_1.getAssociatedElements(newError); @@ -282,9 +283,13 @@ var ValidationController = (function () { if (!binding.isBound) { return; } - var _a = property_info_1.getPropertyInfo(binding.sourceExpression, binding.source), object = _a.object, propertyName = _a.propertyName; + var _a = property_info_1.getPropertyInfo(binding.sourceExpression, binding.source), object = _a.object, propertyName = _a.propertyName, ruleSrc = _a.ruleSrc; var registeredBinding = this.bindings.get(binding); var rules = registeredBinding ? registeredBinding.rules : undefined; + if (!rules && ruleSrc) { + //if we got ruleSrc back we need to get the rules for the subprop which are located in the root of the model + rules = rules_1.Rules.get(ruleSrc); + } this.validate({ object: object, propertyName: propertyName, rules: rules }); }; /** diff --git a/dist/es2015/implementation/standard-validator.d.ts b/dist/es2015/implementation/standard-validator.d.ts index d17f819b..3594b7a1 100644 --- a/dist/es2015/implementation/standard-validator.d.ts +++ b/dist/es2015/implementation/standard-validator.d.ts @@ -7,7 +7,7 @@ import { ValidationMessageProvider } from './validation-messages'; * Responsible for validating objects and properties. */ export declare class StandardValidator extends Validator { - static inject: (typeof ValidationMessageProvider | typeof ViewResources)[]; + static inject: (typeof ViewResources | typeof ValidationMessageProvider)[]; private messageProvider; private lookupFunctions; private getDisplayName; diff --git a/dist/es2015/implementation/standard-validator.js b/dist/es2015/implementation/standard-validator.js index baa9c04e..5a02d78a 100644 --- a/dist/es2015/implementation/standard-validator.js +++ b/dist/es2015/implementation/standard-validator.js @@ -60,7 +60,14 @@ export class StandardValidator extends Validator { continue; } // validate. - const value = rule.property.name === null ? object : object[rule.property.name]; + let value = rule.property.name === null ? object : object[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 object containing the field. get the last propertyy in the chain + //to get the field name. Use thi to get the correct value. + let parts = rule.property.name.split('.'); + value = object[parts[parts.length - 1]]; + } const promiseOrBoolean = rule.condition(value, object); if (promiseOrBoolean instanceof Promise) { promises.push(promiseOrBoolean.then(isValid => { diff --git a/dist/es2015/implementation/validation-parser.js b/dist/es2015/implementation/validation-parser.js index a693e726..0c429329 100644 --- a/dist/es2015/implementation/validation-parser.js +++ b/dist/es2015/implementation/validation-parser.js @@ -32,26 +32,33 @@ export class ValidationParser { return expression; } getAccessorExpression(fn) { - 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]+)+)\s*;?\s*\}$/; + const arrow = /^\(?[$_\w\d]+\)?\s*=>\s*(?:\{?.*return\s+)?[$_\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}`); } - return this.parser.parse(match[1]); + const name = match[1][0] == "." ? match[1].substr(1) : match[1]; + return this.parser.parse(name); } parseProperty(property) { - let accessor; if (isString(property)) { - accessor = this.parser.parse(property); + return { name: property, displayName: null }; } - else { - accessor = this.getAccessorExpression(property.toString()); - } - if (accessor instanceof AccessScope - || accessor instanceof AccessMember && accessor.object instanceof AccessScope) { + const accessor = this.getAccessorExpression(property.toString()); + const isSubProp = accessor instanceof AccessMember && accessor.object instanceof AccessScope; + if (accessor instanceof AccessScope || isSubProp) { + let propName = accessor.name; + if (isSubProp) { + //iterate up the chain until we are in the 1st sub-object of the root object. + let ao = accessor.object; + while (ao) { + propName = ao.name + '.' + propName; + ao = ao.object; + } + } return { - name: accessor.name, + name: propName, displayName: null }; } diff --git a/dist/es2015/property-info.d.ts b/dist/es2015/property-info.d.ts index ea68d9a0..469635ce 100644 --- a/dist/es2015/property-info.d.ts +++ b/dist/es2015/property-info.d.ts @@ -7,4 +7,5 @@ import { Expression } from 'aurelia-binding'; export declare function getPropertyInfo(expression: Expression, source: any): { object: any; propertyName: string; + ruleSrc: null; }; diff --git a/dist/es2015/property-info.js b/dist/es2015/property-info.js index 19d445b9..461f43f7 100644 --- a/dist/es2015/property-info.js +++ b/dist/es2015/property-info.js @@ -24,6 +24,7 @@ export function getPropertyInfo(expression, source) { } let object; let propertyName; + let ruleSrc = null; if (expression instanceof AccessScope) { object = source.bindingContext; propertyName = expression.name; @@ -31,6 +32,15 @@ export function getPropertyInfo(expression, source) { 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 = expression.object; + while (exp.object) { + propertyName = exp.name + '.' + propertyName; + exp = exp.object; + } + ruleSrc = getObject(originalExpression, exp, source); + } } else if (expression instanceof AccessKeyed) { object = getObject(originalExpression, expression.object, source); @@ -39,5 +49,5 @@ export function getPropertyInfo(expression, source) { else { throw new Error(`Expression '${originalExpression}' is not compatible with the validate binding-behavior.`); } - return { object, propertyName }; + return { object, propertyName, ruleSrc }; } diff --git a/dist/es2015/validation-controller.js b/dist/es2015/validation-controller.js index 7cb3f54d..c74fdba0 100644 --- a/dist/es2015/validation-controller.js +++ b/dist/es2015/validation-controller.js @@ -2,6 +2,7 @@ import { Validator } from './validator'; import { validateTrigger } from './validate-trigger'; import { getPropertyInfo } from './property-info'; import { ValidationError } from './validation-error'; +import { Rules } from './implementation/rules'; /** * Orchestrates validation. * Manages a set of bindings, renderers and objects. @@ -236,7 +237,7 @@ export class ValidationController { this.errors.splice(this.errors.indexOf(oldError), 1); } else { - // there is a corresponding new error... + // there is a corresponding new error... const newError = newErrors.splice(newErrorIndex, 1)[0]; // get the elements that are associated with the new error. const elements = this.getAssociatedElements(newError); @@ -267,9 +268,13 @@ export class ValidationController { if (!binding.isBound) { return; } - const { object, propertyName } = getPropertyInfo(binding.sourceExpression, binding.source); + const { object, propertyName, ruleSrc } = getPropertyInfo(binding.sourceExpression, binding.source); const registeredBinding = this.bindings.get(binding); - const rules = registeredBinding ? registeredBinding.rules : undefined; + let rules = registeredBinding ? registeredBinding.rules : undefined; + if (!rules && ruleSrc) { + //if we got ruleSrc back we need to get the rules for the subprop which are located in the root of the model + rules = Rules.get(ruleSrc); + } this.validate({ object, propertyName, rules }); } /** diff --git a/dist/native-modules/implementation/standard-validator.d.ts b/dist/native-modules/implementation/standard-validator.d.ts index d17f819b..3594b7a1 100644 --- a/dist/native-modules/implementation/standard-validator.d.ts +++ b/dist/native-modules/implementation/standard-validator.d.ts @@ -7,7 +7,7 @@ import { ValidationMessageProvider } from './validation-messages'; * Responsible for validating objects and properties. */ export declare class StandardValidator extends Validator { - static inject: (typeof ValidationMessageProvider | typeof ViewResources)[]; + static inject: (typeof ViewResources | typeof ValidationMessageProvider)[]; private messageProvider; private lookupFunctions; private getDisplayName; diff --git a/dist/native-modules/implementation/standard-validator.js b/dist/native-modules/implementation/standard-validator.js index 87196afe..22e73c59 100644 --- a/dist/native-modules/implementation/standard-validator.js +++ b/dist/native-modules/implementation/standard-validator.js @@ -68,6 +68,13 @@ export var StandardValidator = (function (_super) { } // validate. var value = rule.property.name === null ? object : object[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 object containing the field. get the last propertyy in the chain + //to get the field name. Use thi to get the correct value. + var parts = rule.property.name.split('.'); + value = object[parts[parts.length - 1]]; + } var promiseOrBoolean = rule.condition(value, object); if (promiseOrBoolean instanceof Promise) { promises.push(promiseOrBoolean.then(function (isValid) { diff --git a/dist/native-modules/implementation/validation-parser.js b/dist/native-modules/implementation/validation-parser.js index d74994c1..8beb5e7b 100644 --- a/dist/native-modules/implementation/validation-parser.js +++ b/dist/native-modules/implementation/validation-parser.js @@ -37,26 +37,33 @@ export var ValidationParser = (function () { return expression; }; ValidationParser.prototype.getAccessorExpression = function (fn) { - var classic = /^function\s*\([$_\w\d]+\)\s*\{\s*(?:"use strict";)?\s*return\s+[$_\w\d]+\.([$_\w\d]+)\s*;?\s*\}$/; - var arrow = /^[$_\w\d]+\s*=>\s*[$_\w\d]+\.([$_\w\d]+)$/; + var classic = /^function\s*\([$_\w\d]+\)\s*\{\s*(?:"use strict";)?\s*.*return\s+[$_\w\d]+((\.[$_\w\d]+)+)\s*;?\s*\}$/; + var arrow = /^\(?[$_\w\d]+\)?\s*=>\s*(?:\{?.*return\s+)?[$_\w\d]+((\.[$_\w\d]+)+);?\s*\}?$/; var match = classic.exec(fn) || arrow.exec(fn); if (match === null) { throw new Error("Unable to parse accessor function:\n" + fn); } - return this.parser.parse(match[1]); + var name = match[1][0] == "." ? match[1].substr(1) : match[1]; + return this.parser.parse(name); }; ValidationParser.prototype.parseProperty = function (property) { - var accessor; if (isString(property)) { - accessor = this.parser.parse(property); + return { name: property, displayName: null }; } - else { - accessor = this.getAccessorExpression(property.toString()); - } - if (accessor instanceof AccessScope - || accessor instanceof AccessMember && accessor.object instanceof AccessScope) { + var accessor = this.getAccessorExpression(property.toString()); + var isSubProp = accessor instanceof AccessMember && accessor.object instanceof AccessScope; + if (accessor instanceof AccessScope || isSubProp) { + var propName = accessor.name; + if (isSubProp) { + //iterate up the chain until we are in the 1st sub-object of the root object. + var ao = accessor.object; + while (ao) { + propName = ao.name + '.' + propName; + ao = ao.object; + } + } return { - name: accessor.name, + name: propName, displayName: null }; } diff --git a/dist/native-modules/property-info.d.ts b/dist/native-modules/property-info.d.ts index ea68d9a0..469635ce 100644 --- a/dist/native-modules/property-info.d.ts +++ b/dist/native-modules/property-info.d.ts @@ -7,4 +7,5 @@ import { Expression } from 'aurelia-binding'; export declare function getPropertyInfo(expression: Expression, source: any): { object: any; propertyName: string; + ruleSrc: null; }; diff --git a/dist/native-modules/property-info.js b/dist/native-modules/property-info.js index 5b2e3be2..7ca1b839 100644 --- a/dist/native-modules/property-info.js +++ b/dist/native-modules/property-info.js @@ -24,6 +24,7 @@ export function getPropertyInfo(expression, source) { } var object; var propertyName; + var ruleSrc = null; if (expression instanceof AccessScope) { object = source.bindingContext; propertyName = expression.name; @@ -31,6 +32,15 @@ export function getPropertyInfo(expression, source) { 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. + var exp = expression.object; + while (exp.object) { + propertyName = exp.name + '.' + propertyName; + exp = exp.object; + } + ruleSrc = getObject(originalExpression, exp, source); + } } else if (expression instanceof AccessKeyed) { object = getObject(originalExpression, expression.object, source); @@ -39,5 +49,5 @@ export function getPropertyInfo(expression, source) { else { throw new Error("Expression '" + originalExpression + "' is not compatible with the validate binding-behavior."); } - return { object: object, propertyName: propertyName }; + return { object: object, propertyName: propertyName, ruleSrc: ruleSrc }; } diff --git a/dist/native-modules/validation-controller.js b/dist/native-modules/validation-controller.js index 02d56385..33e7f090 100644 --- a/dist/native-modules/validation-controller.js +++ b/dist/native-modules/validation-controller.js @@ -2,6 +2,7 @@ import { Validator } from './validator'; import { validateTrigger } from './validate-trigger'; import { getPropertyInfo } from './property-info'; import { ValidationError } from './validation-error'; +import { Rules } from './implementation/rules'; /** * Orchestrates validation. * Manages a set of bindings, renderers and objects. @@ -243,7 +244,7 @@ export var ValidationController = (function () { this_1.errors.splice(this_1.errors.indexOf(oldError), 1); } else { - // there is a corresponding new error... + // there is a corresponding new error... var newError = newErrors.splice(newErrorIndex, 1)[0]; // get the elements that are associated with the new error. var elements_1 = this_1.getAssociatedElements(newError); @@ -281,9 +282,13 @@ export var ValidationController = (function () { if (!binding.isBound) { return; } - var _a = getPropertyInfo(binding.sourceExpression, binding.source), object = _a.object, propertyName = _a.propertyName; + var _a = getPropertyInfo(binding.sourceExpression, binding.source), object = _a.object, propertyName = _a.propertyName, ruleSrc = _a.ruleSrc; var registeredBinding = this.bindings.get(binding); var rules = registeredBinding ? registeredBinding.rules : undefined; + if (!rules && ruleSrc) { + //if we got ruleSrc back we need to get the rules for the subprop which are located in the root of the model + rules = Rules.get(ruleSrc); + } this.validate({ object: object, propertyName: propertyName, rules: rules }); }; /** diff --git a/dist/system/implementation/standard-validator.d.ts b/dist/system/implementation/standard-validator.d.ts index d17f819b..3594b7a1 100644 --- a/dist/system/implementation/standard-validator.d.ts +++ b/dist/system/implementation/standard-validator.d.ts @@ -7,7 +7,7 @@ import { ValidationMessageProvider } from './validation-messages'; * Responsible for validating objects and properties. */ export declare class StandardValidator extends Validator { - static inject: (typeof ValidationMessageProvider | typeof ViewResources)[]; + static inject: (typeof ViewResources | typeof ValidationMessageProvider)[]; private messageProvider; private lookupFunctions; private getDisplayName; diff --git a/dist/system/implementation/standard-validator.js b/dist/system/implementation/standard-validator.js index 714a3de9..7798233a 100644 --- a/dist/system/implementation/standard-validator.js +++ b/dist/system/implementation/standard-validator.js @@ -86,6 +86,13 @@ System.register(['aurelia-templating', '../validator', '../validation-error', '. } // validate. var value = rule.property.name === null ? object : object[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 object containing the field. get the last propertyy in the chain + //to get the field name. Use thi to get the correct value. + var parts = rule.property.name.split('.'); + value = object[parts[parts.length - 1]]; + } var promiseOrBoolean = rule.condition(value, object); if (promiseOrBoolean instanceof Promise) { promises.push(promiseOrBoolean.then(function (isValid) { diff --git a/dist/system/implementation/validation-parser.js b/dist/system/implementation/validation-parser.js index 730bfd46..e1b4d7d1 100644 --- a/dist/system/implementation/validation-parser.js +++ b/dist/system/implementation/validation-parser.js @@ -53,26 +53,33 @@ System.register(['aurelia-binding', 'aurelia-templating', './util', 'aurelia-log return expression; }; ValidationParser.prototype.getAccessorExpression = function (fn) { - var classic = /^function\s*\([$_\w\d]+\)\s*\{\s*(?:"use strict";)?\s*return\s+[$_\w\d]+\.([$_\w\d]+)\s*;?\s*\}$/; - var arrow = /^[$_\w\d]+\s*=>\s*[$_\w\d]+\.([$_\w\d]+)$/; + var classic = /^function\s*\([$_\w\d]+\)\s*\{\s*(?:"use strict";)?\s*.*return\s+[$_\w\d]+((\.[$_\w\d]+)+)\s*;?\s*\}$/; + var arrow = /^\(?[$_\w\d]+\)?\s*=>\s*(?:\{?.*return\s+)?[$_\w\d]+((\.[$_\w\d]+)+);?\s*\}?$/; var match = classic.exec(fn) || arrow.exec(fn); if (match === null) { throw new Error("Unable to parse accessor function:\n" + fn); } - return this.parser.parse(match[1]); + var name = match[1][0] == "." ? match[1].substr(1) : match[1]; + return this.parser.parse(name); }; ValidationParser.prototype.parseProperty = function (property) { - var accessor; if (util_1.isString(property)) { - accessor = this.parser.parse(property); + return { name: property, displayName: null }; } - else { - accessor = this.getAccessorExpression(property.toString()); - } - if (accessor instanceof aurelia_binding_1.AccessScope - || accessor instanceof aurelia_binding_1.AccessMember && accessor.object instanceof aurelia_binding_1.AccessScope) { + var accessor = this.getAccessorExpression(property.toString()); + var isSubProp = accessor instanceof aurelia_binding_1.AccessMember && accessor.object instanceof aurelia_binding_1.AccessScope; + if (accessor instanceof aurelia_binding_1.AccessScope || isSubProp) { + var propName = accessor.name; + if (isSubProp) { + //iterate up the chain until we are in the 1st sub-object of the root object. + var ao = accessor.object; + while (ao) { + propName = ao.name + '.' + propName; + ao = ao.object; + } + } return { - name: accessor.name, + name: propName, displayName: null }; } diff --git a/dist/system/property-info.d.ts b/dist/system/property-info.d.ts index ea68d9a0..469635ce 100644 --- a/dist/system/property-info.d.ts +++ b/dist/system/property-info.d.ts @@ -7,4 +7,5 @@ import { Expression } from 'aurelia-binding'; export declare function getPropertyInfo(expression: Expression, source: any): { object: any; propertyName: string; + ruleSrc: null; }; diff --git a/dist/system/property-info.js b/dist/system/property-info.js index 6aa9b2b3..f07e3c29 100644 --- a/dist/system/property-info.js +++ b/dist/system/property-info.js @@ -27,6 +27,7 @@ System.register(['aurelia-binding'], function(exports_1, context_1) { } var object; var propertyName; + var ruleSrc = null; if (expression instanceof aurelia_binding_1.AccessScope) { object = source.bindingContext; propertyName = expression.name; @@ -34,6 +35,15 @@ System.register(['aurelia-binding'], function(exports_1, context_1) { else if (expression instanceof aurelia_binding_1.AccessMember) { object = getObject(originalExpression, expression.object, source); propertyName = expression.name; + if (expression.object) { + //build the path to the property from the object root. + var exp = expression.object; + while (exp.object) { + propertyName = exp.name + '.' + propertyName; + exp = exp.object; + } + ruleSrc = getObject(originalExpression, exp, source); + } } else if (expression instanceof aurelia_binding_1.AccessKeyed) { object = getObject(originalExpression, expression.object, source); @@ -42,7 +52,7 @@ System.register(['aurelia-binding'], function(exports_1, context_1) { else { throw new Error("Expression '" + originalExpression + "' is not compatible with the validate binding-behavior."); } - return { object: object, propertyName: propertyName }; + return { object: object, propertyName: propertyName, ruleSrc: ruleSrc }; } exports_1("getPropertyInfo", getPropertyInfo); return { diff --git a/dist/system/validation-controller.js b/dist/system/validation-controller.js index 7d5c618d..f6afd2a9 100644 --- a/dist/system/validation-controller.js +++ b/dist/system/validation-controller.js @@ -1,7 +1,7 @@ -System.register(['./validator', './validate-trigger', './property-info', './validation-error'], function(exports_1, context_1) { +System.register(['./validator', './validate-trigger', './property-info', './validation-error', './implementation/rules'], function(exports_1, context_1) { "use strict"; var __moduleName = context_1 && context_1.id; - var validator_1, validate_trigger_1, property_info_1, validation_error_1; + var validator_1, validate_trigger_1, property_info_1, validation_error_1, rules_1; var ValidationController; return { setters:[ @@ -16,6 +16,9 @@ System.register(['./validator', './validate-trigger', './property-info', './vali }, function (validation_error_1_1) { validation_error_1 = validation_error_1_1; + }, + function (rules_1_1) { + rules_1 = rules_1_1; }], execute: function() { /** @@ -130,7 +133,7 @@ System.register(['./validator', './validate-trigger', './property-info', './vali */ ValidationController.prototype.getInstructionPredicate = function (instruction) { if (instruction) { - var object_1 = instruction.object, propertyName_1 = instruction.propertyName, rules_1 = instruction.rules; + var object_1 = instruction.object, propertyName_1 = instruction.propertyName, rules_2 = instruction.rules; var predicate_1; if (instruction.propertyName) { predicate_1 = function (x) { return x.object === object_1 && x.propertyName === propertyName_1; }; @@ -139,8 +142,8 @@ System.register(['./validator', './validate-trigger', './property-info', './vali predicate_1 = function (x) { return x.object === object_1; }; } // todo: move to Validator interface: - if (rules_1 && rules_1.indexOf) { - return function (x) { return predicate_1(x) && rules_1.indexOf(x.rule) !== -1; }; + if (rules_2 && rules_2.indexOf) { + return function (x) { return predicate_1(x) && rules_2.indexOf(x.rule) !== -1; }; } return predicate_1; } @@ -157,17 +160,17 @@ System.register(['./validator', './validate-trigger', './property-info', './vali // Get a function that will process the validation instruction. var execute; if (instruction) { - var object_2 = instruction.object, propertyName_2 = instruction.propertyName, rules_2 = instruction.rules; + var object_2 = instruction.object, propertyName_2 = instruction.propertyName, rules_3 = instruction.rules; // if rules were not specified, check the object map. - rules_2 = rules_2 || this.objects.get(object_2); + rules_3 = rules_3 || this.objects.get(object_2); // property specified? if (instruction.propertyName === undefined) { // validate the specified object. - execute = function () { return _this.validator.validateObject(object_2, rules_2); }; + execute = function () { return _this.validator.validateObject(object_2, rules_3); }; } else { // validate the specified property. - execute = function () { return _this.validator.validateProperty(object_2, propertyName_2, rules_2); }; + execute = function () { return _this.validator.validateProperty(object_2, propertyName_2, rules_3); }; } } else { @@ -259,7 +262,7 @@ System.register(['./validator', './validate-trigger', './property-info', './vali this_1.errors.splice(this_1.errors.indexOf(oldError), 1); } else { - // there is a corresponding new error... + // there is a corresponding new error... var newError = newErrors.splice(newErrorIndex, 1)[0]; // get the elements that are associated with the new error. var elements_1 = this_1.getAssociatedElements(newError); @@ -297,9 +300,13 @@ System.register(['./validator', './validate-trigger', './property-info', './vali if (!binding.isBound) { return; } - var _a = property_info_1.getPropertyInfo(binding.sourceExpression, binding.source), object = _a.object, propertyName = _a.propertyName; + var _a = property_info_1.getPropertyInfo(binding.sourceExpression, binding.source), object = _a.object, propertyName = _a.propertyName, ruleSrc = _a.ruleSrc; var registeredBinding = this.bindings.get(binding); var rules = registeredBinding ? registeredBinding.rules : undefined; + if (!rules && ruleSrc) { + //if we got ruleSrc back we need to get the rules for the subprop which are located in the root of the model + rules = rules_1.Rules.get(ruleSrc); + } this.validate({ object: object, propertyName: propertyName, rules: rules }); }; /** diff --git a/package.json b/package.json index 518060d4..13d2fad1 100644 --- a/package.json +++ b/package.json @@ -1,102 +1,97 @@ -{ - "name": "aurelia-validation", - "version": "0.12.5", - "description": "Validation for Aurelia applications", - "keywords": [ - "aurelia", - "plugin", - "validation" - ], - "homepage": "http://aurelia.io", - "bugs": { - "url": "https://github.com/aurelia/validation/issues" - }, - "license": "MIT", - "author": "Jeremy Danyow (https:/danyow.net/)", - "main": "dist/commonjs/aurelia-validation.js", - "typings": "dist/commonjs/aurelia-validation.d.ts", - "repository": { - "type": "git", - "url": "https://github.com/aurelia/validation" - }, - "scripts": { - "test": "./node_modules/.bin/tsc && ./node_modules/karma/bin/karma start --single-run", - "test-watch": "concurrently \"./node_modules/.bin/tsc --watch\" \"./node_modules/karma/bin/karma start\"", - "prebuild:compile": "del 'dist/**'", - "build:compile": "concurrently \"node_modules/.bin/tsc --project tsconfig.build.json --outDir dist/amd\" \"node_modules/.bin/tsc --project tsconfig.build.json --outDir dist/commonjs --module commonjs\" \"node_modules/.bin/tsc --project tsconfig.build.json --outDir dist/native-modules --module es2015\" \"node_modules/.bin/tsc --project tsconfig.build.json --outDir dist/system --module system\" \"node_modules/.bin/tsc --project tsconfig.build.json --outDir dist/es2015 --target es2015 --module es2015\" || true", - "build": "npm run build:compile", - "postbuild": "npm run build:doc", - "prebuild:doc": "del 'doc/api.json' && del 'dist/doc-temp/**' && node_modules/.bin/tsc --project tsconfig.build.json --outFile dist/doc-temp/aurelia-validation.js && node doc/shape-defs", - "build:doc": "./node_modules/.bin/typedoc --json doc/api.json --excludeExternals --includeDeclarations --mode modules --target ES6 --name aurelia-validation-docs --ignoreCompilerErrors --tsconfig doc/tsconfig.json dist/doc-temp/", - "postbuild:doc": "concurrently \"node doc/shape-doc\" \"del 'dist/doc-temp/**'\"" - }, - "jspm": { - "registry": "npm", - "jspmPackage": true, - "main": "aurelia-validation", - "format": "amd", - "directories": { - "dist": "dist/amd" - }, - "peerDependencies": { - "aurelia-binding": "^1.0.4", - "aurelia-dependency-injection": "^1.0.0", - "aurelia-logging": "^1.0.0", - "aurelia-pal": "^1.0.0", - "aurelia-task-queue": "^1.0.0", - "aurelia-templating": "^1.1.0" - }, - "dependencies": { - "aurelia-binding": "^1.0.4", - "aurelia-dependency-injection": "^1.0.0", - "aurelia-logging": "^1.0.0", - "aurelia-pal": "^1.0.0", - "aurelia-task-queue": "^1.0.0", - "aurelia-templating": "^1.1.0" - }, - "devDependencies": {} - }, - "dependencies": { - "aurelia-binding": "^1.0.4", - "aurelia-dependency-injection": "^1.0.0", - "aurelia-logging": "^1.0.0", - "aurelia-pal": "^1.0.0", - "aurelia-task-queue": "^1.0.0", - "aurelia-templating": "^1.1.0" - }, - "devDependencies": { - "aurelia-bootstrapper": "^1.0.0", - "aurelia-pal-browser": "^1.0.0", - "aurelia-polyfills": "^1.1.0", - "aurelia-testing": "^1.0.0-beta.2.0.0", - "concurrently": "^2.2.0", - "del-cli": "^0.2.0", - "jasmine-core": "^2.4.1", - "karma": "^1.2.0", - "karma-chrome-launcher": "^2.0.0", - "karma-ie-launcher": "^1.0.0", - "karma-jasmine": "^1.0.2", - "karma-requirejs": "^1.0.0", - "requirejs": "^2.2.0", - "requirejs-text": "^2.0.12", - "typedoc": "^0.4.5", - "typescript": "^2.0.0" - }, - "aurelia": { - "build": { - "resources": [ - "validate-binding-behavior", - "validation-errors-custom-attribute", - "validation-renderer-custom-attribute" - ] - }, - "documentation": { - "articles": [ - { - "title": "Validation: Basics", - "href": "doc/article/en-US/validation-basics.md" - } - ] - } - } -} +{ + "name": "aurelia-validation", + "version": "0.12.5", + "description": "Validation for Aurelia applications", + "keywords": [ + "aurelia", + "plugin", + "validation" + ], + "homepage": "http://aurelia.io", + "bugs": { + "url": "https://github.com/aurelia/validation/issues" + }, + "license": "MIT", + "author": "Jeremy Danyow (https:/danyow.net/)", + "main": "dist/commonjs/aurelia-validation.js", + "typings": "dist/commonjs/aurelia-validation.d.ts", + "repository": { + "type": "git", + "url": "https://github.com/aurelia/validation" + }, + "scripts": { + "test": "./node_modules/.bin/tsc && ./node_modules/karma/bin/karma start --single-run", + "test-watch": "concurrently \"./node_modules/.bin/tsc --watch\" \"./node_modules/karma/bin/karma start\"", + "prebuild:compile": "del 'dist/**'", + "build:compile": "concurrently \"node_modules/.bin/tsc --project tsconfig.build.json --outDir dist/amd\" \"node_modules/.bin/tsc --project tsconfig.build.json --outDir dist/commonjs --module commonjs\" \"node_modules/.bin/tsc --project tsconfig.build.json --outDir dist/native-modules --module es2015\" \"node_modules/.bin/tsc --project tsconfig.build.json --outDir dist/system --module system\" \"node_modules/.bin/tsc --project tsconfig.build.json --outDir dist/es2015 --target es2015 --module es2015\" || true", + "build": "npm run build:compile", + "postbuild": "npm run build:doc", + "prebuild:doc": "del 'doc/api.json' && del 'dist/doc-temp/**' && node_modules/.bin/tsc --project tsconfig.build.json --outFile dist/doc-temp/aurelia-validation.js && node doc/shape-defs", + "build:doc": "./node_modules/.bin/typedoc --json doc/api.json --excludeExternals --includeDeclarations --mode modules --target ES6 --name aurelia-validation-docs --ignoreCompilerErrors --tsconfig doc/tsconfig.json dist/doc-temp/", + "postbuild:doc": "concurrently \"node doc/shape-doc\" \"del 'dist/doc-temp/**'\"" + }, + "jspm": { + "registry": "npm", + "main": "aurelia-validation", + "format": "amd", + "directories": { + "dist": "dist/amd" + }, + "dependencies": { + "aurelia-binding": "^1.0.4", + "aurelia-dependency-injection": "^1.0.0", + "aurelia-logging": "^1.0.0", + "aurelia-pal": "^1.0.0", + "aurelia-task-queue": "^1.0.0", + "aurelia-templating": "^1.1.0" + }, + "devDependencies": { + "babel": "babel-core@^5.8.24", + "babel-runtime": "^5.8.24", + "core-js": "^1.1.4" + } + }, + "dependencies": { + "aurelia-binding": "^1.0.4", + "aurelia-dependency-injection": "^1.0.0", + "aurelia-logging": "^1.0.0", + "aurelia-pal": "^1.0.0", + "aurelia-task-queue": "^1.0.0", + "aurelia-templating": "^1.1.0" + }, + "devDependencies": { + "aurelia-bootstrapper": "^1.0.0", + "aurelia-pal-browser": "^1.0.0", + "aurelia-polyfills": "^1.1.0", + "aurelia-testing": "^1.0.0-beta.2.0.0", + "concurrently": "^2.2.0", + "del-cli": "^0.2.0", + "jasmine-core": "^2.4.1", + "karma": "^1.2.0", + "karma-chrome-launcher": "^2.0.0", + "karma-ie-launcher": "^1.0.0", + "karma-jasmine": "^1.0.2", + "karma-requirejs": "^1.0.0", + "requirejs": "^2.2.0", + "requirejs-text": "^2.0.12", + "typedoc": "^0.4.5", + "typescript": "^2.0.0" + }, + "aurelia": { + "build": { + "resources": [ + "validate-binding-behavior", + "validation-errors-custom-attribute", + "validation-renderer-custom-attribute" + ] + }, + "documentation": { + "articles": [ + { + "title": "Validation: Basics", + "href": "doc/article/en-US/validation-basics.md" + } + ] + } + } +} diff --git a/src/implementation/standard-validator.ts b/src/implementation/standard-validator.ts index d790c822..921f0354 100644 --- a/src/implementation/standard-validator.ts +++ b/src/implementation/standard-validator.ts @@ -36,7 +36,7 @@ export class StandardValidator extends Validator { $value: value, $object: object, $config: rule.config, - $getDisplayName: this.getDisplayName + $getDisplayName: this.getDisplayName }; return expression.evaluate( { bindingContext: object, overrideContext }, @@ -45,7 +45,6 @@ export class StandardValidator extends Validator { private validate(object: any, propertyName: string|null, rules: Rule[]|null): Promise { const errors: ValidationError[] = []; - // rules specified? if (!rules) { // no. locate the rules via metadata. @@ -59,7 +58,7 @@ export class StandardValidator extends Validator { // are we validating all properties or a single property? const validateAllProperties = propertyName === null || propertyName === undefined; - + const addError = (rule: Rule, value: any) => { const message = this.getMessage(rule, object, value); errors.push(new ValidationError(rule, message, object, rule.property.name)); @@ -69,7 +68,6 @@ export class StandardValidator extends Validator { const promises: Promise[] = []; for (let i = 0; i < rules.length; i++) { const rule = rules[i]; - // is the rule related to the property we're validating. if (!validateAllProperties && rule.property.name !== propertyName) { continue; @@ -81,13 +79,21 @@ 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]; + if (rule.property.name && rule.property.name.indexOf('.') !== -1) + { + //if the rule name has a '.', we have a sub property. + //object is the object containing the field. get the last propertyy in the chain + //to get the field name. Use thi to get the correct value. + let parts =rule.property.name.split('.'); + value= object[parts[parts.length-1]]; + } const promiseOrBoolean = rule.condition(value, object); if (promiseOrBoolean instanceof Promise) { promises.push(promiseOrBoolean.then(isValid => { if (!isValid) { addError(rule, value); - } + } })); continue; } @@ -106,7 +112,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) */ validateProperty(object: any, propertyName: string, rules?: any): Promise { @@ -116,10 +122,10 @@ 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) */ validateObject(object: any, rules?: any): Promise { return this.validate(object, null, rules || null); - } + } } diff --git a/src/implementation/validation-parser.ts b/src/implementation/validation-parser.ts index 2080ca90..ef8b4d57 100644 --- a/src/implementation/validation-parser.ts +++ b/src/implementation/validation-parser.ts @@ -64,33 +64,45 @@ export class ValidationParser { ) ); } - + MessageExpressionValidator.validate(expression, message); - + this.cache[message] = expression; - + return expression; } 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]+)+)\s*;?\s*\}$/; + const arrow = /^\(?[$_\w\d]+\)?\s*=>\s*(?:\{?.*return\s+)?[$_\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}`); } - return this.parser.parse(match[1]); + const name = match[1][0]=="."?match[1].substr(1):match[1]; + return this.parser.parse(name); } parseProperty(property: string|PropertyAccessor): RuleProperty { if (isString(property)) { - return { name: property, displayName: null }; - } - const accessor = this.getAccessorExpression(property.toString()); - if (accessor instanceof AccessScope - || accessor instanceof AccessMember && accessor.object instanceof AccessScope) { + return { name: property, displayName: null }; + } + const accessor = this.getAccessorExpression(property.toString()); + const isSubProp = accessor instanceof AccessMember && accessor.object instanceof AccessScope; + if (accessor instanceof AccessScope || isSubProp) { + let propName = accessor.name; + if (isSubProp) + { + //iterate up the chain until we are in the 1st sub-object of the root object. + let ao = accessor.object; + while(ao) + { + propName = ao.name +'.' + propName; + ao = ao.object; + } + } return { - name: accessor.name, + name: propName, displayName: null }; } diff --git a/src/property-info.ts b/src/property-info.ts index cb5fb30e..7d9900b0 100644 --- a/src/property-info.ts +++ b/src/property-info.ts @@ -33,12 +33,24 @@ export function getPropertyInfo(expression: Expression, source: any) { let object: any; 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 = expression.object; + while(exp.object) + { + propertyName = exp.name +'.' + propertyName; + exp = exp.object; + } + ruleSrc= getObject(originalExpression, exp, source); + } } else if (expression instanceof AccessKeyed) { object = getObject(originalExpression, expression.object, source); propertyName = expression.key.evaluate(source); @@ -46,5 +58,5 @@ export function getPropertyInfo(expression: Expression, source: any) { throw new Error(`Expression '${originalExpression}' is not compatible with the validate binding-behavior.`); } - return { object, propertyName }; + return { object, propertyName, ruleSrc}; } diff --git a/src/validation-controller.ts b/src/validation-controller.ts index 563d1beb..e60b2715 100644 --- a/src/validation-controller.ts +++ b/src/validation-controller.ts @@ -4,7 +4,7 @@ import {validateTrigger} from './validate-trigger'; import {getPropertyInfo} from './property-info'; import {ValidationRenderer, RenderInstruction} from './validation-renderer'; import {ValidationError} from './validation-error'; - +import {Rules} from './implementation/rules'; /** * Information related to an "& validate" decorated binding. */ @@ -110,7 +110,7 @@ export class ValidationController { /** * Removes and unrenders a ValidationError. */ - removeError(error: ValidationError) { + removeError(error: ValidationError) { if (this.errors.indexOf(error) !== -1) { this.processErrorDelta('reset', [error], []); } @@ -168,7 +168,7 @@ export class ValidationController { private getInstructionPredicate(instruction?: ValidateInstruction): (error: ValidationError) => boolean { if (instruction) { const { object, propertyName, rules } = instruction; - let predicate: (error: ValidationError) => boolean; + let predicate: (error: ValidationError) => boolean; if (instruction.propertyName) { predicate = x => x.object === object && x.propertyName === propertyName; } else { @@ -194,7 +194,7 @@ export class ValidationController { if (instruction) { let { object, propertyName, rules } = instruction; // if rules were not specified, check the object map. - rules = rules || this.objects.get(object); + rules = rules || this.objects.get(object); // property specified? if (instruction.propertyName === undefined) { // validate the specified object. @@ -215,7 +215,7 @@ export class ValidationController { if (this.objects.has(object)) { continue; } - promises.push(this.validator.validateProperty(object, propertyName, rules)); + promises.push(this.validator.validateProperty(object, propertyName, rules)); } return Promise.all(promises).then(errorSets => errorSets.reduce((a, b) => a.concat(b), [])); }; @@ -226,13 +226,13 @@ export class ValidationController { let result = this.finishValidating .then(execute) .then(newErrors => { - const predicate = this.getInstructionPredicate(instruction); + const predicate = this.getInstructionPredicate(instruction); const oldErrors = this.errors.filter(predicate); this.processErrorDelta('validate', oldErrors, newErrors); if (result === this.finishValidating) { this.validating = false; } - return newErrors; + return newErrors; }) .catch(error => { // recover, to enable subsequent calls to validate() @@ -241,7 +241,7 @@ export class ValidationController { return Promise.reject(error); }); - + this.finishValidating = result; return result; @@ -251,10 +251,10 @@ export class ValidationController { * Resets any rendered errors (unrenders). * @param instruction Optional. Instructions on what to reset. If unspecified all rendered errors will be unrendered. */ - reset(instruction?: ValidateInstruction) { - const predicate = this.getInstructionPredicate(instruction); + reset(instruction?: ValidateInstruction) { + const predicate = this.getInstructionPredicate(instruction); const oldErrors = this.errors.filter(predicate); - this.processErrorDelta('reset', oldErrors, []); + this.processErrorDelta('reset', oldErrors, []); } /** @@ -286,7 +286,7 @@ export class ValidationController { for (let oldError of oldErrors) { // get the elements associated with the old error. const elements = this.elements.get(oldError); - + // remove the old error from the element map. this.elements.delete(oldError); @@ -299,13 +299,13 @@ export class ValidationController { // no corresponding new error... simple remove. this.errors.splice(this.errors.indexOf(oldError), 1); } else { - // there is a corresponding new error... + // there is a corresponding new error... const newError = newErrors.splice(newErrorIndex, 1)[0]; - + // get the elements that are associated with the new error. const elements = this.getAssociatedElements(newError); this.elements.set(newError, elements); - + // create a render instruction for the new error. instruction.render.push({ error: newError, elements }); @@ -336,9 +336,14 @@ export class ValidationController { if (!binding.isBound) { return; } - const { object, propertyName } = getPropertyInfo(binding.sourceExpression, (binding).source); + const { object, propertyName , ruleSrc} = getPropertyInfo(binding.sourceExpression, (binding).source); const registeredBinding = this.bindings.get(binding); - const rules = registeredBinding ? registeredBinding.rules : undefined; + let rules = registeredBinding ? registeredBinding.rules : undefined; + if(!rules && ruleSrc) + { + //if we got ruleSrc back we need to get the rules for the subprop which are located in the root of the model + rules = Rules.get(ruleSrc); + } this.validate({ object, propertyName, rules }); } diff --git a/test/basic.ts b/test/basic.ts index cfa5488c..566b4a87 100644 --- a/test/basic.ts +++ b/test/basic.ts @@ -20,7 +20,7 @@ function blur(element: Element): Promise { function change(element: HTMLInputElement, value: string): Promise { element.value = value; - element.dispatchEvent(DOM.createCustomEvent('change', { bubbles: true })); + element.dispatchEvent(DOM.createCustomEvent('change', { bubbles: true })); return new Promise(setTimeout); } @@ -33,6 +33,7 @@ describe('end to end', () => { component.bootstrap(configure); let firstName: HTMLInputElement, lastName: HTMLInputElement, + subprop: HTMLInputElement, number1: HTMLInputElement, number2: HTMLInputElement, password: HTMLInputElement, confirmPassword: HTMLInputElement; @@ -44,9 +45,10 @@ describe('end to end', () => { // grab some references. .then(() => { viewModel = component.viewModel; - viewModel.controller.addRenderer(renderer); + viewModel.controller.addRenderer(renderer); firstName = component.element.querySelector('#firstName'); lastName = component.element.querySelector('#lastName'); + subprop = component.element.querySelector('#subProp'); number1 = component.element.querySelector('#number1'); number2 = component.element.querySelector('#number2'); password = component.element.querySelector('#password'); @@ -58,7 +60,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. @@ -72,6 +74,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. @@ -90,21 +101,21 @@ 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. - .then(() => expect(viewModel.controller.errors.length).toBe(1)) + .then(() => expect(viewModel.controller.errors.length).toBe(1)) // change the numbers back to invalid values. .then(() => { viewModel.number1 = 0; viewModel.number2 = 0; - }) + }) // hide the form and change the validateTrigger. .then(() => { @@ -118,7 +129,7 @@ describe('end to end', () => { // change the firstName field- this should trigger validation. .then(() => change(firstName, 'test')) // confirm there's no error. - .then(() => expect(viewModel.controller.errors.length).toBe(0)) + .then(() => expect(viewModel.controller.errors.length).toBe(0)) // change the firstName field- this should trigger validation. .then(() => change(firstName, '')) // confirm there's an error. @@ -151,7 +162,6 @@ describe('end to end', () => { .then(() => change(confirmPassword, 'b')) // confirm the custom validator worked .then(() => expect(viewModel.controller.errors[0].message).toBe('Confirm Password must match Password')) - // hide the form and change the validateTrigger. .then(() => { viewModel.showForm = false; diff --git a/test/resources/registration-form.ts b/test/resources/registration-form.ts index 90eb6cea..f56088dc 100644 --- a/test/resources/registration-form.ts +++ b/test/resources/registration-form.ts @@ -12,8 +12,9 @@ import {
  • \${error.message}
+ - + @@ -24,6 +25,9 @@ export class RegistrationForm { firstName = ''; lastName = ''; email = ''; + settings = { + subprop : "" + }; number1 = 0; number2 = 0; password = ''; @@ -38,7 +42,7 @@ export class RegistrationForm { ValidationRules.customRule( 'matchesProperty', - (value, obj, otherPropertyName) => + (value, obj, otherPropertyName) => value === null || value === undefined || value === '' @@ -54,6 +58,8 @@ ValidationRules .ensure((f: RegistrationForm) => f.firstName).required() .ensure(f => f.lastName).required() .ensure('email').required().email() + .ensure("f.settings.subprop").minLength(2) + //.ensure(f => f.settings.subprop).required().minLength(2) .ensure(f => f.number1).satisfies(value => value > 0) .ensure(f => f.number2).satisfies(value => value > 0).withMessage('${displayName} gots to be greater than zero.') .ensure(f => f.password).required()