diff --git a/dist/amd/implementation/standard-validator.d.ts b/dist/amd/implementation/standard-validator.d.ts index e05e7b8e..0dc492c3 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 89686019..65fee742 100644 --- a/dist/amd/implementation/standard-validator.js +++ b/dist/amd/implementation/standard-validator.js @@ -53,6 +53,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)) { promiseOrBoolean = Promise.resolve(promiseOrBoolean); diff --git a/dist/amd/implementation/validation-parser.js b/dist/amd/implementation/validation-parser.js index 6b112f11..45250e1a 100644 --- a/dist/amd/implementation/validation-parser.js +++ b/dist/amd/implementation/validation-parser.js @@ -35,23 +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) { if (util_1.isString(property)) { return { name: property, displayName: null }; } var 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 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.d.ts b/dist/amd/validation-controller.d.ts index dcc1c174..be966d21 100644 --- a/dist/amd/validation-controller.d.ts +++ b/dist/amd/validation-controller.d.ts @@ -41,6 +41,7 @@ export declare class ValidationController { */ validateTrigger: number; private finishValidating; + isValid: boolean; constructor(validator: Validator); /** * Adds an object to the set of objects that should be validated when validate is called. diff --git a/dist/amd/validation-controller.js b/dist/amd/validation-controller.js index a950fc56..4a497f57 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. @@ -30,6 +30,7 @@ define(["require", "exports", './validator', './validate-trigger', './property-i this.validateTrigger = validate_trigger_1.validateTrigger.blur; // Promise that resolves when validation has completed. this.finishValidating = Promise.resolve(); + this.isValid = false; } /** * Adds an object to the set of objects that should be validated when validate is called. @@ -112,7 +113,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 +122,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 +140,20 @@ 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); + if (!rules_3) { + rules_3 = rules_1.Rules.get(ruleSrc); + } // 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 { @@ -166,6 +170,15 @@ define(["require", "exports", './validator', './validate-trigger', './property-i if (_this.objects.has(object)) { continue; } + if (propertyName.indexOf(".") !== -1) { + var parentProp = ""; + var ittr = binding.sourceExpression.expression; + while (ittr.object) { + ittr = ittr.object; + parentProp = ittr.name; + } + rules = rules_1.Rules.get(binding._observer0._callable0._observer0.obj[parentProp]); + } promises.push(_this.validator.validateProperty(object, propertyName, rules)); } return Promise.all(promises).then(function (errorSets) { return errorSets.reduce(function (a, b) { return a.concat(b); }, []); }); @@ -241,7 +254,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 +292,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 e05e7b8e..0dc492c3 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 82314d0d..8340c0fb 100644 --- a/dist/commonjs/implementation/standard-validator.js +++ b/dist/commonjs/implementation/standard-validator.js @@ -57,6 +57,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)) { promiseOrBoolean = Promise.resolve(promiseOrBoolean); diff --git a/dist/commonjs/implementation/validation-parser.js b/dist/commonjs/implementation/validation-parser.js index 67c6b45b..fd2839ad 100644 --- a/dist/commonjs/implementation/validation-parser.js +++ b/dist/commonjs/implementation/validation-parser.js @@ -38,23 +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) { if (util_1.isString(property)) { return { name: property, displayName: null }; } var 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 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.d.ts b/dist/commonjs/validation-controller.d.ts index dcc1c174..be966d21 100644 --- a/dist/commonjs/validation-controller.d.ts +++ b/dist/commonjs/validation-controller.d.ts @@ -41,6 +41,7 @@ export declare class ValidationController { */ validateTrigger: number; private finishValidating; + isValid: boolean; constructor(validator: Validator); /** * Adds an object to the set of objects that should be validated when validate is called. diff --git a/dist/commonjs/validation-controller.js b/dist/commonjs/validation-controller.js index a85eeb6e..2aa79ec6 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. @@ -33,6 +34,7 @@ var ValidationController = (function () { this.validateTrigger = validate_trigger_1.validateTrigger.blur; // Promise that resolves when validation has completed. this.finishValidating = Promise.resolve(); + this.isValid = false; } /** * Adds an object to the set of objects that should be validated when validate is called. @@ -115,7 +117,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 +126,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 +144,20 @@ 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); + if (!rules_3) { + rules_3 = rules_1.Rules.get(ruleSrc); + } // 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 { @@ -169,6 +174,15 @@ var ValidationController = (function () { if (_this.objects.has(object)) { continue; } + if (propertyName.indexOf(".") !== -1) { + var parentProp = ""; + var ittr = binding.sourceExpression.expression; + while (ittr.object) { + ittr = ittr.object; + parentProp = ittr.name; + } + rules = rules_1.Rules.get(binding._observer0._callable0._observer0.obj[parentProp]); + } promises.push(_this.validator.validateProperty(object, propertyName, rules)); } return Promise.all(promises).then(function (errorSets) { return errorSets.reduce(function (a, b) { return a.concat(b); }, []); }); @@ -244,7 +258,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 +296,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 e05e7b8e..0dc492c3 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 f81057ec..46c96b62 100644 --- a/dist/es2015/implementation/standard-validator.js +++ b/dist/es2015/implementation/standard-validator.js @@ -48,7 +48,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]]; + } let promiseOrBoolean = rule.condition(value, object); if (!(promiseOrBoolean instanceof Promise)) { promiseOrBoolean = Promise.resolve(promiseOrBoolean); diff --git a/dist/es2015/implementation/validation-parser.js b/dist/es2015/implementation/validation-parser.js index 96b0aeba..0c429329 100644 --- a/dist/es2015/implementation/validation-parser.js +++ b/dist/es2015/implementation/validation-parser.js @@ -32,23 +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) { 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) { + 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.d.ts b/dist/es2015/validation-controller.d.ts index dcc1c174..be966d21 100644 --- a/dist/es2015/validation-controller.d.ts +++ b/dist/es2015/validation-controller.d.ts @@ -41,6 +41,7 @@ export declare class ValidationController { */ validateTrigger: number; private finishValidating; + isValid: boolean; constructor(validator: Validator); /** * Adds an object to the set of objects that should be validated when validate is called. diff --git a/dist/es2015/validation-controller.js b/dist/es2015/validation-controller.js index 7cb3f54d..c8c84357 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. @@ -32,6 +33,7 @@ export class ValidationController { this.validateTrigger = validateTrigger.blur; // Promise that resolves when validation has completed. this.finishValidating = Promise.resolve(); + this.isValid = false; } /** * Adds an object to the set of objects that should be validated when validate is called. @@ -141,6 +143,9 @@ 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) { + rules = Rules.get(ruleSrc); + } // property specified? if (instruction.propertyName === undefined) { // validate the specified object. @@ -163,6 +168,15 @@ export class ValidationController { if (this.objects.has(object)) { continue; } + if (propertyName.indexOf(".") !== -1) { + let parentProp = ""; + let ittr = binding.sourceExpression.expression; + while (ittr.object) { + ittr = ittr.object; + parentProp = ittr.name; + } + rules = Rules.get(binding._observer0._callable0._observer0.obj[parentProp]); + } promises.push(this.validator.validateProperty(object, propertyName, rules)); } return Promise.all(promises).then(errorSets => errorSets.reduce((a, b) => a.concat(b), [])); @@ -236,7 +250,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 +281,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 e05e7b8e..0dc492c3 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 6ad67066..67a9196d 100644 --- a/dist/native-modules/implementation/standard-validator.js +++ b/dist/native-modules/implementation/standard-validator.js @@ -56,6 +56,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)) { promiseOrBoolean = Promise.resolve(promiseOrBoolean); diff --git a/dist/native-modules/implementation/validation-parser.js b/dist/native-modules/implementation/validation-parser.js index 1587f771..8beb5e7b 100644 --- a/dist/native-modules/implementation/validation-parser.js +++ b/dist/native-modules/implementation/validation-parser.js @@ -37,23 +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) { if (isString(property)) { return { name: property, displayName: null }; } var accessor = this.getAccessorExpression(property.toString()); - if (accessor instanceof AccessScope - || accessor instanceof AccessMember && accessor.object instanceof AccessScope) { + 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.d.ts b/dist/native-modules/validation-controller.d.ts index dcc1c174..be966d21 100644 --- a/dist/native-modules/validation-controller.d.ts +++ b/dist/native-modules/validation-controller.d.ts @@ -41,6 +41,7 @@ export declare class ValidationController { */ validateTrigger: number; private finishValidating; + isValid: boolean; constructor(validator: Validator); /** * Adds an object to the set of objects that should be validated when validate is called. diff --git a/dist/native-modules/validation-controller.js b/dist/native-modules/validation-controller.js index 02d56385..b01f4993 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. @@ -32,6 +33,7 @@ export var ValidationController = (function () { this.validateTrigger = validateTrigger.blur; // Promise that resolves when validation has completed. this.finishValidating = Promise.resolve(); + this.isValid = false; } /** * Adds an object to the set of objects that should be validated when validate is called. @@ -144,6 +146,9 @@ export var ValidationController = (function () { var object_2 = instruction.object, propertyName_2 = instruction.propertyName, rules_2 = instruction.rules; // if rules were not specified, check the object map. rules_2 = rules_2 || this.objects.get(object_2); + if (!rules_2) { + rules_2 = Rules.get(ruleSrc); + } // property specified? if (instruction.propertyName === undefined) { // validate the specified object. @@ -168,6 +173,15 @@ export var ValidationController = (function () { if (_this.objects.has(object)) { continue; } + if (propertyName.indexOf(".") !== -1) { + var parentProp = ""; + var ittr = binding.sourceExpression.expression; + while (ittr.object) { + ittr = ittr.object; + parentProp = ittr.name; + } + rules = Rules.get(binding._observer0._callable0._observer0.obj[parentProp]); + } promises.push(_this.validator.validateProperty(object, propertyName, rules)); } return Promise.all(promises).then(function (errorSets) { return errorSets.reduce(function (a, b) { return a.concat(b); }, []); }); @@ -243,7 +257,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 +295,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 e05e7b8e..0dc492c3 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 a7d85a9d..62bcc4bf 100644 --- a/dist/system/implementation/standard-validator.js +++ b/dist/system/implementation/standard-validator.js @@ -74,6 +74,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)) { promiseOrBoolean = Promise.resolve(promiseOrBoolean); diff --git a/dist/system/implementation/validation-parser.js b/dist/system/implementation/validation-parser.js index 95033a48..e1b4d7d1 100644 --- a/dist/system/implementation/validation-parser.js +++ b/dist/system/implementation/validation-parser.js @@ -53,23 +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) { if (util_1.isString(property)) { return { name: property, displayName: null }; } var 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 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.d.ts b/dist/system/validation-controller.d.ts index dcc1c174..be966d21 100644 --- a/dist/system/validation-controller.d.ts +++ b/dist/system/validation-controller.d.ts @@ -41,6 +41,7 @@ export declare class ValidationController { */ validateTrigger: number; private finishValidating; + isValid: boolean; constructor(validator: Validator); /** * Adds an object to the set of objects that should be validated when validate is called. diff --git a/dist/system/validation-controller.js b/dist/system/validation-controller.js index 7d5c618d..b3bbb9cc 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() { /** @@ -48,6 +51,7 @@ System.register(['./validator', './validate-trigger', './property-info', './vali this.validateTrigger = validate_trigger_1.validateTrigger.blur; // Promise that resolves when validation has completed. this.finishValidating = Promise.resolve(); + this.isValid = false; } /** * Adds an object to the set of objects that should be validated when validate is called. @@ -130,7 +134,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 +143,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 +161,20 @@ 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); + if (!rules_3) { + rules_3 = rules_1.Rules.get(ruleSrc); + } // 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 { @@ -184,6 +191,15 @@ System.register(['./validator', './validate-trigger', './property-info', './vali if (_this.objects.has(object)) { continue; } + if (propertyName.indexOf(".") !== -1) { + var parentProp = ""; + var ittr = binding.sourceExpression.expression; + while (ittr.object) { + ittr = ittr.object; + parentProp = ittr.name; + } + rules = rules_1.Rules.get(binding._observer0._callable0._observer0.obj[parentProp]); + } promises.push(_this.validator.validateProperty(object, propertyName, rules)); } return Promise.all(promises).then(function (errorSets) { return errorSets.reduce(function (a, b) { return a.concat(b); }, []); }); @@ -259,7 +275,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 +313,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/src/implementation/standard-validator.ts b/src/implementation/standard-validator.ts index e4951a9a..ac628a68 100644 --- a/src/implementation/standard-validator.ts +++ b/src/implementation/standard-validator.ts @@ -66,7 +66,16 @@ 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]]; + } + let promiseOrBoolean = rule.condition(value, object); if (!(promiseOrBoolean instanceof Promise)) { promiseOrBoolean = Promise.resolve(promiseOrBoolean); 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..a17c2d2e 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. */ @@ -75,6 +75,7 @@ export class ValidationController { // Promise that resolves when validation has completed. private finishValidating = Promise.resolve(); + public isValid = false; constructor(private validator: Validator) {} /** @@ -110,7 +111,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 +169,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 +195,11 @@ 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); + if (!rules) + { + rules = Rules.get(ruleSrc); + } // property specified? if (instruction.propertyName === undefined) { // validate the specified object. @@ -215,7 +220,18 @@ export class ValidationController { if (this.objects.has(object)) { continue; } - promises.push(this.validator.validateProperty(object, propertyName, rules)); + if (propertyName.indexOf(".") !== -1) + { + let parentProp =""; + let ittr= binding.sourceExpression.expression; + while(ittr.object) + { + ittr = ittr.object; + parentProp = ittr.name; + } + rules = Rules.get(binding._observer0._callable0._observer0.obj[parentProp]); + } + promises.push(this.validator.validateProperty(object, propertyName, rules)); } return Promise.all(promises).then(errorSets => errorSets.reduce((a, b) => a.concat(b), [])); }; @@ -226,13 +242,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 +257,7 @@ export class ValidationController { return Promise.reject(error); }); - + this.finishValidating = result; return result; @@ -251,10 +267,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, []); } /** @@ -281,12 +297,11 @@ export class ValidationController { // create a shallow copy of newErrors so we can mutate it without causing side-effects. newErrors = newErrors.slice(0); - // create unrender instructions from the old errors. 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 +314,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 +351,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 d74679c4..3aca5eae 100644 --- a/test/basic.ts +++ b/test/basic.ts @@ -13,6 +13,7 @@ describe('end to end', () => { component.bootstrap(configure); let firstName: HTMLInputElement, lastName: HTMLInputElement, + subprop: HTMLInputElement, number1: HTMLInputElement, number2: HTMLInputElement, password: HTMLInputElement, confirmPassword: HTMLInputElement; @@ -27,6 +28,7 @@ describe('end to end', () => { 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'); @@ -52,6 +54,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. diff --git a/test/resources/registration-form.ts b/test/resources/registration-form.ts index 045ce8c4..b191135e 100644 --- a/test/resources/registration-form.ts +++ b/test/resources/registration-form.ts @@ -11,8 +11,9 @@ import {
+ - + @@ -23,6 +24,9 @@ export class RegistrationForm { firstName = ''; lastName = ''; email = ''; + settings = { + subprop : "" + }; number1 = 0; number2 = 0; password = ''; @@ -37,7 +41,7 @@ export class RegistrationForm { ValidationRules.customRule( 'matchesProperty', - (value, obj, otherPropertyName) => + (value, obj, otherPropertyName) => value === null || value === undefined || value === '' @@ -53,6 +57,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()