From 3b62826374e84ca5915f299d26b614b8b5bf465f Mon Sep 17 00:00:00 2001 From: Eric Joyce Date: Tue, 18 Oct 2016 07:44:40 -0400 Subject: [PATCH] enhance(ValidationParser): handle validate props that subpropeties of an object fixes #283 --- dist/amd/implementation/rules.d.ts | 2 +- .../implementation/standard-validator.d.ts | 15 ++- dist/amd/implementation/standard-validator.js | 60 ++++++---- .../amd/implementation/validation-parser.d.ts | 4 +- dist/amd/implementation/validation-parser.js | 41 ++++--- dist/amd/implementation/validation-rules.d.ts | 6 +- dist/amd/implementation/validation-rules.js | 12 +- dist/amd/property-info.d.ts | 4 +- dist/amd/property-info.js | 25 ++-- dist/amd/validate-binding-behavior-base.d.ts | 10 +- dist/amd/validate-binding-behavior-base.js | 18 ++- dist/amd/validate-trigger.d.ts | 4 +- dist/amd/validate-trigger.js | 18 +-- dist/amd/validation-controller.d.ts | 11 +- dist/amd/validation-controller.js | 102 +++++++++++++---- .../amd/validation-errors-custom-attribute.js | 2 + dist/amd/validator.d.ts | 6 + dist/commonjs/implementation/rules.d.ts | 2 +- .../implementation/standard-validator.d.ts | 15 ++- .../implementation/standard-validator.js | 60 ++++++---- .../implementation/validation-parser.d.ts | 4 +- .../implementation/validation-parser.js | 41 ++++--- .../implementation/validation-rules.d.ts | 6 +- .../implementation/validation-rules.js | 12 +- dist/commonjs/property-info.d.ts | 4 +- dist/commonjs/property-info.js | 25 ++-- .../validate-binding-behavior-base.d.ts | 10 +- .../validate-binding-behavior-base.js | 18 ++- dist/commonjs/validate-trigger.d.ts | 4 +- dist/commonjs/validate-trigger.js | 18 +-- dist/commonjs/validation-controller.d.ts | 11 +- dist/commonjs/validation-controller.js | 101 +++++++++++++---- .../validation-errors-custom-attribute.js | 2 + dist/commonjs/validator.d.ts | 6 + dist/es2015/implementation/rules.d.ts | 2 +- .../implementation/standard-validator.d.ts | 15 ++- .../implementation/standard-validator.js | 62 ++++++---- .../implementation/validation-parser.d.ts | 4 +- .../implementation/validation-parser.js | 41 ++++--- .../implementation/validation-rules.d.ts | 6 +- .../es2015/implementation/validation-rules.js | 12 +- dist/es2015/property-info.d.ts | 4 +- dist/es2015/property-info.js | 25 ++-- .../validate-binding-behavior-base.d.ts | 10 +- dist/es2015/validate-binding-behavior-base.js | 18 ++- dist/es2015/validate-trigger.d.ts | 4 +- dist/es2015/validate-trigger.js | 18 +-- dist/es2015/validation-controller.d.ts | 11 +- dist/es2015/validation-controller.js | 89 +++++++++++---- .../validation-errors-custom-attribute.js | 4 +- dist/es2015/validator.d.ts | 6 + dist/native-modules/implementation/rules.d.ts | 2 +- .../implementation/standard-validator.d.ts | 15 ++- .../implementation/standard-validator.js | 60 ++++++---- .../implementation/validation-parser.d.ts | 4 +- .../implementation/validation-parser.js | 41 ++++--- .../implementation/validation-rules.d.ts | 6 +- .../implementation/validation-rules.js | 12 +- dist/native-modules/property-info.d.ts | 4 +- dist/native-modules/property-info.js | 25 ++-- .../validate-binding-behavior-base.d.ts | 10 +- .../validate-binding-behavior-base.js | 18 ++- dist/native-modules/validate-trigger.d.ts | 4 +- dist/native-modules/validate-trigger.js | 18 +-- .../native-modules/validation-controller.d.ts | 11 +- dist/native-modules/validation-controller.js | 91 +++++++++++---- .../validation-errors-custom-attribute.js | 2 + dist/native-modules/validator.d.ts | 6 + dist/system/implementation/rules.d.ts | 2 +- .../implementation/standard-validator.d.ts | 15 ++- .../implementation/standard-validator.js | 60 ++++++---- .../implementation/validation-parser.d.ts | 4 +- .../implementation/validation-parser.js | 41 ++++--- .../implementation/validation-rules.d.ts | 6 +- .../system/implementation/validation-rules.js | 12 +- dist/system/property-info.d.ts | 4 +- dist/system/property-info.js | 25 ++-- .../validate-binding-behavior-base.d.ts | 10 +- dist/system/validate-binding-behavior-base.js | 18 ++- dist/system/validate-trigger.d.ts | 4 +- dist/system/validate-trigger.js | 18 +-- dist/system/validation-controller.d.ts | 11 +- dist/system/validation-controller.js | 107 +++++++++++++----- .../validation-errors-custom-attribute.js | 2 + dist/system/validator.d.ts | 6 + src/implementation/standard-validator.ts | 15 ++- src/implementation/validation-parser.ts | 21 +++- src/property-info.ts | 12 ++ src/validation-controller.ts | 44 ++++++- test/basic.ts | 11 ++ test/resources/registration-form.ts | 5 + 91 files changed, 1251 insertions(+), 541 deletions(-) diff --git a/dist/amd/implementation/rules.d.ts b/dist/amd/implementation/rules.d.ts index 71c46daf..30947f6f 100644 --- a/dist/amd/implementation/rules.d.ts +++ b/dist/amd/implementation/rules.d.ts @@ -6,7 +6,7 @@ export declare class Rules { /** * The name of the property that stores the rules. */ - static key: string; + private static key; /** * Applies the rules to a target. */ diff --git a/dist/amd/implementation/standard-validator.d.ts b/dist/amd/implementation/standard-validator.d.ts index e05e7b8e..ad544cbb 100644 --- a/dist/amd/implementation/standard-validator.d.ts +++ b/dist/amd/implementation/standard-validator.d.ts @@ -1,20 +1,18 @@ import { ViewResources } from 'aurelia-templating'; import { Validator } from '../validator'; import { ValidationError } from '../validation-error'; +import { Rule } from './rule'; import { ValidationMessageProvider } from './validation-messages'; /** * Validates. * 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; constructor(messageProvider: ValidationMessageProvider, resources: ViewResources); - private getMessage(rule, object, value); - private validateRuleSequence(object, propertyName, ruleSequence, sequence); - private validate(object, propertyName, rules); /** * Validates the specified property. * @param object The object to validate. @@ -30,4 +28,13 @@ export declare class StandardValidator extends Validator { * for the object created by ValidationRules....on(class/object) */ validateObject(object: any, rules?: any): Promise; + /** + * Determines whether a rule exists in a set of rules. + * @param rules The rules to search. + * @parem rule The rule to find. + */ + ruleExists(rules: Rule[][], rule: Rule): boolean; + private getMessage(rule, object, value); + private validateRuleSequence(object, propertyName, ruleSequence, sequence); + private validate(object, propertyName, rules); } diff --git a/dist/amd/implementation/standard-validator.js b/dist/amd/implementation/standard-validator.js index 89686019..ae91dc5e 100644 --- a/dist/amd/implementation/standard-validator.js +++ b/dist/amd/implementation/standard-validator.js @@ -17,6 +17,39 @@ define(["require", "exports", 'aurelia-templating', '../validator', '../validati this.lookupFunctions = resources.lookupFunctions; this.getDisplayName = messageProvider.getDisplayName.bind(messageProvider); } + /** + * 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 + * for the object created by ValidationRules....on(class/object) + */ + StandardValidator.prototype.validateProperty = function (object, propertyName, rules) { + return this.validate(object, propertyName, rules || null); + }; + /** + * 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 + * for the object created by ValidationRules....on(class/object) + */ + StandardValidator.prototype.validateObject = function (object, rules) { + return this.validate(object, null, rules || null); + }; + /** + * Determines whether a rule exists in a set of rules. + * @param rules The rules to search. + * @parem rule The rule to find. + */ + StandardValidator.prototype.ruleExists = function (rules, rule) { + var i = rules.length; + while (i--) { + if (rules[i].indexOf(rule) !== -1) { + return true; + } + } + return false; + }; StandardValidator.prototype.getMessage = function (rule, object, value) { var expression = rule.message || this.messageProvider.getMessage(rule.messageKey); var _a = rule.property, propertyName = _a.name, displayName = _a.displayName; @@ -53,6 +86,14 @@ 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 parent containing the field. + //The field is the last part of the propert path + //e.g. finalProp in object.sub1.sub2.finalProp + 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); @@ -88,25 +129,6 @@ define(["require", "exports", 'aurelia-templating', '../validator', '../validati } return this.validateRuleSequence(object, propertyName, rules, 0); }; - /** - * 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 - * for the object created by ValidationRules....on(class/object) - */ - StandardValidator.prototype.validateProperty = function (object, propertyName, rules) { - return this.validate(object, propertyName, rules || null); - }; - /** - * 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 - * for the object created by ValidationRules....on(class/object) - */ - StandardValidator.prototype.validateObject = function (object, rules) { - return this.validate(object, null, rules || null); - }; StandardValidator.inject = [validation_messages_1.ValidationMessageProvider, aurelia_templating_1.ViewResources]; return StandardValidator; }(validator_1.Validator)); diff --git a/dist/amd/implementation/validation-parser.d.ts b/dist/amd/implementation/validation-parser.d.ts index 43f09ca5..75b2c7af 100644 --- a/dist/amd/implementation/validation-parser.d.ts +++ b/dist/amd/implementation/validation-parser.d.ts @@ -13,10 +13,10 @@ export declare class ValidationParser { private undefinedExpression; private cache; constructor(parser: Parser, bindinqLanguage: BindingLanguage); - private coalesce(part); parseMessage(message: string): Expression; - private getAccessorExpression(fn); parseProperty(property: string | PropertyAccessor): RuleProperty; + private coalesce(part); + private getAccessorExpression(fn); } export declare class MessageExpressionValidator extends Unparser { private originalMessage; diff --git a/dist/amd/implementation/validation-parser.js b/dist/amd/implementation/validation-parser.js index 6b112f11..23d54204 100644 --- a/dist/amd/implementation/validation-parser.js +++ b/dist/amd/implementation/validation-parser.js @@ -14,10 +14,6 @@ define(["require", "exports", 'aurelia-binding', 'aurelia-templating', './util', this.undefinedExpression = new aurelia_binding_1.LiteralPrimitive(undefined); this.cache = {}; } - ValidationParser.prototype.coalesce = function (part) { - // part === null || part === undefined ? '' : part - return new aurelia_binding_1.Conditional(new aurelia_binding_1.Binary('||', new aurelia_binding_1.Binary('===', part, this.nullExpression), new aurelia_binding_1.Binary('===', part, this.undefinedExpression)), this.emptyStringExpression, new aurelia_binding_1.CallMember(part, 'toString', [])); - }; ValidationParser.prototype.parseMessage = function (message) { if (this.cache[message] !== undefined) { return this.cache[message]; @@ -34,29 +30,42 @@ define(["require", "exports", 'aurelia-binding', 'aurelia-templating', './util', this.cache[message] = expression; 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 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]); - }; 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 }; } throw new Error("Invalid subject: \"" + accessor + "\""); }; + ValidationParser.prototype.coalesce = function (part) { + // part === null || part === undefined ? '' : part + return new aurelia_binding_1.Conditional(new aurelia_binding_1.Binary('||', new aurelia_binding_1.Binary('===', part, this.nullExpression), new aurelia_binding_1.Binary('===', part, this.undefinedExpression)), this.emptyStringExpression, new aurelia_binding_1.CallMember(part, 'toString', [])); + }; + ValidationParser.prototype.getAccessorExpression = function (fn) { + var classic = /^function\s*\([$_\w\d]+\)\s*\{\s*(?:"use strict";)?\s*return\s+[$_\w\d]+\.([$_\w\d]+(\.[$_\w\d]+)*)\s*;?\s*\}$/; + var arrow = /^\(?[$_\w\d]+\)?\s*=>\s*(?:\{?.*return\s+)?[$_\w\d]+\.([$_\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]); + }; ValidationParser.inject = [aurelia_binding_1.Parser, aurelia_templating_1.BindingLanguage]; return ValidationParser; }()); diff --git a/dist/amd/implementation/validation-rules.d.ts b/dist/amd/implementation/validation-rules.d.ts index 1bd93972..59ba7410 100644 --- a/dist/amd/implementation/validation-rules.d.ts +++ b/dist/amd/implementation/validation-rules.d.ts @@ -195,7 +195,8 @@ export declare class FluentEnsure { constructor(parser: ValidationParser); /** * Target a property with validation rules. - * @param property The property to target. Can be the property name or a property accessor function. + * @param property The property to target. Can be the property name or a property accessor + * function. */ ensure(property: string | PropertyAccessor): FluentRules; /** @@ -233,7 +234,8 @@ export declare class ValidationRules { * @param name The name of the custom rule. Also serves as the message key. * @param condition The rule function. * @param message The message expression - * @param argsToConfig A function that maps the rule's arguments to a "config" object that can be used when evaluating the message expression. + * @param argsToConfig A function that maps the rule's arguments to a "config" + * object that can be used when evaluating the message expression. */ static customRule(name: string, condition: (value: any, object?: any, ...args: any[]) => boolean | Promise, message: string, argsToConfig?: (...args: any[]) => any): void; /** diff --git a/dist/amd/implementation/validation-rules.js b/dist/amd/implementation/validation-rules.js index 8829a8b5..75d98847 100644 --- a/dist/amd/implementation/validation-rules.js +++ b/dist/amd/implementation/validation-rules.js @@ -250,7 +250,9 @@ define(["require", "exports", './util', './rules', './validation-messages'], fun * null, undefined and empty-string values are considered valid. */ FluentRules.prototype.email = function () { - return this.matches(/^((([a-z]|\d|[!#\$%&'\*\+\-\/=\?\^_`{\|}~]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])+(\.([a-z]|\d|[!#\$%&'\*\+\-\/=\?\^_`{\|}~]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])+)*)|((\x22)((((\x20|\x09)*(\x0d\x0a))?(\x20|\x09)+)?(([\x01-\x08\x0b\x0c\x0e-\x1f\x7f]|\x21|[\x23-\x5b]|[\x5d-\x7e]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(\\([\x01-\x09\x0b\x0c\x0d-\x7f]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF]))))*(((\x20|\x09)*(\x0d\x0a))?(\x20|\x09)+)?(\x22)))@((([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])*([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])))\.)+(([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])*([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])))$/i) + // regex from https://html.spec.whatwg.org/multipage/forms.html#valid-e-mail-address + /* tslint:disable:max-line-length */ + return this.matches(/^[a-zA-Z0-9.!#$%&'*+\/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$/) .withMessageKey('email'); }; /** @@ -301,17 +303,20 @@ define(["require", "exports", './util', './rules', './validation-messages'], fun * Part of the fluent rule API. Enables targeting properties and objects with rules. */ var FluentEnsure = (function () { + /* tslint:enable */ function FluentEnsure(parser) { this.parser = parser; /** * Rules that have been defined using the fluent API. */ this.rules = []; + /* tslint:disable */ this._sequence = 0; } /** * Target a property with validation rules. - * @param property The property to target. Can be the property name or a property accessor function. + * @param property The property to target. Can be the property name or a property accessor + * function. */ FluentEnsure.prototype.ensure = function (property) { this.assertInitialized(); @@ -377,7 +382,8 @@ define(["require", "exports", './util', './rules', './validation-messages'], fun * @param name The name of the custom rule. Also serves as the message key. * @param condition The rule function. * @param message The message expression - * @param argsToConfig A function that maps the rule's arguments to a "config" object that can be used when evaluating the message expression. + * @param argsToConfig A function that maps the rule's arguments to a "config" + * object that can be used when evaluating the message expression. */ ValidationRules.customRule = function (name, condition, message, argsToConfig) { validation_messages_1.validationMessages[name] = message; diff --git a/dist/amd/property-info.d.ts b/dist/amd/property-info.d.ts index ea68d9a0..59b760bf 100644 --- a/dist/amd/property-info.d.ts +++ b/dist/amd/property-info.d.ts @@ -5,6 +5,6 @@ import { Expression } from 'aurelia-binding'; * @param source The scope */ export declare function getPropertyInfo(expression: Expression, source: any): { - object: any; + object: Object; propertyName: string; -}; +} | null; diff --git a/dist/amd/property-info.js b/dist/amd/property-info.js index 911ae7a1..91e7d32b 100644 --- a/dist/amd/property-info.js +++ b/dist/amd/property-info.js @@ -2,16 +2,12 @@ define(["require", "exports", 'aurelia-binding'], function (require, exports, au "use strict"; function getObject(expression, objectExpression, source) { var value = objectExpression.evaluate(source, null); - if (value !== null && (typeof value === 'object' || typeof value === 'function')) { + if (value === null || value === undefined || value instanceof Object) { return value; } - if (value === null) { - value = 'null'; - } - else if (value === undefined) { - value = 'undefined'; - } - throw new Error("The '" + objectExpression + "' part of '" + expression + "' evaluates to " + value + " instead of an object."); + /* tslint:disable */ + throw new Error("The '" + objectExpression + "' part of '" + expression + "' evaluates to " + value + " instead of an object, null or undefined."); + /* tslint:enable */ } /** * Retrieves the object and property name for the specified expression. @@ -25,6 +21,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 +29,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,6 +46,9 @@ define(["require", "exports", 'aurelia-binding'], function (require, exports, au else { throw new Error("Expression '" + originalExpression + "' is not compatible with the validate binding-behavior."); } + if (object === null || object === undefined) { + return null; + } return { object: object, propertyName: propertyName }; } exports.getPropertyInfo = getPropertyInfo; diff --git a/dist/amd/validate-binding-behavior-base.d.ts b/dist/amd/validate-binding-behavior-base.d.ts index cb4eb912..09c18430 100644 --- a/dist/amd/validate-binding-behavior-base.d.ts +++ b/dist/amd/validate-binding-behavior-base.d.ts @@ -8,11 +8,11 @@ export declare abstract class ValidateBindingBehaviorBase { constructor(taskQueue: TaskQueue); protected abstract getValidateTrigger(controller: ValidationController): number; /** - * Gets the DOM element associated with the data-binding. Most of the time it's - * the binding.target but sometimes binding.target is an aurelia custom element, - * or custom attribute which is a javascript "class" instance, so we need to use - * the controller's container to retrieve the actual DOM element. - */ + * Gets the DOM element associated with the data-binding. Most of the time it's + * the binding.target but sometimes binding.target is an aurelia custom element, + * or custom attribute which is a javascript "class" instance, so we need to use + * the controller's container to retrieve the actual DOM element. + */ getTarget(binding: any, view: any): any; bind(binding: any, source: any, rulesOrController?: ValidationController | any, rules?: any): void; unbind(binding: any): void; diff --git a/dist/amd/validate-binding-behavior-base.js b/dist/amd/validate-binding-behavior-base.js index b50129ba..9ecc7469 100644 --- a/dist/amd/validate-binding-behavior-base.js +++ b/dist/amd/validate-binding-behavior-base.js @@ -8,11 +8,11 @@ define(["require", "exports", 'aurelia-dependency-injection', 'aurelia-pal', './ this.taskQueue = taskQueue; } /** - * Gets the DOM element associated with the data-binding. Most of the time it's - * the binding.target but sometimes binding.target is an aurelia custom element, - * or custom attribute which is a javascript "class" instance, so we need to use - * the controller's container to retrieve the actual DOM element. - */ + * Gets the DOM element associated with the data-binding. Most of the time it's + * the binding.target but sometimes binding.target is an aurelia custom element, + * or custom attribute which is a javascript "class" instance, so we need to use + * the controller's container to retrieve the actual DOM element. + */ ValidateBindingBehaviorBase.prototype.getTarget = function (binding, view) { var target = binding.target; // DOM element @@ -51,14 +51,20 @@ define(["require", "exports", 'aurelia-dependency-injection', 'aurelia-pal', './ controller.registerBinding(binding, target, rules); binding.validationController = controller; var trigger = this.getValidateTrigger(controller); + /* tslint:disable:no-bitwise */ if (trigger & validate_trigger_1.validateTrigger.change) { + /* tslint:enable:no-bitwise */ binding.standardUpdateSource = binding.updateSource; + /* tslint:disable:only-arrow-functions */ binding.updateSource = function (value) { + /* tslint:enable:only-arrow-functions */ this.standardUpdateSource(value); this.validationController.validateBinding(this); }; } + /* tslint:disable:no-bitwise */ if (trigger & validate_trigger_1.validateTrigger.blur) { + /* tslint:enable:no-bitwise */ binding.validateBlurHandler = function () { _this.taskQueue.queueMicroTask(function () { return controller.validateBinding(binding); }); }; @@ -67,7 +73,9 @@ define(["require", "exports", 'aurelia-dependency-injection', 'aurelia-pal', './ } if (trigger !== validate_trigger_1.validateTrigger.manual) { binding.standardUpdateTarget = binding.updateTarget; + /* tslint:disable:only-arrow-functions */ binding.updateTarget = function (value) { + /* tslint:enable:only-arrow-functions */ this.standardUpdateTarget(value); this.validationController.resetBinding(this); }; diff --git a/dist/amd/validate-trigger.d.ts b/dist/amd/validate-trigger.d.ts index e001bfa3..82c60096 100644 --- a/dist/amd/validate-trigger.d.ts +++ b/dist/amd/validate-trigger.d.ts @@ -1,6 +1,6 @@ /** -* Validation triggers. -*/ + * Validation triggers. + */ export declare const validateTrigger: { manual: number; blur: number; diff --git a/dist/amd/validate-trigger.js b/dist/amd/validate-trigger.js index 2c1a1b0e..c14d4ea7 100644 --- a/dist/amd/validate-trigger.js +++ b/dist/amd/validate-trigger.js @@ -1,21 +1,21 @@ define(["require", "exports"], function (require, exports) { "use strict"; /** - * Validation triggers. - */ + * Validation triggers. + */ exports.validateTrigger = { /** - * Manual validation. Use the controller's `validate()` and `reset()` methods - * to validate all bindings. - */ + * Manual validation. Use the controller's `validate()` and `reset()` methods + * to validate all bindings. + */ manual: 0, /** - * Validate the binding when the binding's target element fires a DOM "blur" event. - */ + * Validate the binding when the binding's target element fires a DOM "blur" event. + */ blur: 1, /** - * Validate the binding when it updates the model due to a change in the view. - */ + * Validate the binding when it updates the model due to a change in the view. + */ change: 2, /** * Validate the binding when the binding's target element fires a DOM "blur" event and diff --git a/dist/amd/validation-controller.d.ts b/dist/amd/validation-controller.d.ts index dcc1c174..a65621f3 100644 --- a/dist/amd/validation-controller.d.ts +++ b/dist/amd/validation-controller.d.ts @@ -90,7 +90,8 @@ export declare class ValidationController { private getInstructionPredicate(instruction?); /** * Validates and renders errors. - * @param instruction Optional. Instructions on what to validate. If undefined, all objects and bindings will be validated. + * @param instruction Optional. Instructions on what to validate. If undefined, all + * objects and bindings will be validated. */ validate(instruction?: ValidateInstruction): Promise; /** @@ -104,11 +105,11 @@ export declare class ValidationController { private getAssociatedElements({object, propertyName}); private processErrorDelta(kind, oldErrors, newErrors); /** - * Validates the property associated with a binding. - */ + * Validates the property associated with a binding. + */ validateBinding(binding: Binding): void; /** - * Resets the errors for a property associated with a binding. - */ + * Resets the errors for a property associated with a binding. + */ resetBinding(binding: Binding): void; } diff --git a/dist/amd/validation-controller.js b/dist/amd/validation-controller.js index a950fc56..8ea7228a 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. @@ -96,7 +96,7 @@ define(["require", "exports", './validator', './validate-trigger', './property-i * @param rules (optional) rules associated with the binding. Validator implementation specific. */ ValidationController.prototype.registerBinding = function (binding, target, rules) { - this.bindings.set(binding, { target: target, rules: rules }); + this.bindings.set(binding, { target: target, rules: rules, propertyInfo: null }); }; /** * Unregisters a binding with the controller. @@ -111,8 +111,9 @@ define(["require", "exports", './validator', './validate-trigger', './property-i * relevant errors in the list of rendered errors. */ ValidationController.prototype.getInstructionPredicate = function (instruction) { + var _this = this; 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; }; @@ -120,9 +121,8 @@ define(["require", "exports", './validator', './validate-trigger', './property-i else { 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) { + return function (x) { return predicate_1(x) && _this.validator.ruleExists(rules_2, x.rule); }; } return predicate_1; } @@ -132,24 +132,43 @@ define(["require", "exports", './validator', './validate-trigger', './property-i }; /** * Validates and renders errors. - * @param instruction Optional. Instructions on what to validate. If undefined, all objects and bindings will be validated. + * @param instruction Optional. Instructions on what to validate. If undefined, all + * objects and bindings will be validated. */ ValidationController.prototype.validate = function (instruction) { var _this = this; // 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) { + for (var _i = 0, _a = Array.from(this.bindings); _i < _a.length; _i++) { + var _b = _a[_i], binding = _b[0], rulesObj = _b[1].rulesObj; + var propertyInfo = property_info_1.getPropertyInfo(binding.sourceExpression, binding.source); + if (propertyInfo.propertyName != propertyName_2 || !propertyInfo || this.objects.has(propertyInfo.object)) { + continue; + } + if (propertyInfo.propertyName.indexOf(".") !== -1) { + var parentProp = ""; + var ittr = binding.sourceExpression.expression; + while (ittr.object) { + ittr = ittr.object; + parentProp = ittr.name; + } + rules_3 = rules_1.Rules.get(binding._observer0._callable0._observer0.obj[parentProp]); + } + } + } // 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 { @@ -161,12 +180,26 @@ define(["require", "exports", './validator', './validate-trigger', './property-i promises.push(_this.validator.validateObject(object, rules)); } for (var _c = 0, _d = Array.from(_this.bindings); _c < _d.length; _c++) { - var _e = _d[_c], binding = _e[0], rules = _e[1].rules; - var _f = property_info_1.getPropertyInfo(binding.sourceExpression, binding.source), object = _f.object, propertyName = _f.propertyName; - if (_this.objects.has(object)) { + var _e = _d[_c], binding = _e[0], rulesObj = _e[1].rulesObj; + var propertyInfo = property_info_1.getPropertyInfo(binding.sourceExpression, binding.source); + if (!propertyInfo || _this.objects.has(propertyInfo.object)) { continue; } - promises.push(_this.validator.validateProperty(object, propertyName, rules)); + var propName = propertyInfo.propertyName; + if (propertyInfo.propertyName.indexOf(".") !== -1) { + var parentProp = ""; + var ittr = binding.sourceExpression.expression; + while (ittr.object) { + ittr = ittr.object; + parentProp = ittr.name; + } + propName = propertyInfo.propertyName.substr(propertyInfo.propertyName.lastIndexOf('.') + 1); + rules = rules_1.Rules.get(binding._observer0._callable0._observer0.obj[parentProp]); + } + else { + rules = rulesObj; + } + promises.push(_this.validator.validateProperty(propertyInfo.object, propertyInfo.propertyName, rules)); } return Promise.all(promises).then(function (errorSets) { return errorSets.reduce(function (a, b) { return a.concat(b); }, []); }); }; @@ -210,8 +243,8 @@ define(["require", "exports", './validator', './validate-trigger', './property-i var elements = []; for (var _i = 0, _b = Array.from(this.bindings); _i < _b.length; _i++) { var _c = _b[_i], binding = _c[0], target = _c[1].target; - var _d = property_info_1.getPropertyInfo(binding.sourceExpression, binding.source), o = _d.object, p = _d.propertyName; - if (o === object && p === propertyName) { + var propertyInfo = property_info_1.getPropertyInfo(binding.sourceExpression, binding.source); + if (propertyInfo && propertyInfo.object === object && propertyInfo.propertyName === propertyName) { elements.push(target); } } @@ -241,7 +274,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); @@ -273,22 +306,41 @@ define(["require", "exports", './validator', './validate-trigger', './property-i } }; /** - * Validates the property associated with a binding. - */ + * Validates the property associated with a binding. + */ ValidationController.prototype.validateBinding = function (binding) { if (!binding.isBound) { return; } - var _a = property_info_1.getPropertyInfo(binding.sourceExpression, binding.source), object = _a.object, propertyName = _a.propertyName; + var propertyInfo = property_info_1.getPropertyInfo(binding.sourceExpression, binding.source); + var rules = undefined; var registeredBinding = this.bindings.get(binding); - var rules = registeredBinding ? registeredBinding.rules : undefined; + if (registeredBinding) { + rules = registeredBinding.rules; + registeredBinding.propertyInfo = propertyInfo; + } + if (!propertyInfo) { + return; + } + var object = propertyInfo.object, propertyName = propertyInfo.propertyName; this.validate({ object: object, propertyName: propertyName, rules: rules }); }; /** - * Resets the errors for a property associated with a binding. - */ + * Resets the errors for a property associated with a binding. + */ ValidationController.prototype.resetBinding = function (binding) { - var _a = property_info_1.getPropertyInfo(binding.sourceExpression, binding.source), object = _a.object, propertyName = _a.propertyName; + var registeredBinding = this.bindings.get(binding); + var propertyInfo = property_info_1.getPropertyInfo(binding.sourceExpression, binding.source); + if (!propertyInfo && registeredBinding) { + propertyInfo = registeredBinding.propertyInfo; + } + if (registeredBinding) { + registeredBinding.propertyInfo = null; + } + if (!propertyInfo) { + return; + } + var object = propertyInfo.object, propertyName = propertyInfo.propertyName; this.reset({ object: object, propertyName: propertyName }); }; ValidationController.inject = [validator_1.Validator]; diff --git a/dist/amd/validation-errors-custom-attribute.js b/dist/amd/validation-errors-custom-attribute.js index 2834d01f..9f38186b 100644 --- a/dist/amd/validation-errors-custom-attribute.js +++ b/dist/amd/validation-errors-custom-attribute.js @@ -17,7 +17,9 @@ define(["require", "exports", 'aurelia-binding', 'aurelia-dependency-injection', if (a.targets[0] === b.targets[0]) { return 0; } + /* tslint:disable:no-bitwise */ return a.targets[0].compareDocumentPosition(b.targets[0]) & 2 ? 1 : -1; + /* tslint:enable:no-bitwise */ }); }; ValidationErrorsCustomAttribute.prototype.interestingElements = function (elements) { diff --git a/dist/amd/validator.d.ts b/dist/amd/validator.d.ts index 104dc3a1..edaa3f40 100644 --- a/dist/amd/validator.d.ts +++ b/dist/amd/validator.d.ts @@ -19,4 +19,10 @@ export declare abstract class Validator { * specified object. This may not be possible for all implementations of this interface. */ abstract validateObject(object: any, rules?: any): Promise; + /** + * Determines whether a rule exists in a set of rules. + * @param rules The rules to search. + * @parem rule The rule to find. + */ + abstract ruleExists(rules: any, rule: any): boolean; } diff --git a/dist/commonjs/implementation/rules.d.ts b/dist/commonjs/implementation/rules.d.ts index 71c46daf..30947f6f 100644 --- a/dist/commonjs/implementation/rules.d.ts +++ b/dist/commonjs/implementation/rules.d.ts @@ -6,7 +6,7 @@ export declare class Rules { /** * The name of the property that stores the rules. */ - static key: string; + private static key; /** * Applies the rules to a target. */ diff --git a/dist/commonjs/implementation/standard-validator.d.ts b/dist/commonjs/implementation/standard-validator.d.ts index e05e7b8e..ad544cbb 100644 --- a/dist/commonjs/implementation/standard-validator.d.ts +++ b/dist/commonjs/implementation/standard-validator.d.ts @@ -1,20 +1,18 @@ import { ViewResources } from 'aurelia-templating'; import { Validator } from '../validator'; import { ValidationError } from '../validation-error'; +import { Rule } from './rule'; import { ValidationMessageProvider } from './validation-messages'; /** * Validates. * 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; constructor(messageProvider: ValidationMessageProvider, resources: ViewResources); - private getMessage(rule, object, value); - private validateRuleSequence(object, propertyName, ruleSequence, sequence); - private validate(object, propertyName, rules); /** * Validates the specified property. * @param object The object to validate. @@ -30,4 +28,13 @@ export declare class StandardValidator extends Validator { * for the object created by ValidationRules....on(class/object) */ validateObject(object: any, rules?: any): Promise; + /** + * Determines whether a rule exists in a set of rules. + * @param rules The rules to search. + * @parem rule The rule to find. + */ + ruleExists(rules: Rule[][], rule: Rule): boolean; + private getMessage(rule, object, value); + private validateRuleSequence(object, propertyName, ruleSequence, sequence); + private validate(object, propertyName, rules); } diff --git a/dist/commonjs/implementation/standard-validator.js b/dist/commonjs/implementation/standard-validator.js index 82314d0d..c205842c 100644 --- a/dist/commonjs/implementation/standard-validator.js +++ b/dist/commonjs/implementation/standard-validator.js @@ -21,6 +21,39 @@ var StandardValidator = (function (_super) { this.lookupFunctions = resources.lookupFunctions; this.getDisplayName = messageProvider.getDisplayName.bind(messageProvider); } + /** + * 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 + * for the object created by ValidationRules....on(class/object) + */ + StandardValidator.prototype.validateProperty = function (object, propertyName, rules) { + return this.validate(object, propertyName, rules || null); + }; + /** + * 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 + * for the object created by ValidationRules....on(class/object) + */ + StandardValidator.prototype.validateObject = function (object, rules) { + return this.validate(object, null, rules || null); + }; + /** + * Determines whether a rule exists in a set of rules. + * @param rules The rules to search. + * @parem rule The rule to find. + */ + StandardValidator.prototype.ruleExists = function (rules, rule) { + var i = rules.length; + while (i--) { + if (rules[i].indexOf(rule) !== -1) { + return true; + } + } + return false; + }; StandardValidator.prototype.getMessage = function (rule, object, value) { var expression = rule.message || this.messageProvider.getMessage(rule.messageKey); var _a = rule.property, propertyName = _a.name, displayName = _a.displayName; @@ -57,6 +90,14 @@ 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 parent containing the field. + //The field is the last part of the propert path + //e.g. finalProp in object.sub1.sub2.finalProp + 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); @@ -92,25 +133,6 @@ var StandardValidator = (function (_super) { } return this.validateRuleSequence(object, propertyName, rules, 0); }; - /** - * 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 - * for the object created by ValidationRules....on(class/object) - */ - StandardValidator.prototype.validateProperty = function (object, propertyName, rules) { - return this.validate(object, propertyName, rules || null); - }; - /** - * 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 - * for the object created by ValidationRules....on(class/object) - */ - StandardValidator.prototype.validateObject = function (object, rules) { - return this.validate(object, null, rules || null); - }; StandardValidator.inject = [validation_messages_1.ValidationMessageProvider, aurelia_templating_1.ViewResources]; return StandardValidator; }(validator_1.Validator)); diff --git a/dist/commonjs/implementation/validation-parser.d.ts b/dist/commonjs/implementation/validation-parser.d.ts index 43f09ca5..75b2c7af 100644 --- a/dist/commonjs/implementation/validation-parser.d.ts +++ b/dist/commonjs/implementation/validation-parser.d.ts @@ -13,10 +13,10 @@ export declare class ValidationParser { private undefinedExpression; private cache; constructor(parser: Parser, bindinqLanguage: BindingLanguage); - private coalesce(part); parseMessage(message: string): Expression; - private getAccessorExpression(fn); parseProperty(property: string | PropertyAccessor): RuleProperty; + private coalesce(part); + private getAccessorExpression(fn); } export declare class MessageExpressionValidator extends Unparser { private originalMessage; diff --git a/dist/commonjs/implementation/validation-parser.js b/dist/commonjs/implementation/validation-parser.js index 67c6b45b..29177183 100644 --- a/dist/commonjs/implementation/validation-parser.js +++ b/dist/commonjs/implementation/validation-parser.js @@ -17,10 +17,6 @@ var ValidationParser = (function () { this.undefinedExpression = new aurelia_binding_1.LiteralPrimitive(undefined); this.cache = {}; } - ValidationParser.prototype.coalesce = function (part) { - // part === null || part === undefined ? '' : part - return new aurelia_binding_1.Conditional(new aurelia_binding_1.Binary('||', new aurelia_binding_1.Binary('===', part, this.nullExpression), new aurelia_binding_1.Binary('===', part, this.undefinedExpression)), this.emptyStringExpression, new aurelia_binding_1.CallMember(part, 'toString', [])); - }; ValidationParser.prototype.parseMessage = function (message) { if (this.cache[message] !== undefined) { return this.cache[message]; @@ -37,29 +33,42 @@ var ValidationParser = (function () { this.cache[message] = expression; 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 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]); - }; 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 }; } throw new Error("Invalid subject: \"" + accessor + "\""); }; + ValidationParser.prototype.coalesce = function (part) { + // part === null || part === undefined ? '' : part + return new aurelia_binding_1.Conditional(new aurelia_binding_1.Binary('||', new aurelia_binding_1.Binary('===', part, this.nullExpression), new aurelia_binding_1.Binary('===', part, this.undefinedExpression)), this.emptyStringExpression, new aurelia_binding_1.CallMember(part, 'toString', [])); + }; + ValidationParser.prototype.getAccessorExpression = function (fn) { + var classic = /^function\s*\([$_\w\d]+\)\s*\{\s*(?:"use strict";)?\s*return\s+[$_\w\d]+\.([$_\w\d]+(\.[$_\w\d]+)*)\s*;?\s*\}$/; + var arrow = /^\(?[$_\w\d]+\)?\s*=>\s*(?:\{?.*return\s+)?[$_\w\d]+\.([$_\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]); + }; ValidationParser.inject = [aurelia_binding_1.Parser, aurelia_templating_1.BindingLanguage]; return ValidationParser; }()); diff --git a/dist/commonjs/implementation/validation-rules.d.ts b/dist/commonjs/implementation/validation-rules.d.ts index 1bd93972..59ba7410 100644 --- a/dist/commonjs/implementation/validation-rules.d.ts +++ b/dist/commonjs/implementation/validation-rules.d.ts @@ -195,7 +195,8 @@ export declare class FluentEnsure { constructor(parser: ValidationParser); /** * Target a property with validation rules. - * @param property The property to target. Can be the property name or a property accessor function. + * @param property The property to target. Can be the property name or a property accessor + * function. */ ensure(property: string | PropertyAccessor): FluentRules; /** @@ -233,7 +234,8 @@ export declare class ValidationRules { * @param name The name of the custom rule. Also serves as the message key. * @param condition The rule function. * @param message The message expression - * @param argsToConfig A function that maps the rule's arguments to a "config" object that can be used when evaluating the message expression. + * @param argsToConfig A function that maps the rule's arguments to a "config" + * object that can be used when evaluating the message expression. */ static customRule(name: string, condition: (value: any, object?: any, ...args: any[]) => boolean | Promise, message: string, argsToConfig?: (...args: any[]) => any): void; /** diff --git a/dist/commonjs/implementation/validation-rules.js b/dist/commonjs/implementation/validation-rules.js index d6291c4b..2726596e 100644 --- a/dist/commonjs/implementation/validation-rules.js +++ b/dist/commonjs/implementation/validation-rules.js @@ -252,7 +252,9 @@ var FluentRules = (function () { * null, undefined and empty-string values are considered valid. */ FluentRules.prototype.email = function () { - return this.matches(/^((([a-z]|\d|[!#\$%&'\*\+\-\/=\?\^_`{\|}~]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])+(\.([a-z]|\d|[!#\$%&'\*\+\-\/=\?\^_`{\|}~]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])+)*)|((\x22)((((\x20|\x09)*(\x0d\x0a))?(\x20|\x09)+)?(([\x01-\x08\x0b\x0c\x0e-\x1f\x7f]|\x21|[\x23-\x5b]|[\x5d-\x7e]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(\\([\x01-\x09\x0b\x0c\x0d-\x7f]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF]))))*(((\x20|\x09)*(\x0d\x0a))?(\x20|\x09)+)?(\x22)))@((([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])*([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])))\.)+(([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])*([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])))$/i) + // regex from https://html.spec.whatwg.org/multipage/forms.html#valid-e-mail-address + /* tslint:disable:max-line-length */ + return this.matches(/^[a-zA-Z0-9.!#$%&'*+\/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$/) .withMessageKey('email'); }; /** @@ -303,17 +305,20 @@ exports.FluentRules = FluentRules; * Part of the fluent rule API. Enables targeting properties and objects with rules. */ var FluentEnsure = (function () { + /* tslint:enable */ function FluentEnsure(parser) { this.parser = parser; /** * Rules that have been defined using the fluent API. */ this.rules = []; + /* tslint:disable */ this._sequence = 0; } /** * Target a property with validation rules. - * @param property The property to target. Can be the property name or a property accessor function. + * @param property The property to target. Can be the property name or a property accessor + * function. */ FluentEnsure.prototype.ensure = function (property) { this.assertInitialized(); @@ -379,7 +384,8 @@ var ValidationRules = (function () { * @param name The name of the custom rule. Also serves as the message key. * @param condition The rule function. * @param message The message expression - * @param argsToConfig A function that maps the rule's arguments to a "config" object that can be used when evaluating the message expression. + * @param argsToConfig A function that maps the rule's arguments to a "config" + * object that can be used when evaluating the message expression. */ ValidationRules.customRule = function (name, condition, message, argsToConfig) { validation_messages_1.validationMessages[name] = message; diff --git a/dist/commonjs/property-info.d.ts b/dist/commonjs/property-info.d.ts index ea68d9a0..59b760bf 100644 --- a/dist/commonjs/property-info.d.ts +++ b/dist/commonjs/property-info.d.ts @@ -5,6 +5,6 @@ import { Expression } from 'aurelia-binding'; * @param source The scope */ export declare function getPropertyInfo(expression: Expression, source: any): { - object: any; + object: Object; propertyName: string; -}; +} | null; diff --git a/dist/commonjs/property-info.js b/dist/commonjs/property-info.js index 9a8030de..d9c89192 100644 --- a/dist/commonjs/property-info.js +++ b/dist/commonjs/property-info.js @@ -2,16 +2,12 @@ var aurelia_binding_1 = require('aurelia-binding'); function getObject(expression, objectExpression, source) { var value = objectExpression.evaluate(source, null); - if (value !== null && (typeof value === 'object' || typeof value === 'function')) { + if (value === null || value === undefined || value instanceof Object) { return value; } - if (value === null) { - value = 'null'; - } - else if (value === undefined) { - value = 'undefined'; - } - throw new Error("The '" + objectExpression + "' part of '" + expression + "' evaluates to " + value + " instead of an object."); + /* tslint:disable */ + throw new Error("The '" + objectExpression + "' part of '" + expression + "' evaluates to " + value + " instead of an object, null or undefined."); + /* tslint:enable */ } /** * Retrieves the object and property name for the specified expression. @@ -25,6 +21,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 +29,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 +46,9 @@ function getPropertyInfo(expression, source) { else { throw new Error("Expression '" + originalExpression + "' is not compatible with the validate binding-behavior."); } + if (object === null || object === undefined) { + return null; + } return { object: object, propertyName: propertyName }; } exports.getPropertyInfo = getPropertyInfo; diff --git a/dist/commonjs/validate-binding-behavior-base.d.ts b/dist/commonjs/validate-binding-behavior-base.d.ts index cb4eb912..09c18430 100644 --- a/dist/commonjs/validate-binding-behavior-base.d.ts +++ b/dist/commonjs/validate-binding-behavior-base.d.ts @@ -8,11 +8,11 @@ export declare abstract class ValidateBindingBehaviorBase { constructor(taskQueue: TaskQueue); protected abstract getValidateTrigger(controller: ValidationController): number; /** - * Gets the DOM element associated with the data-binding. Most of the time it's - * the binding.target but sometimes binding.target is an aurelia custom element, - * or custom attribute which is a javascript "class" instance, so we need to use - * the controller's container to retrieve the actual DOM element. - */ + * Gets the DOM element associated with the data-binding. Most of the time it's + * the binding.target but sometimes binding.target is an aurelia custom element, + * or custom attribute which is a javascript "class" instance, so we need to use + * the controller's container to retrieve the actual DOM element. + */ getTarget(binding: any, view: any): any; bind(binding: any, source: any, rulesOrController?: ValidationController | any, rules?: any): void; unbind(binding: any): void; diff --git a/dist/commonjs/validate-binding-behavior-base.js b/dist/commonjs/validate-binding-behavior-base.js index 6b8df181..b54e1aac 100644 --- a/dist/commonjs/validate-binding-behavior-base.js +++ b/dist/commonjs/validate-binding-behavior-base.js @@ -11,11 +11,11 @@ var ValidateBindingBehaviorBase = (function () { this.taskQueue = taskQueue; } /** - * Gets the DOM element associated with the data-binding. Most of the time it's - * the binding.target but sometimes binding.target is an aurelia custom element, - * or custom attribute which is a javascript "class" instance, so we need to use - * the controller's container to retrieve the actual DOM element. - */ + * Gets the DOM element associated with the data-binding. Most of the time it's + * the binding.target but sometimes binding.target is an aurelia custom element, + * or custom attribute which is a javascript "class" instance, so we need to use + * the controller's container to retrieve the actual DOM element. + */ ValidateBindingBehaviorBase.prototype.getTarget = function (binding, view) { var target = binding.target; // DOM element @@ -54,14 +54,20 @@ var ValidateBindingBehaviorBase = (function () { controller.registerBinding(binding, target, rules); binding.validationController = controller; var trigger = this.getValidateTrigger(controller); + /* tslint:disable:no-bitwise */ if (trigger & validate_trigger_1.validateTrigger.change) { + /* tslint:enable:no-bitwise */ binding.standardUpdateSource = binding.updateSource; + /* tslint:disable:only-arrow-functions */ binding.updateSource = function (value) { + /* tslint:enable:only-arrow-functions */ this.standardUpdateSource(value); this.validationController.validateBinding(this); }; } + /* tslint:disable:no-bitwise */ if (trigger & validate_trigger_1.validateTrigger.blur) { + /* tslint:enable:no-bitwise */ binding.validateBlurHandler = function () { _this.taskQueue.queueMicroTask(function () { return controller.validateBinding(binding); }); }; @@ -70,7 +76,9 @@ var ValidateBindingBehaviorBase = (function () { } if (trigger !== validate_trigger_1.validateTrigger.manual) { binding.standardUpdateTarget = binding.updateTarget; + /* tslint:disable:only-arrow-functions */ binding.updateTarget = function (value) { + /* tslint:enable:only-arrow-functions */ this.standardUpdateTarget(value); this.validationController.resetBinding(this); }; diff --git a/dist/commonjs/validate-trigger.d.ts b/dist/commonjs/validate-trigger.d.ts index e001bfa3..82c60096 100644 --- a/dist/commonjs/validate-trigger.d.ts +++ b/dist/commonjs/validate-trigger.d.ts @@ -1,6 +1,6 @@ /** -* Validation triggers. -*/ + * Validation triggers. + */ export declare const validateTrigger: { manual: number; blur: number; diff --git a/dist/commonjs/validate-trigger.js b/dist/commonjs/validate-trigger.js index 5cf0cf1c..1d18fc8f 100644 --- a/dist/commonjs/validate-trigger.js +++ b/dist/commonjs/validate-trigger.js @@ -1,20 +1,20 @@ "use strict"; /** -* Validation triggers. -*/ + * Validation triggers. + */ exports.validateTrigger = { /** - * Manual validation. Use the controller's `validate()` and `reset()` methods - * to validate all bindings. - */ + * Manual validation. Use the controller's `validate()` and `reset()` methods + * to validate all bindings. + */ manual: 0, /** - * Validate the binding when the binding's target element fires a DOM "blur" event. - */ + * Validate the binding when the binding's target element fires a DOM "blur" event. + */ blur: 1, /** - * Validate the binding when it updates the model due to a change in the view. - */ + * Validate the binding when it updates the model due to a change in the view. + */ change: 2, /** * Validate the binding when the binding's target element fires a DOM "blur" event and diff --git a/dist/commonjs/validation-controller.d.ts b/dist/commonjs/validation-controller.d.ts index dcc1c174..a65621f3 100644 --- a/dist/commonjs/validation-controller.d.ts +++ b/dist/commonjs/validation-controller.d.ts @@ -90,7 +90,8 @@ export declare class ValidationController { private getInstructionPredicate(instruction?); /** * Validates and renders errors. - * @param instruction Optional. Instructions on what to validate. If undefined, all objects and bindings will be validated. + * @param instruction Optional. Instructions on what to validate. If undefined, all + * objects and bindings will be validated. */ validate(instruction?: ValidateInstruction): Promise; /** @@ -104,11 +105,11 @@ export declare class ValidationController { private getAssociatedElements({object, propertyName}); private processErrorDelta(kind, oldErrors, newErrors); /** - * Validates the property associated with a binding. - */ + * Validates the property associated with a binding. + */ validateBinding(binding: Binding): void; /** - * Resets the errors for a property associated with a binding. - */ + * Resets the errors for a property associated with a binding. + */ resetBinding(binding: Binding): void; } diff --git a/dist/commonjs/validation-controller.js b/dist/commonjs/validation-controller.js index a85eeb6e..bd92a60c 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. @@ -99,7 +100,7 @@ var ValidationController = (function () { * @param rules (optional) rules associated with the binding. Validator implementation specific. */ ValidationController.prototype.registerBinding = function (binding, target, rules) { - this.bindings.set(binding, { target: target, rules: rules }); + this.bindings.set(binding, { target: target, rules: rules, propertyInfo: null }); }; /** * Unregisters a binding with the controller. @@ -114,8 +115,9 @@ var ValidationController = (function () { * relevant errors in the list of rendered errors. */ ValidationController.prototype.getInstructionPredicate = function (instruction) { + var _this = this; 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; }; @@ -123,9 +125,8 @@ var ValidationController = (function () { else { 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) { + return function (x) { return predicate_1(x) && _this.validator.ruleExists(rules_2, x.rule); }; } return predicate_1; } @@ -135,24 +136,43 @@ var ValidationController = (function () { }; /** * Validates and renders errors. - * @param instruction Optional. Instructions on what to validate. If undefined, all objects and bindings will be validated. + * @param instruction Optional. Instructions on what to validate. If undefined, all + * objects and bindings will be validated. */ ValidationController.prototype.validate = function (instruction) { var _this = this; // 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) { + for (var _i = 0, _a = Array.from(this.bindings); _i < _a.length; _i++) { + var _b = _a[_i], binding = _b[0], rulesObj = _b[1].rulesObj; + var propertyInfo = property_info_1.getPropertyInfo(binding.sourceExpression, binding.source); + if (propertyInfo.propertyName != propertyName_2 || !propertyInfo || this.objects.has(propertyInfo.object)) { + continue; + } + if (propertyInfo.propertyName.indexOf(".") !== -1) { + var parentProp = ""; + var ittr = binding.sourceExpression.expression; + while (ittr.object) { + ittr = ittr.object; + parentProp = ittr.name; + } + rules_3 = rules_1.Rules.get(binding._observer0._callable0._observer0.obj[parentProp]); + } + } + } // 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 { @@ -164,12 +184,26 @@ var ValidationController = (function () { promises.push(_this.validator.validateObject(object, rules)); } for (var _c = 0, _d = Array.from(_this.bindings); _c < _d.length; _c++) { - var _e = _d[_c], binding = _e[0], rules = _e[1].rules; - var _f = property_info_1.getPropertyInfo(binding.sourceExpression, binding.source), object = _f.object, propertyName = _f.propertyName; - if (_this.objects.has(object)) { + var _e = _d[_c], binding = _e[0], rulesObj = _e[1].rulesObj; + var propertyInfo = property_info_1.getPropertyInfo(binding.sourceExpression, binding.source); + if (!propertyInfo || _this.objects.has(propertyInfo.object)) { continue; } - promises.push(_this.validator.validateProperty(object, propertyName, rules)); + var propName = propertyInfo.propertyName; + if (propertyInfo.propertyName.indexOf(".") !== -1) { + var parentProp = ""; + var ittr = binding.sourceExpression.expression; + while (ittr.object) { + ittr = ittr.object; + parentProp = ittr.name; + } + propName = propertyInfo.propertyName.substr(propertyInfo.propertyName.lastIndexOf('.') + 1); + rules = rules_1.Rules.get(binding._observer0._callable0._observer0.obj[parentProp]); + } + else { + rules = rulesObj; + } + promises.push(_this.validator.validateProperty(propertyInfo.object, propertyInfo.propertyName, rules)); } return Promise.all(promises).then(function (errorSets) { return errorSets.reduce(function (a, b) { return a.concat(b); }, []); }); }; @@ -213,8 +247,8 @@ var ValidationController = (function () { var elements = []; for (var _i = 0, _b = Array.from(this.bindings); _i < _b.length; _i++) { var _c = _b[_i], binding = _c[0], target = _c[1].target; - var _d = property_info_1.getPropertyInfo(binding.sourceExpression, binding.source), o = _d.object, p = _d.propertyName; - if (o === object && p === propertyName) { + var propertyInfo = property_info_1.getPropertyInfo(binding.sourceExpression, binding.source); + if (propertyInfo && propertyInfo.object === object && propertyInfo.propertyName === propertyName) { elements.push(target); } } @@ -244,7 +278,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); @@ -276,22 +310,41 @@ var ValidationController = (function () { } }; /** - * Validates the property associated with a binding. - */ + * Validates the property associated with a binding. + */ ValidationController.prototype.validateBinding = function (binding) { if (!binding.isBound) { return; } - var _a = property_info_1.getPropertyInfo(binding.sourceExpression, binding.source), object = _a.object, propertyName = _a.propertyName; + var propertyInfo = property_info_1.getPropertyInfo(binding.sourceExpression, binding.source); + var rules = undefined; var registeredBinding = this.bindings.get(binding); - var rules = registeredBinding ? registeredBinding.rules : undefined; + if (registeredBinding) { + rules = registeredBinding.rules; + registeredBinding.propertyInfo = propertyInfo; + } + if (!propertyInfo) { + return; + } + var object = propertyInfo.object, propertyName = propertyInfo.propertyName; this.validate({ object: object, propertyName: propertyName, rules: rules }); }; /** - * Resets the errors for a property associated with a binding. - */ + * Resets the errors for a property associated with a binding. + */ ValidationController.prototype.resetBinding = function (binding) { - var _a = property_info_1.getPropertyInfo(binding.sourceExpression, binding.source), object = _a.object, propertyName = _a.propertyName; + var registeredBinding = this.bindings.get(binding); + var propertyInfo = property_info_1.getPropertyInfo(binding.sourceExpression, binding.source); + if (!propertyInfo && registeredBinding) { + propertyInfo = registeredBinding.propertyInfo; + } + if (registeredBinding) { + registeredBinding.propertyInfo = null; + } + if (!propertyInfo) { + return; + } + var object = propertyInfo.object, propertyName = propertyInfo.propertyName; this.reset({ object: object, propertyName: propertyName }); }; ValidationController.inject = [validator_1.Validator]; diff --git a/dist/commonjs/validation-errors-custom-attribute.js b/dist/commonjs/validation-errors-custom-attribute.js index e3e538da..24098a40 100644 --- a/dist/commonjs/validation-errors-custom-attribute.js +++ b/dist/commonjs/validation-errors-custom-attribute.js @@ -20,7 +20,9 @@ var ValidationErrorsCustomAttribute = (function () { if (a.targets[0] === b.targets[0]) { return 0; } + /* tslint:disable:no-bitwise */ return a.targets[0].compareDocumentPosition(b.targets[0]) & 2 ? 1 : -1; + /* tslint:enable:no-bitwise */ }); }; ValidationErrorsCustomAttribute.prototype.interestingElements = function (elements) { diff --git a/dist/commonjs/validator.d.ts b/dist/commonjs/validator.d.ts index 104dc3a1..edaa3f40 100644 --- a/dist/commonjs/validator.d.ts +++ b/dist/commonjs/validator.d.ts @@ -19,4 +19,10 @@ export declare abstract class Validator { * specified object. This may not be possible for all implementations of this interface. */ abstract validateObject(object: any, rules?: any): Promise; + /** + * Determines whether a rule exists in a set of rules. + * @param rules The rules to search. + * @parem rule The rule to find. + */ + abstract ruleExists(rules: any, rule: any): boolean; } diff --git a/dist/es2015/implementation/rules.d.ts b/dist/es2015/implementation/rules.d.ts index 71c46daf..30947f6f 100644 --- a/dist/es2015/implementation/rules.d.ts +++ b/dist/es2015/implementation/rules.d.ts @@ -6,7 +6,7 @@ export declare class Rules { /** * The name of the property that stores the rules. */ - static key: string; + private static key; /** * Applies the rules to a target. */ diff --git a/dist/es2015/implementation/standard-validator.d.ts b/dist/es2015/implementation/standard-validator.d.ts index e05e7b8e..ad544cbb 100644 --- a/dist/es2015/implementation/standard-validator.d.ts +++ b/dist/es2015/implementation/standard-validator.d.ts @@ -1,20 +1,18 @@ import { ViewResources } from 'aurelia-templating'; import { Validator } from '../validator'; import { ValidationError } from '../validation-error'; +import { Rule } from './rule'; import { ValidationMessageProvider } from './validation-messages'; /** * Validates. * 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; constructor(messageProvider: ValidationMessageProvider, resources: ViewResources); - private getMessage(rule, object, value); - private validateRuleSequence(object, propertyName, ruleSequence, sequence); - private validate(object, propertyName, rules); /** * Validates the specified property. * @param object The object to validate. @@ -30,4 +28,13 @@ export declare class StandardValidator extends Validator { * for the object created by ValidationRules....on(class/object) */ validateObject(object: any, rules?: any): Promise; + /** + * Determines whether a rule exists in a set of rules. + * @param rules The rules to search. + * @parem rule The rule to find. + */ + ruleExists(rules: Rule[][], rule: Rule): boolean; + private getMessage(rule, object, value); + private validateRuleSequence(object, propertyName, ruleSequence, sequence); + private validate(object, propertyName, rules); } diff --git a/dist/es2015/implementation/standard-validator.js b/dist/es2015/implementation/standard-validator.js index f81057ec..27a59adc 100644 --- a/dist/es2015/implementation/standard-validator.js +++ b/dist/es2015/implementation/standard-validator.js @@ -14,6 +14,39 @@ export class StandardValidator extends Validator { this.lookupFunctions = resources.lookupFunctions; this.getDisplayName = messageProvider.getDisplayName.bind(messageProvider); } + /** + * 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 + * for the object created by ValidationRules....on(class/object) + */ + validateProperty(object, propertyName, rules) { + return this.validate(object, propertyName, rules || null); + } + /** + * 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 + * for the object created by ValidationRules....on(class/object) + */ + validateObject(object, rules) { + return this.validate(object, null, rules || null); + } + /** + * Determines whether a rule exists in a set of rules. + * @param rules The rules to search. + * @parem rule The rule to find. + */ + ruleExists(rules, rule) { + let i = rules.length; + while (i--) { + if (rules[i].indexOf(rule) !== -1) { + return true; + } + } + return false; + } getMessage(rule, object, value) { const expression = rule.message || this.messageProvider.getMessage(rule.messageKey); let { name: propertyName, displayName } = rule.property; @@ -48,7 +81,15 @@ 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 parent containing the field. + //The field is the last part of the propert path + //e.g. finalProp in object.sub1.sub2.finalProp + let parts = rule.property.name.split('.'); + value = object[parts[parts.length - 1]]; + } let promiseOrBoolean = rule.condition(value, object); if (!(promiseOrBoolean instanceof Promise)) { promiseOrBoolean = Promise.resolve(promiseOrBoolean); @@ -81,24 +122,5 @@ export class StandardValidator extends Validator { } return this.validateRuleSequence(object, propertyName, rules, 0); } - /** - * 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 - * for the object created by ValidationRules....on(class/object) - */ - validateProperty(object, propertyName, rules) { - return this.validate(object, propertyName, rules || null); - } - /** - * 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 - * for the object created by ValidationRules....on(class/object) - */ - validateObject(object, rules) { - return this.validate(object, null, rules || null); - } } StandardValidator.inject = [ValidationMessageProvider, ViewResources]; diff --git a/dist/es2015/implementation/validation-parser.d.ts b/dist/es2015/implementation/validation-parser.d.ts index 43f09ca5..75b2c7af 100644 --- a/dist/es2015/implementation/validation-parser.d.ts +++ b/dist/es2015/implementation/validation-parser.d.ts @@ -13,10 +13,10 @@ export declare class ValidationParser { private undefinedExpression; private cache; constructor(parser: Parser, bindinqLanguage: BindingLanguage); - private coalesce(part); parseMessage(message: string): Expression; - private getAccessorExpression(fn); parseProperty(property: string | PropertyAccessor): RuleProperty; + private coalesce(part); + private getAccessorExpression(fn); } export declare class MessageExpressionValidator extends Unparser { private originalMessage; diff --git a/dist/es2015/implementation/validation-parser.js b/dist/es2015/implementation/validation-parser.js index 96b0aeba..c76a1313 100644 --- a/dist/es2015/implementation/validation-parser.js +++ b/dist/es2015/implementation/validation-parser.js @@ -11,10 +11,6 @@ export class ValidationParser { this.undefinedExpression = new LiteralPrimitive(undefined); this.cache = {}; } - coalesce(part) { - // part === null || part === undefined ? '' : part - return new Conditional(new Binary('||', new Binary('===', part, this.nullExpression), new Binary('===', part, this.undefinedExpression)), this.emptyStringExpression, new CallMember(part, 'toString', [])); - } parseMessage(message) { if (this.cache[message] !== undefined) { return this.cache[message]; @@ -31,29 +27,42 @@ export class ValidationParser { this.cache[message] = expression; 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 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]); - } 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 }; } throw new Error(`Invalid subject: "${accessor}"`); } + coalesce(part) { + // part === null || part === undefined ? '' : part + return new Conditional(new Binary('||', new Binary('===', part, this.nullExpression), new Binary('===', part, this.undefinedExpression)), this.emptyStringExpression, new CallMember(part, 'toString', [])); + } + getAccessorExpression(fn) { + const classic = /^function\s*\([$_\w\d]+\)\s*\{\s*(?:"use strict";)?\s*return\s+[$_\w\d]+\.([$_\w\d]+(\.[$_\w\d]+)*)\s*;?\s*\}$/; + const arrow = /^\(?[$_\w\d]+\)?\s*=>\s*(?:\{?.*return\s+)?[$_\w\d]+\.([$_\w\d]+(\.[$_\w\d]+)*);?\s*\}?$/; + const match = classic.exec(fn) || arrow.exec(fn); + if (match === null) { + throw new Error(`Unable to parse accessor function:\n${fn}`); + } + return this.parser.parse(match[1]); + } } ValidationParser.inject = [Parser, BindingLanguage]; export class MessageExpressionValidator extends Unparser { diff --git a/dist/es2015/implementation/validation-rules.d.ts b/dist/es2015/implementation/validation-rules.d.ts index 1bd93972..59ba7410 100644 --- a/dist/es2015/implementation/validation-rules.d.ts +++ b/dist/es2015/implementation/validation-rules.d.ts @@ -195,7 +195,8 @@ export declare class FluentEnsure { constructor(parser: ValidationParser); /** * Target a property with validation rules. - * @param property The property to target. Can be the property name or a property accessor function. + * @param property The property to target. Can be the property name or a property accessor + * function. */ ensure(property: string | PropertyAccessor): FluentRules; /** @@ -233,7 +234,8 @@ export declare class ValidationRules { * @param name The name of the custom rule. Also serves as the message key. * @param condition The rule function. * @param message The message expression - * @param argsToConfig A function that maps the rule's arguments to a "config" object that can be used when evaluating the message expression. + * @param argsToConfig A function that maps the rule's arguments to a "config" + * object that can be used when evaluating the message expression. */ static customRule(name: string, condition: (value: any, object?: any, ...args: any[]) => boolean | Promise, message: string, argsToConfig?: (...args: any[]) => any): void; /** diff --git a/dist/es2015/implementation/validation-rules.js b/dist/es2015/implementation/validation-rules.js index d38e5cbb..88f87df4 100644 --- a/dist/es2015/implementation/validation-rules.js +++ b/dist/es2015/implementation/validation-rules.js @@ -232,7 +232,9 @@ export class FluentRules { * null, undefined and empty-string values are considered valid. */ email() { - return this.matches(/^((([a-z]|\d|[!#\$%&'\*\+\-\/=\?\^_`{\|}~]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])+(\.([a-z]|\d|[!#\$%&'\*\+\-\/=\?\^_`{\|}~]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])+)*)|((\x22)((((\x20|\x09)*(\x0d\x0a))?(\x20|\x09)+)?(([\x01-\x08\x0b\x0c\x0e-\x1f\x7f]|\x21|[\x23-\x5b]|[\x5d-\x7e]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(\\([\x01-\x09\x0b\x0c\x0d-\x7f]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF]))))*(((\x20|\x09)*(\x0d\x0a))?(\x20|\x09)+)?(\x22)))@((([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])*([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])))\.)+(([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])*([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])))$/i) + // regex from https://html.spec.whatwg.org/multipage/forms.html#valid-e-mail-address + /* tslint:disable:max-line-length */ + return this.matches(/^[a-zA-Z0-9.!#$%&'*+\/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$/) .withMessageKey('email'); } /** @@ -281,17 +283,20 @@ FluentRules.customRules = {}; * Part of the fluent rule API. Enables targeting properties and objects with rules. */ export class FluentEnsure { + /* tslint:enable */ constructor(parser) { this.parser = parser; /** * Rules that have been defined using the fluent API. */ this.rules = []; + /* tslint:disable */ this._sequence = 0; } /** * Target a property with validation rules. - * @param property The property to target. Can be the property name or a property accessor function. + * @param property The property to target. Can be the property name or a property accessor + * function. */ ensure(property) { this.assertInitialized(); @@ -353,7 +358,8 @@ export class ValidationRules { * @param name The name of the custom rule. Also serves as the message key. * @param condition The rule function. * @param message The message expression - * @param argsToConfig A function that maps the rule's arguments to a "config" object that can be used when evaluating the message expression. + * @param argsToConfig A function that maps the rule's arguments to a "config" + * object that can be used when evaluating the message expression. */ static customRule(name, condition, message, argsToConfig) { validationMessages[name] = message; diff --git a/dist/es2015/property-info.d.ts b/dist/es2015/property-info.d.ts index ea68d9a0..59b760bf 100644 --- a/dist/es2015/property-info.d.ts +++ b/dist/es2015/property-info.d.ts @@ -5,6 +5,6 @@ import { Expression } from 'aurelia-binding'; * @param source The scope */ export declare function getPropertyInfo(expression: Expression, source: any): { - object: any; + object: Object; propertyName: string; -}; +} | null; diff --git a/dist/es2015/property-info.js b/dist/es2015/property-info.js index 19d445b9..d2a4dc69 100644 --- a/dist/es2015/property-info.js +++ b/dist/es2015/property-info.js @@ -1,16 +1,12 @@ import { AccessMember, AccessScope, AccessKeyed, BindingBehavior, ValueConverter } from 'aurelia-binding'; function getObject(expression, objectExpression, source) { let value = objectExpression.evaluate(source, null); - if (value !== null && (typeof value === 'object' || typeof value === 'function')) { + if (value === null || value === undefined || value instanceof Object) { return value; } - if (value === null) { - value = 'null'; - } - else if (value === undefined) { - value = 'undefined'; - } - throw new Error(`The '${objectExpression}' part of '${expression}' evaluates to ${value} instead of an object.`); + /* tslint:disable */ + throw new Error(`The '${objectExpression}' part of '${expression}' evaluates to ${value} instead of an object, null or undefined.`); + /* tslint:enable */ } /** * Retrieves the object and property name for the specified expression. @@ -24,6 +20,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 +28,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 +45,8 @@ export function getPropertyInfo(expression, source) { else { throw new Error(`Expression '${originalExpression}' is not compatible with the validate binding-behavior.`); } + if (object === null || object === undefined) { + return null; + } return { object, propertyName }; } diff --git a/dist/es2015/validate-binding-behavior-base.d.ts b/dist/es2015/validate-binding-behavior-base.d.ts index cb4eb912..09c18430 100644 --- a/dist/es2015/validate-binding-behavior-base.d.ts +++ b/dist/es2015/validate-binding-behavior-base.d.ts @@ -8,11 +8,11 @@ export declare abstract class ValidateBindingBehaviorBase { constructor(taskQueue: TaskQueue); protected abstract getValidateTrigger(controller: ValidationController): number; /** - * Gets the DOM element associated with the data-binding. Most of the time it's - * the binding.target but sometimes binding.target is an aurelia custom element, - * or custom attribute which is a javascript "class" instance, so we need to use - * the controller's container to retrieve the actual DOM element. - */ + * Gets the DOM element associated with the data-binding. Most of the time it's + * the binding.target but sometimes binding.target is an aurelia custom element, + * or custom attribute which is a javascript "class" instance, so we need to use + * the controller's container to retrieve the actual DOM element. + */ getTarget(binding: any, view: any): any; bind(binding: any, source: any, rulesOrController?: ValidationController | any, rules?: any): void; unbind(binding: any): void; diff --git a/dist/es2015/validate-binding-behavior-base.js b/dist/es2015/validate-binding-behavior-base.js index c9dcbc6b..991f7c52 100644 --- a/dist/es2015/validate-binding-behavior-base.js +++ b/dist/es2015/validate-binding-behavior-base.js @@ -10,11 +10,11 @@ export class ValidateBindingBehaviorBase { this.taskQueue = taskQueue; } /** - * Gets the DOM element associated with the data-binding. Most of the time it's - * the binding.target but sometimes binding.target is an aurelia custom element, - * or custom attribute which is a javascript "class" instance, so we need to use - * the controller's container to retrieve the actual DOM element. - */ + * Gets the DOM element associated with the data-binding. Most of the time it's + * the binding.target but sometimes binding.target is an aurelia custom element, + * or custom attribute which is a javascript "class" instance, so we need to use + * the controller's container to retrieve the actual DOM element. + */ getTarget(binding, view) { const target = binding.target; // DOM element @@ -52,14 +52,20 @@ export class ValidateBindingBehaviorBase { controller.registerBinding(binding, target, rules); binding.validationController = controller; const trigger = this.getValidateTrigger(controller); + /* tslint:disable:no-bitwise */ if (trigger & validateTrigger.change) { + /* tslint:enable:no-bitwise */ binding.standardUpdateSource = binding.updateSource; + /* tslint:disable:only-arrow-functions */ binding.updateSource = function (value) { + /* tslint:enable:only-arrow-functions */ this.standardUpdateSource(value); this.validationController.validateBinding(this); }; } + /* tslint:disable:no-bitwise */ if (trigger & validateTrigger.blur) { + /* tslint:enable:no-bitwise */ binding.validateBlurHandler = () => { this.taskQueue.queueMicroTask(() => controller.validateBinding(binding)); }; @@ -68,7 +74,9 @@ export class ValidateBindingBehaviorBase { } if (trigger !== validateTrigger.manual) { binding.standardUpdateTarget = binding.updateTarget; + /* tslint:disable:only-arrow-functions */ binding.updateTarget = function (value) { + /* tslint:enable:only-arrow-functions */ this.standardUpdateTarget(value); this.validationController.resetBinding(this); }; diff --git a/dist/es2015/validate-trigger.d.ts b/dist/es2015/validate-trigger.d.ts index e001bfa3..82c60096 100644 --- a/dist/es2015/validate-trigger.d.ts +++ b/dist/es2015/validate-trigger.d.ts @@ -1,6 +1,6 @@ /** -* Validation triggers. -*/ + * Validation triggers. + */ export declare const validateTrigger: { manual: number; blur: number; diff --git a/dist/es2015/validate-trigger.js b/dist/es2015/validate-trigger.js index f999a39b..9329aaf3 100644 --- a/dist/es2015/validate-trigger.js +++ b/dist/es2015/validate-trigger.js @@ -1,19 +1,19 @@ /** -* Validation triggers. -*/ + * Validation triggers. + */ export const validateTrigger = { /** - * Manual validation. Use the controller's `validate()` and `reset()` methods - * to validate all bindings. - */ + * Manual validation. Use the controller's `validate()` and `reset()` methods + * to validate all bindings. + */ manual: 0, /** - * Validate the binding when the binding's target element fires a DOM "blur" event. - */ + * Validate the binding when the binding's target element fires a DOM "blur" event. + */ blur: 1, /** - * Validate the binding when it updates the model due to a change in the view. - */ + * Validate the binding when it updates the model due to a change in the view. + */ change: 2, /** * Validate the binding when the binding's target element fires a DOM "blur" event and diff --git a/dist/es2015/validation-controller.d.ts b/dist/es2015/validation-controller.d.ts index dcc1c174..a65621f3 100644 --- a/dist/es2015/validation-controller.d.ts +++ b/dist/es2015/validation-controller.d.ts @@ -90,7 +90,8 @@ export declare class ValidationController { private getInstructionPredicate(instruction?); /** * Validates and renders errors. - * @param instruction Optional. Instructions on what to validate. If undefined, all objects and bindings will be validated. + * @param instruction Optional. Instructions on what to validate. If undefined, all + * objects and bindings will be validated. */ validate(instruction?: ValidateInstruction): Promise; /** @@ -104,11 +105,11 @@ export declare class ValidationController { private getAssociatedElements({object, propertyName}); private processErrorDelta(kind, oldErrors, newErrors); /** - * Validates the property associated with a binding. - */ + * Validates the property associated with a binding. + */ validateBinding(binding: Binding): void; /** - * Resets the errors for a property associated with a binding. - */ + * Resets the errors for a property associated with a binding. + */ resetBinding(binding: Binding): void; } diff --git a/dist/es2015/validation-controller.js b/dist/es2015/validation-controller.js index 7cb3f54d..d6983268 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. @@ -96,7 +97,7 @@ export class ValidationController { * @param rules (optional) rules associated with the binding. Validator implementation specific. */ registerBinding(binding, target, rules) { - this.bindings.set(binding, { target, rules }); + this.bindings.set(binding, { target, rules, propertyInfo: null }); } /** * Unregisters a binding with the controller. @@ -120,9 +121,8 @@ export class ValidationController { else { predicate = x => x.object === object; } - // todo: move to Validator interface: - if (rules && rules.indexOf) { - return x => predicate(x) && rules.indexOf(x.rule) !== -1; + if (rules) { + return x => predicate(x) && this.validator.ruleExists(rules, x.rule); } return predicate; } @@ -132,7 +132,8 @@ export class ValidationController { } /** * Validates and renders errors. - * @param instruction Optional. Instructions on what to validate. If undefined, all objects and bindings will be validated. + * @param instruction Optional. Instructions on what to validate. If undefined, all + * objects and bindings will be validated. */ validate(instruction) { // Get a function that will process the validation instruction. @@ -141,6 +142,23 @@ export class ValidationController { let { object, propertyName, rules } = instruction; // if rules were not specified, check the object map. rules = rules || this.objects.get(object); + if (!rules) { + for (let [binding, { rulesObj }] of Array.from(this.bindings)) { + const propertyInfo = getPropertyInfo(binding.sourceExpression, binding.source); + if (propertyInfo.propertyName != propertyName || !propertyInfo || this.objects.has(propertyInfo.object)) { + continue; + } + if (propertyInfo.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]); + } + } + } // property specified? if (instruction.propertyName === undefined) { // validate the specified object. @@ -158,12 +176,26 @@ export class ValidationController { for (let [object, rules] of Array.from(this.objects)) { promises.push(this.validator.validateObject(object, rules)); } - for (let [binding, { rules }] of Array.from(this.bindings)) { - const { object, propertyName } = getPropertyInfo(binding.sourceExpression, binding.source); - if (this.objects.has(object)) { + for (let [binding, { rulesObj }] of Array.from(this.bindings)) { + const propertyInfo = getPropertyInfo(binding.sourceExpression, binding.source); + if (!propertyInfo || this.objects.has(propertyInfo.object)) { continue; } - promises.push(this.validator.validateProperty(object, propertyName, rules)); + const propName = propertyInfo.propertyName; + if (propertyInfo.propertyName.indexOf(".") !== -1) { + let parentProp = ""; + let ittr = binding.sourceExpression.expression; + while (ittr.object) { + ittr = ittr.object; + parentProp = ittr.name; + } + propName = propertyInfo.propertyName.substr(propertyInfo.propertyName.lastIndexOf('.') + 1); + rules = Rules.get(binding._observer0._callable0._observer0.obj[parentProp]); + } + else { + rules = rulesObj; + } + promises.push(this.validator.validateProperty(propertyInfo.object, propertyInfo.propertyName, rules)); } return Promise.all(promises).then(errorSets => errorSets.reduce((a, b) => a.concat(b), [])); }; @@ -205,8 +237,8 @@ export class ValidationController { getAssociatedElements({ object, propertyName }) { const elements = []; for (let [binding, { target }] of Array.from(this.bindings)) { - const { object: o, propertyName: p } = getPropertyInfo(binding.sourceExpression, binding.source); - if (o === object && p === propertyName) { + const propertyInfo = getPropertyInfo(binding.sourceExpression, binding.source); + if (propertyInfo && propertyInfo.object === object && propertyInfo.propertyName === propertyName) { elements.push(target); } } @@ -236,7 +268,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); @@ -261,22 +293,41 @@ export class ValidationController { } } /** - * Validates the property associated with a binding. - */ + * Validates the property associated with a binding. + */ validateBinding(binding) { if (!binding.isBound) { return; } - const { object, propertyName } = getPropertyInfo(binding.sourceExpression, binding.source); + let propertyInfo = getPropertyInfo(binding.sourceExpression, binding.source); + let rules = undefined; const registeredBinding = this.bindings.get(binding); - const rules = registeredBinding ? registeredBinding.rules : undefined; + if (registeredBinding) { + rules = registeredBinding.rules; + registeredBinding.propertyInfo = propertyInfo; + } + if (!propertyInfo) { + return; + } + const { object, propertyName } = propertyInfo; this.validate({ object, propertyName, rules }); } /** - * Resets the errors for a property associated with a binding. - */ + * Resets the errors for a property associated with a binding. + */ resetBinding(binding) { - const { object, propertyName } = getPropertyInfo(binding.sourceExpression, binding.source); + const registeredBinding = this.bindings.get(binding); + let propertyInfo = getPropertyInfo(binding.sourceExpression, binding.source); + if (!propertyInfo && registeredBinding) { + propertyInfo = registeredBinding.propertyInfo; + } + if (registeredBinding) { + registeredBinding.propertyInfo = null; + } + if (!propertyInfo) { + return; + } + const { object, propertyName } = propertyInfo; this.reset({ object, propertyName }); } } diff --git a/dist/es2015/validation-errors-custom-attribute.js b/dist/es2015/validation-errors-custom-attribute.js index e4888635..0debeb0d 100644 --- a/dist/es2015/validation-errors-custom-attribute.js +++ b/dist/es2015/validation-errors-custom-attribute.js @@ -19,7 +19,9 @@ export let ValidationErrorsCustomAttribute = class ValidationErrorsCustomAttribu if (a.targets[0] === b.targets[0]) { return 0; } + /* tslint:disable:no-bitwise */ return a.targets[0].compareDocumentPosition(b.targets[0]) & 2 ? 1 : -1; + /* tslint:enable:no-bitwise */ }); } interestingElements(elements) { @@ -35,7 +37,7 @@ export let ValidationErrorsCustomAttribute = class ValidationErrorsCustomAttribu for (let { error, elements } of instruction.render) { const targets = this.interestingElements(elements); if (targets.length) { - this.errors.push({ error: error, targets }); + this.errors.push({ error, targets }); } } this.sort(); diff --git a/dist/es2015/validator.d.ts b/dist/es2015/validator.d.ts index 104dc3a1..edaa3f40 100644 --- a/dist/es2015/validator.d.ts +++ b/dist/es2015/validator.d.ts @@ -19,4 +19,10 @@ export declare abstract class Validator { * specified object. This may not be possible for all implementations of this interface. */ abstract validateObject(object: any, rules?: any): Promise; + /** + * Determines whether a rule exists in a set of rules. + * @param rules The rules to search. + * @parem rule The rule to find. + */ + abstract ruleExists(rules: any, rule: any): boolean; } diff --git a/dist/native-modules/implementation/rules.d.ts b/dist/native-modules/implementation/rules.d.ts index 71c46daf..30947f6f 100644 --- a/dist/native-modules/implementation/rules.d.ts +++ b/dist/native-modules/implementation/rules.d.ts @@ -6,7 +6,7 @@ export declare class Rules { /** * The name of the property that stores the rules. */ - static key: string; + private static key; /** * Applies the rules to a target. */ diff --git a/dist/native-modules/implementation/standard-validator.d.ts b/dist/native-modules/implementation/standard-validator.d.ts index e05e7b8e..ad544cbb 100644 --- a/dist/native-modules/implementation/standard-validator.d.ts +++ b/dist/native-modules/implementation/standard-validator.d.ts @@ -1,20 +1,18 @@ import { ViewResources } from 'aurelia-templating'; import { Validator } from '../validator'; import { ValidationError } from '../validation-error'; +import { Rule } from './rule'; import { ValidationMessageProvider } from './validation-messages'; /** * Validates. * 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; constructor(messageProvider: ValidationMessageProvider, resources: ViewResources); - private getMessage(rule, object, value); - private validateRuleSequence(object, propertyName, ruleSequence, sequence); - private validate(object, propertyName, rules); /** * Validates the specified property. * @param object The object to validate. @@ -30,4 +28,13 @@ export declare class StandardValidator extends Validator { * for the object created by ValidationRules....on(class/object) */ validateObject(object: any, rules?: any): Promise; + /** + * Determines whether a rule exists in a set of rules. + * @param rules The rules to search. + * @parem rule The rule to find. + */ + ruleExists(rules: Rule[][], rule: Rule): boolean; + private getMessage(rule, object, value); + private validateRuleSequence(object, propertyName, ruleSequence, sequence); + private validate(object, propertyName, rules); } diff --git a/dist/native-modules/implementation/standard-validator.js b/dist/native-modules/implementation/standard-validator.js index 6ad67066..a1bcd935 100644 --- a/dist/native-modules/implementation/standard-validator.js +++ b/dist/native-modules/implementation/standard-validator.js @@ -20,6 +20,39 @@ export var StandardValidator = (function (_super) { this.lookupFunctions = resources.lookupFunctions; this.getDisplayName = messageProvider.getDisplayName.bind(messageProvider); } + /** + * 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 + * for the object created by ValidationRules....on(class/object) + */ + StandardValidator.prototype.validateProperty = function (object, propertyName, rules) { + return this.validate(object, propertyName, rules || null); + }; + /** + * 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 + * for the object created by ValidationRules....on(class/object) + */ + StandardValidator.prototype.validateObject = function (object, rules) { + return this.validate(object, null, rules || null); + }; + /** + * Determines whether a rule exists in a set of rules. + * @param rules The rules to search. + * @parem rule The rule to find. + */ + StandardValidator.prototype.ruleExists = function (rules, rule) { + var i = rules.length; + while (i--) { + if (rules[i].indexOf(rule) !== -1) { + return true; + } + } + return false; + }; StandardValidator.prototype.getMessage = function (rule, object, value) { var expression = rule.message || this.messageProvider.getMessage(rule.messageKey); var _a = rule.property, propertyName = _a.name, displayName = _a.displayName; @@ -56,6 +89,14 @@ 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 parent containing the field. + //The field is the last part of the propert path + //e.g. finalProp in object.sub1.sub2.finalProp + 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); @@ -91,25 +132,6 @@ export var StandardValidator = (function (_super) { } return this.validateRuleSequence(object, propertyName, rules, 0); }; - /** - * 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 - * for the object created by ValidationRules....on(class/object) - */ - StandardValidator.prototype.validateProperty = function (object, propertyName, rules) { - return this.validate(object, propertyName, rules || null); - }; - /** - * 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 - * for the object created by ValidationRules....on(class/object) - */ - StandardValidator.prototype.validateObject = function (object, rules) { - return this.validate(object, null, rules || null); - }; StandardValidator.inject = [ValidationMessageProvider, ViewResources]; return StandardValidator; }(Validator)); diff --git a/dist/native-modules/implementation/validation-parser.d.ts b/dist/native-modules/implementation/validation-parser.d.ts index 43f09ca5..75b2c7af 100644 --- a/dist/native-modules/implementation/validation-parser.d.ts +++ b/dist/native-modules/implementation/validation-parser.d.ts @@ -13,10 +13,10 @@ export declare class ValidationParser { private undefinedExpression; private cache; constructor(parser: Parser, bindinqLanguage: BindingLanguage); - private coalesce(part); parseMessage(message: string): Expression; - private getAccessorExpression(fn); parseProperty(property: string | PropertyAccessor): RuleProperty; + private coalesce(part); + private getAccessorExpression(fn); } export declare class MessageExpressionValidator extends Unparser { private originalMessage; diff --git a/dist/native-modules/implementation/validation-parser.js b/dist/native-modules/implementation/validation-parser.js index 1587f771..bcbb41b4 100644 --- a/dist/native-modules/implementation/validation-parser.js +++ b/dist/native-modules/implementation/validation-parser.js @@ -16,10 +16,6 @@ export var ValidationParser = (function () { this.undefinedExpression = new LiteralPrimitive(undefined); this.cache = {}; } - ValidationParser.prototype.coalesce = function (part) { - // part === null || part === undefined ? '' : part - return new Conditional(new Binary('||', new Binary('===', part, this.nullExpression), new Binary('===', part, this.undefinedExpression)), this.emptyStringExpression, new CallMember(part, 'toString', [])); - }; ValidationParser.prototype.parseMessage = function (message) { if (this.cache[message] !== undefined) { return this.cache[message]; @@ -36,29 +32,42 @@ export var ValidationParser = (function () { this.cache[message] = expression; 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 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]); - }; 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 }; } throw new Error("Invalid subject: \"" + accessor + "\""); }; + ValidationParser.prototype.coalesce = function (part) { + // part === null || part === undefined ? '' : part + return new Conditional(new Binary('||', new Binary('===', part, this.nullExpression), new Binary('===', part, this.undefinedExpression)), this.emptyStringExpression, new CallMember(part, 'toString', [])); + }; + ValidationParser.prototype.getAccessorExpression = function (fn) { + var classic = /^function\s*\([$_\w\d]+\)\s*\{\s*(?:"use strict";)?\s*return\s+[$_\w\d]+\.([$_\w\d]+(\.[$_\w\d]+)*)\s*;?\s*\}$/; + var arrow = /^\(?[$_\w\d]+\)?\s*=>\s*(?:\{?.*return\s+)?[$_\w\d]+\.([$_\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]); + }; ValidationParser.inject = [Parser, BindingLanguage]; return ValidationParser; }()); diff --git a/dist/native-modules/implementation/validation-rules.d.ts b/dist/native-modules/implementation/validation-rules.d.ts index 1bd93972..59ba7410 100644 --- a/dist/native-modules/implementation/validation-rules.d.ts +++ b/dist/native-modules/implementation/validation-rules.d.ts @@ -195,7 +195,8 @@ export declare class FluentEnsure { constructor(parser: ValidationParser); /** * Target a property with validation rules. - * @param property The property to target. Can be the property name or a property accessor function. + * @param property The property to target. Can be the property name or a property accessor + * function. */ ensure(property: string | PropertyAccessor): FluentRules; /** @@ -233,7 +234,8 @@ export declare class ValidationRules { * @param name The name of the custom rule. Also serves as the message key. * @param condition The rule function. * @param message The message expression - * @param argsToConfig A function that maps the rule's arguments to a "config" object that can be used when evaluating the message expression. + * @param argsToConfig A function that maps the rule's arguments to a "config" + * object that can be used when evaluating the message expression. */ static customRule(name: string, condition: (value: any, object?: any, ...args: any[]) => boolean | Promise, message: string, argsToConfig?: (...args: any[]) => any): void; /** diff --git a/dist/native-modules/implementation/validation-rules.js b/dist/native-modules/implementation/validation-rules.js index 4fc9daa2..02920554 100644 --- a/dist/native-modules/implementation/validation-rules.js +++ b/dist/native-modules/implementation/validation-rules.js @@ -250,7 +250,9 @@ export var FluentRules = (function () { * null, undefined and empty-string values are considered valid. */ FluentRules.prototype.email = function () { - return this.matches(/^((([a-z]|\d|[!#\$%&'\*\+\-\/=\?\^_`{\|}~]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])+(\.([a-z]|\d|[!#\$%&'\*\+\-\/=\?\^_`{\|}~]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])+)*)|((\x22)((((\x20|\x09)*(\x0d\x0a))?(\x20|\x09)+)?(([\x01-\x08\x0b\x0c\x0e-\x1f\x7f]|\x21|[\x23-\x5b]|[\x5d-\x7e]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(\\([\x01-\x09\x0b\x0c\x0d-\x7f]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF]))))*(((\x20|\x09)*(\x0d\x0a))?(\x20|\x09)+)?(\x22)))@((([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])*([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])))\.)+(([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])*([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])))$/i) + // regex from https://html.spec.whatwg.org/multipage/forms.html#valid-e-mail-address + /* tslint:disable:max-line-length */ + return this.matches(/^[a-zA-Z0-9.!#$%&'*+\/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$/) .withMessageKey('email'); }; /** @@ -300,17 +302,20 @@ export var FluentRules = (function () { * Part of the fluent rule API. Enables targeting properties and objects with rules. */ export var FluentEnsure = (function () { + /* tslint:enable */ function FluentEnsure(parser) { this.parser = parser; /** * Rules that have been defined using the fluent API. */ this.rules = []; + /* tslint:disable */ this._sequence = 0; } /** * Target a property with validation rules. - * @param property The property to target. Can be the property name or a property accessor function. + * @param property The property to target. Can be the property name or a property accessor + * function. */ FluentEnsure.prototype.ensure = function (property) { this.assertInitialized(); @@ -375,7 +380,8 @@ export var ValidationRules = (function () { * @param name The name of the custom rule. Also serves as the message key. * @param condition The rule function. * @param message The message expression - * @param argsToConfig A function that maps the rule's arguments to a "config" object that can be used when evaluating the message expression. + * @param argsToConfig A function that maps the rule's arguments to a "config" + * object that can be used when evaluating the message expression. */ ValidationRules.customRule = function (name, condition, message, argsToConfig) { validationMessages[name] = message; diff --git a/dist/native-modules/property-info.d.ts b/dist/native-modules/property-info.d.ts index ea68d9a0..59b760bf 100644 --- a/dist/native-modules/property-info.d.ts +++ b/dist/native-modules/property-info.d.ts @@ -5,6 +5,6 @@ import { Expression } from 'aurelia-binding'; * @param source The scope */ export declare function getPropertyInfo(expression: Expression, source: any): { - object: any; + object: Object; propertyName: string; -}; +} | null; diff --git a/dist/native-modules/property-info.js b/dist/native-modules/property-info.js index 5b2e3be2..58a35451 100644 --- a/dist/native-modules/property-info.js +++ b/dist/native-modules/property-info.js @@ -1,16 +1,12 @@ import { AccessMember, AccessScope, AccessKeyed, BindingBehavior, ValueConverter } from 'aurelia-binding'; function getObject(expression, objectExpression, source) { var value = objectExpression.evaluate(source, null); - if (value !== null && (typeof value === 'object' || typeof value === 'function')) { + if (value === null || value === undefined || value instanceof Object) { return value; } - if (value === null) { - value = 'null'; - } - else if (value === undefined) { - value = 'undefined'; - } - throw new Error("The '" + objectExpression + "' part of '" + expression + "' evaluates to " + value + " instead of an object."); + /* tslint:disable */ + throw new Error("The '" + objectExpression + "' part of '" + expression + "' evaluates to " + value + " instead of an object, null or undefined."); + /* tslint:enable */ } /** * Retrieves the object and property name for the specified expression. @@ -24,6 +20,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 +28,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 +45,8 @@ export function getPropertyInfo(expression, source) { else { throw new Error("Expression '" + originalExpression + "' is not compatible with the validate binding-behavior."); } + if (object === null || object === undefined) { + return null; + } return { object: object, propertyName: propertyName }; } diff --git a/dist/native-modules/validate-binding-behavior-base.d.ts b/dist/native-modules/validate-binding-behavior-base.d.ts index cb4eb912..09c18430 100644 --- a/dist/native-modules/validate-binding-behavior-base.d.ts +++ b/dist/native-modules/validate-binding-behavior-base.d.ts @@ -8,11 +8,11 @@ export declare abstract class ValidateBindingBehaviorBase { constructor(taskQueue: TaskQueue); protected abstract getValidateTrigger(controller: ValidationController): number; /** - * Gets the DOM element associated with the data-binding. Most of the time it's - * the binding.target but sometimes binding.target is an aurelia custom element, - * or custom attribute which is a javascript "class" instance, so we need to use - * the controller's container to retrieve the actual DOM element. - */ + * Gets the DOM element associated with the data-binding. Most of the time it's + * the binding.target but sometimes binding.target is an aurelia custom element, + * or custom attribute which is a javascript "class" instance, so we need to use + * the controller's container to retrieve the actual DOM element. + */ getTarget(binding: any, view: any): any; bind(binding: any, source: any, rulesOrController?: ValidationController | any, rules?: any): void; unbind(binding: any): void; diff --git a/dist/native-modules/validate-binding-behavior-base.js b/dist/native-modules/validate-binding-behavior-base.js index 6f96fd0e..88d6aa73 100644 --- a/dist/native-modules/validate-binding-behavior-base.js +++ b/dist/native-modules/validate-binding-behavior-base.js @@ -10,11 +10,11 @@ export var ValidateBindingBehaviorBase = (function () { this.taskQueue = taskQueue; } /** - * Gets the DOM element associated with the data-binding. Most of the time it's - * the binding.target but sometimes binding.target is an aurelia custom element, - * or custom attribute which is a javascript "class" instance, so we need to use - * the controller's container to retrieve the actual DOM element. - */ + * Gets the DOM element associated with the data-binding. Most of the time it's + * the binding.target but sometimes binding.target is an aurelia custom element, + * or custom attribute which is a javascript "class" instance, so we need to use + * the controller's container to retrieve the actual DOM element. + */ ValidateBindingBehaviorBase.prototype.getTarget = function (binding, view) { var target = binding.target; // DOM element @@ -53,14 +53,20 @@ export var ValidateBindingBehaviorBase = (function () { controller.registerBinding(binding, target, rules); binding.validationController = controller; var trigger = this.getValidateTrigger(controller); + /* tslint:disable:no-bitwise */ if (trigger & validateTrigger.change) { + /* tslint:enable:no-bitwise */ binding.standardUpdateSource = binding.updateSource; + /* tslint:disable:only-arrow-functions */ binding.updateSource = function (value) { + /* tslint:enable:only-arrow-functions */ this.standardUpdateSource(value); this.validationController.validateBinding(this); }; } + /* tslint:disable:no-bitwise */ if (trigger & validateTrigger.blur) { + /* tslint:enable:no-bitwise */ binding.validateBlurHandler = function () { _this.taskQueue.queueMicroTask(function () { return controller.validateBinding(binding); }); }; @@ -69,7 +75,9 @@ export var ValidateBindingBehaviorBase = (function () { } if (trigger !== validateTrigger.manual) { binding.standardUpdateTarget = binding.updateTarget; + /* tslint:disable:only-arrow-functions */ binding.updateTarget = function (value) { + /* tslint:enable:only-arrow-functions */ this.standardUpdateTarget(value); this.validationController.resetBinding(this); }; diff --git a/dist/native-modules/validate-trigger.d.ts b/dist/native-modules/validate-trigger.d.ts index e001bfa3..82c60096 100644 --- a/dist/native-modules/validate-trigger.d.ts +++ b/dist/native-modules/validate-trigger.d.ts @@ -1,6 +1,6 @@ /** -* Validation triggers. -*/ + * Validation triggers. + */ export declare const validateTrigger: { manual: number; blur: number; diff --git a/dist/native-modules/validate-trigger.js b/dist/native-modules/validate-trigger.js index 06c3554b..7bae6c41 100644 --- a/dist/native-modules/validate-trigger.js +++ b/dist/native-modules/validate-trigger.js @@ -1,19 +1,19 @@ /** -* Validation triggers. -*/ + * Validation triggers. + */ export var validateTrigger = { /** - * Manual validation. Use the controller's `validate()` and `reset()` methods - * to validate all bindings. - */ + * Manual validation. Use the controller's `validate()` and `reset()` methods + * to validate all bindings. + */ manual: 0, /** - * Validate the binding when the binding's target element fires a DOM "blur" event. - */ + * Validate the binding when the binding's target element fires a DOM "blur" event. + */ blur: 1, /** - * Validate the binding when it updates the model due to a change in the view. - */ + * Validate the binding when it updates the model due to a change in the view. + */ change: 2, /** * Validate the binding when the binding's target element fires a DOM "blur" event and diff --git a/dist/native-modules/validation-controller.d.ts b/dist/native-modules/validation-controller.d.ts index dcc1c174..a65621f3 100644 --- a/dist/native-modules/validation-controller.d.ts +++ b/dist/native-modules/validation-controller.d.ts @@ -90,7 +90,8 @@ export declare class ValidationController { private getInstructionPredicate(instruction?); /** * Validates and renders errors. - * @param instruction Optional. Instructions on what to validate. If undefined, all objects and bindings will be validated. + * @param instruction Optional. Instructions on what to validate. If undefined, all + * objects and bindings will be validated. */ validate(instruction?: ValidateInstruction): Promise; /** @@ -104,11 +105,11 @@ export declare class ValidationController { private getAssociatedElements({object, propertyName}); private processErrorDelta(kind, oldErrors, newErrors); /** - * Validates the property associated with a binding. - */ + * Validates the property associated with a binding. + */ validateBinding(binding: Binding): void; /** - * Resets the errors for a property associated with a binding. - */ + * Resets the errors for a property associated with a binding. + */ resetBinding(binding: Binding): void; } diff --git a/dist/native-modules/validation-controller.js b/dist/native-modules/validation-controller.js index 02d56385..fd442567 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. @@ -98,7 +99,7 @@ export var ValidationController = (function () { * @param rules (optional) rules associated with the binding. Validator implementation specific. */ ValidationController.prototype.registerBinding = function (binding, target, rules) { - this.bindings.set(binding, { target: target, rules: rules }); + this.bindings.set(binding, { target: target, rules: rules, propertyInfo: null }); }; /** * Unregisters a binding with the controller. @@ -113,6 +114,7 @@ export var ValidationController = (function () { * relevant errors in the list of rendered errors. */ ValidationController.prototype.getInstructionPredicate = function (instruction) { + var _this = this; if (instruction) { var object_1 = instruction.object, propertyName_1 = instruction.propertyName, rules_1 = instruction.rules; var predicate_1; @@ -122,9 +124,8 @@ export var ValidationController = (function () { else { 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_1) { + return function (x) { return predicate_1(x) && _this.validator.ruleExists(rules_1, x.rule); }; } return predicate_1; } @@ -134,7 +135,8 @@ export var ValidationController = (function () { }; /** * Validates and renders errors. - * @param instruction Optional. Instructions on what to validate. If undefined, all objects and bindings will be validated. + * @param instruction Optional. Instructions on what to validate. If undefined, all + * objects and bindings will be validated. */ ValidationController.prototype.validate = function (instruction) { var _this = this; @@ -144,6 +146,24 @@ 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) { + for (var _i = 0, _a = Array.from(this.bindings); _i < _a.length; _i++) { + var _b = _a[_i], binding = _b[0], rulesObj = _b[1].rulesObj; + var propertyInfo = getPropertyInfo(binding.sourceExpression, binding.source); + if (propertyInfo.propertyName != propertyName_2 || !propertyInfo || this.objects.has(propertyInfo.object)) { + continue; + } + if (propertyInfo.propertyName.indexOf(".") !== -1) { + var parentProp = ""; + var ittr = binding.sourceExpression.expression; + while (ittr.object) { + ittr = ittr.object; + parentProp = ittr.name; + } + rules_2 = Rules.get(binding._observer0._callable0._observer0.obj[parentProp]); + } + } + } // property specified? if (instruction.propertyName === undefined) { // validate the specified object. @@ -163,12 +183,26 @@ export var ValidationController = (function () { promises.push(_this.validator.validateObject(object, rules)); } for (var _c = 0, _d = Array.from(_this.bindings); _c < _d.length; _c++) { - var _e = _d[_c], binding = _e[0], rules = _e[1].rules; - var _f = getPropertyInfo(binding.sourceExpression, binding.source), object = _f.object, propertyName = _f.propertyName; - if (_this.objects.has(object)) { + var _e = _d[_c], binding = _e[0], rulesObj = _e[1].rulesObj; + var propertyInfo = getPropertyInfo(binding.sourceExpression, binding.source); + if (!propertyInfo || _this.objects.has(propertyInfo.object)) { continue; } - promises.push(_this.validator.validateProperty(object, propertyName, rules)); + var propName = propertyInfo.propertyName; + if (propertyInfo.propertyName.indexOf(".") !== -1) { + var parentProp = ""; + var ittr = binding.sourceExpression.expression; + while (ittr.object) { + ittr = ittr.object; + parentProp = ittr.name; + } + propName = propertyInfo.propertyName.substr(propertyInfo.propertyName.lastIndexOf('.') + 1); + rules = Rules.get(binding._observer0._callable0._observer0.obj[parentProp]); + } + else { + rules = rulesObj; + } + promises.push(_this.validator.validateProperty(propertyInfo.object, propertyInfo.propertyName, rules)); } return Promise.all(promises).then(function (errorSets) { return errorSets.reduce(function (a, b) { return a.concat(b); }, []); }); }; @@ -212,8 +246,8 @@ export var ValidationController = (function () { var elements = []; for (var _i = 0, _b = Array.from(this.bindings); _i < _b.length; _i++) { var _c = _b[_i], binding = _c[0], target = _c[1].target; - var _d = getPropertyInfo(binding.sourceExpression, binding.source), o = _d.object, p = _d.propertyName; - if (o === object && p === propertyName) { + var propertyInfo = getPropertyInfo(binding.sourceExpression, binding.source); + if (propertyInfo && propertyInfo.object === object && propertyInfo.propertyName === propertyName) { elements.push(target); } } @@ -243,7 +277,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); @@ -275,22 +309,41 @@ export var ValidationController = (function () { } }; /** - * Validates the property associated with a binding. - */ + * Validates the property associated with a binding. + */ ValidationController.prototype.validateBinding = function (binding) { if (!binding.isBound) { return; } - var _a = getPropertyInfo(binding.sourceExpression, binding.source), object = _a.object, propertyName = _a.propertyName; + var propertyInfo = getPropertyInfo(binding.sourceExpression, binding.source); + var rules = undefined; var registeredBinding = this.bindings.get(binding); - var rules = registeredBinding ? registeredBinding.rules : undefined; + if (registeredBinding) { + rules = registeredBinding.rules; + registeredBinding.propertyInfo = propertyInfo; + } + if (!propertyInfo) { + return; + } + var object = propertyInfo.object, propertyName = propertyInfo.propertyName; this.validate({ object: object, propertyName: propertyName, rules: rules }); }; /** - * Resets the errors for a property associated with a binding. - */ + * Resets the errors for a property associated with a binding. + */ ValidationController.prototype.resetBinding = function (binding) { - var _a = getPropertyInfo(binding.sourceExpression, binding.source), object = _a.object, propertyName = _a.propertyName; + var registeredBinding = this.bindings.get(binding); + var propertyInfo = getPropertyInfo(binding.sourceExpression, binding.source); + if (!propertyInfo && registeredBinding) { + propertyInfo = registeredBinding.propertyInfo; + } + if (registeredBinding) { + registeredBinding.propertyInfo = null; + } + if (!propertyInfo) { + return; + } + var object = propertyInfo.object, propertyName = propertyInfo.propertyName; this.reset({ object: object, propertyName: propertyName }); }; ValidationController.inject = [Validator]; diff --git a/dist/native-modules/validation-errors-custom-attribute.js b/dist/native-modules/validation-errors-custom-attribute.js index 9e70fb9c..118320ac 100644 --- a/dist/native-modules/validation-errors-custom-attribute.js +++ b/dist/native-modules/validation-errors-custom-attribute.js @@ -19,7 +19,9 @@ export var ValidationErrorsCustomAttribute = (function () { if (a.targets[0] === b.targets[0]) { return 0; } + /* tslint:disable:no-bitwise */ return a.targets[0].compareDocumentPosition(b.targets[0]) & 2 ? 1 : -1; + /* tslint:enable:no-bitwise */ }); }; ValidationErrorsCustomAttribute.prototype.interestingElements = function (elements) { diff --git a/dist/native-modules/validator.d.ts b/dist/native-modules/validator.d.ts index 104dc3a1..edaa3f40 100644 --- a/dist/native-modules/validator.d.ts +++ b/dist/native-modules/validator.d.ts @@ -19,4 +19,10 @@ export declare abstract class Validator { * specified object. This may not be possible for all implementations of this interface. */ abstract validateObject(object: any, rules?: any): Promise; + /** + * Determines whether a rule exists in a set of rules. + * @param rules The rules to search. + * @parem rule The rule to find. + */ + abstract ruleExists(rules: any, rule: any): boolean; } diff --git a/dist/system/implementation/rules.d.ts b/dist/system/implementation/rules.d.ts index 71c46daf..30947f6f 100644 --- a/dist/system/implementation/rules.d.ts +++ b/dist/system/implementation/rules.d.ts @@ -6,7 +6,7 @@ export declare class Rules { /** * The name of the property that stores the rules. */ - static key: string; + private static key; /** * Applies the rules to a target. */ diff --git a/dist/system/implementation/standard-validator.d.ts b/dist/system/implementation/standard-validator.d.ts index e05e7b8e..ad544cbb 100644 --- a/dist/system/implementation/standard-validator.d.ts +++ b/dist/system/implementation/standard-validator.d.ts @@ -1,20 +1,18 @@ import { ViewResources } from 'aurelia-templating'; import { Validator } from '../validator'; import { ValidationError } from '../validation-error'; +import { Rule } from './rule'; import { ValidationMessageProvider } from './validation-messages'; /** * Validates. * 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; constructor(messageProvider: ValidationMessageProvider, resources: ViewResources); - private getMessage(rule, object, value); - private validateRuleSequence(object, propertyName, ruleSequence, sequence); - private validate(object, propertyName, rules); /** * Validates the specified property. * @param object The object to validate. @@ -30,4 +28,13 @@ export declare class StandardValidator extends Validator { * for the object created by ValidationRules....on(class/object) */ validateObject(object: any, rules?: any): Promise; + /** + * Determines whether a rule exists in a set of rules. + * @param rules The rules to search. + * @parem rule The rule to find. + */ + ruleExists(rules: Rule[][], rule: Rule): boolean; + private getMessage(rule, object, value); + private validateRuleSequence(object, propertyName, ruleSequence, sequence); + private validate(object, propertyName, rules); } diff --git a/dist/system/implementation/standard-validator.js b/dist/system/implementation/standard-validator.js index a7d85a9d..b017b73c 100644 --- a/dist/system/implementation/standard-validator.js +++ b/dist/system/implementation/standard-validator.js @@ -38,6 +38,39 @@ System.register(['aurelia-templating', '../validator', '../validation-error', '. this.lookupFunctions = resources.lookupFunctions; this.getDisplayName = messageProvider.getDisplayName.bind(messageProvider); } + /** + * 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 + * for the object created by ValidationRules....on(class/object) + */ + StandardValidator.prototype.validateProperty = function (object, propertyName, rules) { + return this.validate(object, propertyName, rules || null); + }; + /** + * 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 + * for the object created by ValidationRules....on(class/object) + */ + StandardValidator.prototype.validateObject = function (object, rules) { + return this.validate(object, null, rules || null); + }; + /** + * Determines whether a rule exists in a set of rules. + * @param rules The rules to search. + * @parem rule The rule to find. + */ + StandardValidator.prototype.ruleExists = function (rules, rule) { + var i = rules.length; + while (i--) { + if (rules[i].indexOf(rule) !== -1) { + return true; + } + } + return false; + }; StandardValidator.prototype.getMessage = function (rule, object, value) { var expression = rule.message || this.messageProvider.getMessage(rule.messageKey); var _a = rule.property, propertyName = _a.name, displayName = _a.displayName; @@ -74,6 +107,14 @@ 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 parent containing the field. + //The field is the last part of the propert path + //e.g. finalProp in object.sub1.sub2.finalProp + 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); @@ -109,25 +150,6 @@ System.register(['aurelia-templating', '../validator', '../validation-error', '. } return this.validateRuleSequence(object, propertyName, rules, 0); }; - /** - * 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 - * for the object created by ValidationRules....on(class/object) - */ - StandardValidator.prototype.validateProperty = function (object, propertyName, rules) { - return this.validate(object, propertyName, rules || null); - }; - /** - * 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 - * for the object created by ValidationRules....on(class/object) - */ - StandardValidator.prototype.validateObject = function (object, rules) { - return this.validate(object, null, rules || null); - }; StandardValidator.inject = [validation_messages_1.ValidationMessageProvider, aurelia_templating_1.ViewResources]; return StandardValidator; }(validator_1.Validator)); diff --git a/dist/system/implementation/validation-parser.d.ts b/dist/system/implementation/validation-parser.d.ts index 43f09ca5..75b2c7af 100644 --- a/dist/system/implementation/validation-parser.d.ts +++ b/dist/system/implementation/validation-parser.d.ts @@ -13,10 +13,10 @@ export declare class ValidationParser { private undefinedExpression; private cache; constructor(parser: Parser, bindinqLanguage: BindingLanguage); - private coalesce(part); parseMessage(message: string): Expression; - private getAccessorExpression(fn); parseProperty(property: string | PropertyAccessor): RuleProperty; + private coalesce(part); + private getAccessorExpression(fn); } export declare class MessageExpressionValidator extends Unparser { private originalMessage; diff --git a/dist/system/implementation/validation-parser.js b/dist/system/implementation/validation-parser.js index 95033a48..0c6f0573 100644 --- a/dist/system/implementation/validation-parser.js +++ b/dist/system/implementation/validation-parser.js @@ -32,10 +32,6 @@ System.register(['aurelia-binding', 'aurelia-templating', './util', 'aurelia-log this.undefinedExpression = new aurelia_binding_1.LiteralPrimitive(undefined); this.cache = {}; } - ValidationParser.prototype.coalesce = function (part) { - // part === null || part === undefined ? '' : part - return new aurelia_binding_1.Conditional(new aurelia_binding_1.Binary('||', new aurelia_binding_1.Binary('===', part, this.nullExpression), new aurelia_binding_1.Binary('===', part, this.undefinedExpression)), this.emptyStringExpression, new aurelia_binding_1.CallMember(part, 'toString', [])); - }; ValidationParser.prototype.parseMessage = function (message) { if (this.cache[message] !== undefined) { return this.cache[message]; @@ -52,29 +48,42 @@ System.register(['aurelia-binding', 'aurelia-templating', './util', 'aurelia-log this.cache[message] = expression; 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 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]); - }; 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 }; } throw new Error("Invalid subject: \"" + accessor + "\""); }; + ValidationParser.prototype.coalesce = function (part) { + // part === null || part === undefined ? '' : part + return new aurelia_binding_1.Conditional(new aurelia_binding_1.Binary('||', new aurelia_binding_1.Binary('===', part, this.nullExpression), new aurelia_binding_1.Binary('===', part, this.undefinedExpression)), this.emptyStringExpression, new aurelia_binding_1.CallMember(part, 'toString', [])); + }; + ValidationParser.prototype.getAccessorExpression = function (fn) { + var classic = /^function\s*\([$_\w\d]+\)\s*\{\s*(?:"use strict";)?\s*return\s+[$_\w\d]+\.([$_\w\d]+(\.[$_\w\d]+)*)\s*;?\s*\}$/; + var arrow = /^\(?[$_\w\d]+\)?\s*=>\s*(?:\{?.*return\s+)?[$_\w\d]+\.([$_\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]); + }; ValidationParser.inject = [aurelia_binding_1.Parser, aurelia_templating_1.BindingLanguage]; return ValidationParser; }()); diff --git a/dist/system/implementation/validation-rules.d.ts b/dist/system/implementation/validation-rules.d.ts index 1bd93972..59ba7410 100644 --- a/dist/system/implementation/validation-rules.d.ts +++ b/dist/system/implementation/validation-rules.d.ts @@ -195,7 +195,8 @@ export declare class FluentEnsure { constructor(parser: ValidationParser); /** * Target a property with validation rules. - * @param property The property to target. Can be the property name or a property accessor function. + * @param property The property to target. Can be the property name or a property accessor + * function. */ ensure(property: string | PropertyAccessor): FluentRules; /** @@ -233,7 +234,8 @@ export declare class ValidationRules { * @param name The name of the custom rule. Also serves as the message key. * @param condition The rule function. * @param message The message expression - * @param argsToConfig A function that maps the rule's arguments to a "config" object that can be used when evaluating the message expression. + * @param argsToConfig A function that maps the rule's arguments to a "config" + * object that can be used when evaluating the message expression. */ static customRule(name: string, condition: (value: any, object?: any, ...args: any[]) => boolean | Promise, message: string, argsToConfig?: (...args: any[]) => any): void; /** diff --git a/dist/system/implementation/validation-rules.js b/dist/system/implementation/validation-rules.js index 0e06eab0..6984b449 100644 --- a/dist/system/implementation/validation-rules.js +++ b/dist/system/implementation/validation-rules.js @@ -265,7 +265,9 @@ System.register(['./util', './rules', './validation-messages'], function(exports * null, undefined and empty-string values are considered valid. */ FluentRules.prototype.email = function () { - return this.matches(/^((([a-z]|\d|[!#\$%&'\*\+\-\/=\?\^_`{\|}~]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])+(\.([a-z]|\d|[!#\$%&'\*\+\-\/=\?\^_`{\|}~]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])+)*)|((\x22)((((\x20|\x09)*(\x0d\x0a))?(\x20|\x09)+)?(([\x01-\x08\x0b\x0c\x0e-\x1f\x7f]|\x21|[\x23-\x5b]|[\x5d-\x7e]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(\\([\x01-\x09\x0b\x0c\x0d-\x7f]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF]))))*(((\x20|\x09)*(\x0d\x0a))?(\x20|\x09)+)?(\x22)))@((([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])*([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])))\.)+(([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])*([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])))$/i) + // regex from https://html.spec.whatwg.org/multipage/forms.html#valid-e-mail-address + /* tslint:disable:max-line-length */ + return this.matches(/^[a-zA-Z0-9.!#$%&'*+\/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$/) .withMessageKey('email'); }; /** @@ -316,17 +318,20 @@ System.register(['./util', './rules', './validation-messages'], function(exports * Part of the fluent rule API. Enables targeting properties and objects with rules. */ FluentEnsure = (function () { + /* tslint:enable */ function FluentEnsure(parser) { this.parser = parser; /** * Rules that have been defined using the fluent API. */ this.rules = []; + /* tslint:disable */ this._sequence = 0; } /** * Target a property with validation rules. - * @param property The property to target. Can be the property name or a property accessor function. + * @param property The property to target. Can be the property name or a property accessor + * function. */ FluentEnsure.prototype.ensure = function (property) { this.assertInitialized(); @@ -392,7 +397,8 @@ System.register(['./util', './rules', './validation-messages'], function(exports * @param name The name of the custom rule. Also serves as the message key. * @param condition The rule function. * @param message The message expression - * @param argsToConfig A function that maps the rule's arguments to a "config" object that can be used when evaluating the message expression. + * @param argsToConfig A function that maps the rule's arguments to a "config" + * object that can be used when evaluating the message expression. */ ValidationRules.customRule = function (name, condition, message, argsToConfig) { validation_messages_1.validationMessages[name] = message; diff --git a/dist/system/property-info.d.ts b/dist/system/property-info.d.ts index ea68d9a0..59b760bf 100644 --- a/dist/system/property-info.d.ts +++ b/dist/system/property-info.d.ts @@ -5,6 +5,6 @@ import { Expression } from 'aurelia-binding'; * @param source The scope */ export declare function getPropertyInfo(expression: Expression, source: any): { - object: any; + object: Object; propertyName: string; -}; +} | null; diff --git a/dist/system/property-info.js b/dist/system/property-info.js index 6aa9b2b3..906ec2ba 100644 --- a/dist/system/property-info.js +++ b/dist/system/property-info.js @@ -4,16 +4,12 @@ System.register(['aurelia-binding'], function(exports_1, context_1) { var aurelia_binding_1; function getObject(expression, objectExpression, source) { var value = objectExpression.evaluate(source, null); - if (value !== null && (typeof value === 'object' || typeof value === 'function')) { + if (value === null || value === undefined || value instanceof Object) { return value; } - if (value === null) { - value = 'null'; - } - else if (value === undefined) { - value = 'undefined'; - } - throw new Error("The '" + objectExpression + "' part of '" + expression + "' evaluates to " + value + " instead of an object."); + /* tslint:disable */ + throw new Error("The '" + objectExpression + "' part of '" + expression + "' evaluates to " + value + " instead of an object, null or undefined."); + /* tslint:enable */ } /** * Retrieves the object and property name for the specified expression. @@ -27,6 +23,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 +31,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,6 +48,9 @@ System.register(['aurelia-binding'], function(exports_1, context_1) { else { throw new Error("Expression '" + originalExpression + "' is not compatible with the validate binding-behavior."); } + if (object === null || object === undefined) { + return null; + } return { object: object, propertyName: propertyName }; } exports_1("getPropertyInfo", getPropertyInfo); diff --git a/dist/system/validate-binding-behavior-base.d.ts b/dist/system/validate-binding-behavior-base.d.ts index cb4eb912..09c18430 100644 --- a/dist/system/validate-binding-behavior-base.d.ts +++ b/dist/system/validate-binding-behavior-base.d.ts @@ -8,11 +8,11 @@ export declare abstract class ValidateBindingBehaviorBase { constructor(taskQueue: TaskQueue); protected abstract getValidateTrigger(controller: ValidationController): number; /** - * Gets the DOM element associated with the data-binding. Most of the time it's - * the binding.target but sometimes binding.target is an aurelia custom element, - * or custom attribute which is a javascript "class" instance, so we need to use - * the controller's container to retrieve the actual DOM element. - */ + * Gets the DOM element associated with the data-binding. Most of the time it's + * the binding.target but sometimes binding.target is an aurelia custom element, + * or custom attribute which is a javascript "class" instance, so we need to use + * the controller's container to retrieve the actual DOM element. + */ getTarget(binding: any, view: any): any; bind(binding: any, source: any, rulesOrController?: ValidationController | any, rules?: any): void; unbind(binding: any): void; diff --git a/dist/system/validate-binding-behavior-base.js b/dist/system/validate-binding-behavior-base.js index d05523de..441f776d 100644 --- a/dist/system/validate-binding-behavior-base.js +++ b/dist/system/validate-binding-behavior-base.js @@ -26,11 +26,11 @@ System.register(['aurelia-dependency-injection', 'aurelia-pal', './validation-co this.taskQueue = taskQueue; } /** - * Gets the DOM element associated with the data-binding. Most of the time it's - * the binding.target but sometimes binding.target is an aurelia custom element, - * or custom attribute which is a javascript "class" instance, so we need to use - * the controller's container to retrieve the actual DOM element. - */ + * Gets the DOM element associated with the data-binding. Most of the time it's + * the binding.target but sometimes binding.target is an aurelia custom element, + * or custom attribute which is a javascript "class" instance, so we need to use + * the controller's container to retrieve the actual DOM element. + */ ValidateBindingBehaviorBase.prototype.getTarget = function (binding, view) { var target = binding.target; // DOM element @@ -69,14 +69,20 @@ System.register(['aurelia-dependency-injection', 'aurelia-pal', './validation-co controller.registerBinding(binding, target, rules); binding.validationController = controller; var trigger = this.getValidateTrigger(controller); + /* tslint:disable:no-bitwise */ if (trigger & validate_trigger_1.validateTrigger.change) { + /* tslint:enable:no-bitwise */ binding.standardUpdateSource = binding.updateSource; + /* tslint:disable:only-arrow-functions */ binding.updateSource = function (value) { + /* tslint:enable:only-arrow-functions */ this.standardUpdateSource(value); this.validationController.validateBinding(this); }; } + /* tslint:disable:no-bitwise */ if (trigger & validate_trigger_1.validateTrigger.blur) { + /* tslint:enable:no-bitwise */ binding.validateBlurHandler = function () { _this.taskQueue.queueMicroTask(function () { return controller.validateBinding(binding); }); }; @@ -85,7 +91,9 @@ System.register(['aurelia-dependency-injection', 'aurelia-pal', './validation-co } if (trigger !== validate_trigger_1.validateTrigger.manual) { binding.standardUpdateTarget = binding.updateTarget; + /* tslint:disable:only-arrow-functions */ binding.updateTarget = function (value) { + /* tslint:enable:only-arrow-functions */ this.standardUpdateTarget(value); this.validationController.resetBinding(this); }; diff --git a/dist/system/validate-trigger.d.ts b/dist/system/validate-trigger.d.ts index e001bfa3..82c60096 100644 --- a/dist/system/validate-trigger.d.ts +++ b/dist/system/validate-trigger.d.ts @@ -1,6 +1,6 @@ /** -* Validation triggers. -*/ + * Validation triggers. + */ export declare const validateTrigger: { manual: number; blur: number; diff --git a/dist/system/validate-trigger.js b/dist/system/validate-trigger.js index f3598e2c..398898da 100644 --- a/dist/system/validate-trigger.js +++ b/dist/system/validate-trigger.js @@ -6,21 +6,21 @@ System.register([], function(exports_1, context_1) { setters:[], execute: function() { /** - * Validation triggers. - */ + * Validation triggers. + */ exports_1("validateTrigger", validateTrigger = { /** - * Manual validation. Use the controller's `validate()` and `reset()` methods - * to validate all bindings. - */ + * Manual validation. Use the controller's `validate()` and `reset()` methods + * to validate all bindings. + */ manual: 0, /** - * Validate the binding when the binding's target element fires a DOM "blur" event. - */ + * Validate the binding when the binding's target element fires a DOM "blur" event. + */ blur: 1, /** - * Validate the binding when it updates the model due to a change in the view. - */ + * Validate the binding when it updates the model due to a change in the view. + */ change: 2, /** * Validate the binding when the binding's target element fires a DOM "blur" event and diff --git a/dist/system/validation-controller.d.ts b/dist/system/validation-controller.d.ts index dcc1c174..a65621f3 100644 --- a/dist/system/validation-controller.d.ts +++ b/dist/system/validation-controller.d.ts @@ -90,7 +90,8 @@ export declare class ValidationController { private getInstructionPredicate(instruction?); /** * Validates and renders errors. - * @param instruction Optional. Instructions on what to validate. If undefined, all objects and bindings will be validated. + * @param instruction Optional. Instructions on what to validate. If undefined, all + * objects and bindings will be validated. */ validate(instruction?: ValidateInstruction): Promise; /** @@ -104,11 +105,11 @@ export declare class ValidationController { private getAssociatedElements({object, propertyName}); private processErrorDelta(kind, oldErrors, newErrors); /** - * Validates the property associated with a binding. - */ + * Validates the property associated with a binding. + */ validateBinding(binding: Binding): void; /** - * Resets the errors for a property associated with a binding. - */ + * Resets the errors for a property associated with a binding. + */ resetBinding(binding: Binding): void; } diff --git a/dist/system/validation-controller.js b/dist/system/validation-controller.js index 7d5c618d..de8cd8ed 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() { /** @@ -114,7 +117,7 @@ System.register(['./validator', './validate-trigger', './property-info', './vali * @param rules (optional) rules associated with the binding. Validator implementation specific. */ ValidationController.prototype.registerBinding = function (binding, target, rules) { - this.bindings.set(binding, { target: target, rules: rules }); + this.bindings.set(binding, { target: target, rules: rules, propertyInfo: null }); }; /** * Unregisters a binding with the controller. @@ -129,8 +132,9 @@ System.register(['./validator', './validate-trigger', './property-info', './vali * relevant errors in the list of rendered errors. */ ValidationController.prototype.getInstructionPredicate = function (instruction) { + var _this = this; 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; }; @@ -138,9 +142,8 @@ System.register(['./validator', './validate-trigger', './property-info', './vali else { 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) { + return function (x) { return predicate_1(x) && _this.validator.ruleExists(rules_2, x.rule); }; } return predicate_1; } @@ -150,24 +153,43 @@ System.register(['./validator', './validate-trigger', './property-info', './vali }; /** * Validates and renders errors. - * @param instruction Optional. Instructions on what to validate. If undefined, all objects and bindings will be validated. + * @param instruction Optional. Instructions on what to validate. If undefined, all + * objects and bindings will be validated. */ ValidationController.prototype.validate = function (instruction) { var _this = this; // 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) { + for (var _i = 0, _a = Array.from(this.bindings); _i < _a.length; _i++) { + var _b = _a[_i], binding = _b[0], rulesObj = _b[1].rulesObj; + var propertyInfo = property_info_1.getPropertyInfo(binding.sourceExpression, binding.source); + if (propertyInfo.propertyName != propertyName_2 || !propertyInfo || this.objects.has(propertyInfo.object)) { + continue; + } + if (propertyInfo.propertyName.indexOf(".") !== -1) { + var parentProp = ""; + var ittr = binding.sourceExpression.expression; + while (ittr.object) { + ittr = ittr.object; + parentProp = ittr.name; + } + rules_3 = rules_1.Rules.get(binding._observer0._callable0._observer0.obj[parentProp]); + } + } + } // 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 { @@ -179,12 +201,26 @@ System.register(['./validator', './validate-trigger', './property-info', './vali promises.push(_this.validator.validateObject(object, rules)); } for (var _c = 0, _d = Array.from(_this.bindings); _c < _d.length; _c++) { - var _e = _d[_c], binding = _e[0], rules = _e[1].rules; - var _f = property_info_1.getPropertyInfo(binding.sourceExpression, binding.source), object = _f.object, propertyName = _f.propertyName; - if (_this.objects.has(object)) { + var _e = _d[_c], binding = _e[0], rulesObj = _e[1].rulesObj; + var propertyInfo = property_info_1.getPropertyInfo(binding.sourceExpression, binding.source); + if (!propertyInfo || _this.objects.has(propertyInfo.object)) { continue; } - promises.push(_this.validator.validateProperty(object, propertyName, rules)); + var propName = propertyInfo.propertyName; + if (propertyInfo.propertyName.indexOf(".") !== -1) { + var parentProp = ""; + var ittr = binding.sourceExpression.expression; + while (ittr.object) { + ittr = ittr.object; + parentProp = ittr.name; + } + propName = propertyInfo.propertyName.substr(propertyInfo.propertyName.lastIndexOf('.') + 1); + rules = rules_1.Rules.get(binding._observer0._callable0._observer0.obj[parentProp]); + } + else { + rules = rulesObj; + } + promises.push(_this.validator.validateProperty(propertyInfo.object, propertyInfo.propertyName, rules)); } return Promise.all(promises).then(function (errorSets) { return errorSets.reduce(function (a, b) { return a.concat(b); }, []); }); }; @@ -228,8 +264,8 @@ System.register(['./validator', './validate-trigger', './property-info', './vali var elements = []; for (var _i = 0, _b = Array.from(this.bindings); _i < _b.length; _i++) { var _c = _b[_i], binding = _c[0], target = _c[1].target; - var _d = property_info_1.getPropertyInfo(binding.sourceExpression, binding.source), o = _d.object, p = _d.propertyName; - if (o === object && p === propertyName) { + var propertyInfo = property_info_1.getPropertyInfo(binding.sourceExpression, binding.source); + if (propertyInfo && propertyInfo.object === object && propertyInfo.propertyName === propertyName) { elements.push(target); } } @@ -259,7 +295,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); @@ -291,22 +327,41 @@ System.register(['./validator', './validate-trigger', './property-info', './vali } }; /** - * Validates the property associated with a binding. - */ + * Validates the property associated with a binding. + */ ValidationController.prototype.validateBinding = function (binding) { if (!binding.isBound) { return; } - var _a = property_info_1.getPropertyInfo(binding.sourceExpression, binding.source), object = _a.object, propertyName = _a.propertyName; + var propertyInfo = property_info_1.getPropertyInfo(binding.sourceExpression, binding.source); + var rules = undefined; var registeredBinding = this.bindings.get(binding); - var rules = registeredBinding ? registeredBinding.rules : undefined; + if (registeredBinding) { + rules = registeredBinding.rules; + registeredBinding.propertyInfo = propertyInfo; + } + if (!propertyInfo) { + return; + } + var object = propertyInfo.object, propertyName = propertyInfo.propertyName; this.validate({ object: object, propertyName: propertyName, rules: rules }); }; /** - * Resets the errors for a property associated with a binding. - */ + * Resets the errors for a property associated with a binding. + */ ValidationController.prototype.resetBinding = function (binding) { - var _a = property_info_1.getPropertyInfo(binding.sourceExpression, binding.source), object = _a.object, propertyName = _a.propertyName; + var registeredBinding = this.bindings.get(binding); + var propertyInfo = property_info_1.getPropertyInfo(binding.sourceExpression, binding.source); + if (!propertyInfo && registeredBinding) { + propertyInfo = registeredBinding.propertyInfo; + } + if (registeredBinding) { + registeredBinding.propertyInfo = null; + } + if (!propertyInfo) { + return; + } + var object = propertyInfo.object, propertyName = propertyInfo.propertyName; this.reset({ object: object, propertyName: propertyName }); }; ValidationController.inject = [validator_1.Validator]; diff --git a/dist/system/validation-errors-custom-attribute.js b/dist/system/validation-errors-custom-attribute.js index be9d6dfb..c91e2de2 100644 --- a/dist/system/validation-errors-custom-attribute.js +++ b/dist/system/validation-errors-custom-attribute.js @@ -35,7 +35,9 @@ System.register(['aurelia-binding', 'aurelia-dependency-injection', 'aurelia-tem if (a.targets[0] === b.targets[0]) { return 0; } + /* tslint:disable:no-bitwise */ return a.targets[0].compareDocumentPosition(b.targets[0]) & 2 ? 1 : -1; + /* tslint:enable:no-bitwise */ }); }; ValidationErrorsCustomAttribute.prototype.interestingElements = function (elements) { diff --git a/dist/system/validator.d.ts b/dist/system/validator.d.ts index 104dc3a1..edaa3f40 100644 --- a/dist/system/validator.d.ts +++ b/dist/system/validator.d.ts @@ -19,4 +19,10 @@ export declare abstract class Validator { * specified object. This may not be possible for all implementations of this interface. */ abstract validateObject(object: any, rules?: any): Promise; + /** + * Determines whether a rule exists in a set of rules. + * @param rules The rules to search. + * @parem rule The rule to find. + */ + abstract ruleExists(rules: any, rule: any): boolean; } diff --git a/src/implementation/standard-validator.ts b/src/implementation/standard-validator.ts index 18906848..520925aa 100644 --- a/src/implementation/standard-validator.ts +++ b/src/implementation/standard-validator.ts @@ -28,7 +28,7 @@ export class StandardValidator extends Validator { * Validates the specified property. * @param object The object to validate. * @param propertyName The name of the property to validate. - * @param rules Optional. If unspecified, the rules will be looked up using the metadata + * @param rules Optional. If unspecified, the rules will be looked up using the metadata * for the object created by ValidationRules....on(class/object) */ public validateProperty(object: any, propertyName: string, rules?: any): Promise { @@ -38,7 +38,7 @@ export class StandardValidator extends Validator { /** * Validates all rules for specified object and it's properties. * @param object The object to validate. - * @param rules Optional. If unspecified, the rules will be looked up using the metadata + * @param rules Optional. If unspecified, the rules will be looked up using the metadata * for the object created by ValidationRules....on(class/object) */ public validateObject(object: any, rules?: any): Promise { @@ -105,7 +105,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 parent containing the field. + //The field is the last part of the propert path + //e.g. finalProp in object.sub1.sub2.finalProp + let parts =rule.property.name.split('.'); + value= object[parts[parts.length-1]]; + } 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 a24b04fe..f8f5154a 100644 --- a/src/implementation/validation-parser.ts +++ b/src/implementation/validation-parser.ts @@ -64,10 +64,21 @@ export class ValidationParser { 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 }; } @@ -88,8 +99,8 @@ export class ValidationParser { } private getAccessorExpression(fn: string): Expression { - const classic = /^function\s*\([$_\w\d]+\)\s*\{\s*(?:"use strict";)?\s*return\s+[$_\w\d]+\.([$_\w\d]+)\s*;?\s*\}$/; - const arrow = /^[$_\w\d]+\s*=>\s*[$_\w\d]+\.([$_\w\d]+)$/; + const classic = /^function\s*\([$_\w\d]+\)\s*\{\s*(?:"use strict";)?\s*return\s+[$_\w\d]+\.([$_\w\d]+(\.[$_\w\d]+)*)\s*;?\s*\}$/; + const arrow = /^\(?[$_\w\d]+\)?\s*=>\s*(?:\{?.*return\s+)?[$_\w\d]+\.([$_\w\d]+(\.[$_\w\d]+)*);?\s*\}?$/; const match = classic.exec(fn) || arrow.exec(fn); if (match === null) { throw new Error(`Unable to parse accessor function:\n${fn}`); diff --git a/src/property-info.ts b/src/property-info.ts index 15fb07bc..002dc951 100644 --- a/src/property-info.ts +++ b/src/property-info.ts @@ -30,12 +30,24 @@ export function getPropertyInfo(expression: Expression, source: any): { object: let object: null | undefined | Object; let propertyName: string; + let ruleSrc = null; if (expression instanceof AccessScope) { object = source.bindingContext; propertyName = expression.name; } else if (expression instanceof AccessMember) { object = getObject(originalExpression, expression.object, source); propertyName = expression.name; + if (expression.object) + { + //build the path to the property from the object root. + let exp:any = expression.object; + while(exp.object) + { + propertyName = exp.name +'.' + propertyName; + exp = exp.object; + } + ruleSrc= getObject(originalExpression, exp, source); + } } else if (expression instanceof AccessKeyed) { object = getObject(originalExpression, expression.object, source); propertyName = expression.key.evaluate(source); diff --git a/src/validation-controller.ts b/src/validation-controller.ts index faf1eb5a..832d067a 100644 --- a/src/validation-controller.ts +++ b/src/validation-controller.ts @@ -4,6 +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. @@ -190,7 +191,7 @@ export class ValidationController { /** * Validates and renders errors. - * @param instruction Optional. Instructions on what to validate. If undefined, all + * @param instruction Optional. Instructions on what to validate. If undefined, all * objects and bindings will be validated. */ public validate(instruction?: ValidateInstruction): Promise { @@ -200,6 +201,27 @@ export class ValidationController { let { object, propertyName, rules } = instruction; // if rules were not specified, check the object map. rules = rules || this.objects.get(object); + if (!rules) + { + for (let [binding, { rulesObj }] of Array.from(this.bindings)) + { + const propertyInfo = getPropertyInfo(binding.sourceExpression, (binding).source); + if (propertyInfo.propertyName!= propertyName || !propertyInfo || this.objects.has(propertyInfo.object)) { + continue; + } + if (propertyInfo.propertyName.indexOf(".") !== -1) + { + let parentProp =""; + let ittr:any= (binding).sourceExpression.expression; + while(ittr.object) + { + ittr = ittr.object; + parentProp = ittr.name; + } + rules = Rules.get((binding)._observer0._callable0._observer0.obj[parentProp]); + } + } + } // property specified? if (instruction.propertyName === undefined) { // validate the specified object. @@ -215,11 +237,27 @@ export class ValidationController { for (let [object, rules] of Array.from(this.objects)) { promises.push(this.validator.validateObject(object, rules)); } - for (let [binding, { rules }] of Array.from(this.bindings)) { + for (let [binding, { rulesObj }] of Array.from(this.bindings)) { const propertyInfo = getPropertyInfo(binding.sourceExpression, (binding).source); if (!propertyInfo || this.objects.has(propertyInfo.object)) { continue; } + const propName = propertyInfo.propertyName; + if (propertyInfo.propertyName.indexOf(".") !== -1) + { + let parentProp =""; + let ittr:any= (binding).sourceExpression.expression; + while(ittr.object) + { + ittr = ittr.object; + parentProp = ittr.name; + } + propName = propertyInfo.propertyName.substr(propertyInfo.propertyName.lastIndexOf('.')+1); + rules = Rules.get((binding)._observer0._callable0._observer0.obj[parentProp]); + } + else{ + rules = rulesObj; + } promises.push(this.validator.validateProperty(propertyInfo.object, propertyInfo.propertyName, rules)); } return Promise.all(promises).then(errorSets => errorSets.reduce((a, b) => a.concat(b), [])); @@ -305,7 +343,7 @@ 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. diff --git a/test/basic.ts b/test/basic.ts index 4849e5fd..14e4484c 100644 --- a/test/basic.ts +++ b/test/basic.ts @@ -13,6 +13,7 @@ describe('end to end', () => { component.bootstrap(configure); let firstName: HTMLInputElement; + let subprop: HTMLInputElement; let lastName: HTMLInputElement; let number1: HTMLInputElement; let number2: HTMLInputElement; @@ -30,6 +31,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'); @@ -55,6 +57,15 @@ describe('end to end', () => { const renderInstruction = calls.argsFor(calls.count() - 1)[0]; expect(renderInstruction.render[0].elements[0]).toBe(lastName); }) + // blur the subprop- this should trigger validation. + .then(() => blur(subprop)) + // confirm there's an error. + .then(() => {console.log (viewModel.controller.errors); return true;}) + .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 f6c5d967..bb37ae97 100644 --- a/test/resources/registration-form.ts +++ b/test/resources/registration-form.ts @@ -11,6 +11,7 @@ import {
+ @@ -23,6 +24,9 @@ export class RegistrationForm { public firstName = ''; public lastName = ''; public email = ''; + public settings = { + subprop : "" + }; public number1 = 0; public number2 = 0; public password = ''; @@ -52,6 +56,7 @@ ValidationRules.customRule( ValidationRules .ensure((f: RegistrationForm) => f.firstName).required() .ensure(f => f.lastName).required() + .ensure(f => f.settings.subprop).required() .ensure('email').required().email() .ensure(f => f.number1).satisfies(value => value > 0) .ensure(f => f.number2).satisfies(value => value > 0).withMessage('${displayName} gots to be greater than zero.')