diff --git a/bower.json b/bower.json index a251a9d2..9cda7ba7 100644 --- a/bower.json +++ b/bower.json @@ -1,6 +1,6 @@ { "name": "aurelia-validation", - "version": "0.12.5", + "version": "0.13.0", "description": "Validation for Aurelia applications", "keywords": [ "aurelia", diff --git a/dist/amd/implementation/rule.d.ts b/dist/amd/implementation/rule.d.ts index 2eb47660..8bba9d76 100644 --- a/dist/amd/implementation/rule.d.ts +++ b/dist/amd/implementation/rule.d.ts @@ -1,4 +1,7 @@ import { Expression } from 'aurelia-binding'; +/** + * Information related to a property that is the subject of validation. + */ export interface RuleProperty { /** * The property name. null indicates the rule targets the object itself. @@ -9,6 +12,9 @@ export interface RuleProperty { */ displayName: string | null; } +/** + * A rule definition. Associations a rule with a property or object. + */ export interface Rule { property: RuleProperty; condition: (value: TValue, object?: TObject) => boolean | Promise; @@ -18,5 +24,6 @@ export interface Rule { } | null; messageKey: string; message: Expression | null; + sequence: number; tag?: string; } diff --git a/dist/amd/implementation/rules.d.ts b/dist/amd/implementation/rules.d.ts index 56bd6856..71c46daf 100644 --- a/dist/amd/implementation/rules.d.ts +++ b/dist/amd/implementation/rules.d.ts @@ -10,7 +10,7 @@ export declare class Rules { /** * Applies the rules to a target. */ - static set(target: any, rules: Rule[]): void; + static set(target: any, rules: Rule[][]): void; /** * Removes rules from a target. */ @@ -18,5 +18,5 @@ export declare class Rules { /** * Retrieves the target's rules. */ - static get(target: any): Rule[] | null; + static get(target: any): Rule[][] | null; } diff --git a/dist/amd/implementation/standard-validator.d.ts b/dist/amd/implementation/standard-validator.d.ts index d17f819b..e05e7b8e 100644 --- a/dist/amd/implementation/standard-validator.d.ts +++ b/dist/amd/implementation/standard-validator.d.ts @@ -13,6 +13,7 @@ export declare class StandardValidator extends Validator { 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. diff --git a/dist/amd/implementation/standard-validator.js b/dist/amd/implementation/standard-validator.js index dbb409f3..89686019 100644 --- a/dist/amd/implementation/standard-validator.js +++ b/dist/amd/implementation/standard-validator.js @@ -33,24 +33,12 @@ define(["require", "exports", 'aurelia-templating', '../validator', '../validati }; return expression.evaluate({ bindingContext: object, overrideContext: overrideContext }, this.lookupFunctions); }; - StandardValidator.prototype.validate = function (object, propertyName, rules) { + StandardValidator.prototype.validateRuleSequence = function (object, propertyName, ruleSequence, sequence) { var _this = this; - var errors = []; - // rules specified? - if (!rules) { - // no. locate the rules via metadata. - rules = rules_1.Rules.get(object); - } - // any rules? - if (!rules) { - return Promise.resolve(errors); - } // are we validating all properties or a single property? var validateAllProperties = propertyName === null || propertyName === undefined; - var addError = function (rule, value) { - var message = _this.getMessage(rule, object, value); - errors.push(new validation_error_1.ValidationError(rule, message, object, rule.property.name)); - }; + var rules = ruleSequence[sequence]; + var errors = []; // validate each rule. var promises = []; var _loop_1 = function(i) { @@ -66,25 +54,39 @@ define(["require", "exports", 'aurelia-templating', '../validator', '../validati // validate. var value = rule.property.name === null ? object : object[rule.property.name]; var promiseOrBoolean = rule.condition(value, object); - if (promiseOrBoolean instanceof Promise) { - promises.push(promiseOrBoolean.then(function (isValid) { - if (!isValid) { - addError(rule, value); - } - })); - return "continue"; - } - if (!promiseOrBoolean) { - addError(rule, value); + if (!(promiseOrBoolean instanceof Promise)) { + promiseOrBoolean = Promise.resolve(promiseOrBoolean); } + promises.push(promiseOrBoolean.then(function (isValid) { + if (!isValid) { + var message = _this.getMessage(rule, object, value); + errors.push(new validation_error_1.ValidationError(rule, message, object, rule.property.name)); + } + })); }; for (var i = 0; i < rules.length; i++) { _loop_1(i); } - if (promises.length === 0) { - return Promise.resolve(errors); + return Promise.all(promises) + .then(function () { + sequence++; + if (errors.length === 0 && sequence < ruleSequence.length) { + return _this.validateRuleSequence(object, propertyName, ruleSequence, sequence); + } + return errors; + }); + }; + StandardValidator.prototype.validate = function (object, propertyName, rules) { + // rules specified? + if (!rules) { + // no. attempt to locate the rules. + rules = rules_1.Rules.get(object); + } + // any rules? + if (!rules) { + return Promise.resolve([]); } - return Promise.all(promises).then(function () { return errors; }); + return this.validateRuleSequence(object, propertyName, rules, 0); }; /** * Validates the specified property. diff --git a/dist/amd/implementation/validation-parser.js b/dist/amd/implementation/validation-parser.js index 82fc7d03..6b112f11 100644 --- a/dist/amd/implementation/validation-parser.js +++ b/dist/amd/implementation/validation-parser.js @@ -44,13 +44,10 @@ define(["require", "exports", 'aurelia-binding', 'aurelia-templating', './util', return this.parser.parse(match[1]); }; ValidationParser.prototype.parseProperty = function (property) { - var accessor; if (util_1.isString(property)) { - accessor = this.parser.parse(property); - } - else { - accessor = this.getAccessorExpression(property.toString()); + 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) { return { diff --git a/dist/amd/implementation/validation-rules.d.ts b/dist/amd/implementation/validation-rules.d.ts index 997065f7..1bd93972 100644 --- a/dist/amd/implementation/validation-rules.d.ts +++ b/dist/amd/implementation/validation-rules.d.ts @@ -9,6 +9,12 @@ export declare class FluentRuleCustomizer { private parser; private rule; constructor(property: RuleProperty, condition: (value: TValue, object?: TObject) => boolean | Promise, config: Object, fluentEnsure: FluentEnsure, fluentRules: FluentRules, parser: ValidationParser); + /** + * Validate subsequent rules after previously declared rules have + * been validated successfully. Use to postpone validation of costly + * rules until less expensive rules pass validation. + */ + then(): this; /** * Specifies the key to use when looking up the rule's validation message. */ @@ -42,7 +48,7 @@ export declare class FluentRuleCustomizer { /** * Rules that have been defined using the fluent API. */ - readonly rules: Rule[]; + readonly rules: Rule[][]; /** * Applies the rules to a class or object, making them discoverable by the StandardValidator. * @param target A class or object. @@ -184,7 +190,8 @@ export declare class FluentEnsure { /** * Rules that have been defined using the fluent API. */ - rules: Rule[]; + rules: Rule[][]; + _sequence: number; constructor(parser: ValidationParser); /** * Target a property with validation rules. @@ -200,6 +207,10 @@ export declare class FluentEnsure { * @param target A class or object. */ on(target: any): this; + /** + * Adds a rule definition to the sequenced ruleset. + */ + _addRule(rule: Rule): void; private assertInitialized(); } /** @@ -230,7 +241,7 @@ export declare class ValidationRules { * @param rules The rules to search. * @param tag The tag to search for. */ - static taggedRules(rules: Rule[], tag: string): Rule[]; + static taggedRules(rules: Rule[][], tag: string): Rule[][]; /** * Removes the rules from a class or object. * @param target A class or object. diff --git a/dist/amd/implementation/validation-rules.js b/dist/amd/implementation/validation-rules.js index 50029752..8829a8b5 100644 --- a/dist/amd/implementation/validation-rules.js +++ b/dist/amd/implementation/validation-rules.js @@ -15,10 +15,20 @@ define(["require", "exports", './util', './rules', './validation-messages'], fun config: config, when: null, messageKey: 'default', - message: null + message: null, + sequence: fluentEnsure._sequence }; - this.fluentEnsure.rules.push(this.rule); + this.fluentEnsure._addRule(this.rule); } + /** + * Validate subsequent rules after previously declared rules have + * been validated successfully. Use to postpone validation of costly + * rules until less expensive rules pass validation. + */ + FluentRuleCustomizer.prototype.then = function () { + this.fluentEnsure._sequence++; + return this; + }; /** * Specifies the key to use when looking up the rule's validation message. */ @@ -240,7 +250,7 @@ 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])))$/) + 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) .withMessageKey('email'); }; /** @@ -297,6 +307,7 @@ define(["require", "exports", './util', './rules', './validation-messages'], fun * Rules that have been defined using the fluent API. */ this.rules = []; + this._sequence = 0; } /** * Target a property with validation rules. @@ -321,6 +332,15 @@ define(["require", "exports", './util', './rules', './validation-messages'], fun rules_1.Rules.set(target, this.rules); return this; }; + /** + * Adds a rule definition to the sequenced ruleset. + */ + FluentEnsure.prototype._addRule = function (rule) { + while (this.rules.length < rule.sequence + 1) { + this.rules.push([]); + } + this.rules[rule.sequence].push(rule); + }; FluentEnsure.prototype.assertInitialized = function () { if (this.parser) { return; @@ -369,7 +389,7 @@ define(["require", "exports", './util', './rules', './validation-messages'], fun * @param tag The tag to search for. */ ValidationRules.taggedRules = function (rules, tag) { - return rules.filter(function (r) { return r.tag === tag; }); + return rules.map(function (x) { return x.filter(function (r) { return r.tag === tag; }); }); }; /** * Removes the rules from a class or object. diff --git a/dist/amd/validate-binding-behavior-base.d.ts b/dist/amd/validate-binding-behavior-base.d.ts new file mode 100644 index 00000000..cb4eb912 --- /dev/null +++ b/dist/amd/validate-binding-behavior-base.d.ts @@ -0,0 +1,19 @@ +import { TaskQueue } from 'aurelia-task-queue'; +import { ValidationController } from './validation-controller'; +/** + * Binding behavior. Indicates the bound property should be validated. + */ +export declare abstract class ValidateBindingBehaviorBase { + private taskQueue; + 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. + */ + 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 new file mode 100644 index 00000000..b50129ba --- /dev/null +++ b/dist/amd/validate-binding-behavior-base.js @@ -0,0 +1,97 @@ +define(["require", "exports", 'aurelia-dependency-injection', 'aurelia-pal', './validation-controller', './validate-trigger'], function (require, exports, aurelia_dependency_injection_1, aurelia_pal_1, validation_controller_1, validate_trigger_1) { + "use strict"; + /** + * Binding behavior. Indicates the bound property should be validated. + */ + var ValidateBindingBehaviorBase = (function () { + function ValidateBindingBehaviorBase(taskQueue) { + 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. + */ + ValidateBindingBehaviorBase.prototype.getTarget = function (binding, view) { + var target = binding.target; + // DOM element + if (target instanceof Element) { + return target; + } + // custom element or custom attribute + for (var i = 0, ii = view.controllers.length; i < ii; i++) { + var controller = view.controllers[i]; + if (controller.viewModel === target) { + var element = controller.container.get(aurelia_pal_1.DOM.Element); + if (element) { + return element; + } + throw new Error("Unable to locate target element for \"" + binding.sourceExpression + "\"."); + } + } + throw new Error("Unable to locate target element for \"" + binding.sourceExpression + "\"."); + }; + ValidateBindingBehaviorBase.prototype.bind = function (binding, source, rulesOrController, rules) { + var _this = this; + // identify the target element. + var target = this.getTarget(binding, source); + // locate the controller. + var controller; + if (rulesOrController instanceof validation_controller_1.ValidationController) { + controller = rulesOrController; + } + else { + controller = source.container.get(aurelia_dependency_injection_1.Optional.of(validation_controller_1.ValidationController)); + rules = rulesOrController; + } + if (controller === null) { + throw new Error("A ValidationController has not been registered."); + } + controller.registerBinding(binding, target, rules); + binding.validationController = controller; + var trigger = this.getValidateTrigger(controller); + if (trigger & validate_trigger_1.validateTrigger.change) { + binding.standardUpdateSource = binding.updateSource; + binding.updateSource = function (value) { + this.standardUpdateSource(value); + this.validationController.validateBinding(this); + }; + } + if (trigger & validate_trigger_1.validateTrigger.blur) { + binding.validateBlurHandler = function () { + _this.taskQueue.queueMicroTask(function () { return controller.validateBinding(binding); }); + }; + binding.validateTarget = target; + target.addEventListener('blur', binding.validateBlurHandler); + } + if (trigger !== validate_trigger_1.validateTrigger.manual) { + binding.standardUpdateTarget = binding.updateTarget; + binding.updateTarget = function (value) { + this.standardUpdateTarget(value); + this.validationController.resetBinding(this); + }; + } + }; + ValidateBindingBehaviorBase.prototype.unbind = function (binding) { + // reset the binding to it's original state. + if (binding.standardUpdateSource) { + binding.updateSource = binding.standardUpdateSource; + binding.standardUpdateSource = null; + } + if (binding.standardUpdateTarget) { + binding.updateTarget = binding.standardUpdateTarget; + binding.standardUpdateTarget = null; + } + if (binding.validateBlurHandler) { + binding.validateTarget.removeEventListener('blur', binding.validateBlurHandler); + binding.validateBlurHandler = null; + binding.validateTarget = null; + } + binding.validationController.unregisterBinding(binding); + binding.validationController = null; + }; + return ValidateBindingBehaviorBase; + }()); + exports.ValidateBindingBehaviorBase = ValidateBindingBehaviorBase; +}); diff --git a/dist/amd/validate-binding-behavior.d.ts b/dist/amd/validate-binding-behavior.d.ts index a846db99..2d55c7ec 100644 --- a/dist/amd/validate-binding-behavior.d.ts +++ b/dist/amd/validate-binding-behavior.d.ts @@ -1,19 +1,47 @@ import { TaskQueue } from 'aurelia-task-queue'; import { ValidationController } from './validation-controller'; +import { ValidateBindingBehaviorBase } from './validate-binding-behavior-base'; /** - * Binding behavior. Indicates the bound property should be validated. + * Binding behavior. Indicates the bound property should be validated + * when the validate trigger specified by the associated controller's + * validateTrigger property occurs. */ -export declare class ValidateBindingBehavior { - private taskQueue; +export declare class ValidateBindingBehavior extends ValidateBindingBehaviorBase { static inject: typeof TaskQueue[]; - constructor(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. - */ - getTarget(binding: any, view: any): any; - bind(binding: any, source: any, rulesOrController?: ValidationController | any, rules?: any): void; - unbind(binding: any): void; + getValidateTrigger(controller: ValidationController): number; +} +/** + * Binding behavior. Indicates the bound property will be validated + * manually, by calling controller.validate(). No automatic validation + * triggered by data-entry or blur will occur. + */ +export declare class ValidateManuallyBindingBehavior extends ValidateBindingBehaviorBase { + static inject: typeof TaskQueue[]; + getValidateTrigger(): number; +} +/** + * Binding behavior. Indicates the bound property should be validated + * when the associated element blurs. + */ +export declare class ValidateOnBlurBindingBehavior extends ValidateBindingBehaviorBase { + static inject: typeof TaskQueue[]; + getValidateTrigger(): number; +} +/** + * Binding behavior. Indicates the bound property should be validated + * when the associated element is changed by the user, causing a change + * to the model. + */ +export declare class ValidateOnChangeBindingBehavior extends ValidateBindingBehaviorBase { + static inject: typeof TaskQueue[]; + getValidateTrigger(): number; +} +/** + * Binding behavior. Indicates the bound property should be validated + * when the associated element blurs or is changed by the user, causing + * a change to the model. + */ +export declare class ValidateOnChangeOrBlurBindingBehavior extends ValidateBindingBehaviorBase { + static inject: typeof TaskQueue[]; + getValidateTrigger(): number; } diff --git a/dist/amd/validate-binding-behavior.js b/dist/amd/validate-binding-behavior.js index 87335178..c5411a76 100644 --- a/dist/amd/validate-binding-behavior.js +++ b/dist/amd/validate-binding-behavior.js @@ -1,97 +1,92 @@ -define(["require", "exports", 'aurelia-dependency-injection', 'aurelia-pal', 'aurelia-task-queue', './validation-controller', './validate-trigger'], function (require, exports, aurelia_dependency_injection_1, aurelia_pal_1, aurelia_task_queue_1, validation_controller_1, validate_trigger_1) { +var __extends = (this && this.__extends) || function (d, b) { + for (var p in b) if (b.hasOwnProperty(p)) d[p] = b[p]; + function __() { this.constructor = d; } + d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __()); +}; +define(["require", "exports", 'aurelia-task-queue', './validate-trigger', './validate-binding-behavior-base'], function (require, exports, aurelia_task_queue_1, validate_trigger_1, validate_binding_behavior_base_1) { "use strict"; /** - * Binding behavior. Indicates the bound property should be validated. + * Binding behavior. Indicates the bound property should be validated + * when the validate trigger specified by the associated controller's + * validateTrigger property occurs. */ - var ValidateBindingBehavior = (function () { - function ValidateBindingBehavior(taskQueue) { - this.taskQueue = taskQueue; + var ValidateBindingBehavior = (function (_super) { + __extends(ValidateBindingBehavior, _super); + function ValidateBindingBehavior() { + _super.apply(this, arguments); } - /** - * 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. - */ - ValidateBindingBehavior.prototype.getTarget = function (binding, view) { - var target = binding.target; - // DOM element - if (target instanceof Element) { - return target; - } - // custom element or custom attribute - for (var i = 0, ii = view.controllers.length; i < ii; i++) { - var controller = view.controllers[i]; - if (controller.viewModel === target) { - var element = controller.container.get(aurelia_pal_1.DOM.Element); - if (element) { - return element; - } - throw new Error("Unable to locate target element for \"" + binding.sourceExpression + "\"."); - } - } - throw new Error("Unable to locate target element for \"" + binding.sourceExpression + "\"."); - }; - ValidateBindingBehavior.prototype.bind = function (binding, source, rulesOrController, rules) { - var _this = this; - // identify the target element. - var target = this.getTarget(binding, source); - // locate the controller. - var controller; - if (rulesOrController instanceof validation_controller_1.ValidationController) { - controller = rulesOrController; - } - else { - controller = source.container.get(aurelia_dependency_injection_1.Optional.of(validation_controller_1.ValidationController)); - rules = rulesOrController; - } - if (controller === null) { - throw new Error("A ValidationController has not been registered."); - } - controller.registerBinding(binding, target, rules); - binding.validationController = controller; - if (controller.validateTrigger === validate_trigger_1.validateTrigger.change) { - binding.standardUpdateSource = binding.updateSource; - binding.updateSource = function (value) { - this.standardUpdateSource(value); - this.validationController.validateBinding(this); - }; - } - else if (controller.validateTrigger === validate_trigger_1.validateTrigger.blur) { - binding.validateBlurHandler = function () { - _this.taskQueue.queueMicroTask(function () { return controller.validateBinding(binding); }); - }; - binding.validateTarget = target; - target.addEventListener('blur', binding.validateBlurHandler); - } - if (controller.validateTrigger !== validate_trigger_1.validateTrigger.manual) { - binding.standardUpdateTarget = binding.updateTarget; - binding.updateTarget = function (value) { - this.standardUpdateTarget(value); - this.validationController.resetBinding(this); - }; - } - }; - ValidateBindingBehavior.prototype.unbind = function (binding) { - // reset the binding to it's original state. - if (binding.standardUpdateSource) { - binding.updateSource = binding.standardUpdateSource; - binding.standardUpdateSource = null; - } - if (binding.standardUpdateTarget) { - binding.updateTarget = binding.standardUpdateTarget; - binding.standardUpdateTarget = null; - } - if (binding.validateBlurHandler) { - binding.validateTarget.removeEventListener('blur', binding.validateBlurHandler); - binding.validateBlurHandler = null; - binding.validateTarget = null; - } - binding.validationController.unregisterBinding(binding); - binding.validationController = null; + ValidateBindingBehavior.prototype.getValidateTrigger = function (controller) { + return controller.validateTrigger; }; ValidateBindingBehavior.inject = [aurelia_task_queue_1.TaskQueue]; return ValidateBindingBehavior; - }()); + }(validate_binding_behavior_base_1.ValidateBindingBehaviorBase)); exports.ValidateBindingBehavior = ValidateBindingBehavior; + /** + * Binding behavior. Indicates the bound property will be validated + * manually, by calling controller.validate(). No automatic validation + * triggered by data-entry or blur will occur. + */ + var ValidateManuallyBindingBehavior = (function (_super) { + __extends(ValidateManuallyBindingBehavior, _super); + function ValidateManuallyBindingBehavior() { + _super.apply(this, arguments); + } + ValidateManuallyBindingBehavior.prototype.getValidateTrigger = function () { + return validate_trigger_1.validateTrigger.manual; + }; + ValidateManuallyBindingBehavior.inject = [aurelia_task_queue_1.TaskQueue]; + return ValidateManuallyBindingBehavior; + }(validate_binding_behavior_base_1.ValidateBindingBehaviorBase)); + exports.ValidateManuallyBindingBehavior = ValidateManuallyBindingBehavior; + /** + * Binding behavior. Indicates the bound property should be validated + * when the associated element blurs. + */ + var ValidateOnBlurBindingBehavior = (function (_super) { + __extends(ValidateOnBlurBindingBehavior, _super); + function ValidateOnBlurBindingBehavior() { + _super.apply(this, arguments); + } + ValidateOnBlurBindingBehavior.prototype.getValidateTrigger = function () { + return validate_trigger_1.validateTrigger.blur; + }; + ValidateOnBlurBindingBehavior.inject = [aurelia_task_queue_1.TaskQueue]; + return ValidateOnBlurBindingBehavior; + }(validate_binding_behavior_base_1.ValidateBindingBehaviorBase)); + exports.ValidateOnBlurBindingBehavior = ValidateOnBlurBindingBehavior; + /** + * Binding behavior. Indicates the bound property should be validated + * when the associated element is changed by the user, causing a change + * to the model. + */ + var ValidateOnChangeBindingBehavior = (function (_super) { + __extends(ValidateOnChangeBindingBehavior, _super); + function ValidateOnChangeBindingBehavior() { + _super.apply(this, arguments); + } + ValidateOnChangeBindingBehavior.prototype.getValidateTrigger = function () { + return validate_trigger_1.validateTrigger.change; + }; + ValidateOnChangeBindingBehavior.inject = [aurelia_task_queue_1.TaskQueue]; + return ValidateOnChangeBindingBehavior; + }(validate_binding_behavior_base_1.ValidateBindingBehaviorBase)); + exports.ValidateOnChangeBindingBehavior = ValidateOnChangeBindingBehavior; + /** + * Binding behavior. Indicates the bound property should be validated + * when the associated element blurs or is changed by the user, causing + * a change to the model. + */ + var ValidateOnChangeOrBlurBindingBehavior = (function (_super) { + __extends(ValidateOnChangeOrBlurBindingBehavior, _super); + function ValidateOnChangeOrBlurBindingBehavior() { + _super.apply(this, arguments); + } + ValidateOnChangeOrBlurBindingBehavior.prototype.getValidateTrigger = function () { + return validate_trigger_1.validateTrigger.changeOrBlur; + }; + ValidateOnChangeOrBlurBindingBehavior.inject = [aurelia_task_queue_1.TaskQueue]; + return ValidateOnChangeOrBlurBindingBehavior; + }(validate_binding_behavior_base_1.ValidateBindingBehaviorBase)); + exports.ValidateOnChangeOrBlurBindingBehavior = ValidateOnChangeOrBlurBindingBehavior; }); diff --git a/dist/amd/validate-trigger.d.ts b/dist/amd/validate-trigger.d.ts index 06d80aed..e001bfa3 100644 --- a/dist/amd/validate-trigger.d.ts +++ b/dist/amd/validate-trigger.d.ts @@ -2,7 +2,8 @@ * Validation triggers. */ export declare const validateTrigger: { - blur: string; - change: string; - manual: string; + manual: number; + blur: number; + change: number; + changeOrBlur: number; }; diff --git a/dist/amd/validate-trigger.js b/dist/amd/validate-trigger.js index b8201aa1..2c1a1b0e 100644 --- a/dist/amd/validate-trigger.js +++ b/dist/amd/validate-trigger.js @@ -4,19 +4,23 @@ define(["require", "exports"], function (require, exports) { * Validation triggers. */ exports.validateTrigger = { + /** + * 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. */ - blur: 'blur', + blur: 1, /** * Validate the binding when it updates the model due to a change in the view. - * Not specific to DOM "change" events. */ - change: 'change', + change: 2, /** - * Manual validation. Use the controller's `validate()` and `reset()` methods - * to validate all bindings. - */ - manual: 'manual' + * Validate the binding when the binding's target element fires a DOM "blur" event and + * when it updates the model due to a change in the view. + */ + changeOrBlur: 3 }; }); diff --git a/dist/amd/validation-controller-factory.d.ts b/dist/amd/validation-controller-factory.d.ts index fcb37841..2c51999e 100644 --- a/dist/amd/validation-controller-factory.d.ts +++ b/dist/amd/validation-controller-factory.d.ts @@ -1,4 +1,6 @@ import { Container } from 'aurelia-dependency-injection'; +import { ValidationController } from './validation-controller'; +import { Validator } from './validator'; /** * Creates ValidationController instances. */ @@ -7,13 +9,12 @@ export declare class ValidationControllerFactory { static get(container: Container): ValidationControllerFactory; constructor(container: Container); /** - * Creates a new controller and registers it in the current element's container so that it's - * available to the validate binding behavior and renderers. + * Creates a new controller instance. */ - create(): any; + create(validator?: Validator): ValidationController; /** * Creates a new controller and registers it in the current element's container so that it's * available to the validate binding behavior and renderers. */ - createForCurrentScope(): any; + createForCurrentScope(validator?: Validator): ValidationController; } diff --git a/dist/amd/validation-controller-factory.js b/dist/amd/validation-controller-factory.js index cd9263d7..344a4c56 100644 --- a/dist/amd/validation-controller-factory.js +++ b/dist/amd/validation-controller-factory.js @@ -1,4 +1,4 @@ -define(["require", "exports", './validation-controller'], function (require, exports, validation_controller_1) { +define(["require", "exports", './validation-controller', './validator'], function (require, exports, validation_controller_1, validator_1) { "use strict"; /** * Creates ValidationController instances. @@ -11,18 +11,20 @@ define(["require", "exports", './validation-controller'], function (require, exp return new ValidationControllerFactory(container); }; /** - * Creates a new controller and registers it in the current element's container so that it's - * available to the validate binding behavior and renderers. + * Creates a new controller instance. */ - ValidationControllerFactory.prototype.create = function () { - return this.container.invoke(validation_controller_1.ValidationController); + ValidationControllerFactory.prototype.create = function (validator) { + if (!validator) { + validator = this.container.get(validator_1.Validator); + } + return new validation_controller_1.ValidationController(validator); }; /** * Creates a new controller and registers it in the current element's container so that it's * available to the validate binding behavior and renderers. */ - ValidationControllerFactory.prototype.createForCurrentScope = function () { - var controller = this.create(); + ValidationControllerFactory.prototype.createForCurrentScope = function (validator) { + var controller = this.create(validator); this.container.registerInstance(validation_controller_1.ValidationController, controller); return controller; }; diff --git a/dist/amd/validation-controller.d.ts b/dist/amd/validation-controller.d.ts index 82379694..dcc1c174 100644 --- a/dist/amd/validation-controller.d.ts +++ b/dist/amd/validation-controller.d.ts @@ -39,7 +39,7 @@ export declare class ValidationController { /** * The trigger that will invoke automatic validation of a property used in a binding. */ - validateTrigger: string; + validateTrigger: number; private finishValidating; constructor(validator: Validator); /** diff --git a/dist/commonjs/implementation/rule.d.ts b/dist/commonjs/implementation/rule.d.ts index 2eb47660..8bba9d76 100644 --- a/dist/commonjs/implementation/rule.d.ts +++ b/dist/commonjs/implementation/rule.d.ts @@ -1,4 +1,7 @@ import { Expression } from 'aurelia-binding'; +/** + * Information related to a property that is the subject of validation. + */ export interface RuleProperty { /** * The property name. null indicates the rule targets the object itself. @@ -9,6 +12,9 @@ export interface RuleProperty { */ displayName: string | null; } +/** + * A rule definition. Associations a rule with a property or object. + */ export interface Rule { property: RuleProperty; condition: (value: TValue, object?: TObject) => boolean | Promise; @@ -18,5 +24,6 @@ export interface Rule { } | null; messageKey: string; message: Expression | null; + sequence: number; tag?: string; } diff --git a/dist/commonjs/implementation/rules.d.ts b/dist/commonjs/implementation/rules.d.ts index 56bd6856..71c46daf 100644 --- a/dist/commonjs/implementation/rules.d.ts +++ b/dist/commonjs/implementation/rules.d.ts @@ -10,7 +10,7 @@ export declare class Rules { /** * Applies the rules to a target. */ - static set(target: any, rules: Rule[]): void; + static set(target: any, rules: Rule[][]): void; /** * Removes rules from a target. */ @@ -18,5 +18,5 @@ export declare class Rules { /** * Retrieves the target's rules. */ - static get(target: any): Rule[] | null; + static get(target: any): Rule[][] | null; } diff --git a/dist/commonjs/implementation/standard-validator.d.ts b/dist/commonjs/implementation/standard-validator.d.ts index d17f819b..e05e7b8e 100644 --- a/dist/commonjs/implementation/standard-validator.d.ts +++ b/dist/commonjs/implementation/standard-validator.d.ts @@ -13,6 +13,7 @@ export declare class StandardValidator extends Validator { 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. diff --git a/dist/commonjs/implementation/standard-validator.js b/dist/commonjs/implementation/standard-validator.js index ef860752..82314d0d 100644 --- a/dist/commonjs/implementation/standard-validator.js +++ b/dist/commonjs/implementation/standard-validator.js @@ -37,24 +37,12 @@ var StandardValidator = (function (_super) { }; return expression.evaluate({ bindingContext: object, overrideContext: overrideContext }, this.lookupFunctions); }; - StandardValidator.prototype.validate = function (object, propertyName, rules) { + StandardValidator.prototype.validateRuleSequence = function (object, propertyName, ruleSequence, sequence) { var _this = this; - var errors = []; - // rules specified? - if (!rules) { - // no. locate the rules via metadata. - rules = rules_1.Rules.get(object); - } - // any rules? - if (!rules) { - return Promise.resolve(errors); - } // are we validating all properties or a single property? var validateAllProperties = propertyName === null || propertyName === undefined; - var addError = function (rule, value) { - var message = _this.getMessage(rule, object, value); - errors.push(new validation_error_1.ValidationError(rule, message, object, rule.property.name)); - }; + var rules = ruleSequence[sequence]; + var errors = []; // validate each rule. var promises = []; var _loop_1 = function(i) { @@ -70,25 +58,39 @@ var StandardValidator = (function (_super) { // validate. var value = rule.property.name === null ? object : object[rule.property.name]; var promiseOrBoolean = rule.condition(value, object); - if (promiseOrBoolean instanceof Promise) { - promises.push(promiseOrBoolean.then(function (isValid) { - if (!isValid) { - addError(rule, value); - } - })); - return "continue"; - } - if (!promiseOrBoolean) { - addError(rule, value); + if (!(promiseOrBoolean instanceof Promise)) { + promiseOrBoolean = Promise.resolve(promiseOrBoolean); } + promises.push(promiseOrBoolean.then(function (isValid) { + if (!isValid) { + var message = _this.getMessage(rule, object, value); + errors.push(new validation_error_1.ValidationError(rule, message, object, rule.property.name)); + } + })); }; for (var i = 0; i < rules.length; i++) { _loop_1(i); } - if (promises.length === 0) { - return Promise.resolve(errors); + return Promise.all(promises) + .then(function () { + sequence++; + if (errors.length === 0 && sequence < ruleSequence.length) { + return _this.validateRuleSequence(object, propertyName, ruleSequence, sequence); + } + return errors; + }); + }; + StandardValidator.prototype.validate = function (object, propertyName, rules) { + // rules specified? + if (!rules) { + // no. attempt to locate the rules. + rules = rules_1.Rules.get(object); + } + // any rules? + if (!rules) { + return Promise.resolve([]); } - return Promise.all(promises).then(function () { return errors; }); + return this.validateRuleSequence(object, propertyName, rules, 0); }; /** * Validates the specified property. diff --git a/dist/commonjs/implementation/validation-parser.js b/dist/commonjs/implementation/validation-parser.js index b02edd02..67c6b45b 100644 --- a/dist/commonjs/implementation/validation-parser.js +++ b/dist/commonjs/implementation/validation-parser.js @@ -47,13 +47,10 @@ var ValidationParser = (function () { return this.parser.parse(match[1]); }; ValidationParser.prototype.parseProperty = function (property) { - var accessor; if (util_1.isString(property)) { - accessor = this.parser.parse(property); - } - else { - accessor = this.getAccessorExpression(property.toString()); + 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) { return { diff --git a/dist/commonjs/implementation/validation-rules.d.ts b/dist/commonjs/implementation/validation-rules.d.ts index 997065f7..1bd93972 100644 --- a/dist/commonjs/implementation/validation-rules.d.ts +++ b/dist/commonjs/implementation/validation-rules.d.ts @@ -9,6 +9,12 @@ export declare class FluentRuleCustomizer { private parser; private rule; constructor(property: RuleProperty, condition: (value: TValue, object?: TObject) => boolean | Promise, config: Object, fluentEnsure: FluentEnsure, fluentRules: FluentRules, parser: ValidationParser); + /** + * Validate subsequent rules after previously declared rules have + * been validated successfully. Use to postpone validation of costly + * rules until less expensive rules pass validation. + */ + then(): this; /** * Specifies the key to use when looking up the rule's validation message. */ @@ -42,7 +48,7 @@ export declare class FluentRuleCustomizer { /** * Rules that have been defined using the fluent API. */ - readonly rules: Rule[]; + readonly rules: Rule[][]; /** * Applies the rules to a class or object, making them discoverable by the StandardValidator. * @param target A class or object. @@ -184,7 +190,8 @@ export declare class FluentEnsure { /** * Rules that have been defined using the fluent API. */ - rules: Rule[]; + rules: Rule[][]; + _sequence: number; constructor(parser: ValidationParser); /** * Target a property with validation rules. @@ -200,6 +207,10 @@ export declare class FluentEnsure { * @param target A class or object. */ on(target: any): this; + /** + * Adds a rule definition to the sequenced ruleset. + */ + _addRule(rule: Rule): void; private assertInitialized(); } /** @@ -230,7 +241,7 @@ export declare class ValidationRules { * @param rules The rules to search. * @param tag The tag to search for. */ - static taggedRules(rules: Rule[], tag: string): Rule[]; + static taggedRules(rules: Rule[][], tag: string): Rule[][]; /** * Removes the rules from a class or object. * @param target A class or object. diff --git a/dist/commonjs/implementation/validation-rules.js b/dist/commonjs/implementation/validation-rules.js index 49d0086c..d6291c4b 100644 --- a/dist/commonjs/implementation/validation-rules.js +++ b/dist/commonjs/implementation/validation-rules.js @@ -17,10 +17,20 @@ var FluentRuleCustomizer = (function () { config: config, when: null, messageKey: 'default', - message: null + message: null, + sequence: fluentEnsure._sequence }; - this.fluentEnsure.rules.push(this.rule); + this.fluentEnsure._addRule(this.rule); } + /** + * Validate subsequent rules after previously declared rules have + * been validated successfully. Use to postpone validation of costly + * rules until less expensive rules pass validation. + */ + FluentRuleCustomizer.prototype.then = function () { + this.fluentEnsure._sequence++; + return this; + }; /** * Specifies the key to use when looking up the rule's validation message. */ @@ -242,7 +252,7 @@ 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])))$/) + 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) .withMessageKey('email'); }; /** @@ -299,6 +309,7 @@ var FluentEnsure = (function () { * Rules that have been defined using the fluent API. */ this.rules = []; + this._sequence = 0; } /** * Target a property with validation rules. @@ -323,6 +334,15 @@ var FluentEnsure = (function () { rules_1.Rules.set(target, this.rules); return this; }; + /** + * Adds a rule definition to the sequenced ruleset. + */ + FluentEnsure.prototype._addRule = function (rule) { + while (this.rules.length < rule.sequence + 1) { + this.rules.push([]); + } + this.rules[rule.sequence].push(rule); + }; FluentEnsure.prototype.assertInitialized = function () { if (this.parser) { return; @@ -371,7 +391,7 @@ var ValidationRules = (function () { * @param tag The tag to search for. */ ValidationRules.taggedRules = function (rules, tag) { - return rules.filter(function (r) { return r.tag === tag; }); + return rules.map(function (x) { return x.filter(function (r) { return r.tag === tag; }); }); }; /** * Removes the rules from a class or object. diff --git a/dist/commonjs/validate-binding-behavior-base.d.ts b/dist/commonjs/validate-binding-behavior-base.d.ts new file mode 100644 index 00000000..cb4eb912 --- /dev/null +++ b/dist/commonjs/validate-binding-behavior-base.d.ts @@ -0,0 +1,19 @@ +import { TaskQueue } from 'aurelia-task-queue'; +import { ValidationController } from './validation-controller'; +/** + * Binding behavior. Indicates the bound property should be validated. + */ +export declare abstract class ValidateBindingBehaviorBase { + private taskQueue; + 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. + */ + 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 new file mode 100644 index 00000000..6b8df181 --- /dev/null +++ b/dist/commonjs/validate-binding-behavior-base.js @@ -0,0 +1,99 @@ +"use strict"; +var aurelia_dependency_injection_1 = require('aurelia-dependency-injection'); +var aurelia_pal_1 = require('aurelia-pal'); +var validation_controller_1 = require('./validation-controller'); +var validate_trigger_1 = require('./validate-trigger'); +/** + * Binding behavior. Indicates the bound property should be validated. + */ +var ValidateBindingBehaviorBase = (function () { + function ValidateBindingBehaviorBase(taskQueue) { + 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. + */ + ValidateBindingBehaviorBase.prototype.getTarget = function (binding, view) { + var target = binding.target; + // DOM element + if (target instanceof Element) { + return target; + } + // custom element or custom attribute + for (var i = 0, ii = view.controllers.length; i < ii; i++) { + var controller = view.controllers[i]; + if (controller.viewModel === target) { + var element = controller.container.get(aurelia_pal_1.DOM.Element); + if (element) { + return element; + } + throw new Error("Unable to locate target element for \"" + binding.sourceExpression + "\"."); + } + } + throw new Error("Unable to locate target element for \"" + binding.sourceExpression + "\"."); + }; + ValidateBindingBehaviorBase.prototype.bind = function (binding, source, rulesOrController, rules) { + var _this = this; + // identify the target element. + var target = this.getTarget(binding, source); + // locate the controller. + var controller; + if (rulesOrController instanceof validation_controller_1.ValidationController) { + controller = rulesOrController; + } + else { + controller = source.container.get(aurelia_dependency_injection_1.Optional.of(validation_controller_1.ValidationController)); + rules = rulesOrController; + } + if (controller === null) { + throw new Error("A ValidationController has not been registered."); + } + controller.registerBinding(binding, target, rules); + binding.validationController = controller; + var trigger = this.getValidateTrigger(controller); + if (trigger & validate_trigger_1.validateTrigger.change) { + binding.standardUpdateSource = binding.updateSource; + binding.updateSource = function (value) { + this.standardUpdateSource(value); + this.validationController.validateBinding(this); + }; + } + if (trigger & validate_trigger_1.validateTrigger.blur) { + binding.validateBlurHandler = function () { + _this.taskQueue.queueMicroTask(function () { return controller.validateBinding(binding); }); + }; + binding.validateTarget = target; + target.addEventListener('blur', binding.validateBlurHandler); + } + if (trigger !== validate_trigger_1.validateTrigger.manual) { + binding.standardUpdateTarget = binding.updateTarget; + binding.updateTarget = function (value) { + this.standardUpdateTarget(value); + this.validationController.resetBinding(this); + }; + } + }; + ValidateBindingBehaviorBase.prototype.unbind = function (binding) { + // reset the binding to it's original state. + if (binding.standardUpdateSource) { + binding.updateSource = binding.standardUpdateSource; + binding.standardUpdateSource = null; + } + if (binding.standardUpdateTarget) { + binding.updateTarget = binding.standardUpdateTarget; + binding.standardUpdateTarget = null; + } + if (binding.validateBlurHandler) { + binding.validateTarget.removeEventListener('blur', binding.validateBlurHandler); + binding.validateBlurHandler = null; + binding.validateTarget = null; + } + binding.validationController.unregisterBinding(binding); + binding.validationController = null; + }; + return ValidateBindingBehaviorBase; +}()); +exports.ValidateBindingBehaviorBase = ValidateBindingBehaviorBase; diff --git a/dist/commonjs/validate-binding-behavior.d.ts b/dist/commonjs/validate-binding-behavior.d.ts index a846db99..2d55c7ec 100644 --- a/dist/commonjs/validate-binding-behavior.d.ts +++ b/dist/commonjs/validate-binding-behavior.d.ts @@ -1,19 +1,47 @@ import { TaskQueue } from 'aurelia-task-queue'; import { ValidationController } from './validation-controller'; +import { ValidateBindingBehaviorBase } from './validate-binding-behavior-base'; /** - * Binding behavior. Indicates the bound property should be validated. + * Binding behavior. Indicates the bound property should be validated + * when the validate trigger specified by the associated controller's + * validateTrigger property occurs. */ -export declare class ValidateBindingBehavior { - private taskQueue; +export declare class ValidateBindingBehavior extends ValidateBindingBehaviorBase { static inject: typeof TaskQueue[]; - constructor(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. - */ - getTarget(binding: any, view: any): any; - bind(binding: any, source: any, rulesOrController?: ValidationController | any, rules?: any): void; - unbind(binding: any): void; + getValidateTrigger(controller: ValidationController): number; +} +/** + * Binding behavior. Indicates the bound property will be validated + * manually, by calling controller.validate(). No automatic validation + * triggered by data-entry or blur will occur. + */ +export declare class ValidateManuallyBindingBehavior extends ValidateBindingBehaviorBase { + static inject: typeof TaskQueue[]; + getValidateTrigger(): number; +} +/** + * Binding behavior. Indicates the bound property should be validated + * when the associated element blurs. + */ +export declare class ValidateOnBlurBindingBehavior extends ValidateBindingBehaviorBase { + static inject: typeof TaskQueue[]; + getValidateTrigger(): number; +} +/** + * Binding behavior. Indicates the bound property should be validated + * when the associated element is changed by the user, causing a change + * to the model. + */ +export declare class ValidateOnChangeBindingBehavior extends ValidateBindingBehaviorBase { + static inject: typeof TaskQueue[]; + getValidateTrigger(): number; +} +/** + * Binding behavior. Indicates the bound property should be validated + * when the associated element blurs or is changed by the user, causing + * a change to the model. + */ +export declare class ValidateOnChangeOrBlurBindingBehavior extends ValidateBindingBehaviorBase { + static inject: typeof TaskQueue[]; + getValidateTrigger(): number; } diff --git a/dist/commonjs/validate-binding-behavior.js b/dist/commonjs/validate-binding-behavior.js index ff497056..e19b1c20 100644 --- a/dist/commonjs/validate-binding-behavior.js +++ b/dist/commonjs/validate-binding-behavior.js @@ -1,100 +1,93 @@ "use strict"; -var aurelia_dependency_injection_1 = require('aurelia-dependency-injection'); -var aurelia_pal_1 = require('aurelia-pal'); +var __extends = (this && this.__extends) || function (d, b) { + for (var p in b) if (b.hasOwnProperty(p)) d[p] = b[p]; + function __() { this.constructor = d; } + d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __()); +}; var aurelia_task_queue_1 = require('aurelia-task-queue'); -var validation_controller_1 = require('./validation-controller'); var validate_trigger_1 = require('./validate-trigger'); +var validate_binding_behavior_base_1 = require('./validate-binding-behavior-base'); /** - * Binding behavior. Indicates the bound property should be validated. + * Binding behavior. Indicates the bound property should be validated + * when the validate trigger specified by the associated controller's + * validateTrigger property occurs. */ -var ValidateBindingBehavior = (function () { - function ValidateBindingBehavior(taskQueue) { - this.taskQueue = taskQueue; +var ValidateBindingBehavior = (function (_super) { + __extends(ValidateBindingBehavior, _super); + function ValidateBindingBehavior() { + _super.apply(this, arguments); } - /** - * 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. - */ - ValidateBindingBehavior.prototype.getTarget = function (binding, view) { - var target = binding.target; - // DOM element - if (target instanceof Element) { - return target; - } - // custom element or custom attribute - for (var i = 0, ii = view.controllers.length; i < ii; i++) { - var controller = view.controllers[i]; - if (controller.viewModel === target) { - var element = controller.container.get(aurelia_pal_1.DOM.Element); - if (element) { - return element; - } - throw new Error("Unable to locate target element for \"" + binding.sourceExpression + "\"."); - } - } - throw new Error("Unable to locate target element for \"" + binding.sourceExpression + "\"."); - }; - ValidateBindingBehavior.prototype.bind = function (binding, source, rulesOrController, rules) { - var _this = this; - // identify the target element. - var target = this.getTarget(binding, source); - // locate the controller. - var controller; - if (rulesOrController instanceof validation_controller_1.ValidationController) { - controller = rulesOrController; - } - else { - controller = source.container.get(aurelia_dependency_injection_1.Optional.of(validation_controller_1.ValidationController)); - rules = rulesOrController; - } - if (controller === null) { - throw new Error("A ValidationController has not been registered."); - } - controller.registerBinding(binding, target, rules); - binding.validationController = controller; - if (controller.validateTrigger === validate_trigger_1.validateTrigger.change) { - binding.standardUpdateSource = binding.updateSource; - binding.updateSource = function (value) { - this.standardUpdateSource(value); - this.validationController.validateBinding(this); - }; - } - else if (controller.validateTrigger === validate_trigger_1.validateTrigger.blur) { - binding.validateBlurHandler = function () { - _this.taskQueue.queueMicroTask(function () { return controller.validateBinding(binding); }); - }; - binding.validateTarget = target; - target.addEventListener('blur', binding.validateBlurHandler); - } - if (controller.validateTrigger !== validate_trigger_1.validateTrigger.manual) { - binding.standardUpdateTarget = binding.updateTarget; - binding.updateTarget = function (value) { - this.standardUpdateTarget(value); - this.validationController.resetBinding(this); - }; - } - }; - ValidateBindingBehavior.prototype.unbind = function (binding) { - // reset the binding to it's original state. - if (binding.standardUpdateSource) { - binding.updateSource = binding.standardUpdateSource; - binding.standardUpdateSource = null; - } - if (binding.standardUpdateTarget) { - binding.updateTarget = binding.standardUpdateTarget; - binding.standardUpdateTarget = null; - } - if (binding.validateBlurHandler) { - binding.validateTarget.removeEventListener('blur', binding.validateBlurHandler); - binding.validateBlurHandler = null; - binding.validateTarget = null; - } - binding.validationController.unregisterBinding(binding); - binding.validationController = null; + ValidateBindingBehavior.prototype.getValidateTrigger = function (controller) { + return controller.validateTrigger; }; ValidateBindingBehavior.inject = [aurelia_task_queue_1.TaskQueue]; return ValidateBindingBehavior; -}()); +}(validate_binding_behavior_base_1.ValidateBindingBehaviorBase)); exports.ValidateBindingBehavior = ValidateBindingBehavior; +/** + * Binding behavior. Indicates the bound property will be validated + * manually, by calling controller.validate(). No automatic validation + * triggered by data-entry or blur will occur. + */ +var ValidateManuallyBindingBehavior = (function (_super) { + __extends(ValidateManuallyBindingBehavior, _super); + function ValidateManuallyBindingBehavior() { + _super.apply(this, arguments); + } + ValidateManuallyBindingBehavior.prototype.getValidateTrigger = function () { + return validate_trigger_1.validateTrigger.manual; + }; + ValidateManuallyBindingBehavior.inject = [aurelia_task_queue_1.TaskQueue]; + return ValidateManuallyBindingBehavior; +}(validate_binding_behavior_base_1.ValidateBindingBehaviorBase)); +exports.ValidateManuallyBindingBehavior = ValidateManuallyBindingBehavior; +/** + * Binding behavior. Indicates the bound property should be validated + * when the associated element blurs. + */ +var ValidateOnBlurBindingBehavior = (function (_super) { + __extends(ValidateOnBlurBindingBehavior, _super); + function ValidateOnBlurBindingBehavior() { + _super.apply(this, arguments); + } + ValidateOnBlurBindingBehavior.prototype.getValidateTrigger = function () { + return validate_trigger_1.validateTrigger.blur; + }; + ValidateOnBlurBindingBehavior.inject = [aurelia_task_queue_1.TaskQueue]; + return ValidateOnBlurBindingBehavior; +}(validate_binding_behavior_base_1.ValidateBindingBehaviorBase)); +exports.ValidateOnBlurBindingBehavior = ValidateOnBlurBindingBehavior; +/** + * Binding behavior. Indicates the bound property should be validated + * when the associated element is changed by the user, causing a change + * to the model. + */ +var ValidateOnChangeBindingBehavior = (function (_super) { + __extends(ValidateOnChangeBindingBehavior, _super); + function ValidateOnChangeBindingBehavior() { + _super.apply(this, arguments); + } + ValidateOnChangeBindingBehavior.prototype.getValidateTrigger = function () { + return validate_trigger_1.validateTrigger.change; + }; + ValidateOnChangeBindingBehavior.inject = [aurelia_task_queue_1.TaskQueue]; + return ValidateOnChangeBindingBehavior; +}(validate_binding_behavior_base_1.ValidateBindingBehaviorBase)); +exports.ValidateOnChangeBindingBehavior = ValidateOnChangeBindingBehavior; +/** + * Binding behavior. Indicates the bound property should be validated + * when the associated element blurs or is changed by the user, causing + * a change to the model. + */ +var ValidateOnChangeOrBlurBindingBehavior = (function (_super) { + __extends(ValidateOnChangeOrBlurBindingBehavior, _super); + function ValidateOnChangeOrBlurBindingBehavior() { + _super.apply(this, arguments); + } + ValidateOnChangeOrBlurBindingBehavior.prototype.getValidateTrigger = function () { + return validate_trigger_1.validateTrigger.changeOrBlur; + }; + ValidateOnChangeOrBlurBindingBehavior.inject = [aurelia_task_queue_1.TaskQueue]; + return ValidateOnChangeOrBlurBindingBehavior; +}(validate_binding_behavior_base_1.ValidateBindingBehaviorBase)); +exports.ValidateOnChangeOrBlurBindingBehavior = ValidateOnChangeOrBlurBindingBehavior; diff --git a/dist/commonjs/validate-trigger.d.ts b/dist/commonjs/validate-trigger.d.ts index 06d80aed..e001bfa3 100644 --- a/dist/commonjs/validate-trigger.d.ts +++ b/dist/commonjs/validate-trigger.d.ts @@ -2,7 +2,8 @@ * Validation triggers. */ export declare const validateTrigger: { - blur: string; - change: string; - manual: string; + manual: number; + blur: number; + change: number; + changeOrBlur: number; }; diff --git a/dist/commonjs/validate-trigger.js b/dist/commonjs/validate-trigger.js index 569ccc9b..5cf0cf1c 100644 --- a/dist/commonjs/validate-trigger.js +++ b/dist/commonjs/validate-trigger.js @@ -3,18 +3,22 @@ * Validation triggers. */ exports.validateTrigger = { + /** + * 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. */ - blur: 'blur', + blur: 1, /** * Validate the binding when it updates the model due to a change in the view. - * Not specific to DOM "change" events. */ - change: 'change', + change: 2, /** - * Manual validation. Use the controller's `validate()` and `reset()` methods - * to validate all bindings. - */ - manual: 'manual' + * Validate the binding when the binding's target element fires a DOM "blur" event and + * when it updates the model due to a change in the view. + */ + changeOrBlur: 3 }; diff --git a/dist/commonjs/validation-controller-factory.d.ts b/dist/commonjs/validation-controller-factory.d.ts index fcb37841..2c51999e 100644 --- a/dist/commonjs/validation-controller-factory.d.ts +++ b/dist/commonjs/validation-controller-factory.d.ts @@ -1,4 +1,6 @@ import { Container } from 'aurelia-dependency-injection'; +import { ValidationController } from './validation-controller'; +import { Validator } from './validator'; /** * Creates ValidationController instances. */ @@ -7,13 +9,12 @@ export declare class ValidationControllerFactory { static get(container: Container): ValidationControllerFactory; constructor(container: Container); /** - * Creates a new controller and registers it in the current element's container so that it's - * available to the validate binding behavior and renderers. + * Creates a new controller instance. */ - create(): any; + create(validator?: Validator): ValidationController; /** * Creates a new controller and registers it in the current element's container so that it's * available to the validate binding behavior and renderers. */ - createForCurrentScope(): any; + createForCurrentScope(validator?: Validator): ValidationController; } diff --git a/dist/commonjs/validation-controller-factory.js b/dist/commonjs/validation-controller-factory.js index f20af392..f358350e 100644 --- a/dist/commonjs/validation-controller-factory.js +++ b/dist/commonjs/validation-controller-factory.js @@ -1,5 +1,6 @@ "use strict"; var validation_controller_1 = require('./validation-controller'); +var validator_1 = require('./validator'); /** * Creates ValidationController instances. */ @@ -11,18 +12,20 @@ var ValidationControllerFactory = (function () { return new ValidationControllerFactory(container); }; /** - * Creates a new controller and registers it in the current element's container so that it's - * available to the validate binding behavior and renderers. + * Creates a new controller instance. */ - ValidationControllerFactory.prototype.create = function () { - return this.container.invoke(validation_controller_1.ValidationController); + ValidationControllerFactory.prototype.create = function (validator) { + if (!validator) { + validator = this.container.get(validator_1.Validator); + } + return new validation_controller_1.ValidationController(validator); }; /** * Creates a new controller and registers it in the current element's container so that it's * available to the validate binding behavior and renderers. */ - ValidationControllerFactory.prototype.createForCurrentScope = function () { - var controller = this.create(); + ValidationControllerFactory.prototype.createForCurrentScope = function (validator) { + var controller = this.create(validator); this.container.registerInstance(validation_controller_1.ValidationController, controller); return controller; }; diff --git a/dist/commonjs/validation-controller.d.ts b/dist/commonjs/validation-controller.d.ts index 82379694..dcc1c174 100644 --- a/dist/commonjs/validation-controller.d.ts +++ b/dist/commonjs/validation-controller.d.ts @@ -39,7 +39,7 @@ export declare class ValidationController { /** * The trigger that will invoke automatic validation of a property used in a binding. */ - validateTrigger: string; + validateTrigger: number; private finishValidating; constructor(validator: Validator); /** diff --git a/dist/es2015/implementation/rule.d.ts b/dist/es2015/implementation/rule.d.ts index 2eb47660..8bba9d76 100644 --- a/dist/es2015/implementation/rule.d.ts +++ b/dist/es2015/implementation/rule.d.ts @@ -1,4 +1,7 @@ import { Expression } from 'aurelia-binding'; +/** + * Information related to a property that is the subject of validation. + */ export interface RuleProperty { /** * The property name. null indicates the rule targets the object itself. @@ -9,6 +12,9 @@ export interface RuleProperty { */ displayName: string | null; } +/** + * A rule definition. Associations a rule with a property or object. + */ export interface Rule { property: RuleProperty; condition: (value: TValue, object?: TObject) => boolean | Promise; @@ -18,5 +24,6 @@ export interface Rule { } | null; messageKey: string; message: Expression | null; + sequence: number; tag?: string; } diff --git a/dist/es2015/implementation/rules.d.ts b/dist/es2015/implementation/rules.d.ts index 56bd6856..71c46daf 100644 --- a/dist/es2015/implementation/rules.d.ts +++ b/dist/es2015/implementation/rules.d.ts @@ -10,7 +10,7 @@ export declare class Rules { /** * Applies the rules to a target. */ - static set(target: any, rules: Rule[]): void; + static set(target: any, rules: Rule[][]): void; /** * Removes rules from a target. */ @@ -18,5 +18,5 @@ export declare class Rules { /** * Retrieves the target's rules. */ - static get(target: any): Rule[] | null; + static get(target: any): Rule[][] | null; } diff --git a/dist/es2015/implementation/standard-validator.d.ts b/dist/es2015/implementation/standard-validator.d.ts index d17f819b..e05e7b8e 100644 --- a/dist/es2015/implementation/standard-validator.d.ts +++ b/dist/es2015/implementation/standard-validator.d.ts @@ -13,6 +13,7 @@ export declare class StandardValidator extends Validator { 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. diff --git a/dist/es2015/implementation/standard-validator.js b/dist/es2015/implementation/standard-validator.js index baa9c04e..f81057ec 100644 --- a/dist/es2015/implementation/standard-validator.js +++ b/dist/es2015/implementation/standard-validator.js @@ -30,23 +30,11 @@ export class StandardValidator extends Validator { }; return expression.evaluate({ bindingContext: object, overrideContext }, this.lookupFunctions); } - validate(object, propertyName, rules) { - const errors = []; - // rules specified? - if (!rules) { - // no. locate the rules via metadata. - rules = Rules.get(object); - } - // any rules? - if (!rules) { - return Promise.resolve(errors); - } + validateRuleSequence(object, propertyName, ruleSequence, sequence) { // are we validating all properties or a single property? const validateAllProperties = propertyName === null || propertyName === undefined; - const addError = (rule, value) => { - const message = this.getMessage(rule, object, value); - errors.push(new ValidationError(rule, message, object, rule.property.name)); - }; + const rules = ruleSequence[sequence]; + const errors = []; // validate each rule. const promises = []; for (let i = 0; i < rules.length; i++) { @@ -61,23 +49,37 @@ export class StandardValidator extends Validator { } // validate. const value = rule.property.name === null ? object : object[rule.property.name]; - const promiseOrBoolean = rule.condition(value, object); - if (promiseOrBoolean instanceof Promise) { - promises.push(promiseOrBoolean.then(isValid => { - if (!isValid) { - addError(rule, value); - } - })); - continue; + let promiseOrBoolean = rule.condition(value, object); + if (!(promiseOrBoolean instanceof Promise)) { + promiseOrBoolean = Promise.resolve(promiseOrBoolean); } - if (!promiseOrBoolean) { - addError(rule, value); + promises.push(promiseOrBoolean.then(isValid => { + if (!isValid) { + const message = this.getMessage(rule, object, value); + errors.push(new ValidationError(rule, message, object, rule.property.name)); + } + })); + } + return Promise.all(promises) + .then(() => { + sequence++; + if (errors.length === 0 && sequence < ruleSequence.length) { + return this.validateRuleSequence(object, propertyName, ruleSequence, sequence); } + return errors; + }); + } + validate(object, propertyName, rules) { + // rules specified? + if (!rules) { + // no. attempt to locate the rules. + rules = Rules.get(object); } - if (promises.length === 0) { - return Promise.resolve(errors); + // any rules? + if (!rules) { + return Promise.resolve([]); } - return Promise.all(promises).then(() => errors); + return this.validateRuleSequence(object, propertyName, rules, 0); } /** * Validates the specified property. diff --git a/dist/es2015/implementation/validation-parser.js b/dist/es2015/implementation/validation-parser.js index a693e726..96b0aeba 100644 --- a/dist/es2015/implementation/validation-parser.js +++ b/dist/es2015/implementation/validation-parser.js @@ -41,13 +41,10 @@ export class ValidationParser { return this.parser.parse(match[1]); } parseProperty(property) { - let accessor; if (isString(property)) { - accessor = this.parser.parse(property); - } - else { - accessor = this.getAccessorExpression(property.toString()); + return { name: property, displayName: null }; } + const accessor = this.getAccessorExpression(property.toString()); if (accessor instanceof AccessScope || accessor instanceof AccessMember && accessor.object instanceof AccessScope) { return { diff --git a/dist/es2015/implementation/validation-rules.d.ts b/dist/es2015/implementation/validation-rules.d.ts index 997065f7..1bd93972 100644 --- a/dist/es2015/implementation/validation-rules.d.ts +++ b/dist/es2015/implementation/validation-rules.d.ts @@ -9,6 +9,12 @@ export declare class FluentRuleCustomizer { private parser; private rule; constructor(property: RuleProperty, condition: (value: TValue, object?: TObject) => boolean | Promise, config: Object, fluentEnsure: FluentEnsure, fluentRules: FluentRules, parser: ValidationParser); + /** + * Validate subsequent rules after previously declared rules have + * been validated successfully. Use to postpone validation of costly + * rules until less expensive rules pass validation. + */ + then(): this; /** * Specifies the key to use when looking up the rule's validation message. */ @@ -42,7 +48,7 @@ export declare class FluentRuleCustomizer { /** * Rules that have been defined using the fluent API. */ - readonly rules: Rule[]; + readonly rules: Rule[][]; /** * Applies the rules to a class or object, making them discoverable by the StandardValidator. * @param target A class or object. @@ -184,7 +190,8 @@ export declare class FluentEnsure { /** * Rules that have been defined using the fluent API. */ - rules: Rule[]; + rules: Rule[][]; + _sequence: number; constructor(parser: ValidationParser); /** * Target a property with validation rules. @@ -200,6 +207,10 @@ export declare class FluentEnsure { * @param target A class or object. */ on(target: any): this; + /** + * Adds a rule definition to the sequenced ruleset. + */ + _addRule(rule: Rule): void; private assertInitialized(); } /** @@ -230,7 +241,7 @@ export declare class ValidationRules { * @param rules The rules to search. * @param tag The tag to search for. */ - static taggedRules(rules: Rule[], tag: string): Rule[]; + static taggedRules(rules: Rule[][], tag: string): Rule[][]; /** * Removes the rules from a class or object. * @param target A class or object. diff --git a/dist/es2015/implementation/validation-rules.js b/dist/es2015/implementation/validation-rules.js index 84d4598f..d38e5cbb 100644 --- a/dist/es2015/implementation/validation-rules.js +++ b/dist/es2015/implementation/validation-rules.js @@ -15,9 +15,19 @@ export class FluentRuleCustomizer { config, when: null, messageKey: 'default', - message: null + message: null, + sequence: fluentEnsure._sequence }; - this.fluentEnsure.rules.push(this.rule); + this.fluentEnsure._addRule(this.rule); + } + /** + * Validate subsequent rules after previously declared rules have + * been validated successfully. Use to postpone validation of costly + * rules until less expensive rules pass validation. + */ + then() { + this.fluentEnsure._sequence++; + return this; } /** * Specifies the key to use when looking up the rule's validation message. @@ -222,7 +232,7 @@ 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])))$/) + 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) .withMessageKey('email'); } /** @@ -277,6 +287,7 @@ export class FluentEnsure { * Rules that have been defined using the fluent API. */ this.rules = []; + this._sequence = 0; } /** * Target a property with validation rules. @@ -301,6 +312,15 @@ export class FluentEnsure { Rules.set(target, this.rules); return this; } + /** + * Adds a rule definition to the sequenced ruleset. + */ + _addRule(rule) { + while (this.rules.length < rule.sequence + 1) { + this.rules.push([]); + } + this.rules[rule.sequence].push(rule); + } assertInitialized() { if (this.parser) { return; @@ -345,7 +365,7 @@ export class ValidationRules { * @param tag The tag to search for. */ static taggedRules(rules, tag) { - return rules.filter(r => r.tag === tag); + return rules.map(x => x.filter(r => r.tag === tag)); } /** * Removes the rules from a class or object. diff --git a/dist/es2015/validate-binding-behavior-base.d.ts b/dist/es2015/validate-binding-behavior-base.d.ts new file mode 100644 index 00000000..cb4eb912 --- /dev/null +++ b/dist/es2015/validate-binding-behavior-base.d.ts @@ -0,0 +1,19 @@ +import { TaskQueue } from 'aurelia-task-queue'; +import { ValidationController } from './validation-controller'; +/** + * Binding behavior. Indicates the bound property should be validated. + */ +export declare abstract class ValidateBindingBehaviorBase { + private taskQueue; + 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. + */ + 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 new file mode 100644 index 00000000..c9dcbc6b --- /dev/null +++ b/dist/es2015/validate-binding-behavior-base.js @@ -0,0 +1,95 @@ +import { Optional } from 'aurelia-dependency-injection'; +import { DOM } from 'aurelia-pal'; +import { ValidationController } from './validation-controller'; +import { validateTrigger } from './validate-trigger'; +/** + * Binding behavior. Indicates the bound property should be validated. + */ +export class ValidateBindingBehaviorBase { + constructor(taskQueue) { + 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. + */ + getTarget(binding, view) { + const target = binding.target; + // DOM element + if (target instanceof Element) { + return target; + } + // custom element or custom attribute + for (let i = 0, ii = view.controllers.length; i < ii; i++) { + let controller = view.controllers[i]; + if (controller.viewModel === target) { + const element = controller.container.get(DOM.Element); + if (element) { + return element; + } + throw new Error(`Unable to locate target element for "${binding.sourceExpression}".`); + } + } + throw new Error(`Unable to locate target element for "${binding.sourceExpression}".`); + } + bind(binding, source, rulesOrController, rules) { + // identify the target element. + const target = this.getTarget(binding, source); + // locate the controller. + let controller; + if (rulesOrController instanceof ValidationController) { + controller = rulesOrController; + } + else { + controller = source.container.get(Optional.of(ValidationController)); + rules = rulesOrController; + } + if (controller === null) { + throw new Error(`A ValidationController has not been registered.`); + } + controller.registerBinding(binding, target, rules); + binding.validationController = controller; + const trigger = this.getValidateTrigger(controller); + if (trigger & validateTrigger.change) { + binding.standardUpdateSource = binding.updateSource; + binding.updateSource = function (value) { + this.standardUpdateSource(value); + this.validationController.validateBinding(this); + }; + } + if (trigger & validateTrigger.blur) { + binding.validateBlurHandler = () => { + this.taskQueue.queueMicroTask(() => controller.validateBinding(binding)); + }; + binding.validateTarget = target; + target.addEventListener('blur', binding.validateBlurHandler); + } + if (trigger !== validateTrigger.manual) { + binding.standardUpdateTarget = binding.updateTarget; + binding.updateTarget = function (value) { + this.standardUpdateTarget(value); + this.validationController.resetBinding(this); + }; + } + } + unbind(binding) { + // reset the binding to it's original state. + if (binding.standardUpdateSource) { + binding.updateSource = binding.standardUpdateSource; + binding.standardUpdateSource = null; + } + if (binding.standardUpdateTarget) { + binding.updateTarget = binding.standardUpdateTarget; + binding.standardUpdateTarget = null; + } + if (binding.validateBlurHandler) { + binding.validateTarget.removeEventListener('blur', binding.validateBlurHandler); + binding.validateBlurHandler = null; + binding.validateTarget = null; + } + binding.validationController.unregisterBinding(binding); + binding.validationController = null; + } +} diff --git a/dist/es2015/validate-binding-behavior.d.ts b/dist/es2015/validate-binding-behavior.d.ts index a846db99..2d55c7ec 100644 --- a/dist/es2015/validate-binding-behavior.d.ts +++ b/dist/es2015/validate-binding-behavior.d.ts @@ -1,19 +1,47 @@ import { TaskQueue } from 'aurelia-task-queue'; import { ValidationController } from './validation-controller'; +import { ValidateBindingBehaviorBase } from './validate-binding-behavior-base'; /** - * Binding behavior. Indicates the bound property should be validated. + * Binding behavior. Indicates the bound property should be validated + * when the validate trigger specified by the associated controller's + * validateTrigger property occurs. */ -export declare class ValidateBindingBehavior { - private taskQueue; +export declare class ValidateBindingBehavior extends ValidateBindingBehaviorBase { static inject: typeof TaskQueue[]; - constructor(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. - */ - getTarget(binding: any, view: any): any; - bind(binding: any, source: any, rulesOrController?: ValidationController | any, rules?: any): void; - unbind(binding: any): void; + getValidateTrigger(controller: ValidationController): number; +} +/** + * Binding behavior. Indicates the bound property will be validated + * manually, by calling controller.validate(). No automatic validation + * triggered by data-entry or blur will occur. + */ +export declare class ValidateManuallyBindingBehavior extends ValidateBindingBehaviorBase { + static inject: typeof TaskQueue[]; + getValidateTrigger(): number; +} +/** + * Binding behavior. Indicates the bound property should be validated + * when the associated element blurs. + */ +export declare class ValidateOnBlurBindingBehavior extends ValidateBindingBehaviorBase { + static inject: typeof TaskQueue[]; + getValidateTrigger(): number; +} +/** + * Binding behavior. Indicates the bound property should be validated + * when the associated element is changed by the user, causing a change + * to the model. + */ +export declare class ValidateOnChangeBindingBehavior extends ValidateBindingBehaviorBase { + static inject: typeof TaskQueue[]; + getValidateTrigger(): number; +} +/** + * Binding behavior. Indicates the bound property should be validated + * when the associated element blurs or is changed by the user, causing + * a change to the model. + */ +export declare class ValidateOnChangeOrBlurBindingBehavior extends ValidateBindingBehaviorBase { + static inject: typeof TaskQueue[]; + getValidateTrigger(): number; } diff --git a/dist/es2015/validate-binding-behavior.js b/dist/es2015/validate-binding-behavior.js index 1caed45b..318d227a 100644 --- a/dist/es2015/validate-binding-behavior.js +++ b/dist/es2015/validate-binding-behavior.js @@ -1,96 +1,57 @@ -import { Optional } from 'aurelia-dependency-injection'; -import { DOM } from 'aurelia-pal'; import { TaskQueue } from 'aurelia-task-queue'; -import { ValidationController } from './validation-controller'; import { validateTrigger } from './validate-trigger'; +import { ValidateBindingBehaviorBase } from './validate-binding-behavior-base'; /** - * Binding behavior. Indicates the bound property should be validated. + * Binding behavior. Indicates the bound property should be validated + * when the validate trigger specified by the associated controller's + * validateTrigger property occurs. */ -export class ValidateBindingBehavior { - constructor(taskQueue) { - this.taskQueue = taskQueue; +export class ValidateBindingBehavior extends ValidateBindingBehaviorBase { + getValidateTrigger(controller) { + return controller.validateTrigger; } - /** - * 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 - if (target instanceof Element) { - return target; - } - // custom element or custom attribute - for (let i = 0, ii = view.controllers.length; i < ii; i++) { - let controller = view.controllers[i]; - if (controller.viewModel === target) { - const element = controller.container.get(DOM.Element); - if (element) { - return element; - } - throw new Error(`Unable to locate target element for "${binding.sourceExpression}".`); - } - } - throw new Error(`Unable to locate target element for "${binding.sourceExpression}".`); +} +ValidateBindingBehavior.inject = [TaskQueue]; +/** + * Binding behavior. Indicates the bound property will be validated + * manually, by calling controller.validate(). No automatic validation + * triggered by data-entry or blur will occur. + */ +export class ValidateManuallyBindingBehavior extends ValidateBindingBehaviorBase { + getValidateTrigger() { + return validateTrigger.manual; } - bind(binding, source, rulesOrController, rules) { - // identify the target element. - const target = this.getTarget(binding, source); - // locate the controller. - let controller; - if (rulesOrController instanceof ValidationController) { - controller = rulesOrController; - } - else { - controller = source.container.get(Optional.of(ValidationController)); - rules = rulesOrController; - } - if (controller === null) { - throw new Error(`A ValidationController has not been registered.`); - } - controller.registerBinding(binding, target, rules); - binding.validationController = controller; - if (controller.validateTrigger === validateTrigger.change) { - binding.standardUpdateSource = binding.updateSource; - binding.updateSource = function (value) { - this.standardUpdateSource(value); - this.validationController.validateBinding(this); - }; - } - else if (controller.validateTrigger === validateTrigger.blur) { - binding.validateBlurHandler = () => { - this.taskQueue.queueMicroTask(() => controller.validateBinding(binding)); - }; - binding.validateTarget = target; - target.addEventListener('blur', binding.validateBlurHandler); - } - if (controller.validateTrigger !== validateTrigger.manual) { - binding.standardUpdateTarget = binding.updateTarget; - binding.updateTarget = function (value) { - this.standardUpdateTarget(value); - this.validationController.resetBinding(this); - }; - } +} +ValidateManuallyBindingBehavior.inject = [TaskQueue]; +/** + * Binding behavior. Indicates the bound property should be validated + * when the associated element blurs. + */ +export class ValidateOnBlurBindingBehavior extends ValidateBindingBehaviorBase { + getValidateTrigger() { + return validateTrigger.blur; } - unbind(binding) { - // reset the binding to it's original state. - if (binding.standardUpdateSource) { - binding.updateSource = binding.standardUpdateSource; - binding.standardUpdateSource = null; - } - if (binding.standardUpdateTarget) { - binding.updateTarget = binding.standardUpdateTarget; - binding.standardUpdateTarget = null; - } - if (binding.validateBlurHandler) { - binding.validateTarget.removeEventListener('blur', binding.validateBlurHandler); - binding.validateBlurHandler = null; - binding.validateTarget = null; - } - binding.validationController.unregisterBinding(binding); - binding.validationController = null; +} +ValidateOnBlurBindingBehavior.inject = [TaskQueue]; +/** + * Binding behavior. Indicates the bound property should be validated + * when the associated element is changed by the user, causing a change + * to the model. + */ +export class ValidateOnChangeBindingBehavior extends ValidateBindingBehaviorBase { + getValidateTrigger() { + return validateTrigger.change; } } -ValidateBindingBehavior.inject = [TaskQueue]; +ValidateOnChangeBindingBehavior.inject = [TaskQueue]; +/** + * Binding behavior. Indicates the bound property should be validated + * when the associated element blurs or is changed by the user, causing + * a change to the model. + */ +export class ValidateOnChangeOrBlurBindingBehavior extends ValidateBindingBehaviorBase { + getValidateTrigger() { + return validateTrigger.changeOrBlur; + } +} +ValidateOnChangeOrBlurBindingBehavior.inject = [TaskQueue]; diff --git a/dist/es2015/validate-trigger.d.ts b/dist/es2015/validate-trigger.d.ts index 06d80aed..e001bfa3 100644 --- a/dist/es2015/validate-trigger.d.ts +++ b/dist/es2015/validate-trigger.d.ts @@ -2,7 +2,8 @@ * Validation triggers. */ export declare const validateTrigger: { - blur: string; - change: string; - manual: string; + manual: number; + blur: number; + change: number; + changeOrBlur: number; }; diff --git a/dist/es2015/validate-trigger.js b/dist/es2015/validate-trigger.js index 813b1c0a..f999a39b 100644 --- a/dist/es2015/validate-trigger.js +++ b/dist/es2015/validate-trigger.js @@ -2,18 +2,22 @@ * Validation triggers. */ export const validateTrigger = { + /** + * 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. */ - blur: 'blur', + blur: 1, /** * Validate the binding when it updates the model due to a change in the view. - * Not specific to DOM "change" events. */ - change: 'change', + change: 2, /** - * Manual validation. Use the controller's `validate()` and `reset()` methods - * to validate all bindings. - */ - manual: 'manual' + * Validate the binding when the binding's target element fires a DOM "blur" event and + * when it updates the model due to a change in the view. + */ + changeOrBlur: 3 }; diff --git a/dist/es2015/validation-controller-factory.d.ts b/dist/es2015/validation-controller-factory.d.ts index fcb37841..2c51999e 100644 --- a/dist/es2015/validation-controller-factory.d.ts +++ b/dist/es2015/validation-controller-factory.d.ts @@ -1,4 +1,6 @@ import { Container } from 'aurelia-dependency-injection'; +import { ValidationController } from './validation-controller'; +import { Validator } from './validator'; /** * Creates ValidationController instances. */ @@ -7,13 +9,12 @@ export declare class ValidationControllerFactory { static get(container: Container): ValidationControllerFactory; constructor(container: Container); /** - * Creates a new controller and registers it in the current element's container so that it's - * available to the validate binding behavior and renderers. + * Creates a new controller instance. */ - create(): any; + create(validator?: Validator): ValidationController; /** * Creates a new controller and registers it in the current element's container so that it's * available to the validate binding behavior and renderers. */ - createForCurrentScope(): any; + createForCurrentScope(validator?: Validator): ValidationController; } diff --git a/dist/es2015/validation-controller-factory.js b/dist/es2015/validation-controller-factory.js index 1380c159..4f431d2b 100644 --- a/dist/es2015/validation-controller-factory.js +++ b/dist/es2015/validation-controller-factory.js @@ -1,4 +1,5 @@ import { ValidationController } from './validation-controller'; +import { Validator } from './validator'; /** * Creates ValidationController instances. */ @@ -10,18 +11,20 @@ export class ValidationControllerFactory { return new ValidationControllerFactory(container); } /** - * Creates a new controller and registers it in the current element's container so that it's - * available to the validate binding behavior and renderers. + * Creates a new controller instance. */ - create() { - return this.container.invoke(ValidationController); + create(validator) { + if (!validator) { + validator = this.container.get(Validator); + } + return new ValidationController(validator); } /** * Creates a new controller and registers it in the current element's container so that it's * available to the validate binding behavior and renderers. */ - createForCurrentScope() { - const controller = this.create(); + createForCurrentScope(validator) { + const controller = this.create(validator); this.container.registerInstance(ValidationController, controller); return controller; } diff --git a/dist/es2015/validation-controller.d.ts b/dist/es2015/validation-controller.d.ts index 82379694..dcc1c174 100644 --- a/dist/es2015/validation-controller.d.ts +++ b/dist/es2015/validation-controller.d.ts @@ -39,7 +39,7 @@ export declare class ValidationController { /** * The trigger that will invoke automatic validation of a property used in a binding. */ - validateTrigger: string; + validateTrigger: number; private finishValidating; constructor(validator: Validator); /** diff --git a/dist/native-modules/implementation/rule.d.ts b/dist/native-modules/implementation/rule.d.ts index 2eb47660..8bba9d76 100644 --- a/dist/native-modules/implementation/rule.d.ts +++ b/dist/native-modules/implementation/rule.d.ts @@ -1,4 +1,7 @@ import { Expression } from 'aurelia-binding'; +/** + * Information related to a property that is the subject of validation. + */ export interface RuleProperty { /** * The property name. null indicates the rule targets the object itself. @@ -9,6 +12,9 @@ export interface RuleProperty { */ displayName: string | null; } +/** + * A rule definition. Associations a rule with a property or object. + */ export interface Rule { property: RuleProperty; condition: (value: TValue, object?: TObject) => boolean | Promise; @@ -18,5 +24,6 @@ export interface Rule { } | null; messageKey: string; message: Expression | null; + sequence: number; tag?: string; } diff --git a/dist/native-modules/implementation/rules.d.ts b/dist/native-modules/implementation/rules.d.ts index 56bd6856..71c46daf 100644 --- a/dist/native-modules/implementation/rules.d.ts +++ b/dist/native-modules/implementation/rules.d.ts @@ -10,7 +10,7 @@ export declare class Rules { /** * Applies the rules to a target. */ - static set(target: any, rules: Rule[]): void; + static set(target: any, rules: Rule[][]): void; /** * Removes rules from a target. */ @@ -18,5 +18,5 @@ export declare class Rules { /** * Retrieves the target's rules. */ - static get(target: any): Rule[] | null; + static get(target: any): Rule[][] | null; } diff --git a/dist/native-modules/implementation/standard-validator.d.ts b/dist/native-modules/implementation/standard-validator.d.ts index d17f819b..e05e7b8e 100644 --- a/dist/native-modules/implementation/standard-validator.d.ts +++ b/dist/native-modules/implementation/standard-validator.d.ts @@ -13,6 +13,7 @@ export declare class StandardValidator extends Validator { 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. diff --git a/dist/native-modules/implementation/standard-validator.js b/dist/native-modules/implementation/standard-validator.js index 87196afe..6ad67066 100644 --- a/dist/native-modules/implementation/standard-validator.js +++ b/dist/native-modules/implementation/standard-validator.js @@ -36,24 +36,12 @@ export var StandardValidator = (function (_super) { }; return expression.evaluate({ bindingContext: object, overrideContext: overrideContext }, this.lookupFunctions); }; - StandardValidator.prototype.validate = function (object, propertyName, rules) { + StandardValidator.prototype.validateRuleSequence = function (object, propertyName, ruleSequence, sequence) { var _this = this; - var errors = []; - // rules specified? - if (!rules) { - // no. locate the rules via metadata. - rules = Rules.get(object); - } - // any rules? - if (!rules) { - return Promise.resolve(errors); - } // are we validating all properties or a single property? var validateAllProperties = propertyName === null || propertyName === undefined; - var addError = function (rule, value) { - var message = _this.getMessage(rule, object, value); - errors.push(new ValidationError(rule, message, object, rule.property.name)); - }; + var rules = ruleSequence[sequence]; + var errors = []; // validate each rule. var promises = []; var _loop_1 = function(i) { @@ -69,25 +57,39 @@ export var StandardValidator = (function (_super) { // validate. var value = rule.property.name === null ? object : object[rule.property.name]; var promiseOrBoolean = rule.condition(value, object); - if (promiseOrBoolean instanceof Promise) { - promises.push(promiseOrBoolean.then(function (isValid) { - if (!isValid) { - addError(rule, value); - } - })); - return "continue"; - } - if (!promiseOrBoolean) { - addError(rule, value); + if (!(promiseOrBoolean instanceof Promise)) { + promiseOrBoolean = Promise.resolve(promiseOrBoolean); } + promises.push(promiseOrBoolean.then(function (isValid) { + if (!isValid) { + var message = _this.getMessage(rule, object, value); + errors.push(new ValidationError(rule, message, object, rule.property.name)); + } + })); }; for (var i = 0; i < rules.length; i++) { _loop_1(i); } - if (promises.length === 0) { - return Promise.resolve(errors); + return Promise.all(promises) + .then(function () { + sequence++; + if (errors.length === 0 && sequence < ruleSequence.length) { + return _this.validateRuleSequence(object, propertyName, ruleSequence, sequence); + } + return errors; + }); + }; + StandardValidator.prototype.validate = function (object, propertyName, rules) { + // rules specified? + if (!rules) { + // no. attempt to locate the rules. + rules = Rules.get(object); + } + // any rules? + if (!rules) { + return Promise.resolve([]); } - return Promise.all(promises).then(function () { return errors; }); + return this.validateRuleSequence(object, propertyName, rules, 0); }; /** * Validates the specified property. diff --git a/dist/native-modules/implementation/validation-parser.js b/dist/native-modules/implementation/validation-parser.js index d74994c1..1587f771 100644 --- a/dist/native-modules/implementation/validation-parser.js +++ b/dist/native-modules/implementation/validation-parser.js @@ -46,13 +46,10 @@ export var ValidationParser = (function () { return this.parser.parse(match[1]); }; ValidationParser.prototype.parseProperty = function (property) { - var accessor; if (isString(property)) { - accessor = this.parser.parse(property); - } - else { - accessor = this.getAccessorExpression(property.toString()); + return { name: property, displayName: null }; } + var accessor = this.getAccessorExpression(property.toString()); if (accessor instanceof AccessScope || accessor instanceof AccessMember && accessor.object instanceof AccessScope) { return { diff --git a/dist/native-modules/implementation/validation-rules.d.ts b/dist/native-modules/implementation/validation-rules.d.ts index 997065f7..1bd93972 100644 --- a/dist/native-modules/implementation/validation-rules.d.ts +++ b/dist/native-modules/implementation/validation-rules.d.ts @@ -9,6 +9,12 @@ export declare class FluentRuleCustomizer { private parser; private rule; constructor(property: RuleProperty, condition: (value: TValue, object?: TObject) => boolean | Promise, config: Object, fluentEnsure: FluentEnsure, fluentRules: FluentRules, parser: ValidationParser); + /** + * Validate subsequent rules after previously declared rules have + * been validated successfully. Use to postpone validation of costly + * rules until less expensive rules pass validation. + */ + then(): this; /** * Specifies the key to use when looking up the rule's validation message. */ @@ -42,7 +48,7 @@ export declare class FluentRuleCustomizer { /** * Rules that have been defined using the fluent API. */ - readonly rules: Rule[]; + readonly rules: Rule[][]; /** * Applies the rules to a class or object, making them discoverable by the StandardValidator. * @param target A class or object. @@ -184,7 +190,8 @@ export declare class FluentEnsure { /** * Rules that have been defined using the fluent API. */ - rules: Rule[]; + rules: Rule[][]; + _sequence: number; constructor(parser: ValidationParser); /** * Target a property with validation rules. @@ -200,6 +207,10 @@ export declare class FluentEnsure { * @param target A class or object. */ on(target: any): this; + /** + * Adds a rule definition to the sequenced ruleset. + */ + _addRule(rule: Rule): void; private assertInitialized(); } /** @@ -230,7 +241,7 @@ export declare class ValidationRules { * @param rules The rules to search. * @param tag The tag to search for. */ - static taggedRules(rules: Rule[], tag: string): Rule[]; + static taggedRules(rules: Rule[][], tag: string): Rule[][]; /** * Removes the rules from a class or object. * @param target A class or object. diff --git a/dist/native-modules/implementation/validation-rules.js b/dist/native-modules/implementation/validation-rules.js index 1e7a3e55..4fc9daa2 100644 --- a/dist/native-modules/implementation/validation-rules.js +++ b/dist/native-modules/implementation/validation-rules.js @@ -16,10 +16,20 @@ export var FluentRuleCustomizer = (function () { config: config, when: null, messageKey: 'default', - message: null + message: null, + sequence: fluentEnsure._sequence }; - this.fluentEnsure.rules.push(this.rule); + this.fluentEnsure._addRule(this.rule); } + /** + * Validate subsequent rules after previously declared rules have + * been validated successfully. Use to postpone validation of costly + * rules until less expensive rules pass validation. + */ + FluentRuleCustomizer.prototype.then = function () { + this.fluentEnsure._sequence++; + return this; + }; /** * Specifies the key to use when looking up the rule's validation message. */ @@ -240,7 +250,7 @@ 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])))$/) + 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) .withMessageKey('email'); }; /** @@ -296,6 +306,7 @@ export var FluentEnsure = (function () { * Rules that have been defined using the fluent API. */ this.rules = []; + this._sequence = 0; } /** * Target a property with validation rules. @@ -320,6 +331,15 @@ export var FluentEnsure = (function () { Rules.set(target, this.rules); return this; }; + /** + * Adds a rule definition to the sequenced ruleset. + */ + FluentEnsure.prototype._addRule = function (rule) { + while (this.rules.length < rule.sequence + 1) { + this.rules.push([]); + } + this.rules[rule.sequence].push(rule); + }; FluentEnsure.prototype.assertInitialized = function () { if (this.parser) { return; @@ -367,7 +387,7 @@ export var ValidationRules = (function () { * @param tag The tag to search for. */ ValidationRules.taggedRules = function (rules, tag) { - return rules.filter(function (r) { return r.tag === tag; }); + return rules.map(function (x) { return x.filter(function (r) { return r.tag === tag; }); }); }; /** * Removes the rules from a class or object. diff --git a/dist/native-modules/validate-binding-behavior-base.d.ts b/dist/native-modules/validate-binding-behavior-base.d.ts new file mode 100644 index 00000000..cb4eb912 --- /dev/null +++ b/dist/native-modules/validate-binding-behavior-base.d.ts @@ -0,0 +1,19 @@ +import { TaskQueue } from 'aurelia-task-queue'; +import { ValidationController } from './validation-controller'; +/** + * Binding behavior. Indicates the bound property should be validated. + */ +export declare abstract class ValidateBindingBehaviorBase { + private taskQueue; + 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. + */ + 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 new file mode 100644 index 00000000..6f96fd0e --- /dev/null +++ b/dist/native-modules/validate-binding-behavior-base.js @@ -0,0 +1,97 @@ +import { Optional } from 'aurelia-dependency-injection'; +import { DOM } from 'aurelia-pal'; +import { ValidationController } from './validation-controller'; +import { validateTrigger } from './validate-trigger'; +/** + * Binding behavior. Indicates the bound property should be validated. + */ +export var ValidateBindingBehaviorBase = (function () { + function ValidateBindingBehaviorBase(taskQueue) { + 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. + */ + ValidateBindingBehaviorBase.prototype.getTarget = function (binding, view) { + var target = binding.target; + // DOM element + if (target instanceof Element) { + return target; + } + // custom element or custom attribute + for (var i = 0, ii = view.controllers.length; i < ii; i++) { + var controller = view.controllers[i]; + if (controller.viewModel === target) { + var element = controller.container.get(DOM.Element); + if (element) { + return element; + } + throw new Error("Unable to locate target element for \"" + binding.sourceExpression + "\"."); + } + } + throw new Error("Unable to locate target element for \"" + binding.sourceExpression + "\"."); + }; + ValidateBindingBehaviorBase.prototype.bind = function (binding, source, rulesOrController, rules) { + var _this = this; + // identify the target element. + var target = this.getTarget(binding, source); + // locate the controller. + var controller; + if (rulesOrController instanceof ValidationController) { + controller = rulesOrController; + } + else { + controller = source.container.get(Optional.of(ValidationController)); + rules = rulesOrController; + } + if (controller === null) { + throw new Error("A ValidationController has not been registered."); + } + controller.registerBinding(binding, target, rules); + binding.validationController = controller; + var trigger = this.getValidateTrigger(controller); + if (trigger & validateTrigger.change) { + binding.standardUpdateSource = binding.updateSource; + binding.updateSource = function (value) { + this.standardUpdateSource(value); + this.validationController.validateBinding(this); + }; + } + if (trigger & validateTrigger.blur) { + binding.validateBlurHandler = function () { + _this.taskQueue.queueMicroTask(function () { return controller.validateBinding(binding); }); + }; + binding.validateTarget = target; + target.addEventListener('blur', binding.validateBlurHandler); + } + if (trigger !== validateTrigger.manual) { + binding.standardUpdateTarget = binding.updateTarget; + binding.updateTarget = function (value) { + this.standardUpdateTarget(value); + this.validationController.resetBinding(this); + }; + } + }; + ValidateBindingBehaviorBase.prototype.unbind = function (binding) { + // reset the binding to it's original state. + if (binding.standardUpdateSource) { + binding.updateSource = binding.standardUpdateSource; + binding.standardUpdateSource = null; + } + if (binding.standardUpdateTarget) { + binding.updateTarget = binding.standardUpdateTarget; + binding.standardUpdateTarget = null; + } + if (binding.validateBlurHandler) { + binding.validateTarget.removeEventListener('blur', binding.validateBlurHandler); + binding.validateBlurHandler = null; + binding.validateTarget = null; + } + binding.validationController.unregisterBinding(binding); + binding.validationController = null; + }; + return ValidateBindingBehaviorBase; +}()); diff --git a/dist/native-modules/validate-binding-behavior.d.ts b/dist/native-modules/validate-binding-behavior.d.ts index a846db99..2d55c7ec 100644 --- a/dist/native-modules/validate-binding-behavior.d.ts +++ b/dist/native-modules/validate-binding-behavior.d.ts @@ -1,19 +1,47 @@ import { TaskQueue } from 'aurelia-task-queue'; import { ValidationController } from './validation-controller'; +import { ValidateBindingBehaviorBase } from './validate-binding-behavior-base'; /** - * Binding behavior. Indicates the bound property should be validated. + * Binding behavior. Indicates the bound property should be validated + * when the validate trigger specified by the associated controller's + * validateTrigger property occurs. */ -export declare class ValidateBindingBehavior { - private taskQueue; +export declare class ValidateBindingBehavior extends ValidateBindingBehaviorBase { static inject: typeof TaskQueue[]; - constructor(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. - */ - getTarget(binding: any, view: any): any; - bind(binding: any, source: any, rulesOrController?: ValidationController | any, rules?: any): void; - unbind(binding: any): void; + getValidateTrigger(controller: ValidationController): number; +} +/** + * Binding behavior. Indicates the bound property will be validated + * manually, by calling controller.validate(). No automatic validation + * triggered by data-entry or blur will occur. + */ +export declare class ValidateManuallyBindingBehavior extends ValidateBindingBehaviorBase { + static inject: typeof TaskQueue[]; + getValidateTrigger(): number; +} +/** + * Binding behavior. Indicates the bound property should be validated + * when the associated element blurs. + */ +export declare class ValidateOnBlurBindingBehavior extends ValidateBindingBehaviorBase { + static inject: typeof TaskQueue[]; + getValidateTrigger(): number; +} +/** + * Binding behavior. Indicates the bound property should be validated + * when the associated element is changed by the user, causing a change + * to the model. + */ +export declare class ValidateOnChangeBindingBehavior extends ValidateBindingBehaviorBase { + static inject: typeof TaskQueue[]; + getValidateTrigger(): number; +} +/** + * Binding behavior. Indicates the bound property should be validated + * when the associated element blurs or is changed by the user, causing + * a change to the model. + */ +export declare class ValidateOnChangeOrBlurBindingBehavior extends ValidateBindingBehaviorBase { + static inject: typeof TaskQueue[]; + getValidateTrigger(): number; } diff --git a/dist/native-modules/validate-binding-behavior.js b/dist/native-modules/validate-binding-behavior.js index 9711b5f9..4b6525b8 100644 --- a/dist/native-modules/validate-binding-behavior.js +++ b/dist/native-modules/validate-binding-behavior.js @@ -1,98 +1,87 @@ -import { Optional } from 'aurelia-dependency-injection'; -import { DOM } from 'aurelia-pal'; +var __extends = (this && this.__extends) || function (d, b) { + for (var p in b) if (b.hasOwnProperty(p)) d[p] = b[p]; + function __() { this.constructor = d; } + d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __()); +}; import { TaskQueue } from 'aurelia-task-queue'; -import { ValidationController } from './validation-controller'; import { validateTrigger } from './validate-trigger'; +import { ValidateBindingBehaviorBase } from './validate-binding-behavior-base'; /** - * Binding behavior. Indicates the bound property should be validated. + * Binding behavior. Indicates the bound property should be validated + * when the validate trigger specified by the associated controller's + * validateTrigger property occurs. */ -export var ValidateBindingBehavior = (function () { - function ValidateBindingBehavior(taskQueue) { - this.taskQueue = taskQueue; +export var ValidateBindingBehavior = (function (_super) { + __extends(ValidateBindingBehavior, _super); + function ValidateBindingBehavior() { + _super.apply(this, arguments); } - /** - * 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. - */ - ValidateBindingBehavior.prototype.getTarget = function (binding, view) { - var target = binding.target; - // DOM element - if (target instanceof Element) { - return target; - } - // custom element or custom attribute - for (var i = 0, ii = view.controllers.length; i < ii; i++) { - var controller = view.controllers[i]; - if (controller.viewModel === target) { - var element = controller.container.get(DOM.Element); - if (element) { - return element; - } - throw new Error("Unable to locate target element for \"" + binding.sourceExpression + "\"."); - } - } - throw new Error("Unable to locate target element for \"" + binding.sourceExpression + "\"."); - }; - ValidateBindingBehavior.prototype.bind = function (binding, source, rulesOrController, rules) { - var _this = this; - // identify the target element. - var target = this.getTarget(binding, source); - // locate the controller. - var controller; - if (rulesOrController instanceof ValidationController) { - controller = rulesOrController; - } - else { - controller = source.container.get(Optional.of(ValidationController)); - rules = rulesOrController; - } - if (controller === null) { - throw new Error("A ValidationController has not been registered."); - } - controller.registerBinding(binding, target, rules); - binding.validationController = controller; - if (controller.validateTrigger === validateTrigger.change) { - binding.standardUpdateSource = binding.updateSource; - binding.updateSource = function (value) { - this.standardUpdateSource(value); - this.validationController.validateBinding(this); - }; - } - else if (controller.validateTrigger === validateTrigger.blur) { - binding.validateBlurHandler = function () { - _this.taskQueue.queueMicroTask(function () { return controller.validateBinding(binding); }); - }; - binding.validateTarget = target; - target.addEventListener('blur', binding.validateBlurHandler); - } - if (controller.validateTrigger !== validateTrigger.manual) { - binding.standardUpdateTarget = binding.updateTarget; - binding.updateTarget = function (value) { - this.standardUpdateTarget(value); - this.validationController.resetBinding(this); - }; - } - }; - ValidateBindingBehavior.prototype.unbind = function (binding) { - // reset the binding to it's original state. - if (binding.standardUpdateSource) { - binding.updateSource = binding.standardUpdateSource; - binding.standardUpdateSource = null; - } - if (binding.standardUpdateTarget) { - binding.updateTarget = binding.standardUpdateTarget; - binding.standardUpdateTarget = null; - } - if (binding.validateBlurHandler) { - binding.validateTarget.removeEventListener('blur', binding.validateBlurHandler); - binding.validateBlurHandler = null; - binding.validateTarget = null; - } - binding.validationController.unregisterBinding(binding); - binding.validationController = null; + ValidateBindingBehavior.prototype.getValidateTrigger = function (controller) { + return controller.validateTrigger; }; ValidateBindingBehavior.inject = [TaskQueue]; return ValidateBindingBehavior; -}()); +}(ValidateBindingBehaviorBase)); +/** + * Binding behavior. Indicates the bound property will be validated + * manually, by calling controller.validate(). No automatic validation + * triggered by data-entry or blur will occur. + */ +export var ValidateManuallyBindingBehavior = (function (_super) { + __extends(ValidateManuallyBindingBehavior, _super); + function ValidateManuallyBindingBehavior() { + _super.apply(this, arguments); + } + ValidateManuallyBindingBehavior.prototype.getValidateTrigger = function () { + return validateTrigger.manual; + }; + ValidateManuallyBindingBehavior.inject = [TaskQueue]; + return ValidateManuallyBindingBehavior; +}(ValidateBindingBehaviorBase)); +/** + * Binding behavior. Indicates the bound property should be validated + * when the associated element blurs. + */ +export var ValidateOnBlurBindingBehavior = (function (_super) { + __extends(ValidateOnBlurBindingBehavior, _super); + function ValidateOnBlurBindingBehavior() { + _super.apply(this, arguments); + } + ValidateOnBlurBindingBehavior.prototype.getValidateTrigger = function () { + return validateTrigger.blur; + }; + ValidateOnBlurBindingBehavior.inject = [TaskQueue]; + return ValidateOnBlurBindingBehavior; +}(ValidateBindingBehaviorBase)); +/** + * Binding behavior. Indicates the bound property should be validated + * when the associated element is changed by the user, causing a change + * to the model. + */ +export var ValidateOnChangeBindingBehavior = (function (_super) { + __extends(ValidateOnChangeBindingBehavior, _super); + function ValidateOnChangeBindingBehavior() { + _super.apply(this, arguments); + } + ValidateOnChangeBindingBehavior.prototype.getValidateTrigger = function () { + return validateTrigger.change; + }; + ValidateOnChangeBindingBehavior.inject = [TaskQueue]; + return ValidateOnChangeBindingBehavior; +}(ValidateBindingBehaviorBase)); +/** + * Binding behavior. Indicates the bound property should be validated + * when the associated element blurs or is changed by the user, causing + * a change to the model. + */ +export var ValidateOnChangeOrBlurBindingBehavior = (function (_super) { + __extends(ValidateOnChangeOrBlurBindingBehavior, _super); + function ValidateOnChangeOrBlurBindingBehavior() { + _super.apply(this, arguments); + } + ValidateOnChangeOrBlurBindingBehavior.prototype.getValidateTrigger = function () { + return validateTrigger.changeOrBlur; + }; + ValidateOnChangeOrBlurBindingBehavior.inject = [TaskQueue]; + return ValidateOnChangeOrBlurBindingBehavior; +}(ValidateBindingBehaviorBase)); diff --git a/dist/native-modules/validate-trigger.d.ts b/dist/native-modules/validate-trigger.d.ts index 06d80aed..e001bfa3 100644 --- a/dist/native-modules/validate-trigger.d.ts +++ b/dist/native-modules/validate-trigger.d.ts @@ -2,7 +2,8 @@ * Validation triggers. */ export declare const validateTrigger: { - blur: string; - change: string; - manual: string; + manual: number; + blur: number; + change: number; + changeOrBlur: number; }; diff --git a/dist/native-modules/validate-trigger.js b/dist/native-modules/validate-trigger.js index 1906a871..06c3554b 100644 --- a/dist/native-modules/validate-trigger.js +++ b/dist/native-modules/validate-trigger.js @@ -2,18 +2,22 @@ * Validation triggers. */ export var validateTrigger = { + /** + * 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. */ - blur: 'blur', + blur: 1, /** * Validate the binding when it updates the model due to a change in the view. - * Not specific to DOM "change" events. */ - change: 'change', + change: 2, /** - * Manual validation. Use the controller's `validate()` and `reset()` methods - * to validate all bindings. - */ - manual: 'manual' + * Validate the binding when the binding's target element fires a DOM "blur" event and + * when it updates the model due to a change in the view. + */ + changeOrBlur: 3 }; diff --git a/dist/native-modules/validation-controller-factory.d.ts b/dist/native-modules/validation-controller-factory.d.ts index fcb37841..2c51999e 100644 --- a/dist/native-modules/validation-controller-factory.d.ts +++ b/dist/native-modules/validation-controller-factory.d.ts @@ -1,4 +1,6 @@ import { Container } from 'aurelia-dependency-injection'; +import { ValidationController } from './validation-controller'; +import { Validator } from './validator'; /** * Creates ValidationController instances. */ @@ -7,13 +9,12 @@ export declare class ValidationControllerFactory { static get(container: Container): ValidationControllerFactory; constructor(container: Container); /** - * Creates a new controller and registers it in the current element's container so that it's - * available to the validate binding behavior and renderers. + * Creates a new controller instance. */ - create(): any; + create(validator?: Validator): ValidationController; /** * Creates a new controller and registers it in the current element's container so that it's * available to the validate binding behavior and renderers. */ - createForCurrentScope(): any; + createForCurrentScope(validator?: Validator): ValidationController; } diff --git a/dist/native-modules/validation-controller-factory.js b/dist/native-modules/validation-controller-factory.js index a1a55b44..2f18cd72 100644 --- a/dist/native-modules/validation-controller-factory.js +++ b/dist/native-modules/validation-controller-factory.js @@ -1,4 +1,5 @@ import { ValidationController } from './validation-controller'; +import { Validator } from './validator'; /** * Creates ValidationController instances. */ @@ -10,18 +11,20 @@ export var ValidationControllerFactory = (function () { return new ValidationControllerFactory(container); }; /** - * Creates a new controller and registers it in the current element's container so that it's - * available to the validate binding behavior and renderers. + * Creates a new controller instance. */ - ValidationControllerFactory.prototype.create = function () { - return this.container.invoke(ValidationController); + ValidationControllerFactory.prototype.create = function (validator) { + if (!validator) { + validator = this.container.get(Validator); + } + return new ValidationController(validator); }; /** * Creates a new controller and registers it in the current element's container so that it's * available to the validate binding behavior and renderers. */ - ValidationControllerFactory.prototype.createForCurrentScope = function () { - var controller = this.create(); + ValidationControllerFactory.prototype.createForCurrentScope = function (validator) { + var controller = this.create(validator); this.container.registerInstance(ValidationController, controller); return controller; }; diff --git a/dist/native-modules/validation-controller.d.ts b/dist/native-modules/validation-controller.d.ts index 82379694..dcc1c174 100644 --- a/dist/native-modules/validation-controller.d.ts +++ b/dist/native-modules/validation-controller.d.ts @@ -39,7 +39,7 @@ export declare class ValidationController { /** * The trigger that will invoke automatic validation of a property used in a binding. */ - validateTrigger: string; + validateTrigger: number; private finishValidating; constructor(validator: Validator); /** diff --git a/dist/system/implementation/rule.d.ts b/dist/system/implementation/rule.d.ts index 2eb47660..8bba9d76 100644 --- a/dist/system/implementation/rule.d.ts +++ b/dist/system/implementation/rule.d.ts @@ -1,4 +1,7 @@ import { Expression } from 'aurelia-binding'; +/** + * Information related to a property that is the subject of validation. + */ export interface RuleProperty { /** * The property name. null indicates the rule targets the object itself. @@ -9,6 +12,9 @@ export interface RuleProperty { */ displayName: string | null; } +/** + * A rule definition. Associations a rule with a property or object. + */ export interface Rule { property: RuleProperty; condition: (value: TValue, object?: TObject) => boolean | Promise; @@ -18,5 +24,6 @@ export interface Rule { } | null; messageKey: string; message: Expression | null; + sequence: number; tag?: string; } diff --git a/dist/system/implementation/rules.d.ts b/dist/system/implementation/rules.d.ts index 56bd6856..71c46daf 100644 --- a/dist/system/implementation/rules.d.ts +++ b/dist/system/implementation/rules.d.ts @@ -10,7 +10,7 @@ export declare class Rules { /** * Applies the rules to a target. */ - static set(target: any, rules: Rule[]): void; + static set(target: any, rules: Rule[][]): void; /** * Removes rules from a target. */ @@ -18,5 +18,5 @@ export declare class Rules { /** * Retrieves the target's rules. */ - static get(target: any): Rule[] | null; + static get(target: any): Rule[][] | null; } diff --git a/dist/system/implementation/standard-validator.d.ts b/dist/system/implementation/standard-validator.d.ts index d17f819b..e05e7b8e 100644 --- a/dist/system/implementation/standard-validator.d.ts +++ b/dist/system/implementation/standard-validator.d.ts @@ -13,6 +13,7 @@ export declare class StandardValidator extends Validator { 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. diff --git a/dist/system/implementation/standard-validator.js b/dist/system/implementation/standard-validator.js index 714a3de9..a7d85a9d 100644 --- a/dist/system/implementation/standard-validator.js +++ b/dist/system/implementation/standard-validator.js @@ -54,24 +54,12 @@ System.register(['aurelia-templating', '../validator', '../validation-error', '. }; return expression.evaluate({ bindingContext: object, overrideContext: overrideContext }, this.lookupFunctions); }; - StandardValidator.prototype.validate = function (object, propertyName, rules) { + StandardValidator.prototype.validateRuleSequence = function (object, propertyName, ruleSequence, sequence) { var _this = this; - var errors = []; - // rules specified? - if (!rules) { - // no. locate the rules via metadata. - rules = rules_1.Rules.get(object); - } - // any rules? - if (!rules) { - return Promise.resolve(errors); - } // are we validating all properties or a single property? var validateAllProperties = propertyName === null || propertyName === undefined; - var addError = function (rule, value) { - var message = _this.getMessage(rule, object, value); - errors.push(new validation_error_1.ValidationError(rule, message, object, rule.property.name)); - }; + var rules = ruleSequence[sequence]; + var errors = []; // validate each rule. var promises = []; var _loop_1 = function(i) { @@ -87,25 +75,39 @@ System.register(['aurelia-templating', '../validator', '../validation-error', '. // validate. var value = rule.property.name === null ? object : object[rule.property.name]; var promiseOrBoolean = rule.condition(value, object); - if (promiseOrBoolean instanceof Promise) { - promises.push(promiseOrBoolean.then(function (isValid) { - if (!isValid) { - addError(rule, value); - } - })); - return "continue"; - } - if (!promiseOrBoolean) { - addError(rule, value); + if (!(promiseOrBoolean instanceof Promise)) { + promiseOrBoolean = Promise.resolve(promiseOrBoolean); } + promises.push(promiseOrBoolean.then(function (isValid) { + if (!isValid) { + var message = _this.getMessage(rule, object, value); + errors.push(new validation_error_1.ValidationError(rule, message, object, rule.property.name)); + } + })); }; for (var i = 0; i < rules.length; i++) { _loop_1(i); } - if (promises.length === 0) { - return Promise.resolve(errors); + return Promise.all(promises) + .then(function () { + sequence++; + if (errors.length === 0 && sequence < ruleSequence.length) { + return _this.validateRuleSequence(object, propertyName, ruleSequence, sequence); + } + return errors; + }); + }; + StandardValidator.prototype.validate = function (object, propertyName, rules) { + // rules specified? + if (!rules) { + // no. attempt to locate the rules. + rules = rules_1.Rules.get(object); + } + // any rules? + if (!rules) { + return Promise.resolve([]); } - return Promise.all(promises).then(function () { return errors; }); + return this.validateRuleSequence(object, propertyName, rules, 0); }; /** * Validates the specified property. diff --git a/dist/system/implementation/validation-parser.js b/dist/system/implementation/validation-parser.js index 730bfd46..95033a48 100644 --- a/dist/system/implementation/validation-parser.js +++ b/dist/system/implementation/validation-parser.js @@ -62,13 +62,10 @@ System.register(['aurelia-binding', 'aurelia-templating', './util', 'aurelia-log return this.parser.parse(match[1]); }; ValidationParser.prototype.parseProperty = function (property) { - var accessor; if (util_1.isString(property)) { - accessor = this.parser.parse(property); - } - else { - accessor = this.getAccessorExpression(property.toString()); + 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) { return { diff --git a/dist/system/implementation/validation-rules.d.ts b/dist/system/implementation/validation-rules.d.ts index 997065f7..1bd93972 100644 --- a/dist/system/implementation/validation-rules.d.ts +++ b/dist/system/implementation/validation-rules.d.ts @@ -9,6 +9,12 @@ export declare class FluentRuleCustomizer { private parser; private rule; constructor(property: RuleProperty, condition: (value: TValue, object?: TObject) => boolean | Promise, config: Object, fluentEnsure: FluentEnsure, fluentRules: FluentRules, parser: ValidationParser); + /** + * Validate subsequent rules after previously declared rules have + * been validated successfully. Use to postpone validation of costly + * rules until less expensive rules pass validation. + */ + then(): this; /** * Specifies the key to use when looking up the rule's validation message. */ @@ -42,7 +48,7 @@ export declare class FluentRuleCustomizer { /** * Rules that have been defined using the fluent API. */ - readonly rules: Rule[]; + readonly rules: Rule[][]; /** * Applies the rules to a class or object, making them discoverable by the StandardValidator. * @param target A class or object. @@ -184,7 +190,8 @@ export declare class FluentEnsure { /** * Rules that have been defined using the fluent API. */ - rules: Rule[]; + rules: Rule[][]; + _sequence: number; constructor(parser: ValidationParser); /** * Target a property with validation rules. @@ -200,6 +207,10 @@ export declare class FluentEnsure { * @param target A class or object. */ on(target: any): this; + /** + * Adds a rule definition to the sequenced ruleset. + */ + _addRule(rule: Rule): void; private assertInitialized(); } /** @@ -230,7 +241,7 @@ export declare class ValidationRules { * @param rules The rules to search. * @param tag The tag to search for. */ - static taggedRules(rules: Rule[], tag: string): Rule[]; + static taggedRules(rules: Rule[][], tag: string): Rule[][]; /** * Removes the rules from a class or object. * @param target A class or object. diff --git a/dist/system/implementation/validation-rules.js b/dist/system/implementation/validation-rules.js index 0e53b7d6..0e06eab0 100644 --- a/dist/system/implementation/validation-rules.js +++ b/dist/system/implementation/validation-rules.js @@ -30,10 +30,20 @@ System.register(['./util', './rules', './validation-messages'], function(exports config: config, when: null, messageKey: 'default', - message: null + message: null, + sequence: fluentEnsure._sequence }; - this.fluentEnsure.rules.push(this.rule); + this.fluentEnsure._addRule(this.rule); } + /** + * Validate subsequent rules after previously declared rules have + * been validated successfully. Use to postpone validation of costly + * rules until less expensive rules pass validation. + */ + FluentRuleCustomizer.prototype.then = function () { + this.fluentEnsure._sequence++; + return this; + }; /** * Specifies the key to use when looking up the rule's validation message. */ @@ -255,7 +265,7 @@ 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])))$/) + 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) .withMessageKey('email'); }; /** @@ -312,6 +322,7 @@ System.register(['./util', './rules', './validation-messages'], function(exports * Rules that have been defined using the fluent API. */ this.rules = []; + this._sequence = 0; } /** * Target a property with validation rules. @@ -336,6 +347,15 @@ System.register(['./util', './rules', './validation-messages'], function(exports rules_1.Rules.set(target, this.rules); return this; }; + /** + * Adds a rule definition to the sequenced ruleset. + */ + FluentEnsure.prototype._addRule = function (rule) { + while (this.rules.length < rule.sequence + 1) { + this.rules.push([]); + } + this.rules[rule.sequence].push(rule); + }; FluentEnsure.prototype.assertInitialized = function () { if (this.parser) { return; @@ -384,7 +404,7 @@ System.register(['./util', './rules', './validation-messages'], function(exports * @param tag The tag to search for. */ ValidationRules.taggedRules = function (rules, tag) { - return rules.filter(function (r) { return r.tag === tag; }); + return rules.map(function (x) { return x.filter(function (r) { return r.tag === tag; }); }); }; /** * Removes the rules from a class or object. diff --git a/dist/system/validate-binding-behavior-base.d.ts b/dist/system/validate-binding-behavior-base.d.ts new file mode 100644 index 00000000..cb4eb912 --- /dev/null +++ b/dist/system/validate-binding-behavior-base.d.ts @@ -0,0 +1,19 @@ +import { TaskQueue } from 'aurelia-task-queue'; +import { ValidationController } from './validation-controller'; +/** + * Binding behavior. Indicates the bound property should be validated. + */ +export declare abstract class ValidateBindingBehaviorBase { + private taskQueue; + 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. + */ + 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 new file mode 100644 index 00000000..d05523de --- /dev/null +++ b/dist/system/validate-binding-behavior-base.js @@ -0,0 +1,117 @@ +System.register(['aurelia-dependency-injection', 'aurelia-pal', './validation-controller', './validate-trigger'], function(exports_1, context_1) { + "use strict"; + var __moduleName = context_1 && context_1.id; + var aurelia_dependency_injection_1, aurelia_pal_1, validation_controller_1, validate_trigger_1; + var ValidateBindingBehaviorBase; + return { + setters:[ + function (aurelia_dependency_injection_1_1) { + aurelia_dependency_injection_1 = aurelia_dependency_injection_1_1; + }, + function (aurelia_pal_1_1) { + aurelia_pal_1 = aurelia_pal_1_1; + }, + function (validation_controller_1_1) { + validation_controller_1 = validation_controller_1_1; + }, + function (validate_trigger_1_1) { + validate_trigger_1 = validate_trigger_1_1; + }], + execute: function() { + /** + * Binding behavior. Indicates the bound property should be validated. + */ + ValidateBindingBehaviorBase = (function () { + function ValidateBindingBehaviorBase(taskQueue) { + 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. + */ + ValidateBindingBehaviorBase.prototype.getTarget = function (binding, view) { + var target = binding.target; + // DOM element + if (target instanceof Element) { + return target; + } + // custom element or custom attribute + for (var i = 0, ii = view.controllers.length; i < ii; i++) { + var controller = view.controllers[i]; + if (controller.viewModel === target) { + var element = controller.container.get(aurelia_pal_1.DOM.Element); + if (element) { + return element; + } + throw new Error("Unable to locate target element for \"" + binding.sourceExpression + "\"."); + } + } + throw new Error("Unable to locate target element for \"" + binding.sourceExpression + "\"."); + }; + ValidateBindingBehaviorBase.prototype.bind = function (binding, source, rulesOrController, rules) { + var _this = this; + // identify the target element. + var target = this.getTarget(binding, source); + // locate the controller. + var controller; + if (rulesOrController instanceof validation_controller_1.ValidationController) { + controller = rulesOrController; + } + else { + controller = source.container.get(aurelia_dependency_injection_1.Optional.of(validation_controller_1.ValidationController)); + rules = rulesOrController; + } + if (controller === null) { + throw new Error("A ValidationController has not been registered."); + } + controller.registerBinding(binding, target, rules); + binding.validationController = controller; + var trigger = this.getValidateTrigger(controller); + if (trigger & validate_trigger_1.validateTrigger.change) { + binding.standardUpdateSource = binding.updateSource; + binding.updateSource = function (value) { + this.standardUpdateSource(value); + this.validationController.validateBinding(this); + }; + } + if (trigger & validate_trigger_1.validateTrigger.blur) { + binding.validateBlurHandler = function () { + _this.taskQueue.queueMicroTask(function () { return controller.validateBinding(binding); }); + }; + binding.validateTarget = target; + target.addEventListener('blur', binding.validateBlurHandler); + } + if (trigger !== validate_trigger_1.validateTrigger.manual) { + binding.standardUpdateTarget = binding.updateTarget; + binding.updateTarget = function (value) { + this.standardUpdateTarget(value); + this.validationController.resetBinding(this); + }; + } + }; + ValidateBindingBehaviorBase.prototype.unbind = function (binding) { + // reset the binding to it's original state. + if (binding.standardUpdateSource) { + binding.updateSource = binding.standardUpdateSource; + binding.standardUpdateSource = null; + } + if (binding.standardUpdateTarget) { + binding.updateTarget = binding.standardUpdateTarget; + binding.standardUpdateTarget = null; + } + if (binding.validateBlurHandler) { + binding.validateTarget.removeEventListener('blur', binding.validateBlurHandler); + binding.validateBlurHandler = null; + binding.validateTarget = null; + } + binding.validationController.unregisterBinding(binding); + binding.validationController = null; + }; + return ValidateBindingBehaviorBase; + }()); + exports_1("ValidateBindingBehaviorBase", ValidateBindingBehaviorBase); + } + } +}); diff --git a/dist/system/validate-binding-behavior.d.ts b/dist/system/validate-binding-behavior.d.ts index a846db99..2d55c7ec 100644 --- a/dist/system/validate-binding-behavior.d.ts +++ b/dist/system/validate-binding-behavior.d.ts @@ -1,19 +1,47 @@ import { TaskQueue } from 'aurelia-task-queue'; import { ValidationController } from './validation-controller'; +import { ValidateBindingBehaviorBase } from './validate-binding-behavior-base'; /** - * Binding behavior. Indicates the bound property should be validated. + * Binding behavior. Indicates the bound property should be validated + * when the validate trigger specified by the associated controller's + * validateTrigger property occurs. */ -export declare class ValidateBindingBehavior { - private taskQueue; +export declare class ValidateBindingBehavior extends ValidateBindingBehaviorBase { static inject: typeof TaskQueue[]; - constructor(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. - */ - getTarget(binding: any, view: any): any; - bind(binding: any, source: any, rulesOrController?: ValidationController | any, rules?: any): void; - unbind(binding: any): void; + getValidateTrigger(controller: ValidationController): number; +} +/** + * Binding behavior. Indicates the bound property will be validated + * manually, by calling controller.validate(). No automatic validation + * triggered by data-entry or blur will occur. + */ +export declare class ValidateManuallyBindingBehavior extends ValidateBindingBehaviorBase { + static inject: typeof TaskQueue[]; + getValidateTrigger(): number; +} +/** + * Binding behavior. Indicates the bound property should be validated + * when the associated element blurs. + */ +export declare class ValidateOnBlurBindingBehavior extends ValidateBindingBehaviorBase { + static inject: typeof TaskQueue[]; + getValidateTrigger(): number; +} +/** + * Binding behavior. Indicates the bound property should be validated + * when the associated element is changed by the user, causing a change + * to the model. + */ +export declare class ValidateOnChangeBindingBehavior extends ValidateBindingBehaviorBase { + static inject: typeof TaskQueue[]; + getValidateTrigger(): number; +} +/** + * Binding behavior. Indicates the bound property should be validated + * when the associated element blurs or is changed by the user, causing + * a change to the model. + */ +export declare class ValidateOnChangeOrBlurBindingBehavior extends ValidateBindingBehaviorBase { + static inject: typeof TaskQueue[]; + getValidateTrigger(): number; } diff --git a/dist/system/validate-binding-behavior.js b/dist/system/validate-binding-behavior.js index 2781e88f..0d73a34c 100644 --- a/dist/system/validate-binding-behavior.js +++ b/dist/system/validate-binding-behavior.js @@ -1,120 +1,109 @@ -System.register(['aurelia-dependency-injection', 'aurelia-pal', 'aurelia-task-queue', './validation-controller', './validate-trigger'], function(exports_1, context_1) { +System.register(['aurelia-task-queue', './validate-trigger', './validate-binding-behavior-base'], function(exports_1, context_1) { "use strict"; var __moduleName = context_1 && context_1.id; - var aurelia_dependency_injection_1, aurelia_pal_1, aurelia_task_queue_1, validation_controller_1, validate_trigger_1; - var ValidateBindingBehavior; + var __extends = (this && this.__extends) || function (d, b) { + for (var p in b) if (b.hasOwnProperty(p)) d[p] = b[p]; + function __() { this.constructor = d; } + d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __()); + }; + var aurelia_task_queue_1, validate_trigger_1, validate_binding_behavior_base_1; + var ValidateBindingBehavior, ValidateManuallyBindingBehavior, ValidateOnBlurBindingBehavior, ValidateOnChangeBindingBehavior, ValidateOnChangeOrBlurBindingBehavior; return { setters:[ - function (aurelia_dependency_injection_1_1) { - aurelia_dependency_injection_1 = aurelia_dependency_injection_1_1; - }, - function (aurelia_pal_1_1) { - aurelia_pal_1 = aurelia_pal_1_1; - }, function (aurelia_task_queue_1_1) { aurelia_task_queue_1 = aurelia_task_queue_1_1; }, - function (validation_controller_1_1) { - validation_controller_1 = validation_controller_1_1; - }, function (validate_trigger_1_1) { validate_trigger_1 = validate_trigger_1_1; + }, + function (validate_binding_behavior_base_1_1) { + validate_binding_behavior_base_1 = validate_binding_behavior_base_1_1; }], execute: function() { /** - * Binding behavior. Indicates the bound property should be validated. + * Binding behavior. Indicates the bound property should be validated + * when the validate trigger specified by the associated controller's + * validateTrigger property occurs. */ - ValidateBindingBehavior = (function () { - function ValidateBindingBehavior(taskQueue) { - this.taskQueue = taskQueue; + ValidateBindingBehavior = (function (_super) { + __extends(ValidateBindingBehavior, _super); + function ValidateBindingBehavior() { + _super.apply(this, arguments); } - /** - * 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. - */ - ValidateBindingBehavior.prototype.getTarget = function (binding, view) { - var target = binding.target; - // DOM element - if (target instanceof Element) { - return target; - } - // custom element or custom attribute - for (var i = 0, ii = view.controllers.length; i < ii; i++) { - var controller = view.controllers[i]; - if (controller.viewModel === target) { - var element = controller.container.get(aurelia_pal_1.DOM.Element); - if (element) { - return element; - } - throw new Error("Unable to locate target element for \"" + binding.sourceExpression + "\"."); - } - } - throw new Error("Unable to locate target element for \"" + binding.sourceExpression + "\"."); - }; - ValidateBindingBehavior.prototype.bind = function (binding, source, rulesOrController, rules) { - var _this = this; - // identify the target element. - var target = this.getTarget(binding, source); - // locate the controller. - var controller; - if (rulesOrController instanceof validation_controller_1.ValidationController) { - controller = rulesOrController; - } - else { - controller = source.container.get(aurelia_dependency_injection_1.Optional.of(validation_controller_1.ValidationController)); - rules = rulesOrController; - } - if (controller === null) { - throw new Error("A ValidationController has not been registered."); - } - controller.registerBinding(binding, target, rules); - binding.validationController = controller; - if (controller.validateTrigger === validate_trigger_1.validateTrigger.change) { - binding.standardUpdateSource = binding.updateSource; - binding.updateSource = function (value) { - this.standardUpdateSource(value); - this.validationController.validateBinding(this); - }; - } - else if (controller.validateTrigger === validate_trigger_1.validateTrigger.blur) { - binding.validateBlurHandler = function () { - _this.taskQueue.queueMicroTask(function () { return controller.validateBinding(binding); }); - }; - binding.validateTarget = target; - target.addEventListener('blur', binding.validateBlurHandler); - } - if (controller.validateTrigger !== validate_trigger_1.validateTrigger.manual) { - binding.standardUpdateTarget = binding.updateTarget; - binding.updateTarget = function (value) { - this.standardUpdateTarget(value); - this.validationController.resetBinding(this); - }; - } - }; - ValidateBindingBehavior.prototype.unbind = function (binding) { - // reset the binding to it's original state. - if (binding.standardUpdateSource) { - binding.updateSource = binding.standardUpdateSource; - binding.standardUpdateSource = null; - } - if (binding.standardUpdateTarget) { - binding.updateTarget = binding.standardUpdateTarget; - binding.standardUpdateTarget = null; - } - if (binding.validateBlurHandler) { - binding.validateTarget.removeEventListener('blur', binding.validateBlurHandler); - binding.validateBlurHandler = null; - binding.validateTarget = null; - } - binding.validationController.unregisterBinding(binding); - binding.validationController = null; + ValidateBindingBehavior.prototype.getValidateTrigger = function (controller) { + return controller.validateTrigger; }; ValidateBindingBehavior.inject = [aurelia_task_queue_1.TaskQueue]; return ValidateBindingBehavior; - }()); + }(validate_binding_behavior_base_1.ValidateBindingBehaviorBase)); exports_1("ValidateBindingBehavior", ValidateBindingBehavior); + /** + * Binding behavior. Indicates the bound property will be validated + * manually, by calling controller.validate(). No automatic validation + * triggered by data-entry or blur will occur. + */ + ValidateManuallyBindingBehavior = (function (_super) { + __extends(ValidateManuallyBindingBehavior, _super); + function ValidateManuallyBindingBehavior() { + _super.apply(this, arguments); + } + ValidateManuallyBindingBehavior.prototype.getValidateTrigger = function () { + return validate_trigger_1.validateTrigger.manual; + }; + ValidateManuallyBindingBehavior.inject = [aurelia_task_queue_1.TaskQueue]; + return ValidateManuallyBindingBehavior; + }(validate_binding_behavior_base_1.ValidateBindingBehaviorBase)); + exports_1("ValidateManuallyBindingBehavior", ValidateManuallyBindingBehavior); + /** + * Binding behavior. Indicates the bound property should be validated + * when the associated element blurs. + */ + ValidateOnBlurBindingBehavior = (function (_super) { + __extends(ValidateOnBlurBindingBehavior, _super); + function ValidateOnBlurBindingBehavior() { + _super.apply(this, arguments); + } + ValidateOnBlurBindingBehavior.prototype.getValidateTrigger = function () { + return validate_trigger_1.validateTrigger.blur; + }; + ValidateOnBlurBindingBehavior.inject = [aurelia_task_queue_1.TaskQueue]; + return ValidateOnBlurBindingBehavior; + }(validate_binding_behavior_base_1.ValidateBindingBehaviorBase)); + exports_1("ValidateOnBlurBindingBehavior", ValidateOnBlurBindingBehavior); + /** + * Binding behavior. Indicates the bound property should be validated + * when the associated element is changed by the user, causing a change + * to the model. + */ + ValidateOnChangeBindingBehavior = (function (_super) { + __extends(ValidateOnChangeBindingBehavior, _super); + function ValidateOnChangeBindingBehavior() { + _super.apply(this, arguments); + } + ValidateOnChangeBindingBehavior.prototype.getValidateTrigger = function () { + return validate_trigger_1.validateTrigger.change; + }; + ValidateOnChangeBindingBehavior.inject = [aurelia_task_queue_1.TaskQueue]; + return ValidateOnChangeBindingBehavior; + }(validate_binding_behavior_base_1.ValidateBindingBehaviorBase)); + exports_1("ValidateOnChangeBindingBehavior", ValidateOnChangeBindingBehavior); + /** + * Binding behavior. Indicates the bound property should be validated + * when the associated element blurs or is changed by the user, causing + * a change to the model. + */ + ValidateOnChangeOrBlurBindingBehavior = (function (_super) { + __extends(ValidateOnChangeOrBlurBindingBehavior, _super); + function ValidateOnChangeOrBlurBindingBehavior() { + _super.apply(this, arguments); + } + ValidateOnChangeOrBlurBindingBehavior.prototype.getValidateTrigger = function () { + return validate_trigger_1.validateTrigger.changeOrBlur; + }; + ValidateOnChangeOrBlurBindingBehavior.inject = [aurelia_task_queue_1.TaskQueue]; + return ValidateOnChangeOrBlurBindingBehavior; + }(validate_binding_behavior_base_1.ValidateBindingBehaviorBase)); + exports_1("ValidateOnChangeOrBlurBindingBehavior", ValidateOnChangeOrBlurBindingBehavior); } } }); diff --git a/dist/system/validate-trigger.d.ts b/dist/system/validate-trigger.d.ts index 06d80aed..e001bfa3 100644 --- a/dist/system/validate-trigger.d.ts +++ b/dist/system/validate-trigger.d.ts @@ -2,7 +2,8 @@ * Validation triggers. */ export declare const validateTrigger: { - blur: string; - change: string; - manual: string; + manual: number; + blur: number; + change: number; + changeOrBlur: number; }; diff --git a/dist/system/validate-trigger.js b/dist/system/validate-trigger.js index fc7dd28a..f3598e2c 100644 --- a/dist/system/validate-trigger.js +++ b/dist/system/validate-trigger.js @@ -9,20 +9,24 @@ System.register([], function(exports_1, context_1) { * Validation triggers. */ exports_1("validateTrigger", validateTrigger = { + /** + * 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. */ - blur: 'blur', + blur: 1, /** * Validate the binding when it updates the model due to a change in the view. - * Not specific to DOM "change" events. */ - change: 'change', + change: 2, /** - * Manual validation. Use the controller's `validate()` and `reset()` methods - * to validate all bindings. - */ - manual: 'manual' + * Validate the binding when the binding's target element fires a DOM "blur" event and + * when it updates the model due to a change in the view. + */ + changeOrBlur: 3 }); } } diff --git a/dist/system/validation-controller-factory.d.ts b/dist/system/validation-controller-factory.d.ts index fcb37841..2c51999e 100644 --- a/dist/system/validation-controller-factory.d.ts +++ b/dist/system/validation-controller-factory.d.ts @@ -1,4 +1,6 @@ import { Container } from 'aurelia-dependency-injection'; +import { ValidationController } from './validation-controller'; +import { Validator } from './validator'; /** * Creates ValidationController instances. */ @@ -7,13 +9,12 @@ export declare class ValidationControllerFactory { static get(container: Container): ValidationControllerFactory; constructor(container: Container); /** - * Creates a new controller and registers it in the current element's container so that it's - * available to the validate binding behavior and renderers. + * Creates a new controller instance. */ - create(): any; + create(validator?: Validator): ValidationController; /** * Creates a new controller and registers it in the current element's container so that it's * available to the validate binding behavior and renderers. */ - createForCurrentScope(): any; + createForCurrentScope(validator?: Validator): ValidationController; } diff --git a/dist/system/validation-controller-factory.js b/dist/system/validation-controller-factory.js index 0e20863c..70adaa85 100644 --- a/dist/system/validation-controller-factory.js +++ b/dist/system/validation-controller-factory.js @@ -1,12 +1,15 @@ -System.register(['./validation-controller'], function(exports_1, context_1) { +System.register(['./validation-controller', './validator'], function(exports_1, context_1) { "use strict"; var __moduleName = context_1 && context_1.id; - var validation_controller_1; + var validation_controller_1, validator_1; var ValidationControllerFactory; return { setters:[ function (validation_controller_1_1) { validation_controller_1 = validation_controller_1_1; + }, + function (validator_1_1) { + validator_1 = validator_1_1; }], execute: function() { /** @@ -20,18 +23,20 @@ System.register(['./validation-controller'], function(exports_1, context_1) { return new ValidationControllerFactory(container); }; /** - * Creates a new controller and registers it in the current element's container so that it's - * available to the validate binding behavior and renderers. + * Creates a new controller instance. */ - ValidationControllerFactory.prototype.create = function () { - return this.container.invoke(validation_controller_1.ValidationController); + ValidationControllerFactory.prototype.create = function (validator) { + if (!validator) { + validator = this.container.get(validator_1.Validator); + } + return new validation_controller_1.ValidationController(validator); }; /** * Creates a new controller and registers it in the current element's container so that it's * available to the validate binding behavior and renderers. */ - ValidationControllerFactory.prototype.createForCurrentScope = function () { - var controller = this.create(); + ValidationControllerFactory.prototype.createForCurrentScope = function (validator) { + var controller = this.create(validator); this.container.registerInstance(validation_controller_1.ValidationController, controller); return controller; }; diff --git a/dist/system/validation-controller.d.ts b/dist/system/validation-controller.d.ts index 82379694..dcc1c174 100644 --- a/dist/system/validation-controller.d.ts +++ b/dist/system/validation-controller.d.ts @@ -39,7 +39,7 @@ export declare class ValidationController { /** * The trigger that will invoke automatic validation of a property used in a binding. */ - validateTrigger: string; + validateTrigger: number; private finishValidating; constructor(validator: Validator); /** diff --git a/package.json b/package.json index ca7d2a0b..2ae26ec2 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "aurelia-validation", - "version": "0.12.5", + "version": "0.13.0", "description": "Validation for Aurelia applications", "keywords": [ "aurelia",