From b47cbf5559eca9f009faf9a077c57ed7d077ac23 Mon Sep 17 00:00:00 2001 From: Rob Eisenberg Date: Wed, 18 Dec 2019 12:18:44 -0800 Subject: [PATCH] chore(all): prepare release --- dist/amd/aurelia-validation.js | 2232 ++++++++++---------- dist/aurelia-validation.d.ts | 170 +- dist/commonjs/aurelia-validation.js | 2240 ++++++++++---------- dist/es2015/aurelia-validation.js | 2110 +++++++++---------- dist/es2017/aurelia-validation.js | 2110 +++++++++---------- dist/native-modules/aurelia-validation.js | 2254 ++++++++++---------- dist/system/aurelia-validation.js | 2282 +++++++++++---------- dist/umd-es2015/aurelia-validation.js | 2118 +++++++++---------- dist/umd/aurelia-validation.js | 2238 ++++++++++---------- 9 files changed, 8941 insertions(+), 8813 deletions(-) diff --git a/dist/amd/aurelia-validation.js b/dist/amd/aurelia-validation.js index 8440d9ca..db769a8f 100644 --- a/dist/amd/aurelia-validation.js +++ b/dist/amd/aurelia-validation.js @@ -1,110 +1,13 @@ -define('aurelia-validation', ['exports', 'aurelia-pal', 'aurelia-binding', 'aurelia-dependency-injection', 'aurelia-task-queue', 'aurelia-templating', 'aurelia-logging'], function (exports, aureliaPal, aureliaBinding, aureliaDependencyInjection, aureliaTaskQueue, aureliaTemplating, LogManager) { 'use strict'; +define('aurelia-validation', ['exports', 'aurelia-binding', 'aurelia-templating', 'aurelia-logging', 'aurelia-pal', 'aurelia-dependency-injection', 'aurelia-task-queue'], function (exports, aureliaBinding, aureliaTemplating, LogManager, aureliaPal, aureliaDependencyInjection, aureliaTaskQueue) { 'use strict'; /** - * 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. - */ - function getTargetDOMElement(binding, view) { - var target = binding.target; - // DOM element - if (target instanceof Element) { - return target; - } - // custom element or custom attribute - // tslint:disable-next-line:prefer-const - 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(aureliaPal.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 + "\"."); - } - - function getObject(expression, objectExpression, source) { - var value = objectExpression.evaluate(source, null); - if (value === null || value === undefined || value instanceof Object) { - return value; - } - // tslint:disable-next-line:max-line-length - throw new Error("The '" + objectExpression + "' part of '" + expression + "' evaluates to " + value + " instead of an object, null or undefined."); - } - /** - * Retrieves the object and property name for the specified expression. - * @param expression The expression - * @param source The scope + * Validates objects and properties. */ - function getPropertyInfo(expression, source) { - var originalExpression = expression; - while (expression instanceof aureliaBinding.BindingBehavior || expression instanceof aureliaBinding.ValueConverter) { - expression = expression.expression; - } - var object; - var propertyName; - if (expression instanceof aureliaBinding.AccessScope) { - object = aureliaBinding.getContextFor(expression.name, source, expression.ancestor); - propertyName = expression.name; - } - else if (expression instanceof aureliaBinding.AccessMember) { - object = getObject(originalExpression, expression.object, source); - propertyName = expression.name; - } - else if (expression instanceof aureliaBinding.AccessKeyed) { - object = getObject(originalExpression, expression.object, source); - propertyName = expression.key.evaluate(source); - } - else { - throw new Error("Expression '" + originalExpression + "' is not compatible with the validate binding-behavior."); - } - if (object === null || object === undefined) { - return null; - } - return { object: object, propertyName: propertyName }; - } - - function isString(value) { - return Object.prototype.toString.call(value) === '[object String]'; - } - function isNumber(value) { - return Object.prototype.toString.call(value) === '[object Number]'; - } - - var PropertyAccessorParser = /** @class */ (function () { - function PropertyAccessorParser(parser) { - this.parser = parser; - } - PropertyAccessorParser.prototype.parse = function (property) { - if (isString(property) || isNumber(property)) { - return property; - } - var accessorText = getAccessorExpression(property.toString()); - var accessor = this.parser.parse(accessorText); - if (accessor instanceof aureliaBinding.AccessScope - || accessor instanceof aureliaBinding.AccessMember && accessor.object instanceof aureliaBinding.AccessScope) { - return accessor.name; - } - throw new Error("Invalid property expression: \"" + accessor + "\""); - }; - PropertyAccessorParser.inject = [aureliaBinding.Parser]; - return PropertyAccessorParser; - }()); - function getAccessorExpression(fn) { - /* tslint:disable:max-line-length */ - var classic = /^function\s*\([$_\w\d]+\)\s*\{(?:\s*"use strict";)?\s*(?:[$_\w\d.['"\]+;]+)?\s*return\s+[$_\w\d]+\.([$_\w\d]+)\s*;?\s*\}$/; - /* tslint:enable:max-line-length */ - var arrow = /^\(?[$_\w\d]+\)?\s*=>\s*[$_\w\d]+\.([$_\w\d]+)$/; - var match = classic.exec(fn) || arrow.exec(fn); - if (match === null) { - throw new Error("Unable to parse accessor function:\n" + fn); + var Validator = /** @class */ (function () { + function Validator() { } - return match[1]; - } + return Validator; + }()); /*! ***************************************************************************** Copyright (c) Microsoft Corporation. All rights reserved. @@ -140,41 +43,16 @@ define('aurelia-validation', ['exports', 'aurelia-pal', 'aurelia-binding', 'aure if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc); else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r; return c > 3 && r && Object.defineProperty(target, key, r), r; + } + + function __spreadArrays() { + for (var s = 0, i = 0, il = arguments.length; i < il; i++) s += arguments[i].length; + for (var r = Array(s), k = 0, i = 0; i < il; i++) + for (var a = arguments[i], j = 0, jl = a.length; j < jl; j++, k++) + r[k] = a[j]; + return r; } - /** - * Validation triggers. - */ - (function (validateTrigger) { - /** - * Manual validation. Use the controller's `validate()` and `reset()` methods - * to validate all bindings. - */ - validateTrigger[validateTrigger["manual"] = 0] = "manual"; - /** - * Validate the binding when the binding's target element fires a DOM "blur" event. - */ - validateTrigger[validateTrigger["blur"] = 1] = "blur"; - /** - * Validate the binding when it updates the model due to a change in the view. - */ - validateTrigger[validateTrigger["change"] = 2] = "change"; - /** - * 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. - */ - validateTrigger[validateTrigger["changeOrBlur"] = 3] = "changeOrBlur"; - })(exports.validateTrigger || (exports.validateTrigger = {})); - - /** - * Validates objects and properties. - */ - var Validator = /** @class */ (function () { - function Validator() { - } - return Validator; - }()); - /** * The result of validating an individual validation rule. */ @@ -201,1102 +79,1266 @@ define('aurelia-validation', ['exports', 'aurelia-pal', 'aurelia-binding', 'aure return ValidateResult; }()); - var ValidateEvent = /** @class */ (function () { - function ValidateEvent( - /** - * The type of validate event. Either "validate" or "reset". - */ - type, - /** - * The controller's current array of errors. For an array containing both - * failed rules and passed rules, use the "results" property. - */ - errors, - /** - * The controller's current array of validate results. This - * includes both passed rules and failed rules. For an array of only failed rules, - * use the "errors" property. - */ - results, - /** - * The instruction passed to the "validate" or "reset" event. Will be null when - * the controller's validate/reset method was called with no instruction argument. - */ - instruction, - /** - * In events with type === "validate", this property will contain the result - * of validating the instruction (see "instruction" property). Use the controllerValidateResult - * to access the validate results specific to the call to "validate" - * (as opposed to using the "results" and "errors" properties to access the controller's entire - * set of results/errors). - */ - controllerValidateResult) { - this.type = type; - this.errors = errors; - this.results = results; - this.instruction = instruction; - this.controllerValidateResult = controllerValidateResult; - } - return ValidateEvent; - }()); - /** - * Orchestrates validation. - * Manages a set of bindings, renderers and objects. - * Exposes the current list of validation results for binding purposes. + * Sets, unsets and retrieves rules on an object or constructor function. */ - var ValidationController = /** @class */ (function () { - function ValidationController(validator, propertyParser) { - this.validator = validator; - this.propertyParser = propertyParser; - // Registered bindings (via the validate binding behavior) - this.bindings = new Map(); - // Renderers that have been added to the controller instance. - this.renderers = []; - /** - * Validation results that have been rendered by the controller. - */ - this.results = []; - /** - * Validation errors that have been rendered by the controller. - */ - this.errors = []; - /** - * Whether the controller is currently validating. - */ - this.validating = false; - // Elements related to validation results that have been rendered. - this.elements = new Map(); - // Objects that have been added to the controller instance (entity-style validation). - this.objects = new Map(); - /** - * The trigger that will invoke automatic validation of a property used in a binding. - */ - this.validateTrigger = exports.validateTrigger.blur; - // Promise that resolves when validation has completed. - this.finishValidating = Promise.resolve(); - this.eventCallbacks = []; + var Rules = /** @class */ (function () { + function Rules() { } /** - * Subscribe to controller validate and reset events. These events occur when the - * controller's "validate"" and "reset" methods are called. - * @param callback The callback to be invoked when the controller validates or resets. + * Applies the rules to a target. */ - ValidationController.prototype.subscribe = function (callback) { - var _this = this; - this.eventCallbacks.push(callback); - return { - dispose: function () { - var index = _this.eventCallbacks.indexOf(callback); - if (index === -1) { - return; - } - _this.eventCallbacks.splice(index, 1); - } - }; + Rules.set = function (target, rules) { + if (target instanceof Function) { + target = target.prototype; + } + Object.defineProperty(target, Rules.key, { enumerable: false, configurable: false, writable: true, value: rules }); }; /** - * Adds an object to the set of objects that should be validated when validate is called. - * @param object The object. - * @param rules Optional. The rules. If rules aren't supplied the Validator implementation will lookup the rules. + * Removes rules from a target. */ - ValidationController.prototype.addObject = function (object, rules) { - this.objects.set(object, rules); + Rules.unset = function (target) { + if (target instanceof Function) { + target = target.prototype; + } + target[Rules.key] = null; }; /** - * Removes an object from the set of objects that should be validated when validate is called. - * @param object The object. + * Retrieves the target's rules. */ - ValidationController.prototype.removeObject = function (object) { - this.objects.delete(object); - this.processResultDelta('reset', this.results.filter(function (result) { return result.object === object; }), []); + Rules.get = function (target) { + return target[Rules.key] || null; }; /** - * Adds and renders an error. + * The name of the property that stores the rules. */ - ValidationController.prototype.addError = function (message, object, propertyName) { - if (propertyName === void 0) { propertyName = null; } - var resolvedPropertyName; - if (propertyName === null) { - resolvedPropertyName = propertyName; - } - else { - resolvedPropertyName = this.propertyParser.parse(propertyName); + Rules.key = '__rules__'; + return Rules; + }()); + + // tslint:disable:no-empty + var ExpressionVisitor = /** @class */ (function () { + function ExpressionVisitor() { + } + ExpressionVisitor.prototype.visitChain = function (chain) { + this.visitArgs(chain.expressions); + }; + ExpressionVisitor.prototype.visitBindingBehavior = function (behavior) { + behavior.expression.accept(this); + this.visitArgs(behavior.args); + }; + ExpressionVisitor.prototype.visitValueConverter = function (converter) { + converter.expression.accept(this); + this.visitArgs(converter.args); + }; + ExpressionVisitor.prototype.visitAssign = function (assign) { + assign.target.accept(this); + assign.value.accept(this); + }; + ExpressionVisitor.prototype.visitConditional = function (conditional) { + conditional.condition.accept(this); + conditional.yes.accept(this); + conditional.no.accept(this); + }; + ExpressionVisitor.prototype.visitAccessThis = function (access) { + access.ancestor = access.ancestor; + }; + ExpressionVisitor.prototype.visitAccessScope = function (access) { + access.name = access.name; + }; + ExpressionVisitor.prototype.visitAccessMember = function (access) { + access.object.accept(this); + }; + ExpressionVisitor.prototype.visitAccessKeyed = function (access) { + access.object.accept(this); + access.key.accept(this); + }; + ExpressionVisitor.prototype.visitCallScope = function (call) { + this.visitArgs(call.args); + }; + ExpressionVisitor.prototype.visitCallFunction = function (call) { + call.func.accept(this); + this.visitArgs(call.args); + }; + ExpressionVisitor.prototype.visitCallMember = function (call) { + call.object.accept(this); + this.visitArgs(call.args); + }; + ExpressionVisitor.prototype.visitPrefix = function (prefix) { + prefix.expression.accept(this); + }; + ExpressionVisitor.prototype.visitBinary = function (binary) { + binary.left.accept(this); + binary.right.accept(this); + }; + ExpressionVisitor.prototype.visitLiteralPrimitive = function (literal) { + literal.value = literal.value; + }; + ExpressionVisitor.prototype.visitLiteralArray = function (literal) { + this.visitArgs(literal.elements); + }; + ExpressionVisitor.prototype.visitLiteralObject = function (literal) { + this.visitArgs(literal.values); + }; + ExpressionVisitor.prototype.visitLiteralString = function (literal) { + literal.value = literal.value; + }; + ExpressionVisitor.prototype.visitArgs = function (args) { + for (var i = 0; i < args.length; i++) { + args[i].accept(this); } - var result = new ValidateResult({ __manuallyAdded__: true }, object, resolvedPropertyName, false, message); - this.processResultDelta('validate', [], [result]); - return result; }; - /** - * Removes and unrenders an error. - */ - ValidationController.prototype.removeError = function (result) { - if (this.results.indexOf(result) !== -1) { - this.processResultDelta('reset', [result], []); + return ExpressionVisitor; + }()); + + var ValidationMessageParser = /** @class */ (function () { + function ValidationMessageParser(bindinqLanguage) { + this.bindinqLanguage = bindinqLanguage; + this.emptyStringExpression = new aureliaBinding.LiteralString(''); + this.nullExpression = new aureliaBinding.LiteralPrimitive(null); + this.undefinedExpression = new aureliaBinding.LiteralPrimitive(undefined); + this.cache = {}; + } + ValidationMessageParser.prototype.parse = function (message) { + if (this.cache[message] !== undefined) { + return this.cache[message]; + } + var parts = this.bindinqLanguage.parseInterpolation(null, message); + if (parts === null) { + return new aureliaBinding.LiteralString(message); + } + var expression = new aureliaBinding.LiteralString(parts[0]); + for (var i = 1; i < parts.length; i += 2) { + expression = new aureliaBinding.Binary('+', expression, new aureliaBinding.Binary('+', this.coalesce(parts[i]), new aureliaBinding.LiteralString(parts[i + 1]))); } + MessageExpressionValidator.validate(expression, message); + this.cache[message] = expression; + return expression; }; - /** - * Adds a renderer. - * @param renderer The renderer. - */ - ValidationController.prototype.addRenderer = function (renderer) { - var _this = this; - this.renderers.push(renderer); - renderer.render({ - kind: 'validate', - render: this.results.map(function (result) { return ({ result: result, elements: _this.elements.get(result) }); }), - unrender: [] - }); + ValidationMessageParser.prototype.coalesce = function (part) { + // part === null || part === undefined ? '' : part + return new aureliaBinding.Conditional(new aureliaBinding.Binary('||', new aureliaBinding.Binary('===', part, this.nullExpression), new aureliaBinding.Binary('===', part, this.undefinedExpression)), this.emptyStringExpression, new aureliaBinding.CallMember(part, 'toString', [])); }; - /** - * Removes a renderer. - * @param renderer The renderer. - */ - ValidationController.prototype.removeRenderer = function (renderer) { - var _this = this; - this.renderers.splice(this.renderers.indexOf(renderer), 1); - renderer.render({ - kind: 'reset', - render: [], - unrender: this.results.map(function (result) { return ({ result: result, elements: _this.elements.get(result) }); }) - }); + ValidationMessageParser.inject = [aureliaTemplating.BindingLanguage]; + return ValidationMessageParser; + }()); + var MessageExpressionValidator = /** @class */ (function (_super) { + __extends(MessageExpressionValidator, _super); + function MessageExpressionValidator(originalMessage) { + var _this = _super.call(this) || this; + _this.originalMessage = originalMessage; + return _this; + } + MessageExpressionValidator.validate = function (expression, originalMessage) { + var visitor = new MessageExpressionValidator(originalMessage); + expression.accept(visitor); }; - /** - * Registers a binding with the controller. - * @param binding The binding instance. - * @param target The DOM element. - * @param rules (optional) rules associated with the binding. Validator implementation specific. - */ - ValidationController.prototype.registerBinding = function (binding, target, rules) { - this.bindings.set(binding, { target: target, rules: rules, propertyInfo: null }); + MessageExpressionValidator.prototype.visitAccessScope = function (access) { + if (access.ancestor !== 0) { + throw new Error('$parent is not permitted in validation message expressions.'); + } + if (['displayName', 'propertyName', 'value', 'object', 'config', 'getDisplayName'].indexOf(access.name) !== -1) { + LogManager.getLogger('aurelia-validation') + // tslint:disable-next-line:max-line-length + .warn("Did you mean to use \"$" + access.name + "\" instead of \"" + access.name + "\" in this validation message template: \"" + this.originalMessage + "\"?"); + } }; + return MessageExpressionValidator; + }(ExpressionVisitor)); + + /** + * Dictionary of validation messages. [messageKey]: messageExpression + */ + var validationMessages = { /** - * Unregisters a binding with the controller. - * @param binding The binding instance. + * The default validation message. Used with rules that have no standard message. */ - ValidationController.prototype.unregisterBinding = function (binding) { - this.resetBinding(binding); - this.bindings.delete(binding); - }; + default: "${$displayName} is invalid.", + required: "${$displayName} is required.", + matches: "${$displayName} is not correctly formatted.", + email: "${$displayName} is not a valid email.", + minLength: "${$displayName} must be at least ${$config.length} character${$config.length === 1 ? '' : 's'}.", + maxLength: "${$displayName} cannot be longer than ${$config.length} character${$config.length === 1 ? '' : 's'}.", + minItems: "${$displayName} must contain at least ${$config.count} item${$config.count === 1 ? '' : 's'}.", + maxItems: "${$displayName} cannot contain more than ${$config.count} item${$config.count === 1 ? '' : 's'}.", + min: "${$displayName} must be at least ${$config.constraint}.", + max: "${$displayName} must be at most ${$config.constraint}.", + range: "${$displayName} must be between or equal to ${$config.min} and ${$config.max}.", + between: "${$displayName} must be between but not equal to ${$config.min} and ${$config.max}.", + equals: "${$displayName} must be ${$config.expectedValue}.", + }; + /** + * Retrieves validation messages and property display names. + */ + var ValidationMessageProvider = /** @class */ (function () { + function ValidationMessageProvider(parser) { + this.parser = parser; + } /** - * Interprets the instruction and returns a predicate that will identify - * relevant results in the list of rendered validation results. + * Returns a message binding expression that corresponds to the key. + * @param key The message key. */ - ValidationController.prototype.getInstructionPredicate = function (instruction) { - var _this = this; - if (instruction) { - var object_1 = instruction.object, propertyName_1 = instruction.propertyName, rules_1 = instruction.rules; - var predicate_1; - if (instruction.propertyName) { - predicate_1 = function (x) { return x.object === object_1 && x.propertyName === propertyName_1; }; - } - else { - predicate_1 = function (x) { return x.object === object_1; }; - } - if (rules_1) { - return function (x) { return predicate_1(x) && _this.validator.ruleExists(rules_1, x.rule); }; - } - return predicate_1; + ValidationMessageProvider.prototype.getMessage = function (key) { + var message; + if (key in validationMessages) { + message = validationMessages[key]; } else { - return function () { return true; }; + message = validationMessages['default']; } + return this.parser.parse(message); }; /** - * Validates and renders results. - * @param instruction Optional. Instructions on what to validate. If undefined, all - * objects and bindings will be validated. + * Formulates a property display name using the property name and the configured + * displayName (if provided). + * Override this with your own custom logic. + * @param propertyName The property name. */ - ValidationController.prototype.validate = function (instruction) { - var _this = this; - // Get a function that will process the validation instruction. - var execute; - if (instruction) { - // tslint:disable-next-line:prefer-const - var object_2 = instruction.object, propertyName_2 = instruction.propertyName, rules_2 = instruction.rules; - // if rules were not specified, check the object map. - rules_2 = rules_2 || this.objects.get(object_2); - // property specified? - if (instruction.propertyName === undefined) { - // validate the specified object. - execute = function () { return _this.validator.validateObject(object_2, rules_2); }; - } - else { - // validate the specified property. - execute = function () { return _this.validator.validateProperty(object_2, propertyName_2, rules_2); }; - } - } - else { - // validate all objects and bindings. - execute = function () { - var promises = []; - for (var _i = 0, _a = Array.from(_this.objects); _i < _a.length; _i++) { - var _b = _a[_i], object = _b[0], rules = _b[1]; - promises.push(_this.validator.validateObject(object, rules)); - } - for (var _c = 0, _d = Array.from(_this.bindings); _c < _d.length; _c++) { - var _e = _d[_c], binding = _e[0], rules = _e[1].rules; - var propertyInfo = getPropertyInfo(binding.sourceExpression, binding.source); - if (!propertyInfo || _this.objects.has(propertyInfo.object)) { - continue; - } - promises.push(_this.validator.validateProperty(propertyInfo.object, propertyInfo.propertyName, rules)); - } - return Promise.all(promises).then(function (resultSets) { return resultSets.reduce(function (a, b) { return a.concat(b); }, []); }); - }; + ValidationMessageProvider.prototype.getDisplayName = function (propertyName, displayName) { + if (displayName !== null && displayName !== undefined) { + return (displayName instanceof Function) ? displayName() : displayName; } - // Wait for any existing validation to finish, execute the instruction, render the results. - this.validating = true; - var returnPromise = this.finishValidating - .then(execute) - .then(function (newResults) { - var predicate = _this.getInstructionPredicate(instruction); - var oldResults = _this.results.filter(predicate); - _this.processResultDelta('validate', oldResults, newResults); - if (returnPromise === _this.finishValidating) { - _this.validating = false; - } - var result = { - instruction: instruction, - valid: newResults.find(function (x) { return !x.valid; }) === undefined, - results: newResults - }; - _this.invokeCallbacks(instruction, result); - return result; - }) - .catch(function (exception) { - // recover, to enable subsequent calls to validate() - _this.validating = false; - _this.finishValidating = Promise.resolve(); - return Promise.reject(exception); - }); - this.finishValidating = returnPromise; - return returnPromise; + // split on upper-case letters. + var words = propertyName.toString().split(/(?=[A-Z])/).join(' '); + // capitalize first letter. + return words.charAt(0).toUpperCase() + words.slice(1); + }; + ValidationMessageProvider.inject = [ValidationMessageParser]; + return ValidationMessageProvider; + }()); + + /** + * Validates. + * Responsible for validating objects and properties. + */ + var StandardValidator = /** @class */ (function (_super) { + __extends(StandardValidator, _super); + function StandardValidator(messageProvider, resources) { + var _this = _super.call(this) || this; + _this.messageProvider = messageProvider; + _this.lookupFunctions = resources.lookupFunctions; + _this.getDisplayName = messageProvider.getDisplayName.bind(messageProvider); + return _this; + } + /** + * Validates the specified property. + * @param object The object to validate. + * @param propertyName The name of the property to validate. + * @param rules Optional. If unspecified, the rules will be looked up using the metadata + * for the object created by ValidationRules....on(class/object) + */ + StandardValidator.prototype.validateProperty = function (object, propertyName, rules) { + return this.validate(object, propertyName, rules || null); }; /** - * Resets any rendered validation results (unrenders). - * @param instruction Optional. Instructions on what to reset. If unspecified all rendered results - * will be unrendered. + * Validates all rules for specified object and it's properties. + * @param object The object to validate. + * @param rules Optional. If unspecified, the rules will be looked up using the metadata + * for the object created by ValidationRules....on(class/object) */ - ValidationController.prototype.reset = function (instruction) { - var predicate = this.getInstructionPredicate(instruction); - var oldResults = this.results.filter(predicate); - this.processResultDelta('reset', oldResults, []); - this.invokeCallbacks(instruction, null); + StandardValidator.prototype.validateObject = function (object, rules) { + return this.validate(object, null, rules || null); }; /** - * Gets the elements associated with an object and propertyName (if any). + * Determines whether a rule exists in a set of rules. + * @param rules The rules to search. + * @parem rule The rule to find. */ - ValidationController.prototype.getAssociatedElements = function (_a) { - var object = _a.object, propertyName = _a.propertyName; - var elements = []; - for (var _i = 0, _b = Array.from(this.bindings); _i < _b.length; _i++) { - var _c = _b[_i], binding = _c[0], target = _c[1].target; - var propertyInfo = getPropertyInfo(binding.sourceExpression, binding.source); - if (propertyInfo && propertyInfo.object === object && propertyInfo.propertyName === propertyName) { - elements.push(target); + StandardValidator.prototype.ruleExists = function (rules, rule) { + var i = rules.length; + while (i--) { + if (rules[i].indexOf(rule) !== -1) { + return true; } } - return elements; + return false; }; - ValidationController.prototype.processResultDelta = function (kind, oldResults, newResults) { - // prepare the instruction. - var instruction = { - kind: kind, - render: [], - unrender: [] + StandardValidator.prototype.getMessage = function (rule, object, value) { + var expression = rule.message || this.messageProvider.getMessage(rule.messageKey); + // tslint:disable-next-line:prefer-const + var _a = rule.property, propertyName = _a.name, displayName = _a.displayName; + if (propertyName !== null) { + displayName = this.messageProvider.getDisplayName(propertyName, displayName); + } + var overrideContext = { + $displayName: displayName, + $propertyName: propertyName, + $value: value, + $object: object, + $config: rule.config, + // returns the name of a given property, given just the property name (irrespective of the property's displayName) + // split on capital letters, first letter ensured to be capitalized + $getDisplayName: this.getDisplayName }; - // create a shallow copy of newResults so we can mutate it without causing side-effects. - newResults = newResults.slice(0); - var _loop_1 = function (oldResult) { - // get the elements associated with the old result. - var elements = this_1.elements.get(oldResult); - // remove the old result from the element map. - this_1.elements.delete(oldResult); - // create the unrender instruction. - instruction.unrender.push({ result: oldResult, elements: elements }); - // determine if there's a corresponding new result for the old result we are unrendering. - var newResultIndex = newResults.findIndex(function (x) { return x.rule === oldResult.rule && x.object === oldResult.object && x.propertyName === oldResult.propertyName; }); - if (newResultIndex === -1) { - // no corresponding new result... simple remove. - this_1.results.splice(this_1.results.indexOf(oldResult), 1); - if (!oldResult.valid) { - this_1.errors.splice(this_1.errors.indexOf(oldResult), 1); - } + return expression.evaluate({ bindingContext: object, overrideContext: overrideContext }, this.lookupFunctions); + }; + StandardValidator.prototype.validateRuleSequence = function (object, propertyName, ruleSequence, sequence, results) { + var _this = this; + // are we validating all properties or a single property? + var validateAllProperties = propertyName === null || propertyName === undefined; + var rules = ruleSequence[sequence]; + var allValid = true; + // validate each rule. + var promises = []; + var _loop_1 = function (i) { + var rule = rules[i]; + // is the rule related to the property we're validating. + // tslint:disable-next-line:triple-equals | Use loose equality for property keys + if (!validateAllProperties && rule.property.name != propertyName) { + return "continue"; } - else { - // there is a corresponding new result... - var newResult = newResults.splice(newResultIndex, 1)[0]; - // get the elements that are associated with the new result. - var elements_1 = this_1.getAssociatedElements(newResult); - this_1.elements.set(newResult, elements_1); - // create a render instruction for the new result. - instruction.render.push({ result: newResult, elements: elements_1 }); - // do an in-place replacement of the old result with the new result. - // this ensures any repeats bound to this.results will not thrash. - this_1.results.splice(this_1.results.indexOf(oldResult), 1, newResult); - if (!oldResult.valid && newResult.valid) { - this_1.errors.splice(this_1.errors.indexOf(oldResult), 1); - } - else if (!oldResult.valid && !newResult.valid) { - this_1.errors.splice(this_1.errors.indexOf(oldResult), 1, newResult); - } - else if (!newResult.valid) { - this_1.errors.push(newResult); - } + // is this a conditional rule? is the condition met? + if (rule.when && !rule.when(object)) { + return "continue"; + } + // validate. + var value = rule.property.name === null ? object : object[rule.property.name]; + var promiseOrBoolean = rule.condition(value, object); + if (!(promiseOrBoolean instanceof Promise)) { + promiseOrBoolean = Promise.resolve(promiseOrBoolean); } + promises.push(promiseOrBoolean.then(function (valid) { + var message = valid ? null : _this.getMessage(rule, object, value); + results.push(new ValidateResult(rule, object, rule.property.name, valid, message)); + allValid = allValid && valid; + return valid; + })); }; - var this_1 = this; - // create unrender instructions from the old results. - for (var _i = 0, oldResults_1 = oldResults; _i < oldResults_1.length; _i++) { - var oldResult = oldResults_1[_i]; - _loop_1(oldResult); + for (var i = 0; i < rules.length; i++) { + _loop_1(i); } - // create render instructions from the remaining new results. - for (var _a = 0, newResults_1 = newResults; _a < newResults_1.length; _a++) { - var result = newResults_1[_a]; - var elements = this.getAssociatedElements(result); - instruction.render.push({ result: result, elements: elements }); - this.elements.set(result, elements); - this.results.push(result); - if (!result.valid) { - this.errors.push(result); + return Promise.all(promises) + .then(function () { + sequence++; + if (allValid && sequence < ruleSequence.length) { + return _this.validateRuleSequence(object, propertyName, ruleSequence, sequence, results); } + return results; + }); + }; + StandardValidator.prototype.validate = function (object, propertyName, rules) { + // rules specified? + if (!rules) { + // no. attempt to locate the rules. + rules = Rules.get(object); } - // render. - for (var _b = 0, _c = this.renderers; _b < _c.length; _b++) { - var renderer = _c[_b]; - renderer.render(instruction); + // any rules? + if (!rules || rules.length === 0) { + return Promise.resolve([]); } + return this.validateRuleSequence(object, propertyName, rules, 0, []); }; + StandardValidator.inject = [ValidationMessageProvider, aureliaTemplating.ViewResources]; + return StandardValidator; + }(Validator)); + + /** + * Validation triggers. + */ + (function (validateTrigger) { /** - * Validates the property associated with a binding. + * Manual validation. Use the controller's `validate()` and `reset()` methods + * to validate all bindings. */ - ValidationController.prototype.validateBinding = function (binding) { - if (!binding.isBound) { - return; - } - var propertyInfo = getPropertyInfo(binding.sourceExpression, binding.source); - var rules; - var registeredBinding = this.bindings.get(binding); - if (registeredBinding) { - rules = registeredBinding.rules; - registeredBinding.propertyInfo = propertyInfo; - } - if (!propertyInfo) { - return; - } - var object = propertyInfo.object, propertyName = propertyInfo.propertyName; - this.validate({ object: object, propertyName: propertyName, rules: rules }); - }; + validateTrigger[validateTrigger["manual"] = 0] = "manual"; /** - * Resets the results for a property associated with a binding. + * Validate the binding when the binding's target element fires a DOM "blur" event. */ - ValidationController.prototype.resetBinding = function (binding) { - var registeredBinding = this.bindings.get(binding); - var propertyInfo = getPropertyInfo(binding.sourceExpression, binding.source); - if (!propertyInfo && registeredBinding) { - propertyInfo = registeredBinding.propertyInfo; - } - if (registeredBinding) { - registeredBinding.propertyInfo = null; - } - if (!propertyInfo) { - return; - } - var object = propertyInfo.object, propertyName = propertyInfo.propertyName; - this.reset({ object: object, propertyName: propertyName }); - }; + validateTrigger[validateTrigger["blur"] = 1] = "blur"; /** - * Changes the controller's validateTrigger. - * @param newTrigger The new validateTrigger + * Validate the binding when it updates the model due to a change in the view. */ - ValidationController.prototype.changeTrigger = function (newTrigger) { - this.validateTrigger = newTrigger; - var bindings = Array.from(this.bindings.keys()); - for (var _i = 0, bindings_1 = bindings; _i < bindings_1.length; _i++) { - var binding = bindings_1[_i]; - var source = binding.source; - binding.unbind(); - binding.bind(source); - } - }; + validateTrigger[validateTrigger["change"] = 2] = "change"; /** - * Revalidates the controller's current set of errors. + * 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. */ - ValidationController.prototype.revalidateErrors = function () { - for (var _i = 0, _a = this.errors; _i < _a.length; _i++) { - var _b = _a[_i], object = _b.object, propertyName = _b.propertyName, rule = _b.rule; - if (rule.__manuallyAdded__) { - continue; - } - var rules = [[rule]]; - this.validate({ object: object, propertyName: propertyName, rules: rules }); - } - }; - ValidationController.prototype.invokeCallbacks = function (instruction, result) { - if (this.eventCallbacks.length === 0) { - return; - } - var event = new ValidateEvent(result ? 'validate' : 'reset', this.errors, this.results, instruction || null, result); - for (var i = 0; i < this.eventCallbacks.length; i++) { - this.eventCallbacks[i](event); - } - }; - ValidationController.inject = [Validator, PropertyAccessorParser]; - return ValidationController; - }()); + validateTrigger[validateTrigger["changeOrBlur"] = 3] = "changeOrBlur"; + })(exports.validateTrigger || (exports.validateTrigger = {})); /** - * Binding behavior. Indicates the bound property should be validated. + * Aurelia Validation Configuration API */ - var ValidateBindingBehaviorBase = /** @class */ (function () { - function ValidateBindingBehaviorBase(taskQueue) { - this.taskQueue = taskQueue; + var GlobalValidationConfiguration = /** @class */ (function () { + function GlobalValidationConfiguration() { + this.validatorType = StandardValidator; + this.validationTrigger = GlobalValidationConfiguration.DEFAULT_VALIDATION_TRIGGER; } - ValidateBindingBehaviorBase.prototype.bind = function (binding, source, rulesOrController, rules) { - var _this = this; - // identify the target element. - var target = getTargetDOMElement(binding, source); - // locate the controller. - var controller; - if (rulesOrController instanceof ValidationController) { - controller = rulesOrController; - } - else { - controller = source.container.get(aureliaDependencyInjection.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); - // tslint:disable-next-line:no-bitwise - if (trigger & exports.validateTrigger.change) { - binding.vbbUpdateSource = binding.updateSource; - // tslint:disable-next-line:only-arrow-functions - // tslint:disable-next-line:space-before-function-paren - binding.updateSource = function (value) { - this.vbbUpdateSource(value); - this.validationController.validateBinding(this); - }; - } - // tslint:disable-next-line:no-bitwise - if (trigger & exports.validateTrigger.blur) { - binding.validateBlurHandler = function () { - _this.taskQueue.queueMicroTask(function () { return controller.validateBinding(binding); }); - }; - binding.validateTarget = target; - target.addEventListener('blur', binding.validateBlurHandler); - } - if (trigger !== exports.validateTrigger.manual) { - binding.standardUpdateTarget = binding.updateTarget; - // tslint:disable-next-line:only-arrow-functions - // tslint:disable-next-line:space-before-function-paren - binding.updateTarget = function (value) { - this.standardUpdateTarget(value); - this.validationController.resetBinding(this); - }; - } + /** + * Use a custom Validator implementation. + */ + GlobalValidationConfiguration.prototype.customValidator = function (type) { + this.validatorType = type; + return this; }; - ValidateBindingBehaviorBase.prototype.unbind = function (binding) { - // reset the binding to it's original state. - if (binding.vbbUpdateSource) { - binding.updateSource = binding.vbbUpdateSource; - binding.vbbUpdateSource = 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; + GlobalValidationConfiguration.prototype.defaultValidationTrigger = function (trigger) { + this.validationTrigger = trigger; + return this; }; - return ValidateBindingBehaviorBase; + GlobalValidationConfiguration.prototype.getDefaultValidationTrigger = function () { + return this.validationTrigger; + }; + /** + * Applies the configuration. + */ + GlobalValidationConfiguration.prototype.apply = function (container) { + var validator = container.get(this.validatorType); + container.registerInstance(Validator, validator); + container.registerInstance(GlobalValidationConfiguration, this); + }; + GlobalValidationConfiguration.DEFAULT_VALIDATION_TRIGGER = exports.validateTrigger.blur; + return GlobalValidationConfiguration; }()); /** - * Binding behavior. Indicates the bound property should be validated - * when the validate trigger specified by the associated controller's - * validateTrigger property occurs. + * 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. */ - var ValidateBindingBehavior = /** @class */ (function (_super) { - __extends(ValidateBindingBehavior, _super); - function ValidateBindingBehavior() { - return _super !== null && _super.apply(this, arguments) || this; + function getTargetDOMElement(binding, view) { + var target = binding.target; + // DOM element + if (target instanceof Element) { + return target; } - ValidateBindingBehavior.prototype.getValidateTrigger = function (controller) { - return controller.validateTrigger; - }; - ValidateBindingBehavior.inject = [aureliaTaskQueue.TaskQueue]; - ValidateBindingBehavior = __decorate([ - aureliaBinding.bindingBehavior('validate') - ], ValidateBindingBehavior); - 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. - */ - var ValidateManuallyBindingBehavior = /** @class */ (function (_super) { - __extends(ValidateManuallyBindingBehavior, _super); - function ValidateManuallyBindingBehavior() { - return _super !== null && _super.apply(this, arguments) || this; + // custom element or custom attribute + // tslint:disable-next-line:prefer-const + 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(aureliaPal.DOM.Element); + if (element) { + return element; + } + throw new Error("Unable to locate target element for \"" + binding.sourceExpression + "\"."); + } } - ValidateManuallyBindingBehavior.prototype.getValidateTrigger = function () { - return exports.validateTrigger.manual; - }; - ValidateManuallyBindingBehavior.inject = [aureliaTaskQueue.TaskQueue]; - ValidateManuallyBindingBehavior = __decorate([ - aureliaBinding.bindingBehavior('validateManually') - ], ValidateManuallyBindingBehavior); - return ValidateManuallyBindingBehavior; - }(ValidateBindingBehaviorBase)); - /** - * Binding behavior. Indicates the bound property should be validated - * when the associated element blurs. - */ - var ValidateOnBlurBindingBehavior = /** @class */ (function (_super) { - __extends(ValidateOnBlurBindingBehavior, _super); - function ValidateOnBlurBindingBehavior() { - return _super !== null && _super.apply(this, arguments) || this; + throw new Error("Unable to locate target element for \"" + binding.sourceExpression + "\"."); + } + + function getObject(expression, objectExpression, source) { + var value = objectExpression.evaluate(source, null); + if (value === null || value === undefined || value instanceof Object) { + return value; } - ValidateOnBlurBindingBehavior.prototype.getValidateTrigger = function () { - return exports.validateTrigger.blur; - }; - ValidateOnBlurBindingBehavior.inject = [aureliaTaskQueue.TaskQueue]; - ValidateOnBlurBindingBehavior = __decorate([ - aureliaBinding.bindingBehavior('validateOnBlur') - ], ValidateOnBlurBindingBehavior); - return ValidateOnBlurBindingBehavior; - }(ValidateBindingBehaviorBase)); + // tslint:disable-next-line:max-line-length + throw new Error("The '" + objectExpression + "' part of '" + expression + "' evaluates to " + value + " instead of an object, null or undefined."); + } /** - * Binding behavior. Indicates the bound property should be validated - * when the associated element is changed by the user, causing a change - * to the model. + * Retrieves the object and property name for the specified expression. + * @param expression The expression + * @param source The scope */ - var ValidateOnChangeBindingBehavior = /** @class */ (function (_super) { - __extends(ValidateOnChangeBindingBehavior, _super); - function ValidateOnChangeBindingBehavior() { - return _super !== null && _super.apply(this, arguments) || this; + function getPropertyInfo(expression, source) { + var originalExpression = expression; + while (expression instanceof aureliaBinding.BindingBehavior || expression instanceof aureliaBinding.ValueConverter) { + expression = expression.expression; } - ValidateOnChangeBindingBehavior.prototype.getValidateTrigger = function () { - return exports.validateTrigger.change; - }; - ValidateOnChangeBindingBehavior.inject = [aureliaTaskQueue.TaskQueue]; - ValidateOnChangeBindingBehavior = __decorate([ - aureliaBinding.bindingBehavior('validateOnChange') - ], ValidateOnChangeBindingBehavior); - 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. - */ - var ValidateOnChangeOrBlurBindingBehavior = /** @class */ (function (_super) { - __extends(ValidateOnChangeOrBlurBindingBehavior, _super); - function ValidateOnChangeOrBlurBindingBehavior() { - return _super !== null && _super.apply(this, arguments) || this; + var object; + var propertyName; + if (expression instanceof aureliaBinding.AccessScope) { + object = aureliaBinding.getContextFor(expression.name, source, expression.ancestor); + propertyName = expression.name; } - ValidateOnChangeOrBlurBindingBehavior.prototype.getValidateTrigger = function () { - return exports.validateTrigger.changeOrBlur; + else if (expression instanceof aureliaBinding.AccessMember) { + object = getObject(originalExpression, expression.object, source); + propertyName = expression.name; + } + else if (expression instanceof aureliaBinding.AccessKeyed) { + object = getObject(originalExpression, expression.object, source); + propertyName = expression.key.evaluate(source); + } + else { + throw new Error("Expression '" + originalExpression + "' is not compatible with the validate binding-behavior."); + } + if (object === null || object === undefined) { + return null; + } + return { object: object, propertyName: propertyName }; + } + + function isString(value) { + return Object.prototype.toString.call(value) === '[object String]'; + } + function isNumber(value) { + return Object.prototype.toString.call(value) === '[object Number]'; + } + + var PropertyAccessorParser = /** @class */ (function () { + function PropertyAccessorParser(parser) { + this.parser = parser; + } + PropertyAccessorParser.prototype.parse = function (property) { + if (isString(property) || isNumber(property)) { + return property; + } + var accessorText = getAccessorExpression(property.toString()); + var accessor = this.parser.parse(accessorText); + if (accessor instanceof aureliaBinding.AccessScope + || accessor instanceof aureliaBinding.AccessMember && accessor.object instanceof aureliaBinding.AccessScope) { + return accessor.name; + } + throw new Error("Invalid property expression: \"" + accessor + "\""); }; - ValidateOnChangeOrBlurBindingBehavior.inject = [aureliaTaskQueue.TaskQueue]; - ValidateOnChangeOrBlurBindingBehavior = __decorate([ - aureliaBinding.bindingBehavior('validateOnChangeOrBlur') - ], ValidateOnChangeOrBlurBindingBehavior); - return ValidateOnChangeOrBlurBindingBehavior; - }(ValidateBindingBehaviorBase)); + PropertyAccessorParser.inject = [aureliaBinding.Parser]; + return PropertyAccessorParser; + }()); + function getAccessorExpression(fn) { + /* tslint:disable:max-line-length */ + var classic = /^function\s*\([$_\w\d]+\)\s*\{(?:\s*"use strict";)?\s*(?:[$_\w\d.['"\]+;]+)?\s*return\s+[$_\w\d]+\.([$_\w\d]+)\s*;?\s*\}$/; + /* tslint:enable:max-line-length */ + var arrow = /^\(?[$_\w\d]+\)?\s*=>\s*[$_\w\d]+\.([$_\w\d]+)$/; + var match = classic.exec(fn) || arrow.exec(fn); + if (match === null) { + throw new Error("Unable to parse accessor function:\n" + fn); + } + return match[1]; + } + + var ValidateEvent = /** @class */ (function () { + function ValidateEvent( + /** + * The type of validate event. Either "validate" or "reset". + */ + type, + /** + * The controller's current array of errors. For an array containing both + * failed rules and passed rules, use the "results" property. + */ + errors, + /** + * The controller's current array of validate results. This + * includes both passed rules and failed rules. For an array of only failed rules, + * use the "errors" property. + */ + results, + /** + * The instruction passed to the "validate" or "reset" event. Will be null when + * the controller's validate/reset method was called with no instruction argument. + */ + instruction, + /** + * In events with type === "validate", this property will contain the result + * of validating the instruction (see "instruction" property). Use the controllerValidateResult + * to access the validate results specific to the call to "validate" + * (as opposed to using the "results" and "errors" properties to access the controller's entire + * set of results/errors). + */ + controllerValidateResult) { + this.type = type; + this.errors = errors; + this.results = results; + this.instruction = instruction; + this.controllerValidateResult = controllerValidateResult; + } + return ValidateEvent; + }()); /** - * Creates ValidationController instances. + * Orchestrates validation. + * Manages a set of bindings, renderers and objects. + * Exposes the current list of validation results for binding purposes. */ - var ValidationControllerFactory = /** @class */ (function () { - function ValidationControllerFactory(container) { - this.container = container; + var ValidationController = /** @class */ (function () { + function ValidationController(validator, propertyParser, config) { + this.validator = validator; + this.propertyParser = propertyParser; + // Registered bindings (via the validate binding behavior) + this.bindings = new Map(); + // Renderers that have been added to the controller instance. + this.renderers = []; + /** + * Validation results that have been rendered by the controller. + */ + this.results = []; + /** + * Validation errors that have been rendered by the controller. + */ + this.errors = []; + /** + * Whether the controller is currently validating. + */ + this.validating = false; + // Elements related to validation results that have been rendered. + this.elements = new Map(); + // Objects that have been added to the controller instance (entity-style validation). + this.objects = new Map(); + // Promise that resolves when validation has completed. + this.finishValidating = Promise.resolve(); + this.eventCallbacks = []; + this.validateTrigger = config instanceof GlobalValidationConfiguration + ? config.getDefaultValidationTrigger() + : GlobalValidationConfiguration.DEFAULT_VALIDATION_TRIGGER; } - ValidationControllerFactory.get = function (container) { - return new ValidationControllerFactory(container); + /** + * Subscribe to controller validate and reset events. These events occur when the + * controller's "validate"" and "reset" methods are called. + * @param callback The callback to be invoked when the controller validates or resets. + */ + ValidationController.prototype.subscribe = function (callback) { + var _this = this; + this.eventCallbacks.push(callback); + return { + dispose: function () { + var index = _this.eventCallbacks.indexOf(callback); + if (index === -1) { + return; + } + _this.eventCallbacks.splice(index, 1); + } + }; }; /** - * Creates a new controller instance. + * Adds an object to the set of objects that should be validated when validate is called. + * @param object The object. + * @param rules Optional. The rules. If rules aren't supplied the Validator implementation will lookup the rules. */ - ValidationControllerFactory.prototype.create = function (validator) { - if (!validator) { - validator = this.container.get(Validator); + ValidationController.prototype.addObject = function (object, rules) { + this.objects.set(object, rules); + }; + /** + * Removes an object from the set of objects that should be validated when validate is called. + * @param object The object. + */ + ValidationController.prototype.removeObject = function (object) { + this.objects.delete(object); + this.processResultDelta('reset', this.results.filter(function (result) { return result.object === object; }), []); + }; + /** + * Adds and renders an error. + */ + ValidationController.prototype.addError = function (message, object, propertyName) { + if (propertyName === void 0) { propertyName = null; } + var resolvedPropertyName; + if (propertyName === null) { + resolvedPropertyName = propertyName; } - var propertyParser = this.container.get(PropertyAccessorParser); - return new ValidationController(validator, propertyParser); + else { + resolvedPropertyName = this.propertyParser.parse(propertyName); + } + var result = new ValidateResult({ __manuallyAdded__: true }, object, resolvedPropertyName, false, message); + this.processResultDelta('validate', [], [result]); + return result; }; /** - * 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. + * Removes and unrenders an error. */ - ValidationControllerFactory.prototype.createForCurrentScope = function (validator) { - var controller = this.create(validator); - this.container.registerInstance(ValidationController, controller); - return controller; + ValidationController.prototype.removeError = function (result) { + if (this.results.indexOf(result) !== -1) { + this.processResultDelta('reset', [result], []); + } }; - return ValidationControllerFactory; - }()); - ValidationControllerFactory['protocol:aurelia:resolver'] = true; - - var ValidationErrorsCustomAttribute = /** @class */ (function () { - function ValidationErrorsCustomAttribute(boundaryElement, controllerAccessor) { - this.boundaryElement = boundaryElement; - this.controllerAccessor = controllerAccessor; - this.controller = null; - this.errors = []; - this.errorsInternal = []; - } - ValidationErrorsCustomAttribute.inject = function () { - return [aureliaPal.DOM.Element, aureliaDependencyInjection.Lazy.of(ValidationController)]; + /** + * Adds a renderer. + * @param renderer The renderer. + */ + ValidationController.prototype.addRenderer = function (renderer) { + var _this = this; + this.renderers.push(renderer); + renderer.render({ + kind: 'validate', + render: this.results.map(function (result) { return ({ result: result, elements: _this.elements.get(result) }); }), + unrender: [] + }); }; - ValidationErrorsCustomAttribute.prototype.sort = function () { - this.errorsInternal.sort(function (a, b) { - if (a.targets[0] === b.targets[0]) { - return 0; - } - // tslint:disable-next-line:no-bitwise - return a.targets[0].compareDocumentPosition(b.targets[0]) & 2 ? 1 : -1; + /** + * Removes a renderer. + * @param renderer The renderer. + */ + ValidationController.prototype.removeRenderer = function (renderer) { + var _this = this; + this.renderers.splice(this.renderers.indexOf(renderer), 1); + renderer.render({ + kind: 'reset', + render: [], + unrender: this.results.map(function (result) { return ({ result: result, elements: _this.elements.get(result) }); }) }); }; - ValidationErrorsCustomAttribute.prototype.interestingElements = function (elements) { + /** + * Registers a binding with the controller. + * @param binding The binding instance. + * @param target The DOM element. + * @param rules (optional) rules associated with the binding. Validator implementation specific. + */ + ValidationController.prototype.registerBinding = function (binding, target, rules) { + this.bindings.set(binding, { target: target, rules: rules, propertyInfo: null }); + }; + /** + * Unregisters a binding with the controller. + * @param binding The binding instance. + */ + ValidationController.prototype.unregisterBinding = function (binding) { + this.resetBinding(binding); + this.bindings.delete(binding); + }; + /** + * Interprets the instruction and returns a predicate that will identify + * relevant results in the list of rendered validation results. + */ + ValidationController.prototype.getInstructionPredicate = function (instruction) { var _this = this; - return elements.filter(function (e) { return _this.boundaryElement.contains(e); }); + if (instruction) { + var object_1 = instruction.object, propertyName_1 = instruction.propertyName, rules_1 = instruction.rules; + var predicate_1; + if (instruction.propertyName) { + predicate_1 = function (x) { return x.object === object_1 && x.propertyName === propertyName_1; }; + } + else { + predicate_1 = function (x) { return x.object === object_1; }; + } + if (rules_1) { + return function (x) { return predicate_1(x) && _this.validator.ruleExists(rules_1, x.rule); }; + } + return predicate_1; + } + else { + return function () { return true; }; + } + }; + /** + * Validates and renders results. + * @param instruction Optional. Instructions on what to validate. If undefined, all + * objects and bindings will be validated. + */ + ValidationController.prototype.validate = function (instruction) { + var _this = this; + // Get a function that will process the validation instruction. + var execute; + if (instruction) { + // tslint:disable-next-line:prefer-const + var object_2 = instruction.object, propertyName_2 = instruction.propertyName, rules_2 = instruction.rules; + // if rules were not specified, check the object map. + rules_2 = rules_2 || this.objects.get(object_2); + // property specified? + if (instruction.propertyName === undefined) { + // validate the specified object. + execute = function () { return _this.validator.validateObject(object_2, rules_2); }; + } + else { + // validate the specified property. + execute = function () { return _this.validator.validateProperty(object_2, propertyName_2, rules_2); }; + } + } + else { + // validate all objects and bindings. + execute = function () { + var promises = []; + for (var _i = 0, _a = Array.from(_this.objects); _i < _a.length; _i++) { + var _b = _a[_i], object = _b[0], rules = _b[1]; + promises.push(_this.validator.validateObject(object, rules)); + } + for (var _c = 0, _d = Array.from(_this.bindings); _c < _d.length; _c++) { + var _e = _d[_c], binding = _e[0], rules = _e[1].rules; + var propertyInfo = getPropertyInfo(binding.sourceExpression, binding.source); + if (!propertyInfo || _this.objects.has(propertyInfo.object)) { + continue; + } + promises.push(_this.validator.validateProperty(propertyInfo.object, propertyInfo.propertyName, rules)); + } + return Promise.all(promises).then(function (resultSets) { return resultSets.reduce(function (a, b) { return a.concat(b); }, []); }); + }; + } + // Wait for any existing validation to finish, execute the instruction, render the results. + this.validating = true; + var returnPromise = this.finishValidating + .then(execute) + .then(function (newResults) { + var predicate = _this.getInstructionPredicate(instruction); + var oldResults = _this.results.filter(predicate); + _this.processResultDelta('validate', oldResults, newResults); + if (returnPromise === _this.finishValidating) { + _this.validating = false; + } + var result = { + instruction: instruction, + valid: newResults.find(function (x) { return !x.valid; }) === undefined, + results: newResults + }; + _this.invokeCallbacks(instruction, result); + return result; + }) + .catch(function (exception) { + // recover, to enable subsequent calls to validate() + _this.validating = false; + _this.finishValidating = Promise.resolve(); + return Promise.reject(exception); + }); + this.finishValidating = returnPromise; + return returnPromise; + }; + /** + * Resets any rendered validation results (unrenders). + * @param instruction Optional. Instructions on what to reset. If unspecified all rendered results + * will be unrendered. + */ + ValidationController.prototype.reset = function (instruction) { + var predicate = this.getInstructionPredicate(instruction); + var oldResults = this.results.filter(predicate); + this.processResultDelta('reset', oldResults, []); + this.invokeCallbacks(instruction, null); + }; + /** + * Gets the elements associated with an object and propertyName (if any). + */ + ValidationController.prototype.getAssociatedElements = function (_a) { + var object = _a.object, propertyName = _a.propertyName; + var elements = []; + for (var _i = 0, _b = Array.from(this.bindings); _i < _b.length; _i++) { + var _c = _b[_i], binding = _c[0], target = _c[1].target; + var propertyInfo = getPropertyInfo(binding.sourceExpression, binding.source); + if (propertyInfo && propertyInfo.object === object && propertyInfo.propertyName === propertyName) { + elements.push(target); + } + } + return elements; }; - ValidationErrorsCustomAttribute.prototype.render = function (instruction) { - var _loop_1 = function (result) { - var index = this_1.errorsInternal.findIndex(function (x) { return x.error === result; }); - if (index !== -1) { - this_1.errorsInternal.splice(index, 1); + ValidationController.prototype.processResultDelta = function (kind, oldResults, newResults) { + // prepare the instruction. + var instruction = { + kind: kind, + render: [], + unrender: [] + }; + // create a shallow copy of newResults so we can mutate it without causing side-effects. + newResults = newResults.slice(0); + var _loop_1 = function (oldResult) { + // get the elements associated with the old result. + var elements = this_1.elements.get(oldResult); + // remove the old result from the element map. + this_1.elements.delete(oldResult); + // create the unrender instruction. + instruction.unrender.push({ result: oldResult, elements: elements }); + // determine if there's a corresponding new result for the old result we are unrendering. + var newResultIndex = newResults.findIndex(function (x) { return x.rule === oldResult.rule && x.object === oldResult.object && x.propertyName === oldResult.propertyName; }); + if (newResultIndex === -1) { + // no corresponding new result... simple remove. + this_1.results.splice(this_1.results.indexOf(oldResult), 1); + if (!oldResult.valid) { + this_1.errors.splice(this_1.errors.indexOf(oldResult), 1); + } + } + else { + // there is a corresponding new result... + var newResult = newResults.splice(newResultIndex, 1)[0]; + // get the elements that are associated with the new result. + var elements_1 = this_1.getAssociatedElements(newResult); + this_1.elements.set(newResult, elements_1); + // create a render instruction for the new result. + instruction.render.push({ result: newResult, elements: elements_1 }); + // do an in-place replacement of the old result with the new result. + // this ensures any repeats bound to this.results will not thrash. + this_1.results.splice(this_1.results.indexOf(oldResult), 1, newResult); + if (!oldResult.valid && newResult.valid) { + this_1.errors.splice(this_1.errors.indexOf(oldResult), 1); + } + else if (!oldResult.valid && !newResult.valid) { + this_1.errors.splice(this_1.errors.indexOf(oldResult), 1, newResult); + } + else if (!newResult.valid) { + this_1.errors.push(newResult); + } } }; var this_1 = this; - for (var _i = 0, _a = instruction.unrender; _i < _a.length; _i++) { - var result = _a[_i].result; - _loop_1(result); + // create unrender instructions from the old results. + for (var _i = 0, oldResults_1 = oldResults; _i < oldResults_1.length; _i++) { + var oldResult = oldResults_1[_i]; + _loop_1(oldResult); } - for (var _b = 0, _c = instruction.render; _b < _c.length; _b++) { - var _d = _c[_b], result = _d.result, elements = _d.elements; - if (result.valid) { - continue; - } - var targets = this.interestingElements(elements); - if (targets.length) { - this.errorsInternal.push({ error: result, targets: targets }); + // create render instructions from the remaining new results. + for (var _a = 0, newResults_1 = newResults; _a < newResults_1.length; _a++) { + var result = newResults_1[_a]; + var elements = this.getAssociatedElements(result); + instruction.render.push({ result: result, elements: elements }); + this.elements.set(result, elements); + this.results.push(result); + if (!result.valid) { + this.errors.push(result); } } - this.sort(); - this.errors = this.errorsInternal; - }; - ValidationErrorsCustomAttribute.prototype.bind = function () { - if (!this.controller) { - this.controller = this.controllerAccessor(); - } - // this will call render() with the side-effect of updating this.errors - this.controller.addRenderer(this); - }; - ValidationErrorsCustomAttribute.prototype.unbind = function () { - if (this.controller) { - this.controller.removeRenderer(this); + // render. + for (var _b = 0, _c = this.renderers; _b < _c.length; _b++) { + var renderer = _c[_b]; + renderer.render(instruction); } }; - __decorate([ - aureliaTemplating.bindable({ defaultBindingMode: aureliaBinding.bindingMode.oneWay }) - ], ValidationErrorsCustomAttribute.prototype, "controller", void 0); - __decorate([ - aureliaTemplating.bindable({ primaryProperty: true, defaultBindingMode: aureliaBinding.bindingMode.twoWay }) - ], ValidationErrorsCustomAttribute.prototype, "errors", void 0); - ValidationErrorsCustomAttribute = __decorate([ - aureliaTemplating.customAttribute('validation-errors') - ], ValidationErrorsCustomAttribute); - return ValidationErrorsCustomAttribute; - }()); - - var ValidationRendererCustomAttribute = /** @class */ (function () { - function ValidationRendererCustomAttribute() { - } - ValidationRendererCustomAttribute.prototype.created = function (view) { - this.container = view.container; - }; - ValidationRendererCustomAttribute.prototype.bind = function () { - this.controller = this.container.get(ValidationController); - this.renderer = this.container.get(this.value); - this.controller.addRenderer(this.renderer); - }; - ValidationRendererCustomAttribute.prototype.unbind = function () { - this.controller.removeRenderer(this.renderer); - this.controller = null; - this.renderer = null; - }; - ValidationRendererCustomAttribute = __decorate([ - aureliaTemplating.customAttribute('validation-renderer') - ], ValidationRendererCustomAttribute); - return ValidationRendererCustomAttribute; - }()); - - /** - * Sets, unsets and retrieves rules on an object or constructor function. - */ - var Rules = /** @class */ (function () { - function Rules() { - } /** - * Applies the rules to a target. + * Validates the property associated with a binding. */ - Rules.set = function (target, rules) { - if (target instanceof Function) { - target = target.prototype; + ValidationController.prototype.validateBinding = function (binding) { + if (!binding.isBound) { + return; } - Object.defineProperty(target, Rules.key, { enumerable: false, configurable: false, writable: true, value: rules }); - }; - /** - * Removes rules from a target. - */ - Rules.unset = function (target) { - if (target instanceof Function) { - target = target.prototype; + var propertyInfo = getPropertyInfo(binding.sourceExpression, binding.source); + var rules; + var registeredBinding = this.bindings.get(binding); + if (registeredBinding) { + rules = registeredBinding.rules; + registeredBinding.propertyInfo = propertyInfo; } - target[Rules.key] = null; + if (!propertyInfo) { + return; + } + var object = propertyInfo.object, propertyName = propertyInfo.propertyName; + this.validate({ object: object, propertyName: propertyName, rules: rules }); }; /** - * Retrieves the target's rules. + * Resets the results for a property associated with a binding. */ - Rules.get = function (target) { - return target[Rules.key] || null; + ValidationController.prototype.resetBinding = function (binding) { + var registeredBinding = this.bindings.get(binding); + var propertyInfo = getPropertyInfo(binding.sourceExpression, binding.source); + if (!propertyInfo && registeredBinding) { + propertyInfo = registeredBinding.propertyInfo; + } + if (registeredBinding) { + registeredBinding.propertyInfo = null; + } + if (!propertyInfo) { + return; + } + var object = propertyInfo.object, propertyName = propertyInfo.propertyName; + this.reset({ object: object, propertyName: propertyName }); }; /** - * The name of the property that stores the rules. + * Changes the controller's validateTrigger. + * @param newTrigger The new validateTrigger */ - Rules.key = '__rules__'; - return Rules; - }()); - - // tslint:disable:no-empty - var ExpressionVisitor = /** @class */ (function () { - function ExpressionVisitor() { - } - ExpressionVisitor.prototype.visitChain = function (chain) { - this.visitArgs(chain.expressions); - }; - ExpressionVisitor.prototype.visitBindingBehavior = function (behavior) { - behavior.expression.accept(this); - this.visitArgs(behavior.args); - }; - ExpressionVisitor.prototype.visitValueConverter = function (converter) { - converter.expression.accept(this); - this.visitArgs(converter.args); - }; - ExpressionVisitor.prototype.visitAssign = function (assign) { - assign.target.accept(this); - assign.value.accept(this); - }; - ExpressionVisitor.prototype.visitConditional = function (conditional) { - conditional.condition.accept(this); - conditional.yes.accept(this); - conditional.no.accept(this); - }; - ExpressionVisitor.prototype.visitAccessThis = function (access) { - access.ancestor = access.ancestor; - }; - ExpressionVisitor.prototype.visitAccessScope = function (access) { - access.name = access.name; - }; - ExpressionVisitor.prototype.visitAccessMember = function (access) { - access.object.accept(this); - }; - ExpressionVisitor.prototype.visitAccessKeyed = function (access) { - access.object.accept(this); - access.key.accept(this); - }; - ExpressionVisitor.prototype.visitCallScope = function (call) { - this.visitArgs(call.args); - }; - ExpressionVisitor.prototype.visitCallFunction = function (call) { - call.func.accept(this); - this.visitArgs(call.args); - }; - ExpressionVisitor.prototype.visitCallMember = function (call) { - call.object.accept(this); - this.visitArgs(call.args); - }; - ExpressionVisitor.prototype.visitPrefix = function (prefix) { - prefix.expression.accept(this); - }; - ExpressionVisitor.prototype.visitBinary = function (binary) { - binary.left.accept(this); - binary.right.accept(this); - }; - ExpressionVisitor.prototype.visitLiteralPrimitive = function (literal) { - literal.value = literal.value; - }; - ExpressionVisitor.prototype.visitLiteralArray = function (literal) { - this.visitArgs(literal.elements); - }; - ExpressionVisitor.prototype.visitLiteralObject = function (literal) { - this.visitArgs(literal.values); + ValidationController.prototype.changeTrigger = function (newTrigger) { + this.validateTrigger = newTrigger; + var bindings = Array.from(this.bindings.keys()); + for (var _i = 0, bindings_1 = bindings; _i < bindings_1.length; _i++) { + var binding = bindings_1[_i]; + var source = binding.source; + binding.unbind(); + binding.bind(source); + } }; - ExpressionVisitor.prototype.visitLiteralString = function (literal) { - literal.value = literal.value; + /** + * Revalidates the controller's current set of errors. + */ + ValidationController.prototype.revalidateErrors = function () { + for (var _i = 0, _a = this.errors; _i < _a.length; _i++) { + var _b = _a[_i], object = _b.object, propertyName = _b.propertyName, rule = _b.rule; + if (rule.__manuallyAdded__) { + continue; + } + var rules = [[rule]]; + this.validate({ object: object, propertyName: propertyName, rules: rules }); + } }; - ExpressionVisitor.prototype.visitArgs = function (args) { - for (var i = 0; i < args.length; i++) { - args[i].accept(this); + ValidationController.prototype.invokeCallbacks = function (instruction, result) { + if (this.eventCallbacks.length === 0) { + return; + } + var event = new ValidateEvent(result ? 'validate' : 'reset', this.errors, this.results, instruction || null, result); + for (var i = 0; i < this.eventCallbacks.length; i++) { + this.eventCallbacks[i](event); } }; - return ExpressionVisitor; + ValidationController.inject = [Validator, PropertyAccessorParser, GlobalValidationConfiguration]; + return ValidationController; }()); - var ValidationMessageParser = /** @class */ (function () { - function ValidationMessageParser(bindinqLanguage) { - this.bindinqLanguage = bindinqLanguage; - this.emptyStringExpression = new aureliaBinding.LiteralString(''); - this.nullExpression = new aureliaBinding.LiteralPrimitive(null); - this.undefinedExpression = new aureliaBinding.LiteralPrimitive(undefined); - this.cache = {}; + /** + * Binding behavior. Indicates the bound property should be validated. + */ + var ValidateBindingBehaviorBase = /** @class */ (function () { + function ValidateBindingBehaviorBase(taskQueue) { + this.taskQueue = taskQueue; } - ValidationMessageParser.prototype.parse = function (message) { - if (this.cache[message] !== undefined) { - return this.cache[message]; + ValidateBindingBehaviorBase.prototype.bind = function (binding, source, rulesOrController, rules) { + var _this = this; + // identify the target element. + var target = getTargetDOMElement(binding, source); + // locate the controller. + var controller; + if (rulesOrController instanceof ValidationController) { + controller = rulesOrController; } - var parts = this.bindinqLanguage.parseInterpolation(null, message); - if (parts === null) { - return new aureliaBinding.LiteralString(message); + else { + controller = source.container.get(aureliaDependencyInjection.Optional.of(ValidationController)); + rules = rulesOrController; } - var expression = new aureliaBinding.LiteralString(parts[0]); - for (var i = 1; i < parts.length; i += 2) { - expression = new aureliaBinding.Binary('+', expression, new aureliaBinding.Binary('+', this.coalesce(parts[i]), new aureliaBinding.LiteralString(parts[i + 1]))); + 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); + // tslint:disable-next-line:no-bitwise + if (trigger & exports.validateTrigger.change) { + binding.vbbUpdateSource = binding.updateSource; + // tslint:disable-next-line:only-arrow-functions + // tslint:disable-next-line:space-before-function-paren + binding.updateSource = function (value) { + this.vbbUpdateSource(value); + this.validationController.validateBinding(this); + }; + } + // tslint:disable-next-line:no-bitwise + if (trigger & exports.validateTrigger.blur) { + binding.validateBlurHandler = function () { + _this.taskQueue.queueMicroTask(function () { return controller.validateBinding(binding); }); + }; + binding.validateTarget = target; + target.addEventListener('blur', binding.validateBlurHandler); + } + if (trigger !== exports.validateTrigger.manual) { + binding.standardUpdateTarget = binding.updateTarget; + // tslint:disable-next-line:only-arrow-functions + // tslint:disable-next-line:space-before-function-paren + binding.updateTarget = function (value) { + this.standardUpdateTarget(value); + this.validationController.resetBinding(this); + }; } - MessageExpressionValidator.validate(expression, message); - this.cache[message] = expression; - return expression; - }; - ValidationMessageParser.prototype.coalesce = function (part) { - // part === null || part === undefined ? '' : part - return new aureliaBinding.Conditional(new aureliaBinding.Binary('||', new aureliaBinding.Binary('===', part, this.nullExpression), new aureliaBinding.Binary('===', part, this.undefinedExpression)), this.emptyStringExpression, new aureliaBinding.CallMember(part, 'toString', [])); - }; - ValidationMessageParser.inject = [aureliaTemplating.BindingLanguage]; - return ValidationMessageParser; - }()); - var MessageExpressionValidator = /** @class */ (function (_super) { - __extends(MessageExpressionValidator, _super); - function MessageExpressionValidator(originalMessage) { - var _this = _super.call(this) || this; - _this.originalMessage = originalMessage; - return _this; - } - MessageExpressionValidator.validate = function (expression, originalMessage) { - var visitor = new MessageExpressionValidator(originalMessage); - expression.accept(visitor); }; - MessageExpressionValidator.prototype.visitAccessScope = function (access) { - if (access.ancestor !== 0) { - throw new Error('$parent is not permitted in validation message expressions.'); + ValidateBindingBehaviorBase.prototype.unbind = function (binding) { + // reset the binding to it's original state. + if (binding.vbbUpdateSource) { + binding.updateSource = binding.vbbUpdateSource; + binding.vbbUpdateSource = null; } - if (['displayName', 'propertyName', 'value', 'object', 'config', 'getDisplayName'].indexOf(access.name) !== -1) { - LogManager.getLogger('aurelia-validation') - // tslint:disable-next-line:max-line-length - .warn("Did you mean to use \"$" + access.name + "\" instead of \"" + access.name + "\" in this validation message template: \"" + this.originalMessage + "\"?"); + 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 MessageExpressionValidator; - }(ExpressionVisitor)); + return ValidateBindingBehaviorBase; + }()); /** - * Dictionary of validation messages. [messageKey]: messageExpression + * Binding behavior. Indicates the bound property should be validated + * when the validate trigger specified by the associated controller's + * validateTrigger property occurs. */ - var validationMessages = { - /** - * The default validation message. Used with rules that have no standard message. - */ - default: "${$displayName} is invalid.", - required: "${$displayName} is required.", - matches: "${$displayName} is not correctly formatted.", - email: "${$displayName} is not a valid email.", - minLength: "${$displayName} must be at least ${$config.length} character${$config.length === 1 ? '' : 's'}.", - maxLength: "${$displayName} cannot be longer than ${$config.length} character${$config.length === 1 ? '' : 's'}.", - minItems: "${$displayName} must contain at least ${$config.count} item${$config.count === 1 ? '' : 's'}.", - maxItems: "${$displayName} cannot contain more than ${$config.count} item${$config.count === 1 ? '' : 's'}.", - min: "${$displayName} must be at least ${$config.constraint}.", - max: "${$displayName} must be at most ${$config.constraint}.", - range: "${$displayName} must be between or equal to ${$config.min} and ${$config.max}.", - between: "${$displayName} must be between but not equal to ${$config.min} and ${$config.max}.", - equals: "${$displayName} must be ${$config.expectedValue}.", - }; + var ValidateBindingBehavior = /** @class */ (function (_super) { + __extends(ValidateBindingBehavior, _super); + function ValidateBindingBehavior() { + return _super !== null && _super.apply(this, arguments) || this; + } + ValidateBindingBehavior.prototype.getValidateTrigger = function (controller) { + return controller.validateTrigger; + }; + ValidateBindingBehavior.inject = [aureliaTaskQueue.TaskQueue]; + ValidateBindingBehavior = __decorate([ + aureliaBinding.bindingBehavior('validate') + ], ValidateBindingBehavior); + return ValidateBindingBehavior; + }(ValidateBindingBehaviorBase)); /** - * Retrieves validation messages and property display names. + * 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 ValidationMessageProvider = /** @class */ (function () { - function ValidationMessageProvider(parser) { - this.parser = parser; + var ValidateManuallyBindingBehavior = /** @class */ (function (_super) { + __extends(ValidateManuallyBindingBehavior, _super); + function ValidateManuallyBindingBehavior() { + return _super !== null && _super.apply(this, arguments) || this; } - /** - * Returns a message binding expression that corresponds to the key. - * @param key The message key. - */ - ValidationMessageProvider.prototype.getMessage = function (key) { - var message; - if (key in validationMessages) { - message = validationMessages[key]; - } - else { - message = validationMessages['default']; - } - return this.parser.parse(message); + ValidateManuallyBindingBehavior.prototype.getValidateTrigger = function () { + return exports.validateTrigger.manual; }; - /** - * Formulates a property display name using the property name and the configured - * displayName (if provided). - * Override this with your own custom logic. - * @param propertyName The property name. - */ - ValidationMessageProvider.prototype.getDisplayName = function (propertyName, displayName) { - if (displayName !== null && displayName !== undefined) { - return (displayName instanceof Function) ? displayName() : displayName; - } - // split on upper-case letters. - var words = propertyName.toString().split(/(?=[A-Z])/).join(' '); - // capitalize first letter. - return words.charAt(0).toUpperCase() + words.slice(1); + ValidateManuallyBindingBehavior.inject = [aureliaTaskQueue.TaskQueue]; + ValidateManuallyBindingBehavior = __decorate([ + aureliaBinding.bindingBehavior('validateManually') + ], ValidateManuallyBindingBehavior); + return ValidateManuallyBindingBehavior; + }(ValidateBindingBehaviorBase)); + /** + * Binding behavior. Indicates the bound property should be validated + * when the associated element blurs. + */ + var ValidateOnBlurBindingBehavior = /** @class */ (function (_super) { + __extends(ValidateOnBlurBindingBehavior, _super); + function ValidateOnBlurBindingBehavior() { + return _super !== null && _super.apply(this, arguments) || this; + } + ValidateOnBlurBindingBehavior.prototype.getValidateTrigger = function () { + return exports.validateTrigger.blur; + }; + ValidateOnBlurBindingBehavior.inject = [aureliaTaskQueue.TaskQueue]; + ValidateOnBlurBindingBehavior = __decorate([ + aureliaBinding.bindingBehavior('validateOnBlur') + ], ValidateOnBlurBindingBehavior); + 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. + */ + var ValidateOnChangeBindingBehavior = /** @class */ (function (_super) { + __extends(ValidateOnChangeBindingBehavior, _super); + function ValidateOnChangeBindingBehavior() { + return _super !== null && _super.apply(this, arguments) || this; + } + ValidateOnChangeBindingBehavior.prototype.getValidateTrigger = function () { + return exports.validateTrigger.change; + }; + ValidateOnChangeBindingBehavior.inject = [aureliaTaskQueue.TaskQueue]; + ValidateOnChangeBindingBehavior = __decorate([ + aureliaBinding.bindingBehavior('validateOnChange') + ], ValidateOnChangeBindingBehavior); + 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. + */ + var ValidateOnChangeOrBlurBindingBehavior = /** @class */ (function (_super) { + __extends(ValidateOnChangeOrBlurBindingBehavior, _super); + function ValidateOnChangeOrBlurBindingBehavior() { + return _super !== null && _super.apply(this, arguments) || this; + } + ValidateOnChangeOrBlurBindingBehavior.prototype.getValidateTrigger = function () { + return exports.validateTrigger.changeOrBlur; }; - ValidationMessageProvider.inject = [ValidationMessageParser]; - return ValidationMessageProvider; - }()); + ValidateOnChangeOrBlurBindingBehavior.inject = [aureliaTaskQueue.TaskQueue]; + ValidateOnChangeOrBlurBindingBehavior = __decorate([ + aureliaBinding.bindingBehavior('validateOnChangeOrBlur') + ], ValidateOnChangeOrBlurBindingBehavior); + return ValidateOnChangeOrBlurBindingBehavior; + }(ValidateBindingBehaviorBase)); /** - * Validates. - * Responsible for validating objects and properties. + * Creates ValidationController instances. */ - var StandardValidator = /** @class */ (function (_super) { - __extends(StandardValidator, _super); - function StandardValidator(messageProvider, resources) { - var _this = _super.call(this) || this; - _this.messageProvider = messageProvider; - _this.lookupFunctions = resources.lookupFunctions; - _this.getDisplayName = messageProvider.getDisplayName.bind(messageProvider); - return _this; + var ValidationControllerFactory = /** @class */ (function () { + function ValidationControllerFactory(container) { + this.container = container; } - /** - * Validates the specified property. - * @param object The object to validate. - * @param propertyName The name of the property to validate. - * @param rules Optional. If unspecified, the rules will be looked up using the metadata - * for the object created by ValidationRules....on(class/object) - */ - StandardValidator.prototype.validateProperty = function (object, propertyName, rules) { - return this.validate(object, propertyName, rules || null); + ValidationControllerFactory.get = function (container) { + return new ValidationControllerFactory(container); }; /** - * Validates all rules for specified object and it's properties. - * @param object The object to validate. - * @param rules Optional. If unspecified, the rules will be looked up using the metadata - * for the object created by ValidationRules....on(class/object) + * Creates a new controller instance. */ - StandardValidator.prototype.validateObject = function (object, rules) { - return this.validate(object, null, rules || null); + ValidationControllerFactory.prototype.create = function (validator) { + if (!validator) { + validator = this.container.get(Validator); + } + var propertyParser = this.container.get(PropertyAccessorParser); + var config = this.container.get(GlobalValidationConfiguration); + return new ValidationController(validator, propertyParser, config); }; /** - * Determines whether a rule exists in a set of rules. - * @param rules The rules to search. - * @parem rule The rule to find. + * 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. */ - StandardValidator.prototype.ruleExists = function (rules, rule) { - var i = rules.length; - while (i--) { - if (rules[i].indexOf(rule) !== -1) { - return true; - } - } - return false; + ValidationControllerFactory.prototype.createForCurrentScope = function (validator) { + var controller = this.create(validator); + this.container.registerInstance(ValidationController, controller); + return controller; }; - StandardValidator.prototype.getMessage = function (rule, object, value) { - var expression = rule.message || this.messageProvider.getMessage(rule.messageKey); - // tslint:disable-next-line:prefer-const - var _a = rule.property, propertyName = _a.name, displayName = _a.displayName; - if (propertyName !== null) { - displayName = this.messageProvider.getDisplayName(propertyName, displayName); - } - var overrideContext = { - $displayName: displayName, - $propertyName: propertyName, - $value: value, - $object: object, - $config: rule.config, - // returns the name of a given property, given just the property name (irrespective of the property's displayName) - // split on capital letters, first letter ensured to be capitalized - $getDisplayName: this.getDisplayName - }; - return expression.evaluate({ bindingContext: object, overrideContext: overrideContext }, this.lookupFunctions); + return ValidationControllerFactory; + }()); + ValidationControllerFactory['protocol:aurelia:resolver'] = true; + + var ValidationErrorsCustomAttribute = /** @class */ (function () { + function ValidationErrorsCustomAttribute(boundaryElement, controllerAccessor) { + this.boundaryElement = boundaryElement; + this.controllerAccessor = controllerAccessor; + this.controller = null; + this.errors = []; + this.errorsInternal = []; + } + ValidationErrorsCustomAttribute.inject = function () { + return [aureliaPal.DOM.Element, aureliaDependencyInjection.Lazy.of(ValidationController)]; }; - StandardValidator.prototype.validateRuleSequence = function (object, propertyName, ruleSequence, sequence, results) { - var _this = this; - // are we validating all properties or a single property? - var validateAllProperties = propertyName === null || propertyName === undefined; - var rules = ruleSequence[sequence]; - var allValid = true; - // validate each rule. - var promises = []; - var _loop_1 = function (i) { - var rule = rules[i]; - // is the rule related to the property we're validating. - // tslint:disable-next-line:triple-equals | Use loose equality for property keys - if (!validateAllProperties && rule.property.name != propertyName) { - return "continue"; - } - // is this a conditional rule? is the condition met? - if (rule.when && !rule.when(object)) { - return "continue"; + ValidationErrorsCustomAttribute.prototype.sort = function () { + this.errorsInternal.sort(function (a, b) { + if (a.targets[0] === b.targets[0]) { + return 0; } - // validate. - var value = rule.property.name === null ? object : object[rule.property.name]; - var promiseOrBoolean = rule.condition(value, object); - if (!(promiseOrBoolean instanceof Promise)) { - promiseOrBoolean = Promise.resolve(promiseOrBoolean); + // tslint:disable-next-line:no-bitwise + return a.targets[0].compareDocumentPosition(b.targets[0]) & 2 ? 1 : -1; + }); + }; + ValidationErrorsCustomAttribute.prototype.interestingElements = function (elements) { + var _this = this; + return elements.filter(function (e) { return _this.boundaryElement.contains(e); }); + }; + ValidationErrorsCustomAttribute.prototype.render = function (instruction) { + var _loop_1 = function (result) { + var index = this_1.errorsInternal.findIndex(function (x) { return x.error === result; }); + if (index !== -1) { + this_1.errorsInternal.splice(index, 1); } - promises.push(promiseOrBoolean.then(function (valid) { - var message = valid ? null : _this.getMessage(rule, object, value); - results.push(new ValidateResult(rule, object, rule.property.name, valid, message)); - allValid = allValid && valid; - return valid; - })); }; - for (var i = 0; i < rules.length; i++) { - _loop_1(i); + var this_1 = this; + for (var _i = 0, _a = instruction.unrender; _i < _a.length; _i++) { + var result = _a[_i].result; + _loop_1(result); } - return Promise.all(promises) - .then(function () { - sequence++; - if (allValid && sequence < ruleSequence.length) { - return _this.validateRuleSequence(object, propertyName, ruleSequence, sequence, results); + for (var _b = 0, _c = instruction.render; _b < _c.length; _b++) { + var _d = _c[_b], result = _d.result, elements = _d.elements; + if (result.valid) { + continue; } - return results; - }); + var targets = this.interestingElements(elements); + if (targets.length) { + this.errorsInternal.push({ error: result, targets: targets }); + } + } + this.sort(); + this.errors = this.errorsInternal; }; - StandardValidator.prototype.validate = function (object, propertyName, rules) { - // rules specified? - if (!rules) { - // no. attempt to locate the rules. - rules = Rules.get(object); + ValidationErrorsCustomAttribute.prototype.bind = function () { + if (!this.controller) { + this.controller = this.controllerAccessor(); } - // any rules? - if (!rules || rules.length === 0) { - return Promise.resolve([]); + // this will call render() with the side-effect of updating this.errors + this.controller.addRenderer(this); + }; + ValidationErrorsCustomAttribute.prototype.unbind = function () { + if (this.controller) { + this.controller.removeRenderer(this); } - return this.validateRuleSequence(object, propertyName, rules, 0, []); }; - StandardValidator.inject = [ValidationMessageProvider, aureliaTemplating.ViewResources]; - return StandardValidator; - }(Validator)); + __decorate([ + aureliaTemplating.bindable({ defaultBindingMode: aureliaBinding.bindingMode.oneWay }) + ], ValidationErrorsCustomAttribute.prototype, "controller", void 0); + __decorate([ + aureliaTemplating.bindable({ primaryProperty: true, defaultBindingMode: aureliaBinding.bindingMode.twoWay }) + ], ValidationErrorsCustomAttribute.prototype, "errors", void 0); + ValidationErrorsCustomAttribute = __decorate([ + aureliaTemplating.customAttribute('validation-errors') + ], ValidationErrorsCustomAttribute); + return ValidationErrorsCustomAttribute; + }()); + + var ValidationRendererCustomAttribute = /** @class */ (function () { + function ValidationRendererCustomAttribute() { + } + ValidationRendererCustomAttribute.prototype.created = function (view) { + this.container = view.container; + }; + ValidationRendererCustomAttribute.prototype.bind = function () { + this.controller = this.container.get(ValidationController); + this.renderer = this.container.get(this.value); + this.controller.addRenderer(this.renderer); + }; + ValidationRendererCustomAttribute.prototype.unbind = function () { + this.controller.removeRenderer(this.renderer); + this.controller = null; + this.renderer = null; + }; + ValidationRendererCustomAttribute = __decorate([ + aureliaTemplating.customAttribute('validation-renderer') + ], ValidationRendererCustomAttribute); + return ValidationRendererCustomAttribute; + }()); /** * Part of the fluent rule API. Enables customizing property rules. @@ -1407,12 +1449,12 @@ define('aurelia-validation', ['exports', 'aurelia-pal', 'aurelia-binding', 'aure * @param args The rule's arguments. */ FluentRuleCustomizer.prototype.satisfiesRule = function (name) { + var _a; var args = []; for (var _i = 1; _i < arguments.length; _i++) { args[_i - 1] = arguments[_i]; } - var _a; - return (_a = this.fluentRules).satisfiesRule.apply(_a, [name].concat(args)); + return (_a = this.fluentRules).satisfiesRule.apply(_a, __spreadArrays([name], args)); }; /** * Applies the "required" rule to the property. @@ -1552,14 +1594,14 @@ define('aurelia-validation', ['exports', 'aurelia-pal', 'aurelia-binding', 'aure // standard rule? rule = this[name]; if (rule instanceof Function) { - return rule.call.apply(rule, [this].concat(args)); + return rule.call.apply(rule, __spreadArrays([this], args)); } throw new Error("Rule with name \"" + name + "\" does not exist."); } var config = rule.argsToConfig ? rule.argsToConfig.apply(rule, args) : undefined; return this.satisfies(function (value, obj) { var _a; - return (_a = rule.condition).call.apply(_a, [_this, value, obj].concat(args)); + return (_a = rule.condition).call.apply(_a, __spreadArrays([_this, value, obj], args)); }, config) .withMessageKey(name); }; @@ -1804,28 +1846,6 @@ define('aurelia-validation', ['exports', 'aurelia-pal', 'aurelia-binding', 'aure }()); // Exports - /** - * Aurelia Validation Configuration API - */ - var AureliaValidationConfiguration = /** @class */ (function () { - function AureliaValidationConfiguration() { - this.validatorType = StandardValidator; - } - /** - * Use a custom Validator implementation. - */ - AureliaValidationConfiguration.prototype.customValidator = function (type) { - this.validatorType = type; - }; - /** - * Applies the configuration. - */ - AureliaValidationConfiguration.prototype.apply = function (container) { - var validator = container.get(this.validatorType); - container.registerInstance(Validator, validator); - }; - return AureliaValidationConfiguration; - }()); /** * Configures the plugin. */ @@ -1838,7 +1858,7 @@ define('aurelia-validation', ['exports', 'aurelia-pal', 'aurelia-binding', 'aure var propertyParser = frameworkConfig.container.get(PropertyAccessorParser); ValidationRules.initialize(messageParser, propertyParser); // configure... - var config = new AureliaValidationConfiguration(); + var config = new GlobalValidationConfiguration(); if (callback instanceof Function) { callback(config); } @@ -1849,8 +1869,8 @@ define('aurelia-validation', ['exports', 'aurelia-pal', 'aurelia-binding', 'aure } } - exports.AureliaValidationConfiguration = AureliaValidationConfiguration; exports.configure = configure; + exports.GlobalValidationConfiguration = GlobalValidationConfiguration; exports.getTargetDOMElement = getTargetDOMElement; exports.getPropertyInfo = getPropertyInfo; exports.PropertyAccessorParser = PropertyAccessorParser; diff --git a/dist/aurelia-validation.d.ts b/dist/aurelia-validation.d.ts index d5f7f566..ba8b1cf4 100644 --- a/dist/aurelia-validation.d.ts +++ b/dist/aurelia-validation.d.ts @@ -1,4 +1,4 @@ -import { AccessKeyed, AccessMember, AccessScope, Binary, Binding, BindingBehavior, CallMember, Conditional, Expression, Parser, Scope, ValueConverter } from 'aurelia-binding'; +import { AccessKeyed, AccessMember, AccessScope, Binary, Binding, BindingBehavior, CallMember, Conditional, Expression, LiteralPrimitive, LiteralString, Parser, Scope, ValueConverter } from 'aurelia-binding'; import { Container, Lazy } from 'aurelia-dependency-injection'; import { DOM } from 'aurelia-pal'; import { TaskQueue } from 'aurelia-task-queue'; @@ -27,6 +27,74 @@ export declare class ValidateResult { constructor(rule: any, object: any, propertyName: string | number | null, valid: boolean, message?: string | null); toString(): string | null; } +/** + * Validates objects and properties. + */ +export declare abstract class Validator { + /** + * Validates the specified property. + * @param object The object to validate. + * @param propertyName The name of the property to validate. + * @param rules Optional. If unspecified, the implementation should lookup the rules for the + * specified object. This may not be possible for all implementations of this interface. + */ + abstract validateProperty(object: any, propertyName: string, rules?: any): Promise; + /** + * Validates all rules for specified object and it's properties. + * @param object The object to validate. + * @param rules Optional. If unspecified, the implementation should lookup the rules for the + * specified object. This may not be possible for all implementations of this interface. + */ + abstract validateObject(object: any, rules?: any): Promise; + /** + * Determines whether a rule exists in a set of rules. + * @param rules The rules to search. + * @parem rule The rule to find. + */ + abstract ruleExists(rules: any, rule: any): boolean; +} +/** + * Validation triggers. + */ +export declare enum 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 = 1, + /** + * Validate the binding when it updates the model due to a change in the view. + */ + change = 2, + /** + * Validate the binding when the binding's target element fires a DOM "blur" event and + * when it updates the model due to a change in the view. + */ + changeOrBlur = 3 +} +export declare type ValidatorCtor = new (...args: any[]) => Validator; +/** + * Aurelia Validation Configuration API + */ +export declare class GlobalValidationConfiguration { + static DEFAULT_VALIDATION_TRIGGER: validateTrigger; + private validatorType; + private validationTrigger; + /** + * Use a custom Validator implementation. + */ + customValidator(type: ValidatorCtor): this; + defaultValidationTrigger(trigger: validateTrigger): this; + getDefaultValidationTrigger(): validateTrigger; + /** + * Applies the configuration. + */ + apply(container: Container): void; +} /** * Instructions for the validation controller's validate method. */ @@ -85,55 +153,6 @@ export declare class PropertyAccessorParser { parse(property: string | number | PropertyAccessor): string | number; } export declare function getAccessorExpression(fn: string): string; -/** - * Validates objects and properties. - */ -export declare abstract class Validator { - /** - * Validates the specified property. - * @param object The object to validate. - * @param propertyName The name of the property to validate. - * @param rules Optional. If unspecified, the implementation should lookup the rules for the - * specified object. This may not be possible for all implementations of this interface. - */ - abstract validateProperty(object: any, propertyName: string, rules?: any): Promise; - /** - * Validates all rules for specified object and it's properties. - * @param object The object to validate. - * @param rules Optional. If unspecified, the implementation should lookup the rules for the - * specified object. This may not be possible for all implementations of this interface. - */ - abstract validateObject(object: any, rules?: any): Promise; - /** - * Determines whether a rule exists in a set of rules. - * @param rules The rules to search. - * @parem rule The rule to find. - */ - abstract ruleExists(rules: any, rule: any): boolean; -} -/** - * Validation triggers. - */ -export declare enum 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 = 1, - /** - * Validate the binding when it updates the model due to a change in the view. - */ - change = 2, - /** - * Validate the binding when the binding's target element fires a DOM "blur" event and - * when it updates the model due to a change in the view. - */ - changeOrBlur = 3 -} /** * A result to render (or unrender) and the associated elements (if any) */ @@ -242,7 +261,7 @@ export declare class ValidateEvent { export declare class ValidationController { private validator; private propertyParser; - static inject: (typeof PropertyAccessorParser | typeof Validator)[]; + static inject: (typeof Validator | typeof GlobalValidationConfiguration | typeof PropertyAccessorParser)[]; private bindings; private renderers; /** @@ -265,7 +284,7 @@ export declare class ValidationController { validateTrigger: validateTrigger; private finishValidating; private eventCallbacks; - constructor(validator: Validator, propertyParser: PropertyAccessorParser); + constructor(validator: Validator, propertyParser: PropertyAccessorParser, config?: GlobalValidationConfiguration); /** * Subscribe to controller validate and reset events. These events occur when the * controller's "validate"" and "reset" methods are called. @@ -499,35 +518,24 @@ export declare class Rules { */ static get(target: any): Rule[][] | null; } -export declare type Chain = any; -export declare type Assign = any; -export declare type AccessThis = any; -export declare type AccessScope = any; -export declare type CallScope = any; -export declare type CallFunction = any; -export declare type PrefixNot = any; -export declare type LiteralPrimitive = any; -export declare type LiteralArray = any; -export declare type LiteralObject = any; -export declare type LiteralString = any; declare class ExpressionVisitor { - visitChain(chain: Chain): void; + visitChain(chain: any): void; visitBindingBehavior(behavior: BindingBehavior): void; visitValueConverter(converter: ValueConverter): void; - visitAssign(assign: Assign): void; + visitAssign(assign: any): void; visitConditional(conditional: Conditional): void; - visitAccessThis(access: AccessThis): void; + visitAccessThis(access: any): void; visitAccessScope(access: AccessScope): void; visitAccessMember(access: AccessMember): void; visitAccessKeyed(access: AccessKeyed): void; - visitCallScope(call: CallScope): void; - visitCallFunction(call: CallFunction): void; + visitCallScope(call: any): void; + visitCallFunction(call: any): void; visitCallMember(call: CallMember): void; - visitPrefix(prefix: PrefixNot): void; + visitPrefix(prefix: any): void; visitBinary(binary: Binary): void; visitLiteralPrimitive(literal: LiteralPrimitive): void; - visitLiteralArray(literal: LiteralArray): void; - visitLiteralObject(literal: LiteralObject): void; + visitLiteralArray(literal: any): void; + visitLiteralObject(literal: any): void; visitLiteralString(literal: LiteralString): void; private visitArgs; } @@ -656,7 +664,7 @@ export declare class FluentRuleCustomizer { /** * Rules that have been defined using the fluent API. */ - readonly rules: Rule[][]; + get rules(): Rule[][]; /** * Applies the rules to a class or object, making them discoverable by the StandardValidator. * @param target A class or object. @@ -917,26 +925,10 @@ export interface Parsers { message: ValidationMessageParser; property: PropertyAccessorParser; } -/** - * Aurelia Validation Configuration API - */ -export declare class AureliaValidationConfiguration { - private validatorType; - /** - * Use a custom Validator implementation. - */ - customValidator(type: { - new (...args: any[]): Validator; - }): void; - /** - * Applies the configuration. - */ - apply(container: Container): void; -} /** * Configures the plugin. */ export declare function configure(frameworkConfig: { container: Container; globalResources?: (...resources: any[]) => any; -}, callback?: (config: AureliaValidationConfiguration) => void): void; \ No newline at end of file +}, callback?: (config: GlobalValidationConfiguration) => void): void; \ No newline at end of file diff --git a/dist/commonjs/aurelia-validation.js b/dist/commonjs/aurelia-validation.js index 49e45b31..75c9ef1c 100644 --- a/dist/commonjs/aurelia-validation.js +++ b/dist/commonjs/aurelia-validation.js @@ -2,118 +2,21 @@ Object.defineProperty(exports, '__esModule', { value: true }); -var aureliaPal = require('aurelia-pal'); var aureliaBinding = require('aurelia-binding'); -var aureliaDependencyInjection = require('aurelia-dependency-injection'); -var aureliaTaskQueue = require('aurelia-task-queue'); var aureliaTemplating = require('aurelia-templating'); var LogManager = require('aurelia-logging'); +var aureliaPal = require('aurelia-pal'); +var aureliaDependencyInjection = require('aurelia-dependency-injection'); +var aureliaTaskQueue = require('aurelia-task-queue'); /** - * 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. - */ -function getTargetDOMElement(binding, view) { - var target = binding.target; - // DOM element - if (target instanceof Element) { - return target; - } - // custom element or custom attribute - // tslint:disable-next-line:prefer-const - 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(aureliaPal.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 + "\"."); -} - -function getObject(expression, objectExpression, source) { - var value = objectExpression.evaluate(source, null); - if (value === null || value === undefined || value instanceof Object) { - return value; - } - // tslint:disable-next-line:max-line-length - throw new Error("The '" + objectExpression + "' part of '" + expression + "' evaluates to " + value + " instead of an object, null or undefined."); -} -/** - * Retrieves the object and property name for the specified expression. - * @param expression The expression - * @param source The scope + * Validates objects and properties. */ -function getPropertyInfo(expression, source) { - var originalExpression = expression; - while (expression instanceof aureliaBinding.BindingBehavior || expression instanceof aureliaBinding.ValueConverter) { - expression = expression.expression; - } - var object; - var propertyName; - if (expression instanceof aureliaBinding.AccessScope) { - object = aureliaBinding.getContextFor(expression.name, source, expression.ancestor); - propertyName = expression.name; - } - else if (expression instanceof aureliaBinding.AccessMember) { - object = getObject(originalExpression, expression.object, source); - propertyName = expression.name; - } - else if (expression instanceof aureliaBinding.AccessKeyed) { - object = getObject(originalExpression, expression.object, source); - propertyName = expression.key.evaluate(source); - } - else { - throw new Error("Expression '" + originalExpression + "' is not compatible with the validate binding-behavior."); - } - if (object === null || object === undefined) { - return null; - } - return { object: object, propertyName: propertyName }; -} - -function isString(value) { - return Object.prototype.toString.call(value) === '[object String]'; -} -function isNumber(value) { - return Object.prototype.toString.call(value) === '[object Number]'; -} - -var PropertyAccessorParser = /** @class */ (function () { - function PropertyAccessorParser(parser) { - this.parser = parser; - } - PropertyAccessorParser.prototype.parse = function (property) { - if (isString(property) || isNumber(property)) { - return property; - } - var accessorText = getAccessorExpression(property.toString()); - var accessor = this.parser.parse(accessorText); - if (accessor instanceof aureliaBinding.AccessScope - || accessor instanceof aureliaBinding.AccessMember && accessor.object instanceof aureliaBinding.AccessScope) { - return accessor.name; - } - throw new Error("Invalid property expression: \"" + accessor + "\""); - }; - PropertyAccessorParser.inject = [aureliaBinding.Parser]; - return PropertyAccessorParser; -}()); -function getAccessorExpression(fn) { - /* tslint:disable:max-line-length */ - var classic = /^function\s*\([$_\w\d]+\)\s*\{(?:\s*"use strict";)?\s*(?:[$_\w\d.['"\]+;]+)?\s*return\s+[$_\w\d]+\.([$_\w\d]+)\s*;?\s*\}$/; - /* tslint:enable:max-line-length */ - var arrow = /^\(?[$_\w\d]+\)?\s*=>\s*[$_\w\d]+\.([$_\w\d]+)$/; - var match = classic.exec(fn) || arrow.exec(fn); - if (match === null) { - throw new Error("Unable to parse accessor function:\n" + fn); +var Validator = /** @class */ (function () { + function Validator() { } - return match[1]; -} + return Validator; +}()); /*! ***************************************************************************** Copyright (c) Microsoft Corporation. All rights reserved. @@ -149,41 +52,16 @@ function __decorate(decorators, target, key, desc) { if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc); else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r; return c > 3 && r && Object.defineProperty(target, key, r), r; +} + +function __spreadArrays() { + for (var s = 0, i = 0, il = arguments.length; i < il; i++) s += arguments[i].length; + for (var r = Array(s), k = 0, i = 0; i < il; i++) + for (var a = arguments[i], j = 0, jl = a.length; j < jl; j++, k++) + r[k] = a[j]; + return r; } -/** - * Validation triggers. - */ -(function (validateTrigger) { - /** - * Manual validation. Use the controller's `validate()` and `reset()` methods - * to validate all bindings. - */ - validateTrigger[validateTrigger["manual"] = 0] = "manual"; - /** - * Validate the binding when the binding's target element fires a DOM "blur" event. - */ - validateTrigger[validateTrigger["blur"] = 1] = "blur"; - /** - * Validate the binding when it updates the model due to a change in the view. - */ - validateTrigger[validateTrigger["change"] = 2] = "change"; - /** - * 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. - */ - validateTrigger[validateTrigger["changeOrBlur"] = 3] = "changeOrBlur"; -})(exports.validateTrigger || (exports.validateTrigger = {})); - -/** - * Validates objects and properties. - */ -var Validator = /** @class */ (function () { - function Validator() { - } - return Validator; -}()); - /** * The result of validating an individual validation rule. */ @@ -210,1102 +88,1266 @@ var ValidateResult = /** @class */ (function () { return ValidateResult; }()); -var ValidateEvent = /** @class */ (function () { - function ValidateEvent( - /** - * The type of validate event. Either "validate" or "reset". - */ - type, - /** - * The controller's current array of errors. For an array containing both - * failed rules and passed rules, use the "results" property. - */ - errors, - /** - * The controller's current array of validate results. This - * includes both passed rules and failed rules. For an array of only failed rules, - * use the "errors" property. - */ - results, - /** - * The instruction passed to the "validate" or "reset" event. Will be null when - * the controller's validate/reset method was called with no instruction argument. - */ - instruction, - /** - * In events with type === "validate", this property will contain the result - * of validating the instruction (see "instruction" property). Use the controllerValidateResult - * to access the validate results specific to the call to "validate" - * (as opposed to using the "results" and "errors" properties to access the controller's entire - * set of results/errors). - */ - controllerValidateResult) { - this.type = type; - this.errors = errors; - this.results = results; - this.instruction = instruction; - this.controllerValidateResult = controllerValidateResult; - } - return ValidateEvent; -}()); - /** - * Orchestrates validation. - * Manages a set of bindings, renderers and objects. - * Exposes the current list of validation results for binding purposes. + * Sets, unsets and retrieves rules on an object or constructor function. */ -var ValidationController = /** @class */ (function () { - function ValidationController(validator, propertyParser) { - this.validator = validator; - this.propertyParser = propertyParser; - // Registered bindings (via the validate binding behavior) - this.bindings = new Map(); - // Renderers that have been added to the controller instance. - this.renderers = []; - /** - * Validation results that have been rendered by the controller. - */ - this.results = []; - /** - * Validation errors that have been rendered by the controller. - */ - this.errors = []; - /** - * Whether the controller is currently validating. - */ - this.validating = false; - // Elements related to validation results that have been rendered. - this.elements = new Map(); - // Objects that have been added to the controller instance (entity-style validation). - this.objects = new Map(); - /** - * The trigger that will invoke automatic validation of a property used in a binding. - */ - this.validateTrigger = exports.validateTrigger.blur; - // Promise that resolves when validation has completed. - this.finishValidating = Promise.resolve(); - this.eventCallbacks = []; +var Rules = /** @class */ (function () { + function Rules() { } /** - * Subscribe to controller validate and reset events. These events occur when the - * controller's "validate"" and "reset" methods are called. - * @param callback The callback to be invoked when the controller validates or resets. + * Applies the rules to a target. */ - ValidationController.prototype.subscribe = function (callback) { - var _this = this; - this.eventCallbacks.push(callback); - return { - dispose: function () { - var index = _this.eventCallbacks.indexOf(callback); - if (index === -1) { - return; - } - _this.eventCallbacks.splice(index, 1); - } - }; + Rules.set = function (target, rules) { + if (target instanceof Function) { + target = target.prototype; + } + Object.defineProperty(target, Rules.key, { enumerable: false, configurable: false, writable: true, value: rules }); }; /** - * Adds an object to the set of objects that should be validated when validate is called. - * @param object The object. - * @param rules Optional. The rules. If rules aren't supplied the Validator implementation will lookup the rules. + * Removes rules from a target. */ - ValidationController.prototype.addObject = function (object, rules) { - this.objects.set(object, rules); + Rules.unset = function (target) { + if (target instanceof Function) { + target = target.prototype; + } + target[Rules.key] = null; }; /** - * Removes an object from the set of objects that should be validated when validate is called. - * @param object The object. + * Retrieves the target's rules. */ - ValidationController.prototype.removeObject = function (object) { - this.objects.delete(object); - this.processResultDelta('reset', this.results.filter(function (result) { return result.object === object; }), []); + Rules.get = function (target) { + return target[Rules.key] || null; }; /** - * Adds and renders an error. + * The name of the property that stores the rules. */ - ValidationController.prototype.addError = function (message, object, propertyName) { - if (propertyName === void 0) { propertyName = null; } - var resolvedPropertyName; - if (propertyName === null) { - resolvedPropertyName = propertyName; - } - else { - resolvedPropertyName = this.propertyParser.parse(propertyName); + Rules.key = '__rules__'; + return Rules; +}()); + +// tslint:disable:no-empty +var ExpressionVisitor = /** @class */ (function () { + function ExpressionVisitor() { + } + ExpressionVisitor.prototype.visitChain = function (chain) { + this.visitArgs(chain.expressions); + }; + ExpressionVisitor.prototype.visitBindingBehavior = function (behavior) { + behavior.expression.accept(this); + this.visitArgs(behavior.args); + }; + ExpressionVisitor.prototype.visitValueConverter = function (converter) { + converter.expression.accept(this); + this.visitArgs(converter.args); + }; + ExpressionVisitor.prototype.visitAssign = function (assign) { + assign.target.accept(this); + assign.value.accept(this); + }; + ExpressionVisitor.prototype.visitConditional = function (conditional) { + conditional.condition.accept(this); + conditional.yes.accept(this); + conditional.no.accept(this); + }; + ExpressionVisitor.prototype.visitAccessThis = function (access) { + access.ancestor = access.ancestor; + }; + ExpressionVisitor.prototype.visitAccessScope = function (access) { + access.name = access.name; + }; + ExpressionVisitor.prototype.visitAccessMember = function (access) { + access.object.accept(this); + }; + ExpressionVisitor.prototype.visitAccessKeyed = function (access) { + access.object.accept(this); + access.key.accept(this); + }; + ExpressionVisitor.prototype.visitCallScope = function (call) { + this.visitArgs(call.args); + }; + ExpressionVisitor.prototype.visitCallFunction = function (call) { + call.func.accept(this); + this.visitArgs(call.args); + }; + ExpressionVisitor.prototype.visitCallMember = function (call) { + call.object.accept(this); + this.visitArgs(call.args); + }; + ExpressionVisitor.prototype.visitPrefix = function (prefix) { + prefix.expression.accept(this); + }; + ExpressionVisitor.prototype.visitBinary = function (binary) { + binary.left.accept(this); + binary.right.accept(this); + }; + ExpressionVisitor.prototype.visitLiteralPrimitive = function (literal) { + literal.value = literal.value; + }; + ExpressionVisitor.prototype.visitLiteralArray = function (literal) { + this.visitArgs(literal.elements); + }; + ExpressionVisitor.prototype.visitLiteralObject = function (literal) { + this.visitArgs(literal.values); + }; + ExpressionVisitor.prototype.visitLiteralString = function (literal) { + literal.value = literal.value; + }; + ExpressionVisitor.prototype.visitArgs = function (args) { + for (var i = 0; i < args.length; i++) { + args[i].accept(this); } - var result = new ValidateResult({ __manuallyAdded__: true }, object, resolvedPropertyName, false, message); - this.processResultDelta('validate', [], [result]); - return result; }; - /** - * Removes and unrenders an error. - */ - ValidationController.prototype.removeError = function (result) { - if (this.results.indexOf(result) !== -1) { - this.processResultDelta('reset', [result], []); + return ExpressionVisitor; +}()); + +var ValidationMessageParser = /** @class */ (function () { + function ValidationMessageParser(bindinqLanguage) { + this.bindinqLanguage = bindinqLanguage; + this.emptyStringExpression = new aureliaBinding.LiteralString(''); + this.nullExpression = new aureliaBinding.LiteralPrimitive(null); + this.undefinedExpression = new aureliaBinding.LiteralPrimitive(undefined); + this.cache = {}; + } + ValidationMessageParser.prototype.parse = function (message) { + if (this.cache[message] !== undefined) { + return this.cache[message]; } + var parts = this.bindinqLanguage.parseInterpolation(null, message); + if (parts === null) { + return new aureliaBinding.LiteralString(message); + } + var expression = new aureliaBinding.LiteralString(parts[0]); + for (var i = 1; i < parts.length; i += 2) { + expression = new aureliaBinding.Binary('+', expression, new aureliaBinding.Binary('+', this.coalesce(parts[i]), new aureliaBinding.LiteralString(parts[i + 1]))); + } + MessageExpressionValidator.validate(expression, message); + this.cache[message] = expression; + return expression; }; - /** - * Adds a renderer. - * @param renderer The renderer. - */ - ValidationController.prototype.addRenderer = function (renderer) { - var _this = this; - this.renderers.push(renderer); - renderer.render({ - kind: 'validate', - render: this.results.map(function (result) { return ({ result: result, elements: _this.elements.get(result) }); }), - unrender: [] - }); + ValidationMessageParser.prototype.coalesce = function (part) { + // part === null || part === undefined ? '' : part + return new aureliaBinding.Conditional(new aureliaBinding.Binary('||', new aureliaBinding.Binary('===', part, this.nullExpression), new aureliaBinding.Binary('===', part, this.undefinedExpression)), this.emptyStringExpression, new aureliaBinding.CallMember(part, 'toString', [])); }; - /** - * Removes a renderer. - * @param renderer The renderer. - */ - ValidationController.prototype.removeRenderer = function (renderer) { - var _this = this; - this.renderers.splice(this.renderers.indexOf(renderer), 1); - renderer.render({ - kind: 'reset', - render: [], - unrender: this.results.map(function (result) { return ({ result: result, elements: _this.elements.get(result) }); }) - }); + ValidationMessageParser.inject = [aureliaTemplating.BindingLanguage]; + return ValidationMessageParser; +}()); +var MessageExpressionValidator = /** @class */ (function (_super) { + __extends(MessageExpressionValidator, _super); + function MessageExpressionValidator(originalMessage) { + var _this = _super.call(this) || this; + _this.originalMessage = originalMessage; + return _this; + } + MessageExpressionValidator.validate = function (expression, originalMessage) { + var visitor = new MessageExpressionValidator(originalMessage); + expression.accept(visitor); }; - /** - * Registers a binding with the controller. - * @param binding The binding instance. - * @param target The DOM element. - * @param rules (optional) rules associated with the binding. Validator implementation specific. - */ - ValidationController.prototype.registerBinding = function (binding, target, rules) { - this.bindings.set(binding, { target: target, rules: rules, propertyInfo: null }); + MessageExpressionValidator.prototype.visitAccessScope = function (access) { + if (access.ancestor !== 0) { + throw new Error('$parent is not permitted in validation message expressions.'); + } + if (['displayName', 'propertyName', 'value', 'object', 'config', 'getDisplayName'].indexOf(access.name) !== -1) { + LogManager.getLogger('aurelia-validation') + // tslint:disable-next-line:max-line-length + .warn("Did you mean to use \"$" + access.name + "\" instead of \"" + access.name + "\" in this validation message template: \"" + this.originalMessage + "\"?"); + } }; + return MessageExpressionValidator; +}(ExpressionVisitor)); + +/** + * Dictionary of validation messages. [messageKey]: messageExpression + */ +var validationMessages = { /** - * Unregisters a binding with the controller. - * @param binding The binding instance. + * The default validation message. Used with rules that have no standard message. */ - ValidationController.prototype.unregisterBinding = function (binding) { - this.resetBinding(binding); - this.bindings.delete(binding); - }; + default: "${$displayName} is invalid.", + required: "${$displayName} is required.", + matches: "${$displayName} is not correctly formatted.", + email: "${$displayName} is not a valid email.", + minLength: "${$displayName} must be at least ${$config.length} character${$config.length === 1 ? '' : 's'}.", + maxLength: "${$displayName} cannot be longer than ${$config.length} character${$config.length === 1 ? '' : 's'}.", + minItems: "${$displayName} must contain at least ${$config.count} item${$config.count === 1 ? '' : 's'}.", + maxItems: "${$displayName} cannot contain more than ${$config.count} item${$config.count === 1 ? '' : 's'}.", + min: "${$displayName} must be at least ${$config.constraint}.", + max: "${$displayName} must be at most ${$config.constraint}.", + range: "${$displayName} must be between or equal to ${$config.min} and ${$config.max}.", + between: "${$displayName} must be between but not equal to ${$config.min} and ${$config.max}.", + equals: "${$displayName} must be ${$config.expectedValue}.", +}; +/** + * Retrieves validation messages and property display names. + */ +var ValidationMessageProvider = /** @class */ (function () { + function ValidationMessageProvider(parser) { + this.parser = parser; + } /** - * Interprets the instruction and returns a predicate that will identify - * relevant results in the list of rendered validation results. + * Returns a message binding expression that corresponds to the key. + * @param key The message key. */ - ValidationController.prototype.getInstructionPredicate = function (instruction) { - var _this = this; - if (instruction) { - var object_1 = instruction.object, propertyName_1 = instruction.propertyName, rules_1 = instruction.rules; - var predicate_1; - if (instruction.propertyName) { - predicate_1 = function (x) { return x.object === object_1 && x.propertyName === propertyName_1; }; - } - else { - predicate_1 = function (x) { return x.object === object_1; }; - } - if (rules_1) { - return function (x) { return predicate_1(x) && _this.validator.ruleExists(rules_1, x.rule); }; - } - return predicate_1; + ValidationMessageProvider.prototype.getMessage = function (key) { + var message; + if (key in validationMessages) { + message = validationMessages[key]; } else { - return function () { return true; }; + message = validationMessages['default']; } + return this.parser.parse(message); }; /** - * Validates and renders results. - * @param instruction Optional. Instructions on what to validate. If undefined, all - * objects and bindings will be validated. + * Formulates a property display name using the property name and the configured + * displayName (if provided). + * Override this with your own custom logic. + * @param propertyName The property name. */ - ValidationController.prototype.validate = function (instruction) { - var _this = this; - // Get a function that will process the validation instruction. - var execute; - if (instruction) { - // tslint:disable-next-line:prefer-const - var object_2 = instruction.object, propertyName_2 = instruction.propertyName, rules_2 = instruction.rules; - // if rules were not specified, check the object map. - rules_2 = rules_2 || this.objects.get(object_2); - // property specified? - if (instruction.propertyName === undefined) { - // validate the specified object. - execute = function () { return _this.validator.validateObject(object_2, rules_2); }; - } - else { - // validate the specified property. - execute = function () { return _this.validator.validateProperty(object_2, propertyName_2, rules_2); }; - } - } - else { - // validate all objects and bindings. - execute = function () { - var promises = []; - for (var _i = 0, _a = Array.from(_this.objects); _i < _a.length; _i++) { - var _b = _a[_i], object = _b[0], rules = _b[1]; - promises.push(_this.validator.validateObject(object, rules)); - } - for (var _c = 0, _d = Array.from(_this.bindings); _c < _d.length; _c++) { - var _e = _d[_c], binding = _e[0], rules = _e[1].rules; - var propertyInfo = getPropertyInfo(binding.sourceExpression, binding.source); - if (!propertyInfo || _this.objects.has(propertyInfo.object)) { - continue; - } - promises.push(_this.validator.validateProperty(propertyInfo.object, propertyInfo.propertyName, rules)); - } - return Promise.all(promises).then(function (resultSets) { return resultSets.reduce(function (a, b) { return a.concat(b); }, []); }); - }; + ValidationMessageProvider.prototype.getDisplayName = function (propertyName, displayName) { + if (displayName !== null && displayName !== undefined) { + return (displayName instanceof Function) ? displayName() : displayName; } - // Wait for any existing validation to finish, execute the instruction, render the results. - this.validating = true; - var returnPromise = this.finishValidating - .then(execute) - .then(function (newResults) { - var predicate = _this.getInstructionPredicate(instruction); - var oldResults = _this.results.filter(predicate); - _this.processResultDelta('validate', oldResults, newResults); - if (returnPromise === _this.finishValidating) { - _this.validating = false; - } - var result = { - instruction: instruction, - valid: newResults.find(function (x) { return !x.valid; }) === undefined, - results: newResults - }; - _this.invokeCallbacks(instruction, result); - return result; - }) - .catch(function (exception) { - // recover, to enable subsequent calls to validate() - _this.validating = false; - _this.finishValidating = Promise.resolve(); - return Promise.reject(exception); - }); - this.finishValidating = returnPromise; - return returnPromise; + // split on upper-case letters. + var words = propertyName.toString().split(/(?=[A-Z])/).join(' '); + // capitalize first letter. + return words.charAt(0).toUpperCase() + words.slice(1); + }; + ValidationMessageProvider.inject = [ValidationMessageParser]; + return ValidationMessageProvider; +}()); + +/** + * Validates. + * Responsible for validating objects and properties. + */ +var StandardValidator = /** @class */ (function (_super) { + __extends(StandardValidator, _super); + function StandardValidator(messageProvider, resources) { + var _this = _super.call(this) || this; + _this.messageProvider = messageProvider; + _this.lookupFunctions = resources.lookupFunctions; + _this.getDisplayName = messageProvider.getDisplayName.bind(messageProvider); + return _this; + } + /** + * Validates the specified property. + * @param object The object to validate. + * @param propertyName The name of the property to validate. + * @param rules Optional. If unspecified, the rules will be looked up using the metadata + * for the object created by ValidationRules....on(class/object) + */ + StandardValidator.prototype.validateProperty = function (object, propertyName, rules) { + return this.validate(object, propertyName, rules || null); }; /** - * Resets any rendered validation results (unrenders). - * @param instruction Optional. Instructions on what to reset. If unspecified all rendered results - * will be unrendered. + * Validates all rules for specified object and it's properties. + * @param object The object to validate. + * @param rules Optional. If unspecified, the rules will be looked up using the metadata + * for the object created by ValidationRules....on(class/object) */ - ValidationController.prototype.reset = function (instruction) { - var predicate = this.getInstructionPredicate(instruction); - var oldResults = this.results.filter(predicate); - this.processResultDelta('reset', oldResults, []); - this.invokeCallbacks(instruction, null); + StandardValidator.prototype.validateObject = function (object, rules) { + return this.validate(object, null, rules || null); }; /** - * Gets the elements associated with an object and propertyName (if any). + * Determines whether a rule exists in a set of rules. + * @param rules The rules to search. + * @parem rule The rule to find. */ - ValidationController.prototype.getAssociatedElements = function (_a) { - var object = _a.object, propertyName = _a.propertyName; - var elements = []; - for (var _i = 0, _b = Array.from(this.bindings); _i < _b.length; _i++) { - var _c = _b[_i], binding = _c[0], target = _c[1].target; - var propertyInfo = getPropertyInfo(binding.sourceExpression, binding.source); - if (propertyInfo && propertyInfo.object === object && propertyInfo.propertyName === propertyName) { - elements.push(target); + StandardValidator.prototype.ruleExists = function (rules, rule) { + var i = rules.length; + while (i--) { + if (rules[i].indexOf(rule) !== -1) { + return true; } } - return elements; + return false; }; - ValidationController.prototype.processResultDelta = function (kind, oldResults, newResults) { - // prepare the instruction. - var instruction = { - kind: kind, - render: [], - unrender: [] + StandardValidator.prototype.getMessage = function (rule, object, value) { + var expression = rule.message || this.messageProvider.getMessage(rule.messageKey); + // tslint:disable-next-line:prefer-const + var _a = rule.property, propertyName = _a.name, displayName = _a.displayName; + if (propertyName !== null) { + displayName = this.messageProvider.getDisplayName(propertyName, displayName); + } + var overrideContext = { + $displayName: displayName, + $propertyName: propertyName, + $value: value, + $object: object, + $config: rule.config, + // returns the name of a given property, given just the property name (irrespective of the property's displayName) + // split on capital letters, first letter ensured to be capitalized + $getDisplayName: this.getDisplayName }; - // create a shallow copy of newResults so we can mutate it without causing side-effects. - newResults = newResults.slice(0); - var _loop_1 = function (oldResult) { - // get the elements associated with the old result. - var elements = this_1.elements.get(oldResult); - // remove the old result from the element map. - this_1.elements.delete(oldResult); - // create the unrender instruction. - instruction.unrender.push({ result: oldResult, elements: elements }); - // determine if there's a corresponding new result for the old result we are unrendering. - var newResultIndex = newResults.findIndex(function (x) { return x.rule === oldResult.rule && x.object === oldResult.object && x.propertyName === oldResult.propertyName; }); - if (newResultIndex === -1) { - // no corresponding new result... simple remove. - this_1.results.splice(this_1.results.indexOf(oldResult), 1); - if (!oldResult.valid) { - this_1.errors.splice(this_1.errors.indexOf(oldResult), 1); - } + return expression.evaluate({ bindingContext: object, overrideContext: overrideContext }, this.lookupFunctions); + }; + StandardValidator.prototype.validateRuleSequence = function (object, propertyName, ruleSequence, sequence, results) { + var _this = this; + // are we validating all properties or a single property? + var validateAllProperties = propertyName === null || propertyName === undefined; + var rules = ruleSequence[sequence]; + var allValid = true; + // validate each rule. + var promises = []; + var _loop_1 = function (i) { + var rule = rules[i]; + // is the rule related to the property we're validating. + // tslint:disable-next-line:triple-equals | Use loose equality for property keys + if (!validateAllProperties && rule.property.name != propertyName) { + return "continue"; } - else { - // there is a corresponding new result... - var newResult = newResults.splice(newResultIndex, 1)[0]; - // get the elements that are associated with the new result. - var elements_1 = this_1.getAssociatedElements(newResult); - this_1.elements.set(newResult, elements_1); - // create a render instruction for the new result. - instruction.render.push({ result: newResult, elements: elements_1 }); - // do an in-place replacement of the old result with the new result. - // this ensures any repeats bound to this.results will not thrash. - this_1.results.splice(this_1.results.indexOf(oldResult), 1, newResult); - if (!oldResult.valid && newResult.valid) { - this_1.errors.splice(this_1.errors.indexOf(oldResult), 1); - } - else if (!oldResult.valid && !newResult.valid) { - this_1.errors.splice(this_1.errors.indexOf(oldResult), 1, newResult); - } - else if (!newResult.valid) { - this_1.errors.push(newResult); - } + // is this a conditional rule? is the condition met? + if (rule.when && !rule.when(object)) { + return "continue"; + } + // validate. + var value = rule.property.name === null ? object : object[rule.property.name]; + var promiseOrBoolean = rule.condition(value, object); + if (!(promiseOrBoolean instanceof Promise)) { + promiseOrBoolean = Promise.resolve(promiseOrBoolean); } + promises.push(promiseOrBoolean.then(function (valid) { + var message = valid ? null : _this.getMessage(rule, object, value); + results.push(new ValidateResult(rule, object, rule.property.name, valid, message)); + allValid = allValid && valid; + return valid; + })); }; - var this_1 = this; - // create unrender instructions from the old results. - for (var _i = 0, oldResults_1 = oldResults; _i < oldResults_1.length; _i++) { - var oldResult = oldResults_1[_i]; - _loop_1(oldResult); + for (var i = 0; i < rules.length; i++) { + _loop_1(i); } - // create render instructions from the remaining new results. - for (var _a = 0, newResults_1 = newResults; _a < newResults_1.length; _a++) { - var result = newResults_1[_a]; - var elements = this.getAssociatedElements(result); - instruction.render.push({ result: result, elements: elements }); - this.elements.set(result, elements); - this.results.push(result); - if (!result.valid) { - this.errors.push(result); + return Promise.all(promises) + .then(function () { + sequence++; + if (allValid && sequence < ruleSequence.length) { + return _this.validateRuleSequence(object, propertyName, ruleSequence, sequence, results); } + return results; + }); + }; + StandardValidator.prototype.validate = function (object, propertyName, rules) { + // rules specified? + if (!rules) { + // no. attempt to locate the rules. + rules = Rules.get(object); } - // render. - for (var _b = 0, _c = this.renderers; _b < _c.length; _b++) { - var renderer = _c[_b]; - renderer.render(instruction); + // any rules? + if (!rules || rules.length === 0) { + return Promise.resolve([]); } + return this.validateRuleSequence(object, propertyName, rules, 0, []); }; + StandardValidator.inject = [ValidationMessageProvider, aureliaTemplating.ViewResources]; + return StandardValidator; +}(Validator)); + +/** + * Validation triggers. + */ +(function (validateTrigger) { /** - * Validates the property associated with a binding. - */ - ValidationController.prototype.validateBinding = function (binding) { - if (!binding.isBound) { - return; - } - var propertyInfo = getPropertyInfo(binding.sourceExpression, binding.source); - var rules; - var registeredBinding = this.bindings.get(binding); - if (registeredBinding) { - rules = registeredBinding.rules; - registeredBinding.propertyInfo = propertyInfo; - } - if (!propertyInfo) { - return; - } - var object = propertyInfo.object, propertyName = propertyInfo.propertyName; - this.validate({ object: object, propertyName: propertyName, rules: rules }); - }; - /** - * Resets the results for a property associated with a binding. + * Manual validation. Use the controller's `validate()` and `reset()` methods + * to validate all bindings. */ - ValidationController.prototype.resetBinding = function (binding) { - var registeredBinding = this.bindings.get(binding); - var propertyInfo = getPropertyInfo(binding.sourceExpression, binding.source); - if (!propertyInfo && registeredBinding) { - propertyInfo = registeredBinding.propertyInfo; - } - if (registeredBinding) { - registeredBinding.propertyInfo = null; - } - if (!propertyInfo) { - return; - } - var object = propertyInfo.object, propertyName = propertyInfo.propertyName; - this.reset({ object: object, propertyName: propertyName }); - }; + validateTrigger[validateTrigger["manual"] = 0] = "manual"; /** - * Changes the controller's validateTrigger. - * @param newTrigger The new validateTrigger + * Validate the binding when the binding's target element fires a DOM "blur" event. */ - ValidationController.prototype.changeTrigger = function (newTrigger) { - this.validateTrigger = newTrigger; - var bindings = Array.from(this.bindings.keys()); - for (var _i = 0, bindings_1 = bindings; _i < bindings_1.length; _i++) { - var binding = bindings_1[_i]; - var source = binding.source; - binding.unbind(); - binding.bind(source); - } - }; + validateTrigger[validateTrigger["blur"] = 1] = "blur"; /** - * Revalidates the controller's current set of errors. + * Validate the binding when it updates the model due to a change in the view. */ - ValidationController.prototype.revalidateErrors = function () { - for (var _i = 0, _a = this.errors; _i < _a.length; _i++) { - var _b = _a[_i], object = _b.object, propertyName = _b.propertyName, rule = _b.rule; - if (rule.__manuallyAdded__) { - continue; - } - var rules = [[rule]]; - this.validate({ object: object, propertyName: propertyName, rules: rules }); - } - }; - ValidationController.prototype.invokeCallbacks = function (instruction, result) { - if (this.eventCallbacks.length === 0) { - return; - } - var event = new ValidateEvent(result ? 'validate' : 'reset', this.errors, this.results, instruction || null, result); - for (var i = 0; i < this.eventCallbacks.length; i++) { - this.eventCallbacks[i](event); - } - }; - ValidationController.inject = [Validator, PropertyAccessorParser]; - return ValidationController; -}()); + validateTrigger[validateTrigger["change"] = 2] = "change"; + /** + * 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. + */ + validateTrigger[validateTrigger["changeOrBlur"] = 3] = "changeOrBlur"; +})(exports.validateTrigger || (exports.validateTrigger = {})); /** - * Binding behavior. Indicates the bound property should be validated. + * Aurelia Validation Configuration API */ -var ValidateBindingBehaviorBase = /** @class */ (function () { - function ValidateBindingBehaviorBase(taskQueue) { - this.taskQueue = taskQueue; +var GlobalValidationConfiguration = /** @class */ (function () { + function GlobalValidationConfiguration() { + this.validatorType = StandardValidator; + this.validationTrigger = GlobalValidationConfiguration.DEFAULT_VALIDATION_TRIGGER; } - ValidateBindingBehaviorBase.prototype.bind = function (binding, source, rulesOrController, rules) { - var _this = this; - // identify the target element. - var target = getTargetDOMElement(binding, source); - // locate the controller. - var controller; - if (rulesOrController instanceof ValidationController) { - controller = rulesOrController; - } - else { - controller = source.container.get(aureliaDependencyInjection.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); - // tslint:disable-next-line:no-bitwise - if (trigger & exports.validateTrigger.change) { - binding.vbbUpdateSource = binding.updateSource; - // tslint:disable-next-line:only-arrow-functions - // tslint:disable-next-line:space-before-function-paren - binding.updateSource = function (value) { - this.vbbUpdateSource(value); - this.validationController.validateBinding(this); - }; - } - // tslint:disable-next-line:no-bitwise - if (trigger & exports.validateTrigger.blur) { - binding.validateBlurHandler = function () { - _this.taskQueue.queueMicroTask(function () { return controller.validateBinding(binding); }); - }; - binding.validateTarget = target; - target.addEventListener('blur', binding.validateBlurHandler); - } - if (trigger !== exports.validateTrigger.manual) { - binding.standardUpdateTarget = binding.updateTarget; - // tslint:disable-next-line:only-arrow-functions - // tslint:disable-next-line:space-before-function-paren - binding.updateTarget = function (value) { - this.standardUpdateTarget(value); - this.validationController.resetBinding(this); - }; - } + /** + * Use a custom Validator implementation. + */ + GlobalValidationConfiguration.prototype.customValidator = function (type) { + this.validatorType = type; + return this; }; - ValidateBindingBehaviorBase.prototype.unbind = function (binding) { - // reset the binding to it's original state. - if (binding.vbbUpdateSource) { - binding.updateSource = binding.vbbUpdateSource; - binding.vbbUpdateSource = 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; + GlobalValidationConfiguration.prototype.defaultValidationTrigger = function (trigger) { + this.validationTrigger = trigger; + return this; }; - return ValidateBindingBehaviorBase; + GlobalValidationConfiguration.prototype.getDefaultValidationTrigger = function () { + return this.validationTrigger; + }; + /** + * Applies the configuration. + */ + GlobalValidationConfiguration.prototype.apply = function (container) { + var validator = container.get(this.validatorType); + container.registerInstance(Validator, validator); + container.registerInstance(GlobalValidationConfiguration, this); + }; + GlobalValidationConfiguration.DEFAULT_VALIDATION_TRIGGER = exports.validateTrigger.blur; + return GlobalValidationConfiguration; }()); /** - * Binding behavior. Indicates the bound property should be validated - * when the validate trigger specified by the associated controller's - * validateTrigger property occurs. + * 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. */ -var ValidateBindingBehavior = /** @class */ (function (_super) { - __extends(ValidateBindingBehavior, _super); - function ValidateBindingBehavior() { - return _super !== null && _super.apply(this, arguments) || this; +function getTargetDOMElement(binding, view) { + var target = binding.target; + // DOM element + if (target instanceof Element) { + return target; } - ValidateBindingBehavior.prototype.getValidateTrigger = function (controller) { - return controller.validateTrigger; - }; - ValidateBindingBehavior.inject = [aureliaTaskQueue.TaskQueue]; - ValidateBindingBehavior = __decorate([ - aureliaBinding.bindingBehavior('validate') - ], ValidateBindingBehavior); - 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. - */ -var ValidateManuallyBindingBehavior = /** @class */ (function (_super) { - __extends(ValidateManuallyBindingBehavior, _super); - function ValidateManuallyBindingBehavior() { - return _super !== null && _super.apply(this, arguments) || this; + // custom element or custom attribute + // tslint:disable-next-line:prefer-const + 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(aureliaPal.DOM.Element); + if (element) { + return element; + } + throw new Error("Unable to locate target element for \"" + binding.sourceExpression + "\"."); + } } - ValidateManuallyBindingBehavior.prototype.getValidateTrigger = function () { - return exports.validateTrigger.manual; - }; - ValidateManuallyBindingBehavior.inject = [aureliaTaskQueue.TaskQueue]; - ValidateManuallyBindingBehavior = __decorate([ - aureliaBinding.bindingBehavior('validateManually') - ], ValidateManuallyBindingBehavior); - return ValidateManuallyBindingBehavior; -}(ValidateBindingBehaviorBase)); -/** - * Binding behavior. Indicates the bound property should be validated - * when the associated element blurs. - */ -var ValidateOnBlurBindingBehavior = /** @class */ (function (_super) { - __extends(ValidateOnBlurBindingBehavior, _super); - function ValidateOnBlurBindingBehavior() { - return _super !== null && _super.apply(this, arguments) || this; + throw new Error("Unable to locate target element for \"" + binding.sourceExpression + "\"."); +} + +function getObject(expression, objectExpression, source) { + var value = objectExpression.evaluate(source, null); + if (value === null || value === undefined || value instanceof Object) { + return value; } - ValidateOnBlurBindingBehavior.prototype.getValidateTrigger = function () { - return exports.validateTrigger.blur; - }; - ValidateOnBlurBindingBehavior.inject = [aureliaTaskQueue.TaskQueue]; - ValidateOnBlurBindingBehavior = __decorate([ - aureliaBinding.bindingBehavior('validateOnBlur') - ], ValidateOnBlurBindingBehavior); - return ValidateOnBlurBindingBehavior; -}(ValidateBindingBehaviorBase)); + // tslint:disable-next-line:max-line-length + throw new Error("The '" + objectExpression + "' part of '" + expression + "' evaluates to " + value + " instead of an object, null or undefined."); +} /** - * Binding behavior. Indicates the bound property should be validated - * when the associated element is changed by the user, causing a change - * to the model. + * Retrieves the object and property name for the specified expression. + * @param expression The expression + * @param source The scope */ -var ValidateOnChangeBindingBehavior = /** @class */ (function (_super) { - __extends(ValidateOnChangeBindingBehavior, _super); - function ValidateOnChangeBindingBehavior() { - return _super !== null && _super.apply(this, arguments) || this; +function getPropertyInfo(expression, source) { + var originalExpression = expression; + while (expression instanceof aureliaBinding.BindingBehavior || expression instanceof aureliaBinding.ValueConverter) { + expression = expression.expression; } - ValidateOnChangeBindingBehavior.prototype.getValidateTrigger = function () { - return exports.validateTrigger.change; - }; - ValidateOnChangeBindingBehavior.inject = [aureliaTaskQueue.TaskQueue]; - ValidateOnChangeBindingBehavior = __decorate([ - aureliaBinding.bindingBehavior('validateOnChange') - ], ValidateOnChangeBindingBehavior); - 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. - */ -var ValidateOnChangeOrBlurBindingBehavior = /** @class */ (function (_super) { - __extends(ValidateOnChangeOrBlurBindingBehavior, _super); - function ValidateOnChangeOrBlurBindingBehavior() { - return _super !== null && _super.apply(this, arguments) || this; + var object; + var propertyName; + if (expression instanceof aureliaBinding.AccessScope) { + object = aureliaBinding.getContextFor(expression.name, source, expression.ancestor); + propertyName = expression.name; } - ValidateOnChangeOrBlurBindingBehavior.prototype.getValidateTrigger = function () { - return exports.validateTrigger.changeOrBlur; + else if (expression instanceof aureliaBinding.AccessMember) { + object = getObject(originalExpression, expression.object, source); + propertyName = expression.name; + } + else if (expression instanceof aureliaBinding.AccessKeyed) { + object = getObject(originalExpression, expression.object, source); + propertyName = expression.key.evaluate(source); + } + else { + throw new Error("Expression '" + originalExpression + "' is not compatible with the validate binding-behavior."); + } + if (object === null || object === undefined) { + return null; + } + return { object: object, propertyName: propertyName }; +} + +function isString(value) { + return Object.prototype.toString.call(value) === '[object String]'; +} +function isNumber(value) { + return Object.prototype.toString.call(value) === '[object Number]'; +} + +var PropertyAccessorParser = /** @class */ (function () { + function PropertyAccessorParser(parser) { + this.parser = parser; + } + PropertyAccessorParser.prototype.parse = function (property) { + if (isString(property) || isNumber(property)) { + return property; + } + var accessorText = getAccessorExpression(property.toString()); + var accessor = this.parser.parse(accessorText); + if (accessor instanceof aureliaBinding.AccessScope + || accessor instanceof aureliaBinding.AccessMember && accessor.object instanceof aureliaBinding.AccessScope) { + return accessor.name; + } + throw new Error("Invalid property expression: \"" + accessor + "\""); }; - ValidateOnChangeOrBlurBindingBehavior.inject = [aureliaTaskQueue.TaskQueue]; - ValidateOnChangeOrBlurBindingBehavior = __decorate([ - aureliaBinding.bindingBehavior('validateOnChangeOrBlur') - ], ValidateOnChangeOrBlurBindingBehavior); - return ValidateOnChangeOrBlurBindingBehavior; -}(ValidateBindingBehaviorBase)); + PropertyAccessorParser.inject = [aureliaBinding.Parser]; + return PropertyAccessorParser; +}()); +function getAccessorExpression(fn) { + /* tslint:disable:max-line-length */ + var classic = /^function\s*\([$_\w\d]+\)\s*\{(?:\s*"use strict";)?\s*(?:[$_\w\d.['"\]+;]+)?\s*return\s+[$_\w\d]+\.([$_\w\d]+)\s*;?\s*\}$/; + /* tslint:enable:max-line-length */ + var arrow = /^\(?[$_\w\d]+\)?\s*=>\s*[$_\w\d]+\.([$_\w\d]+)$/; + var match = classic.exec(fn) || arrow.exec(fn); + if (match === null) { + throw new Error("Unable to parse accessor function:\n" + fn); + } + return match[1]; +} + +var ValidateEvent = /** @class */ (function () { + function ValidateEvent( + /** + * The type of validate event. Either "validate" or "reset". + */ + type, + /** + * The controller's current array of errors. For an array containing both + * failed rules and passed rules, use the "results" property. + */ + errors, + /** + * The controller's current array of validate results. This + * includes both passed rules and failed rules. For an array of only failed rules, + * use the "errors" property. + */ + results, + /** + * The instruction passed to the "validate" or "reset" event. Will be null when + * the controller's validate/reset method was called with no instruction argument. + */ + instruction, + /** + * In events with type === "validate", this property will contain the result + * of validating the instruction (see "instruction" property). Use the controllerValidateResult + * to access the validate results specific to the call to "validate" + * (as opposed to using the "results" and "errors" properties to access the controller's entire + * set of results/errors). + */ + controllerValidateResult) { + this.type = type; + this.errors = errors; + this.results = results; + this.instruction = instruction; + this.controllerValidateResult = controllerValidateResult; + } + return ValidateEvent; +}()); /** - * Creates ValidationController instances. + * Orchestrates validation. + * Manages a set of bindings, renderers and objects. + * Exposes the current list of validation results for binding purposes. */ -var ValidationControllerFactory = /** @class */ (function () { - function ValidationControllerFactory(container) { - this.container = container; +var ValidationController = /** @class */ (function () { + function ValidationController(validator, propertyParser, config) { + this.validator = validator; + this.propertyParser = propertyParser; + // Registered bindings (via the validate binding behavior) + this.bindings = new Map(); + // Renderers that have been added to the controller instance. + this.renderers = []; + /** + * Validation results that have been rendered by the controller. + */ + this.results = []; + /** + * Validation errors that have been rendered by the controller. + */ + this.errors = []; + /** + * Whether the controller is currently validating. + */ + this.validating = false; + // Elements related to validation results that have been rendered. + this.elements = new Map(); + // Objects that have been added to the controller instance (entity-style validation). + this.objects = new Map(); + // Promise that resolves when validation has completed. + this.finishValidating = Promise.resolve(); + this.eventCallbacks = []; + this.validateTrigger = config instanceof GlobalValidationConfiguration + ? config.getDefaultValidationTrigger() + : GlobalValidationConfiguration.DEFAULT_VALIDATION_TRIGGER; } - ValidationControllerFactory.get = function (container) { - return new ValidationControllerFactory(container); + /** + * Subscribe to controller validate and reset events. These events occur when the + * controller's "validate"" and "reset" methods are called. + * @param callback The callback to be invoked when the controller validates or resets. + */ + ValidationController.prototype.subscribe = function (callback) { + var _this = this; + this.eventCallbacks.push(callback); + return { + dispose: function () { + var index = _this.eventCallbacks.indexOf(callback); + if (index === -1) { + return; + } + _this.eventCallbacks.splice(index, 1); + } + }; }; /** - * Creates a new controller instance. + * Adds an object to the set of objects that should be validated when validate is called. + * @param object The object. + * @param rules Optional. The rules. If rules aren't supplied the Validator implementation will lookup the rules. */ - ValidationControllerFactory.prototype.create = function (validator) { - if (!validator) { - validator = this.container.get(Validator); + ValidationController.prototype.addObject = function (object, rules) { + this.objects.set(object, rules); + }; + /** + * Removes an object from the set of objects that should be validated when validate is called. + * @param object The object. + */ + ValidationController.prototype.removeObject = function (object) { + this.objects.delete(object); + this.processResultDelta('reset', this.results.filter(function (result) { return result.object === object; }), []); + }; + /** + * Adds and renders an error. + */ + ValidationController.prototype.addError = function (message, object, propertyName) { + if (propertyName === void 0) { propertyName = null; } + var resolvedPropertyName; + if (propertyName === null) { + resolvedPropertyName = propertyName; } - var propertyParser = this.container.get(PropertyAccessorParser); - return new ValidationController(validator, propertyParser); + else { + resolvedPropertyName = this.propertyParser.parse(propertyName); + } + var result = new ValidateResult({ __manuallyAdded__: true }, object, resolvedPropertyName, false, message); + this.processResultDelta('validate', [], [result]); + return result; }; /** - * 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. + * Removes and unrenders an error. */ - ValidationControllerFactory.prototype.createForCurrentScope = function (validator) { - var controller = this.create(validator); - this.container.registerInstance(ValidationController, controller); - return controller; + ValidationController.prototype.removeError = function (result) { + if (this.results.indexOf(result) !== -1) { + this.processResultDelta('reset', [result], []); + } }; - return ValidationControllerFactory; -}()); -ValidationControllerFactory['protocol:aurelia:resolver'] = true; - -var ValidationErrorsCustomAttribute = /** @class */ (function () { - function ValidationErrorsCustomAttribute(boundaryElement, controllerAccessor) { - this.boundaryElement = boundaryElement; - this.controllerAccessor = controllerAccessor; - this.controller = null; - this.errors = []; - this.errorsInternal = []; - } - ValidationErrorsCustomAttribute.inject = function () { - return [aureliaPal.DOM.Element, aureliaDependencyInjection.Lazy.of(ValidationController)]; + /** + * Adds a renderer. + * @param renderer The renderer. + */ + ValidationController.prototype.addRenderer = function (renderer) { + var _this = this; + this.renderers.push(renderer); + renderer.render({ + kind: 'validate', + render: this.results.map(function (result) { return ({ result: result, elements: _this.elements.get(result) }); }), + unrender: [] + }); }; - ValidationErrorsCustomAttribute.prototype.sort = function () { - this.errorsInternal.sort(function (a, b) { - if (a.targets[0] === b.targets[0]) { - return 0; - } - // tslint:disable-next-line:no-bitwise - return a.targets[0].compareDocumentPosition(b.targets[0]) & 2 ? 1 : -1; + /** + * Removes a renderer. + * @param renderer The renderer. + */ + ValidationController.prototype.removeRenderer = function (renderer) { + var _this = this; + this.renderers.splice(this.renderers.indexOf(renderer), 1); + renderer.render({ + kind: 'reset', + render: [], + unrender: this.results.map(function (result) { return ({ result: result, elements: _this.elements.get(result) }); }) }); }; - ValidationErrorsCustomAttribute.prototype.interestingElements = function (elements) { + /** + * Registers a binding with the controller. + * @param binding The binding instance. + * @param target The DOM element. + * @param rules (optional) rules associated with the binding. Validator implementation specific. + */ + ValidationController.prototype.registerBinding = function (binding, target, rules) { + this.bindings.set(binding, { target: target, rules: rules, propertyInfo: null }); + }; + /** + * Unregisters a binding with the controller. + * @param binding The binding instance. + */ + ValidationController.prototype.unregisterBinding = function (binding) { + this.resetBinding(binding); + this.bindings.delete(binding); + }; + /** + * Interprets the instruction and returns a predicate that will identify + * relevant results in the list of rendered validation results. + */ + ValidationController.prototype.getInstructionPredicate = function (instruction) { var _this = this; - return elements.filter(function (e) { return _this.boundaryElement.contains(e); }); + if (instruction) { + var object_1 = instruction.object, propertyName_1 = instruction.propertyName, rules_1 = instruction.rules; + var predicate_1; + if (instruction.propertyName) { + predicate_1 = function (x) { return x.object === object_1 && x.propertyName === propertyName_1; }; + } + else { + predicate_1 = function (x) { return x.object === object_1; }; + } + if (rules_1) { + return function (x) { return predicate_1(x) && _this.validator.ruleExists(rules_1, x.rule); }; + } + return predicate_1; + } + else { + return function () { return true; }; + } + }; + /** + * Validates and renders results. + * @param instruction Optional. Instructions on what to validate. If undefined, all + * objects and bindings will be validated. + */ + ValidationController.prototype.validate = function (instruction) { + var _this = this; + // Get a function that will process the validation instruction. + var execute; + if (instruction) { + // tslint:disable-next-line:prefer-const + var object_2 = instruction.object, propertyName_2 = instruction.propertyName, rules_2 = instruction.rules; + // if rules were not specified, check the object map. + rules_2 = rules_2 || this.objects.get(object_2); + // property specified? + if (instruction.propertyName === undefined) { + // validate the specified object. + execute = function () { return _this.validator.validateObject(object_2, rules_2); }; + } + else { + // validate the specified property. + execute = function () { return _this.validator.validateProperty(object_2, propertyName_2, rules_2); }; + } + } + else { + // validate all objects and bindings. + execute = function () { + var promises = []; + for (var _i = 0, _a = Array.from(_this.objects); _i < _a.length; _i++) { + var _b = _a[_i], object = _b[0], rules = _b[1]; + promises.push(_this.validator.validateObject(object, rules)); + } + for (var _c = 0, _d = Array.from(_this.bindings); _c < _d.length; _c++) { + var _e = _d[_c], binding = _e[0], rules = _e[1].rules; + var propertyInfo = getPropertyInfo(binding.sourceExpression, binding.source); + if (!propertyInfo || _this.objects.has(propertyInfo.object)) { + continue; + } + promises.push(_this.validator.validateProperty(propertyInfo.object, propertyInfo.propertyName, rules)); + } + return Promise.all(promises).then(function (resultSets) { return resultSets.reduce(function (a, b) { return a.concat(b); }, []); }); + }; + } + // Wait for any existing validation to finish, execute the instruction, render the results. + this.validating = true; + var returnPromise = this.finishValidating + .then(execute) + .then(function (newResults) { + var predicate = _this.getInstructionPredicate(instruction); + var oldResults = _this.results.filter(predicate); + _this.processResultDelta('validate', oldResults, newResults); + if (returnPromise === _this.finishValidating) { + _this.validating = false; + } + var result = { + instruction: instruction, + valid: newResults.find(function (x) { return !x.valid; }) === undefined, + results: newResults + }; + _this.invokeCallbacks(instruction, result); + return result; + }) + .catch(function (exception) { + // recover, to enable subsequent calls to validate() + _this.validating = false; + _this.finishValidating = Promise.resolve(); + return Promise.reject(exception); + }); + this.finishValidating = returnPromise; + return returnPromise; + }; + /** + * Resets any rendered validation results (unrenders). + * @param instruction Optional. Instructions on what to reset. If unspecified all rendered results + * will be unrendered. + */ + ValidationController.prototype.reset = function (instruction) { + var predicate = this.getInstructionPredicate(instruction); + var oldResults = this.results.filter(predicate); + this.processResultDelta('reset', oldResults, []); + this.invokeCallbacks(instruction, null); + }; + /** + * Gets the elements associated with an object and propertyName (if any). + */ + ValidationController.prototype.getAssociatedElements = function (_a) { + var object = _a.object, propertyName = _a.propertyName; + var elements = []; + for (var _i = 0, _b = Array.from(this.bindings); _i < _b.length; _i++) { + var _c = _b[_i], binding = _c[0], target = _c[1].target; + var propertyInfo = getPropertyInfo(binding.sourceExpression, binding.source); + if (propertyInfo && propertyInfo.object === object && propertyInfo.propertyName === propertyName) { + elements.push(target); + } + } + return elements; }; - ValidationErrorsCustomAttribute.prototype.render = function (instruction) { - var _loop_1 = function (result) { - var index = this_1.errorsInternal.findIndex(function (x) { return x.error === result; }); - if (index !== -1) { - this_1.errorsInternal.splice(index, 1); + ValidationController.prototype.processResultDelta = function (kind, oldResults, newResults) { + // prepare the instruction. + var instruction = { + kind: kind, + render: [], + unrender: [] + }; + // create a shallow copy of newResults so we can mutate it without causing side-effects. + newResults = newResults.slice(0); + var _loop_1 = function (oldResult) { + // get the elements associated with the old result. + var elements = this_1.elements.get(oldResult); + // remove the old result from the element map. + this_1.elements.delete(oldResult); + // create the unrender instruction. + instruction.unrender.push({ result: oldResult, elements: elements }); + // determine if there's a corresponding new result for the old result we are unrendering. + var newResultIndex = newResults.findIndex(function (x) { return x.rule === oldResult.rule && x.object === oldResult.object && x.propertyName === oldResult.propertyName; }); + if (newResultIndex === -1) { + // no corresponding new result... simple remove. + this_1.results.splice(this_1.results.indexOf(oldResult), 1); + if (!oldResult.valid) { + this_1.errors.splice(this_1.errors.indexOf(oldResult), 1); + } + } + else { + // there is a corresponding new result... + var newResult = newResults.splice(newResultIndex, 1)[0]; + // get the elements that are associated with the new result. + var elements_1 = this_1.getAssociatedElements(newResult); + this_1.elements.set(newResult, elements_1); + // create a render instruction for the new result. + instruction.render.push({ result: newResult, elements: elements_1 }); + // do an in-place replacement of the old result with the new result. + // this ensures any repeats bound to this.results will not thrash. + this_1.results.splice(this_1.results.indexOf(oldResult), 1, newResult); + if (!oldResult.valid && newResult.valid) { + this_1.errors.splice(this_1.errors.indexOf(oldResult), 1); + } + else if (!oldResult.valid && !newResult.valid) { + this_1.errors.splice(this_1.errors.indexOf(oldResult), 1, newResult); + } + else if (!newResult.valid) { + this_1.errors.push(newResult); + } } }; var this_1 = this; - for (var _i = 0, _a = instruction.unrender; _i < _a.length; _i++) { - var result = _a[_i].result; - _loop_1(result); + // create unrender instructions from the old results. + for (var _i = 0, oldResults_1 = oldResults; _i < oldResults_1.length; _i++) { + var oldResult = oldResults_1[_i]; + _loop_1(oldResult); } - for (var _b = 0, _c = instruction.render; _b < _c.length; _b++) { - var _d = _c[_b], result = _d.result, elements = _d.elements; - if (result.valid) { - continue; - } - var targets = this.interestingElements(elements); - if (targets.length) { - this.errorsInternal.push({ error: result, targets: targets }); + // create render instructions from the remaining new results. + for (var _a = 0, newResults_1 = newResults; _a < newResults_1.length; _a++) { + var result = newResults_1[_a]; + var elements = this.getAssociatedElements(result); + instruction.render.push({ result: result, elements: elements }); + this.elements.set(result, elements); + this.results.push(result); + if (!result.valid) { + this.errors.push(result); } } - this.sort(); - this.errors = this.errorsInternal; - }; - ValidationErrorsCustomAttribute.prototype.bind = function () { - if (!this.controller) { - this.controller = this.controllerAccessor(); - } - // this will call render() with the side-effect of updating this.errors - this.controller.addRenderer(this); - }; - ValidationErrorsCustomAttribute.prototype.unbind = function () { - if (this.controller) { - this.controller.removeRenderer(this); + // render. + for (var _b = 0, _c = this.renderers; _b < _c.length; _b++) { + var renderer = _c[_b]; + renderer.render(instruction); } }; - __decorate([ - aureliaTemplating.bindable({ defaultBindingMode: aureliaBinding.bindingMode.oneWay }) - ], ValidationErrorsCustomAttribute.prototype, "controller", void 0); - __decorate([ - aureliaTemplating.bindable({ primaryProperty: true, defaultBindingMode: aureliaBinding.bindingMode.twoWay }) - ], ValidationErrorsCustomAttribute.prototype, "errors", void 0); - ValidationErrorsCustomAttribute = __decorate([ - aureliaTemplating.customAttribute('validation-errors') - ], ValidationErrorsCustomAttribute); - return ValidationErrorsCustomAttribute; -}()); - -var ValidationRendererCustomAttribute = /** @class */ (function () { - function ValidationRendererCustomAttribute() { - } - ValidationRendererCustomAttribute.prototype.created = function (view) { - this.container = view.container; - }; - ValidationRendererCustomAttribute.prototype.bind = function () { - this.controller = this.container.get(ValidationController); - this.renderer = this.container.get(this.value); - this.controller.addRenderer(this.renderer); - }; - ValidationRendererCustomAttribute.prototype.unbind = function () { - this.controller.removeRenderer(this.renderer); - this.controller = null; - this.renderer = null; - }; - ValidationRendererCustomAttribute = __decorate([ - aureliaTemplating.customAttribute('validation-renderer') - ], ValidationRendererCustomAttribute); - return ValidationRendererCustomAttribute; -}()); - -/** - * Sets, unsets and retrieves rules on an object or constructor function. - */ -var Rules = /** @class */ (function () { - function Rules() { - } /** - * Applies the rules to a target. + * Validates the property associated with a binding. */ - Rules.set = function (target, rules) { - if (target instanceof Function) { - target = target.prototype; + ValidationController.prototype.validateBinding = function (binding) { + if (!binding.isBound) { + return; } - Object.defineProperty(target, Rules.key, { enumerable: false, configurable: false, writable: true, value: rules }); - }; - /** - * Removes rules from a target. - */ - Rules.unset = function (target) { - if (target instanceof Function) { - target = target.prototype; + var propertyInfo = getPropertyInfo(binding.sourceExpression, binding.source); + var rules; + var registeredBinding = this.bindings.get(binding); + if (registeredBinding) { + rules = registeredBinding.rules; + registeredBinding.propertyInfo = propertyInfo; } - target[Rules.key] = null; + if (!propertyInfo) { + return; + } + var object = propertyInfo.object, propertyName = propertyInfo.propertyName; + this.validate({ object: object, propertyName: propertyName, rules: rules }); }; /** - * Retrieves the target's rules. + * Resets the results for a property associated with a binding. */ - Rules.get = function (target) { - return target[Rules.key] || null; + ValidationController.prototype.resetBinding = function (binding) { + var registeredBinding = this.bindings.get(binding); + var propertyInfo = getPropertyInfo(binding.sourceExpression, binding.source); + if (!propertyInfo && registeredBinding) { + propertyInfo = registeredBinding.propertyInfo; + } + if (registeredBinding) { + registeredBinding.propertyInfo = null; + } + if (!propertyInfo) { + return; + } + var object = propertyInfo.object, propertyName = propertyInfo.propertyName; + this.reset({ object: object, propertyName: propertyName }); }; /** - * The name of the property that stores the rules. + * Changes the controller's validateTrigger. + * @param newTrigger The new validateTrigger */ - Rules.key = '__rules__'; - return Rules; -}()); - -// tslint:disable:no-empty -var ExpressionVisitor = /** @class */ (function () { - function ExpressionVisitor() { - } - ExpressionVisitor.prototype.visitChain = function (chain) { - this.visitArgs(chain.expressions); - }; - ExpressionVisitor.prototype.visitBindingBehavior = function (behavior) { - behavior.expression.accept(this); - this.visitArgs(behavior.args); - }; - ExpressionVisitor.prototype.visitValueConverter = function (converter) { - converter.expression.accept(this); - this.visitArgs(converter.args); - }; - ExpressionVisitor.prototype.visitAssign = function (assign) { - assign.target.accept(this); - assign.value.accept(this); - }; - ExpressionVisitor.prototype.visitConditional = function (conditional) { - conditional.condition.accept(this); - conditional.yes.accept(this); - conditional.no.accept(this); - }; - ExpressionVisitor.prototype.visitAccessThis = function (access) { - access.ancestor = access.ancestor; - }; - ExpressionVisitor.prototype.visitAccessScope = function (access) { - access.name = access.name; - }; - ExpressionVisitor.prototype.visitAccessMember = function (access) { - access.object.accept(this); - }; - ExpressionVisitor.prototype.visitAccessKeyed = function (access) { - access.object.accept(this); - access.key.accept(this); - }; - ExpressionVisitor.prototype.visitCallScope = function (call) { - this.visitArgs(call.args); - }; - ExpressionVisitor.prototype.visitCallFunction = function (call) { - call.func.accept(this); - this.visitArgs(call.args); - }; - ExpressionVisitor.prototype.visitCallMember = function (call) { - call.object.accept(this); - this.visitArgs(call.args); - }; - ExpressionVisitor.prototype.visitPrefix = function (prefix) { - prefix.expression.accept(this); - }; - ExpressionVisitor.prototype.visitBinary = function (binary) { - binary.left.accept(this); - binary.right.accept(this); - }; - ExpressionVisitor.prototype.visitLiteralPrimitive = function (literal) { - literal.value = literal.value; - }; - ExpressionVisitor.prototype.visitLiteralArray = function (literal) { - this.visitArgs(literal.elements); - }; - ExpressionVisitor.prototype.visitLiteralObject = function (literal) { - this.visitArgs(literal.values); + ValidationController.prototype.changeTrigger = function (newTrigger) { + this.validateTrigger = newTrigger; + var bindings = Array.from(this.bindings.keys()); + for (var _i = 0, bindings_1 = bindings; _i < bindings_1.length; _i++) { + var binding = bindings_1[_i]; + var source = binding.source; + binding.unbind(); + binding.bind(source); + } }; - ExpressionVisitor.prototype.visitLiteralString = function (literal) { - literal.value = literal.value; + /** + * Revalidates the controller's current set of errors. + */ + ValidationController.prototype.revalidateErrors = function () { + for (var _i = 0, _a = this.errors; _i < _a.length; _i++) { + var _b = _a[_i], object = _b.object, propertyName = _b.propertyName, rule = _b.rule; + if (rule.__manuallyAdded__) { + continue; + } + var rules = [[rule]]; + this.validate({ object: object, propertyName: propertyName, rules: rules }); + } }; - ExpressionVisitor.prototype.visitArgs = function (args) { - for (var i = 0; i < args.length; i++) { - args[i].accept(this); + ValidationController.prototype.invokeCallbacks = function (instruction, result) { + if (this.eventCallbacks.length === 0) { + return; + } + var event = new ValidateEvent(result ? 'validate' : 'reset', this.errors, this.results, instruction || null, result); + for (var i = 0; i < this.eventCallbacks.length; i++) { + this.eventCallbacks[i](event); } }; - return ExpressionVisitor; + ValidationController.inject = [Validator, PropertyAccessorParser, GlobalValidationConfiguration]; + return ValidationController; }()); -var ValidationMessageParser = /** @class */ (function () { - function ValidationMessageParser(bindinqLanguage) { - this.bindinqLanguage = bindinqLanguage; - this.emptyStringExpression = new aureliaBinding.LiteralString(''); - this.nullExpression = new aureliaBinding.LiteralPrimitive(null); - this.undefinedExpression = new aureliaBinding.LiteralPrimitive(undefined); - this.cache = {}; +/** + * Binding behavior. Indicates the bound property should be validated. + */ +var ValidateBindingBehaviorBase = /** @class */ (function () { + function ValidateBindingBehaviorBase(taskQueue) { + this.taskQueue = taskQueue; } - ValidationMessageParser.prototype.parse = function (message) { - if (this.cache[message] !== undefined) { - return this.cache[message]; + ValidateBindingBehaviorBase.prototype.bind = function (binding, source, rulesOrController, rules) { + var _this = this; + // identify the target element. + var target = getTargetDOMElement(binding, source); + // locate the controller. + var controller; + if (rulesOrController instanceof ValidationController) { + controller = rulesOrController; } - var parts = this.bindinqLanguage.parseInterpolation(null, message); - if (parts === null) { - return new aureliaBinding.LiteralString(message); + else { + controller = source.container.get(aureliaDependencyInjection.Optional.of(ValidationController)); + rules = rulesOrController; } - var expression = new aureliaBinding.LiteralString(parts[0]); - for (var i = 1; i < parts.length; i += 2) { - expression = new aureliaBinding.Binary('+', expression, new aureliaBinding.Binary('+', this.coalesce(parts[i]), new aureliaBinding.LiteralString(parts[i + 1]))); + 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); + // tslint:disable-next-line:no-bitwise + if (trigger & exports.validateTrigger.change) { + binding.vbbUpdateSource = binding.updateSource; + // tslint:disable-next-line:only-arrow-functions + // tslint:disable-next-line:space-before-function-paren + binding.updateSource = function (value) { + this.vbbUpdateSource(value); + this.validationController.validateBinding(this); + }; + } + // tslint:disable-next-line:no-bitwise + if (trigger & exports.validateTrigger.blur) { + binding.validateBlurHandler = function () { + _this.taskQueue.queueMicroTask(function () { return controller.validateBinding(binding); }); + }; + binding.validateTarget = target; + target.addEventListener('blur', binding.validateBlurHandler); + } + if (trigger !== exports.validateTrigger.manual) { + binding.standardUpdateTarget = binding.updateTarget; + // tslint:disable-next-line:only-arrow-functions + // tslint:disable-next-line:space-before-function-paren + binding.updateTarget = function (value) { + this.standardUpdateTarget(value); + this.validationController.resetBinding(this); + }; } - MessageExpressionValidator.validate(expression, message); - this.cache[message] = expression; - return expression; - }; - ValidationMessageParser.prototype.coalesce = function (part) { - // part === null || part === undefined ? '' : part - return new aureliaBinding.Conditional(new aureliaBinding.Binary('||', new aureliaBinding.Binary('===', part, this.nullExpression), new aureliaBinding.Binary('===', part, this.undefinedExpression)), this.emptyStringExpression, new aureliaBinding.CallMember(part, 'toString', [])); - }; - ValidationMessageParser.inject = [aureliaTemplating.BindingLanguage]; - return ValidationMessageParser; -}()); -var MessageExpressionValidator = /** @class */ (function (_super) { - __extends(MessageExpressionValidator, _super); - function MessageExpressionValidator(originalMessage) { - var _this = _super.call(this) || this; - _this.originalMessage = originalMessage; - return _this; - } - MessageExpressionValidator.validate = function (expression, originalMessage) { - var visitor = new MessageExpressionValidator(originalMessage); - expression.accept(visitor); }; - MessageExpressionValidator.prototype.visitAccessScope = function (access) { - if (access.ancestor !== 0) { - throw new Error('$parent is not permitted in validation message expressions.'); + ValidateBindingBehaviorBase.prototype.unbind = function (binding) { + // reset the binding to it's original state. + if (binding.vbbUpdateSource) { + binding.updateSource = binding.vbbUpdateSource; + binding.vbbUpdateSource = null; } - if (['displayName', 'propertyName', 'value', 'object', 'config', 'getDisplayName'].indexOf(access.name) !== -1) { - LogManager.getLogger('aurelia-validation') - // tslint:disable-next-line:max-line-length - .warn("Did you mean to use \"$" + access.name + "\" instead of \"" + access.name + "\" in this validation message template: \"" + this.originalMessage + "\"?"); + 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 MessageExpressionValidator; -}(ExpressionVisitor)); + return ValidateBindingBehaviorBase; +}()); /** - * Dictionary of validation messages. [messageKey]: messageExpression + * Binding behavior. Indicates the bound property should be validated + * when the validate trigger specified by the associated controller's + * validateTrigger property occurs. */ -var validationMessages = { - /** - * The default validation message. Used with rules that have no standard message. - */ - default: "${$displayName} is invalid.", - required: "${$displayName} is required.", - matches: "${$displayName} is not correctly formatted.", - email: "${$displayName} is not a valid email.", - minLength: "${$displayName} must be at least ${$config.length} character${$config.length === 1 ? '' : 's'}.", - maxLength: "${$displayName} cannot be longer than ${$config.length} character${$config.length === 1 ? '' : 's'}.", - minItems: "${$displayName} must contain at least ${$config.count} item${$config.count === 1 ? '' : 's'}.", - maxItems: "${$displayName} cannot contain more than ${$config.count} item${$config.count === 1 ? '' : 's'}.", - min: "${$displayName} must be at least ${$config.constraint}.", - max: "${$displayName} must be at most ${$config.constraint}.", - range: "${$displayName} must be between or equal to ${$config.min} and ${$config.max}.", - between: "${$displayName} must be between but not equal to ${$config.min} and ${$config.max}.", - equals: "${$displayName} must be ${$config.expectedValue}.", -}; +var ValidateBindingBehavior = /** @class */ (function (_super) { + __extends(ValidateBindingBehavior, _super); + function ValidateBindingBehavior() { + return _super !== null && _super.apply(this, arguments) || this; + } + ValidateBindingBehavior.prototype.getValidateTrigger = function (controller) { + return controller.validateTrigger; + }; + ValidateBindingBehavior.inject = [aureliaTaskQueue.TaskQueue]; + ValidateBindingBehavior = __decorate([ + aureliaBinding.bindingBehavior('validate') + ], ValidateBindingBehavior); + return ValidateBindingBehavior; +}(ValidateBindingBehaviorBase)); /** - * Retrieves validation messages and property display names. + * 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 ValidationMessageProvider = /** @class */ (function () { - function ValidationMessageProvider(parser) { - this.parser = parser; +var ValidateManuallyBindingBehavior = /** @class */ (function (_super) { + __extends(ValidateManuallyBindingBehavior, _super); + function ValidateManuallyBindingBehavior() { + return _super !== null && _super.apply(this, arguments) || this; } - /** - * Returns a message binding expression that corresponds to the key. - * @param key The message key. - */ - ValidationMessageProvider.prototype.getMessage = function (key) { - var message; - if (key in validationMessages) { - message = validationMessages[key]; - } - else { - message = validationMessages['default']; - } - return this.parser.parse(message); + ValidateManuallyBindingBehavior.prototype.getValidateTrigger = function () { + return exports.validateTrigger.manual; }; - /** - * Formulates a property display name using the property name and the configured - * displayName (if provided). - * Override this with your own custom logic. - * @param propertyName The property name. - */ - ValidationMessageProvider.prototype.getDisplayName = function (propertyName, displayName) { - if (displayName !== null && displayName !== undefined) { - return (displayName instanceof Function) ? displayName() : displayName; - } - // split on upper-case letters. - var words = propertyName.toString().split(/(?=[A-Z])/).join(' '); - // capitalize first letter. - return words.charAt(0).toUpperCase() + words.slice(1); + ValidateManuallyBindingBehavior.inject = [aureliaTaskQueue.TaskQueue]; + ValidateManuallyBindingBehavior = __decorate([ + aureliaBinding.bindingBehavior('validateManually') + ], ValidateManuallyBindingBehavior); + return ValidateManuallyBindingBehavior; +}(ValidateBindingBehaviorBase)); +/** + * Binding behavior. Indicates the bound property should be validated + * when the associated element blurs. + */ +var ValidateOnBlurBindingBehavior = /** @class */ (function (_super) { + __extends(ValidateOnBlurBindingBehavior, _super); + function ValidateOnBlurBindingBehavior() { + return _super !== null && _super.apply(this, arguments) || this; + } + ValidateOnBlurBindingBehavior.prototype.getValidateTrigger = function () { + return exports.validateTrigger.blur; + }; + ValidateOnBlurBindingBehavior.inject = [aureliaTaskQueue.TaskQueue]; + ValidateOnBlurBindingBehavior = __decorate([ + aureliaBinding.bindingBehavior('validateOnBlur') + ], ValidateOnBlurBindingBehavior); + 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. + */ +var ValidateOnChangeBindingBehavior = /** @class */ (function (_super) { + __extends(ValidateOnChangeBindingBehavior, _super); + function ValidateOnChangeBindingBehavior() { + return _super !== null && _super.apply(this, arguments) || this; + } + ValidateOnChangeBindingBehavior.prototype.getValidateTrigger = function () { + return exports.validateTrigger.change; + }; + ValidateOnChangeBindingBehavior.inject = [aureliaTaskQueue.TaskQueue]; + ValidateOnChangeBindingBehavior = __decorate([ + aureliaBinding.bindingBehavior('validateOnChange') + ], ValidateOnChangeBindingBehavior); + 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. + */ +var ValidateOnChangeOrBlurBindingBehavior = /** @class */ (function (_super) { + __extends(ValidateOnChangeOrBlurBindingBehavior, _super); + function ValidateOnChangeOrBlurBindingBehavior() { + return _super !== null && _super.apply(this, arguments) || this; + } + ValidateOnChangeOrBlurBindingBehavior.prototype.getValidateTrigger = function () { + return exports.validateTrigger.changeOrBlur; }; - ValidationMessageProvider.inject = [ValidationMessageParser]; - return ValidationMessageProvider; -}()); + ValidateOnChangeOrBlurBindingBehavior.inject = [aureliaTaskQueue.TaskQueue]; + ValidateOnChangeOrBlurBindingBehavior = __decorate([ + aureliaBinding.bindingBehavior('validateOnChangeOrBlur') + ], ValidateOnChangeOrBlurBindingBehavior); + return ValidateOnChangeOrBlurBindingBehavior; +}(ValidateBindingBehaviorBase)); /** - * Validates. - * Responsible for validating objects and properties. + * Creates ValidationController instances. */ -var StandardValidator = /** @class */ (function (_super) { - __extends(StandardValidator, _super); - function StandardValidator(messageProvider, resources) { - var _this = _super.call(this) || this; - _this.messageProvider = messageProvider; - _this.lookupFunctions = resources.lookupFunctions; - _this.getDisplayName = messageProvider.getDisplayName.bind(messageProvider); - return _this; +var ValidationControllerFactory = /** @class */ (function () { + function ValidationControllerFactory(container) { + this.container = container; } - /** - * Validates the specified property. - * @param object The object to validate. - * @param propertyName The name of the property to validate. - * @param rules Optional. If unspecified, the rules will be looked up using the metadata - * for the object created by ValidationRules....on(class/object) - */ - StandardValidator.prototype.validateProperty = function (object, propertyName, rules) { - return this.validate(object, propertyName, rules || null); + ValidationControllerFactory.get = function (container) { + return new ValidationControllerFactory(container); }; /** - * Validates all rules for specified object and it's properties. - * @param object The object to validate. - * @param rules Optional. If unspecified, the rules will be looked up using the metadata - * for the object created by ValidationRules....on(class/object) + * Creates a new controller instance. */ - StandardValidator.prototype.validateObject = function (object, rules) { - return this.validate(object, null, rules || null); + ValidationControllerFactory.prototype.create = function (validator) { + if (!validator) { + validator = this.container.get(Validator); + } + var propertyParser = this.container.get(PropertyAccessorParser); + var config = this.container.get(GlobalValidationConfiguration); + return new ValidationController(validator, propertyParser, config); }; /** - * Determines whether a rule exists in a set of rules. - * @param rules The rules to search. - * @parem rule The rule to find. + * 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. */ - StandardValidator.prototype.ruleExists = function (rules, rule) { - var i = rules.length; - while (i--) { - if (rules[i].indexOf(rule) !== -1) { - return true; - } - } - return false; + ValidationControllerFactory.prototype.createForCurrentScope = function (validator) { + var controller = this.create(validator); + this.container.registerInstance(ValidationController, controller); + return controller; }; - StandardValidator.prototype.getMessage = function (rule, object, value) { - var expression = rule.message || this.messageProvider.getMessage(rule.messageKey); - // tslint:disable-next-line:prefer-const - var _a = rule.property, propertyName = _a.name, displayName = _a.displayName; - if (propertyName !== null) { - displayName = this.messageProvider.getDisplayName(propertyName, displayName); - } - var overrideContext = { - $displayName: displayName, - $propertyName: propertyName, - $value: value, - $object: object, - $config: rule.config, - // returns the name of a given property, given just the property name (irrespective of the property's displayName) - // split on capital letters, first letter ensured to be capitalized - $getDisplayName: this.getDisplayName - }; - return expression.evaluate({ bindingContext: object, overrideContext: overrideContext }, this.lookupFunctions); + return ValidationControllerFactory; +}()); +ValidationControllerFactory['protocol:aurelia:resolver'] = true; + +var ValidationErrorsCustomAttribute = /** @class */ (function () { + function ValidationErrorsCustomAttribute(boundaryElement, controllerAccessor) { + this.boundaryElement = boundaryElement; + this.controllerAccessor = controllerAccessor; + this.controller = null; + this.errors = []; + this.errorsInternal = []; + } + ValidationErrorsCustomAttribute.inject = function () { + return [aureliaPal.DOM.Element, aureliaDependencyInjection.Lazy.of(ValidationController)]; }; - StandardValidator.prototype.validateRuleSequence = function (object, propertyName, ruleSequence, sequence, results) { - var _this = this; - // are we validating all properties or a single property? - var validateAllProperties = propertyName === null || propertyName === undefined; - var rules = ruleSequence[sequence]; - var allValid = true; - // validate each rule. - var promises = []; - var _loop_1 = function (i) { - var rule = rules[i]; - // is the rule related to the property we're validating. - // tslint:disable-next-line:triple-equals | Use loose equality for property keys - if (!validateAllProperties && rule.property.name != propertyName) { - return "continue"; - } - // is this a conditional rule? is the condition met? - if (rule.when && !rule.when(object)) { - return "continue"; + ValidationErrorsCustomAttribute.prototype.sort = function () { + this.errorsInternal.sort(function (a, b) { + if (a.targets[0] === b.targets[0]) { + return 0; } - // validate. - var value = rule.property.name === null ? object : object[rule.property.name]; - var promiseOrBoolean = rule.condition(value, object); - if (!(promiseOrBoolean instanceof Promise)) { - promiseOrBoolean = Promise.resolve(promiseOrBoolean); + // tslint:disable-next-line:no-bitwise + return a.targets[0].compareDocumentPosition(b.targets[0]) & 2 ? 1 : -1; + }); + }; + ValidationErrorsCustomAttribute.prototype.interestingElements = function (elements) { + var _this = this; + return elements.filter(function (e) { return _this.boundaryElement.contains(e); }); + }; + ValidationErrorsCustomAttribute.prototype.render = function (instruction) { + var _loop_1 = function (result) { + var index = this_1.errorsInternal.findIndex(function (x) { return x.error === result; }); + if (index !== -1) { + this_1.errorsInternal.splice(index, 1); } - promises.push(promiseOrBoolean.then(function (valid) { - var message = valid ? null : _this.getMessage(rule, object, value); - results.push(new ValidateResult(rule, object, rule.property.name, valid, message)); - allValid = allValid && valid; - return valid; - })); }; - for (var i = 0; i < rules.length; i++) { - _loop_1(i); + var this_1 = this; + for (var _i = 0, _a = instruction.unrender; _i < _a.length; _i++) { + var result = _a[_i].result; + _loop_1(result); } - return Promise.all(promises) - .then(function () { - sequence++; - if (allValid && sequence < ruleSequence.length) { - return _this.validateRuleSequence(object, propertyName, ruleSequence, sequence, results); + for (var _b = 0, _c = instruction.render; _b < _c.length; _b++) { + var _d = _c[_b], result = _d.result, elements = _d.elements; + if (result.valid) { + continue; } - return results; - }); + var targets = this.interestingElements(elements); + if (targets.length) { + this.errorsInternal.push({ error: result, targets: targets }); + } + } + this.sort(); + this.errors = this.errorsInternal; }; - StandardValidator.prototype.validate = function (object, propertyName, rules) { - // rules specified? - if (!rules) { - // no. attempt to locate the rules. - rules = Rules.get(object); + ValidationErrorsCustomAttribute.prototype.bind = function () { + if (!this.controller) { + this.controller = this.controllerAccessor(); } - // any rules? - if (!rules || rules.length === 0) { - return Promise.resolve([]); + // this will call render() with the side-effect of updating this.errors + this.controller.addRenderer(this); + }; + ValidationErrorsCustomAttribute.prototype.unbind = function () { + if (this.controller) { + this.controller.removeRenderer(this); } - return this.validateRuleSequence(object, propertyName, rules, 0, []); }; - StandardValidator.inject = [ValidationMessageProvider, aureliaTemplating.ViewResources]; - return StandardValidator; -}(Validator)); + __decorate([ + aureliaTemplating.bindable({ defaultBindingMode: aureliaBinding.bindingMode.oneWay }) + ], ValidationErrorsCustomAttribute.prototype, "controller", void 0); + __decorate([ + aureliaTemplating.bindable({ primaryProperty: true, defaultBindingMode: aureliaBinding.bindingMode.twoWay }) + ], ValidationErrorsCustomAttribute.prototype, "errors", void 0); + ValidationErrorsCustomAttribute = __decorate([ + aureliaTemplating.customAttribute('validation-errors') + ], ValidationErrorsCustomAttribute); + return ValidationErrorsCustomAttribute; +}()); + +var ValidationRendererCustomAttribute = /** @class */ (function () { + function ValidationRendererCustomAttribute() { + } + ValidationRendererCustomAttribute.prototype.created = function (view) { + this.container = view.container; + }; + ValidationRendererCustomAttribute.prototype.bind = function () { + this.controller = this.container.get(ValidationController); + this.renderer = this.container.get(this.value); + this.controller.addRenderer(this.renderer); + }; + ValidationRendererCustomAttribute.prototype.unbind = function () { + this.controller.removeRenderer(this.renderer); + this.controller = null; + this.renderer = null; + }; + ValidationRendererCustomAttribute = __decorate([ + aureliaTemplating.customAttribute('validation-renderer') + ], ValidationRendererCustomAttribute); + return ValidationRendererCustomAttribute; +}()); /** * Part of the fluent rule API. Enables customizing property rules. @@ -1416,12 +1458,12 @@ var FluentRuleCustomizer = /** @class */ (function () { * @param args The rule's arguments. */ FluentRuleCustomizer.prototype.satisfiesRule = function (name) { + var _a; var args = []; for (var _i = 1; _i < arguments.length; _i++) { args[_i - 1] = arguments[_i]; } - var _a; - return (_a = this.fluentRules).satisfiesRule.apply(_a, [name].concat(args)); + return (_a = this.fluentRules).satisfiesRule.apply(_a, __spreadArrays([name], args)); }; /** * Applies the "required" rule to the property. @@ -1561,14 +1603,14 @@ var FluentRules = /** @class */ (function () { // standard rule? rule = this[name]; if (rule instanceof Function) { - return rule.call.apply(rule, [this].concat(args)); + return rule.call.apply(rule, __spreadArrays([this], args)); } throw new Error("Rule with name \"" + name + "\" does not exist."); } var config = rule.argsToConfig ? rule.argsToConfig.apply(rule, args) : undefined; return this.satisfies(function (value, obj) { var _a; - return (_a = rule.condition).call.apply(_a, [_this, value, obj].concat(args)); + return (_a = rule.condition).call.apply(_a, __spreadArrays([_this, value, obj], args)); }, config) .withMessageKey(name); }; @@ -1813,28 +1855,6 @@ var ValidationRules = /** @class */ (function () { }()); // Exports -/** - * Aurelia Validation Configuration API - */ -var AureliaValidationConfiguration = /** @class */ (function () { - function AureliaValidationConfiguration() { - this.validatorType = StandardValidator; - } - /** - * Use a custom Validator implementation. - */ - AureliaValidationConfiguration.prototype.customValidator = function (type) { - this.validatorType = type; - }; - /** - * Applies the configuration. - */ - AureliaValidationConfiguration.prototype.apply = function (container) { - var validator = container.get(this.validatorType); - container.registerInstance(Validator, validator); - }; - return AureliaValidationConfiguration; -}()); /** * Configures the plugin. */ @@ -1847,7 +1867,7 @@ frameworkConfig, callback) { var propertyParser = frameworkConfig.container.get(PropertyAccessorParser); ValidationRules.initialize(messageParser, propertyParser); // configure... - var config = new AureliaValidationConfiguration(); + var config = new GlobalValidationConfiguration(); if (callback instanceof Function) { callback(config); } @@ -1858,8 +1878,8 @@ frameworkConfig, callback) { } } -exports.AureliaValidationConfiguration = AureliaValidationConfiguration; exports.configure = configure; +exports.GlobalValidationConfiguration = GlobalValidationConfiguration; exports.getTargetDOMElement = getTargetDOMElement; exports.getPropertyInfo = getPropertyInfo; exports.PropertyAccessorParser = PropertyAccessorParser; diff --git a/dist/es2015/aurelia-validation.js b/dist/es2015/aurelia-validation.js index 7a46b442..f4d9fa19 100644 --- a/dist/es2015/aurelia-validation.js +++ b/dist/es2015/aurelia-validation.js @@ -1,161 +1,9 @@ +import { LiteralString, Binary, Conditional, LiteralPrimitive, CallMember, AccessMember, AccessScope, AccessKeyed, BindingBehavior, ValueConverter, getContextFor, Parser, bindingBehavior, bindingMode } from 'aurelia-binding'; +import { BindingLanguage, ViewResources, customAttribute, bindable } from 'aurelia-templating'; +import { getLogger } from 'aurelia-logging'; import { DOM } from 'aurelia-pal'; -import { AccessMember, AccessScope, AccessKeyed, BindingBehavior, ValueConverter, getContextFor, Parser, bindingBehavior, bindingMode, LiteralString, Binary, Conditional, LiteralPrimitive, CallMember } from 'aurelia-binding'; import { Optional, Lazy } from 'aurelia-dependency-injection'; import { TaskQueue } from 'aurelia-task-queue'; -import { customAttribute, bindable, BindingLanguage, ViewResources } from 'aurelia-templating'; -import { getLogger } from 'aurelia-logging'; - -/** - * 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. - */ -function getTargetDOMElement(binding, view) { - const target = binding.target; - // DOM element - if (target instanceof Element) { - return target; - } - // custom element or custom attribute - // tslint:disable-next-line:prefer-const - for (let i = 0, ii = view.controllers.length; i < ii; i++) { - const 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}".`); -} - -function getObject(expression, objectExpression, source) { - const value = objectExpression.evaluate(source, null); - if (value === null || value === undefined || value instanceof Object) { - return value; - } - // tslint:disable-next-line:max-line-length - throw new Error(`The '${objectExpression}' part of '${expression}' evaluates to ${value} instead of an object, null or undefined.`); -} -/** - * Retrieves the object and property name for the specified expression. - * @param expression The expression - * @param source The scope - */ -function getPropertyInfo(expression, source) { - const originalExpression = expression; - while (expression instanceof BindingBehavior || expression instanceof ValueConverter) { - expression = expression.expression; - } - let object; - let propertyName; - if (expression instanceof AccessScope) { - object = getContextFor(expression.name, source, expression.ancestor); - propertyName = expression.name; - } - else if (expression instanceof AccessMember) { - object = getObject(originalExpression, expression.object, source); - propertyName = expression.name; - } - else if (expression instanceof AccessKeyed) { - object = getObject(originalExpression, expression.object, source); - propertyName = expression.key.evaluate(source); - } - else { - throw new Error(`Expression '${originalExpression}' is not compatible with the validate binding-behavior.`); - } - if (object === null || object === undefined) { - return null; - } - return { object, propertyName }; -} - -function isString(value) { - return Object.prototype.toString.call(value) === '[object String]'; -} -function isNumber(value) { - return Object.prototype.toString.call(value) === '[object Number]'; -} - -class PropertyAccessorParser { - constructor(parser) { - this.parser = parser; - } - parse(property) { - if (isString(property) || isNumber(property)) { - return property; - } - const accessorText = getAccessorExpression(property.toString()); - const accessor = this.parser.parse(accessorText); - if (accessor instanceof AccessScope - || accessor instanceof AccessMember && accessor.object instanceof AccessScope) { - return accessor.name; - } - throw new Error(`Invalid property expression: "${accessor}"`); - } -} -PropertyAccessorParser.inject = [Parser]; -function getAccessorExpression(fn) { - /* tslint:disable:max-line-length */ - const classic = /^function\s*\([$_\w\d]+\)\s*\{(?:\s*"use strict";)?\s*(?:[$_\w\d.['"\]+;]+)?\s*return\s+[$_\w\d]+\.([$_\w\d]+)\s*;?\s*\}$/; - /* tslint:enable:max-line-length */ - const arrow = /^\(?[$_\w\d]+\)?\s*=>\s*[$_\w\d]+\.([$_\w\d]+)$/; - const match = classic.exec(fn) || arrow.exec(fn); - if (match === null) { - throw new Error(`Unable to parse accessor function:\n${fn}`); - } - return match[1]; -} - -/*! ***************************************************************************** -Copyright (c) Microsoft Corporation. All rights reserved. -Licensed under the Apache License, Version 2.0 (the "License"); you may not use -this file except in compliance with the License. You may obtain a copy of the -License at http://www.apache.org/licenses/LICENSE-2.0 - -THIS CODE IS PROVIDED ON AN *AS IS* BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY -KIND, EITHER EXPRESS OR IMPLIED, INCLUDING WITHOUT LIMITATION ANY IMPLIED -WARRANTIES OR CONDITIONS OF TITLE, FITNESS FOR A PARTICULAR PURPOSE, -MERCHANTABLITY OR NON-INFRINGEMENT. - -See the Apache Version 2.0 License for specific language governing permissions -and limitations under the License. -***************************************************************************** */ - -function __decorate(decorators, target, key, desc) { - var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d; - if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc); - else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r; - return c > 3 && r && Object.defineProperty(target, key, r), r; -} - -/** - * Validation triggers. - */ -var validateTrigger; -(function (validateTrigger) { - /** - * Manual validation. Use the controller's `validate()` and `reset()` methods - * to validate all bindings. - */ - validateTrigger[validateTrigger["manual"] = 0] = "manual"; - /** - * Validate the binding when the binding's target element fires a DOM "blur" event. - */ - validateTrigger[validateTrigger["blur"] = 1] = "blur"; - /** - * Validate the binding when it updates the model due to a change in the view. - */ - validateTrigger[validateTrigger["change"] = 2] = "change"; - /** - * 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. - */ - validateTrigger[validateTrigger["changeOrBlur"] = 3] = "changeOrBlur"; -})(validateTrigger || (validateTrigger = {})); /** * Validates objects and properties. @@ -187,1024 +35,1209 @@ class ValidateResult { } ValidateResult.nextId = 0; -class ValidateEvent { - constructor( - /** - * The type of validate event. Either "validate" or "reset". - */ - type, - /** - * The controller's current array of errors. For an array containing both - * failed rules and passed rules, use the "results" property. - */ - errors, +/** + * Sets, unsets and retrieves rules on an object or constructor function. + */ +class Rules { /** - * The controller's current array of validate results. This - * includes both passed rules and failed rules. For an array of only failed rules, - * use the "errors" property. + * Applies the rules to a target. */ - results, + static set(target, rules) { + if (target instanceof Function) { + target = target.prototype; + } + Object.defineProperty(target, Rules.key, { enumerable: false, configurable: false, writable: true, value: rules }); + } /** - * The instruction passed to the "validate" or "reset" event. Will be null when - * the controller's validate/reset method was called with no instruction argument. + * Removes rules from a target. */ - instruction, + static unset(target) { + if (target instanceof Function) { + target = target.prototype; + } + target[Rules.key] = null; + } /** - * In events with type === "validate", this property will contain the result - * of validating the instruction (see "instruction" property). Use the controllerValidateResult - * to access the validate results specific to the call to "validate" - * (as opposed to using the "results" and "errors" properties to access the controller's entire - * set of results/errors). + * Retrieves the target's rules. */ - controllerValidateResult) { - this.type = type; - this.errors = errors; - this.results = results; - this.instruction = instruction; - this.controllerValidateResult = controllerValidateResult; + static get(target) { + return target[Rules.key] || null; } -} - +} /** - * Orchestrates validation. - * Manages a set of bindings, renderers and objects. - * Exposes the current list of validation results for binding purposes. + * The name of the property that stores the rules. */ -class ValidationController { - constructor(validator, propertyParser) { - this.validator = validator; - this.propertyParser = propertyParser; - // Registered bindings (via the validate binding behavior) - this.bindings = new Map(); - // Renderers that have been added to the controller instance. - this.renderers = []; - /** - * Validation results that have been rendered by the controller. - */ - this.results = []; - /** - * Validation errors that have been rendered by the controller. - */ - this.errors = []; - /** - * Whether the controller is currently validating. - */ - this.validating = false; - // Elements related to validation results that have been rendered. - this.elements = new Map(); - // Objects that have been added to the controller instance (entity-style validation). - this.objects = new Map(); - /** - * The trigger that will invoke automatic validation of a property used in a binding. - */ - this.validateTrigger = validateTrigger.blur; - // Promise that resolves when validation has completed. - this.finishValidating = Promise.resolve(); - this.eventCallbacks = []; +Rules.key = '__rules__'; + +// tslint:disable:no-empty +class ExpressionVisitor { + visitChain(chain) { + this.visitArgs(chain.expressions); } - /** - * Subscribe to controller validate and reset events. These events occur when the - * controller's "validate"" and "reset" methods are called. - * @param callback The callback to be invoked when the controller validates or resets. - */ - subscribe(callback) { - this.eventCallbacks.push(callback); - return { - dispose: () => { - const index = this.eventCallbacks.indexOf(callback); - if (index === -1) { - return; - } - this.eventCallbacks.splice(index, 1); - } - }; + visitBindingBehavior(behavior) { + behavior.expression.accept(this); + this.visitArgs(behavior.args); } - /** - * Adds an object to the set of objects that should be validated when validate is called. - * @param object The object. - * @param rules Optional. The rules. If rules aren't supplied the Validator implementation will lookup the rules. - */ - addObject(object, rules) { - this.objects.set(object, rules); + visitValueConverter(converter) { + converter.expression.accept(this); + this.visitArgs(converter.args); + } + visitAssign(assign) { + assign.target.accept(this); + assign.value.accept(this); + } + visitConditional(conditional) { + conditional.condition.accept(this); + conditional.yes.accept(this); + conditional.no.accept(this); + } + visitAccessThis(access) { + access.ancestor = access.ancestor; + } + visitAccessScope(access) { + access.name = access.name; + } + visitAccessMember(access) { + access.object.accept(this); + } + visitAccessKeyed(access) { + access.object.accept(this); + access.key.accept(this); + } + visitCallScope(call) { + this.visitArgs(call.args); + } + visitCallFunction(call) { + call.func.accept(this); + this.visitArgs(call.args); + } + visitCallMember(call) { + call.object.accept(this); + this.visitArgs(call.args); + } + visitPrefix(prefix) { + prefix.expression.accept(this); + } + visitBinary(binary) { + binary.left.accept(this); + binary.right.accept(this); + } + visitLiteralPrimitive(literal) { + literal.value = literal.value; + } + visitLiteralArray(literal) { + this.visitArgs(literal.elements); } + visitLiteralObject(literal) { + this.visitArgs(literal.values); + } + visitLiteralString(literal) { + literal.value = literal.value; + } + visitArgs(args) { + for (let i = 0; i < args.length; i++) { + args[i].accept(this); + } + } +} + +class ValidationMessageParser { + constructor(bindinqLanguage) { + this.bindinqLanguage = bindinqLanguage; + this.emptyStringExpression = new LiteralString(''); + this.nullExpression = new LiteralPrimitive(null); + this.undefinedExpression = new LiteralPrimitive(undefined); + this.cache = {}; + } + parse(message) { + if (this.cache[message] !== undefined) { + return this.cache[message]; + } + const parts = this.bindinqLanguage.parseInterpolation(null, message); + if (parts === null) { + return new LiteralString(message); + } + let expression = new LiteralString(parts[0]); + for (let i = 1; i < parts.length; i += 2) { + expression = new Binary('+', expression, new Binary('+', this.coalesce(parts[i]), new LiteralString(parts[i + 1]))); + } + MessageExpressionValidator.validate(expression, message); + this.cache[message] = expression; + return expression; + } + coalesce(part) { + // part === null || part === undefined ? '' : part + return new Conditional(new Binary('||', new Binary('===', part, this.nullExpression), new Binary('===', part, this.undefinedExpression)), this.emptyStringExpression, new CallMember(part, 'toString', [])); + } +} +ValidationMessageParser.inject = [BindingLanguage]; +class MessageExpressionValidator extends ExpressionVisitor { + constructor(originalMessage) { + super(); + this.originalMessage = originalMessage; + } + static validate(expression, originalMessage) { + const visitor = new MessageExpressionValidator(originalMessage); + expression.accept(visitor); + } + visitAccessScope(access) { + if (access.ancestor !== 0) { + throw new Error('$parent is not permitted in validation message expressions.'); + } + if (['displayName', 'propertyName', 'value', 'object', 'config', 'getDisplayName'].indexOf(access.name) !== -1) { + getLogger('aurelia-validation') + // tslint:disable-next-line:max-line-length + .warn(`Did you mean to use "$${access.name}" instead of "${access.name}" in this validation message template: "${this.originalMessage}"?`); + } + } +} + +/** + * Dictionary of validation messages. [messageKey]: messageExpression + */ +const validationMessages = { /** - * Removes an object from the set of objects that should be validated when validate is called. - * @param object The object. + * The default validation message. Used with rules that have no standard message. */ - removeObject(object) { - this.objects.delete(object); - this.processResultDelta('reset', this.results.filter(result => result.object === object), []); + default: `\${$displayName} is invalid.`, + required: `\${$displayName} is required.`, + matches: `\${$displayName} is not correctly formatted.`, + email: `\${$displayName} is not a valid email.`, + minLength: `\${$displayName} must be at least \${$config.length} character\${$config.length === 1 ? '' : 's'}.`, + maxLength: `\${$displayName} cannot be longer than \${$config.length} character\${$config.length === 1 ? '' : 's'}.`, + minItems: `\${$displayName} must contain at least \${$config.count} item\${$config.count === 1 ? '' : 's'}.`, + maxItems: `\${$displayName} cannot contain more than \${$config.count} item\${$config.count === 1 ? '' : 's'}.`, + min: `\${$displayName} must be at least \${$config.constraint}.`, + max: `\${$displayName} must be at most \${$config.constraint}.`, + range: `\${$displayName} must be between or equal to \${$config.min} and \${$config.max}.`, + between: `\${$displayName} must be between but not equal to \${$config.min} and \${$config.max}.`, + equals: `\${$displayName} must be \${$config.expectedValue}.`, +}; +/** + * Retrieves validation messages and property display names. + */ +class ValidationMessageProvider { + constructor(parser) { + this.parser = parser; } /** - * Adds and renders an error. + * Returns a message binding expression that corresponds to the key. + * @param key The message key. */ - addError(message, object, propertyName = null) { - let resolvedPropertyName; - if (propertyName === null) { - resolvedPropertyName = propertyName; + getMessage(key) { + let message; + if (key in validationMessages) { + message = validationMessages[key]; } else { - resolvedPropertyName = this.propertyParser.parse(propertyName); + message = validationMessages['default']; } - const result = new ValidateResult({ __manuallyAdded__: true }, object, resolvedPropertyName, false, message); - this.processResultDelta('validate', [], [result]); - return result; + return this.parser.parse(message); } /** - * Removes and unrenders an error. + * Formulates a property display name using the property name and the configured + * displayName (if provided). + * Override this with your own custom logic. + * @param propertyName The property name. */ - removeError(result) { - if (this.results.indexOf(result) !== -1) { - this.processResultDelta('reset', [result], []); + getDisplayName(propertyName, displayName) { + if (displayName !== null && displayName !== undefined) { + return (displayName instanceof Function) ? displayName() : displayName; } + // split on upper-case letters. + const words = propertyName.toString().split(/(?=[A-Z])/).join(' '); + // capitalize first letter. + return words.charAt(0).toUpperCase() + words.slice(1); } - /** - * Adds a renderer. - * @param renderer The renderer. - */ - addRenderer(renderer) { - this.renderers.push(renderer); - renderer.render({ - kind: 'validate', - render: this.results.map(result => ({ result, elements: this.elements.get(result) })), - unrender: [] - }); - } - /** - * Removes a renderer. - * @param renderer The renderer. - */ - removeRenderer(renderer) { - this.renderers.splice(this.renderers.indexOf(renderer), 1); - renderer.render({ - kind: 'reset', - render: [], - unrender: this.results.map(result => ({ result, elements: this.elements.get(result) })) - }); +} +ValidationMessageProvider.inject = [ValidationMessageParser]; + +/** + * Validates. + * Responsible for validating objects and properties. + */ +class StandardValidator extends Validator { + constructor(messageProvider, resources) { + super(); + this.messageProvider = messageProvider; + this.lookupFunctions = resources.lookupFunctions; + this.getDisplayName = messageProvider.getDisplayName.bind(messageProvider); } /** - * Registers a binding with the controller. - * @param binding The binding instance. - * @param target The DOM element. - * @param rules (optional) rules associated with the binding. Validator implementation specific. + * Validates the specified property. + * @param object The object to validate. + * @param propertyName The name of the property to validate. + * @param rules Optional. If unspecified, the rules will be looked up using the metadata + * for the object created by ValidationRules....on(class/object) */ - registerBinding(binding, target, rules) { - this.bindings.set(binding, { target, rules, propertyInfo: null }); + validateProperty(object, propertyName, rules) { + return this.validate(object, propertyName, rules || null); } /** - * Unregisters a binding with the controller. - * @param binding The binding instance. + * Validates all rules for specified object and it's properties. + * @param object The object to validate. + * @param rules Optional. If unspecified, the rules will be looked up using the metadata + * for the object created by ValidationRules....on(class/object) */ - unregisterBinding(binding) { - this.resetBinding(binding); - this.bindings.delete(binding); + validateObject(object, rules) { + return this.validate(object, null, rules || null); } /** - * Interprets the instruction and returns a predicate that will identify - * relevant results in the list of rendered validation results. + * Determines whether a rule exists in a set of rules. + * @param rules The rules to search. + * @parem rule The rule to find. */ - getInstructionPredicate(instruction) { - if (instruction) { - const { object, propertyName, rules } = instruction; - let predicate; - if (instruction.propertyName) { - predicate = x => x.object === object && x.propertyName === propertyName; - } - else { - predicate = x => x.object === object; - } - if (rules) { - return x => predicate(x) && this.validator.ruleExists(rules, x.rule); + ruleExists(rules, rule) { + let i = rules.length; + while (i--) { + if (rules[i].indexOf(rule) !== -1) { + return true; } - return predicate; } - else { - return () => true; + return false; + } + getMessage(rule, object, value) { + const expression = rule.message || this.messageProvider.getMessage(rule.messageKey); + // tslint:disable-next-line:prefer-const + let { name: propertyName, displayName } = rule.property; + if (propertyName !== null) { + displayName = this.messageProvider.getDisplayName(propertyName, displayName); } + const overrideContext = { + $displayName: displayName, + $propertyName: propertyName, + $value: value, + $object: object, + $config: rule.config, + // returns the name of a given property, given just the property name (irrespective of the property's displayName) + // split on capital letters, first letter ensured to be capitalized + $getDisplayName: this.getDisplayName + }; + return expression.evaluate({ bindingContext: object, overrideContext }, this.lookupFunctions); } - /** - * Validates and renders results. - * @param instruction Optional. Instructions on what to validate. If undefined, all - * objects and bindings will be validated. - */ - validate(instruction) { - // Get a function that will process the validation instruction. - let execute; - if (instruction) { - // tslint:disable-next-line:prefer-const - let { object, propertyName, rules } = instruction; - // if rules were not specified, check the object map. - rules = rules || this.objects.get(object); - // property specified? - if (instruction.propertyName === undefined) { - // validate the specified object. - execute = () => this.validator.validateObject(object, rules); + validateRuleSequence(object, propertyName, ruleSequence, sequence, results) { + // are we validating all properties or a single property? + const validateAllProperties = propertyName === null || propertyName === undefined; + const rules = ruleSequence[sequence]; + let allValid = true; + // validate each rule. + const promises = []; + for (let i = 0; i < rules.length; i++) { + const rule = rules[i]; + // is the rule related to the property we're validating. + // tslint:disable-next-line:triple-equals | Use loose equality for property keys + if (!validateAllProperties && rule.property.name != propertyName) { + continue; } - else { - // validate the specified property. - execute = () => this.validator.validateProperty(object, propertyName, rules); + // is this a conditional rule? is the condition met? + if (rule.when && !rule.when(object)) { + continue; } + // validate. + const value = rule.property.name === null ? object : object[rule.property.name]; + let promiseOrBoolean = rule.condition(value, object); + if (!(promiseOrBoolean instanceof Promise)) { + promiseOrBoolean = Promise.resolve(promiseOrBoolean); + } + promises.push(promiseOrBoolean.then(valid => { + const message = valid ? null : this.getMessage(rule, object, value); + results.push(new ValidateResult(rule, object, rule.property.name, valid, message)); + allValid = allValid && valid; + return valid; + })); } - else { - // validate all objects and bindings. - execute = () => { - const promises = []; - for (const [object, rules] of Array.from(this.objects)) { - promises.push(this.validator.validateObject(object, rules)); - } - for (const [binding, { rules }] of Array.from(this.bindings)) { - const propertyInfo = getPropertyInfo(binding.sourceExpression, binding.source); - if (!propertyInfo || this.objects.has(propertyInfo.object)) { - continue; - } - promises.push(this.validator.validateProperty(propertyInfo.object, propertyInfo.propertyName, rules)); - } - return Promise.all(promises).then(resultSets => resultSets.reduce((a, b) => a.concat(b), [])); - }; - } - // Wait for any existing validation to finish, execute the instruction, render the results. - this.validating = true; - const returnPromise = this.finishValidating - .then(execute) - .then((newResults) => { - const predicate = this.getInstructionPredicate(instruction); - const oldResults = this.results.filter(predicate); - this.processResultDelta('validate', oldResults, newResults); - if (returnPromise === this.finishValidating) { - this.validating = false; + return Promise.all(promises) + .then(() => { + sequence++; + if (allValid && sequence < ruleSequence.length) { + return this.validateRuleSequence(object, propertyName, ruleSequence, sequence, results); } - const result = { - instruction, - valid: newResults.find(x => !x.valid) === undefined, - results: newResults - }; - this.invokeCallbacks(instruction, result); - return result; - }) - .catch(exception => { - // recover, to enable subsequent calls to validate() - this.validating = false; - this.finishValidating = Promise.resolve(); - return Promise.reject(exception); + return results; }); - this.finishValidating = returnPromise; - return returnPromise; } + validate(object, propertyName, rules) { + // rules specified? + if (!rules) { + // no. attempt to locate the rules. + rules = Rules.get(object); + } + // any rules? + if (!rules || rules.length === 0) { + return Promise.resolve([]); + } + return this.validateRuleSequence(object, propertyName, rules, 0, []); + } +} +StandardValidator.inject = [ValidationMessageProvider, ViewResources]; + +/** + * Validation triggers. + */ +var validateTrigger; +(function (validateTrigger) { /** - * Resets any rendered validation results (unrenders). - * @param instruction Optional. Instructions on what to reset. If unspecified all rendered results - * will be unrendered. + * Manual validation. Use the controller's `validate()` and `reset()` methods + * to validate all bindings. */ - reset(instruction) { - const predicate = this.getInstructionPredicate(instruction); - const oldResults = this.results.filter(predicate); - this.processResultDelta('reset', oldResults, []); - this.invokeCallbacks(instruction, null); + validateTrigger[validateTrigger["manual"] = 0] = "manual"; + /** + * Validate the binding when the binding's target element fires a DOM "blur" event. + */ + validateTrigger[validateTrigger["blur"] = 1] = "blur"; + /** + * Validate the binding when it updates the model due to a change in the view. + */ + validateTrigger[validateTrigger["change"] = 2] = "change"; + /** + * 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. + */ + validateTrigger[validateTrigger["changeOrBlur"] = 3] = "changeOrBlur"; +})(validateTrigger || (validateTrigger = {})); + +/** + * Aurelia Validation Configuration API + */ +class GlobalValidationConfiguration { + constructor() { + this.validatorType = StandardValidator; + this.validationTrigger = GlobalValidationConfiguration.DEFAULT_VALIDATION_TRIGGER; } /** - * Gets the elements associated with an object and propertyName (if any). + * Use a custom Validator implementation. */ - getAssociatedElements({ object, propertyName }) { - const elements = []; - for (const [binding, { target }] of Array.from(this.bindings)) { - const propertyInfo = getPropertyInfo(binding.sourceExpression, binding.source); - if (propertyInfo && propertyInfo.object === object && propertyInfo.propertyName === propertyName) { - elements.push(target); + customValidator(type) { + this.validatorType = type; + return this; + } + defaultValidationTrigger(trigger) { + this.validationTrigger = trigger; + return this; + } + getDefaultValidationTrigger() { + return this.validationTrigger; + } + /** + * Applies the configuration. + */ + apply(container) { + const validator = container.get(this.validatorType); + container.registerInstance(Validator, validator); + container.registerInstance(GlobalValidationConfiguration, this); + } +} +GlobalValidationConfiguration.DEFAULT_VALIDATION_TRIGGER = validateTrigger.blur; + +/** + * 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. + */ +function getTargetDOMElement(binding, view) { + const target = binding.target; + // DOM element + if (target instanceof Element) { + return target; + } + // custom element or custom attribute + // tslint:disable-next-line:prefer-const + for (let i = 0, ii = view.controllers.length; i < ii; i++) { + const 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}".`); } - return elements; } - processResultDelta(kind, oldResults, newResults) { - // prepare the instruction. - const instruction = { - kind, - render: [], - unrender: [] - }; - // create a shallow copy of newResults so we can mutate it without causing side-effects. - newResults = newResults.slice(0); - // create unrender instructions from the old results. - for (const oldResult of oldResults) { - // get the elements associated with the old result. - const elements = this.elements.get(oldResult); - // remove the old result from the element map. - this.elements.delete(oldResult); - // create the unrender instruction. - instruction.unrender.push({ result: oldResult, elements }); - // determine if there's a corresponding new result for the old result we are unrendering. - const newResultIndex = newResults.findIndex(x => x.rule === oldResult.rule && x.object === oldResult.object && x.propertyName === oldResult.propertyName); - if (newResultIndex === -1) { - // no corresponding new result... simple remove. - this.results.splice(this.results.indexOf(oldResult), 1); - if (!oldResult.valid) { - this.errors.splice(this.errors.indexOf(oldResult), 1); - } - } - else { - // there is a corresponding new result... - const newResult = newResults.splice(newResultIndex, 1)[0]; - // get the elements that are associated with the new result. - const elements = this.getAssociatedElements(newResult); - this.elements.set(newResult, elements); - // create a render instruction for the new result. - instruction.render.push({ result: newResult, elements }); - // do an in-place replacement of the old result with the new result. - // this ensures any repeats bound to this.results will not thrash. - this.results.splice(this.results.indexOf(oldResult), 1, newResult); - if (!oldResult.valid && newResult.valid) { - this.errors.splice(this.errors.indexOf(oldResult), 1); - } - else if (!oldResult.valid && !newResult.valid) { - this.errors.splice(this.errors.indexOf(oldResult), 1, newResult); - } - else if (!newResult.valid) { - this.errors.push(newResult); - } - } - } - // create render instructions from the remaining new results. - for (const result of newResults) { - const elements = this.getAssociatedElements(result); - instruction.render.push({ result, elements }); - this.elements.set(result, elements); - this.results.push(result); - if (!result.valid) { - this.errors.push(result); - } - } - // render. - for (const renderer of this.renderers) { - renderer.render(instruction); - } + throw new Error(`Unable to locate target element for "${binding.sourceExpression}".`); +} + +function getObject(expression, objectExpression, source) { + const value = objectExpression.evaluate(source, null); + if (value === null || value === undefined || value instanceof Object) { + return value; } - /** - * Validates the property associated with a binding. - */ - validateBinding(binding) { - if (!binding.isBound) { - return; - } - const propertyInfo = getPropertyInfo(binding.sourceExpression, binding.source); - let rules; - const registeredBinding = this.bindings.get(binding); - if (registeredBinding) { - rules = registeredBinding.rules; - registeredBinding.propertyInfo = propertyInfo; - } - if (!propertyInfo) { - return; - } - const { object, propertyName } = propertyInfo; - this.validate({ object, propertyName, rules }); + // tslint:disable-next-line:max-line-length + throw new Error(`The '${objectExpression}' part of '${expression}' evaluates to ${value} instead of an object, null or undefined.`); +} +/** + * Retrieves the object and property name for the specified expression. + * @param expression The expression + * @param source The scope + */ +function getPropertyInfo(expression, source) { + const originalExpression = expression; + while (expression instanceof BindingBehavior || expression instanceof ValueConverter) { + expression = expression.expression; } - /** - * Resets the results for a property associated with a binding. - */ - resetBinding(binding) { - const registeredBinding = this.bindings.get(binding); - let propertyInfo = getPropertyInfo(binding.sourceExpression, binding.source); - if (!propertyInfo && registeredBinding) { - propertyInfo = registeredBinding.propertyInfo; - } - if (registeredBinding) { - registeredBinding.propertyInfo = null; - } - if (!propertyInfo) { - return; - } - const { object, propertyName } = propertyInfo; - this.reset({ object, propertyName }); + let object; + let propertyName; + if (expression instanceof AccessScope) { + object = getContextFor(expression.name, source, expression.ancestor); + propertyName = expression.name; } - /** - * Changes the controller's validateTrigger. - * @param newTrigger The new validateTrigger - */ - changeTrigger(newTrigger) { - this.validateTrigger = newTrigger; - const bindings = Array.from(this.bindings.keys()); - for (const binding of bindings) { - const source = binding.source; - binding.unbind(); - binding.bind(source); - } + else if (expression instanceof AccessMember) { + object = getObject(originalExpression, expression.object, source); + propertyName = expression.name; } - /** - * Revalidates the controller's current set of errors. - */ - revalidateErrors() { - for (const { object, propertyName, rule } of this.errors) { - if (rule.__manuallyAdded__) { - continue; - } - const rules = [[rule]]; - this.validate({ object, propertyName, rules }); - } + else if (expression instanceof AccessKeyed) { + object = getObject(originalExpression, expression.object, source); + propertyName = expression.key.evaluate(source); } - invokeCallbacks(instruction, result) { - if (this.eventCallbacks.length === 0) { - return; - } - const event = new ValidateEvent(result ? 'validate' : 'reset', this.errors, this.results, instruction || null, result); - for (let i = 0; i < this.eventCallbacks.length; i++) { - this.eventCallbacks[i](event); - } + else { + throw new Error(`Expression '${originalExpression}' is not compatible with the validate binding-behavior.`); } + if (object === null || object === undefined) { + return null; + } + return { object, propertyName }; +} + +function isString(value) { + return Object.prototype.toString.call(value) === '[object String]'; } -ValidationController.inject = [Validator, PropertyAccessorParser]; +function isNumber(value) { + return Object.prototype.toString.call(value) === '[object Number]'; +} -/** - * Binding behavior. Indicates the bound property should be validated. - */ -class ValidateBindingBehaviorBase { - constructor(taskQueue) { - this.taskQueue = taskQueue; +class PropertyAccessorParser { + constructor(parser) { + this.parser = parser; } - bind(binding, source, rulesOrController, rules) { - // identify the target element. - const target = getTargetDOMElement(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); - // tslint:disable-next-line:no-bitwise - if (trigger & validateTrigger.change) { - binding.vbbUpdateSource = binding.updateSource; - // tslint:disable-next-line:only-arrow-functions - // tslint:disable-next-line:space-before-function-paren - binding.updateSource = function (value) { - this.vbbUpdateSource(value); - this.validationController.validateBinding(this); - }; - } - // tslint:disable-next-line:no-bitwise - if (trigger & validateTrigger.blur) { - binding.validateBlurHandler = () => { - this.taskQueue.queueMicroTask(() => controller.validateBinding(binding)); - }; - binding.validateTarget = target; - target.addEventListener('blur', binding.validateBlurHandler); + parse(property) { + if (isString(property) || isNumber(property)) { + return property; } - if (trigger !== validateTrigger.manual) { - binding.standardUpdateTarget = binding.updateTarget; - // tslint:disable-next-line:only-arrow-functions - // tslint:disable-next-line:space-before-function-paren - binding.updateTarget = function (value) { - this.standardUpdateTarget(value); - this.validationController.resetBinding(this); - }; + const accessorText = getAccessorExpression(property.toString()); + const accessor = this.parser.parse(accessorText); + if (accessor instanceof AccessScope + || accessor instanceof AccessMember && accessor.object instanceof AccessScope) { + return accessor.name; } + throw new Error(`Invalid property expression: "${accessor}"`); } - unbind(binding) { - // reset the binding to it's original state. - if (binding.vbbUpdateSource) { - binding.updateSource = binding.vbbUpdateSource; - binding.vbbUpdateSource = 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; +} +PropertyAccessorParser.inject = [Parser]; +function getAccessorExpression(fn) { + /* tslint:disable:max-line-length */ + const classic = /^function\s*\([$_\w\d]+\)\s*\{(?:\s*"use strict";)?\s*(?:[$_\w\d.['"\]+;]+)?\s*return\s+[$_\w\d]+\.([$_\w\d]+)\s*;?\s*\}$/; + /* tslint:enable:max-line-length */ + const arrow = /^\(?[$_\w\d]+\)?\s*=>\s*[$_\w\d]+\.([$_\w\d]+)$/; + const match = classic.exec(fn) || arrow.exec(fn); + if (match === null) { + throw new Error(`Unable to parse accessor function:\n${fn}`); } + return match[1]; } -/** - * Binding behavior. Indicates the bound property should be validated - * when the validate trigger specified by the associated controller's - * validateTrigger property occurs. - */ -let ValidateBindingBehavior = class ValidateBindingBehavior extends ValidateBindingBehaviorBase { - getValidateTrigger(controller) { - return controller.validateTrigger; - } -}; -ValidateBindingBehavior.inject = [TaskQueue]; -ValidateBindingBehavior = __decorate([ - bindingBehavior('validate') -], 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. - */ -let ValidateManuallyBindingBehavior = class ValidateManuallyBindingBehavior extends ValidateBindingBehaviorBase { - getValidateTrigger() { - return validateTrigger.manual; - } -}; -ValidateManuallyBindingBehavior.inject = [TaskQueue]; -ValidateManuallyBindingBehavior = __decorate([ - bindingBehavior('validateManually') -], ValidateManuallyBindingBehavior); -/** - * Binding behavior. Indicates the bound property should be validated - * when the associated element blurs. - */ -let ValidateOnBlurBindingBehavior = class ValidateOnBlurBindingBehavior extends ValidateBindingBehaviorBase { - getValidateTrigger() { - return validateTrigger.blur; +/*! ***************************************************************************** +Copyright (c) Microsoft Corporation. All rights reserved. +Licensed under the Apache License, Version 2.0 (the "License"); you may not use +this file except in compliance with the License. You may obtain a copy of the +License at http://www.apache.org/licenses/LICENSE-2.0 + +THIS CODE IS PROVIDED ON AN *AS IS* BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +KIND, EITHER EXPRESS OR IMPLIED, INCLUDING WITHOUT LIMITATION ANY IMPLIED +WARRANTIES OR CONDITIONS OF TITLE, FITNESS FOR A PARTICULAR PURPOSE, +MERCHANTABLITY OR NON-INFRINGEMENT. + +See the Apache Version 2.0 License for specific language governing permissions +and limitations under the License. +***************************************************************************** */ + +function __decorate(decorators, target, key, desc) { + var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d; + if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc); + else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r; + return c > 3 && r && Object.defineProperty(target, key, r), r; +} + +class ValidateEvent { + constructor( + /** + * The type of validate event. Either "validate" or "reset". + */ + type, + /** + * The controller's current array of errors. For an array containing both + * failed rules and passed rules, use the "results" property. + */ + errors, + /** + * The controller's current array of validate results. This + * includes both passed rules and failed rules. For an array of only failed rules, + * use the "errors" property. + */ + results, + /** + * The instruction passed to the "validate" or "reset" event. Will be null when + * the controller's validate/reset method was called with no instruction argument. + */ + instruction, + /** + * In events with type === "validate", this property will contain the result + * of validating the instruction (see "instruction" property). Use the controllerValidateResult + * to access the validate results specific to the call to "validate" + * (as opposed to using the "results" and "errors" properties to access the controller's entire + * set of results/errors). + */ + controllerValidateResult) { + this.type = type; + this.errors = errors; + this.results = results; + this.instruction = instruction; + this.controllerValidateResult = controllerValidateResult; } -}; -ValidateOnBlurBindingBehavior.inject = [TaskQueue]; -ValidateOnBlurBindingBehavior = __decorate([ - bindingBehavior('validateOnBlur') -], 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. + * Orchestrates validation. + * Manages a set of bindings, renderers and objects. + * Exposes the current list of validation results for binding purposes. */ -let ValidateOnChangeBindingBehavior = class ValidateOnChangeBindingBehavior extends ValidateBindingBehaviorBase { - getValidateTrigger() { - return validateTrigger.change; +class ValidationController { + constructor(validator, propertyParser, config) { + this.validator = validator; + this.propertyParser = propertyParser; + // Registered bindings (via the validate binding behavior) + this.bindings = new Map(); + // Renderers that have been added to the controller instance. + this.renderers = []; + /** + * Validation results that have been rendered by the controller. + */ + this.results = []; + /** + * Validation errors that have been rendered by the controller. + */ + this.errors = []; + /** + * Whether the controller is currently validating. + */ + this.validating = false; + // Elements related to validation results that have been rendered. + this.elements = new Map(); + // Objects that have been added to the controller instance (entity-style validation). + this.objects = new Map(); + // Promise that resolves when validation has completed. + this.finishValidating = Promise.resolve(); + this.eventCallbacks = []; + this.validateTrigger = config instanceof GlobalValidationConfiguration + ? config.getDefaultValidationTrigger() + : GlobalValidationConfiguration.DEFAULT_VALIDATION_TRIGGER; } -}; -ValidateOnChangeBindingBehavior.inject = [TaskQueue]; -ValidateOnChangeBindingBehavior = __decorate([ - bindingBehavior('validateOnChange') -], 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. - */ -let ValidateOnChangeOrBlurBindingBehavior = class ValidateOnChangeOrBlurBindingBehavior extends ValidateBindingBehaviorBase { - getValidateTrigger() { - return validateTrigger.changeOrBlur; + /** + * Subscribe to controller validate and reset events. These events occur when the + * controller's "validate"" and "reset" methods are called. + * @param callback The callback to be invoked when the controller validates or resets. + */ + subscribe(callback) { + this.eventCallbacks.push(callback); + return { + dispose: () => { + const index = this.eventCallbacks.indexOf(callback); + if (index === -1) { + return; + } + this.eventCallbacks.splice(index, 1); + } + }; } -}; -ValidateOnChangeOrBlurBindingBehavior.inject = [TaskQueue]; -ValidateOnChangeOrBlurBindingBehavior = __decorate([ - bindingBehavior('validateOnChangeOrBlur') -], ValidateOnChangeOrBlurBindingBehavior); - -/** - * Creates ValidationController instances. - */ -class ValidationControllerFactory { - constructor(container) { - this.container = container; + /** + * Adds an object to the set of objects that should be validated when validate is called. + * @param object The object. + * @param rules Optional. The rules. If rules aren't supplied the Validator implementation will lookup the rules. + */ + addObject(object, rules) { + this.objects.set(object, rules); } - static get(container) { - return new ValidationControllerFactory(container); + /** + * Removes an object from the set of objects that should be validated when validate is called. + * @param object The object. + */ + removeObject(object) { + this.objects.delete(object); + this.processResultDelta('reset', this.results.filter(result => result.object === object), []); } /** - * Creates a new controller instance. + * Adds and renders an error. */ - create(validator) { - if (!validator) { - validator = this.container.get(Validator); + addError(message, object, propertyName = null) { + let resolvedPropertyName; + if (propertyName === null) { + resolvedPropertyName = propertyName; } - const propertyParser = this.container.get(PropertyAccessorParser); - return new ValidationController(validator, propertyParser); + else { + resolvedPropertyName = this.propertyParser.parse(propertyName); + } + const result = new ValidateResult({ __manuallyAdded__: true }, object, resolvedPropertyName, false, message); + this.processResultDelta('validate', [], [result]); + return result; } /** - * 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. + * Removes and unrenders an error. */ - createForCurrentScope(validator) { - const controller = this.create(validator); - this.container.registerInstance(ValidationController, controller); - return controller; - } -} -ValidationControllerFactory['protocol:aurelia:resolver'] = true; - -let ValidationErrorsCustomAttribute = class ValidationErrorsCustomAttribute { - constructor(boundaryElement, controllerAccessor) { - this.boundaryElement = boundaryElement; - this.controllerAccessor = controllerAccessor; - this.controller = null; - this.errors = []; - this.errorsInternal = []; + removeError(result) { + if (this.results.indexOf(result) !== -1) { + this.processResultDelta('reset', [result], []); + } } - static inject() { - return [DOM.Element, Lazy.of(ValidationController)]; + /** + * Adds a renderer. + * @param renderer The renderer. + */ + addRenderer(renderer) { + this.renderers.push(renderer); + renderer.render({ + kind: 'validate', + render: this.results.map(result => ({ result, elements: this.elements.get(result) })), + unrender: [] + }); } - sort() { - this.errorsInternal.sort((a, b) => { - if (a.targets[0] === b.targets[0]) { - return 0; - } - // tslint:disable-next-line:no-bitwise - return a.targets[0].compareDocumentPosition(b.targets[0]) & 2 ? 1 : -1; + /** + * Removes a renderer. + * @param renderer The renderer. + */ + removeRenderer(renderer) { + this.renderers.splice(this.renderers.indexOf(renderer), 1); + renderer.render({ + kind: 'reset', + render: [], + unrender: this.results.map(result => ({ result, elements: this.elements.get(result) })) }); } - interestingElements(elements) { - return elements.filter(e => this.boundaryElement.contains(e)); + /** + * Registers a binding with the controller. + * @param binding The binding instance. + * @param target The DOM element. + * @param rules (optional) rules associated with the binding. Validator implementation specific. + */ + registerBinding(binding, target, rules) { + this.bindings.set(binding, { target, rules, propertyInfo: null }); } - render(instruction) { - for (const { result } of instruction.unrender) { - const index = this.errorsInternal.findIndex(x => x.error === result); - if (index !== -1) { - this.errorsInternal.splice(index, 1); - } - } - for (const { result, elements } of instruction.render) { - if (result.valid) { - continue; + /** + * Unregisters a binding with the controller. + * @param binding The binding instance. + */ + unregisterBinding(binding) { + this.resetBinding(binding); + this.bindings.delete(binding); + } + /** + * Interprets the instruction and returns a predicate that will identify + * relevant results in the list of rendered validation results. + */ + getInstructionPredicate(instruction) { + if (instruction) { + const { object, propertyName, rules } = instruction; + let predicate; + if (instruction.propertyName) { + predicate = x => x.object === object && x.propertyName === propertyName; } - const targets = this.interestingElements(elements); - if (targets.length) { - this.errorsInternal.push({ error: result, targets }); + else { + predicate = x => x.object === object; } + if (rules) { + return x => predicate(x) && this.validator.ruleExists(rules, x.rule); + } + return predicate; } - this.sort(); - this.errors = this.errorsInternal; - } - bind() { - if (!this.controller) { - this.controller = this.controllerAccessor(); - } - // this will call render() with the side-effect of updating this.errors - this.controller.addRenderer(this); - } - unbind() { - if (this.controller) { - this.controller.removeRenderer(this); + else { + return () => true; } } -}; -__decorate([ - bindable({ defaultBindingMode: bindingMode.oneWay }) -], ValidationErrorsCustomAttribute.prototype, "controller", void 0); -__decorate([ - bindable({ primaryProperty: true, defaultBindingMode: bindingMode.twoWay }) -], ValidationErrorsCustomAttribute.prototype, "errors", void 0); -ValidationErrorsCustomAttribute = __decorate([ - customAttribute('validation-errors') -], ValidationErrorsCustomAttribute); - -let ValidationRendererCustomAttribute = class ValidationRendererCustomAttribute { - created(view) { - this.container = view.container; - } - bind() { - this.controller = this.container.get(ValidationController); - this.renderer = this.container.get(this.value); - this.controller.addRenderer(this.renderer); - } - unbind() { - this.controller.removeRenderer(this.renderer); - this.controller = null; - this.renderer = null; - } -}; -ValidationRendererCustomAttribute = __decorate([ - customAttribute('validation-renderer') -], ValidationRendererCustomAttribute); - -/** - * Sets, unsets and retrieves rules on an object or constructor function. - */ -class Rules { /** - * Applies the rules to a target. + * Validates and renders results. + * @param instruction Optional. Instructions on what to validate. If undefined, all + * objects and bindings will be validated. */ - static set(target, rules) { - if (target instanceof Function) { - target = target.prototype; + validate(instruction) { + // Get a function that will process the validation instruction. + let execute; + if (instruction) { + // tslint:disable-next-line:prefer-const + let { object, propertyName, rules } = instruction; + // if rules were not specified, check the object map. + rules = rules || this.objects.get(object); + // property specified? + if (instruction.propertyName === undefined) { + // validate the specified object. + execute = () => this.validator.validateObject(object, rules); + } + else { + // validate the specified property. + execute = () => this.validator.validateProperty(object, propertyName, rules); + } } - Object.defineProperty(target, Rules.key, { enumerable: false, configurable: false, writable: true, value: rules }); + else { + // validate all objects and bindings. + execute = () => { + const promises = []; + for (const [object, rules] of Array.from(this.objects)) { + promises.push(this.validator.validateObject(object, rules)); + } + for (const [binding, { rules }] of Array.from(this.bindings)) { + const propertyInfo = getPropertyInfo(binding.sourceExpression, binding.source); + if (!propertyInfo || this.objects.has(propertyInfo.object)) { + continue; + } + promises.push(this.validator.validateProperty(propertyInfo.object, propertyInfo.propertyName, rules)); + } + return Promise.all(promises).then(resultSets => resultSets.reduce((a, b) => a.concat(b), [])); + }; + } + // Wait for any existing validation to finish, execute the instruction, render the results. + this.validating = true; + const returnPromise = this.finishValidating + .then(execute) + .then((newResults) => { + const predicate = this.getInstructionPredicate(instruction); + const oldResults = this.results.filter(predicate); + this.processResultDelta('validate', oldResults, newResults); + if (returnPromise === this.finishValidating) { + this.validating = false; + } + const result = { + instruction, + valid: newResults.find(x => !x.valid) === undefined, + results: newResults + }; + this.invokeCallbacks(instruction, result); + return result; + }) + .catch(exception => { + // recover, to enable subsequent calls to validate() + this.validating = false; + this.finishValidating = Promise.resolve(); + return Promise.reject(exception); + }); + this.finishValidating = returnPromise; + return returnPromise; } /** - * Removes rules from a target. + * Resets any rendered validation results (unrenders). + * @param instruction Optional. Instructions on what to reset. If unspecified all rendered results + * will be unrendered. */ - static unset(target) { - if (target instanceof Function) { - target = target.prototype; - } - target[Rules.key] = null; + reset(instruction) { + const predicate = this.getInstructionPredicate(instruction); + const oldResults = this.results.filter(predicate); + this.processResultDelta('reset', oldResults, []); + this.invokeCallbacks(instruction, null); } /** - * Retrieves the target's rules. + * Gets the elements associated with an object and propertyName (if any). */ - static get(target) { - return target[Rules.key] || null; - } -} -/** - * The name of the property that stores the rules. - */ -Rules.key = '__rules__'; - -// tslint:disable:no-empty -class ExpressionVisitor { - visitChain(chain) { - this.visitArgs(chain.expressions); - } - visitBindingBehavior(behavior) { - behavior.expression.accept(this); - this.visitArgs(behavior.args); - } - visitValueConverter(converter) { - converter.expression.accept(this); - this.visitArgs(converter.args); - } - visitAssign(assign) { - assign.target.accept(this); - assign.value.accept(this); - } - visitConditional(conditional) { - conditional.condition.accept(this); - conditional.yes.accept(this); - conditional.no.accept(this); - } - visitAccessThis(access) { - access.ancestor = access.ancestor; - } - visitAccessScope(access) { - access.name = access.name; - } - visitAccessMember(access) { - access.object.accept(this); - } - visitAccessKeyed(access) { - access.object.accept(this); - access.key.accept(this); - } - visitCallScope(call) { - this.visitArgs(call.args); - } - visitCallFunction(call) { - call.func.accept(this); - this.visitArgs(call.args); - } - visitCallMember(call) { - call.object.accept(this); - this.visitArgs(call.args); - } - visitPrefix(prefix) { - prefix.expression.accept(this); + getAssociatedElements({ object, propertyName }) { + const elements = []; + for (const [binding, { target }] of Array.from(this.bindings)) { + const propertyInfo = getPropertyInfo(binding.sourceExpression, binding.source); + if (propertyInfo && propertyInfo.object === object && propertyInfo.propertyName === propertyName) { + elements.push(target); + } + } + return elements; } - visitBinary(binary) { - binary.left.accept(this); - binary.right.accept(this); + processResultDelta(kind, oldResults, newResults) { + // prepare the instruction. + const instruction = { + kind, + render: [], + unrender: [] + }; + // create a shallow copy of newResults so we can mutate it without causing side-effects. + newResults = newResults.slice(0); + // create unrender instructions from the old results. + for (const oldResult of oldResults) { + // get the elements associated with the old result. + const elements = this.elements.get(oldResult); + // remove the old result from the element map. + this.elements.delete(oldResult); + // create the unrender instruction. + instruction.unrender.push({ result: oldResult, elements }); + // determine if there's a corresponding new result for the old result we are unrendering. + const newResultIndex = newResults.findIndex(x => x.rule === oldResult.rule && x.object === oldResult.object && x.propertyName === oldResult.propertyName); + if (newResultIndex === -1) { + // no corresponding new result... simple remove. + this.results.splice(this.results.indexOf(oldResult), 1); + if (!oldResult.valid) { + this.errors.splice(this.errors.indexOf(oldResult), 1); + } + } + else { + // there is a corresponding new result... + const newResult = newResults.splice(newResultIndex, 1)[0]; + // get the elements that are associated with the new result. + const elements = this.getAssociatedElements(newResult); + this.elements.set(newResult, elements); + // create a render instruction for the new result. + instruction.render.push({ result: newResult, elements }); + // do an in-place replacement of the old result with the new result. + // this ensures any repeats bound to this.results will not thrash. + this.results.splice(this.results.indexOf(oldResult), 1, newResult); + if (!oldResult.valid && newResult.valid) { + this.errors.splice(this.errors.indexOf(oldResult), 1); + } + else if (!oldResult.valid && !newResult.valid) { + this.errors.splice(this.errors.indexOf(oldResult), 1, newResult); + } + else if (!newResult.valid) { + this.errors.push(newResult); + } + } + } + // create render instructions from the remaining new results. + for (const result of newResults) { + const elements = this.getAssociatedElements(result); + instruction.render.push({ result, elements }); + this.elements.set(result, elements); + this.results.push(result); + if (!result.valid) { + this.errors.push(result); + } + } + // render. + for (const renderer of this.renderers) { + renderer.render(instruction); + } } - visitLiteralPrimitive(literal) { - literal.value = literal.value; + /** + * Validates the property associated with a binding. + */ + validateBinding(binding) { + if (!binding.isBound) { + return; + } + const propertyInfo = getPropertyInfo(binding.sourceExpression, binding.source); + let rules; + const registeredBinding = this.bindings.get(binding); + if (registeredBinding) { + rules = registeredBinding.rules; + registeredBinding.propertyInfo = propertyInfo; + } + if (!propertyInfo) { + return; + } + const { object, propertyName } = propertyInfo; + this.validate({ object, propertyName, rules }); } - visitLiteralArray(literal) { - this.visitArgs(literal.elements); + /** + * Resets the results for a property associated with a binding. + */ + resetBinding(binding) { + const registeredBinding = this.bindings.get(binding); + let propertyInfo = getPropertyInfo(binding.sourceExpression, binding.source); + if (!propertyInfo && registeredBinding) { + propertyInfo = registeredBinding.propertyInfo; + } + if (registeredBinding) { + registeredBinding.propertyInfo = null; + } + if (!propertyInfo) { + return; + } + const { object, propertyName } = propertyInfo; + this.reset({ object, propertyName }); } - visitLiteralObject(literal) { - this.visitArgs(literal.values); + /** + * Changes the controller's validateTrigger. + * @param newTrigger The new validateTrigger + */ + changeTrigger(newTrigger) { + this.validateTrigger = newTrigger; + const bindings = Array.from(this.bindings.keys()); + for (const binding of bindings) { + const source = binding.source; + binding.unbind(); + binding.bind(source); + } } - visitLiteralString(literal) { - literal.value = literal.value; + /** + * Revalidates the controller's current set of errors. + */ + revalidateErrors() { + for (const { object, propertyName, rule } of this.errors) { + if (rule.__manuallyAdded__) { + continue; + } + const rules = [[rule]]; + this.validate({ object, propertyName, rules }); + } } - visitArgs(args) { - for (let i = 0; i < args.length; i++) { - args[i].accept(this); + invokeCallbacks(instruction, result) { + if (this.eventCallbacks.length === 0) { + return; + } + const event = new ValidateEvent(result ? 'validate' : 'reset', this.errors, this.results, instruction || null, result); + for (let i = 0; i < this.eventCallbacks.length; i++) { + this.eventCallbacks[i](event); } } -} +} +ValidationController.inject = [Validator, PropertyAccessorParser, GlobalValidationConfiguration]; -class ValidationMessageParser { - constructor(bindinqLanguage) { - this.bindinqLanguage = bindinqLanguage; - this.emptyStringExpression = new LiteralString(''); - this.nullExpression = new LiteralPrimitive(null); - this.undefinedExpression = new LiteralPrimitive(undefined); - this.cache = {}; +/** + * Binding behavior. Indicates the bound property should be validated. + */ +class ValidateBindingBehaviorBase { + constructor(taskQueue) { + this.taskQueue = taskQueue; } - parse(message) { - if (this.cache[message] !== undefined) { - return this.cache[message]; + bind(binding, source, rulesOrController, rules) { + // identify the target element. + const target = getTargetDOMElement(binding, source); + // locate the controller. + let controller; + if (rulesOrController instanceof ValidationController) { + controller = rulesOrController; } - const parts = this.bindinqLanguage.parseInterpolation(null, message); - if (parts === null) { - return new LiteralString(message); + else { + controller = source.container.get(Optional.of(ValidationController)); + rules = rulesOrController; } - let expression = new LiteralString(parts[0]); - for (let i = 1; i < parts.length; i += 2) { - expression = new Binary('+', expression, new Binary('+', this.coalesce(parts[i]), new LiteralString(parts[i + 1]))); + 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); + // tslint:disable-next-line:no-bitwise + if (trigger & validateTrigger.change) { + binding.vbbUpdateSource = binding.updateSource; + // tslint:disable-next-line:only-arrow-functions + // tslint:disable-next-line:space-before-function-paren + binding.updateSource = function (value) { + this.vbbUpdateSource(value); + this.validationController.validateBinding(this); + }; + } + // tslint:disable-next-line:no-bitwise + 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; + // tslint:disable-next-line:only-arrow-functions + // tslint:disable-next-line:space-before-function-paren + binding.updateTarget = function (value) { + this.standardUpdateTarget(value); + this.validationController.resetBinding(this); + }; } - MessageExpressionValidator.validate(expression, message); - this.cache[message] = expression; - return expression; - } - coalesce(part) { - // part === null || part === undefined ? '' : part - return new Conditional(new Binary('||', new Binary('===', part, this.nullExpression), new Binary('===', part, this.undefinedExpression)), this.emptyStringExpression, new CallMember(part, 'toString', [])); - } -} -ValidationMessageParser.inject = [BindingLanguage]; -class MessageExpressionValidator extends ExpressionVisitor { - constructor(originalMessage) { - super(); - this.originalMessage = originalMessage; - } - static validate(expression, originalMessage) { - const visitor = new MessageExpressionValidator(originalMessage); - expression.accept(visitor); } - visitAccessScope(access) { - if (access.ancestor !== 0) { - throw new Error('$parent is not permitted in validation message expressions.'); + unbind(binding) { + // reset the binding to it's original state. + if (binding.vbbUpdateSource) { + binding.updateSource = binding.vbbUpdateSource; + binding.vbbUpdateSource = null; } - if (['displayName', 'propertyName', 'value', 'object', 'config', 'getDisplayName'].indexOf(access.name) !== -1) { - getLogger('aurelia-validation') - // tslint:disable-next-line:max-line-length - .warn(`Did you mean to use "$${access.name}" instead of "${access.name}" in this validation message template: "${this.originalMessage}"?`); + 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; } } /** - * Dictionary of validation messages. [messageKey]: messageExpression + * Binding behavior. Indicates the bound property should be validated + * when the validate trigger specified by the associated controller's + * validateTrigger property occurs. */ -const validationMessages = { - /** - * The default validation message. Used with rules that have no standard message. - */ - default: `\${$displayName} is invalid.`, - required: `\${$displayName} is required.`, - matches: `\${$displayName} is not correctly formatted.`, - email: `\${$displayName} is not a valid email.`, - minLength: `\${$displayName} must be at least \${$config.length} character\${$config.length === 1 ? '' : 's'}.`, - maxLength: `\${$displayName} cannot be longer than \${$config.length} character\${$config.length === 1 ? '' : 's'}.`, - minItems: `\${$displayName} must contain at least \${$config.count} item\${$config.count === 1 ? '' : 's'}.`, - maxItems: `\${$displayName} cannot contain more than \${$config.count} item\${$config.count === 1 ? '' : 's'}.`, - min: `\${$displayName} must be at least \${$config.constraint}.`, - max: `\${$displayName} must be at most \${$config.constraint}.`, - range: `\${$displayName} must be between or equal to \${$config.min} and \${$config.max}.`, - between: `\${$displayName} must be between but not equal to \${$config.min} and \${$config.max}.`, - equals: `\${$displayName} must be \${$config.expectedValue}.`, +let ValidateBindingBehavior = class ValidateBindingBehavior extends ValidateBindingBehaviorBase { + getValidateTrigger(controller) { + return controller.validateTrigger; + } +}; +ValidateBindingBehavior.inject = [TaskQueue]; +ValidateBindingBehavior = __decorate([ + bindingBehavior('validate') +], 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. + */ +let ValidateManuallyBindingBehavior = class ValidateManuallyBindingBehavior extends ValidateBindingBehaviorBase { + getValidateTrigger() { + return validateTrigger.manual; + } +}; +ValidateManuallyBindingBehavior.inject = [TaskQueue]; +ValidateManuallyBindingBehavior = __decorate([ + bindingBehavior('validateManually') +], ValidateManuallyBindingBehavior); +/** + * Binding behavior. Indicates the bound property should be validated + * when the associated element blurs. + */ +let ValidateOnBlurBindingBehavior = class ValidateOnBlurBindingBehavior extends ValidateBindingBehaviorBase { + getValidateTrigger() { + return validateTrigger.blur; + } +}; +ValidateOnBlurBindingBehavior.inject = [TaskQueue]; +ValidateOnBlurBindingBehavior = __decorate([ + bindingBehavior('validateOnBlur') +], 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. + */ +let ValidateOnChangeBindingBehavior = class ValidateOnChangeBindingBehavior extends ValidateBindingBehaviorBase { + getValidateTrigger() { + return validateTrigger.change; + } +}; +ValidateOnChangeBindingBehavior.inject = [TaskQueue]; +ValidateOnChangeBindingBehavior = __decorate([ + bindingBehavior('validateOnChange') +], 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. + */ +let ValidateOnChangeOrBlurBindingBehavior = class ValidateOnChangeOrBlurBindingBehavior extends ValidateBindingBehaviorBase { + getValidateTrigger() { + return validateTrigger.changeOrBlur; + } }; +ValidateOnChangeOrBlurBindingBehavior.inject = [TaskQueue]; +ValidateOnChangeOrBlurBindingBehavior = __decorate([ + bindingBehavior('validateOnChangeOrBlur') +], ValidateOnChangeOrBlurBindingBehavior); + /** - * Retrieves validation messages and property display names. + * Creates ValidationController instances. */ -class ValidationMessageProvider { - constructor(parser) { - this.parser = parser; +class ValidationControllerFactory { + constructor(container) { + this.container = container; + } + static get(container) { + return new ValidationControllerFactory(container); } /** - * Returns a message binding expression that corresponds to the key. - * @param key The message key. + * Creates a new controller instance. */ - getMessage(key) { - let message; - if (key in validationMessages) { - message = validationMessages[key]; - } - else { - message = validationMessages['default']; + create(validator) { + if (!validator) { + validator = this.container.get(Validator); } - return this.parser.parse(message); + const propertyParser = this.container.get(PropertyAccessorParser); + const config = this.container.get(GlobalValidationConfiguration); + return new ValidationController(validator, propertyParser, config); } /** - * Formulates a property display name using the property name and the configured - * displayName (if provided). - * Override this with your own custom logic. - * @param propertyName The property name. + * 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. */ - getDisplayName(propertyName, displayName) { - if (displayName !== null && displayName !== undefined) { - return (displayName instanceof Function) ? displayName() : displayName; - } - // split on upper-case letters. - const words = propertyName.toString().split(/(?=[A-Z])/).join(' '); - // capitalize first letter. - return words.charAt(0).toUpperCase() + words.slice(1); + createForCurrentScope(validator) { + const controller = this.create(validator); + this.container.registerInstance(ValidationController, controller); + return controller; } } -ValidationMessageProvider.inject = [ValidationMessageParser]; +ValidationControllerFactory['protocol:aurelia:resolver'] = true; -/** - * Validates. - * Responsible for validating objects and properties. - */ -class StandardValidator extends Validator { - constructor(messageProvider, resources) { - super(); - this.messageProvider = messageProvider; - this.lookupFunctions = resources.lookupFunctions; - this.getDisplayName = messageProvider.getDisplayName.bind(messageProvider); - } - /** - * Validates the specified property. - * @param object The object to validate. - * @param propertyName The name of the property to validate. - * @param rules Optional. If unspecified, the rules will be looked up using the metadata - * for the object created by ValidationRules....on(class/object) - */ - validateProperty(object, propertyName, rules) { - return this.validate(object, propertyName, rules || null); +let ValidationErrorsCustomAttribute = class ValidationErrorsCustomAttribute { + constructor(boundaryElement, controllerAccessor) { + this.boundaryElement = boundaryElement; + this.controllerAccessor = controllerAccessor; + this.controller = null; + this.errors = []; + this.errorsInternal = []; } - /** - * Validates all rules for specified object and it's properties. - * @param object The object to validate. - * @param rules Optional. If unspecified, the rules will be looked up using the metadata - * for the object created by ValidationRules....on(class/object) - */ - validateObject(object, rules) { - return this.validate(object, null, rules || null); + static inject() { + return [DOM.Element, Lazy.of(ValidationController)]; } - /** - * Determines whether a rule exists in a set of rules. - * @param rules The rules to search. - * @parem rule The rule to find. - */ - ruleExists(rules, rule) { - let i = rules.length; - while (i--) { - if (rules[i].indexOf(rule) !== -1) { - return true; + sort() { + this.errorsInternal.sort((a, b) => { + if (a.targets[0] === b.targets[0]) { + return 0; } - } - return false; + // tslint:disable-next-line:no-bitwise + return a.targets[0].compareDocumentPosition(b.targets[0]) & 2 ? 1 : -1; + }); } - getMessage(rule, object, value) { - const expression = rule.message || this.messageProvider.getMessage(rule.messageKey); - // tslint:disable-next-line:prefer-const - let { name: propertyName, displayName } = rule.property; - if (propertyName !== null) { - displayName = this.messageProvider.getDisplayName(propertyName, displayName); - } - const overrideContext = { - $displayName: displayName, - $propertyName: propertyName, - $value: value, - $object: object, - $config: rule.config, - // returns the name of a given property, given just the property name (irrespective of the property's displayName) - // split on capital letters, first letter ensured to be capitalized - $getDisplayName: this.getDisplayName - }; - return expression.evaluate({ bindingContext: object, overrideContext }, this.lookupFunctions); + interestingElements(elements) { + return elements.filter(e => this.boundaryElement.contains(e)); } - validateRuleSequence(object, propertyName, ruleSequence, sequence, results) { - // are we validating all properties or a single property? - const validateAllProperties = propertyName === null || propertyName === undefined; - const rules = ruleSequence[sequence]; - let allValid = true; - // validate each rule. - const promises = []; - for (let i = 0; i < rules.length; i++) { - const rule = rules[i]; - // is the rule related to the property we're validating. - // tslint:disable-next-line:triple-equals | Use loose equality for property keys - if (!validateAllProperties && rule.property.name != propertyName) { - continue; + render(instruction) { + for (const { result } of instruction.unrender) { + const index = this.errorsInternal.findIndex(x => x.error === result); + if (index !== -1) { + this.errorsInternal.splice(index, 1); } - // is this a conditional rule? is the condition met? - if (rule.when && !rule.when(object)) { + } + for (const { result, elements } of instruction.render) { + if (result.valid) { continue; } - // validate. - const value = rule.property.name === null ? object : object[rule.property.name]; - let promiseOrBoolean = rule.condition(value, object); - if (!(promiseOrBoolean instanceof Promise)) { - promiseOrBoolean = Promise.resolve(promiseOrBoolean); + const targets = this.interestingElements(elements); + if (targets.length) { + this.errorsInternal.push({ error: result, targets }); } - promises.push(promiseOrBoolean.then(valid => { - const message = valid ? null : this.getMessage(rule, object, value); - results.push(new ValidateResult(rule, object, rule.property.name, valid, message)); - allValid = allValid && valid; - return valid; - })); } - return Promise.all(promises) - .then(() => { - sequence++; - if (allValid && sequence < ruleSequence.length) { - return this.validateRuleSequence(object, propertyName, ruleSequence, sequence, results); - } - return results; - }); + this.sort(); + this.errors = this.errorsInternal; } - validate(object, propertyName, rules) { - // rules specified? - if (!rules) { - // no. attempt to locate the rules. - rules = Rules.get(object); + bind() { + if (!this.controller) { + this.controller = this.controllerAccessor(); } - // any rules? - if (!rules || rules.length === 0) { - return Promise.resolve([]); + // this will call render() with the side-effect of updating this.errors + this.controller.addRenderer(this); + } + unbind() { + if (this.controller) { + this.controller.removeRenderer(this); } - return this.validateRuleSequence(object, propertyName, rules, 0, []); } -} -StandardValidator.inject = [ValidationMessageProvider, ViewResources]; +}; +__decorate([ + bindable({ defaultBindingMode: bindingMode.oneWay }) +], ValidationErrorsCustomAttribute.prototype, "controller", void 0); +__decorate([ + bindable({ primaryProperty: true, defaultBindingMode: bindingMode.twoWay }) +], ValidationErrorsCustomAttribute.prototype, "errors", void 0); +ValidationErrorsCustomAttribute = __decorate([ + customAttribute('validation-errors') +], ValidationErrorsCustomAttribute); + +let ValidationRendererCustomAttribute = class ValidationRendererCustomAttribute { + created(view) { + this.container = view.container; + } + bind() { + this.controller = this.container.get(ValidationController); + this.renderer = this.container.get(this.value); + this.controller.addRenderer(this.renderer); + } + unbind() { + this.controller.removeRenderer(this.renderer); + this.controller = null; + this.renderer = null; + } +}; +ValidationRendererCustomAttribute = __decorate([ + customAttribute('validation-renderer') +], ValidationRendererCustomAttribute); /** * Part of the fluent rule API. Enables customizing property rules. @@ -1686,27 +1719,6 @@ class ValidationRules { } // Exports -/** - * Aurelia Validation Configuration API - */ -class AureliaValidationConfiguration { - constructor() { - this.validatorType = StandardValidator; - } - /** - * Use a custom Validator implementation. - */ - customValidator(type) { - this.validatorType = type; - } - /** - * Applies the configuration. - */ - apply(container) { - const validator = container.get(this.validatorType); - container.registerInstance(Validator, validator); - } -} /** * Configures the plugin. */ @@ -1719,7 +1731,7 @@ frameworkConfig, callback) { const propertyParser = frameworkConfig.container.get(PropertyAccessorParser); ValidationRules.initialize(messageParser, propertyParser); // configure... - const config = new AureliaValidationConfiguration(); + const config = new GlobalValidationConfiguration(); if (callback instanceof Function) { callback(config); } @@ -1730,4 +1742,4 @@ frameworkConfig, callback) { } } -export { AureliaValidationConfiguration, configure, getTargetDOMElement, getPropertyInfo, PropertyAccessorParser, getAccessorExpression, ValidateBindingBehavior, ValidateManuallyBindingBehavior, ValidateOnBlurBindingBehavior, ValidateOnChangeBindingBehavior, ValidateOnChangeOrBlurBindingBehavior, ValidateEvent, ValidateResult, validateTrigger, ValidationController, ValidationControllerFactory, ValidationErrorsCustomAttribute, ValidationRendererCustomAttribute, Validator, Rules, StandardValidator, validationMessages, ValidationMessageProvider, ValidationMessageParser, MessageExpressionValidator, FluentRuleCustomizer, FluentRules, FluentEnsure, ValidationRules }; +export { configure, GlobalValidationConfiguration, getTargetDOMElement, getPropertyInfo, PropertyAccessorParser, getAccessorExpression, ValidateBindingBehavior, ValidateManuallyBindingBehavior, ValidateOnBlurBindingBehavior, ValidateOnChangeBindingBehavior, ValidateOnChangeOrBlurBindingBehavior, ValidateEvent, ValidateResult, validateTrigger, ValidationController, ValidationControllerFactory, ValidationErrorsCustomAttribute, ValidationRendererCustomAttribute, Validator, Rules, StandardValidator, validationMessages, ValidationMessageProvider, ValidationMessageParser, MessageExpressionValidator, FluentRuleCustomizer, FluentRules, FluentEnsure, ValidationRules }; diff --git a/dist/es2017/aurelia-validation.js b/dist/es2017/aurelia-validation.js index 7a46b442..f4d9fa19 100644 --- a/dist/es2017/aurelia-validation.js +++ b/dist/es2017/aurelia-validation.js @@ -1,161 +1,9 @@ +import { LiteralString, Binary, Conditional, LiteralPrimitive, CallMember, AccessMember, AccessScope, AccessKeyed, BindingBehavior, ValueConverter, getContextFor, Parser, bindingBehavior, bindingMode } from 'aurelia-binding'; +import { BindingLanguage, ViewResources, customAttribute, bindable } from 'aurelia-templating'; +import { getLogger } from 'aurelia-logging'; import { DOM } from 'aurelia-pal'; -import { AccessMember, AccessScope, AccessKeyed, BindingBehavior, ValueConverter, getContextFor, Parser, bindingBehavior, bindingMode, LiteralString, Binary, Conditional, LiteralPrimitive, CallMember } from 'aurelia-binding'; import { Optional, Lazy } from 'aurelia-dependency-injection'; import { TaskQueue } from 'aurelia-task-queue'; -import { customAttribute, bindable, BindingLanguage, ViewResources } from 'aurelia-templating'; -import { getLogger } from 'aurelia-logging'; - -/** - * 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. - */ -function getTargetDOMElement(binding, view) { - const target = binding.target; - // DOM element - if (target instanceof Element) { - return target; - } - // custom element or custom attribute - // tslint:disable-next-line:prefer-const - for (let i = 0, ii = view.controllers.length; i < ii; i++) { - const 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}".`); -} - -function getObject(expression, objectExpression, source) { - const value = objectExpression.evaluate(source, null); - if (value === null || value === undefined || value instanceof Object) { - return value; - } - // tslint:disable-next-line:max-line-length - throw new Error(`The '${objectExpression}' part of '${expression}' evaluates to ${value} instead of an object, null or undefined.`); -} -/** - * Retrieves the object and property name for the specified expression. - * @param expression The expression - * @param source The scope - */ -function getPropertyInfo(expression, source) { - const originalExpression = expression; - while (expression instanceof BindingBehavior || expression instanceof ValueConverter) { - expression = expression.expression; - } - let object; - let propertyName; - if (expression instanceof AccessScope) { - object = getContextFor(expression.name, source, expression.ancestor); - propertyName = expression.name; - } - else if (expression instanceof AccessMember) { - object = getObject(originalExpression, expression.object, source); - propertyName = expression.name; - } - else if (expression instanceof AccessKeyed) { - object = getObject(originalExpression, expression.object, source); - propertyName = expression.key.evaluate(source); - } - else { - throw new Error(`Expression '${originalExpression}' is not compatible with the validate binding-behavior.`); - } - if (object === null || object === undefined) { - return null; - } - return { object, propertyName }; -} - -function isString(value) { - return Object.prototype.toString.call(value) === '[object String]'; -} -function isNumber(value) { - return Object.prototype.toString.call(value) === '[object Number]'; -} - -class PropertyAccessorParser { - constructor(parser) { - this.parser = parser; - } - parse(property) { - if (isString(property) || isNumber(property)) { - return property; - } - const accessorText = getAccessorExpression(property.toString()); - const accessor = this.parser.parse(accessorText); - if (accessor instanceof AccessScope - || accessor instanceof AccessMember && accessor.object instanceof AccessScope) { - return accessor.name; - } - throw new Error(`Invalid property expression: "${accessor}"`); - } -} -PropertyAccessorParser.inject = [Parser]; -function getAccessorExpression(fn) { - /* tslint:disable:max-line-length */ - const classic = /^function\s*\([$_\w\d]+\)\s*\{(?:\s*"use strict";)?\s*(?:[$_\w\d.['"\]+;]+)?\s*return\s+[$_\w\d]+\.([$_\w\d]+)\s*;?\s*\}$/; - /* tslint:enable:max-line-length */ - const arrow = /^\(?[$_\w\d]+\)?\s*=>\s*[$_\w\d]+\.([$_\w\d]+)$/; - const match = classic.exec(fn) || arrow.exec(fn); - if (match === null) { - throw new Error(`Unable to parse accessor function:\n${fn}`); - } - return match[1]; -} - -/*! ***************************************************************************** -Copyright (c) Microsoft Corporation. All rights reserved. -Licensed under the Apache License, Version 2.0 (the "License"); you may not use -this file except in compliance with the License. You may obtain a copy of the -License at http://www.apache.org/licenses/LICENSE-2.0 - -THIS CODE IS PROVIDED ON AN *AS IS* BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY -KIND, EITHER EXPRESS OR IMPLIED, INCLUDING WITHOUT LIMITATION ANY IMPLIED -WARRANTIES OR CONDITIONS OF TITLE, FITNESS FOR A PARTICULAR PURPOSE, -MERCHANTABLITY OR NON-INFRINGEMENT. - -See the Apache Version 2.0 License for specific language governing permissions -and limitations under the License. -***************************************************************************** */ - -function __decorate(decorators, target, key, desc) { - var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d; - if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc); - else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r; - return c > 3 && r && Object.defineProperty(target, key, r), r; -} - -/** - * Validation triggers. - */ -var validateTrigger; -(function (validateTrigger) { - /** - * Manual validation. Use the controller's `validate()` and `reset()` methods - * to validate all bindings. - */ - validateTrigger[validateTrigger["manual"] = 0] = "manual"; - /** - * Validate the binding when the binding's target element fires a DOM "blur" event. - */ - validateTrigger[validateTrigger["blur"] = 1] = "blur"; - /** - * Validate the binding when it updates the model due to a change in the view. - */ - validateTrigger[validateTrigger["change"] = 2] = "change"; - /** - * 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. - */ - validateTrigger[validateTrigger["changeOrBlur"] = 3] = "changeOrBlur"; -})(validateTrigger || (validateTrigger = {})); /** * Validates objects and properties. @@ -187,1024 +35,1209 @@ class ValidateResult { } ValidateResult.nextId = 0; -class ValidateEvent { - constructor( - /** - * The type of validate event. Either "validate" or "reset". - */ - type, - /** - * The controller's current array of errors. For an array containing both - * failed rules and passed rules, use the "results" property. - */ - errors, +/** + * Sets, unsets and retrieves rules on an object or constructor function. + */ +class Rules { /** - * The controller's current array of validate results. This - * includes both passed rules and failed rules. For an array of only failed rules, - * use the "errors" property. + * Applies the rules to a target. */ - results, + static set(target, rules) { + if (target instanceof Function) { + target = target.prototype; + } + Object.defineProperty(target, Rules.key, { enumerable: false, configurable: false, writable: true, value: rules }); + } /** - * The instruction passed to the "validate" or "reset" event. Will be null when - * the controller's validate/reset method was called with no instruction argument. + * Removes rules from a target. */ - instruction, + static unset(target) { + if (target instanceof Function) { + target = target.prototype; + } + target[Rules.key] = null; + } /** - * In events with type === "validate", this property will contain the result - * of validating the instruction (see "instruction" property). Use the controllerValidateResult - * to access the validate results specific to the call to "validate" - * (as opposed to using the "results" and "errors" properties to access the controller's entire - * set of results/errors). + * Retrieves the target's rules. */ - controllerValidateResult) { - this.type = type; - this.errors = errors; - this.results = results; - this.instruction = instruction; - this.controllerValidateResult = controllerValidateResult; + static get(target) { + return target[Rules.key] || null; } -} - +} /** - * Orchestrates validation. - * Manages a set of bindings, renderers and objects. - * Exposes the current list of validation results for binding purposes. + * The name of the property that stores the rules. */ -class ValidationController { - constructor(validator, propertyParser) { - this.validator = validator; - this.propertyParser = propertyParser; - // Registered bindings (via the validate binding behavior) - this.bindings = new Map(); - // Renderers that have been added to the controller instance. - this.renderers = []; - /** - * Validation results that have been rendered by the controller. - */ - this.results = []; - /** - * Validation errors that have been rendered by the controller. - */ - this.errors = []; - /** - * Whether the controller is currently validating. - */ - this.validating = false; - // Elements related to validation results that have been rendered. - this.elements = new Map(); - // Objects that have been added to the controller instance (entity-style validation). - this.objects = new Map(); - /** - * The trigger that will invoke automatic validation of a property used in a binding. - */ - this.validateTrigger = validateTrigger.blur; - // Promise that resolves when validation has completed. - this.finishValidating = Promise.resolve(); - this.eventCallbacks = []; +Rules.key = '__rules__'; + +// tslint:disable:no-empty +class ExpressionVisitor { + visitChain(chain) { + this.visitArgs(chain.expressions); } - /** - * Subscribe to controller validate and reset events. These events occur when the - * controller's "validate"" and "reset" methods are called. - * @param callback The callback to be invoked when the controller validates or resets. - */ - subscribe(callback) { - this.eventCallbacks.push(callback); - return { - dispose: () => { - const index = this.eventCallbacks.indexOf(callback); - if (index === -1) { - return; - } - this.eventCallbacks.splice(index, 1); - } - }; + visitBindingBehavior(behavior) { + behavior.expression.accept(this); + this.visitArgs(behavior.args); } - /** - * Adds an object to the set of objects that should be validated when validate is called. - * @param object The object. - * @param rules Optional. The rules. If rules aren't supplied the Validator implementation will lookup the rules. - */ - addObject(object, rules) { - this.objects.set(object, rules); + visitValueConverter(converter) { + converter.expression.accept(this); + this.visitArgs(converter.args); + } + visitAssign(assign) { + assign.target.accept(this); + assign.value.accept(this); + } + visitConditional(conditional) { + conditional.condition.accept(this); + conditional.yes.accept(this); + conditional.no.accept(this); + } + visitAccessThis(access) { + access.ancestor = access.ancestor; + } + visitAccessScope(access) { + access.name = access.name; + } + visitAccessMember(access) { + access.object.accept(this); + } + visitAccessKeyed(access) { + access.object.accept(this); + access.key.accept(this); + } + visitCallScope(call) { + this.visitArgs(call.args); + } + visitCallFunction(call) { + call.func.accept(this); + this.visitArgs(call.args); + } + visitCallMember(call) { + call.object.accept(this); + this.visitArgs(call.args); + } + visitPrefix(prefix) { + prefix.expression.accept(this); + } + visitBinary(binary) { + binary.left.accept(this); + binary.right.accept(this); + } + visitLiteralPrimitive(literal) { + literal.value = literal.value; + } + visitLiteralArray(literal) { + this.visitArgs(literal.elements); } + visitLiteralObject(literal) { + this.visitArgs(literal.values); + } + visitLiteralString(literal) { + literal.value = literal.value; + } + visitArgs(args) { + for (let i = 0; i < args.length; i++) { + args[i].accept(this); + } + } +} + +class ValidationMessageParser { + constructor(bindinqLanguage) { + this.bindinqLanguage = bindinqLanguage; + this.emptyStringExpression = new LiteralString(''); + this.nullExpression = new LiteralPrimitive(null); + this.undefinedExpression = new LiteralPrimitive(undefined); + this.cache = {}; + } + parse(message) { + if (this.cache[message] !== undefined) { + return this.cache[message]; + } + const parts = this.bindinqLanguage.parseInterpolation(null, message); + if (parts === null) { + return new LiteralString(message); + } + let expression = new LiteralString(parts[0]); + for (let i = 1; i < parts.length; i += 2) { + expression = new Binary('+', expression, new Binary('+', this.coalesce(parts[i]), new LiteralString(parts[i + 1]))); + } + MessageExpressionValidator.validate(expression, message); + this.cache[message] = expression; + return expression; + } + coalesce(part) { + // part === null || part === undefined ? '' : part + return new Conditional(new Binary('||', new Binary('===', part, this.nullExpression), new Binary('===', part, this.undefinedExpression)), this.emptyStringExpression, new CallMember(part, 'toString', [])); + } +} +ValidationMessageParser.inject = [BindingLanguage]; +class MessageExpressionValidator extends ExpressionVisitor { + constructor(originalMessage) { + super(); + this.originalMessage = originalMessage; + } + static validate(expression, originalMessage) { + const visitor = new MessageExpressionValidator(originalMessage); + expression.accept(visitor); + } + visitAccessScope(access) { + if (access.ancestor !== 0) { + throw new Error('$parent is not permitted in validation message expressions.'); + } + if (['displayName', 'propertyName', 'value', 'object', 'config', 'getDisplayName'].indexOf(access.name) !== -1) { + getLogger('aurelia-validation') + // tslint:disable-next-line:max-line-length + .warn(`Did you mean to use "$${access.name}" instead of "${access.name}" in this validation message template: "${this.originalMessage}"?`); + } + } +} + +/** + * Dictionary of validation messages. [messageKey]: messageExpression + */ +const validationMessages = { /** - * Removes an object from the set of objects that should be validated when validate is called. - * @param object The object. + * The default validation message. Used with rules that have no standard message. */ - removeObject(object) { - this.objects.delete(object); - this.processResultDelta('reset', this.results.filter(result => result.object === object), []); + default: `\${$displayName} is invalid.`, + required: `\${$displayName} is required.`, + matches: `\${$displayName} is not correctly formatted.`, + email: `\${$displayName} is not a valid email.`, + minLength: `\${$displayName} must be at least \${$config.length} character\${$config.length === 1 ? '' : 's'}.`, + maxLength: `\${$displayName} cannot be longer than \${$config.length} character\${$config.length === 1 ? '' : 's'}.`, + minItems: `\${$displayName} must contain at least \${$config.count} item\${$config.count === 1 ? '' : 's'}.`, + maxItems: `\${$displayName} cannot contain more than \${$config.count} item\${$config.count === 1 ? '' : 's'}.`, + min: `\${$displayName} must be at least \${$config.constraint}.`, + max: `\${$displayName} must be at most \${$config.constraint}.`, + range: `\${$displayName} must be between or equal to \${$config.min} and \${$config.max}.`, + between: `\${$displayName} must be between but not equal to \${$config.min} and \${$config.max}.`, + equals: `\${$displayName} must be \${$config.expectedValue}.`, +}; +/** + * Retrieves validation messages and property display names. + */ +class ValidationMessageProvider { + constructor(parser) { + this.parser = parser; } /** - * Adds and renders an error. + * Returns a message binding expression that corresponds to the key. + * @param key The message key. */ - addError(message, object, propertyName = null) { - let resolvedPropertyName; - if (propertyName === null) { - resolvedPropertyName = propertyName; + getMessage(key) { + let message; + if (key in validationMessages) { + message = validationMessages[key]; } else { - resolvedPropertyName = this.propertyParser.parse(propertyName); + message = validationMessages['default']; } - const result = new ValidateResult({ __manuallyAdded__: true }, object, resolvedPropertyName, false, message); - this.processResultDelta('validate', [], [result]); - return result; + return this.parser.parse(message); } /** - * Removes and unrenders an error. + * Formulates a property display name using the property name and the configured + * displayName (if provided). + * Override this with your own custom logic. + * @param propertyName The property name. */ - removeError(result) { - if (this.results.indexOf(result) !== -1) { - this.processResultDelta('reset', [result], []); + getDisplayName(propertyName, displayName) { + if (displayName !== null && displayName !== undefined) { + return (displayName instanceof Function) ? displayName() : displayName; } + // split on upper-case letters. + const words = propertyName.toString().split(/(?=[A-Z])/).join(' '); + // capitalize first letter. + return words.charAt(0).toUpperCase() + words.slice(1); } - /** - * Adds a renderer. - * @param renderer The renderer. - */ - addRenderer(renderer) { - this.renderers.push(renderer); - renderer.render({ - kind: 'validate', - render: this.results.map(result => ({ result, elements: this.elements.get(result) })), - unrender: [] - }); - } - /** - * Removes a renderer. - * @param renderer The renderer. - */ - removeRenderer(renderer) { - this.renderers.splice(this.renderers.indexOf(renderer), 1); - renderer.render({ - kind: 'reset', - render: [], - unrender: this.results.map(result => ({ result, elements: this.elements.get(result) })) - }); +} +ValidationMessageProvider.inject = [ValidationMessageParser]; + +/** + * Validates. + * Responsible for validating objects and properties. + */ +class StandardValidator extends Validator { + constructor(messageProvider, resources) { + super(); + this.messageProvider = messageProvider; + this.lookupFunctions = resources.lookupFunctions; + this.getDisplayName = messageProvider.getDisplayName.bind(messageProvider); } /** - * Registers a binding with the controller. - * @param binding The binding instance. - * @param target The DOM element. - * @param rules (optional) rules associated with the binding. Validator implementation specific. + * Validates the specified property. + * @param object The object to validate. + * @param propertyName The name of the property to validate. + * @param rules Optional. If unspecified, the rules will be looked up using the metadata + * for the object created by ValidationRules....on(class/object) */ - registerBinding(binding, target, rules) { - this.bindings.set(binding, { target, rules, propertyInfo: null }); + validateProperty(object, propertyName, rules) { + return this.validate(object, propertyName, rules || null); } /** - * Unregisters a binding with the controller. - * @param binding The binding instance. + * Validates all rules for specified object and it's properties. + * @param object The object to validate. + * @param rules Optional. If unspecified, the rules will be looked up using the metadata + * for the object created by ValidationRules....on(class/object) */ - unregisterBinding(binding) { - this.resetBinding(binding); - this.bindings.delete(binding); + validateObject(object, rules) { + return this.validate(object, null, rules || null); } /** - * Interprets the instruction and returns a predicate that will identify - * relevant results in the list of rendered validation results. + * Determines whether a rule exists in a set of rules. + * @param rules The rules to search. + * @parem rule The rule to find. */ - getInstructionPredicate(instruction) { - if (instruction) { - const { object, propertyName, rules } = instruction; - let predicate; - if (instruction.propertyName) { - predicate = x => x.object === object && x.propertyName === propertyName; - } - else { - predicate = x => x.object === object; - } - if (rules) { - return x => predicate(x) && this.validator.ruleExists(rules, x.rule); + ruleExists(rules, rule) { + let i = rules.length; + while (i--) { + if (rules[i].indexOf(rule) !== -1) { + return true; } - return predicate; } - else { - return () => true; + return false; + } + getMessage(rule, object, value) { + const expression = rule.message || this.messageProvider.getMessage(rule.messageKey); + // tslint:disable-next-line:prefer-const + let { name: propertyName, displayName } = rule.property; + if (propertyName !== null) { + displayName = this.messageProvider.getDisplayName(propertyName, displayName); } + const overrideContext = { + $displayName: displayName, + $propertyName: propertyName, + $value: value, + $object: object, + $config: rule.config, + // returns the name of a given property, given just the property name (irrespective of the property's displayName) + // split on capital letters, first letter ensured to be capitalized + $getDisplayName: this.getDisplayName + }; + return expression.evaluate({ bindingContext: object, overrideContext }, this.lookupFunctions); } - /** - * Validates and renders results. - * @param instruction Optional. Instructions on what to validate. If undefined, all - * objects and bindings will be validated. - */ - validate(instruction) { - // Get a function that will process the validation instruction. - let execute; - if (instruction) { - // tslint:disable-next-line:prefer-const - let { object, propertyName, rules } = instruction; - // if rules were not specified, check the object map. - rules = rules || this.objects.get(object); - // property specified? - if (instruction.propertyName === undefined) { - // validate the specified object. - execute = () => this.validator.validateObject(object, rules); + validateRuleSequence(object, propertyName, ruleSequence, sequence, results) { + // are we validating all properties or a single property? + const validateAllProperties = propertyName === null || propertyName === undefined; + const rules = ruleSequence[sequence]; + let allValid = true; + // validate each rule. + const promises = []; + for (let i = 0; i < rules.length; i++) { + const rule = rules[i]; + // is the rule related to the property we're validating. + // tslint:disable-next-line:triple-equals | Use loose equality for property keys + if (!validateAllProperties && rule.property.name != propertyName) { + continue; } - else { - // validate the specified property. - execute = () => this.validator.validateProperty(object, propertyName, rules); + // is this a conditional rule? is the condition met? + if (rule.when && !rule.when(object)) { + continue; } + // validate. + const value = rule.property.name === null ? object : object[rule.property.name]; + let promiseOrBoolean = rule.condition(value, object); + if (!(promiseOrBoolean instanceof Promise)) { + promiseOrBoolean = Promise.resolve(promiseOrBoolean); + } + promises.push(promiseOrBoolean.then(valid => { + const message = valid ? null : this.getMessage(rule, object, value); + results.push(new ValidateResult(rule, object, rule.property.name, valid, message)); + allValid = allValid && valid; + return valid; + })); } - else { - // validate all objects and bindings. - execute = () => { - const promises = []; - for (const [object, rules] of Array.from(this.objects)) { - promises.push(this.validator.validateObject(object, rules)); - } - for (const [binding, { rules }] of Array.from(this.bindings)) { - const propertyInfo = getPropertyInfo(binding.sourceExpression, binding.source); - if (!propertyInfo || this.objects.has(propertyInfo.object)) { - continue; - } - promises.push(this.validator.validateProperty(propertyInfo.object, propertyInfo.propertyName, rules)); - } - return Promise.all(promises).then(resultSets => resultSets.reduce((a, b) => a.concat(b), [])); - }; - } - // Wait for any existing validation to finish, execute the instruction, render the results. - this.validating = true; - const returnPromise = this.finishValidating - .then(execute) - .then((newResults) => { - const predicate = this.getInstructionPredicate(instruction); - const oldResults = this.results.filter(predicate); - this.processResultDelta('validate', oldResults, newResults); - if (returnPromise === this.finishValidating) { - this.validating = false; + return Promise.all(promises) + .then(() => { + sequence++; + if (allValid && sequence < ruleSequence.length) { + return this.validateRuleSequence(object, propertyName, ruleSequence, sequence, results); } - const result = { - instruction, - valid: newResults.find(x => !x.valid) === undefined, - results: newResults - }; - this.invokeCallbacks(instruction, result); - return result; - }) - .catch(exception => { - // recover, to enable subsequent calls to validate() - this.validating = false; - this.finishValidating = Promise.resolve(); - return Promise.reject(exception); + return results; }); - this.finishValidating = returnPromise; - return returnPromise; } + validate(object, propertyName, rules) { + // rules specified? + if (!rules) { + // no. attempt to locate the rules. + rules = Rules.get(object); + } + // any rules? + if (!rules || rules.length === 0) { + return Promise.resolve([]); + } + return this.validateRuleSequence(object, propertyName, rules, 0, []); + } +} +StandardValidator.inject = [ValidationMessageProvider, ViewResources]; + +/** + * Validation triggers. + */ +var validateTrigger; +(function (validateTrigger) { /** - * Resets any rendered validation results (unrenders). - * @param instruction Optional. Instructions on what to reset. If unspecified all rendered results - * will be unrendered. + * Manual validation. Use the controller's `validate()` and `reset()` methods + * to validate all bindings. */ - reset(instruction) { - const predicate = this.getInstructionPredicate(instruction); - const oldResults = this.results.filter(predicate); - this.processResultDelta('reset', oldResults, []); - this.invokeCallbacks(instruction, null); + validateTrigger[validateTrigger["manual"] = 0] = "manual"; + /** + * Validate the binding when the binding's target element fires a DOM "blur" event. + */ + validateTrigger[validateTrigger["blur"] = 1] = "blur"; + /** + * Validate the binding when it updates the model due to a change in the view. + */ + validateTrigger[validateTrigger["change"] = 2] = "change"; + /** + * 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. + */ + validateTrigger[validateTrigger["changeOrBlur"] = 3] = "changeOrBlur"; +})(validateTrigger || (validateTrigger = {})); + +/** + * Aurelia Validation Configuration API + */ +class GlobalValidationConfiguration { + constructor() { + this.validatorType = StandardValidator; + this.validationTrigger = GlobalValidationConfiguration.DEFAULT_VALIDATION_TRIGGER; } /** - * Gets the elements associated with an object and propertyName (if any). + * Use a custom Validator implementation. */ - getAssociatedElements({ object, propertyName }) { - const elements = []; - for (const [binding, { target }] of Array.from(this.bindings)) { - const propertyInfo = getPropertyInfo(binding.sourceExpression, binding.source); - if (propertyInfo && propertyInfo.object === object && propertyInfo.propertyName === propertyName) { - elements.push(target); + customValidator(type) { + this.validatorType = type; + return this; + } + defaultValidationTrigger(trigger) { + this.validationTrigger = trigger; + return this; + } + getDefaultValidationTrigger() { + return this.validationTrigger; + } + /** + * Applies the configuration. + */ + apply(container) { + const validator = container.get(this.validatorType); + container.registerInstance(Validator, validator); + container.registerInstance(GlobalValidationConfiguration, this); + } +} +GlobalValidationConfiguration.DEFAULT_VALIDATION_TRIGGER = validateTrigger.blur; + +/** + * 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. + */ +function getTargetDOMElement(binding, view) { + const target = binding.target; + // DOM element + if (target instanceof Element) { + return target; + } + // custom element or custom attribute + // tslint:disable-next-line:prefer-const + for (let i = 0, ii = view.controllers.length; i < ii; i++) { + const 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}".`); } - return elements; } - processResultDelta(kind, oldResults, newResults) { - // prepare the instruction. - const instruction = { - kind, - render: [], - unrender: [] - }; - // create a shallow copy of newResults so we can mutate it without causing side-effects. - newResults = newResults.slice(0); - // create unrender instructions from the old results. - for (const oldResult of oldResults) { - // get the elements associated with the old result. - const elements = this.elements.get(oldResult); - // remove the old result from the element map. - this.elements.delete(oldResult); - // create the unrender instruction. - instruction.unrender.push({ result: oldResult, elements }); - // determine if there's a corresponding new result for the old result we are unrendering. - const newResultIndex = newResults.findIndex(x => x.rule === oldResult.rule && x.object === oldResult.object && x.propertyName === oldResult.propertyName); - if (newResultIndex === -1) { - // no corresponding new result... simple remove. - this.results.splice(this.results.indexOf(oldResult), 1); - if (!oldResult.valid) { - this.errors.splice(this.errors.indexOf(oldResult), 1); - } - } - else { - // there is a corresponding new result... - const newResult = newResults.splice(newResultIndex, 1)[0]; - // get the elements that are associated with the new result. - const elements = this.getAssociatedElements(newResult); - this.elements.set(newResult, elements); - // create a render instruction for the new result. - instruction.render.push({ result: newResult, elements }); - // do an in-place replacement of the old result with the new result. - // this ensures any repeats bound to this.results will not thrash. - this.results.splice(this.results.indexOf(oldResult), 1, newResult); - if (!oldResult.valid && newResult.valid) { - this.errors.splice(this.errors.indexOf(oldResult), 1); - } - else if (!oldResult.valid && !newResult.valid) { - this.errors.splice(this.errors.indexOf(oldResult), 1, newResult); - } - else if (!newResult.valid) { - this.errors.push(newResult); - } - } - } - // create render instructions from the remaining new results. - for (const result of newResults) { - const elements = this.getAssociatedElements(result); - instruction.render.push({ result, elements }); - this.elements.set(result, elements); - this.results.push(result); - if (!result.valid) { - this.errors.push(result); - } - } - // render. - for (const renderer of this.renderers) { - renderer.render(instruction); - } + throw new Error(`Unable to locate target element for "${binding.sourceExpression}".`); +} + +function getObject(expression, objectExpression, source) { + const value = objectExpression.evaluate(source, null); + if (value === null || value === undefined || value instanceof Object) { + return value; } - /** - * Validates the property associated with a binding. - */ - validateBinding(binding) { - if (!binding.isBound) { - return; - } - const propertyInfo = getPropertyInfo(binding.sourceExpression, binding.source); - let rules; - const registeredBinding = this.bindings.get(binding); - if (registeredBinding) { - rules = registeredBinding.rules; - registeredBinding.propertyInfo = propertyInfo; - } - if (!propertyInfo) { - return; - } - const { object, propertyName } = propertyInfo; - this.validate({ object, propertyName, rules }); + // tslint:disable-next-line:max-line-length + throw new Error(`The '${objectExpression}' part of '${expression}' evaluates to ${value} instead of an object, null or undefined.`); +} +/** + * Retrieves the object and property name for the specified expression. + * @param expression The expression + * @param source The scope + */ +function getPropertyInfo(expression, source) { + const originalExpression = expression; + while (expression instanceof BindingBehavior || expression instanceof ValueConverter) { + expression = expression.expression; } - /** - * Resets the results for a property associated with a binding. - */ - resetBinding(binding) { - const registeredBinding = this.bindings.get(binding); - let propertyInfo = getPropertyInfo(binding.sourceExpression, binding.source); - if (!propertyInfo && registeredBinding) { - propertyInfo = registeredBinding.propertyInfo; - } - if (registeredBinding) { - registeredBinding.propertyInfo = null; - } - if (!propertyInfo) { - return; - } - const { object, propertyName } = propertyInfo; - this.reset({ object, propertyName }); + let object; + let propertyName; + if (expression instanceof AccessScope) { + object = getContextFor(expression.name, source, expression.ancestor); + propertyName = expression.name; } - /** - * Changes the controller's validateTrigger. - * @param newTrigger The new validateTrigger - */ - changeTrigger(newTrigger) { - this.validateTrigger = newTrigger; - const bindings = Array.from(this.bindings.keys()); - for (const binding of bindings) { - const source = binding.source; - binding.unbind(); - binding.bind(source); - } + else if (expression instanceof AccessMember) { + object = getObject(originalExpression, expression.object, source); + propertyName = expression.name; } - /** - * Revalidates the controller's current set of errors. - */ - revalidateErrors() { - for (const { object, propertyName, rule } of this.errors) { - if (rule.__manuallyAdded__) { - continue; - } - const rules = [[rule]]; - this.validate({ object, propertyName, rules }); - } + else if (expression instanceof AccessKeyed) { + object = getObject(originalExpression, expression.object, source); + propertyName = expression.key.evaluate(source); } - invokeCallbacks(instruction, result) { - if (this.eventCallbacks.length === 0) { - return; - } - const event = new ValidateEvent(result ? 'validate' : 'reset', this.errors, this.results, instruction || null, result); - for (let i = 0; i < this.eventCallbacks.length; i++) { - this.eventCallbacks[i](event); - } + else { + throw new Error(`Expression '${originalExpression}' is not compatible with the validate binding-behavior.`); } + if (object === null || object === undefined) { + return null; + } + return { object, propertyName }; +} + +function isString(value) { + return Object.prototype.toString.call(value) === '[object String]'; } -ValidationController.inject = [Validator, PropertyAccessorParser]; +function isNumber(value) { + return Object.prototype.toString.call(value) === '[object Number]'; +} -/** - * Binding behavior. Indicates the bound property should be validated. - */ -class ValidateBindingBehaviorBase { - constructor(taskQueue) { - this.taskQueue = taskQueue; +class PropertyAccessorParser { + constructor(parser) { + this.parser = parser; } - bind(binding, source, rulesOrController, rules) { - // identify the target element. - const target = getTargetDOMElement(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); - // tslint:disable-next-line:no-bitwise - if (trigger & validateTrigger.change) { - binding.vbbUpdateSource = binding.updateSource; - // tslint:disable-next-line:only-arrow-functions - // tslint:disable-next-line:space-before-function-paren - binding.updateSource = function (value) { - this.vbbUpdateSource(value); - this.validationController.validateBinding(this); - }; - } - // tslint:disable-next-line:no-bitwise - if (trigger & validateTrigger.blur) { - binding.validateBlurHandler = () => { - this.taskQueue.queueMicroTask(() => controller.validateBinding(binding)); - }; - binding.validateTarget = target; - target.addEventListener('blur', binding.validateBlurHandler); + parse(property) { + if (isString(property) || isNumber(property)) { + return property; } - if (trigger !== validateTrigger.manual) { - binding.standardUpdateTarget = binding.updateTarget; - // tslint:disable-next-line:only-arrow-functions - // tslint:disable-next-line:space-before-function-paren - binding.updateTarget = function (value) { - this.standardUpdateTarget(value); - this.validationController.resetBinding(this); - }; + const accessorText = getAccessorExpression(property.toString()); + const accessor = this.parser.parse(accessorText); + if (accessor instanceof AccessScope + || accessor instanceof AccessMember && accessor.object instanceof AccessScope) { + return accessor.name; } + throw new Error(`Invalid property expression: "${accessor}"`); } - unbind(binding) { - // reset the binding to it's original state. - if (binding.vbbUpdateSource) { - binding.updateSource = binding.vbbUpdateSource; - binding.vbbUpdateSource = 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; +} +PropertyAccessorParser.inject = [Parser]; +function getAccessorExpression(fn) { + /* tslint:disable:max-line-length */ + const classic = /^function\s*\([$_\w\d]+\)\s*\{(?:\s*"use strict";)?\s*(?:[$_\w\d.['"\]+;]+)?\s*return\s+[$_\w\d]+\.([$_\w\d]+)\s*;?\s*\}$/; + /* tslint:enable:max-line-length */ + const arrow = /^\(?[$_\w\d]+\)?\s*=>\s*[$_\w\d]+\.([$_\w\d]+)$/; + const match = classic.exec(fn) || arrow.exec(fn); + if (match === null) { + throw new Error(`Unable to parse accessor function:\n${fn}`); } + return match[1]; } -/** - * Binding behavior. Indicates the bound property should be validated - * when the validate trigger specified by the associated controller's - * validateTrigger property occurs. - */ -let ValidateBindingBehavior = class ValidateBindingBehavior extends ValidateBindingBehaviorBase { - getValidateTrigger(controller) { - return controller.validateTrigger; - } -}; -ValidateBindingBehavior.inject = [TaskQueue]; -ValidateBindingBehavior = __decorate([ - bindingBehavior('validate') -], 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. - */ -let ValidateManuallyBindingBehavior = class ValidateManuallyBindingBehavior extends ValidateBindingBehaviorBase { - getValidateTrigger() { - return validateTrigger.manual; - } -}; -ValidateManuallyBindingBehavior.inject = [TaskQueue]; -ValidateManuallyBindingBehavior = __decorate([ - bindingBehavior('validateManually') -], ValidateManuallyBindingBehavior); -/** - * Binding behavior. Indicates the bound property should be validated - * when the associated element blurs. - */ -let ValidateOnBlurBindingBehavior = class ValidateOnBlurBindingBehavior extends ValidateBindingBehaviorBase { - getValidateTrigger() { - return validateTrigger.blur; +/*! ***************************************************************************** +Copyright (c) Microsoft Corporation. All rights reserved. +Licensed under the Apache License, Version 2.0 (the "License"); you may not use +this file except in compliance with the License. You may obtain a copy of the +License at http://www.apache.org/licenses/LICENSE-2.0 + +THIS CODE IS PROVIDED ON AN *AS IS* BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +KIND, EITHER EXPRESS OR IMPLIED, INCLUDING WITHOUT LIMITATION ANY IMPLIED +WARRANTIES OR CONDITIONS OF TITLE, FITNESS FOR A PARTICULAR PURPOSE, +MERCHANTABLITY OR NON-INFRINGEMENT. + +See the Apache Version 2.0 License for specific language governing permissions +and limitations under the License. +***************************************************************************** */ + +function __decorate(decorators, target, key, desc) { + var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d; + if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc); + else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r; + return c > 3 && r && Object.defineProperty(target, key, r), r; +} + +class ValidateEvent { + constructor( + /** + * The type of validate event. Either "validate" or "reset". + */ + type, + /** + * The controller's current array of errors. For an array containing both + * failed rules and passed rules, use the "results" property. + */ + errors, + /** + * The controller's current array of validate results. This + * includes both passed rules and failed rules. For an array of only failed rules, + * use the "errors" property. + */ + results, + /** + * The instruction passed to the "validate" or "reset" event. Will be null when + * the controller's validate/reset method was called with no instruction argument. + */ + instruction, + /** + * In events with type === "validate", this property will contain the result + * of validating the instruction (see "instruction" property). Use the controllerValidateResult + * to access the validate results specific to the call to "validate" + * (as opposed to using the "results" and "errors" properties to access the controller's entire + * set of results/errors). + */ + controllerValidateResult) { + this.type = type; + this.errors = errors; + this.results = results; + this.instruction = instruction; + this.controllerValidateResult = controllerValidateResult; } -}; -ValidateOnBlurBindingBehavior.inject = [TaskQueue]; -ValidateOnBlurBindingBehavior = __decorate([ - bindingBehavior('validateOnBlur') -], 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. + * Orchestrates validation. + * Manages a set of bindings, renderers and objects. + * Exposes the current list of validation results for binding purposes. */ -let ValidateOnChangeBindingBehavior = class ValidateOnChangeBindingBehavior extends ValidateBindingBehaviorBase { - getValidateTrigger() { - return validateTrigger.change; +class ValidationController { + constructor(validator, propertyParser, config) { + this.validator = validator; + this.propertyParser = propertyParser; + // Registered bindings (via the validate binding behavior) + this.bindings = new Map(); + // Renderers that have been added to the controller instance. + this.renderers = []; + /** + * Validation results that have been rendered by the controller. + */ + this.results = []; + /** + * Validation errors that have been rendered by the controller. + */ + this.errors = []; + /** + * Whether the controller is currently validating. + */ + this.validating = false; + // Elements related to validation results that have been rendered. + this.elements = new Map(); + // Objects that have been added to the controller instance (entity-style validation). + this.objects = new Map(); + // Promise that resolves when validation has completed. + this.finishValidating = Promise.resolve(); + this.eventCallbacks = []; + this.validateTrigger = config instanceof GlobalValidationConfiguration + ? config.getDefaultValidationTrigger() + : GlobalValidationConfiguration.DEFAULT_VALIDATION_TRIGGER; } -}; -ValidateOnChangeBindingBehavior.inject = [TaskQueue]; -ValidateOnChangeBindingBehavior = __decorate([ - bindingBehavior('validateOnChange') -], 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. - */ -let ValidateOnChangeOrBlurBindingBehavior = class ValidateOnChangeOrBlurBindingBehavior extends ValidateBindingBehaviorBase { - getValidateTrigger() { - return validateTrigger.changeOrBlur; + /** + * Subscribe to controller validate and reset events. These events occur when the + * controller's "validate"" and "reset" methods are called. + * @param callback The callback to be invoked when the controller validates or resets. + */ + subscribe(callback) { + this.eventCallbacks.push(callback); + return { + dispose: () => { + const index = this.eventCallbacks.indexOf(callback); + if (index === -1) { + return; + } + this.eventCallbacks.splice(index, 1); + } + }; } -}; -ValidateOnChangeOrBlurBindingBehavior.inject = [TaskQueue]; -ValidateOnChangeOrBlurBindingBehavior = __decorate([ - bindingBehavior('validateOnChangeOrBlur') -], ValidateOnChangeOrBlurBindingBehavior); - -/** - * Creates ValidationController instances. - */ -class ValidationControllerFactory { - constructor(container) { - this.container = container; + /** + * Adds an object to the set of objects that should be validated when validate is called. + * @param object The object. + * @param rules Optional. The rules. If rules aren't supplied the Validator implementation will lookup the rules. + */ + addObject(object, rules) { + this.objects.set(object, rules); } - static get(container) { - return new ValidationControllerFactory(container); + /** + * Removes an object from the set of objects that should be validated when validate is called. + * @param object The object. + */ + removeObject(object) { + this.objects.delete(object); + this.processResultDelta('reset', this.results.filter(result => result.object === object), []); } /** - * Creates a new controller instance. + * Adds and renders an error. */ - create(validator) { - if (!validator) { - validator = this.container.get(Validator); + addError(message, object, propertyName = null) { + let resolvedPropertyName; + if (propertyName === null) { + resolvedPropertyName = propertyName; } - const propertyParser = this.container.get(PropertyAccessorParser); - return new ValidationController(validator, propertyParser); + else { + resolvedPropertyName = this.propertyParser.parse(propertyName); + } + const result = new ValidateResult({ __manuallyAdded__: true }, object, resolvedPropertyName, false, message); + this.processResultDelta('validate', [], [result]); + return result; } /** - * 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. + * Removes and unrenders an error. */ - createForCurrentScope(validator) { - const controller = this.create(validator); - this.container.registerInstance(ValidationController, controller); - return controller; - } -} -ValidationControllerFactory['protocol:aurelia:resolver'] = true; - -let ValidationErrorsCustomAttribute = class ValidationErrorsCustomAttribute { - constructor(boundaryElement, controllerAccessor) { - this.boundaryElement = boundaryElement; - this.controllerAccessor = controllerAccessor; - this.controller = null; - this.errors = []; - this.errorsInternal = []; + removeError(result) { + if (this.results.indexOf(result) !== -1) { + this.processResultDelta('reset', [result], []); + } } - static inject() { - return [DOM.Element, Lazy.of(ValidationController)]; + /** + * Adds a renderer. + * @param renderer The renderer. + */ + addRenderer(renderer) { + this.renderers.push(renderer); + renderer.render({ + kind: 'validate', + render: this.results.map(result => ({ result, elements: this.elements.get(result) })), + unrender: [] + }); } - sort() { - this.errorsInternal.sort((a, b) => { - if (a.targets[0] === b.targets[0]) { - return 0; - } - // tslint:disable-next-line:no-bitwise - return a.targets[0].compareDocumentPosition(b.targets[0]) & 2 ? 1 : -1; + /** + * Removes a renderer. + * @param renderer The renderer. + */ + removeRenderer(renderer) { + this.renderers.splice(this.renderers.indexOf(renderer), 1); + renderer.render({ + kind: 'reset', + render: [], + unrender: this.results.map(result => ({ result, elements: this.elements.get(result) })) }); } - interestingElements(elements) { - return elements.filter(e => this.boundaryElement.contains(e)); + /** + * Registers a binding with the controller. + * @param binding The binding instance. + * @param target The DOM element. + * @param rules (optional) rules associated with the binding. Validator implementation specific. + */ + registerBinding(binding, target, rules) { + this.bindings.set(binding, { target, rules, propertyInfo: null }); } - render(instruction) { - for (const { result } of instruction.unrender) { - const index = this.errorsInternal.findIndex(x => x.error === result); - if (index !== -1) { - this.errorsInternal.splice(index, 1); - } - } - for (const { result, elements } of instruction.render) { - if (result.valid) { - continue; + /** + * Unregisters a binding with the controller. + * @param binding The binding instance. + */ + unregisterBinding(binding) { + this.resetBinding(binding); + this.bindings.delete(binding); + } + /** + * Interprets the instruction and returns a predicate that will identify + * relevant results in the list of rendered validation results. + */ + getInstructionPredicate(instruction) { + if (instruction) { + const { object, propertyName, rules } = instruction; + let predicate; + if (instruction.propertyName) { + predicate = x => x.object === object && x.propertyName === propertyName; } - const targets = this.interestingElements(elements); - if (targets.length) { - this.errorsInternal.push({ error: result, targets }); + else { + predicate = x => x.object === object; } + if (rules) { + return x => predicate(x) && this.validator.ruleExists(rules, x.rule); + } + return predicate; } - this.sort(); - this.errors = this.errorsInternal; - } - bind() { - if (!this.controller) { - this.controller = this.controllerAccessor(); - } - // this will call render() with the side-effect of updating this.errors - this.controller.addRenderer(this); - } - unbind() { - if (this.controller) { - this.controller.removeRenderer(this); + else { + return () => true; } } -}; -__decorate([ - bindable({ defaultBindingMode: bindingMode.oneWay }) -], ValidationErrorsCustomAttribute.prototype, "controller", void 0); -__decorate([ - bindable({ primaryProperty: true, defaultBindingMode: bindingMode.twoWay }) -], ValidationErrorsCustomAttribute.prototype, "errors", void 0); -ValidationErrorsCustomAttribute = __decorate([ - customAttribute('validation-errors') -], ValidationErrorsCustomAttribute); - -let ValidationRendererCustomAttribute = class ValidationRendererCustomAttribute { - created(view) { - this.container = view.container; - } - bind() { - this.controller = this.container.get(ValidationController); - this.renderer = this.container.get(this.value); - this.controller.addRenderer(this.renderer); - } - unbind() { - this.controller.removeRenderer(this.renderer); - this.controller = null; - this.renderer = null; - } -}; -ValidationRendererCustomAttribute = __decorate([ - customAttribute('validation-renderer') -], ValidationRendererCustomAttribute); - -/** - * Sets, unsets and retrieves rules on an object or constructor function. - */ -class Rules { /** - * Applies the rules to a target. + * Validates and renders results. + * @param instruction Optional. Instructions on what to validate. If undefined, all + * objects and bindings will be validated. */ - static set(target, rules) { - if (target instanceof Function) { - target = target.prototype; + validate(instruction) { + // Get a function that will process the validation instruction. + let execute; + if (instruction) { + // tslint:disable-next-line:prefer-const + let { object, propertyName, rules } = instruction; + // if rules were not specified, check the object map. + rules = rules || this.objects.get(object); + // property specified? + if (instruction.propertyName === undefined) { + // validate the specified object. + execute = () => this.validator.validateObject(object, rules); + } + else { + // validate the specified property. + execute = () => this.validator.validateProperty(object, propertyName, rules); + } } - Object.defineProperty(target, Rules.key, { enumerable: false, configurable: false, writable: true, value: rules }); + else { + // validate all objects and bindings. + execute = () => { + const promises = []; + for (const [object, rules] of Array.from(this.objects)) { + promises.push(this.validator.validateObject(object, rules)); + } + for (const [binding, { rules }] of Array.from(this.bindings)) { + const propertyInfo = getPropertyInfo(binding.sourceExpression, binding.source); + if (!propertyInfo || this.objects.has(propertyInfo.object)) { + continue; + } + promises.push(this.validator.validateProperty(propertyInfo.object, propertyInfo.propertyName, rules)); + } + return Promise.all(promises).then(resultSets => resultSets.reduce((a, b) => a.concat(b), [])); + }; + } + // Wait for any existing validation to finish, execute the instruction, render the results. + this.validating = true; + const returnPromise = this.finishValidating + .then(execute) + .then((newResults) => { + const predicate = this.getInstructionPredicate(instruction); + const oldResults = this.results.filter(predicate); + this.processResultDelta('validate', oldResults, newResults); + if (returnPromise === this.finishValidating) { + this.validating = false; + } + const result = { + instruction, + valid: newResults.find(x => !x.valid) === undefined, + results: newResults + }; + this.invokeCallbacks(instruction, result); + return result; + }) + .catch(exception => { + // recover, to enable subsequent calls to validate() + this.validating = false; + this.finishValidating = Promise.resolve(); + return Promise.reject(exception); + }); + this.finishValidating = returnPromise; + return returnPromise; } /** - * Removes rules from a target. + * Resets any rendered validation results (unrenders). + * @param instruction Optional. Instructions on what to reset. If unspecified all rendered results + * will be unrendered. */ - static unset(target) { - if (target instanceof Function) { - target = target.prototype; - } - target[Rules.key] = null; + reset(instruction) { + const predicate = this.getInstructionPredicate(instruction); + const oldResults = this.results.filter(predicate); + this.processResultDelta('reset', oldResults, []); + this.invokeCallbacks(instruction, null); } /** - * Retrieves the target's rules. + * Gets the elements associated with an object and propertyName (if any). */ - static get(target) { - return target[Rules.key] || null; - } -} -/** - * The name of the property that stores the rules. - */ -Rules.key = '__rules__'; - -// tslint:disable:no-empty -class ExpressionVisitor { - visitChain(chain) { - this.visitArgs(chain.expressions); - } - visitBindingBehavior(behavior) { - behavior.expression.accept(this); - this.visitArgs(behavior.args); - } - visitValueConverter(converter) { - converter.expression.accept(this); - this.visitArgs(converter.args); - } - visitAssign(assign) { - assign.target.accept(this); - assign.value.accept(this); - } - visitConditional(conditional) { - conditional.condition.accept(this); - conditional.yes.accept(this); - conditional.no.accept(this); - } - visitAccessThis(access) { - access.ancestor = access.ancestor; - } - visitAccessScope(access) { - access.name = access.name; - } - visitAccessMember(access) { - access.object.accept(this); - } - visitAccessKeyed(access) { - access.object.accept(this); - access.key.accept(this); - } - visitCallScope(call) { - this.visitArgs(call.args); - } - visitCallFunction(call) { - call.func.accept(this); - this.visitArgs(call.args); - } - visitCallMember(call) { - call.object.accept(this); - this.visitArgs(call.args); - } - visitPrefix(prefix) { - prefix.expression.accept(this); + getAssociatedElements({ object, propertyName }) { + const elements = []; + for (const [binding, { target }] of Array.from(this.bindings)) { + const propertyInfo = getPropertyInfo(binding.sourceExpression, binding.source); + if (propertyInfo && propertyInfo.object === object && propertyInfo.propertyName === propertyName) { + elements.push(target); + } + } + return elements; } - visitBinary(binary) { - binary.left.accept(this); - binary.right.accept(this); + processResultDelta(kind, oldResults, newResults) { + // prepare the instruction. + const instruction = { + kind, + render: [], + unrender: [] + }; + // create a shallow copy of newResults so we can mutate it without causing side-effects. + newResults = newResults.slice(0); + // create unrender instructions from the old results. + for (const oldResult of oldResults) { + // get the elements associated with the old result. + const elements = this.elements.get(oldResult); + // remove the old result from the element map. + this.elements.delete(oldResult); + // create the unrender instruction. + instruction.unrender.push({ result: oldResult, elements }); + // determine if there's a corresponding new result for the old result we are unrendering. + const newResultIndex = newResults.findIndex(x => x.rule === oldResult.rule && x.object === oldResult.object && x.propertyName === oldResult.propertyName); + if (newResultIndex === -1) { + // no corresponding new result... simple remove. + this.results.splice(this.results.indexOf(oldResult), 1); + if (!oldResult.valid) { + this.errors.splice(this.errors.indexOf(oldResult), 1); + } + } + else { + // there is a corresponding new result... + const newResult = newResults.splice(newResultIndex, 1)[0]; + // get the elements that are associated with the new result. + const elements = this.getAssociatedElements(newResult); + this.elements.set(newResult, elements); + // create a render instruction for the new result. + instruction.render.push({ result: newResult, elements }); + // do an in-place replacement of the old result with the new result. + // this ensures any repeats bound to this.results will not thrash. + this.results.splice(this.results.indexOf(oldResult), 1, newResult); + if (!oldResult.valid && newResult.valid) { + this.errors.splice(this.errors.indexOf(oldResult), 1); + } + else if (!oldResult.valid && !newResult.valid) { + this.errors.splice(this.errors.indexOf(oldResult), 1, newResult); + } + else if (!newResult.valid) { + this.errors.push(newResult); + } + } + } + // create render instructions from the remaining new results. + for (const result of newResults) { + const elements = this.getAssociatedElements(result); + instruction.render.push({ result, elements }); + this.elements.set(result, elements); + this.results.push(result); + if (!result.valid) { + this.errors.push(result); + } + } + // render. + for (const renderer of this.renderers) { + renderer.render(instruction); + } } - visitLiteralPrimitive(literal) { - literal.value = literal.value; + /** + * Validates the property associated with a binding. + */ + validateBinding(binding) { + if (!binding.isBound) { + return; + } + const propertyInfo = getPropertyInfo(binding.sourceExpression, binding.source); + let rules; + const registeredBinding = this.bindings.get(binding); + if (registeredBinding) { + rules = registeredBinding.rules; + registeredBinding.propertyInfo = propertyInfo; + } + if (!propertyInfo) { + return; + } + const { object, propertyName } = propertyInfo; + this.validate({ object, propertyName, rules }); } - visitLiteralArray(literal) { - this.visitArgs(literal.elements); + /** + * Resets the results for a property associated with a binding. + */ + resetBinding(binding) { + const registeredBinding = this.bindings.get(binding); + let propertyInfo = getPropertyInfo(binding.sourceExpression, binding.source); + if (!propertyInfo && registeredBinding) { + propertyInfo = registeredBinding.propertyInfo; + } + if (registeredBinding) { + registeredBinding.propertyInfo = null; + } + if (!propertyInfo) { + return; + } + const { object, propertyName } = propertyInfo; + this.reset({ object, propertyName }); } - visitLiteralObject(literal) { - this.visitArgs(literal.values); + /** + * Changes the controller's validateTrigger. + * @param newTrigger The new validateTrigger + */ + changeTrigger(newTrigger) { + this.validateTrigger = newTrigger; + const bindings = Array.from(this.bindings.keys()); + for (const binding of bindings) { + const source = binding.source; + binding.unbind(); + binding.bind(source); + } } - visitLiteralString(literal) { - literal.value = literal.value; + /** + * Revalidates the controller's current set of errors. + */ + revalidateErrors() { + for (const { object, propertyName, rule } of this.errors) { + if (rule.__manuallyAdded__) { + continue; + } + const rules = [[rule]]; + this.validate({ object, propertyName, rules }); + } } - visitArgs(args) { - for (let i = 0; i < args.length; i++) { - args[i].accept(this); + invokeCallbacks(instruction, result) { + if (this.eventCallbacks.length === 0) { + return; + } + const event = new ValidateEvent(result ? 'validate' : 'reset', this.errors, this.results, instruction || null, result); + for (let i = 0; i < this.eventCallbacks.length; i++) { + this.eventCallbacks[i](event); } } -} +} +ValidationController.inject = [Validator, PropertyAccessorParser, GlobalValidationConfiguration]; -class ValidationMessageParser { - constructor(bindinqLanguage) { - this.bindinqLanguage = bindinqLanguage; - this.emptyStringExpression = new LiteralString(''); - this.nullExpression = new LiteralPrimitive(null); - this.undefinedExpression = new LiteralPrimitive(undefined); - this.cache = {}; +/** + * Binding behavior. Indicates the bound property should be validated. + */ +class ValidateBindingBehaviorBase { + constructor(taskQueue) { + this.taskQueue = taskQueue; } - parse(message) { - if (this.cache[message] !== undefined) { - return this.cache[message]; + bind(binding, source, rulesOrController, rules) { + // identify the target element. + const target = getTargetDOMElement(binding, source); + // locate the controller. + let controller; + if (rulesOrController instanceof ValidationController) { + controller = rulesOrController; } - const parts = this.bindinqLanguage.parseInterpolation(null, message); - if (parts === null) { - return new LiteralString(message); + else { + controller = source.container.get(Optional.of(ValidationController)); + rules = rulesOrController; } - let expression = new LiteralString(parts[0]); - for (let i = 1; i < parts.length; i += 2) { - expression = new Binary('+', expression, new Binary('+', this.coalesce(parts[i]), new LiteralString(parts[i + 1]))); + 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); + // tslint:disable-next-line:no-bitwise + if (trigger & validateTrigger.change) { + binding.vbbUpdateSource = binding.updateSource; + // tslint:disable-next-line:only-arrow-functions + // tslint:disable-next-line:space-before-function-paren + binding.updateSource = function (value) { + this.vbbUpdateSource(value); + this.validationController.validateBinding(this); + }; + } + // tslint:disable-next-line:no-bitwise + 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; + // tslint:disable-next-line:only-arrow-functions + // tslint:disable-next-line:space-before-function-paren + binding.updateTarget = function (value) { + this.standardUpdateTarget(value); + this.validationController.resetBinding(this); + }; } - MessageExpressionValidator.validate(expression, message); - this.cache[message] = expression; - return expression; - } - coalesce(part) { - // part === null || part === undefined ? '' : part - return new Conditional(new Binary('||', new Binary('===', part, this.nullExpression), new Binary('===', part, this.undefinedExpression)), this.emptyStringExpression, new CallMember(part, 'toString', [])); - } -} -ValidationMessageParser.inject = [BindingLanguage]; -class MessageExpressionValidator extends ExpressionVisitor { - constructor(originalMessage) { - super(); - this.originalMessage = originalMessage; - } - static validate(expression, originalMessage) { - const visitor = new MessageExpressionValidator(originalMessage); - expression.accept(visitor); } - visitAccessScope(access) { - if (access.ancestor !== 0) { - throw new Error('$parent is not permitted in validation message expressions.'); + unbind(binding) { + // reset the binding to it's original state. + if (binding.vbbUpdateSource) { + binding.updateSource = binding.vbbUpdateSource; + binding.vbbUpdateSource = null; } - if (['displayName', 'propertyName', 'value', 'object', 'config', 'getDisplayName'].indexOf(access.name) !== -1) { - getLogger('aurelia-validation') - // tslint:disable-next-line:max-line-length - .warn(`Did you mean to use "$${access.name}" instead of "${access.name}" in this validation message template: "${this.originalMessage}"?`); + 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; } } /** - * Dictionary of validation messages. [messageKey]: messageExpression + * Binding behavior. Indicates the bound property should be validated + * when the validate trigger specified by the associated controller's + * validateTrigger property occurs. */ -const validationMessages = { - /** - * The default validation message. Used with rules that have no standard message. - */ - default: `\${$displayName} is invalid.`, - required: `\${$displayName} is required.`, - matches: `\${$displayName} is not correctly formatted.`, - email: `\${$displayName} is not a valid email.`, - minLength: `\${$displayName} must be at least \${$config.length} character\${$config.length === 1 ? '' : 's'}.`, - maxLength: `\${$displayName} cannot be longer than \${$config.length} character\${$config.length === 1 ? '' : 's'}.`, - minItems: `\${$displayName} must contain at least \${$config.count} item\${$config.count === 1 ? '' : 's'}.`, - maxItems: `\${$displayName} cannot contain more than \${$config.count} item\${$config.count === 1 ? '' : 's'}.`, - min: `\${$displayName} must be at least \${$config.constraint}.`, - max: `\${$displayName} must be at most \${$config.constraint}.`, - range: `\${$displayName} must be between or equal to \${$config.min} and \${$config.max}.`, - between: `\${$displayName} must be between but not equal to \${$config.min} and \${$config.max}.`, - equals: `\${$displayName} must be \${$config.expectedValue}.`, +let ValidateBindingBehavior = class ValidateBindingBehavior extends ValidateBindingBehaviorBase { + getValidateTrigger(controller) { + return controller.validateTrigger; + } +}; +ValidateBindingBehavior.inject = [TaskQueue]; +ValidateBindingBehavior = __decorate([ + bindingBehavior('validate') +], 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. + */ +let ValidateManuallyBindingBehavior = class ValidateManuallyBindingBehavior extends ValidateBindingBehaviorBase { + getValidateTrigger() { + return validateTrigger.manual; + } +}; +ValidateManuallyBindingBehavior.inject = [TaskQueue]; +ValidateManuallyBindingBehavior = __decorate([ + bindingBehavior('validateManually') +], ValidateManuallyBindingBehavior); +/** + * Binding behavior. Indicates the bound property should be validated + * when the associated element blurs. + */ +let ValidateOnBlurBindingBehavior = class ValidateOnBlurBindingBehavior extends ValidateBindingBehaviorBase { + getValidateTrigger() { + return validateTrigger.blur; + } +}; +ValidateOnBlurBindingBehavior.inject = [TaskQueue]; +ValidateOnBlurBindingBehavior = __decorate([ + bindingBehavior('validateOnBlur') +], 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. + */ +let ValidateOnChangeBindingBehavior = class ValidateOnChangeBindingBehavior extends ValidateBindingBehaviorBase { + getValidateTrigger() { + return validateTrigger.change; + } +}; +ValidateOnChangeBindingBehavior.inject = [TaskQueue]; +ValidateOnChangeBindingBehavior = __decorate([ + bindingBehavior('validateOnChange') +], 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. + */ +let ValidateOnChangeOrBlurBindingBehavior = class ValidateOnChangeOrBlurBindingBehavior extends ValidateBindingBehaviorBase { + getValidateTrigger() { + return validateTrigger.changeOrBlur; + } }; +ValidateOnChangeOrBlurBindingBehavior.inject = [TaskQueue]; +ValidateOnChangeOrBlurBindingBehavior = __decorate([ + bindingBehavior('validateOnChangeOrBlur') +], ValidateOnChangeOrBlurBindingBehavior); + /** - * Retrieves validation messages and property display names. + * Creates ValidationController instances. */ -class ValidationMessageProvider { - constructor(parser) { - this.parser = parser; +class ValidationControllerFactory { + constructor(container) { + this.container = container; + } + static get(container) { + return new ValidationControllerFactory(container); } /** - * Returns a message binding expression that corresponds to the key. - * @param key The message key. + * Creates a new controller instance. */ - getMessage(key) { - let message; - if (key in validationMessages) { - message = validationMessages[key]; - } - else { - message = validationMessages['default']; + create(validator) { + if (!validator) { + validator = this.container.get(Validator); } - return this.parser.parse(message); + const propertyParser = this.container.get(PropertyAccessorParser); + const config = this.container.get(GlobalValidationConfiguration); + return new ValidationController(validator, propertyParser, config); } /** - * Formulates a property display name using the property name and the configured - * displayName (if provided). - * Override this with your own custom logic. - * @param propertyName The property name. + * 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. */ - getDisplayName(propertyName, displayName) { - if (displayName !== null && displayName !== undefined) { - return (displayName instanceof Function) ? displayName() : displayName; - } - // split on upper-case letters. - const words = propertyName.toString().split(/(?=[A-Z])/).join(' '); - // capitalize first letter. - return words.charAt(0).toUpperCase() + words.slice(1); + createForCurrentScope(validator) { + const controller = this.create(validator); + this.container.registerInstance(ValidationController, controller); + return controller; } } -ValidationMessageProvider.inject = [ValidationMessageParser]; +ValidationControllerFactory['protocol:aurelia:resolver'] = true; -/** - * Validates. - * Responsible for validating objects and properties. - */ -class StandardValidator extends Validator { - constructor(messageProvider, resources) { - super(); - this.messageProvider = messageProvider; - this.lookupFunctions = resources.lookupFunctions; - this.getDisplayName = messageProvider.getDisplayName.bind(messageProvider); - } - /** - * Validates the specified property. - * @param object The object to validate. - * @param propertyName The name of the property to validate. - * @param rules Optional. If unspecified, the rules will be looked up using the metadata - * for the object created by ValidationRules....on(class/object) - */ - validateProperty(object, propertyName, rules) { - return this.validate(object, propertyName, rules || null); +let ValidationErrorsCustomAttribute = class ValidationErrorsCustomAttribute { + constructor(boundaryElement, controllerAccessor) { + this.boundaryElement = boundaryElement; + this.controllerAccessor = controllerAccessor; + this.controller = null; + this.errors = []; + this.errorsInternal = []; } - /** - * Validates all rules for specified object and it's properties. - * @param object The object to validate. - * @param rules Optional. If unspecified, the rules will be looked up using the metadata - * for the object created by ValidationRules....on(class/object) - */ - validateObject(object, rules) { - return this.validate(object, null, rules || null); + static inject() { + return [DOM.Element, Lazy.of(ValidationController)]; } - /** - * Determines whether a rule exists in a set of rules. - * @param rules The rules to search. - * @parem rule The rule to find. - */ - ruleExists(rules, rule) { - let i = rules.length; - while (i--) { - if (rules[i].indexOf(rule) !== -1) { - return true; + sort() { + this.errorsInternal.sort((a, b) => { + if (a.targets[0] === b.targets[0]) { + return 0; } - } - return false; + // tslint:disable-next-line:no-bitwise + return a.targets[0].compareDocumentPosition(b.targets[0]) & 2 ? 1 : -1; + }); } - getMessage(rule, object, value) { - const expression = rule.message || this.messageProvider.getMessage(rule.messageKey); - // tslint:disable-next-line:prefer-const - let { name: propertyName, displayName } = rule.property; - if (propertyName !== null) { - displayName = this.messageProvider.getDisplayName(propertyName, displayName); - } - const overrideContext = { - $displayName: displayName, - $propertyName: propertyName, - $value: value, - $object: object, - $config: rule.config, - // returns the name of a given property, given just the property name (irrespective of the property's displayName) - // split on capital letters, first letter ensured to be capitalized - $getDisplayName: this.getDisplayName - }; - return expression.evaluate({ bindingContext: object, overrideContext }, this.lookupFunctions); + interestingElements(elements) { + return elements.filter(e => this.boundaryElement.contains(e)); } - validateRuleSequence(object, propertyName, ruleSequence, sequence, results) { - // are we validating all properties or a single property? - const validateAllProperties = propertyName === null || propertyName === undefined; - const rules = ruleSequence[sequence]; - let allValid = true; - // validate each rule. - const promises = []; - for (let i = 0; i < rules.length; i++) { - const rule = rules[i]; - // is the rule related to the property we're validating. - // tslint:disable-next-line:triple-equals | Use loose equality for property keys - if (!validateAllProperties && rule.property.name != propertyName) { - continue; + render(instruction) { + for (const { result } of instruction.unrender) { + const index = this.errorsInternal.findIndex(x => x.error === result); + if (index !== -1) { + this.errorsInternal.splice(index, 1); } - // is this a conditional rule? is the condition met? - if (rule.when && !rule.when(object)) { + } + for (const { result, elements } of instruction.render) { + if (result.valid) { continue; } - // validate. - const value = rule.property.name === null ? object : object[rule.property.name]; - let promiseOrBoolean = rule.condition(value, object); - if (!(promiseOrBoolean instanceof Promise)) { - promiseOrBoolean = Promise.resolve(promiseOrBoolean); + const targets = this.interestingElements(elements); + if (targets.length) { + this.errorsInternal.push({ error: result, targets }); } - promises.push(promiseOrBoolean.then(valid => { - const message = valid ? null : this.getMessage(rule, object, value); - results.push(new ValidateResult(rule, object, rule.property.name, valid, message)); - allValid = allValid && valid; - return valid; - })); } - return Promise.all(promises) - .then(() => { - sequence++; - if (allValid && sequence < ruleSequence.length) { - return this.validateRuleSequence(object, propertyName, ruleSequence, sequence, results); - } - return results; - }); + this.sort(); + this.errors = this.errorsInternal; } - validate(object, propertyName, rules) { - // rules specified? - if (!rules) { - // no. attempt to locate the rules. - rules = Rules.get(object); + bind() { + if (!this.controller) { + this.controller = this.controllerAccessor(); } - // any rules? - if (!rules || rules.length === 0) { - return Promise.resolve([]); + // this will call render() with the side-effect of updating this.errors + this.controller.addRenderer(this); + } + unbind() { + if (this.controller) { + this.controller.removeRenderer(this); } - return this.validateRuleSequence(object, propertyName, rules, 0, []); } -} -StandardValidator.inject = [ValidationMessageProvider, ViewResources]; +}; +__decorate([ + bindable({ defaultBindingMode: bindingMode.oneWay }) +], ValidationErrorsCustomAttribute.prototype, "controller", void 0); +__decorate([ + bindable({ primaryProperty: true, defaultBindingMode: bindingMode.twoWay }) +], ValidationErrorsCustomAttribute.prototype, "errors", void 0); +ValidationErrorsCustomAttribute = __decorate([ + customAttribute('validation-errors') +], ValidationErrorsCustomAttribute); + +let ValidationRendererCustomAttribute = class ValidationRendererCustomAttribute { + created(view) { + this.container = view.container; + } + bind() { + this.controller = this.container.get(ValidationController); + this.renderer = this.container.get(this.value); + this.controller.addRenderer(this.renderer); + } + unbind() { + this.controller.removeRenderer(this.renderer); + this.controller = null; + this.renderer = null; + } +}; +ValidationRendererCustomAttribute = __decorate([ + customAttribute('validation-renderer') +], ValidationRendererCustomAttribute); /** * Part of the fluent rule API. Enables customizing property rules. @@ -1686,27 +1719,6 @@ class ValidationRules { } // Exports -/** - * Aurelia Validation Configuration API - */ -class AureliaValidationConfiguration { - constructor() { - this.validatorType = StandardValidator; - } - /** - * Use a custom Validator implementation. - */ - customValidator(type) { - this.validatorType = type; - } - /** - * Applies the configuration. - */ - apply(container) { - const validator = container.get(this.validatorType); - container.registerInstance(Validator, validator); - } -} /** * Configures the plugin. */ @@ -1719,7 +1731,7 @@ frameworkConfig, callback) { const propertyParser = frameworkConfig.container.get(PropertyAccessorParser); ValidationRules.initialize(messageParser, propertyParser); // configure... - const config = new AureliaValidationConfiguration(); + const config = new GlobalValidationConfiguration(); if (callback instanceof Function) { callback(config); } @@ -1730,4 +1742,4 @@ frameworkConfig, callback) { } } -export { AureliaValidationConfiguration, configure, getTargetDOMElement, getPropertyInfo, PropertyAccessorParser, getAccessorExpression, ValidateBindingBehavior, ValidateManuallyBindingBehavior, ValidateOnBlurBindingBehavior, ValidateOnChangeBindingBehavior, ValidateOnChangeOrBlurBindingBehavior, ValidateEvent, ValidateResult, validateTrigger, ValidationController, ValidationControllerFactory, ValidationErrorsCustomAttribute, ValidationRendererCustomAttribute, Validator, Rules, StandardValidator, validationMessages, ValidationMessageProvider, ValidationMessageParser, MessageExpressionValidator, FluentRuleCustomizer, FluentRules, FluentEnsure, ValidationRules }; +export { configure, GlobalValidationConfiguration, getTargetDOMElement, getPropertyInfo, PropertyAccessorParser, getAccessorExpression, ValidateBindingBehavior, ValidateManuallyBindingBehavior, ValidateOnBlurBindingBehavior, ValidateOnChangeBindingBehavior, ValidateOnChangeOrBlurBindingBehavior, ValidateEvent, ValidateResult, validateTrigger, ValidationController, ValidationControllerFactory, ValidationErrorsCustomAttribute, ValidationRendererCustomAttribute, Validator, Rules, StandardValidator, validationMessages, ValidationMessageProvider, ValidationMessageParser, MessageExpressionValidator, FluentRuleCustomizer, FluentRules, FluentEnsure, ValidationRules }; diff --git a/dist/native-modules/aurelia-validation.js b/dist/native-modules/aurelia-validation.js index 8d9316da..4d995906 100644 --- a/dist/native-modules/aurelia-validation.js +++ b/dist/native-modules/aurelia-validation.js @@ -1,115 +1,18 @@ +import { LiteralString, Binary, Conditional, LiteralPrimitive, CallMember, AccessMember, AccessScope, AccessKeyed, BindingBehavior, ValueConverter, getContextFor, Parser, bindingBehavior, bindingMode } from 'aurelia-binding'; +import { BindingLanguage, ViewResources, customAttribute, bindable } from 'aurelia-templating'; +import { getLogger } from 'aurelia-logging'; import { DOM } from 'aurelia-pal'; -import { AccessMember, AccessScope, AccessKeyed, BindingBehavior, ValueConverter, getContextFor, Parser, bindingBehavior, bindingMode, LiteralString, Binary, Conditional, LiteralPrimitive, CallMember } from 'aurelia-binding'; import { Optional, Lazy } from 'aurelia-dependency-injection'; import { TaskQueue } from 'aurelia-task-queue'; -import { customAttribute, bindable, BindingLanguage, ViewResources } from 'aurelia-templating'; -import { getLogger } from 'aurelia-logging'; - -/** - * 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. - */ -function getTargetDOMElement(binding, view) { - var target = binding.target; - // DOM element - if (target instanceof Element) { - return target; - } - // custom element or custom attribute - // tslint:disable-next-line:prefer-const - 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 + "\"."); -} -function getObject(expression, objectExpression, source) { - var value = objectExpression.evaluate(source, null); - if (value === null || value === undefined || value instanceof Object) { - return value; - } - // tslint:disable-next-line:max-line-length - throw new Error("The '" + objectExpression + "' part of '" + expression + "' evaluates to " + value + " instead of an object, null or undefined."); -} /** - * Retrieves the object and property name for the specified expression. - * @param expression The expression - * @param source The scope + * Validates objects and properties. */ -function getPropertyInfo(expression, source) { - var originalExpression = expression; - while (expression instanceof BindingBehavior || expression instanceof ValueConverter) { - expression = expression.expression; - } - var object; - var propertyName; - if (expression instanceof AccessScope) { - object = getContextFor(expression.name, source, expression.ancestor); - propertyName = expression.name; - } - else if (expression instanceof AccessMember) { - object = getObject(originalExpression, expression.object, source); - propertyName = expression.name; - } - else if (expression instanceof AccessKeyed) { - object = getObject(originalExpression, expression.object, source); - propertyName = expression.key.evaluate(source); - } - else { - throw new Error("Expression '" + originalExpression + "' is not compatible with the validate binding-behavior."); - } - if (object === null || object === undefined) { - return null; - } - return { object: object, propertyName: propertyName }; -} - -function isString(value) { - return Object.prototype.toString.call(value) === '[object String]'; -} -function isNumber(value) { - return Object.prototype.toString.call(value) === '[object Number]'; -} - -var PropertyAccessorParser = /** @class */ (function () { - function PropertyAccessorParser(parser) { - this.parser = parser; - } - PropertyAccessorParser.prototype.parse = function (property) { - if (isString(property) || isNumber(property)) { - return property; - } - var accessorText = getAccessorExpression(property.toString()); - var accessor = this.parser.parse(accessorText); - if (accessor instanceof AccessScope - || accessor instanceof AccessMember && accessor.object instanceof AccessScope) { - return accessor.name; - } - throw new Error("Invalid property expression: \"" + accessor + "\""); - }; - PropertyAccessorParser.inject = [Parser]; - return PropertyAccessorParser; -}()); -function getAccessorExpression(fn) { - /* tslint:disable:max-line-length */ - var classic = /^function\s*\([$_\w\d]+\)\s*\{(?:\s*"use strict";)?\s*(?:[$_\w\d.['"\]+;]+)?\s*return\s+[$_\w\d]+\.([$_\w\d]+)\s*;?\s*\}$/; - /* tslint:enable:max-line-length */ - var arrow = /^\(?[$_\w\d]+\)?\s*=>\s*[$_\w\d]+\.([$_\w\d]+)$/; - var match = classic.exec(fn) || arrow.exec(fn); - if (match === null) { - throw new Error("Unable to parse accessor function:\n" + fn); +var Validator = /** @class */ (function () { + function Validator() { } - return match[1]; -} + return Validator; +}()); /*! ***************************************************************************** Copyright (c) Microsoft Corporation. All rights reserved. @@ -145,42 +48,16 @@ function __decorate(decorators, target, key, desc) { if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc); else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r; return c > 3 && r && Object.defineProperty(target, key, r), r; +} + +function __spreadArrays() { + for (var s = 0, i = 0, il = arguments.length; i < il; i++) s += arguments[i].length; + for (var r = Array(s), k = 0, i = 0; i < il; i++) + for (var a = arguments[i], j = 0, jl = a.length; j < jl; j++, k++) + r[k] = a[j]; + return r; } -/** - * Validation triggers. - */ -var validateTrigger; -(function (validateTrigger) { - /** - * Manual validation. Use the controller's `validate()` and `reset()` methods - * to validate all bindings. - */ - validateTrigger[validateTrigger["manual"] = 0] = "manual"; - /** - * Validate the binding when the binding's target element fires a DOM "blur" event. - */ - validateTrigger[validateTrigger["blur"] = 1] = "blur"; - /** - * Validate the binding when it updates the model due to a change in the view. - */ - validateTrigger[validateTrigger["change"] = 2] = "change"; - /** - * 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. - */ - validateTrigger[validateTrigger["changeOrBlur"] = 3] = "changeOrBlur"; -})(validateTrigger || (validateTrigger = {})); - -/** - * Validates objects and properties. - */ -var Validator = /** @class */ (function () { - function Validator() { - } - return Validator; -}()); - /** * The result of validating an individual validation rule. */ @@ -207,1102 +84,1267 @@ var ValidateResult = /** @class */ (function () { return ValidateResult; }()); -var ValidateEvent = /** @class */ (function () { - function ValidateEvent( - /** - * The type of validate event. Either "validate" or "reset". - */ - type, +/** + * Sets, unsets and retrieves rules on an object or constructor function. + */ +var Rules = /** @class */ (function () { + function Rules() { + } /** - * The controller's current array of errors. For an array containing both - * failed rules and passed rules, use the "results" property. + * Applies the rules to a target. */ - errors, + Rules.set = function (target, rules) { + if (target instanceof Function) { + target = target.prototype; + } + Object.defineProperty(target, Rules.key, { enumerable: false, configurable: false, writable: true, value: rules }); + }; /** - * The controller's current array of validate results. This - * includes both passed rules and failed rules. For an array of only failed rules, - * use the "errors" property. + * Removes rules from a target. */ - results, + Rules.unset = function (target) { + if (target instanceof Function) { + target = target.prototype; + } + target[Rules.key] = null; + }; /** - * The instruction passed to the "validate" or "reset" event. Will be null when - * the controller's validate/reset method was called with no instruction argument. + * Retrieves the target's rules. */ - instruction, + Rules.get = function (target) { + return target[Rules.key] || null; + }; /** - * In events with type === "validate", this property will contain the result - * of validating the instruction (see "instruction" property). Use the controllerValidateResult - * to access the validate results specific to the call to "validate" - * (as opposed to using the "results" and "errors" properties to access the controller's entire - * set of results/errors). + * The name of the property that stores the rules. */ - controllerValidateResult) { - this.type = type; - this.errors = errors; - this.results = results; - this.instruction = instruction; - this.controllerValidateResult = controllerValidateResult; - } - return ValidateEvent; + Rules.key = '__rules__'; + return Rules; }()); -/** - * Orchestrates validation. - * Manages a set of bindings, renderers and objects. - * Exposes the current list of validation results for binding purposes. - */ -var ValidationController = /** @class */ (function () { - function ValidationController(validator, propertyParser) { - this.validator = validator; - this.propertyParser = propertyParser; - // Registered bindings (via the validate binding behavior) - this.bindings = new Map(); - // Renderers that have been added to the controller instance. - this.renderers = []; - /** - * Validation results that have been rendered by the controller. - */ - this.results = []; - /** - * Validation errors that have been rendered by the controller. - */ - this.errors = []; - /** - * Whether the controller is currently validating. - */ - this.validating = false; - // Elements related to validation results that have been rendered. - this.elements = new Map(); - // Objects that have been added to the controller instance (entity-style validation). - this.objects = new Map(); - /** - * The trigger that will invoke automatic validation of a property used in a binding. - */ - this.validateTrigger = validateTrigger.blur; - // Promise that resolves when validation has completed. - this.finishValidating = Promise.resolve(); - this.eventCallbacks = []; +// tslint:disable:no-empty +var ExpressionVisitor = /** @class */ (function () { + function ExpressionVisitor() { } - /** - * Subscribe to controller validate and reset events. These events occur when the - * controller's "validate"" and "reset" methods are called. - * @param callback The callback to be invoked when the controller validates or resets. - */ - ValidationController.prototype.subscribe = function (callback) { - var _this = this; - this.eventCallbacks.push(callback); - return { - dispose: function () { - var index = _this.eventCallbacks.indexOf(callback); - if (index === -1) { - return; - } - _this.eventCallbacks.splice(index, 1); - } - }; + ExpressionVisitor.prototype.visitChain = function (chain) { + this.visitArgs(chain.expressions); }; - /** - * Adds an object to the set of objects that should be validated when validate is called. - * @param object The object. - * @param rules Optional. The rules. If rules aren't supplied the Validator implementation will lookup the rules. - */ - ValidationController.prototype.addObject = function (object, rules) { - this.objects.set(object, rules); + ExpressionVisitor.prototype.visitBindingBehavior = function (behavior) { + behavior.expression.accept(this); + this.visitArgs(behavior.args); + }; + ExpressionVisitor.prototype.visitValueConverter = function (converter) { + converter.expression.accept(this); + this.visitArgs(converter.args); + }; + ExpressionVisitor.prototype.visitAssign = function (assign) { + assign.target.accept(this); + assign.value.accept(this); + }; + ExpressionVisitor.prototype.visitConditional = function (conditional) { + conditional.condition.accept(this); + conditional.yes.accept(this); + conditional.no.accept(this); + }; + ExpressionVisitor.prototype.visitAccessThis = function (access) { + access.ancestor = access.ancestor; + }; + ExpressionVisitor.prototype.visitAccessScope = function (access) { + access.name = access.name; + }; + ExpressionVisitor.prototype.visitAccessMember = function (access) { + access.object.accept(this); + }; + ExpressionVisitor.prototype.visitAccessKeyed = function (access) { + access.object.accept(this); + access.key.accept(this); + }; + ExpressionVisitor.prototype.visitCallScope = function (call) { + this.visitArgs(call.args); + }; + ExpressionVisitor.prototype.visitCallFunction = function (call) { + call.func.accept(this); + this.visitArgs(call.args); + }; + ExpressionVisitor.prototype.visitCallMember = function (call) { + call.object.accept(this); + this.visitArgs(call.args); + }; + ExpressionVisitor.prototype.visitPrefix = function (prefix) { + prefix.expression.accept(this); + }; + ExpressionVisitor.prototype.visitBinary = function (binary) { + binary.left.accept(this); + binary.right.accept(this); + }; + ExpressionVisitor.prototype.visitLiteralPrimitive = function (literal) { + literal.value = literal.value; + }; + ExpressionVisitor.prototype.visitLiteralArray = function (literal) { + this.visitArgs(literal.elements); + }; + ExpressionVisitor.prototype.visitLiteralObject = function (literal) { + this.visitArgs(literal.values); + }; + ExpressionVisitor.prototype.visitLiteralString = function (literal) { + literal.value = literal.value; + }; + ExpressionVisitor.prototype.visitArgs = function (args) { + for (var i = 0; i < args.length; i++) { + args[i].accept(this); + } + }; + return ExpressionVisitor; +}()); + +var ValidationMessageParser = /** @class */ (function () { + function ValidationMessageParser(bindinqLanguage) { + this.bindinqLanguage = bindinqLanguage; + this.emptyStringExpression = new LiteralString(''); + this.nullExpression = new LiteralPrimitive(null); + this.undefinedExpression = new LiteralPrimitive(undefined); + this.cache = {}; + } + ValidationMessageParser.prototype.parse = function (message) { + if (this.cache[message] !== undefined) { + return this.cache[message]; + } + var parts = this.bindinqLanguage.parseInterpolation(null, message); + if (parts === null) { + return new LiteralString(message); + } + var expression = new LiteralString(parts[0]); + for (var i = 1; i < parts.length; i += 2) { + expression = new Binary('+', expression, new Binary('+', this.coalesce(parts[i]), new LiteralString(parts[i + 1]))); + } + MessageExpressionValidator.validate(expression, message); + this.cache[message] = expression; + return expression; + }; + ValidationMessageParser.prototype.coalesce = function (part) { + // part === null || part === undefined ? '' : part + return new Conditional(new Binary('||', new Binary('===', part, this.nullExpression), new Binary('===', part, this.undefinedExpression)), this.emptyStringExpression, new CallMember(part, 'toString', [])); + }; + ValidationMessageParser.inject = [BindingLanguage]; + return ValidationMessageParser; +}()); +var MessageExpressionValidator = /** @class */ (function (_super) { + __extends(MessageExpressionValidator, _super); + function MessageExpressionValidator(originalMessage) { + var _this = _super.call(this) || this; + _this.originalMessage = originalMessage; + return _this; + } + MessageExpressionValidator.validate = function (expression, originalMessage) { + var visitor = new MessageExpressionValidator(originalMessage); + expression.accept(visitor); + }; + MessageExpressionValidator.prototype.visitAccessScope = function (access) { + if (access.ancestor !== 0) { + throw new Error('$parent is not permitted in validation message expressions.'); + } + if (['displayName', 'propertyName', 'value', 'object', 'config', 'getDisplayName'].indexOf(access.name) !== -1) { + getLogger('aurelia-validation') + // tslint:disable-next-line:max-line-length + .warn("Did you mean to use \"$" + access.name + "\" instead of \"" + access.name + "\" in this validation message template: \"" + this.originalMessage + "\"?"); + } }; + return MessageExpressionValidator; +}(ExpressionVisitor)); + +/** + * Dictionary of validation messages. [messageKey]: messageExpression + */ +var validationMessages = { /** - * Removes an object from the set of objects that should be validated when validate is called. - * @param object The object. + * The default validation message. Used with rules that have no standard message. */ - ValidationController.prototype.removeObject = function (object) { - this.objects.delete(object); - this.processResultDelta('reset', this.results.filter(function (result) { return result.object === object; }), []); - }; + default: "${$displayName} is invalid.", + required: "${$displayName} is required.", + matches: "${$displayName} is not correctly formatted.", + email: "${$displayName} is not a valid email.", + minLength: "${$displayName} must be at least ${$config.length} character${$config.length === 1 ? '' : 's'}.", + maxLength: "${$displayName} cannot be longer than ${$config.length} character${$config.length === 1 ? '' : 's'}.", + minItems: "${$displayName} must contain at least ${$config.count} item${$config.count === 1 ? '' : 's'}.", + maxItems: "${$displayName} cannot contain more than ${$config.count} item${$config.count === 1 ? '' : 's'}.", + min: "${$displayName} must be at least ${$config.constraint}.", + max: "${$displayName} must be at most ${$config.constraint}.", + range: "${$displayName} must be between or equal to ${$config.min} and ${$config.max}.", + between: "${$displayName} must be between but not equal to ${$config.min} and ${$config.max}.", + equals: "${$displayName} must be ${$config.expectedValue}.", +}; +/** + * Retrieves validation messages and property display names. + */ +var ValidationMessageProvider = /** @class */ (function () { + function ValidationMessageProvider(parser) { + this.parser = parser; + } /** - * Adds and renders an error. + * Returns a message binding expression that corresponds to the key. + * @param key The message key. */ - ValidationController.prototype.addError = function (message, object, propertyName) { - if (propertyName === void 0) { propertyName = null; } - var resolvedPropertyName; - if (propertyName === null) { - resolvedPropertyName = propertyName; + ValidationMessageProvider.prototype.getMessage = function (key) { + var message; + if (key in validationMessages) { + message = validationMessages[key]; } else { - resolvedPropertyName = this.propertyParser.parse(propertyName); + message = validationMessages['default']; } - var result = new ValidateResult({ __manuallyAdded__: true }, object, resolvedPropertyName, false, message); - this.processResultDelta('validate', [], [result]); - return result; + return this.parser.parse(message); }; /** - * Removes and unrenders an error. + * Formulates a property display name using the property name and the configured + * displayName (if provided). + * Override this with your own custom logic. + * @param propertyName The property name. */ - ValidationController.prototype.removeError = function (result) { - if (this.results.indexOf(result) !== -1) { - this.processResultDelta('reset', [result], []); + ValidationMessageProvider.prototype.getDisplayName = function (propertyName, displayName) { + if (displayName !== null && displayName !== undefined) { + return (displayName instanceof Function) ? displayName() : displayName; } + // split on upper-case letters. + var words = propertyName.toString().split(/(?=[A-Z])/).join(' '); + // capitalize first letter. + return words.charAt(0).toUpperCase() + words.slice(1); }; + ValidationMessageProvider.inject = [ValidationMessageParser]; + return ValidationMessageProvider; +}()); + +/** + * Validates. + * Responsible for validating objects and properties. + */ +var StandardValidator = /** @class */ (function (_super) { + __extends(StandardValidator, _super); + function StandardValidator(messageProvider, resources) { + var _this = _super.call(this) || this; + _this.messageProvider = messageProvider; + _this.lookupFunctions = resources.lookupFunctions; + _this.getDisplayName = messageProvider.getDisplayName.bind(messageProvider); + return _this; + } /** - * Adds a renderer. - * @param renderer The renderer. + * Validates the specified property. + * @param object The object to validate. + * @param propertyName The name of the property to validate. + * @param rules Optional. If unspecified, the rules will be looked up using the metadata + * for the object created by ValidationRules....on(class/object) */ - ValidationController.prototype.addRenderer = function (renderer) { - var _this = this; - this.renderers.push(renderer); - renderer.render({ - kind: 'validate', - render: this.results.map(function (result) { return ({ result: result, elements: _this.elements.get(result) }); }), - unrender: [] - }); + StandardValidator.prototype.validateProperty = function (object, propertyName, rules) { + return this.validate(object, propertyName, rules || null); }; /** - * Removes a renderer. - * @param renderer The renderer. + * Validates all rules for specified object and it's properties. + * @param object The object to validate. + * @param rules Optional. If unspecified, the rules will be looked up using the metadata + * for the object created by ValidationRules....on(class/object) */ - ValidationController.prototype.removeRenderer = function (renderer) { - var _this = this; - this.renderers.splice(this.renderers.indexOf(renderer), 1); - renderer.render({ - kind: 'reset', - render: [], - unrender: this.results.map(function (result) { return ({ result: result, elements: _this.elements.get(result) }); }) - }); + StandardValidator.prototype.validateObject = function (object, rules) { + return this.validate(object, null, rules || null); }; /** - * Registers a binding with the controller. - * @param binding The binding instance. - * @param target The DOM element. - * @param rules (optional) rules associated with the binding. Validator implementation specific. + * Determines whether a rule exists in a set of rules. + * @param rules The rules to search. + * @parem rule The rule to find. */ - ValidationController.prototype.registerBinding = function (binding, target, rules) { - this.bindings.set(binding, { target: target, rules: rules, propertyInfo: null }); - }; - /** - * Unregisters a binding with the controller. - * @param binding The binding instance. - */ - ValidationController.prototype.unregisterBinding = function (binding) { - this.resetBinding(binding); - this.bindings.delete(binding); - }; - /** - * Interprets the instruction and returns a predicate that will identify - * relevant results in the list of rendered validation results. - */ - ValidationController.prototype.getInstructionPredicate = function (instruction) { - var _this = this; - if (instruction) { - var object_1 = instruction.object, propertyName_1 = instruction.propertyName, rules_1 = instruction.rules; - var predicate_1; - if (instruction.propertyName) { - predicate_1 = function (x) { return x.object === object_1 && x.propertyName === propertyName_1; }; - } - else { - predicate_1 = function (x) { return x.object === object_1; }; - } - if (rules_1) { - return function (x) { return predicate_1(x) && _this.validator.ruleExists(rules_1, x.rule); }; + StandardValidator.prototype.ruleExists = function (rules, rule) { + var i = rules.length; + while (i--) { + if (rules[i].indexOf(rule) !== -1) { + return true; } - return predicate_1; } - else { - return function () { return true; }; + return false; + }; + StandardValidator.prototype.getMessage = function (rule, object, value) { + var expression = rule.message || this.messageProvider.getMessage(rule.messageKey); + // tslint:disable-next-line:prefer-const + var _a = rule.property, propertyName = _a.name, displayName = _a.displayName; + if (propertyName !== null) { + displayName = this.messageProvider.getDisplayName(propertyName, displayName); } + var overrideContext = { + $displayName: displayName, + $propertyName: propertyName, + $value: value, + $object: object, + $config: rule.config, + // returns the name of a given property, given just the property name (irrespective of the property's displayName) + // split on capital letters, first letter ensured to be capitalized + $getDisplayName: this.getDisplayName + }; + return expression.evaluate({ bindingContext: object, overrideContext: overrideContext }, this.lookupFunctions); }; - /** - * Validates and renders results. - * @param instruction Optional. Instructions on what to validate. If undefined, all - * objects and bindings will be validated. - */ - ValidationController.prototype.validate = function (instruction) { + StandardValidator.prototype.validateRuleSequence = function (object, propertyName, ruleSequence, sequence, results) { var _this = this; - // Get a function that will process the validation instruction. - var execute; - if (instruction) { - // tslint:disable-next-line:prefer-const - var object_2 = instruction.object, propertyName_2 = instruction.propertyName, rules_2 = instruction.rules; - // if rules were not specified, check the object map. - rules_2 = rules_2 || this.objects.get(object_2); - // property specified? - if (instruction.propertyName === undefined) { - // validate the specified object. - execute = function () { return _this.validator.validateObject(object_2, rules_2); }; + // are we validating all properties or a single property? + var validateAllProperties = propertyName === null || propertyName === undefined; + var rules = ruleSequence[sequence]; + var allValid = true; + // validate each rule. + var promises = []; + var _loop_1 = function (i) { + var rule = rules[i]; + // is the rule related to the property we're validating. + // tslint:disable-next-line:triple-equals | Use loose equality for property keys + if (!validateAllProperties && rule.property.name != propertyName) { + return "continue"; } - else { - // validate the specified property. - execute = function () { return _this.validator.validateProperty(object_2, propertyName_2, rules_2); }; + // is this a conditional rule? is the condition met? + if (rule.when && !rule.when(object)) { + return "continue"; } + // validate. + var value = rule.property.name === null ? object : object[rule.property.name]; + var promiseOrBoolean = rule.condition(value, object); + if (!(promiseOrBoolean instanceof Promise)) { + promiseOrBoolean = Promise.resolve(promiseOrBoolean); + } + promises.push(promiseOrBoolean.then(function (valid) { + var message = valid ? null : _this.getMessage(rule, object, value); + results.push(new ValidateResult(rule, object, rule.property.name, valid, message)); + allValid = allValid && valid; + return valid; + })); + }; + for (var i = 0; i < rules.length; i++) { + _loop_1(i); } - else { - // validate all objects and bindings. - execute = function () { - var promises = []; - for (var _i = 0, _a = Array.from(_this.objects); _i < _a.length; _i++) { - var _b = _a[_i], object = _b[0], rules = _b[1]; - promises.push(_this.validator.validateObject(object, rules)); - } - for (var _c = 0, _d = Array.from(_this.bindings); _c < _d.length; _c++) { - var _e = _d[_c], binding = _e[0], rules = _e[1].rules; - var propertyInfo = getPropertyInfo(binding.sourceExpression, binding.source); - if (!propertyInfo || _this.objects.has(propertyInfo.object)) { - continue; - } - promises.push(_this.validator.validateProperty(propertyInfo.object, propertyInfo.propertyName, rules)); - } - return Promise.all(promises).then(function (resultSets) { return resultSets.reduce(function (a, b) { return a.concat(b); }, []); }); - }; - } - // Wait for any existing validation to finish, execute the instruction, render the results. - this.validating = true; - var returnPromise = this.finishValidating - .then(execute) - .then(function (newResults) { - var predicate = _this.getInstructionPredicate(instruction); - var oldResults = _this.results.filter(predicate); - _this.processResultDelta('validate', oldResults, newResults); - if (returnPromise === _this.finishValidating) { - _this.validating = false; + return Promise.all(promises) + .then(function () { + sequence++; + if (allValid && sequence < ruleSequence.length) { + return _this.validateRuleSequence(object, propertyName, ruleSequence, sequence, results); } - var result = { - instruction: instruction, - valid: newResults.find(function (x) { return !x.valid; }) === undefined, - results: newResults - }; - _this.invokeCallbacks(instruction, result); - return result; - }) - .catch(function (exception) { - // recover, to enable subsequent calls to validate() - _this.validating = false; - _this.finishValidating = Promise.resolve(); - return Promise.reject(exception); + return results; }); - this.finishValidating = returnPromise; - return returnPromise; }; + 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 || rules.length === 0) { + return Promise.resolve([]); + } + return this.validateRuleSequence(object, propertyName, rules, 0, []); + }; + StandardValidator.inject = [ValidationMessageProvider, ViewResources]; + return StandardValidator; +}(Validator)); + +/** + * Validation triggers. + */ +var validateTrigger; +(function (validateTrigger) { /** - * Resets any rendered validation results (unrenders). - * @param instruction Optional. Instructions on what to reset. If unspecified all rendered results - * will be unrendered. + * Manual validation. Use the controller's `validate()` and `reset()` methods + * to validate all bindings. */ - ValidationController.prototype.reset = function (instruction) { - var predicate = this.getInstructionPredicate(instruction); - var oldResults = this.results.filter(predicate); - this.processResultDelta('reset', oldResults, []); - this.invokeCallbacks(instruction, null); + validateTrigger[validateTrigger["manual"] = 0] = "manual"; + /** + * Validate the binding when the binding's target element fires a DOM "blur" event. + */ + validateTrigger[validateTrigger["blur"] = 1] = "blur"; + /** + * Validate the binding when it updates the model due to a change in the view. + */ + validateTrigger[validateTrigger["change"] = 2] = "change"; + /** + * 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. + */ + validateTrigger[validateTrigger["changeOrBlur"] = 3] = "changeOrBlur"; +})(validateTrigger || (validateTrigger = {})); + +/** + * Aurelia Validation Configuration API + */ +var GlobalValidationConfiguration = /** @class */ (function () { + function GlobalValidationConfiguration() { + this.validatorType = StandardValidator; + this.validationTrigger = GlobalValidationConfiguration.DEFAULT_VALIDATION_TRIGGER; + } + /** + * Use a custom Validator implementation. + */ + GlobalValidationConfiguration.prototype.customValidator = function (type) { + this.validatorType = type; + return this; + }; + GlobalValidationConfiguration.prototype.defaultValidationTrigger = function (trigger) { + this.validationTrigger = trigger; + return this; + }; + GlobalValidationConfiguration.prototype.getDefaultValidationTrigger = function () { + return this.validationTrigger; }; /** - * Gets the elements associated with an object and propertyName (if any). + * Applies the configuration. */ - ValidationController.prototype.getAssociatedElements = function (_a) { - var object = _a.object, propertyName = _a.propertyName; - var elements = []; - for (var _i = 0, _b = Array.from(this.bindings); _i < _b.length; _i++) { - var _c = _b[_i], binding = _c[0], target = _c[1].target; - var propertyInfo = getPropertyInfo(binding.sourceExpression, binding.source); - if (propertyInfo && propertyInfo.object === object && propertyInfo.propertyName === propertyName) { - elements.push(target); + GlobalValidationConfiguration.prototype.apply = function (container) { + var validator = container.get(this.validatorType); + container.registerInstance(Validator, validator); + container.registerInstance(GlobalValidationConfiguration, this); + }; + GlobalValidationConfiguration.DEFAULT_VALIDATION_TRIGGER = validateTrigger.blur; + return GlobalValidationConfiguration; +}()); + +/** + * 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. + */ +function getTargetDOMElement(binding, view) { + var target = binding.target; + // DOM element + if (target instanceof Element) { + return target; + } + // custom element or custom attribute + // tslint:disable-next-line:prefer-const + 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 + "\"."); } - return elements; - }; - ValidationController.prototype.processResultDelta = function (kind, oldResults, newResults) { - // prepare the instruction. - var instruction = { - kind: kind, - render: [], - unrender: [] - }; - // create a shallow copy of newResults so we can mutate it without causing side-effects. - newResults = newResults.slice(0); - var _loop_1 = function (oldResult) { - // get the elements associated with the old result. - var elements = this_1.elements.get(oldResult); - // remove the old result from the element map. - this_1.elements.delete(oldResult); - // create the unrender instruction. - instruction.unrender.push({ result: oldResult, elements: elements }); - // determine if there's a corresponding new result for the old result we are unrendering. - var newResultIndex = newResults.findIndex(function (x) { return x.rule === oldResult.rule && x.object === oldResult.object && x.propertyName === oldResult.propertyName; }); - if (newResultIndex === -1) { - // no corresponding new result... simple remove. - this_1.results.splice(this_1.results.indexOf(oldResult), 1); - if (!oldResult.valid) { - this_1.errors.splice(this_1.errors.indexOf(oldResult), 1); - } - } - else { - // there is a corresponding new result... - var newResult = newResults.splice(newResultIndex, 1)[0]; - // get the elements that are associated with the new result. - var elements_1 = this_1.getAssociatedElements(newResult); - this_1.elements.set(newResult, elements_1); - // create a render instruction for the new result. - instruction.render.push({ result: newResult, elements: elements_1 }); - // do an in-place replacement of the old result with the new result. - // this ensures any repeats bound to this.results will not thrash. - this_1.results.splice(this_1.results.indexOf(oldResult), 1, newResult); - if (!oldResult.valid && newResult.valid) { - this_1.errors.splice(this_1.errors.indexOf(oldResult), 1); - } - else if (!oldResult.valid && !newResult.valid) { - this_1.errors.splice(this_1.errors.indexOf(oldResult), 1, newResult); - } - else if (!newResult.valid) { - this_1.errors.push(newResult); - } - } - }; - var this_1 = this; - // create unrender instructions from the old results. - for (var _i = 0, oldResults_1 = oldResults; _i < oldResults_1.length; _i++) { - var oldResult = oldResults_1[_i]; - _loop_1(oldResult); - } - // create render instructions from the remaining new results. - for (var _a = 0, newResults_1 = newResults; _a < newResults_1.length; _a++) { - var result = newResults_1[_a]; - var elements = this.getAssociatedElements(result); - instruction.render.push({ result: result, elements: elements }); - this.elements.set(result, elements); - this.results.push(result); - if (!result.valid) { - this.errors.push(result); - } + } + throw new Error("Unable to locate target element for \"" + binding.sourceExpression + "\"."); +} + +function getObject(expression, objectExpression, source) { + var value = objectExpression.evaluate(source, null); + if (value === null || value === undefined || value instanceof Object) { + return value; + } + // tslint:disable-next-line:max-line-length + throw new Error("The '" + objectExpression + "' part of '" + expression + "' evaluates to " + value + " instead of an object, null or undefined."); +} +/** + * Retrieves the object and property name for the specified expression. + * @param expression The expression + * @param source The scope + */ +function getPropertyInfo(expression, source) { + var originalExpression = expression; + while (expression instanceof BindingBehavior || expression instanceof ValueConverter) { + expression = expression.expression; + } + var object; + var propertyName; + if (expression instanceof AccessScope) { + object = getContextFor(expression.name, source, expression.ancestor); + propertyName = expression.name; + } + else if (expression instanceof AccessMember) { + object = getObject(originalExpression, expression.object, source); + propertyName = expression.name; + } + else if (expression instanceof AccessKeyed) { + object = getObject(originalExpression, expression.object, source); + propertyName = expression.key.evaluate(source); + } + else { + throw new Error("Expression '" + originalExpression + "' is not compatible with the validate binding-behavior."); + } + if (object === null || object === undefined) { + return null; + } + return { object: object, propertyName: propertyName }; +} + +function isString(value) { + return Object.prototype.toString.call(value) === '[object String]'; +} +function isNumber(value) { + return Object.prototype.toString.call(value) === '[object Number]'; +} + +var PropertyAccessorParser = /** @class */ (function () { + function PropertyAccessorParser(parser) { + this.parser = parser; + } + PropertyAccessorParser.prototype.parse = function (property) { + if (isString(property) || isNumber(property)) { + return property; } - // render. - for (var _b = 0, _c = this.renderers; _b < _c.length; _b++) { - var renderer = _c[_b]; - renderer.render(instruction); + var accessorText = getAccessorExpression(property.toString()); + var accessor = this.parser.parse(accessorText); + if (accessor instanceof AccessScope + || accessor instanceof AccessMember && accessor.object instanceof AccessScope) { + return accessor.name; } + throw new Error("Invalid property expression: \"" + accessor + "\""); }; + PropertyAccessorParser.inject = [Parser]; + return PropertyAccessorParser; +}()); +function getAccessorExpression(fn) { + /* tslint:disable:max-line-length */ + var classic = /^function\s*\([$_\w\d]+\)\s*\{(?:\s*"use strict";)?\s*(?:[$_\w\d.['"\]+;]+)?\s*return\s+[$_\w\d]+\.([$_\w\d]+)\s*;?\s*\}$/; + /* tslint:enable:max-line-length */ + var arrow = /^\(?[$_\w\d]+\)?\s*=>\s*[$_\w\d]+\.([$_\w\d]+)$/; + var match = classic.exec(fn) || arrow.exec(fn); + if (match === null) { + throw new Error("Unable to parse accessor function:\n" + fn); + } + return match[1]; +} + +var ValidateEvent = /** @class */ (function () { + function ValidateEvent( /** - * Validates the property associated with a binding. + * The type of validate event. Either "validate" or "reset". */ - ValidationController.prototype.validateBinding = function (binding) { - if (!binding.isBound) { - return; - } - var propertyInfo = getPropertyInfo(binding.sourceExpression, binding.source); - var rules; - var registeredBinding = this.bindings.get(binding); - if (registeredBinding) { - rules = registeredBinding.rules; - registeredBinding.propertyInfo = propertyInfo; - } - if (!propertyInfo) { - return; - } - var object = propertyInfo.object, propertyName = propertyInfo.propertyName; - this.validate({ object: object, propertyName: propertyName, rules: rules }); - }; + type, /** - * Resets the results for a property associated with a binding. + * The controller's current array of errors. For an array containing both + * failed rules and passed rules, use the "results" property. */ - ValidationController.prototype.resetBinding = function (binding) { - var registeredBinding = this.bindings.get(binding); - var propertyInfo = getPropertyInfo(binding.sourceExpression, binding.source); - if (!propertyInfo && registeredBinding) { - propertyInfo = registeredBinding.propertyInfo; - } - if (registeredBinding) { - registeredBinding.propertyInfo = null; - } - if (!propertyInfo) { - return; - } - var object = propertyInfo.object, propertyName = propertyInfo.propertyName; - this.reset({ object: object, propertyName: propertyName }); - }; + errors, /** - * Changes the controller's validateTrigger. - * @param newTrigger The new validateTrigger + * The controller's current array of validate results. This + * includes both passed rules and failed rules. For an array of only failed rules, + * use the "errors" property. */ - ValidationController.prototype.changeTrigger = function (newTrigger) { - this.validateTrigger = newTrigger; - var bindings = Array.from(this.bindings.keys()); - for (var _i = 0, bindings_1 = bindings; _i < bindings_1.length; _i++) { - var binding = bindings_1[_i]; - var source = binding.source; - binding.unbind(); - binding.bind(source); - } - }; + results, /** - * Revalidates the controller's current set of errors. + * The instruction passed to the "validate" or "reset" event. Will be null when + * the controller's validate/reset method was called with no instruction argument. */ - ValidationController.prototype.revalidateErrors = function () { - for (var _i = 0, _a = this.errors; _i < _a.length; _i++) { - var _b = _a[_i], object = _b.object, propertyName = _b.propertyName, rule = _b.rule; - if (rule.__manuallyAdded__) { - continue; - } - var rules = [[rule]]; - this.validate({ object: object, propertyName: propertyName, rules: rules }); - } - }; - ValidationController.prototype.invokeCallbacks = function (instruction, result) { - if (this.eventCallbacks.length === 0) { - return; - } - var event = new ValidateEvent(result ? 'validate' : 'reset', this.errors, this.results, instruction || null, result); - for (var i = 0; i < this.eventCallbacks.length; i++) { - this.eventCallbacks[i](event); - } - }; - ValidationController.inject = [Validator, PropertyAccessorParser]; - return ValidationController; + instruction, + /** + * In events with type === "validate", this property will contain the result + * of validating the instruction (see "instruction" property). Use the controllerValidateResult + * to access the validate results specific to the call to "validate" + * (as opposed to using the "results" and "errors" properties to access the controller's entire + * set of results/errors). + */ + controllerValidateResult) { + this.type = type; + this.errors = errors; + this.results = results; + this.instruction = instruction; + this.controllerValidateResult = controllerValidateResult; + } + return ValidateEvent; }()); /** - * Binding behavior. Indicates the bound property should be validated. + * Orchestrates validation. + * Manages a set of bindings, renderers and objects. + * Exposes the current list of validation results for binding purposes. */ -var ValidateBindingBehaviorBase = /** @class */ (function () { - function ValidateBindingBehaviorBase(taskQueue) { - this.taskQueue = taskQueue; - } - ValidateBindingBehaviorBase.prototype.bind = function (binding, source, rulesOrController, rules) { - var _this = this; - // identify the target element. - var target = getTargetDOMElement(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); - // tslint:disable-next-line:no-bitwise - if (trigger & validateTrigger.change) { - binding.vbbUpdateSource = binding.updateSource; - // tslint:disable-next-line:only-arrow-functions - // tslint:disable-next-line:space-before-function-paren - binding.updateSource = function (value) { - this.vbbUpdateSource(value); - this.validationController.validateBinding(this); - }; - } - // tslint:disable-next-line:no-bitwise - 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; - // tslint:disable-next-line:only-arrow-functions - // tslint:disable-next-line:space-before-function-paren - binding.updateTarget = function (value) { - this.standardUpdateTarget(value); - this.validationController.resetBinding(this); - }; - } +var ValidationController = /** @class */ (function () { + function ValidationController(validator, propertyParser, config) { + this.validator = validator; + this.propertyParser = propertyParser; + // Registered bindings (via the validate binding behavior) + this.bindings = new Map(); + // Renderers that have been added to the controller instance. + this.renderers = []; + /** + * Validation results that have been rendered by the controller. + */ + this.results = []; + /** + * Validation errors that have been rendered by the controller. + */ + this.errors = []; + /** + * Whether the controller is currently validating. + */ + this.validating = false; + // Elements related to validation results that have been rendered. + this.elements = new Map(); + // Objects that have been added to the controller instance (entity-style validation). + this.objects = new Map(); + // Promise that resolves when validation has completed. + this.finishValidating = Promise.resolve(); + this.eventCallbacks = []; + this.validateTrigger = config instanceof GlobalValidationConfiguration + ? config.getDefaultValidationTrigger() + : GlobalValidationConfiguration.DEFAULT_VALIDATION_TRIGGER; + } + /** + * Subscribe to controller validate and reset events. These events occur when the + * controller's "validate"" and "reset" methods are called. + * @param callback The callback to be invoked when the controller validates or resets. + */ + ValidationController.prototype.subscribe = function (callback) { + var _this = this; + this.eventCallbacks.push(callback); + return { + dispose: function () { + var index = _this.eventCallbacks.indexOf(callback); + if (index === -1) { + return; + } + _this.eventCallbacks.splice(index, 1); + } + }; }; - ValidateBindingBehaviorBase.prototype.unbind = function (binding) { - // reset the binding to it's original state. - if (binding.vbbUpdateSource) { - binding.updateSource = binding.vbbUpdateSource; - binding.vbbUpdateSource = null; - } - if (binding.standardUpdateTarget) { - binding.updateTarget = binding.standardUpdateTarget; - binding.standardUpdateTarget = null; + /** + * Adds an object to the set of objects that should be validated when validate is called. + * @param object The object. + * @param rules Optional. The rules. If rules aren't supplied the Validator implementation will lookup the rules. + */ + ValidationController.prototype.addObject = function (object, rules) { + this.objects.set(object, rules); + }; + /** + * Removes an object from the set of objects that should be validated when validate is called. + * @param object The object. + */ + ValidationController.prototype.removeObject = function (object) { + this.objects.delete(object); + this.processResultDelta('reset', this.results.filter(function (result) { return result.object === object; }), []); + }; + /** + * Adds and renders an error. + */ + ValidationController.prototype.addError = function (message, object, propertyName) { + if (propertyName === void 0) { propertyName = null; } + var resolvedPropertyName; + if (propertyName === null) { + resolvedPropertyName = propertyName; } - if (binding.validateBlurHandler) { - binding.validateTarget.removeEventListener('blur', binding.validateBlurHandler); - binding.validateBlurHandler = null; - binding.validateTarget = null; + else { + resolvedPropertyName = this.propertyParser.parse(propertyName); } - binding.validationController.unregisterBinding(binding); - binding.validationController = null; - }; - return ValidateBindingBehaviorBase; -}()); - -/** - * Binding behavior. Indicates the bound property should be validated - * when the validate trigger specified by the associated controller's - * validateTrigger property occurs. - */ -var ValidateBindingBehavior = /** @class */ (function (_super) { - __extends(ValidateBindingBehavior, _super); - function ValidateBindingBehavior() { - return _super !== null && _super.apply(this, arguments) || this; - } - ValidateBindingBehavior.prototype.getValidateTrigger = function (controller) { - return controller.validateTrigger; + var result = new ValidateResult({ __manuallyAdded__: true }, object, resolvedPropertyName, false, message); + this.processResultDelta('validate', [], [result]); + return result; }; - ValidateBindingBehavior.inject = [TaskQueue]; - ValidateBindingBehavior = __decorate([ - bindingBehavior('validate') - ], ValidateBindingBehavior); - 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. - */ -var ValidateManuallyBindingBehavior = /** @class */ (function (_super) { - __extends(ValidateManuallyBindingBehavior, _super); - function ValidateManuallyBindingBehavior() { - return _super !== null && _super.apply(this, arguments) || this; - } - ValidateManuallyBindingBehavior.prototype.getValidateTrigger = function () { - return validateTrigger.manual; + /** + * Removes and unrenders an error. + */ + ValidationController.prototype.removeError = function (result) { + if (this.results.indexOf(result) !== -1) { + this.processResultDelta('reset', [result], []); + } }; - ValidateManuallyBindingBehavior.inject = [TaskQueue]; - ValidateManuallyBindingBehavior = __decorate([ - bindingBehavior('validateManually') - ], ValidateManuallyBindingBehavior); - return ValidateManuallyBindingBehavior; -}(ValidateBindingBehaviorBase)); -/** - * Binding behavior. Indicates the bound property should be validated - * when the associated element blurs. - */ -var ValidateOnBlurBindingBehavior = /** @class */ (function (_super) { - __extends(ValidateOnBlurBindingBehavior, _super); - function ValidateOnBlurBindingBehavior() { - return _super !== null && _super.apply(this, arguments) || this; - } - ValidateOnBlurBindingBehavior.prototype.getValidateTrigger = function () { - return validateTrigger.blur; + /** + * Adds a renderer. + * @param renderer The renderer. + */ + ValidationController.prototype.addRenderer = function (renderer) { + var _this = this; + this.renderers.push(renderer); + renderer.render({ + kind: 'validate', + render: this.results.map(function (result) { return ({ result: result, elements: _this.elements.get(result) }); }), + unrender: [] + }); }; - ValidateOnBlurBindingBehavior.inject = [TaskQueue]; - ValidateOnBlurBindingBehavior = __decorate([ - bindingBehavior('validateOnBlur') - ], ValidateOnBlurBindingBehavior); - 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. - */ -var ValidateOnChangeBindingBehavior = /** @class */ (function (_super) { - __extends(ValidateOnChangeBindingBehavior, _super); - function ValidateOnChangeBindingBehavior() { - return _super !== null && _super.apply(this, arguments) || this; - } - ValidateOnChangeBindingBehavior.prototype.getValidateTrigger = function () { - return validateTrigger.change; + /** + * Removes a renderer. + * @param renderer The renderer. + */ + ValidationController.prototype.removeRenderer = function (renderer) { + var _this = this; + this.renderers.splice(this.renderers.indexOf(renderer), 1); + renderer.render({ + kind: 'reset', + render: [], + unrender: this.results.map(function (result) { return ({ result: result, elements: _this.elements.get(result) }); }) + }); }; - ValidateOnChangeBindingBehavior.inject = [TaskQueue]; - ValidateOnChangeBindingBehavior = __decorate([ - bindingBehavior('validateOnChange') - ], ValidateOnChangeBindingBehavior); - 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. - */ -var ValidateOnChangeOrBlurBindingBehavior = /** @class */ (function (_super) { - __extends(ValidateOnChangeOrBlurBindingBehavior, _super); - function ValidateOnChangeOrBlurBindingBehavior() { - return _super !== null && _super.apply(this, arguments) || this; - } - ValidateOnChangeOrBlurBindingBehavior.prototype.getValidateTrigger = function () { - return validateTrigger.changeOrBlur; + /** + * Registers a binding with the controller. + * @param binding The binding instance. + * @param target The DOM element. + * @param rules (optional) rules associated with the binding. Validator implementation specific. + */ + ValidationController.prototype.registerBinding = function (binding, target, rules) { + this.bindings.set(binding, { target: target, rules: rules, propertyInfo: null }); }; - ValidateOnChangeOrBlurBindingBehavior.inject = [TaskQueue]; - ValidateOnChangeOrBlurBindingBehavior = __decorate([ - bindingBehavior('validateOnChangeOrBlur') - ], ValidateOnChangeOrBlurBindingBehavior); - return ValidateOnChangeOrBlurBindingBehavior; -}(ValidateBindingBehaviorBase)); - -/** - * Creates ValidationController instances. - */ -var ValidationControllerFactory = /** @class */ (function () { - function ValidationControllerFactory(container) { - this.container = container; - } - ValidationControllerFactory.get = function (container) { - return new ValidationControllerFactory(container); + /** + * Unregisters a binding with the controller. + * @param binding The binding instance. + */ + ValidationController.prototype.unregisterBinding = function (binding) { + this.resetBinding(binding); + this.bindings.delete(binding); }; /** - * Creates a new controller instance. + * Interprets the instruction and returns a predicate that will identify + * relevant results in the list of rendered validation results. */ - ValidationControllerFactory.prototype.create = function (validator) { - if (!validator) { - validator = this.container.get(Validator); + ValidationController.prototype.getInstructionPredicate = function (instruction) { + var _this = this; + if (instruction) { + var object_1 = instruction.object, propertyName_1 = instruction.propertyName, rules_1 = instruction.rules; + var predicate_1; + if (instruction.propertyName) { + predicate_1 = function (x) { return x.object === object_1 && x.propertyName === propertyName_1; }; + } + else { + predicate_1 = function (x) { return x.object === object_1; }; + } + if (rules_1) { + return function (x) { return predicate_1(x) && _this.validator.ruleExists(rules_1, x.rule); }; + } + return predicate_1; + } + else { + return function () { return true; }; } - var propertyParser = this.container.get(PropertyAccessorParser); - return new ValidationController(validator, propertyParser); }; /** - * 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. + * Validates and renders results. + * @param instruction Optional. Instructions on what to validate. If undefined, all + * objects and bindings will be validated. */ - ValidationControllerFactory.prototype.createForCurrentScope = function (validator) { - var controller = this.create(validator); - this.container.registerInstance(ValidationController, controller); - return controller; - }; - return ValidationControllerFactory; -}()); -ValidationControllerFactory['protocol:aurelia:resolver'] = true; - -var ValidationErrorsCustomAttribute = /** @class */ (function () { - function ValidationErrorsCustomAttribute(boundaryElement, controllerAccessor) { - this.boundaryElement = boundaryElement; - this.controllerAccessor = controllerAccessor; - this.controller = null; - this.errors = []; - this.errorsInternal = []; - } - ValidationErrorsCustomAttribute.inject = function () { - return [DOM.Element, Lazy.of(ValidationController)]; - }; - ValidationErrorsCustomAttribute.prototype.sort = function () { - this.errorsInternal.sort(function (a, b) { - if (a.targets[0] === b.targets[0]) { - return 0; + ValidationController.prototype.validate = function (instruction) { + var _this = this; + // Get a function that will process the validation instruction. + var execute; + if (instruction) { + // tslint:disable-next-line:prefer-const + var object_2 = instruction.object, propertyName_2 = instruction.propertyName, rules_2 = instruction.rules; + // if rules were not specified, check the object map. + rules_2 = rules_2 || this.objects.get(object_2); + // property specified? + if (instruction.propertyName === undefined) { + // validate the specified object. + execute = function () { return _this.validator.validateObject(object_2, rules_2); }; } - // tslint:disable-next-line:no-bitwise - return a.targets[0].compareDocumentPosition(b.targets[0]) & 2 ? 1 : -1; + else { + // validate the specified property. + execute = function () { return _this.validator.validateProperty(object_2, propertyName_2, rules_2); }; + } + } + else { + // validate all objects and bindings. + execute = function () { + var promises = []; + for (var _i = 0, _a = Array.from(_this.objects); _i < _a.length; _i++) { + var _b = _a[_i], object = _b[0], rules = _b[1]; + promises.push(_this.validator.validateObject(object, rules)); + } + for (var _c = 0, _d = Array.from(_this.bindings); _c < _d.length; _c++) { + var _e = _d[_c], binding = _e[0], rules = _e[1].rules; + var propertyInfo = getPropertyInfo(binding.sourceExpression, binding.source); + if (!propertyInfo || _this.objects.has(propertyInfo.object)) { + continue; + } + promises.push(_this.validator.validateProperty(propertyInfo.object, propertyInfo.propertyName, rules)); + } + return Promise.all(promises).then(function (resultSets) { return resultSets.reduce(function (a, b) { return a.concat(b); }, []); }); + }; + } + // Wait for any existing validation to finish, execute the instruction, render the results. + this.validating = true; + var returnPromise = this.finishValidating + .then(execute) + .then(function (newResults) { + var predicate = _this.getInstructionPredicate(instruction); + var oldResults = _this.results.filter(predicate); + _this.processResultDelta('validate', oldResults, newResults); + if (returnPromise === _this.finishValidating) { + _this.validating = false; + } + var result = { + instruction: instruction, + valid: newResults.find(function (x) { return !x.valid; }) === undefined, + results: newResults + }; + _this.invokeCallbacks(instruction, result); + return result; + }) + .catch(function (exception) { + // recover, to enable subsequent calls to validate() + _this.validating = false; + _this.finishValidating = Promise.resolve(); + return Promise.reject(exception); }); + this.finishValidating = returnPromise; + return returnPromise; }; - ValidationErrorsCustomAttribute.prototype.interestingElements = function (elements) { - var _this = this; - return elements.filter(function (e) { return _this.boundaryElement.contains(e); }); + /** + * Resets any rendered validation results (unrenders). + * @param instruction Optional. Instructions on what to reset. If unspecified all rendered results + * will be unrendered. + */ + ValidationController.prototype.reset = function (instruction) { + var predicate = this.getInstructionPredicate(instruction); + var oldResults = this.results.filter(predicate); + this.processResultDelta('reset', oldResults, []); + this.invokeCallbacks(instruction, null); + }; + /** + * Gets the elements associated with an object and propertyName (if any). + */ + ValidationController.prototype.getAssociatedElements = function (_a) { + var object = _a.object, propertyName = _a.propertyName; + var elements = []; + for (var _i = 0, _b = Array.from(this.bindings); _i < _b.length; _i++) { + var _c = _b[_i], binding = _c[0], target = _c[1].target; + var propertyInfo = getPropertyInfo(binding.sourceExpression, binding.source); + if (propertyInfo && propertyInfo.object === object && propertyInfo.propertyName === propertyName) { + elements.push(target); + } + } + return elements; }; - ValidationErrorsCustomAttribute.prototype.render = function (instruction) { - var _loop_1 = function (result) { - var index = this_1.errorsInternal.findIndex(function (x) { return x.error === result; }); - if (index !== -1) { - this_1.errorsInternal.splice(index, 1); + ValidationController.prototype.processResultDelta = function (kind, oldResults, newResults) { + // prepare the instruction. + var instruction = { + kind: kind, + render: [], + unrender: [] + }; + // create a shallow copy of newResults so we can mutate it without causing side-effects. + newResults = newResults.slice(0); + var _loop_1 = function (oldResult) { + // get the elements associated with the old result. + var elements = this_1.elements.get(oldResult); + // remove the old result from the element map. + this_1.elements.delete(oldResult); + // create the unrender instruction. + instruction.unrender.push({ result: oldResult, elements: elements }); + // determine if there's a corresponding new result for the old result we are unrendering. + var newResultIndex = newResults.findIndex(function (x) { return x.rule === oldResult.rule && x.object === oldResult.object && x.propertyName === oldResult.propertyName; }); + if (newResultIndex === -1) { + // no corresponding new result... simple remove. + this_1.results.splice(this_1.results.indexOf(oldResult), 1); + if (!oldResult.valid) { + this_1.errors.splice(this_1.errors.indexOf(oldResult), 1); + } + } + else { + // there is a corresponding new result... + var newResult = newResults.splice(newResultIndex, 1)[0]; + // get the elements that are associated with the new result. + var elements_1 = this_1.getAssociatedElements(newResult); + this_1.elements.set(newResult, elements_1); + // create a render instruction for the new result. + instruction.render.push({ result: newResult, elements: elements_1 }); + // do an in-place replacement of the old result with the new result. + // this ensures any repeats bound to this.results will not thrash. + this_1.results.splice(this_1.results.indexOf(oldResult), 1, newResult); + if (!oldResult.valid && newResult.valid) { + this_1.errors.splice(this_1.errors.indexOf(oldResult), 1); + } + else if (!oldResult.valid && !newResult.valid) { + this_1.errors.splice(this_1.errors.indexOf(oldResult), 1, newResult); + } + else if (!newResult.valid) { + this_1.errors.push(newResult); + } } }; var this_1 = this; - for (var _i = 0, _a = instruction.unrender; _i < _a.length; _i++) { - var result = _a[_i].result; - _loop_1(result); + // create unrender instructions from the old results. + for (var _i = 0, oldResults_1 = oldResults; _i < oldResults_1.length; _i++) { + var oldResult = oldResults_1[_i]; + _loop_1(oldResult); } - for (var _b = 0, _c = instruction.render; _b < _c.length; _b++) { - var _d = _c[_b], result = _d.result, elements = _d.elements; - if (result.valid) { - continue; - } - var targets = this.interestingElements(elements); - if (targets.length) { - this.errorsInternal.push({ error: result, targets: targets }); + // create render instructions from the remaining new results. + for (var _a = 0, newResults_1 = newResults; _a < newResults_1.length; _a++) { + var result = newResults_1[_a]; + var elements = this.getAssociatedElements(result); + instruction.render.push({ result: result, elements: elements }); + this.elements.set(result, elements); + this.results.push(result); + if (!result.valid) { + this.errors.push(result); } } - this.sort(); - this.errors = this.errorsInternal; - }; - ValidationErrorsCustomAttribute.prototype.bind = function () { - if (!this.controller) { - this.controller = this.controllerAccessor(); - } - // this will call render() with the side-effect of updating this.errors - this.controller.addRenderer(this); - }; - ValidationErrorsCustomAttribute.prototype.unbind = function () { - if (this.controller) { - this.controller.removeRenderer(this); + // render. + for (var _b = 0, _c = this.renderers; _b < _c.length; _b++) { + var renderer = _c[_b]; + renderer.render(instruction); } }; - __decorate([ - bindable({ defaultBindingMode: bindingMode.oneWay }) - ], ValidationErrorsCustomAttribute.prototype, "controller", void 0); - __decorate([ - bindable({ primaryProperty: true, defaultBindingMode: bindingMode.twoWay }) - ], ValidationErrorsCustomAttribute.prototype, "errors", void 0); - ValidationErrorsCustomAttribute = __decorate([ - customAttribute('validation-errors') - ], ValidationErrorsCustomAttribute); - return ValidationErrorsCustomAttribute; -}()); - -var ValidationRendererCustomAttribute = /** @class */ (function () { - function ValidationRendererCustomAttribute() { - } - ValidationRendererCustomAttribute.prototype.created = function (view) { - this.container = view.container; - }; - ValidationRendererCustomAttribute.prototype.bind = function () { - this.controller = this.container.get(ValidationController); - this.renderer = this.container.get(this.value); - this.controller.addRenderer(this.renderer); - }; - ValidationRendererCustomAttribute.prototype.unbind = function () { - this.controller.removeRenderer(this.renderer); - this.controller = null; - this.renderer = null; - }; - ValidationRendererCustomAttribute = __decorate([ - customAttribute('validation-renderer') - ], ValidationRendererCustomAttribute); - return ValidationRendererCustomAttribute; -}()); - -/** - * Sets, unsets and retrieves rules on an object or constructor function. - */ -var Rules = /** @class */ (function () { - function Rules() { - } /** - * Applies the rules to a target. + * Validates the property associated with a binding. */ - Rules.set = function (target, rules) { - if (target instanceof Function) { - target = target.prototype; + ValidationController.prototype.validateBinding = function (binding) { + if (!binding.isBound) { + return; } - Object.defineProperty(target, Rules.key, { enumerable: false, configurable: false, writable: true, value: rules }); - }; - /** - * Removes rules from a target. - */ - Rules.unset = function (target) { - if (target instanceof Function) { - target = target.prototype; + var propertyInfo = getPropertyInfo(binding.sourceExpression, binding.source); + var rules; + var registeredBinding = this.bindings.get(binding); + if (registeredBinding) { + rules = registeredBinding.rules; + registeredBinding.propertyInfo = propertyInfo; } - target[Rules.key] = null; + if (!propertyInfo) { + return; + } + var object = propertyInfo.object, propertyName = propertyInfo.propertyName; + this.validate({ object: object, propertyName: propertyName, rules: rules }); }; /** - * Retrieves the target's rules. + * Resets the results for a property associated with a binding. */ - Rules.get = function (target) { - return target[Rules.key] || null; + ValidationController.prototype.resetBinding = function (binding) { + var registeredBinding = this.bindings.get(binding); + var propertyInfo = getPropertyInfo(binding.sourceExpression, binding.source); + if (!propertyInfo && registeredBinding) { + propertyInfo = registeredBinding.propertyInfo; + } + if (registeredBinding) { + registeredBinding.propertyInfo = null; + } + if (!propertyInfo) { + return; + } + var object = propertyInfo.object, propertyName = propertyInfo.propertyName; + this.reset({ object: object, propertyName: propertyName }); }; /** - * The name of the property that stores the rules. + * Changes the controller's validateTrigger. + * @param newTrigger The new validateTrigger */ - Rules.key = '__rules__'; - return Rules; -}()); - -// tslint:disable:no-empty -var ExpressionVisitor = /** @class */ (function () { - function ExpressionVisitor() { - } - ExpressionVisitor.prototype.visitChain = function (chain) { - this.visitArgs(chain.expressions); - }; - ExpressionVisitor.prototype.visitBindingBehavior = function (behavior) { - behavior.expression.accept(this); - this.visitArgs(behavior.args); - }; - ExpressionVisitor.prototype.visitValueConverter = function (converter) { - converter.expression.accept(this); - this.visitArgs(converter.args); - }; - ExpressionVisitor.prototype.visitAssign = function (assign) { - assign.target.accept(this); - assign.value.accept(this); - }; - ExpressionVisitor.prototype.visitConditional = function (conditional) { - conditional.condition.accept(this); - conditional.yes.accept(this); - conditional.no.accept(this); - }; - ExpressionVisitor.prototype.visitAccessThis = function (access) { - access.ancestor = access.ancestor; - }; - ExpressionVisitor.prototype.visitAccessScope = function (access) { - access.name = access.name; - }; - ExpressionVisitor.prototype.visitAccessMember = function (access) { - access.object.accept(this); - }; - ExpressionVisitor.prototype.visitAccessKeyed = function (access) { - access.object.accept(this); - access.key.accept(this); - }; - ExpressionVisitor.prototype.visitCallScope = function (call) { - this.visitArgs(call.args); - }; - ExpressionVisitor.prototype.visitCallFunction = function (call) { - call.func.accept(this); - this.visitArgs(call.args); - }; - ExpressionVisitor.prototype.visitCallMember = function (call) { - call.object.accept(this); - this.visitArgs(call.args); - }; - ExpressionVisitor.prototype.visitPrefix = function (prefix) { - prefix.expression.accept(this); - }; - ExpressionVisitor.prototype.visitBinary = function (binary) { - binary.left.accept(this); - binary.right.accept(this); - }; - ExpressionVisitor.prototype.visitLiteralPrimitive = function (literal) { - literal.value = literal.value; - }; - ExpressionVisitor.prototype.visitLiteralArray = function (literal) { - this.visitArgs(literal.elements); - }; - ExpressionVisitor.prototype.visitLiteralObject = function (literal) { - this.visitArgs(literal.values); + ValidationController.prototype.changeTrigger = function (newTrigger) { + this.validateTrigger = newTrigger; + var bindings = Array.from(this.bindings.keys()); + for (var _i = 0, bindings_1 = bindings; _i < bindings_1.length; _i++) { + var binding = bindings_1[_i]; + var source = binding.source; + binding.unbind(); + binding.bind(source); + } }; - ExpressionVisitor.prototype.visitLiteralString = function (literal) { - literal.value = literal.value; + /** + * Revalidates the controller's current set of errors. + */ + ValidationController.prototype.revalidateErrors = function () { + for (var _i = 0, _a = this.errors; _i < _a.length; _i++) { + var _b = _a[_i], object = _b.object, propertyName = _b.propertyName, rule = _b.rule; + if (rule.__manuallyAdded__) { + continue; + } + var rules = [[rule]]; + this.validate({ object: object, propertyName: propertyName, rules: rules }); + } }; - ExpressionVisitor.prototype.visitArgs = function (args) { - for (var i = 0; i < args.length; i++) { - args[i].accept(this); + ValidationController.prototype.invokeCallbacks = function (instruction, result) { + if (this.eventCallbacks.length === 0) { + return; + } + var event = new ValidateEvent(result ? 'validate' : 'reset', this.errors, this.results, instruction || null, result); + for (var i = 0; i < this.eventCallbacks.length; i++) { + this.eventCallbacks[i](event); } }; - return ExpressionVisitor; + ValidationController.inject = [Validator, PropertyAccessorParser, GlobalValidationConfiguration]; + return ValidationController; }()); -var ValidationMessageParser = /** @class */ (function () { - function ValidationMessageParser(bindinqLanguage) { - this.bindinqLanguage = bindinqLanguage; - this.emptyStringExpression = new LiteralString(''); - this.nullExpression = new LiteralPrimitive(null); - this.undefinedExpression = new LiteralPrimitive(undefined); - this.cache = {}; +/** + * Binding behavior. Indicates the bound property should be validated. + */ +var ValidateBindingBehaviorBase = /** @class */ (function () { + function ValidateBindingBehaviorBase(taskQueue) { + this.taskQueue = taskQueue; } - ValidationMessageParser.prototype.parse = function (message) { - if (this.cache[message] !== undefined) { - return this.cache[message]; + ValidateBindingBehaviorBase.prototype.bind = function (binding, source, rulesOrController, rules) { + var _this = this; + // identify the target element. + var target = getTargetDOMElement(binding, source); + // locate the controller. + var controller; + if (rulesOrController instanceof ValidationController) { + controller = rulesOrController; } - var parts = this.bindinqLanguage.parseInterpolation(null, message); - if (parts === null) { - return new LiteralString(message); + else { + controller = source.container.get(Optional.of(ValidationController)); + rules = rulesOrController; } - var expression = new LiteralString(parts[0]); - for (var i = 1; i < parts.length; i += 2) { - expression = new Binary('+', expression, new Binary('+', this.coalesce(parts[i]), new LiteralString(parts[i + 1]))); + 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); + // tslint:disable-next-line:no-bitwise + if (trigger & validateTrigger.change) { + binding.vbbUpdateSource = binding.updateSource; + // tslint:disable-next-line:only-arrow-functions + // tslint:disable-next-line:space-before-function-paren + binding.updateSource = function (value) { + this.vbbUpdateSource(value); + this.validationController.validateBinding(this); + }; + } + // tslint:disable-next-line:no-bitwise + 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; + // tslint:disable-next-line:only-arrow-functions + // tslint:disable-next-line:space-before-function-paren + binding.updateTarget = function (value) { + this.standardUpdateTarget(value); + this.validationController.resetBinding(this); + }; } - MessageExpressionValidator.validate(expression, message); - this.cache[message] = expression; - return expression; - }; - ValidationMessageParser.prototype.coalesce = function (part) { - // part === null || part === undefined ? '' : part - return new Conditional(new Binary('||', new Binary('===', part, this.nullExpression), new Binary('===', part, this.undefinedExpression)), this.emptyStringExpression, new CallMember(part, 'toString', [])); - }; - ValidationMessageParser.inject = [BindingLanguage]; - return ValidationMessageParser; -}()); -var MessageExpressionValidator = /** @class */ (function (_super) { - __extends(MessageExpressionValidator, _super); - function MessageExpressionValidator(originalMessage) { - var _this = _super.call(this) || this; - _this.originalMessage = originalMessage; - return _this; - } - MessageExpressionValidator.validate = function (expression, originalMessage) { - var visitor = new MessageExpressionValidator(originalMessage); - expression.accept(visitor); }; - MessageExpressionValidator.prototype.visitAccessScope = function (access) { - if (access.ancestor !== 0) { - throw new Error('$parent is not permitted in validation message expressions.'); + ValidateBindingBehaviorBase.prototype.unbind = function (binding) { + // reset the binding to it's original state. + if (binding.vbbUpdateSource) { + binding.updateSource = binding.vbbUpdateSource; + binding.vbbUpdateSource = null; } - if (['displayName', 'propertyName', 'value', 'object', 'config', 'getDisplayName'].indexOf(access.name) !== -1) { - getLogger('aurelia-validation') - // tslint:disable-next-line:max-line-length - .warn("Did you mean to use \"$" + access.name + "\" instead of \"" + access.name + "\" in this validation message template: \"" + this.originalMessage + "\"?"); + 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 MessageExpressionValidator; -}(ExpressionVisitor)); + return ValidateBindingBehaviorBase; +}()); /** - * Dictionary of validation messages. [messageKey]: messageExpression + * Binding behavior. Indicates the bound property should be validated + * when the validate trigger specified by the associated controller's + * validateTrigger property occurs. */ -var validationMessages = { - /** - * The default validation message. Used with rules that have no standard message. - */ - default: "${$displayName} is invalid.", - required: "${$displayName} is required.", - matches: "${$displayName} is not correctly formatted.", - email: "${$displayName} is not a valid email.", - minLength: "${$displayName} must be at least ${$config.length} character${$config.length === 1 ? '' : 's'}.", - maxLength: "${$displayName} cannot be longer than ${$config.length} character${$config.length === 1 ? '' : 's'}.", - minItems: "${$displayName} must contain at least ${$config.count} item${$config.count === 1 ? '' : 's'}.", - maxItems: "${$displayName} cannot contain more than ${$config.count} item${$config.count === 1 ? '' : 's'}.", - min: "${$displayName} must be at least ${$config.constraint}.", - max: "${$displayName} must be at most ${$config.constraint}.", - range: "${$displayName} must be between or equal to ${$config.min} and ${$config.max}.", - between: "${$displayName} must be between but not equal to ${$config.min} and ${$config.max}.", - equals: "${$displayName} must be ${$config.expectedValue}.", -}; +var ValidateBindingBehavior = /** @class */ (function (_super) { + __extends(ValidateBindingBehavior, _super); + function ValidateBindingBehavior() { + return _super !== null && _super.apply(this, arguments) || this; + } + ValidateBindingBehavior.prototype.getValidateTrigger = function (controller) { + return controller.validateTrigger; + }; + ValidateBindingBehavior.inject = [TaskQueue]; + ValidateBindingBehavior = __decorate([ + bindingBehavior('validate') + ], ValidateBindingBehavior); + return ValidateBindingBehavior; +}(ValidateBindingBehaviorBase)); /** - * Retrieves validation messages and property display names. + * 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 ValidationMessageProvider = /** @class */ (function () { - function ValidationMessageProvider(parser) { - this.parser = parser; +var ValidateManuallyBindingBehavior = /** @class */ (function (_super) { + __extends(ValidateManuallyBindingBehavior, _super); + function ValidateManuallyBindingBehavior() { + return _super !== null && _super.apply(this, arguments) || this; } - /** - * Returns a message binding expression that corresponds to the key. - * @param key The message key. - */ - ValidationMessageProvider.prototype.getMessage = function (key) { - var message; - if (key in validationMessages) { - message = validationMessages[key]; - } - else { - message = validationMessages['default']; - } - return this.parser.parse(message); + ValidateManuallyBindingBehavior.prototype.getValidateTrigger = function () { + return validateTrigger.manual; }; - /** - * Formulates a property display name using the property name and the configured - * displayName (if provided). - * Override this with your own custom logic. - * @param propertyName The property name. - */ - ValidationMessageProvider.prototype.getDisplayName = function (propertyName, displayName) { - if (displayName !== null && displayName !== undefined) { - return (displayName instanceof Function) ? displayName() : displayName; - } - // split on upper-case letters. - var words = propertyName.toString().split(/(?=[A-Z])/).join(' '); - // capitalize first letter. - return words.charAt(0).toUpperCase() + words.slice(1); + ValidateManuallyBindingBehavior.inject = [TaskQueue]; + ValidateManuallyBindingBehavior = __decorate([ + bindingBehavior('validateManually') + ], ValidateManuallyBindingBehavior); + return ValidateManuallyBindingBehavior; +}(ValidateBindingBehaviorBase)); +/** + * Binding behavior. Indicates the bound property should be validated + * when the associated element blurs. + */ +var ValidateOnBlurBindingBehavior = /** @class */ (function (_super) { + __extends(ValidateOnBlurBindingBehavior, _super); + function ValidateOnBlurBindingBehavior() { + return _super !== null && _super.apply(this, arguments) || this; + } + ValidateOnBlurBindingBehavior.prototype.getValidateTrigger = function () { + return validateTrigger.blur; + }; + ValidateOnBlurBindingBehavior.inject = [TaskQueue]; + ValidateOnBlurBindingBehavior = __decorate([ + bindingBehavior('validateOnBlur') + ], ValidateOnBlurBindingBehavior); + 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. + */ +var ValidateOnChangeBindingBehavior = /** @class */ (function (_super) { + __extends(ValidateOnChangeBindingBehavior, _super); + function ValidateOnChangeBindingBehavior() { + return _super !== null && _super.apply(this, arguments) || this; + } + ValidateOnChangeBindingBehavior.prototype.getValidateTrigger = function () { + return validateTrigger.change; + }; + ValidateOnChangeBindingBehavior.inject = [TaskQueue]; + ValidateOnChangeBindingBehavior = __decorate([ + bindingBehavior('validateOnChange') + ], ValidateOnChangeBindingBehavior); + 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. + */ +var ValidateOnChangeOrBlurBindingBehavior = /** @class */ (function (_super) { + __extends(ValidateOnChangeOrBlurBindingBehavior, _super); + function ValidateOnChangeOrBlurBindingBehavior() { + return _super !== null && _super.apply(this, arguments) || this; + } + ValidateOnChangeOrBlurBindingBehavior.prototype.getValidateTrigger = function () { + return validateTrigger.changeOrBlur; }; - ValidationMessageProvider.inject = [ValidationMessageParser]; - return ValidationMessageProvider; -}()); + ValidateOnChangeOrBlurBindingBehavior.inject = [TaskQueue]; + ValidateOnChangeOrBlurBindingBehavior = __decorate([ + bindingBehavior('validateOnChangeOrBlur') + ], ValidateOnChangeOrBlurBindingBehavior); + return ValidateOnChangeOrBlurBindingBehavior; +}(ValidateBindingBehaviorBase)); /** - * Validates. - * Responsible for validating objects and properties. + * Creates ValidationController instances. */ -var StandardValidator = /** @class */ (function (_super) { - __extends(StandardValidator, _super); - function StandardValidator(messageProvider, resources) { - var _this = _super.call(this) || this; - _this.messageProvider = messageProvider; - _this.lookupFunctions = resources.lookupFunctions; - _this.getDisplayName = messageProvider.getDisplayName.bind(messageProvider); - return _this; +var ValidationControllerFactory = /** @class */ (function () { + function ValidationControllerFactory(container) { + this.container = container; } - /** - * Validates the specified property. - * @param object The object to validate. - * @param propertyName The name of the property to validate. - * @param rules Optional. If unspecified, the rules will be looked up using the metadata - * for the object created by ValidationRules....on(class/object) - */ - StandardValidator.prototype.validateProperty = function (object, propertyName, rules) { - return this.validate(object, propertyName, rules || null); + ValidationControllerFactory.get = function (container) { + return new ValidationControllerFactory(container); }; /** - * Validates all rules for specified object and it's properties. - * @param object The object to validate. - * @param rules Optional. If unspecified, the rules will be looked up using the metadata - * for the object created by ValidationRules....on(class/object) + * Creates a new controller instance. */ - StandardValidator.prototype.validateObject = function (object, rules) { - return this.validate(object, null, rules || null); + ValidationControllerFactory.prototype.create = function (validator) { + if (!validator) { + validator = this.container.get(Validator); + } + var propertyParser = this.container.get(PropertyAccessorParser); + var config = this.container.get(GlobalValidationConfiguration); + return new ValidationController(validator, propertyParser, config); }; /** - * Determines whether a rule exists in a set of rules. - * @param rules The rules to search. - * @parem rule The rule to find. + * 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. */ - StandardValidator.prototype.ruleExists = function (rules, rule) { - var i = rules.length; - while (i--) { - if (rules[i].indexOf(rule) !== -1) { - return true; - } - } - return false; + ValidationControllerFactory.prototype.createForCurrentScope = function (validator) { + var controller = this.create(validator); + this.container.registerInstance(ValidationController, controller); + return controller; }; - StandardValidator.prototype.getMessage = function (rule, object, value) { - var expression = rule.message || this.messageProvider.getMessage(rule.messageKey); - // tslint:disable-next-line:prefer-const - var _a = rule.property, propertyName = _a.name, displayName = _a.displayName; - if (propertyName !== null) { - displayName = this.messageProvider.getDisplayName(propertyName, displayName); - } - var overrideContext = { - $displayName: displayName, - $propertyName: propertyName, - $value: value, - $object: object, - $config: rule.config, - // returns the name of a given property, given just the property name (irrespective of the property's displayName) - // split on capital letters, first letter ensured to be capitalized - $getDisplayName: this.getDisplayName - }; - return expression.evaluate({ bindingContext: object, overrideContext: overrideContext }, this.lookupFunctions); + return ValidationControllerFactory; +}()); +ValidationControllerFactory['protocol:aurelia:resolver'] = true; + +var ValidationErrorsCustomAttribute = /** @class */ (function () { + function ValidationErrorsCustomAttribute(boundaryElement, controllerAccessor) { + this.boundaryElement = boundaryElement; + this.controllerAccessor = controllerAccessor; + this.controller = null; + this.errors = []; + this.errorsInternal = []; + } + ValidationErrorsCustomAttribute.inject = function () { + return [DOM.Element, Lazy.of(ValidationController)]; }; - StandardValidator.prototype.validateRuleSequence = function (object, propertyName, ruleSequence, sequence, results) { - var _this = this; - // are we validating all properties or a single property? - var validateAllProperties = propertyName === null || propertyName === undefined; - var rules = ruleSequence[sequence]; - var allValid = true; - // validate each rule. - var promises = []; - var _loop_1 = function (i) { - var rule = rules[i]; - // is the rule related to the property we're validating. - // tslint:disable-next-line:triple-equals | Use loose equality for property keys - if (!validateAllProperties && rule.property.name != propertyName) { - return "continue"; - } - // is this a conditional rule? is the condition met? - if (rule.when && !rule.when(object)) { - return "continue"; + ValidationErrorsCustomAttribute.prototype.sort = function () { + this.errorsInternal.sort(function (a, b) { + if (a.targets[0] === b.targets[0]) { + return 0; } - // validate. - var value = rule.property.name === null ? object : object[rule.property.name]; - var promiseOrBoolean = rule.condition(value, object); - if (!(promiseOrBoolean instanceof Promise)) { - promiseOrBoolean = Promise.resolve(promiseOrBoolean); + // tslint:disable-next-line:no-bitwise + return a.targets[0].compareDocumentPosition(b.targets[0]) & 2 ? 1 : -1; + }); + }; + ValidationErrorsCustomAttribute.prototype.interestingElements = function (elements) { + var _this = this; + return elements.filter(function (e) { return _this.boundaryElement.contains(e); }); + }; + ValidationErrorsCustomAttribute.prototype.render = function (instruction) { + var _loop_1 = function (result) { + var index = this_1.errorsInternal.findIndex(function (x) { return x.error === result; }); + if (index !== -1) { + this_1.errorsInternal.splice(index, 1); } - promises.push(promiseOrBoolean.then(function (valid) { - var message = valid ? null : _this.getMessage(rule, object, value); - results.push(new ValidateResult(rule, object, rule.property.name, valid, message)); - allValid = allValid && valid; - return valid; - })); }; - for (var i = 0; i < rules.length; i++) { - _loop_1(i); + var this_1 = this; + for (var _i = 0, _a = instruction.unrender; _i < _a.length; _i++) { + var result = _a[_i].result; + _loop_1(result); } - return Promise.all(promises) - .then(function () { - sequence++; - if (allValid && sequence < ruleSequence.length) { - return _this.validateRuleSequence(object, propertyName, ruleSequence, sequence, results); + for (var _b = 0, _c = instruction.render; _b < _c.length; _b++) { + var _d = _c[_b], result = _d.result, elements = _d.elements; + if (result.valid) { + continue; } - return results; - }); + var targets = this.interestingElements(elements); + if (targets.length) { + this.errorsInternal.push({ error: result, targets: targets }); + } + } + this.sort(); + this.errors = this.errorsInternal; }; - StandardValidator.prototype.validate = function (object, propertyName, rules) { - // rules specified? - if (!rules) { - // no. attempt to locate the rules. - rules = Rules.get(object); + ValidationErrorsCustomAttribute.prototype.bind = function () { + if (!this.controller) { + this.controller = this.controllerAccessor(); } - // any rules? - if (!rules || rules.length === 0) { - return Promise.resolve([]); + // this will call render() with the side-effect of updating this.errors + this.controller.addRenderer(this); + }; + ValidationErrorsCustomAttribute.prototype.unbind = function () { + if (this.controller) { + this.controller.removeRenderer(this); } - return this.validateRuleSequence(object, propertyName, rules, 0, []); }; - StandardValidator.inject = [ValidationMessageProvider, ViewResources]; - return StandardValidator; -}(Validator)); + __decorate([ + bindable({ defaultBindingMode: bindingMode.oneWay }) + ], ValidationErrorsCustomAttribute.prototype, "controller", void 0); + __decorate([ + bindable({ primaryProperty: true, defaultBindingMode: bindingMode.twoWay }) + ], ValidationErrorsCustomAttribute.prototype, "errors", void 0); + ValidationErrorsCustomAttribute = __decorate([ + customAttribute('validation-errors') + ], ValidationErrorsCustomAttribute); + return ValidationErrorsCustomAttribute; +}()); + +var ValidationRendererCustomAttribute = /** @class */ (function () { + function ValidationRendererCustomAttribute() { + } + ValidationRendererCustomAttribute.prototype.created = function (view) { + this.container = view.container; + }; + ValidationRendererCustomAttribute.prototype.bind = function () { + this.controller = this.container.get(ValidationController); + this.renderer = this.container.get(this.value); + this.controller.addRenderer(this.renderer); + }; + ValidationRendererCustomAttribute.prototype.unbind = function () { + this.controller.removeRenderer(this.renderer); + this.controller = null; + this.renderer = null; + }; + ValidationRendererCustomAttribute = __decorate([ + customAttribute('validation-renderer') + ], ValidationRendererCustomAttribute); + return ValidationRendererCustomAttribute; +}()); /** * Part of the fluent rule API. Enables customizing property rules. @@ -1413,12 +1455,12 @@ var FluentRuleCustomizer = /** @class */ (function () { * @param args The rule's arguments. */ FluentRuleCustomizer.prototype.satisfiesRule = function (name) { + var _a; var args = []; for (var _i = 1; _i < arguments.length; _i++) { args[_i - 1] = arguments[_i]; } - var _a; - return (_a = this.fluentRules).satisfiesRule.apply(_a, [name].concat(args)); + return (_a = this.fluentRules).satisfiesRule.apply(_a, __spreadArrays([name], args)); }; /** * Applies the "required" rule to the property. @@ -1558,14 +1600,14 @@ var FluentRules = /** @class */ (function () { // standard rule? rule = this[name]; if (rule instanceof Function) { - return rule.call.apply(rule, [this].concat(args)); + return rule.call.apply(rule, __spreadArrays([this], args)); } throw new Error("Rule with name \"" + name + "\" does not exist."); } var config = rule.argsToConfig ? rule.argsToConfig.apply(rule, args) : undefined; return this.satisfies(function (value, obj) { var _a; - return (_a = rule.condition).call.apply(_a, [_this, value, obj].concat(args)); + return (_a = rule.condition).call.apply(_a, __spreadArrays([_this, value, obj], args)); }, config) .withMessageKey(name); }; @@ -1810,28 +1852,6 @@ var ValidationRules = /** @class */ (function () { }()); // Exports -/** - * Aurelia Validation Configuration API - */ -var AureliaValidationConfiguration = /** @class */ (function () { - function AureliaValidationConfiguration() { - this.validatorType = StandardValidator; - } - /** - * Use a custom Validator implementation. - */ - AureliaValidationConfiguration.prototype.customValidator = function (type) { - this.validatorType = type; - }; - /** - * Applies the configuration. - */ - AureliaValidationConfiguration.prototype.apply = function (container) { - var validator = container.get(this.validatorType); - container.registerInstance(Validator, validator); - }; - return AureliaValidationConfiguration; -}()); /** * Configures the plugin. */ @@ -1844,7 +1864,7 @@ frameworkConfig, callback) { var propertyParser = frameworkConfig.container.get(PropertyAccessorParser); ValidationRules.initialize(messageParser, propertyParser); // configure... - var config = new AureliaValidationConfiguration(); + var config = new GlobalValidationConfiguration(); if (callback instanceof Function) { callback(config); } @@ -1855,4 +1875,4 @@ frameworkConfig, callback) { } } -export { AureliaValidationConfiguration, configure, getTargetDOMElement, getPropertyInfo, PropertyAccessorParser, getAccessorExpression, ValidateBindingBehavior, ValidateManuallyBindingBehavior, ValidateOnBlurBindingBehavior, ValidateOnChangeBindingBehavior, ValidateOnChangeOrBlurBindingBehavior, ValidateEvent, ValidateResult, validateTrigger, ValidationController, ValidationControllerFactory, ValidationErrorsCustomAttribute, ValidationRendererCustomAttribute, Validator, Rules, StandardValidator, validationMessages, ValidationMessageProvider, ValidationMessageParser, MessageExpressionValidator, FluentRuleCustomizer, FluentRules, FluentEnsure, ValidationRules }; +export { configure, GlobalValidationConfiguration, getTargetDOMElement, getPropertyInfo, PropertyAccessorParser, getAccessorExpression, ValidateBindingBehavior, ValidateManuallyBindingBehavior, ValidateOnBlurBindingBehavior, ValidateOnChangeBindingBehavior, ValidateOnChangeOrBlurBindingBehavior, ValidateEvent, ValidateResult, validateTrigger, ValidationController, ValidationControllerFactory, ValidationErrorsCustomAttribute, ValidationRendererCustomAttribute, Validator, Rules, StandardValidator, validationMessages, ValidationMessageProvider, ValidationMessageParser, MessageExpressionValidator, FluentRuleCustomizer, FluentRules, FluentEnsure, ValidationRules }; diff --git a/dist/system/aurelia-validation.js b/dist/system/aurelia-validation.js index a48f59df..44730c6b 100644 --- a/dist/system/aurelia-validation.js +++ b/dist/system/aurelia-validation.js @@ -1,10 +1,13 @@ -System.register(['aurelia-pal', 'aurelia-binding', 'aurelia-dependency-injection', 'aurelia-task-queue', 'aurelia-templating', 'aurelia-logging'], function (exports, module) { +System.register(['aurelia-binding', 'aurelia-templating', 'aurelia-logging', 'aurelia-pal', 'aurelia-dependency-injection', 'aurelia-task-queue'], function (exports, module) { 'use strict'; - var DOM, AccessMember, AccessScope, AccessKeyed, BindingBehavior, ValueConverter, getContextFor, Parser, bindingBehavior, bindingMode, LiteralString, Binary, Conditional, LiteralPrimitive, CallMember, Optional, Lazy, TaskQueue, customAttribute, bindable, BindingLanguage, ViewResources, getLogger; + var LiteralString, Binary, Conditional, LiteralPrimitive, CallMember, AccessMember, AccessScope, AccessKeyed, BindingBehavior, ValueConverter, getContextFor, Parser, bindingBehavior, bindingMode, BindingLanguage, ViewResources, customAttribute, bindable, getLogger, DOM, Optional, Lazy, TaskQueue; return { setters: [function (module) { - DOM = module.DOM; - }, function (module) { + LiteralString = module.LiteralString; + Binary = module.Binary; + Conditional = module.Conditional; + LiteralPrimitive = module.LiteralPrimitive; + CallMember = module.CallMember; AccessMember = module.AccessMember; AccessScope = module.AccessScope; AccessKeyed = module.AccessKeyed; @@ -14,23 +17,20 @@ System.register(['aurelia-pal', 'aurelia-binding', 'aurelia-dependency-injection Parser = module.Parser; bindingBehavior = module.bindingBehavior; bindingMode = module.bindingMode; - LiteralString = module.LiteralString; - Binary = module.Binary; - Conditional = module.Conditional; - LiteralPrimitive = module.LiteralPrimitive; - CallMember = module.CallMember; - }, function (module) { - Optional = module.Optional; - Lazy = module.Lazy; - }, function (module) { - TaskQueue = module.TaskQueue; }, function (module) { - customAttribute = module.customAttribute; - bindable = module.bindable; BindingLanguage = module.BindingLanguage; ViewResources = module.ViewResources; + customAttribute = module.customAttribute; + bindable = module.bindable; }, function (module) { getLogger = module.getLogger; + }, function (module) { + DOM = module.DOM; + }, function (module) { + Optional = module.Optional; + Lazy = module.Lazy; + }, function (module) { + TaskQueue = module.TaskQueue; }], execute: function () { @@ -43,110 +43,13 @@ System.register(['aurelia-pal', 'aurelia-binding', 'aurelia-dependency-injection }); /** - * 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. - */ - function getTargetDOMElement(binding, view) { - var target = binding.target; - // DOM element - if (target instanceof Element) { - return target; - } - // custom element or custom attribute - // tslint:disable-next-line:prefer-const - 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 + "\"."); - } - - function getObject(expression, objectExpression, source) { - var value = objectExpression.evaluate(source, null); - if (value === null || value === undefined || value instanceof Object) { - return value; - } - // tslint:disable-next-line:max-line-length - throw new Error("The '" + objectExpression + "' part of '" + expression + "' evaluates to " + value + " instead of an object, null or undefined."); - } - /** - * Retrieves the object and property name for the specified expression. - * @param expression The expression - * @param source The scope + * Validates objects and properties. */ - function getPropertyInfo(expression, source) { - var originalExpression = expression; - while (expression instanceof BindingBehavior || expression instanceof ValueConverter) { - expression = expression.expression; - } - var object; - var propertyName; - if (expression instanceof AccessScope) { - object = getContextFor(expression.name, source, expression.ancestor); - propertyName = expression.name; - } - else if (expression instanceof AccessMember) { - object = getObject(originalExpression, expression.object, source); - propertyName = expression.name; - } - else if (expression instanceof AccessKeyed) { - object = getObject(originalExpression, expression.object, source); - propertyName = expression.key.evaluate(source); - } - else { - throw new Error("Expression '" + originalExpression + "' is not compatible with the validate binding-behavior."); - } - if (object === null || object === undefined) { - return null; - } - return { object: object, propertyName: propertyName }; - } - - function isString(value) { - return Object.prototype.toString.call(value) === '[object String]'; - } - function isNumber(value) { - return Object.prototype.toString.call(value) === '[object Number]'; - } - - var PropertyAccessorParser = exports('PropertyAccessorParser', /** @class */ (function () { - function PropertyAccessorParser(parser) { - this.parser = parser; - } - PropertyAccessorParser.prototype.parse = function (property) { - if (isString(property) || isNumber(property)) { - return property; - } - var accessorText = getAccessorExpression(property.toString()); - var accessor = this.parser.parse(accessorText); - if (accessor instanceof AccessScope - || accessor instanceof AccessMember && accessor.object instanceof AccessScope) { - return accessor.name; - } - throw new Error("Invalid property expression: \"" + accessor + "\""); - }; - PropertyAccessorParser.inject = [Parser]; - return PropertyAccessorParser; - }())); - function getAccessorExpression(fn) { - /* tslint:disable:max-line-length */ - var classic = /^function\s*\([$_\w\d]+\)\s*\{(?:\s*"use strict";)?\s*(?:[$_\w\d.['"\]+;]+)?\s*return\s+[$_\w\d]+\.([$_\w\d]+)\s*;?\s*\}$/; - /* tslint:enable:max-line-length */ - var arrow = /^\(?[$_\w\d]+\)?\s*=>\s*[$_\w\d]+\.([$_\w\d]+)$/; - var match = classic.exec(fn) || arrow.exec(fn); - if (match === null) { - throw new Error("Unable to parse accessor function:\n" + fn); + var Validator = exports('Validator', /** @class */ (function () { + function Validator() { } - return match[1]; - } + return Validator; + }())); /*! ***************************************************************************** Copyright (c) Microsoft Corporation. All rights reserved. @@ -182,42 +85,16 @@ System.register(['aurelia-pal', 'aurelia-binding', 'aurelia-dependency-injection if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc); else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r; return c > 3 && r && Object.defineProperty(target, key, r), r; + } + + function __spreadArrays() { + for (var s = 0, i = 0, il = arguments.length; i < il; i++) s += arguments[i].length; + for (var r = Array(s), k = 0, i = 0; i < il; i++) + for (var a = arguments[i], j = 0, jl = a.length; j < jl; j++, k++) + r[k] = a[j]; + return r; } - /** - * Validation triggers. - */ - var validateTrigger; - (function (validateTrigger) { - /** - * Manual validation. Use the controller's `validate()` and `reset()` methods - * to validate all bindings. - */ - validateTrigger[validateTrigger["manual"] = 0] = "manual"; - /** - * Validate the binding when the binding's target element fires a DOM "blur" event. - */ - validateTrigger[validateTrigger["blur"] = 1] = "blur"; - /** - * Validate the binding when it updates the model due to a change in the view. - */ - validateTrigger[validateTrigger["change"] = 2] = "change"; - /** - * 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. - */ - validateTrigger[validateTrigger["changeOrBlur"] = 3] = "changeOrBlur"; - })(validateTrigger || (validateTrigger = exports('validateTrigger', {}))); - - /** - * Validates objects and properties. - */ - var Validator = exports('Validator', /** @class */ (function () { - function Validator() { - } - return Validator; - }())); - /** * The result of validating an individual validation rule. */ @@ -244,1102 +121,1267 @@ System.register(['aurelia-pal', 'aurelia-binding', 'aurelia-dependency-injection return ValidateResult; }())); - var ValidateEvent = exports('ValidateEvent', /** @class */ (function () { - function ValidateEvent( - /** - * The type of validate event. Either "validate" or "reset". - */ - type, + /** + * Sets, unsets and retrieves rules on an object or constructor function. + */ + var Rules = exports('Rules', /** @class */ (function () { + function Rules() { + } /** - * The controller's current array of errors. For an array containing both - * failed rules and passed rules, use the "results" property. + * Applies the rules to a target. */ - errors, + Rules.set = function (target, rules) { + if (target instanceof Function) { + target = target.prototype; + } + Object.defineProperty(target, Rules.key, { enumerable: false, configurable: false, writable: true, value: rules }); + }; /** - * The controller's current array of validate results. This - * includes both passed rules and failed rules. For an array of only failed rules, - * use the "errors" property. + * Removes rules from a target. */ - results, + Rules.unset = function (target) { + if (target instanceof Function) { + target = target.prototype; + } + target[Rules.key] = null; + }; /** - * The instruction passed to the "validate" or "reset" event. Will be null when - * the controller's validate/reset method was called with no instruction argument. + * Retrieves the target's rules. */ - instruction, + Rules.get = function (target) { + return target[Rules.key] || null; + }; /** - * In events with type === "validate", this property will contain the result - * of validating the instruction (see "instruction" property). Use the controllerValidateResult - * to access the validate results specific to the call to "validate" - * (as opposed to using the "results" and "errors" properties to access the controller's entire - * set of results/errors). + * The name of the property that stores the rules. */ - controllerValidateResult) { - this.type = type; - this.errors = errors; - this.results = results; - this.instruction = instruction; - this.controllerValidateResult = controllerValidateResult; - } - return ValidateEvent; + Rules.key = '__rules__'; + return Rules; }())); - /** - * Orchestrates validation. - * Manages a set of bindings, renderers and objects. - * Exposes the current list of validation results for binding purposes. - */ - var ValidationController = exports('ValidationController', /** @class */ (function () { - function ValidationController(validator, propertyParser) { - this.validator = validator; - this.propertyParser = propertyParser; - // Registered bindings (via the validate binding behavior) - this.bindings = new Map(); - // Renderers that have been added to the controller instance. - this.renderers = []; - /** - * Validation results that have been rendered by the controller. - */ - this.results = []; - /** - * Validation errors that have been rendered by the controller. - */ - this.errors = []; - /** - * Whether the controller is currently validating. - */ - this.validating = false; - // Elements related to validation results that have been rendered. - this.elements = new Map(); - // Objects that have been added to the controller instance (entity-style validation). - this.objects = new Map(); - /** - * The trigger that will invoke automatic validation of a property used in a binding. - */ - this.validateTrigger = validateTrigger.blur; - // Promise that resolves when validation has completed. - this.finishValidating = Promise.resolve(); - this.eventCallbacks = []; + // tslint:disable:no-empty + var ExpressionVisitor = /** @class */ (function () { + function ExpressionVisitor() { } - /** - * Subscribe to controller validate and reset events. These events occur when the - * controller's "validate"" and "reset" methods are called. - * @param callback The callback to be invoked when the controller validates or resets. - */ - ValidationController.prototype.subscribe = function (callback) { - var _this = this; - this.eventCallbacks.push(callback); - return { - dispose: function () { - var index = _this.eventCallbacks.indexOf(callback); - if (index === -1) { - return; - } - _this.eventCallbacks.splice(index, 1); - } - }; + ExpressionVisitor.prototype.visitChain = function (chain) { + this.visitArgs(chain.expressions); }; - /** - * Adds an object to the set of objects that should be validated when validate is called. - * @param object The object. - * @param rules Optional. The rules. If rules aren't supplied the Validator implementation will lookup the rules. - */ - ValidationController.prototype.addObject = function (object, rules) { - this.objects.set(object, rules); + ExpressionVisitor.prototype.visitBindingBehavior = function (behavior) { + behavior.expression.accept(this); + this.visitArgs(behavior.args); + }; + ExpressionVisitor.prototype.visitValueConverter = function (converter) { + converter.expression.accept(this); + this.visitArgs(converter.args); + }; + ExpressionVisitor.prototype.visitAssign = function (assign) { + assign.target.accept(this); + assign.value.accept(this); + }; + ExpressionVisitor.prototype.visitConditional = function (conditional) { + conditional.condition.accept(this); + conditional.yes.accept(this); + conditional.no.accept(this); + }; + ExpressionVisitor.prototype.visitAccessThis = function (access) { + access.ancestor = access.ancestor; + }; + ExpressionVisitor.prototype.visitAccessScope = function (access) { + access.name = access.name; + }; + ExpressionVisitor.prototype.visitAccessMember = function (access) { + access.object.accept(this); + }; + ExpressionVisitor.prototype.visitAccessKeyed = function (access) { + access.object.accept(this); + access.key.accept(this); + }; + ExpressionVisitor.prototype.visitCallScope = function (call) { + this.visitArgs(call.args); + }; + ExpressionVisitor.prototype.visitCallFunction = function (call) { + call.func.accept(this); + this.visitArgs(call.args); + }; + ExpressionVisitor.prototype.visitCallMember = function (call) { + call.object.accept(this); + this.visitArgs(call.args); + }; + ExpressionVisitor.prototype.visitPrefix = function (prefix) { + prefix.expression.accept(this); + }; + ExpressionVisitor.prototype.visitBinary = function (binary) { + binary.left.accept(this); + binary.right.accept(this); + }; + ExpressionVisitor.prototype.visitLiteralPrimitive = function (literal) { + literal.value = literal.value; + }; + ExpressionVisitor.prototype.visitLiteralArray = function (literal) { + this.visitArgs(literal.elements); + }; + ExpressionVisitor.prototype.visitLiteralObject = function (literal) { + this.visitArgs(literal.values); + }; + ExpressionVisitor.prototype.visitLiteralString = function (literal) { + literal.value = literal.value; + }; + ExpressionVisitor.prototype.visitArgs = function (args) { + for (var i = 0; i < args.length; i++) { + args[i].accept(this); + } + }; + return ExpressionVisitor; + }()); + + var ValidationMessageParser = exports('ValidationMessageParser', /** @class */ (function () { + function ValidationMessageParser(bindinqLanguage) { + this.bindinqLanguage = bindinqLanguage; + this.emptyStringExpression = new LiteralString(''); + this.nullExpression = new LiteralPrimitive(null); + this.undefinedExpression = new LiteralPrimitive(undefined); + this.cache = {}; + } + ValidationMessageParser.prototype.parse = function (message) { + if (this.cache[message] !== undefined) { + return this.cache[message]; + } + var parts = this.bindinqLanguage.parseInterpolation(null, message); + if (parts === null) { + return new LiteralString(message); + } + var expression = new LiteralString(parts[0]); + for (var i = 1; i < parts.length; i += 2) { + expression = new Binary('+', expression, new Binary('+', this.coalesce(parts[i]), new LiteralString(parts[i + 1]))); + } + MessageExpressionValidator.validate(expression, message); + this.cache[message] = expression; + return expression; + }; + ValidationMessageParser.prototype.coalesce = function (part) { + // part === null || part === undefined ? '' : part + return new Conditional(new Binary('||', new Binary('===', part, this.nullExpression), new Binary('===', part, this.undefinedExpression)), this.emptyStringExpression, new CallMember(part, 'toString', [])); }; + ValidationMessageParser.inject = [BindingLanguage]; + return ValidationMessageParser; + }())); + var MessageExpressionValidator = exports('MessageExpressionValidator', /** @class */ (function (_super) { + __extends(MessageExpressionValidator, _super); + function MessageExpressionValidator(originalMessage) { + var _this = _super.call(this) || this; + _this.originalMessage = originalMessage; + return _this; + } + MessageExpressionValidator.validate = function (expression, originalMessage) { + var visitor = new MessageExpressionValidator(originalMessage); + expression.accept(visitor); + }; + MessageExpressionValidator.prototype.visitAccessScope = function (access) { + if (access.ancestor !== 0) { + throw new Error('$parent is not permitted in validation message expressions.'); + } + if (['displayName', 'propertyName', 'value', 'object', 'config', 'getDisplayName'].indexOf(access.name) !== -1) { + getLogger('aurelia-validation') + // tslint:disable-next-line:max-line-length + .warn("Did you mean to use \"$" + access.name + "\" instead of \"" + access.name + "\" in this validation message template: \"" + this.originalMessage + "\"?"); + } + }; + return MessageExpressionValidator; + }(ExpressionVisitor))); + + /** + * Dictionary of validation messages. [messageKey]: messageExpression + */ + var validationMessages = exports('validationMessages', { /** - * Removes an object from the set of objects that should be validated when validate is called. - * @param object The object. + * The default validation message. Used with rules that have no standard message. */ - ValidationController.prototype.removeObject = function (object) { - this.objects.delete(object); - this.processResultDelta('reset', this.results.filter(function (result) { return result.object === object; }), []); - }; + default: "${$displayName} is invalid.", + required: "${$displayName} is required.", + matches: "${$displayName} is not correctly formatted.", + email: "${$displayName} is not a valid email.", + minLength: "${$displayName} must be at least ${$config.length} character${$config.length === 1 ? '' : 's'}.", + maxLength: "${$displayName} cannot be longer than ${$config.length} character${$config.length === 1 ? '' : 's'}.", + minItems: "${$displayName} must contain at least ${$config.count} item${$config.count === 1 ? '' : 's'}.", + maxItems: "${$displayName} cannot contain more than ${$config.count} item${$config.count === 1 ? '' : 's'}.", + min: "${$displayName} must be at least ${$config.constraint}.", + max: "${$displayName} must be at most ${$config.constraint}.", + range: "${$displayName} must be between or equal to ${$config.min} and ${$config.max}.", + between: "${$displayName} must be between but not equal to ${$config.min} and ${$config.max}.", + equals: "${$displayName} must be ${$config.expectedValue}.", + }); + /** + * Retrieves validation messages and property display names. + */ + var ValidationMessageProvider = exports('ValidationMessageProvider', /** @class */ (function () { + function ValidationMessageProvider(parser) { + this.parser = parser; + } /** - * Adds and renders an error. + * Returns a message binding expression that corresponds to the key. + * @param key The message key. */ - ValidationController.prototype.addError = function (message, object, propertyName) { - if (propertyName === void 0) { propertyName = null; } - var resolvedPropertyName; - if (propertyName === null) { - resolvedPropertyName = propertyName; + ValidationMessageProvider.prototype.getMessage = function (key) { + var message; + if (key in validationMessages) { + message = validationMessages[key]; } else { - resolvedPropertyName = this.propertyParser.parse(propertyName); + message = validationMessages['default']; } - var result = new ValidateResult({ __manuallyAdded__: true }, object, resolvedPropertyName, false, message); - this.processResultDelta('validate', [], [result]); - return result; + return this.parser.parse(message); }; /** - * Removes and unrenders an error. + * Formulates a property display name using the property name and the configured + * displayName (if provided). + * Override this with your own custom logic. + * @param propertyName The property name. */ - ValidationController.prototype.removeError = function (result) { - if (this.results.indexOf(result) !== -1) { - this.processResultDelta('reset', [result], []); + ValidationMessageProvider.prototype.getDisplayName = function (propertyName, displayName) { + if (displayName !== null && displayName !== undefined) { + return (displayName instanceof Function) ? displayName() : displayName; } + // split on upper-case letters. + var words = propertyName.toString().split(/(?=[A-Z])/).join(' '); + // capitalize first letter. + return words.charAt(0).toUpperCase() + words.slice(1); }; + ValidationMessageProvider.inject = [ValidationMessageParser]; + return ValidationMessageProvider; + }())); + + /** + * Validates. + * Responsible for validating objects and properties. + */ + var StandardValidator = exports('StandardValidator', /** @class */ (function (_super) { + __extends(StandardValidator, _super); + function StandardValidator(messageProvider, resources) { + var _this = _super.call(this) || this; + _this.messageProvider = messageProvider; + _this.lookupFunctions = resources.lookupFunctions; + _this.getDisplayName = messageProvider.getDisplayName.bind(messageProvider); + return _this; + } /** - * Adds a renderer. - * @param renderer The renderer. + * Validates the specified property. + * @param object The object to validate. + * @param propertyName The name of the property to validate. + * @param rules Optional. If unspecified, the rules will be looked up using the metadata + * for the object created by ValidationRules....on(class/object) */ - ValidationController.prototype.addRenderer = function (renderer) { - var _this = this; - this.renderers.push(renderer); - renderer.render({ - kind: 'validate', - render: this.results.map(function (result) { return ({ result: result, elements: _this.elements.get(result) }); }), - unrender: [] - }); + StandardValidator.prototype.validateProperty = function (object, propertyName, rules) { + return this.validate(object, propertyName, rules || null); }; /** - * Removes a renderer. - * @param renderer The renderer. - */ - ValidationController.prototype.removeRenderer = function (renderer) { - var _this = this; - this.renderers.splice(this.renderers.indexOf(renderer), 1); - renderer.render({ - kind: 'reset', - render: [], - unrender: this.results.map(function (result) { return ({ result: result, elements: _this.elements.get(result) }); }) - }); - }; - /** - * Registers a binding with the controller. - * @param binding The binding instance. - * @param target The DOM element. - * @param rules (optional) rules associated with the binding. Validator implementation specific. - */ - ValidationController.prototype.registerBinding = function (binding, target, rules) { - this.bindings.set(binding, { target: target, rules: rules, propertyInfo: null }); - }; - /** - * Unregisters a binding with the controller. - * @param binding The binding instance. + * Validates all rules for specified object and it's properties. + * @param object The object to validate. + * @param rules Optional. If unspecified, the rules will be looked up using the metadata + * for the object created by ValidationRules....on(class/object) */ - ValidationController.prototype.unregisterBinding = function (binding) { - this.resetBinding(binding); - this.bindings.delete(binding); + StandardValidator.prototype.validateObject = function (object, rules) { + return this.validate(object, null, rules || null); }; /** - * Interprets the instruction and returns a predicate that will identify - * relevant results in the list of rendered validation results. + * Determines whether a rule exists in a set of rules. + * @param rules The rules to search. + * @parem rule The rule to find. */ - ValidationController.prototype.getInstructionPredicate = function (instruction) { - var _this = this; - if (instruction) { - var object_1 = instruction.object, propertyName_1 = instruction.propertyName, rules_1 = instruction.rules; - var predicate_1; - if (instruction.propertyName) { - predicate_1 = function (x) { return x.object === object_1 && x.propertyName === propertyName_1; }; - } - else { - predicate_1 = function (x) { return x.object === object_1; }; - } - if (rules_1) { - return function (x) { return predicate_1(x) && _this.validator.ruleExists(rules_1, x.rule); }; + StandardValidator.prototype.ruleExists = function (rules, rule) { + var i = rules.length; + while (i--) { + if (rules[i].indexOf(rule) !== -1) { + return true; } - return predicate_1; } - else { - return function () { return true; }; + return false; + }; + StandardValidator.prototype.getMessage = function (rule, object, value) { + var expression = rule.message || this.messageProvider.getMessage(rule.messageKey); + // tslint:disable-next-line:prefer-const + var _a = rule.property, propertyName = _a.name, displayName = _a.displayName; + if (propertyName !== null) { + displayName = this.messageProvider.getDisplayName(propertyName, displayName); } + var overrideContext = { + $displayName: displayName, + $propertyName: propertyName, + $value: value, + $object: object, + $config: rule.config, + // returns the name of a given property, given just the property name (irrespective of the property's displayName) + // split on capital letters, first letter ensured to be capitalized + $getDisplayName: this.getDisplayName + }; + return expression.evaluate({ bindingContext: object, overrideContext: overrideContext }, this.lookupFunctions); }; - /** - * Validates and renders results. - * @param instruction Optional. Instructions on what to validate. If undefined, all - * objects and bindings will be validated. - */ - ValidationController.prototype.validate = function (instruction) { + StandardValidator.prototype.validateRuleSequence = function (object, propertyName, ruleSequence, sequence, results) { var _this = this; - // Get a function that will process the validation instruction. - var execute; - if (instruction) { - // tslint:disable-next-line:prefer-const - var object_2 = instruction.object, propertyName_2 = instruction.propertyName, rules_2 = instruction.rules; - // if rules were not specified, check the object map. - rules_2 = rules_2 || this.objects.get(object_2); - // property specified? - if (instruction.propertyName === undefined) { - // validate the specified object. - execute = function () { return _this.validator.validateObject(object_2, rules_2); }; + // are we validating all properties or a single property? + var validateAllProperties = propertyName === null || propertyName === undefined; + var rules = ruleSequence[sequence]; + var allValid = true; + // validate each rule. + var promises = []; + var _loop_1 = function (i) { + var rule = rules[i]; + // is the rule related to the property we're validating. + // tslint:disable-next-line:triple-equals | Use loose equality for property keys + if (!validateAllProperties && rule.property.name != propertyName) { + return "continue"; } - else { - // validate the specified property. - execute = function () { return _this.validator.validateProperty(object_2, propertyName_2, rules_2); }; + // is this a conditional rule? is the condition met? + if (rule.when && !rule.when(object)) { + return "continue"; } + // validate. + var value = rule.property.name === null ? object : object[rule.property.name]; + var promiseOrBoolean = rule.condition(value, object); + if (!(promiseOrBoolean instanceof Promise)) { + promiseOrBoolean = Promise.resolve(promiseOrBoolean); + } + promises.push(promiseOrBoolean.then(function (valid) { + var message = valid ? null : _this.getMessage(rule, object, value); + results.push(new ValidateResult(rule, object, rule.property.name, valid, message)); + allValid = allValid && valid; + return valid; + })); + }; + for (var i = 0; i < rules.length; i++) { + _loop_1(i); } - else { - // validate all objects and bindings. - execute = function () { - var promises = []; - for (var _i = 0, _a = Array.from(_this.objects); _i < _a.length; _i++) { - var _b = _a[_i], object = _b[0], rules = _b[1]; - promises.push(_this.validator.validateObject(object, rules)); - } - for (var _c = 0, _d = Array.from(_this.bindings); _c < _d.length; _c++) { - var _e = _d[_c], binding = _e[0], rules = _e[1].rules; - var propertyInfo = getPropertyInfo(binding.sourceExpression, binding.source); - if (!propertyInfo || _this.objects.has(propertyInfo.object)) { - continue; - } - promises.push(_this.validator.validateProperty(propertyInfo.object, propertyInfo.propertyName, rules)); - } - return Promise.all(promises).then(function (resultSets) { return resultSets.reduce(function (a, b) { return a.concat(b); }, []); }); - }; - } - // Wait for any existing validation to finish, execute the instruction, render the results. - this.validating = true; - var returnPromise = this.finishValidating - .then(execute) - .then(function (newResults) { - var predicate = _this.getInstructionPredicate(instruction); - var oldResults = _this.results.filter(predicate); - _this.processResultDelta('validate', oldResults, newResults); - if (returnPromise === _this.finishValidating) { - _this.validating = false; + return Promise.all(promises) + .then(function () { + sequence++; + if (allValid && sequence < ruleSequence.length) { + return _this.validateRuleSequence(object, propertyName, ruleSequence, sequence, results); } - var result = { - instruction: instruction, - valid: newResults.find(function (x) { return !x.valid; }) === undefined, - results: newResults - }; - _this.invokeCallbacks(instruction, result); - return result; - }) - .catch(function (exception) { - // recover, to enable subsequent calls to validate() - _this.validating = false; - _this.finishValidating = Promise.resolve(); - return Promise.reject(exception); + return results; }); - this.finishValidating = returnPromise; - return returnPromise; }; + 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 || rules.length === 0) { + return Promise.resolve([]); + } + return this.validateRuleSequence(object, propertyName, rules, 0, []); + }; + StandardValidator.inject = [ValidationMessageProvider, ViewResources]; + return StandardValidator; + }(Validator))); + + /** + * Validation triggers. + */ + var validateTrigger; + (function (validateTrigger) { /** - * Resets any rendered validation results (unrenders). - * @param instruction Optional. Instructions on what to reset. If unspecified all rendered results - * will be unrendered. + * Manual validation. Use the controller's `validate()` and `reset()` methods + * to validate all bindings. */ - ValidationController.prototype.reset = function (instruction) { - var predicate = this.getInstructionPredicate(instruction); - var oldResults = this.results.filter(predicate); - this.processResultDelta('reset', oldResults, []); - this.invokeCallbacks(instruction, null); + validateTrigger[validateTrigger["manual"] = 0] = "manual"; + /** + * Validate the binding when the binding's target element fires a DOM "blur" event. + */ + validateTrigger[validateTrigger["blur"] = 1] = "blur"; + /** + * Validate the binding when it updates the model due to a change in the view. + */ + validateTrigger[validateTrigger["change"] = 2] = "change"; + /** + * 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. + */ + validateTrigger[validateTrigger["changeOrBlur"] = 3] = "changeOrBlur"; + })(validateTrigger || (validateTrigger = exports('validateTrigger', {}))); + + /** + * Aurelia Validation Configuration API + */ + var GlobalValidationConfiguration = exports('GlobalValidationConfiguration', /** @class */ (function () { + function GlobalValidationConfiguration() { + this.validatorType = StandardValidator; + this.validationTrigger = GlobalValidationConfiguration.DEFAULT_VALIDATION_TRIGGER; + } + /** + * Use a custom Validator implementation. + */ + GlobalValidationConfiguration.prototype.customValidator = function (type) { + this.validatorType = type; + return this; + }; + GlobalValidationConfiguration.prototype.defaultValidationTrigger = function (trigger) { + this.validationTrigger = trigger; + return this; + }; + GlobalValidationConfiguration.prototype.getDefaultValidationTrigger = function () { + return this.validationTrigger; }; /** - * Gets the elements associated with an object and propertyName (if any). + * Applies the configuration. */ - ValidationController.prototype.getAssociatedElements = function (_a) { - var object = _a.object, propertyName = _a.propertyName; - var elements = []; - for (var _i = 0, _b = Array.from(this.bindings); _i < _b.length; _i++) { - var _c = _b[_i], binding = _c[0], target = _c[1].target; - var propertyInfo = getPropertyInfo(binding.sourceExpression, binding.source); - if (propertyInfo && propertyInfo.object === object && propertyInfo.propertyName === propertyName) { - elements.push(target); - } - } - return elements; + GlobalValidationConfiguration.prototype.apply = function (container) { + var validator = container.get(this.validatorType); + container.registerInstance(Validator, validator); + container.registerInstance(GlobalValidationConfiguration, this); }; - ValidationController.prototype.processResultDelta = function (kind, oldResults, newResults) { - // prepare the instruction. - var instruction = { - kind: kind, - render: [], - unrender: [] - }; - // create a shallow copy of newResults so we can mutate it without causing side-effects. - newResults = newResults.slice(0); - var _loop_1 = function (oldResult) { - // get the elements associated with the old result. - var elements = this_1.elements.get(oldResult); - // remove the old result from the element map. - this_1.elements.delete(oldResult); - // create the unrender instruction. - instruction.unrender.push({ result: oldResult, elements: elements }); - // determine if there's a corresponding new result for the old result we are unrendering. - var newResultIndex = newResults.findIndex(function (x) { return x.rule === oldResult.rule && x.object === oldResult.object && x.propertyName === oldResult.propertyName; }); - if (newResultIndex === -1) { - // no corresponding new result... simple remove. - this_1.results.splice(this_1.results.indexOf(oldResult), 1); - if (!oldResult.valid) { - this_1.errors.splice(this_1.errors.indexOf(oldResult), 1); - } - } - else { - // there is a corresponding new result... - var newResult = newResults.splice(newResultIndex, 1)[0]; - // get the elements that are associated with the new result. - var elements_1 = this_1.getAssociatedElements(newResult); - this_1.elements.set(newResult, elements_1); - // create a render instruction for the new result. - instruction.render.push({ result: newResult, elements: elements_1 }); - // do an in-place replacement of the old result with the new result. - // this ensures any repeats bound to this.results will not thrash. - this_1.results.splice(this_1.results.indexOf(oldResult), 1, newResult); - if (!oldResult.valid && newResult.valid) { - this_1.errors.splice(this_1.errors.indexOf(oldResult), 1); - } - else if (!oldResult.valid && !newResult.valid) { - this_1.errors.splice(this_1.errors.indexOf(oldResult), 1, newResult); - } - else if (!newResult.valid) { - this_1.errors.push(newResult); - } + GlobalValidationConfiguration.DEFAULT_VALIDATION_TRIGGER = validateTrigger.blur; + return GlobalValidationConfiguration; + }())); + + /** + * 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. + */ + function getTargetDOMElement(binding, view) { + var target = binding.target; + // DOM element + if (target instanceof Element) { + return target; + } + // custom element or custom attribute + // tslint:disable-next-line:prefer-const + 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; } - }; - var this_1 = this; - // create unrender instructions from the old results. - for (var _i = 0, oldResults_1 = oldResults; _i < oldResults_1.length; _i++) { - var oldResult = oldResults_1[_i]; - _loop_1(oldResult); + throw new Error("Unable to locate target element for \"" + binding.sourceExpression + "\"."); } - // create render instructions from the remaining new results. - for (var _a = 0, newResults_1 = newResults; _a < newResults_1.length; _a++) { - var result = newResults_1[_a]; - var elements = this.getAssociatedElements(result); - instruction.render.push({ result: result, elements: elements }); - this.elements.set(result, elements); - this.results.push(result); - if (!result.valid) { - this.errors.push(result); - } + } + throw new Error("Unable to locate target element for \"" + binding.sourceExpression + "\"."); + } + + function getObject(expression, objectExpression, source) { + var value = objectExpression.evaluate(source, null); + if (value === null || value === undefined || value instanceof Object) { + return value; + } + // tslint:disable-next-line:max-line-length + throw new Error("The '" + objectExpression + "' part of '" + expression + "' evaluates to " + value + " instead of an object, null or undefined."); + } + /** + * Retrieves the object and property name for the specified expression. + * @param expression The expression + * @param source The scope + */ + function getPropertyInfo(expression, source) { + var originalExpression = expression; + while (expression instanceof BindingBehavior || expression instanceof ValueConverter) { + expression = expression.expression; + } + var object; + var propertyName; + if (expression instanceof AccessScope) { + object = getContextFor(expression.name, source, expression.ancestor); + propertyName = expression.name; + } + else if (expression instanceof AccessMember) { + object = getObject(originalExpression, expression.object, source); + propertyName = expression.name; + } + else if (expression instanceof AccessKeyed) { + object = getObject(originalExpression, expression.object, source); + propertyName = expression.key.evaluate(source); + } + else { + throw new Error("Expression '" + originalExpression + "' is not compatible with the validate binding-behavior."); + } + if (object === null || object === undefined) { + return null; + } + return { object: object, propertyName: propertyName }; + } + + function isString(value) { + return Object.prototype.toString.call(value) === '[object String]'; + } + function isNumber(value) { + return Object.prototype.toString.call(value) === '[object Number]'; + } + + var PropertyAccessorParser = exports('PropertyAccessorParser', /** @class */ (function () { + function PropertyAccessorParser(parser) { + this.parser = parser; + } + PropertyAccessorParser.prototype.parse = function (property) { + if (isString(property) || isNumber(property)) { + return property; } - // render. - for (var _b = 0, _c = this.renderers; _b < _c.length; _b++) { - var renderer = _c[_b]; - renderer.render(instruction); + var accessorText = getAccessorExpression(property.toString()); + var accessor = this.parser.parse(accessorText); + if (accessor instanceof AccessScope + || accessor instanceof AccessMember && accessor.object instanceof AccessScope) { + return accessor.name; } + throw new Error("Invalid property expression: \"" + accessor + "\""); }; + PropertyAccessorParser.inject = [Parser]; + return PropertyAccessorParser; + }())); + function getAccessorExpression(fn) { + /* tslint:disable:max-line-length */ + var classic = /^function\s*\([$_\w\d]+\)\s*\{(?:\s*"use strict";)?\s*(?:[$_\w\d.['"\]+;]+)?\s*return\s+[$_\w\d]+\.([$_\w\d]+)\s*;?\s*\}$/; + /* tslint:enable:max-line-length */ + var arrow = /^\(?[$_\w\d]+\)?\s*=>\s*[$_\w\d]+\.([$_\w\d]+)$/; + var match = classic.exec(fn) || arrow.exec(fn); + if (match === null) { + throw new Error("Unable to parse accessor function:\n" + fn); + } + return match[1]; + } + + var ValidateEvent = exports('ValidateEvent', /** @class */ (function () { + function ValidateEvent( /** - * Validates the property associated with a binding. + * The type of validate event. Either "validate" or "reset". */ - ValidationController.prototype.validateBinding = function (binding) { - if (!binding.isBound) { - return; - } - var propertyInfo = getPropertyInfo(binding.sourceExpression, binding.source); - var rules; - var registeredBinding = this.bindings.get(binding); - if (registeredBinding) { - rules = registeredBinding.rules; - registeredBinding.propertyInfo = propertyInfo; - } - if (!propertyInfo) { - return; - } - var object = propertyInfo.object, propertyName = propertyInfo.propertyName; - this.validate({ object: object, propertyName: propertyName, rules: rules }); - }; + type, /** - * Resets the results for a property associated with a binding. + * The controller's current array of errors. For an array containing both + * failed rules and passed rules, use the "results" property. */ - ValidationController.prototype.resetBinding = function (binding) { - var registeredBinding = this.bindings.get(binding); - var propertyInfo = getPropertyInfo(binding.sourceExpression, binding.source); - if (!propertyInfo && registeredBinding) { - propertyInfo = registeredBinding.propertyInfo; - } - if (registeredBinding) { - registeredBinding.propertyInfo = null; - } - if (!propertyInfo) { - return; - } - var object = propertyInfo.object, propertyName = propertyInfo.propertyName; - this.reset({ object: object, propertyName: propertyName }); - }; + errors, /** - * Changes the controller's validateTrigger. - * @param newTrigger The new validateTrigger + * The controller's current array of validate results. This + * includes both passed rules and failed rules. For an array of only failed rules, + * use the "errors" property. */ - ValidationController.prototype.changeTrigger = function (newTrigger) { - this.validateTrigger = newTrigger; - var bindings = Array.from(this.bindings.keys()); - for (var _i = 0, bindings_1 = bindings; _i < bindings_1.length; _i++) { - var binding = bindings_1[_i]; - var source = binding.source; - binding.unbind(); - binding.bind(source); - } - }; + results, /** - * Revalidates the controller's current set of errors. + * The instruction passed to the "validate" or "reset" event. Will be null when + * the controller's validate/reset method was called with no instruction argument. */ - ValidationController.prototype.revalidateErrors = function () { - for (var _i = 0, _a = this.errors; _i < _a.length; _i++) { - var _b = _a[_i], object = _b.object, propertyName = _b.propertyName, rule = _b.rule; - if (rule.__manuallyAdded__) { - continue; - } - var rules = [[rule]]; - this.validate({ object: object, propertyName: propertyName, rules: rules }); - } - }; - ValidationController.prototype.invokeCallbacks = function (instruction, result) { - if (this.eventCallbacks.length === 0) { - return; - } - var event = new ValidateEvent(result ? 'validate' : 'reset', this.errors, this.results, instruction || null, result); - for (var i = 0; i < this.eventCallbacks.length; i++) { - this.eventCallbacks[i](event); - } - }; - ValidationController.inject = [Validator, PropertyAccessorParser]; - return ValidationController; + instruction, + /** + * In events with type === "validate", this property will contain the result + * of validating the instruction (see "instruction" property). Use the controllerValidateResult + * to access the validate results specific to the call to "validate" + * (as opposed to using the "results" and "errors" properties to access the controller's entire + * set of results/errors). + */ + controllerValidateResult) { + this.type = type; + this.errors = errors; + this.results = results; + this.instruction = instruction; + this.controllerValidateResult = controllerValidateResult; + } + return ValidateEvent; }())); /** - * Binding behavior. Indicates the bound property should be validated. + * Orchestrates validation. + * Manages a set of bindings, renderers and objects. + * Exposes the current list of validation results for binding purposes. */ - var ValidateBindingBehaviorBase = /** @class */ (function () { - function ValidateBindingBehaviorBase(taskQueue) { - this.taskQueue = taskQueue; + var ValidationController = exports('ValidationController', /** @class */ (function () { + function ValidationController(validator, propertyParser, config) { + this.validator = validator; + this.propertyParser = propertyParser; + // Registered bindings (via the validate binding behavior) + this.bindings = new Map(); + // Renderers that have been added to the controller instance. + this.renderers = []; + /** + * Validation results that have been rendered by the controller. + */ + this.results = []; + /** + * Validation errors that have been rendered by the controller. + */ + this.errors = []; + /** + * Whether the controller is currently validating. + */ + this.validating = false; + // Elements related to validation results that have been rendered. + this.elements = new Map(); + // Objects that have been added to the controller instance (entity-style validation). + this.objects = new Map(); + // Promise that resolves when validation has completed. + this.finishValidating = Promise.resolve(); + this.eventCallbacks = []; + this.validateTrigger = config instanceof GlobalValidationConfiguration + ? config.getDefaultValidationTrigger() + : GlobalValidationConfiguration.DEFAULT_VALIDATION_TRIGGER; } - ValidateBindingBehaviorBase.prototype.bind = function (binding, source, rulesOrController, rules) { + /** + * Subscribe to controller validate and reset events. These events occur when the + * controller's "validate"" and "reset" methods are called. + * @param callback The callback to be invoked when the controller validates or resets. + */ + ValidationController.prototype.subscribe = function (callback) { var _this = this; - // identify the target element. - var target = getTargetDOMElement(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); - // tslint:disable-next-line:no-bitwise - if (trigger & validateTrigger.change) { - binding.vbbUpdateSource = binding.updateSource; - // tslint:disable-next-line:only-arrow-functions - // tslint:disable-next-line:space-before-function-paren - binding.updateSource = function (value) { - this.vbbUpdateSource(value); - this.validationController.validateBinding(this); - }; - } - // tslint:disable-next-line:no-bitwise - 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; - // tslint:disable-next-line:only-arrow-functions - // tslint:disable-next-line:space-before-function-paren - 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.vbbUpdateSource) { - binding.updateSource = binding.vbbUpdateSource; - binding.vbbUpdateSource = 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; - }()); - - /** - * Binding behavior. Indicates the bound property should be validated - * when the validate trigger specified by the associated controller's - * validateTrigger property occurs. - */ - var ValidateBindingBehavior = exports('ValidateBindingBehavior', /** @class */ (function (_super) { - __extends(ValidateBindingBehavior, _super); - function ValidateBindingBehavior() { - return _super !== null && _super.apply(this, arguments) || this; - } - ValidateBindingBehavior.prototype.getValidateTrigger = function (controller) { - return controller.validateTrigger; - }; - ValidateBindingBehavior.inject = [TaskQueue]; - ValidateBindingBehavior = __decorate([ - bindingBehavior('validate') - ], ValidateBindingBehavior); - 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. - */ - var ValidateManuallyBindingBehavior = exports('ValidateManuallyBindingBehavior', /** @class */ (function (_super) { - __extends(ValidateManuallyBindingBehavior, _super); - function ValidateManuallyBindingBehavior() { - return _super !== null && _super.apply(this, arguments) || this; - } - ValidateManuallyBindingBehavior.prototype.getValidateTrigger = function () { - return validateTrigger.manual; - }; - ValidateManuallyBindingBehavior.inject = [TaskQueue]; - ValidateManuallyBindingBehavior = __decorate([ - bindingBehavior('validateManually') - ], ValidateManuallyBindingBehavior); - return ValidateManuallyBindingBehavior; - }(ValidateBindingBehaviorBase))); - /** - * Binding behavior. Indicates the bound property should be validated - * when the associated element blurs. - */ - var ValidateOnBlurBindingBehavior = exports('ValidateOnBlurBindingBehavior', /** @class */ (function (_super) { - __extends(ValidateOnBlurBindingBehavior, _super); - function ValidateOnBlurBindingBehavior() { - return _super !== null && _super.apply(this, arguments) || this; - } - ValidateOnBlurBindingBehavior.prototype.getValidateTrigger = function () { - return validateTrigger.blur; - }; - ValidateOnBlurBindingBehavior.inject = [TaskQueue]; - ValidateOnBlurBindingBehavior = __decorate([ - bindingBehavior('validateOnBlur') - ], ValidateOnBlurBindingBehavior); - 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. - */ - var ValidateOnChangeBindingBehavior = exports('ValidateOnChangeBindingBehavior', /** @class */ (function (_super) { - __extends(ValidateOnChangeBindingBehavior, _super); - function ValidateOnChangeBindingBehavior() { - return _super !== null && _super.apply(this, arguments) || this; - } - ValidateOnChangeBindingBehavior.prototype.getValidateTrigger = function () { - return validateTrigger.change; + this.eventCallbacks.push(callback); + return { + dispose: function () { + var index = _this.eventCallbacks.indexOf(callback); + if (index === -1) { + return; + } + _this.eventCallbacks.splice(index, 1); + } + }; }; - ValidateOnChangeBindingBehavior.inject = [TaskQueue]; - ValidateOnChangeBindingBehavior = __decorate([ - bindingBehavior('validateOnChange') - ], ValidateOnChangeBindingBehavior); - 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. - */ - var ValidateOnChangeOrBlurBindingBehavior = exports('ValidateOnChangeOrBlurBindingBehavior', /** @class */ (function (_super) { - __extends(ValidateOnChangeOrBlurBindingBehavior, _super); - function ValidateOnChangeOrBlurBindingBehavior() { - return _super !== null && _super.apply(this, arguments) || this; - } - ValidateOnChangeOrBlurBindingBehavior.prototype.getValidateTrigger = function () { - return validateTrigger.changeOrBlur; + /** + * Adds an object to the set of objects that should be validated when validate is called. + * @param object The object. + * @param rules Optional. The rules. If rules aren't supplied the Validator implementation will lookup the rules. + */ + ValidationController.prototype.addObject = function (object, rules) { + this.objects.set(object, rules); }; - ValidateOnChangeOrBlurBindingBehavior.inject = [TaskQueue]; - ValidateOnChangeOrBlurBindingBehavior = __decorate([ - bindingBehavior('validateOnChangeOrBlur') - ], ValidateOnChangeOrBlurBindingBehavior); - return ValidateOnChangeOrBlurBindingBehavior; - }(ValidateBindingBehaviorBase))); - - /** - * Creates ValidationController instances. - */ - var ValidationControllerFactory = exports('ValidationControllerFactory', /** @class */ (function () { - function ValidationControllerFactory(container) { - this.container = container; - } - ValidationControllerFactory.get = function (container) { - return new ValidationControllerFactory(container); + /** + * Removes an object from the set of objects that should be validated when validate is called. + * @param object The object. + */ + ValidationController.prototype.removeObject = function (object) { + this.objects.delete(object); + this.processResultDelta('reset', this.results.filter(function (result) { return result.object === object; }), []); }; /** - * Creates a new controller instance. + * Adds and renders an error. */ - ValidationControllerFactory.prototype.create = function (validator) { - if (!validator) { - validator = this.container.get(Validator); + ValidationController.prototype.addError = function (message, object, propertyName) { + if (propertyName === void 0) { propertyName = null; } + var resolvedPropertyName; + if (propertyName === null) { + resolvedPropertyName = propertyName; } - var propertyParser = this.container.get(PropertyAccessorParser); - return new ValidationController(validator, propertyParser); + else { + resolvedPropertyName = this.propertyParser.parse(propertyName); + } + var result = new ValidateResult({ __manuallyAdded__: true }, object, resolvedPropertyName, false, message); + this.processResultDelta('validate', [], [result]); + return result; }; /** - * 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. + * Removes and unrenders an error. */ - ValidationControllerFactory.prototype.createForCurrentScope = function (validator) { - var controller = this.create(validator); - this.container.registerInstance(ValidationController, controller); - return controller; + ValidationController.prototype.removeError = function (result) { + if (this.results.indexOf(result) !== -1) { + this.processResultDelta('reset', [result], []); + } }; - return ValidationControllerFactory; - }())); - ValidationControllerFactory['protocol:aurelia:resolver'] = true; - - var ValidationErrorsCustomAttribute = exports('ValidationErrorsCustomAttribute', /** @class */ (function () { - function ValidationErrorsCustomAttribute(boundaryElement, controllerAccessor) { - this.boundaryElement = boundaryElement; - this.controllerAccessor = controllerAccessor; - this.controller = null; - this.errors = []; - this.errorsInternal = []; - } - ValidationErrorsCustomAttribute.inject = function () { - return [DOM.Element, Lazy.of(ValidationController)]; + /** + * Adds a renderer. + * @param renderer The renderer. + */ + ValidationController.prototype.addRenderer = function (renderer) { + var _this = this; + this.renderers.push(renderer); + renderer.render({ + kind: 'validate', + render: this.results.map(function (result) { return ({ result: result, elements: _this.elements.get(result) }); }), + unrender: [] + }); }; - ValidationErrorsCustomAttribute.prototype.sort = function () { - this.errorsInternal.sort(function (a, b) { - if (a.targets[0] === b.targets[0]) { - return 0; - } - // tslint:disable-next-line:no-bitwise - return a.targets[0].compareDocumentPosition(b.targets[0]) & 2 ? 1 : -1; + /** + * Removes a renderer. + * @param renderer The renderer. + */ + ValidationController.prototype.removeRenderer = function (renderer) { + var _this = this; + this.renderers.splice(this.renderers.indexOf(renderer), 1); + renderer.render({ + kind: 'reset', + render: [], + unrender: this.results.map(function (result) { return ({ result: result, elements: _this.elements.get(result) }); }) }); }; - ValidationErrorsCustomAttribute.prototype.interestingElements = function (elements) { + /** + * Registers a binding with the controller. + * @param binding The binding instance. + * @param target The DOM element. + * @param rules (optional) rules associated with the binding. Validator implementation specific. + */ + ValidationController.prototype.registerBinding = function (binding, target, rules) { + this.bindings.set(binding, { target: target, rules: rules, propertyInfo: null }); + }; + /** + * Unregisters a binding with the controller. + * @param binding The binding instance. + */ + ValidationController.prototype.unregisterBinding = function (binding) { + this.resetBinding(binding); + this.bindings.delete(binding); + }; + /** + * Interprets the instruction and returns a predicate that will identify + * relevant results in the list of rendered validation results. + */ + ValidationController.prototype.getInstructionPredicate = function (instruction) { var _this = this; - return elements.filter(function (e) { return _this.boundaryElement.contains(e); }); + if (instruction) { + var object_1 = instruction.object, propertyName_1 = instruction.propertyName, rules_1 = instruction.rules; + var predicate_1; + if (instruction.propertyName) { + predicate_1 = function (x) { return x.object === object_1 && x.propertyName === propertyName_1; }; + } + else { + predicate_1 = function (x) { return x.object === object_1; }; + } + if (rules_1) { + return function (x) { return predicate_1(x) && _this.validator.ruleExists(rules_1, x.rule); }; + } + return predicate_1; + } + else { + return function () { return true; }; + } + }; + /** + * Validates and renders results. + * @param instruction Optional. Instructions on what to validate. If undefined, all + * objects and bindings will be validated. + */ + ValidationController.prototype.validate = function (instruction) { + var _this = this; + // Get a function that will process the validation instruction. + var execute; + if (instruction) { + // tslint:disable-next-line:prefer-const + var object_2 = instruction.object, propertyName_2 = instruction.propertyName, rules_2 = instruction.rules; + // if rules were not specified, check the object map. + rules_2 = rules_2 || this.objects.get(object_2); + // property specified? + if (instruction.propertyName === undefined) { + // validate the specified object. + execute = function () { return _this.validator.validateObject(object_2, rules_2); }; + } + else { + // validate the specified property. + execute = function () { return _this.validator.validateProperty(object_2, propertyName_2, rules_2); }; + } + } + else { + // validate all objects and bindings. + execute = function () { + var promises = []; + for (var _i = 0, _a = Array.from(_this.objects); _i < _a.length; _i++) { + var _b = _a[_i], object = _b[0], rules = _b[1]; + promises.push(_this.validator.validateObject(object, rules)); + } + for (var _c = 0, _d = Array.from(_this.bindings); _c < _d.length; _c++) { + var _e = _d[_c], binding = _e[0], rules = _e[1].rules; + var propertyInfo = getPropertyInfo(binding.sourceExpression, binding.source); + if (!propertyInfo || _this.objects.has(propertyInfo.object)) { + continue; + } + promises.push(_this.validator.validateProperty(propertyInfo.object, propertyInfo.propertyName, rules)); + } + return Promise.all(promises).then(function (resultSets) { return resultSets.reduce(function (a, b) { return a.concat(b); }, []); }); + }; + } + // Wait for any existing validation to finish, execute the instruction, render the results. + this.validating = true; + var returnPromise = this.finishValidating + .then(execute) + .then(function (newResults) { + var predicate = _this.getInstructionPredicate(instruction); + var oldResults = _this.results.filter(predicate); + _this.processResultDelta('validate', oldResults, newResults); + if (returnPromise === _this.finishValidating) { + _this.validating = false; + } + var result = { + instruction: instruction, + valid: newResults.find(function (x) { return !x.valid; }) === undefined, + results: newResults + }; + _this.invokeCallbacks(instruction, result); + return result; + }) + .catch(function (exception) { + // recover, to enable subsequent calls to validate() + _this.validating = false; + _this.finishValidating = Promise.resolve(); + return Promise.reject(exception); + }); + this.finishValidating = returnPromise; + return returnPromise; + }; + /** + * Resets any rendered validation results (unrenders). + * @param instruction Optional. Instructions on what to reset. If unspecified all rendered results + * will be unrendered. + */ + ValidationController.prototype.reset = function (instruction) { + var predicate = this.getInstructionPredicate(instruction); + var oldResults = this.results.filter(predicate); + this.processResultDelta('reset', oldResults, []); + this.invokeCallbacks(instruction, null); + }; + /** + * Gets the elements associated with an object and propertyName (if any). + */ + ValidationController.prototype.getAssociatedElements = function (_a) { + var object = _a.object, propertyName = _a.propertyName; + var elements = []; + for (var _i = 0, _b = Array.from(this.bindings); _i < _b.length; _i++) { + var _c = _b[_i], binding = _c[0], target = _c[1].target; + var propertyInfo = getPropertyInfo(binding.sourceExpression, binding.source); + if (propertyInfo && propertyInfo.object === object && propertyInfo.propertyName === propertyName) { + elements.push(target); + } + } + return elements; }; - ValidationErrorsCustomAttribute.prototype.render = function (instruction) { - var _loop_1 = function (result) { - var index = this_1.errorsInternal.findIndex(function (x) { return x.error === result; }); - if (index !== -1) { - this_1.errorsInternal.splice(index, 1); + ValidationController.prototype.processResultDelta = function (kind, oldResults, newResults) { + // prepare the instruction. + var instruction = { + kind: kind, + render: [], + unrender: [] + }; + // create a shallow copy of newResults so we can mutate it without causing side-effects. + newResults = newResults.slice(0); + var _loop_1 = function (oldResult) { + // get the elements associated with the old result. + var elements = this_1.elements.get(oldResult); + // remove the old result from the element map. + this_1.elements.delete(oldResult); + // create the unrender instruction. + instruction.unrender.push({ result: oldResult, elements: elements }); + // determine if there's a corresponding new result for the old result we are unrendering. + var newResultIndex = newResults.findIndex(function (x) { return x.rule === oldResult.rule && x.object === oldResult.object && x.propertyName === oldResult.propertyName; }); + if (newResultIndex === -1) { + // no corresponding new result... simple remove. + this_1.results.splice(this_1.results.indexOf(oldResult), 1); + if (!oldResult.valid) { + this_1.errors.splice(this_1.errors.indexOf(oldResult), 1); + } + } + else { + // there is a corresponding new result... + var newResult = newResults.splice(newResultIndex, 1)[0]; + // get the elements that are associated with the new result. + var elements_1 = this_1.getAssociatedElements(newResult); + this_1.elements.set(newResult, elements_1); + // create a render instruction for the new result. + instruction.render.push({ result: newResult, elements: elements_1 }); + // do an in-place replacement of the old result with the new result. + // this ensures any repeats bound to this.results will not thrash. + this_1.results.splice(this_1.results.indexOf(oldResult), 1, newResult); + if (!oldResult.valid && newResult.valid) { + this_1.errors.splice(this_1.errors.indexOf(oldResult), 1); + } + else if (!oldResult.valid && !newResult.valid) { + this_1.errors.splice(this_1.errors.indexOf(oldResult), 1, newResult); + } + else if (!newResult.valid) { + this_1.errors.push(newResult); + } } }; var this_1 = this; - for (var _i = 0, _a = instruction.unrender; _i < _a.length; _i++) { - var result = _a[_i].result; - _loop_1(result); + // create unrender instructions from the old results. + for (var _i = 0, oldResults_1 = oldResults; _i < oldResults_1.length; _i++) { + var oldResult = oldResults_1[_i]; + _loop_1(oldResult); } - for (var _b = 0, _c = instruction.render; _b < _c.length; _b++) { - var _d = _c[_b], result = _d.result, elements = _d.elements; - if (result.valid) { - continue; - } - var targets = this.interestingElements(elements); - if (targets.length) { - this.errorsInternal.push({ error: result, targets: targets }); + // create render instructions from the remaining new results. + for (var _a = 0, newResults_1 = newResults; _a < newResults_1.length; _a++) { + var result = newResults_1[_a]; + var elements = this.getAssociatedElements(result); + instruction.render.push({ result: result, elements: elements }); + this.elements.set(result, elements); + this.results.push(result); + if (!result.valid) { + this.errors.push(result); } } - this.sort(); - this.errors = this.errorsInternal; - }; - ValidationErrorsCustomAttribute.prototype.bind = function () { - if (!this.controller) { - this.controller = this.controllerAccessor(); - } - // this will call render() with the side-effect of updating this.errors - this.controller.addRenderer(this); - }; - ValidationErrorsCustomAttribute.prototype.unbind = function () { - if (this.controller) { - this.controller.removeRenderer(this); + // render. + for (var _b = 0, _c = this.renderers; _b < _c.length; _b++) { + var renderer = _c[_b]; + renderer.render(instruction); } }; - __decorate([ - bindable({ defaultBindingMode: bindingMode.oneWay }) - ], ValidationErrorsCustomAttribute.prototype, "controller", void 0); - __decorate([ - bindable({ primaryProperty: true, defaultBindingMode: bindingMode.twoWay }) - ], ValidationErrorsCustomAttribute.prototype, "errors", void 0); - ValidationErrorsCustomAttribute = __decorate([ - customAttribute('validation-errors') - ], ValidationErrorsCustomAttribute); - return ValidationErrorsCustomAttribute; - }())); - - var ValidationRendererCustomAttribute = exports('ValidationRendererCustomAttribute', /** @class */ (function () { - function ValidationRendererCustomAttribute() { - } - ValidationRendererCustomAttribute.prototype.created = function (view) { - this.container = view.container; - }; - ValidationRendererCustomAttribute.prototype.bind = function () { - this.controller = this.container.get(ValidationController); - this.renderer = this.container.get(this.value); - this.controller.addRenderer(this.renderer); - }; - ValidationRendererCustomAttribute.prototype.unbind = function () { - this.controller.removeRenderer(this.renderer); - this.controller = null; - this.renderer = null; - }; - ValidationRendererCustomAttribute = __decorate([ - customAttribute('validation-renderer') - ], ValidationRendererCustomAttribute); - return ValidationRendererCustomAttribute; - }())); - - /** - * Sets, unsets and retrieves rules on an object or constructor function. - */ - var Rules = exports('Rules', /** @class */ (function () { - function Rules() { - } /** - * Applies the rules to a target. + * Validates the property associated with a binding. */ - Rules.set = function (target, rules) { - if (target instanceof Function) { - target = target.prototype; + ValidationController.prototype.validateBinding = function (binding) { + if (!binding.isBound) { + return; } - Object.defineProperty(target, Rules.key, { enumerable: false, configurable: false, writable: true, value: rules }); - }; - /** - * Removes rules from a target. - */ - Rules.unset = function (target) { - if (target instanceof Function) { - target = target.prototype; + var propertyInfo = getPropertyInfo(binding.sourceExpression, binding.source); + var rules; + var registeredBinding = this.bindings.get(binding); + if (registeredBinding) { + rules = registeredBinding.rules; + registeredBinding.propertyInfo = propertyInfo; } - target[Rules.key] = null; + if (!propertyInfo) { + return; + } + var object = propertyInfo.object, propertyName = propertyInfo.propertyName; + this.validate({ object: object, propertyName: propertyName, rules: rules }); }; /** - * Retrieves the target's rules. + * Resets the results for a property associated with a binding. */ - Rules.get = function (target) { - return target[Rules.key] || null; + ValidationController.prototype.resetBinding = function (binding) { + var registeredBinding = this.bindings.get(binding); + var propertyInfo = getPropertyInfo(binding.sourceExpression, binding.source); + if (!propertyInfo && registeredBinding) { + propertyInfo = registeredBinding.propertyInfo; + } + if (registeredBinding) { + registeredBinding.propertyInfo = null; + } + if (!propertyInfo) { + return; + } + var object = propertyInfo.object, propertyName = propertyInfo.propertyName; + this.reset({ object: object, propertyName: propertyName }); }; /** - * The name of the property that stores the rules. + * Changes the controller's validateTrigger. + * @param newTrigger The new validateTrigger */ - Rules.key = '__rules__'; - return Rules; - }())); - - // tslint:disable:no-empty - var ExpressionVisitor = /** @class */ (function () { - function ExpressionVisitor() { - } - ExpressionVisitor.prototype.visitChain = function (chain) { - this.visitArgs(chain.expressions); - }; - ExpressionVisitor.prototype.visitBindingBehavior = function (behavior) { - behavior.expression.accept(this); - this.visitArgs(behavior.args); - }; - ExpressionVisitor.prototype.visitValueConverter = function (converter) { - converter.expression.accept(this); - this.visitArgs(converter.args); - }; - ExpressionVisitor.prototype.visitAssign = function (assign) { - assign.target.accept(this); - assign.value.accept(this); - }; - ExpressionVisitor.prototype.visitConditional = function (conditional) { - conditional.condition.accept(this); - conditional.yes.accept(this); - conditional.no.accept(this); - }; - ExpressionVisitor.prototype.visitAccessThis = function (access) { - access.ancestor = access.ancestor; - }; - ExpressionVisitor.prototype.visitAccessScope = function (access) { - access.name = access.name; - }; - ExpressionVisitor.prototype.visitAccessMember = function (access) { - access.object.accept(this); - }; - ExpressionVisitor.prototype.visitAccessKeyed = function (access) { - access.object.accept(this); - access.key.accept(this); - }; - ExpressionVisitor.prototype.visitCallScope = function (call) { - this.visitArgs(call.args); - }; - ExpressionVisitor.prototype.visitCallFunction = function (call) { - call.func.accept(this); - this.visitArgs(call.args); - }; - ExpressionVisitor.prototype.visitCallMember = function (call) { - call.object.accept(this); - this.visitArgs(call.args); - }; - ExpressionVisitor.prototype.visitPrefix = function (prefix) { - prefix.expression.accept(this); - }; - ExpressionVisitor.prototype.visitBinary = function (binary) { - binary.left.accept(this); - binary.right.accept(this); - }; - ExpressionVisitor.prototype.visitLiteralPrimitive = function (literal) { - literal.value = literal.value; - }; - ExpressionVisitor.prototype.visitLiteralArray = function (literal) { - this.visitArgs(literal.elements); - }; - ExpressionVisitor.prototype.visitLiteralObject = function (literal) { - this.visitArgs(literal.values); + ValidationController.prototype.changeTrigger = function (newTrigger) { + this.validateTrigger = newTrigger; + var bindings = Array.from(this.bindings.keys()); + for (var _i = 0, bindings_1 = bindings; _i < bindings_1.length; _i++) { + var binding = bindings_1[_i]; + var source = binding.source; + binding.unbind(); + binding.bind(source); + } }; - ExpressionVisitor.prototype.visitLiteralString = function (literal) { - literal.value = literal.value; + /** + * Revalidates the controller's current set of errors. + */ + ValidationController.prototype.revalidateErrors = function () { + for (var _i = 0, _a = this.errors; _i < _a.length; _i++) { + var _b = _a[_i], object = _b.object, propertyName = _b.propertyName, rule = _b.rule; + if (rule.__manuallyAdded__) { + continue; + } + var rules = [[rule]]; + this.validate({ object: object, propertyName: propertyName, rules: rules }); + } }; - ExpressionVisitor.prototype.visitArgs = function (args) { - for (var i = 0; i < args.length; i++) { - args[i].accept(this); + ValidationController.prototype.invokeCallbacks = function (instruction, result) { + if (this.eventCallbacks.length === 0) { + return; + } + var event = new ValidateEvent(result ? 'validate' : 'reset', this.errors, this.results, instruction || null, result); + for (var i = 0; i < this.eventCallbacks.length; i++) { + this.eventCallbacks[i](event); } }; - return ExpressionVisitor; - }()); + ValidationController.inject = [Validator, PropertyAccessorParser, GlobalValidationConfiguration]; + return ValidationController; + }())); - var ValidationMessageParser = exports('ValidationMessageParser', /** @class */ (function () { - function ValidationMessageParser(bindinqLanguage) { - this.bindinqLanguage = bindinqLanguage; - this.emptyStringExpression = new LiteralString(''); - this.nullExpression = new LiteralPrimitive(null); - this.undefinedExpression = new LiteralPrimitive(undefined); - this.cache = {}; + /** + * Binding behavior. Indicates the bound property should be validated. + */ + var ValidateBindingBehaviorBase = /** @class */ (function () { + function ValidateBindingBehaviorBase(taskQueue) { + this.taskQueue = taskQueue; } - ValidationMessageParser.prototype.parse = function (message) { - if (this.cache[message] !== undefined) { - return this.cache[message]; + ValidateBindingBehaviorBase.prototype.bind = function (binding, source, rulesOrController, rules) { + var _this = this; + // identify the target element. + var target = getTargetDOMElement(binding, source); + // locate the controller. + var controller; + if (rulesOrController instanceof ValidationController) { + controller = rulesOrController; } - var parts = this.bindinqLanguage.parseInterpolation(null, message); - if (parts === null) { - return new LiteralString(message); + else { + controller = source.container.get(Optional.of(ValidationController)); + rules = rulesOrController; } - var expression = new LiteralString(parts[0]); - for (var i = 1; i < parts.length; i += 2) { - expression = new Binary('+', expression, new Binary('+', this.coalesce(parts[i]), new LiteralString(parts[i + 1]))); + 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); + // tslint:disable-next-line:no-bitwise + if (trigger & validateTrigger.change) { + binding.vbbUpdateSource = binding.updateSource; + // tslint:disable-next-line:only-arrow-functions + // tslint:disable-next-line:space-before-function-paren + binding.updateSource = function (value) { + this.vbbUpdateSource(value); + this.validationController.validateBinding(this); + }; + } + // tslint:disable-next-line:no-bitwise + 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; + // tslint:disable-next-line:only-arrow-functions + // tslint:disable-next-line:space-before-function-paren + binding.updateTarget = function (value) { + this.standardUpdateTarget(value); + this.validationController.resetBinding(this); + }; } - MessageExpressionValidator.validate(expression, message); - this.cache[message] = expression; - return expression; - }; - ValidationMessageParser.prototype.coalesce = function (part) { - // part === null || part === undefined ? '' : part - return new Conditional(new Binary('||', new Binary('===', part, this.nullExpression), new Binary('===', part, this.undefinedExpression)), this.emptyStringExpression, new CallMember(part, 'toString', [])); - }; - ValidationMessageParser.inject = [BindingLanguage]; - return ValidationMessageParser; - }())); - var MessageExpressionValidator = exports('MessageExpressionValidator', /** @class */ (function (_super) { - __extends(MessageExpressionValidator, _super); - function MessageExpressionValidator(originalMessage) { - var _this = _super.call(this) || this; - _this.originalMessage = originalMessage; - return _this; - } - MessageExpressionValidator.validate = function (expression, originalMessage) { - var visitor = new MessageExpressionValidator(originalMessage); - expression.accept(visitor); }; - MessageExpressionValidator.prototype.visitAccessScope = function (access) { - if (access.ancestor !== 0) { - throw new Error('$parent is not permitted in validation message expressions.'); + ValidateBindingBehaviorBase.prototype.unbind = function (binding) { + // reset the binding to it's original state. + if (binding.vbbUpdateSource) { + binding.updateSource = binding.vbbUpdateSource; + binding.vbbUpdateSource = null; } - if (['displayName', 'propertyName', 'value', 'object', 'config', 'getDisplayName'].indexOf(access.name) !== -1) { - getLogger('aurelia-validation') - // tslint:disable-next-line:max-line-length - .warn("Did you mean to use \"$" + access.name + "\" instead of \"" + access.name + "\" in this validation message template: \"" + this.originalMessage + "\"?"); + 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 MessageExpressionValidator; - }(ExpressionVisitor))); + return ValidateBindingBehaviorBase; + }()); /** - * Dictionary of validation messages. [messageKey]: messageExpression + * Binding behavior. Indicates the bound property should be validated + * when the validate trigger specified by the associated controller's + * validateTrigger property occurs. */ - var validationMessages = exports('validationMessages', { - /** - * The default validation message. Used with rules that have no standard message. - */ - default: "${$displayName} is invalid.", - required: "${$displayName} is required.", - matches: "${$displayName} is not correctly formatted.", - email: "${$displayName} is not a valid email.", - minLength: "${$displayName} must be at least ${$config.length} character${$config.length === 1 ? '' : 's'}.", - maxLength: "${$displayName} cannot be longer than ${$config.length} character${$config.length === 1 ? '' : 's'}.", - minItems: "${$displayName} must contain at least ${$config.count} item${$config.count === 1 ? '' : 's'}.", - maxItems: "${$displayName} cannot contain more than ${$config.count} item${$config.count === 1 ? '' : 's'}.", - min: "${$displayName} must be at least ${$config.constraint}.", - max: "${$displayName} must be at most ${$config.constraint}.", - range: "${$displayName} must be between or equal to ${$config.min} and ${$config.max}.", - between: "${$displayName} must be between but not equal to ${$config.min} and ${$config.max}.", - equals: "${$displayName} must be ${$config.expectedValue}.", - }); + var ValidateBindingBehavior = exports('ValidateBindingBehavior', /** @class */ (function (_super) { + __extends(ValidateBindingBehavior, _super); + function ValidateBindingBehavior() { + return _super !== null && _super.apply(this, arguments) || this; + } + ValidateBindingBehavior.prototype.getValidateTrigger = function (controller) { + return controller.validateTrigger; + }; + ValidateBindingBehavior.inject = [TaskQueue]; + ValidateBindingBehavior = __decorate([ + bindingBehavior('validate') + ], ValidateBindingBehavior); + return ValidateBindingBehavior; + }(ValidateBindingBehaviorBase))); /** - * Retrieves validation messages and property display names. + * 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 ValidationMessageProvider = exports('ValidationMessageProvider', /** @class */ (function () { - function ValidationMessageProvider(parser) { - this.parser = parser; + var ValidateManuallyBindingBehavior = exports('ValidateManuallyBindingBehavior', /** @class */ (function (_super) { + __extends(ValidateManuallyBindingBehavior, _super); + function ValidateManuallyBindingBehavior() { + return _super !== null && _super.apply(this, arguments) || this; } - /** - * Returns a message binding expression that corresponds to the key. - * @param key The message key. - */ - ValidationMessageProvider.prototype.getMessage = function (key) { - var message; - if (key in validationMessages) { - message = validationMessages[key]; - } - else { - message = validationMessages['default']; - } - return this.parser.parse(message); + ValidateManuallyBindingBehavior.prototype.getValidateTrigger = function () { + return validateTrigger.manual; }; - /** - * Formulates a property display name using the property name and the configured - * displayName (if provided). - * Override this with your own custom logic. - * @param propertyName The property name. - */ - ValidationMessageProvider.prototype.getDisplayName = function (propertyName, displayName) { - if (displayName !== null && displayName !== undefined) { - return (displayName instanceof Function) ? displayName() : displayName; - } - // split on upper-case letters. - var words = propertyName.toString().split(/(?=[A-Z])/).join(' '); - // capitalize first letter. - return words.charAt(0).toUpperCase() + words.slice(1); + ValidateManuallyBindingBehavior.inject = [TaskQueue]; + ValidateManuallyBindingBehavior = __decorate([ + bindingBehavior('validateManually') + ], ValidateManuallyBindingBehavior); + return ValidateManuallyBindingBehavior; + }(ValidateBindingBehaviorBase))); + /** + * Binding behavior. Indicates the bound property should be validated + * when the associated element blurs. + */ + var ValidateOnBlurBindingBehavior = exports('ValidateOnBlurBindingBehavior', /** @class */ (function (_super) { + __extends(ValidateOnBlurBindingBehavior, _super); + function ValidateOnBlurBindingBehavior() { + return _super !== null && _super.apply(this, arguments) || this; + } + ValidateOnBlurBindingBehavior.prototype.getValidateTrigger = function () { + return validateTrigger.blur; + }; + ValidateOnBlurBindingBehavior.inject = [TaskQueue]; + ValidateOnBlurBindingBehavior = __decorate([ + bindingBehavior('validateOnBlur') + ], ValidateOnBlurBindingBehavior); + 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. + */ + var ValidateOnChangeBindingBehavior = exports('ValidateOnChangeBindingBehavior', /** @class */ (function (_super) { + __extends(ValidateOnChangeBindingBehavior, _super); + function ValidateOnChangeBindingBehavior() { + return _super !== null && _super.apply(this, arguments) || this; + } + ValidateOnChangeBindingBehavior.prototype.getValidateTrigger = function () { + return validateTrigger.change; + }; + ValidateOnChangeBindingBehavior.inject = [TaskQueue]; + ValidateOnChangeBindingBehavior = __decorate([ + bindingBehavior('validateOnChange') + ], ValidateOnChangeBindingBehavior); + 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. + */ + var ValidateOnChangeOrBlurBindingBehavior = exports('ValidateOnChangeOrBlurBindingBehavior', /** @class */ (function (_super) { + __extends(ValidateOnChangeOrBlurBindingBehavior, _super); + function ValidateOnChangeOrBlurBindingBehavior() { + return _super !== null && _super.apply(this, arguments) || this; + } + ValidateOnChangeOrBlurBindingBehavior.prototype.getValidateTrigger = function () { + return validateTrigger.changeOrBlur; }; - ValidationMessageProvider.inject = [ValidationMessageParser]; - return ValidationMessageProvider; - }())); + ValidateOnChangeOrBlurBindingBehavior.inject = [TaskQueue]; + ValidateOnChangeOrBlurBindingBehavior = __decorate([ + bindingBehavior('validateOnChangeOrBlur') + ], ValidateOnChangeOrBlurBindingBehavior); + return ValidateOnChangeOrBlurBindingBehavior; + }(ValidateBindingBehaviorBase))); /** - * Validates. - * Responsible for validating objects and properties. + * Creates ValidationController instances. */ - var StandardValidator = exports('StandardValidator', /** @class */ (function (_super) { - __extends(StandardValidator, _super); - function StandardValidator(messageProvider, resources) { - var _this = _super.call(this) || this; - _this.messageProvider = messageProvider; - _this.lookupFunctions = resources.lookupFunctions; - _this.getDisplayName = messageProvider.getDisplayName.bind(messageProvider); - return _this; + var ValidationControllerFactory = exports('ValidationControllerFactory', /** @class */ (function () { + function ValidationControllerFactory(container) { + this.container = container; } - /** - * Validates the specified property. - * @param object The object to validate. - * @param propertyName The name of the property to validate. - * @param rules Optional. If unspecified, the rules will be looked up using the metadata - * for the object created by ValidationRules....on(class/object) - */ - StandardValidator.prototype.validateProperty = function (object, propertyName, rules) { - return this.validate(object, propertyName, rules || null); + ValidationControllerFactory.get = function (container) { + return new ValidationControllerFactory(container); }; /** - * Validates all rules for specified object and it's properties. - * @param object The object to validate. - * @param rules Optional. If unspecified, the rules will be looked up using the metadata - * for the object created by ValidationRules....on(class/object) + * Creates a new controller instance. */ - StandardValidator.prototype.validateObject = function (object, rules) { - return this.validate(object, null, rules || null); + ValidationControllerFactory.prototype.create = function (validator) { + if (!validator) { + validator = this.container.get(Validator); + } + var propertyParser = this.container.get(PropertyAccessorParser); + var config = this.container.get(GlobalValidationConfiguration); + return new ValidationController(validator, propertyParser, config); }; /** - * Determines whether a rule exists in a set of rules. - * @param rules The rules to search. - * @parem rule The rule to find. + * 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. */ - StandardValidator.prototype.ruleExists = function (rules, rule) { - var i = rules.length; - while (i--) { - if (rules[i].indexOf(rule) !== -1) { - return true; - } - } - return false; + ValidationControllerFactory.prototype.createForCurrentScope = function (validator) { + var controller = this.create(validator); + this.container.registerInstance(ValidationController, controller); + return controller; }; - StandardValidator.prototype.getMessage = function (rule, object, value) { - var expression = rule.message || this.messageProvider.getMessage(rule.messageKey); - // tslint:disable-next-line:prefer-const - var _a = rule.property, propertyName = _a.name, displayName = _a.displayName; - if (propertyName !== null) { - displayName = this.messageProvider.getDisplayName(propertyName, displayName); - } - var overrideContext = { - $displayName: displayName, - $propertyName: propertyName, - $value: value, - $object: object, - $config: rule.config, - // returns the name of a given property, given just the property name (irrespective of the property's displayName) - // split on capital letters, first letter ensured to be capitalized - $getDisplayName: this.getDisplayName - }; - return expression.evaluate({ bindingContext: object, overrideContext: overrideContext }, this.lookupFunctions); + return ValidationControllerFactory; + }())); + ValidationControllerFactory['protocol:aurelia:resolver'] = true; + + var ValidationErrorsCustomAttribute = exports('ValidationErrorsCustomAttribute', /** @class */ (function () { + function ValidationErrorsCustomAttribute(boundaryElement, controllerAccessor) { + this.boundaryElement = boundaryElement; + this.controllerAccessor = controllerAccessor; + this.controller = null; + this.errors = []; + this.errorsInternal = []; + } + ValidationErrorsCustomAttribute.inject = function () { + return [DOM.Element, Lazy.of(ValidationController)]; }; - StandardValidator.prototype.validateRuleSequence = function (object, propertyName, ruleSequence, sequence, results) { - var _this = this; - // are we validating all properties or a single property? - var validateAllProperties = propertyName === null || propertyName === undefined; - var rules = ruleSequence[sequence]; - var allValid = true; - // validate each rule. - var promises = []; - var _loop_1 = function (i) { - var rule = rules[i]; - // is the rule related to the property we're validating. - // tslint:disable-next-line:triple-equals | Use loose equality for property keys - if (!validateAllProperties && rule.property.name != propertyName) { - return "continue"; - } - // is this a conditional rule? is the condition met? - if (rule.when && !rule.when(object)) { - return "continue"; + ValidationErrorsCustomAttribute.prototype.sort = function () { + this.errorsInternal.sort(function (a, b) { + if (a.targets[0] === b.targets[0]) { + return 0; } - // validate. - var value = rule.property.name === null ? object : object[rule.property.name]; - var promiseOrBoolean = rule.condition(value, object); - if (!(promiseOrBoolean instanceof Promise)) { - promiseOrBoolean = Promise.resolve(promiseOrBoolean); + // tslint:disable-next-line:no-bitwise + return a.targets[0].compareDocumentPosition(b.targets[0]) & 2 ? 1 : -1; + }); + }; + ValidationErrorsCustomAttribute.prototype.interestingElements = function (elements) { + var _this = this; + return elements.filter(function (e) { return _this.boundaryElement.contains(e); }); + }; + ValidationErrorsCustomAttribute.prototype.render = function (instruction) { + var _loop_1 = function (result) { + var index = this_1.errorsInternal.findIndex(function (x) { return x.error === result; }); + if (index !== -1) { + this_1.errorsInternal.splice(index, 1); } - promises.push(promiseOrBoolean.then(function (valid) { - var message = valid ? null : _this.getMessage(rule, object, value); - results.push(new ValidateResult(rule, object, rule.property.name, valid, message)); - allValid = allValid && valid; - return valid; - })); }; - for (var i = 0; i < rules.length; i++) { - _loop_1(i); + var this_1 = this; + for (var _i = 0, _a = instruction.unrender; _i < _a.length; _i++) { + var result = _a[_i].result; + _loop_1(result); } - return Promise.all(promises) - .then(function () { - sequence++; - if (allValid && sequence < ruleSequence.length) { - return _this.validateRuleSequence(object, propertyName, ruleSequence, sequence, results); + for (var _b = 0, _c = instruction.render; _b < _c.length; _b++) { + var _d = _c[_b], result = _d.result, elements = _d.elements; + if (result.valid) { + continue; } - return results; - }); + var targets = this.interestingElements(elements); + if (targets.length) { + this.errorsInternal.push({ error: result, targets: targets }); + } + } + this.sort(); + this.errors = this.errorsInternal; }; - StandardValidator.prototype.validate = function (object, propertyName, rules) { - // rules specified? - if (!rules) { - // no. attempt to locate the rules. - rules = Rules.get(object); + ValidationErrorsCustomAttribute.prototype.bind = function () { + if (!this.controller) { + this.controller = this.controllerAccessor(); } - // any rules? - if (!rules || rules.length === 0) { - return Promise.resolve([]); + // this will call render() with the side-effect of updating this.errors + this.controller.addRenderer(this); + }; + ValidationErrorsCustomAttribute.prototype.unbind = function () { + if (this.controller) { + this.controller.removeRenderer(this); } - return this.validateRuleSequence(object, propertyName, rules, 0, []); }; - StandardValidator.inject = [ValidationMessageProvider, ViewResources]; - return StandardValidator; - }(Validator))); + __decorate([ + bindable({ defaultBindingMode: bindingMode.oneWay }) + ], ValidationErrorsCustomAttribute.prototype, "controller", void 0); + __decorate([ + bindable({ primaryProperty: true, defaultBindingMode: bindingMode.twoWay }) + ], ValidationErrorsCustomAttribute.prototype, "errors", void 0); + ValidationErrorsCustomAttribute = __decorate([ + customAttribute('validation-errors') + ], ValidationErrorsCustomAttribute); + return ValidationErrorsCustomAttribute; + }())); + + var ValidationRendererCustomAttribute = exports('ValidationRendererCustomAttribute', /** @class */ (function () { + function ValidationRendererCustomAttribute() { + } + ValidationRendererCustomAttribute.prototype.created = function (view) { + this.container = view.container; + }; + ValidationRendererCustomAttribute.prototype.bind = function () { + this.controller = this.container.get(ValidationController); + this.renderer = this.container.get(this.value); + this.controller.addRenderer(this.renderer); + }; + ValidationRendererCustomAttribute.prototype.unbind = function () { + this.controller.removeRenderer(this.renderer); + this.controller = null; + this.renderer = null; + }; + ValidationRendererCustomAttribute = __decorate([ + customAttribute('validation-renderer') + ], ValidationRendererCustomAttribute); + return ValidationRendererCustomAttribute; + }())); /** * Part of the fluent rule API. Enables customizing property rules. @@ -1450,12 +1492,12 @@ System.register(['aurelia-pal', 'aurelia-binding', 'aurelia-dependency-injection * @param args The rule's arguments. */ FluentRuleCustomizer.prototype.satisfiesRule = function (name) { + var _a; var args = []; for (var _i = 1; _i < arguments.length; _i++) { args[_i - 1] = arguments[_i]; } - var _a; - return (_a = this.fluentRules).satisfiesRule.apply(_a, [name].concat(args)); + return (_a = this.fluentRules).satisfiesRule.apply(_a, __spreadArrays([name], args)); }; /** * Applies the "required" rule to the property. @@ -1595,14 +1637,14 @@ System.register(['aurelia-pal', 'aurelia-binding', 'aurelia-dependency-injection // standard rule? rule = this[name]; if (rule instanceof Function) { - return rule.call.apply(rule, [this].concat(args)); + return rule.call.apply(rule, __spreadArrays([this], args)); } throw new Error("Rule with name \"" + name + "\" does not exist."); } var config = rule.argsToConfig ? rule.argsToConfig.apply(rule, args) : undefined; return this.satisfies(function (value, obj) { var _a; - return (_a = rule.condition).call.apply(_a, [_this, value, obj].concat(args)); + return (_a = rule.condition).call.apply(_a, __spreadArrays([_this, value, obj], args)); }, config) .withMessageKey(name); }; @@ -1847,28 +1889,6 @@ System.register(['aurelia-pal', 'aurelia-binding', 'aurelia-dependency-injection }())); // Exports - /** - * Aurelia Validation Configuration API - */ - var AureliaValidationConfiguration = exports('AureliaValidationConfiguration', /** @class */ (function () { - function AureliaValidationConfiguration() { - this.validatorType = StandardValidator; - } - /** - * Use a custom Validator implementation. - */ - AureliaValidationConfiguration.prototype.customValidator = function (type) { - this.validatorType = type; - }; - /** - * Applies the configuration. - */ - AureliaValidationConfiguration.prototype.apply = function (container) { - var validator = container.get(this.validatorType); - container.registerInstance(Validator, validator); - }; - return AureliaValidationConfiguration; - }())); /** * Configures the plugin. */ @@ -1881,7 +1901,7 @@ System.register(['aurelia-pal', 'aurelia-binding', 'aurelia-dependency-injection var propertyParser = frameworkConfig.container.get(PropertyAccessorParser); ValidationRules.initialize(messageParser, propertyParser); // configure... - var config = new AureliaValidationConfiguration(); + var config = new GlobalValidationConfiguration(); if (callback instanceof Function) { callback(config); } diff --git a/dist/umd-es2015/aurelia-validation.js b/dist/umd-es2015/aurelia-validation.js index 49165a19..67e8c019 100644 --- a/dist/umd-es2015/aurelia-validation.js +++ b/dist/umd-es2015/aurelia-validation.js @@ -1,159 +1,8 @@ (function (global, factory) { - typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports, require('aurelia-pal'), require('aurelia-binding'), require('aurelia-dependency-injection'), require('aurelia-task-queue'), require('aurelia-templating'), require('aurelia-logging')) : - typeof define === 'function' && define.amd ? define(['exports', 'aurelia-pal', 'aurelia-binding', 'aurelia-dependency-injection', 'aurelia-task-queue', 'aurelia-templating', 'aurelia-logging'], factory) : - (factory((global.au = global.au || {}, global.au.validation = {}),global.au,global.au,global.au,global.au,global.au,global.au.LogManager)); -}(this, (function (exports,aureliaPal,aureliaBinding,aureliaDependencyInjection,aureliaTaskQueue,aureliaTemplating,LogManager) { 'use strict'; - - /** - * 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. - */ - function getTargetDOMElement(binding, view) { - const target = binding.target; - // DOM element - if (target instanceof Element) { - return target; - } - // custom element or custom attribute - // tslint:disable-next-line:prefer-const - for (let i = 0, ii = view.controllers.length; i < ii; i++) { - const controller = view.controllers[i]; - if (controller.viewModel === target) { - const element = controller.container.get(aureliaPal.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}".`); - } - - function getObject(expression, objectExpression, source) { - const value = objectExpression.evaluate(source, null); - if (value === null || value === undefined || value instanceof Object) { - return value; - } - // tslint:disable-next-line:max-line-length - throw new Error(`The '${objectExpression}' part of '${expression}' evaluates to ${value} instead of an object, null or undefined.`); - } - /** - * Retrieves the object and property name for the specified expression. - * @param expression The expression - * @param source The scope - */ - function getPropertyInfo(expression, source) { - const originalExpression = expression; - while (expression instanceof aureliaBinding.BindingBehavior || expression instanceof aureliaBinding.ValueConverter) { - expression = expression.expression; - } - let object; - let propertyName; - if (expression instanceof aureliaBinding.AccessScope) { - object = aureliaBinding.getContextFor(expression.name, source, expression.ancestor); - propertyName = expression.name; - } - else if (expression instanceof aureliaBinding.AccessMember) { - object = getObject(originalExpression, expression.object, source); - propertyName = expression.name; - } - else if (expression instanceof aureliaBinding.AccessKeyed) { - object = getObject(originalExpression, expression.object, source); - propertyName = expression.key.evaluate(source); - } - else { - throw new Error(`Expression '${originalExpression}' is not compatible with the validate binding-behavior.`); - } - if (object === null || object === undefined) { - return null; - } - return { object, propertyName }; - } - - function isString(value) { - return Object.prototype.toString.call(value) === '[object String]'; - } - function isNumber(value) { - return Object.prototype.toString.call(value) === '[object Number]'; - } - - class PropertyAccessorParser { - constructor(parser) { - this.parser = parser; - } - parse(property) { - if (isString(property) || isNumber(property)) { - return property; - } - const accessorText = getAccessorExpression(property.toString()); - const accessor = this.parser.parse(accessorText); - if (accessor instanceof aureliaBinding.AccessScope - || accessor instanceof aureliaBinding.AccessMember && accessor.object instanceof aureliaBinding.AccessScope) { - return accessor.name; - } - throw new Error(`Invalid property expression: "${accessor}"`); - } - } - PropertyAccessorParser.inject = [aureliaBinding.Parser]; - function getAccessorExpression(fn) { - /* tslint:disable:max-line-length */ - const classic = /^function\s*\([$_\w\d]+\)\s*\{(?:\s*"use strict";)?\s*(?:[$_\w\d.['"\]+;]+)?\s*return\s+[$_\w\d]+\.([$_\w\d]+)\s*;?\s*\}$/; - /* tslint:enable:max-line-length */ - const arrow = /^\(?[$_\w\d]+\)?\s*=>\s*[$_\w\d]+\.([$_\w\d]+)$/; - const match = classic.exec(fn) || arrow.exec(fn); - if (match === null) { - throw new Error(`Unable to parse accessor function:\n${fn}`); - } - return match[1]; - } - - /*! ***************************************************************************** - Copyright (c) Microsoft Corporation. All rights reserved. - Licensed under the Apache License, Version 2.0 (the "License"); you may not use - this file except in compliance with the License. You may obtain a copy of the - License at http://www.apache.org/licenses/LICENSE-2.0 - - THIS CODE IS PROVIDED ON AN *AS IS* BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - KIND, EITHER EXPRESS OR IMPLIED, INCLUDING WITHOUT LIMITATION ANY IMPLIED - WARRANTIES OR CONDITIONS OF TITLE, FITNESS FOR A PARTICULAR PURPOSE, - MERCHANTABLITY OR NON-INFRINGEMENT. - - See the Apache Version 2.0 License for specific language governing permissions - and limitations under the License. - ***************************************************************************** */ - - function __decorate(decorators, target, key, desc) { - var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d; - if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc); - else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r; - return c > 3 && r && Object.defineProperty(target, key, r), r; - } - - /** - * Validation triggers. - */ - (function (validateTrigger) { - /** - * Manual validation. Use the controller's `validate()` and `reset()` methods - * to validate all bindings. - */ - validateTrigger[validateTrigger["manual"] = 0] = "manual"; - /** - * Validate the binding when the binding's target element fires a DOM "blur" event. - */ - validateTrigger[validateTrigger["blur"] = 1] = "blur"; - /** - * Validate the binding when it updates the model due to a change in the view. - */ - validateTrigger[validateTrigger["change"] = 2] = "change"; - /** - * 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. - */ - validateTrigger[validateTrigger["changeOrBlur"] = 3] = "changeOrBlur"; - })(exports.validateTrigger || (exports.validateTrigger = {})); + typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports, require('aurelia-binding'), require('aurelia-templating'), require('aurelia-logging'), require('aurelia-pal'), require('aurelia-dependency-injection'), require('aurelia-task-queue')) : + typeof define === 'function' && define.amd ? define(['exports', 'aurelia-binding', 'aurelia-templating', 'aurelia-logging', 'aurelia-pal', 'aurelia-dependency-injection', 'aurelia-task-queue'], factory) : + (factory((global.au = global.au || {}, global.au.validation = {}),global.au,global.au,global.au.LogManager,global.au,global.au,global.au)); +}(this, (function (exports,aureliaBinding,aureliaTemplating,LogManager,aureliaPal,aureliaDependencyInjection,aureliaTaskQueue) { 'use strict'; /** * Validates objects and properties. @@ -185,1024 +34,1208 @@ } ValidateResult.nextId = 0; - class ValidateEvent { - constructor( - /** - * The type of validate event. Either "validate" or "reset". - */ - type, - /** - * The controller's current array of errors. For an array containing both - * failed rules and passed rules, use the "results" property. - */ - errors, + /** + * Sets, unsets and retrieves rules on an object or constructor function. + */ + class Rules { /** - * The controller's current array of validate results. This - * includes both passed rules and failed rules. For an array of only failed rules, - * use the "errors" property. + * Applies the rules to a target. */ - results, + static set(target, rules) { + if (target instanceof Function) { + target = target.prototype; + } + Object.defineProperty(target, Rules.key, { enumerable: false, configurable: false, writable: true, value: rules }); + } /** - * The instruction passed to the "validate" or "reset" event. Will be null when - * the controller's validate/reset method was called with no instruction argument. + * Removes rules from a target. */ - instruction, + static unset(target) { + if (target instanceof Function) { + target = target.prototype; + } + target[Rules.key] = null; + } /** - * In events with type === "validate", this property will contain the result - * of validating the instruction (see "instruction" property). Use the controllerValidateResult - * to access the validate results specific to the call to "validate" - * (as opposed to using the "results" and "errors" properties to access the controller's entire - * set of results/errors). + * Retrieves the target's rules. */ - controllerValidateResult) { - this.type = type; - this.errors = errors; - this.results = results; - this.instruction = instruction; - this.controllerValidateResult = controllerValidateResult; + static get(target) { + return target[Rules.key] || null; } - } - + } /** - * Orchestrates validation. - * Manages a set of bindings, renderers and objects. - * Exposes the current list of validation results for binding purposes. + * The name of the property that stores the rules. */ - class ValidationController { - constructor(validator, propertyParser) { - this.validator = validator; - this.propertyParser = propertyParser; - // Registered bindings (via the validate binding behavior) - this.bindings = new Map(); - // Renderers that have been added to the controller instance. - this.renderers = []; - /** - * Validation results that have been rendered by the controller. - */ - this.results = []; - /** - * Validation errors that have been rendered by the controller. - */ - this.errors = []; - /** - * Whether the controller is currently validating. - */ - this.validating = false; - // Elements related to validation results that have been rendered. - this.elements = new Map(); - // Objects that have been added to the controller instance (entity-style validation). - this.objects = new Map(); - /** - * The trigger that will invoke automatic validation of a property used in a binding. - */ - this.validateTrigger = exports.validateTrigger.blur; - // Promise that resolves when validation has completed. - this.finishValidating = Promise.resolve(); - this.eventCallbacks = []; + Rules.key = '__rules__'; + + // tslint:disable:no-empty + class ExpressionVisitor { + visitChain(chain) { + this.visitArgs(chain.expressions); } - /** - * Subscribe to controller validate and reset events. These events occur when the - * controller's "validate"" and "reset" methods are called. - * @param callback The callback to be invoked when the controller validates or resets. - */ - subscribe(callback) { - this.eventCallbacks.push(callback); - return { - dispose: () => { - const index = this.eventCallbacks.indexOf(callback); - if (index === -1) { - return; - } - this.eventCallbacks.splice(index, 1); - } - }; + visitBindingBehavior(behavior) { + behavior.expression.accept(this); + this.visitArgs(behavior.args); } - /** - * Adds an object to the set of objects that should be validated when validate is called. - * @param object The object. - * @param rules Optional. The rules. If rules aren't supplied the Validator implementation will lookup the rules. - */ - addObject(object, rules) { - this.objects.set(object, rules); + visitValueConverter(converter) { + converter.expression.accept(this); + this.visitArgs(converter.args); + } + visitAssign(assign) { + assign.target.accept(this); + assign.value.accept(this); + } + visitConditional(conditional) { + conditional.condition.accept(this); + conditional.yes.accept(this); + conditional.no.accept(this); + } + visitAccessThis(access) { + access.ancestor = access.ancestor; + } + visitAccessScope(access) { + access.name = access.name; + } + visitAccessMember(access) { + access.object.accept(this); + } + visitAccessKeyed(access) { + access.object.accept(this); + access.key.accept(this); + } + visitCallScope(call) { + this.visitArgs(call.args); + } + visitCallFunction(call) { + call.func.accept(this); + this.visitArgs(call.args); + } + visitCallMember(call) { + call.object.accept(this); + this.visitArgs(call.args); + } + visitPrefix(prefix) { + prefix.expression.accept(this); + } + visitBinary(binary) { + binary.left.accept(this); + binary.right.accept(this); + } + visitLiteralPrimitive(literal) { + literal.value = literal.value; + } + visitLiteralArray(literal) { + this.visitArgs(literal.elements); + } + visitLiteralObject(literal) { + this.visitArgs(literal.values); } + visitLiteralString(literal) { + literal.value = literal.value; + } + visitArgs(args) { + for (let i = 0; i < args.length; i++) { + args[i].accept(this); + } + } + } + + class ValidationMessageParser { + constructor(bindinqLanguage) { + this.bindinqLanguage = bindinqLanguage; + this.emptyStringExpression = new aureliaBinding.LiteralString(''); + this.nullExpression = new aureliaBinding.LiteralPrimitive(null); + this.undefinedExpression = new aureliaBinding.LiteralPrimitive(undefined); + this.cache = {}; + } + parse(message) { + if (this.cache[message] !== undefined) { + return this.cache[message]; + } + const parts = this.bindinqLanguage.parseInterpolation(null, message); + if (parts === null) { + return new aureliaBinding.LiteralString(message); + } + let expression = new aureliaBinding.LiteralString(parts[0]); + for (let i = 1; i < parts.length; i += 2) { + expression = new aureliaBinding.Binary('+', expression, new aureliaBinding.Binary('+', this.coalesce(parts[i]), new aureliaBinding.LiteralString(parts[i + 1]))); + } + MessageExpressionValidator.validate(expression, message); + this.cache[message] = expression; + return expression; + } + coalesce(part) { + // part === null || part === undefined ? '' : part + return new aureliaBinding.Conditional(new aureliaBinding.Binary('||', new aureliaBinding.Binary('===', part, this.nullExpression), new aureliaBinding.Binary('===', part, this.undefinedExpression)), this.emptyStringExpression, new aureliaBinding.CallMember(part, 'toString', [])); + } + } + ValidationMessageParser.inject = [aureliaTemplating.BindingLanguage]; + class MessageExpressionValidator extends ExpressionVisitor { + constructor(originalMessage) { + super(); + this.originalMessage = originalMessage; + } + static validate(expression, originalMessage) { + const visitor = new MessageExpressionValidator(originalMessage); + expression.accept(visitor); + } + visitAccessScope(access) { + if (access.ancestor !== 0) { + throw new Error('$parent is not permitted in validation message expressions.'); + } + if (['displayName', 'propertyName', 'value', 'object', 'config', 'getDisplayName'].indexOf(access.name) !== -1) { + LogManager.getLogger('aurelia-validation') + // tslint:disable-next-line:max-line-length + .warn(`Did you mean to use "$${access.name}" instead of "${access.name}" in this validation message template: "${this.originalMessage}"?`); + } + } + } + + /** + * Dictionary of validation messages. [messageKey]: messageExpression + */ + const validationMessages = { /** - * Removes an object from the set of objects that should be validated when validate is called. - * @param object The object. + * The default validation message. Used with rules that have no standard message. */ - removeObject(object) { - this.objects.delete(object); - this.processResultDelta('reset', this.results.filter(result => result.object === object), []); + default: `\${$displayName} is invalid.`, + required: `\${$displayName} is required.`, + matches: `\${$displayName} is not correctly formatted.`, + email: `\${$displayName} is not a valid email.`, + minLength: `\${$displayName} must be at least \${$config.length} character\${$config.length === 1 ? '' : 's'}.`, + maxLength: `\${$displayName} cannot be longer than \${$config.length} character\${$config.length === 1 ? '' : 's'}.`, + minItems: `\${$displayName} must contain at least \${$config.count} item\${$config.count === 1 ? '' : 's'}.`, + maxItems: `\${$displayName} cannot contain more than \${$config.count} item\${$config.count === 1 ? '' : 's'}.`, + min: `\${$displayName} must be at least \${$config.constraint}.`, + max: `\${$displayName} must be at most \${$config.constraint}.`, + range: `\${$displayName} must be between or equal to \${$config.min} and \${$config.max}.`, + between: `\${$displayName} must be between but not equal to \${$config.min} and \${$config.max}.`, + equals: `\${$displayName} must be \${$config.expectedValue}.`, + }; + /** + * Retrieves validation messages and property display names. + */ + class ValidationMessageProvider { + constructor(parser) { + this.parser = parser; } /** - * Adds and renders an error. + * Returns a message binding expression that corresponds to the key. + * @param key The message key. */ - addError(message, object, propertyName = null) { - let resolvedPropertyName; - if (propertyName === null) { - resolvedPropertyName = propertyName; + getMessage(key) { + let message; + if (key in validationMessages) { + message = validationMessages[key]; } else { - resolvedPropertyName = this.propertyParser.parse(propertyName); + message = validationMessages['default']; } - const result = new ValidateResult({ __manuallyAdded__: true }, object, resolvedPropertyName, false, message); - this.processResultDelta('validate', [], [result]); - return result; + return this.parser.parse(message); } /** - * Removes and unrenders an error. + * Formulates a property display name using the property name and the configured + * displayName (if provided). + * Override this with your own custom logic. + * @param propertyName The property name. */ - removeError(result) { - if (this.results.indexOf(result) !== -1) { - this.processResultDelta('reset', [result], []); + getDisplayName(propertyName, displayName) { + if (displayName !== null && displayName !== undefined) { + return (displayName instanceof Function) ? displayName() : displayName; } + // split on upper-case letters. + const words = propertyName.toString().split(/(?=[A-Z])/).join(' '); + // capitalize first letter. + return words.charAt(0).toUpperCase() + words.slice(1); } - /** - * Adds a renderer. - * @param renderer The renderer. - */ - addRenderer(renderer) { - this.renderers.push(renderer); - renderer.render({ - kind: 'validate', - render: this.results.map(result => ({ result, elements: this.elements.get(result) })), - unrender: [] - }); + } + ValidationMessageProvider.inject = [ValidationMessageParser]; + + /** + * Validates. + * Responsible for validating objects and properties. + */ + class StandardValidator extends Validator { + constructor(messageProvider, resources) { + super(); + this.messageProvider = messageProvider; + this.lookupFunctions = resources.lookupFunctions; + this.getDisplayName = messageProvider.getDisplayName.bind(messageProvider); } /** - * Removes a renderer. - * @param renderer The renderer. + * Validates the specified property. + * @param object The object to validate. + * @param propertyName The name of the property to validate. + * @param rules Optional. If unspecified, the rules will be looked up using the metadata + * for the object created by ValidationRules....on(class/object) */ - removeRenderer(renderer) { - this.renderers.splice(this.renderers.indexOf(renderer), 1); - renderer.render({ - kind: 'reset', - render: [], - unrender: this.results.map(result => ({ result, elements: this.elements.get(result) })) - }); + validateProperty(object, propertyName, rules) { + return this.validate(object, propertyName, rules || null); } /** - * Registers a binding with the controller. - * @param binding The binding instance. - * @param target The DOM element. - * @param rules (optional) rules associated with the binding. Validator implementation specific. + * Validates all rules for specified object and it's properties. + * @param object The object to validate. + * @param rules Optional. If unspecified, the rules will be looked up using the metadata + * for the object created by ValidationRules....on(class/object) */ - registerBinding(binding, target, rules) { - this.bindings.set(binding, { target, rules, propertyInfo: null }); + validateObject(object, rules) { + return this.validate(object, null, rules || null); } /** - * Unregisters a binding with the controller. - * @param binding The binding instance. + * Determines whether a rule exists in a set of rules. + * @param rules The rules to search. + * @parem rule The rule to find. */ - unregisterBinding(binding) { - this.resetBinding(binding); - this.bindings.delete(binding); + ruleExists(rules, rule) { + let i = rules.length; + while (i--) { + if (rules[i].indexOf(rule) !== -1) { + return true; + } + } + return false; } - /** - * Interprets the instruction and returns a predicate that will identify - * relevant results in the list of rendered validation results. - */ - getInstructionPredicate(instruction) { - if (instruction) { - const { object, propertyName, rules } = instruction; - let predicate; - if (instruction.propertyName) { - predicate = x => x.object === object && x.propertyName === propertyName; + getMessage(rule, object, value) { + const expression = rule.message || this.messageProvider.getMessage(rule.messageKey); + // tslint:disable-next-line:prefer-const + let { name: propertyName, displayName } = rule.property; + if (propertyName !== null) { + displayName = this.messageProvider.getDisplayName(propertyName, displayName); + } + const overrideContext = { + $displayName: displayName, + $propertyName: propertyName, + $value: value, + $object: object, + $config: rule.config, + // returns the name of a given property, given just the property name (irrespective of the property's displayName) + // split on capital letters, first letter ensured to be capitalized + $getDisplayName: this.getDisplayName + }; + return expression.evaluate({ bindingContext: object, overrideContext }, this.lookupFunctions); + } + validateRuleSequence(object, propertyName, ruleSequence, sequence, results) { + // are we validating all properties or a single property? + const validateAllProperties = propertyName === null || propertyName === undefined; + const rules = ruleSequence[sequence]; + let allValid = true; + // validate each rule. + const promises = []; + for (let i = 0; i < rules.length; i++) { + const rule = rules[i]; + // is the rule related to the property we're validating. + // tslint:disable-next-line:triple-equals | Use loose equality for property keys + if (!validateAllProperties && rule.property.name != propertyName) { + continue; } - else { - predicate = x => x.object === object; + // is this a conditional rule? is the condition met? + if (rule.when && !rule.when(object)) { + continue; } - if (rules) { - return x => predicate(x) && this.validator.ruleExists(rules, x.rule); + // validate. + const value = rule.property.name === null ? object : object[rule.property.name]; + let promiseOrBoolean = rule.condition(value, object); + if (!(promiseOrBoolean instanceof Promise)) { + promiseOrBoolean = Promise.resolve(promiseOrBoolean); } - return predicate; + promises.push(promiseOrBoolean.then(valid => { + const message = valid ? null : this.getMessage(rule, object, value); + results.push(new ValidateResult(rule, object, rule.property.name, valid, message)); + allValid = allValid && valid; + return valid; + })); } - else { - return () => true; + return Promise.all(promises) + .then(() => { + sequence++; + if (allValid && sequence < ruleSequence.length) { + return this.validateRuleSequence(object, propertyName, ruleSequence, sequence, results); + } + return results; + }); + } + validate(object, propertyName, rules) { + // rules specified? + if (!rules) { + // no. attempt to locate the rules. + rules = Rules.get(object); + } + // any rules? + if (!rules || rules.length === 0) { + return Promise.resolve([]); } + return this.validateRuleSequence(object, propertyName, rules, 0, []); } + } + StandardValidator.inject = [ValidationMessageProvider, aureliaTemplating.ViewResources]; + + /** + * Validation triggers. + */ + (function (validateTrigger) { /** - * Validates and renders results. - * @param instruction Optional. Instructions on what to validate. If undefined, all - * objects and bindings will be validated. + * Manual validation. Use the controller's `validate()` and `reset()` methods + * to validate all bindings. */ - validate(instruction) { - // Get a function that will process the validation instruction. - let execute; - if (instruction) { - // tslint:disable-next-line:prefer-const - let { object, propertyName, rules } = instruction; - // if rules were not specified, check the object map. - rules = rules || this.objects.get(object); - // property specified? - if (instruction.propertyName === undefined) { - // validate the specified object. - execute = () => this.validator.validateObject(object, rules); - } - else { - // validate the specified property. - execute = () => this.validator.validateProperty(object, propertyName, rules); - } - } - else { - // validate all objects and bindings. - execute = () => { - const promises = []; - for (const [object, rules] of Array.from(this.objects)) { - promises.push(this.validator.validateObject(object, rules)); - } - for (const [binding, { rules }] of Array.from(this.bindings)) { - const propertyInfo = getPropertyInfo(binding.sourceExpression, binding.source); - if (!propertyInfo || this.objects.has(propertyInfo.object)) { - continue; - } - promises.push(this.validator.validateProperty(propertyInfo.object, propertyInfo.propertyName, rules)); - } - return Promise.all(promises).then(resultSets => resultSets.reduce((a, b) => a.concat(b), [])); - }; - } - // Wait for any existing validation to finish, execute the instruction, render the results. - this.validating = true; - const returnPromise = this.finishValidating - .then(execute) - .then((newResults) => { - const predicate = this.getInstructionPredicate(instruction); - const oldResults = this.results.filter(predicate); - this.processResultDelta('validate', oldResults, newResults); - if (returnPromise === this.finishValidating) { - this.validating = false; - } - const result = { - instruction, - valid: newResults.find(x => !x.valid) === undefined, - results: newResults - }; - this.invokeCallbacks(instruction, result); - return result; - }) - .catch(exception => { - // recover, to enable subsequent calls to validate() - this.validating = false; - this.finishValidating = Promise.resolve(); - return Promise.reject(exception); - }); - this.finishValidating = returnPromise; - return returnPromise; + validateTrigger[validateTrigger["manual"] = 0] = "manual"; + /** + * Validate the binding when the binding's target element fires a DOM "blur" event. + */ + validateTrigger[validateTrigger["blur"] = 1] = "blur"; + /** + * Validate the binding when it updates the model due to a change in the view. + */ + validateTrigger[validateTrigger["change"] = 2] = "change"; + /** + * 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. + */ + validateTrigger[validateTrigger["changeOrBlur"] = 3] = "changeOrBlur"; + })(exports.validateTrigger || (exports.validateTrigger = {})); + + /** + * Aurelia Validation Configuration API + */ + class GlobalValidationConfiguration { + constructor() { + this.validatorType = StandardValidator; + this.validationTrigger = GlobalValidationConfiguration.DEFAULT_VALIDATION_TRIGGER; } /** - * Resets any rendered validation results (unrenders). - * @param instruction Optional. Instructions on what to reset. If unspecified all rendered results - * will be unrendered. + * Use a custom Validator implementation. */ - reset(instruction) { - const predicate = this.getInstructionPredicate(instruction); - const oldResults = this.results.filter(predicate); - this.processResultDelta('reset', oldResults, []); - this.invokeCallbacks(instruction, null); + customValidator(type) { + this.validatorType = type; + return this; + } + defaultValidationTrigger(trigger) { + this.validationTrigger = trigger; + return this; + } + getDefaultValidationTrigger() { + return this.validationTrigger; } /** - * Gets the elements associated with an object and propertyName (if any). + * Applies the configuration. */ - getAssociatedElements({ object, propertyName }) { - const elements = []; - for (const [binding, { target }] of Array.from(this.bindings)) { - const propertyInfo = getPropertyInfo(binding.sourceExpression, binding.source); - if (propertyInfo && propertyInfo.object === object && propertyInfo.propertyName === propertyName) { - elements.push(target); + apply(container) { + const validator = container.get(this.validatorType); + container.registerInstance(Validator, validator); + container.registerInstance(GlobalValidationConfiguration, this); + } + } + GlobalValidationConfiguration.DEFAULT_VALIDATION_TRIGGER = exports.validateTrigger.blur; + + /** + * 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. + */ + function getTargetDOMElement(binding, view) { + const target = binding.target; + // DOM element + if (target instanceof Element) { + return target; + } + // custom element or custom attribute + // tslint:disable-next-line:prefer-const + for (let i = 0, ii = view.controllers.length; i < ii; i++) { + const controller = view.controllers[i]; + if (controller.viewModel === target) { + const element = controller.container.get(aureliaPal.DOM.Element); + if (element) { + return element; } + throw new Error(`Unable to locate target element for "${binding.sourceExpression}".`); } - return elements; } - processResultDelta(kind, oldResults, newResults) { - // prepare the instruction. - const instruction = { - kind, - render: [], - unrender: [] - }; - // create a shallow copy of newResults so we can mutate it without causing side-effects. - newResults = newResults.slice(0); - // create unrender instructions from the old results. - for (const oldResult of oldResults) { - // get the elements associated with the old result. - const elements = this.elements.get(oldResult); - // remove the old result from the element map. - this.elements.delete(oldResult); - // create the unrender instruction. - instruction.unrender.push({ result: oldResult, elements }); - // determine if there's a corresponding new result for the old result we are unrendering. - const newResultIndex = newResults.findIndex(x => x.rule === oldResult.rule && x.object === oldResult.object && x.propertyName === oldResult.propertyName); - if (newResultIndex === -1) { - // no corresponding new result... simple remove. - this.results.splice(this.results.indexOf(oldResult), 1); - if (!oldResult.valid) { - this.errors.splice(this.errors.indexOf(oldResult), 1); - } - } - else { - // there is a corresponding new result... - const newResult = newResults.splice(newResultIndex, 1)[0]; - // get the elements that are associated with the new result. - const elements = this.getAssociatedElements(newResult); - this.elements.set(newResult, elements); - // create a render instruction for the new result. - instruction.render.push({ result: newResult, elements }); - // do an in-place replacement of the old result with the new result. - // this ensures any repeats bound to this.results will not thrash. - this.results.splice(this.results.indexOf(oldResult), 1, newResult); - if (!oldResult.valid && newResult.valid) { - this.errors.splice(this.errors.indexOf(oldResult), 1); - } - else if (!oldResult.valid && !newResult.valid) { - this.errors.splice(this.errors.indexOf(oldResult), 1, newResult); - } - else if (!newResult.valid) { - this.errors.push(newResult); - } - } - } - // create render instructions from the remaining new results. - for (const result of newResults) { - const elements = this.getAssociatedElements(result); - instruction.render.push({ result, elements }); - this.elements.set(result, elements); - this.results.push(result); - if (!result.valid) { - this.errors.push(result); - } + throw new Error(`Unable to locate target element for "${binding.sourceExpression}".`); + } + + function getObject(expression, objectExpression, source) { + const value = objectExpression.evaluate(source, null); + if (value === null || value === undefined || value instanceof Object) { + return value; + } + // tslint:disable-next-line:max-line-length + throw new Error(`The '${objectExpression}' part of '${expression}' evaluates to ${value} instead of an object, null or undefined.`); + } + /** + * Retrieves the object and property name for the specified expression. + * @param expression The expression + * @param source The scope + */ + function getPropertyInfo(expression, source) { + const originalExpression = expression; + while (expression instanceof aureliaBinding.BindingBehavior || expression instanceof aureliaBinding.ValueConverter) { + expression = expression.expression; + } + let object; + let propertyName; + if (expression instanceof aureliaBinding.AccessScope) { + object = aureliaBinding.getContextFor(expression.name, source, expression.ancestor); + propertyName = expression.name; + } + else if (expression instanceof aureliaBinding.AccessMember) { + object = getObject(originalExpression, expression.object, source); + propertyName = expression.name; + } + else if (expression instanceof aureliaBinding.AccessKeyed) { + object = getObject(originalExpression, expression.object, source); + propertyName = expression.key.evaluate(source); + } + else { + throw new Error(`Expression '${originalExpression}' is not compatible with the validate binding-behavior.`); + } + if (object === null || object === undefined) { + return null; + } + return { object, propertyName }; + } + + function isString(value) { + return Object.prototype.toString.call(value) === '[object String]'; + } + function isNumber(value) { + return Object.prototype.toString.call(value) === '[object Number]'; + } + + class PropertyAccessorParser { + constructor(parser) { + this.parser = parser; + } + parse(property) { + if (isString(property) || isNumber(property)) { + return property; } - // render. - for (const renderer of this.renderers) { - renderer.render(instruction); + const accessorText = getAccessorExpression(property.toString()); + const accessor = this.parser.parse(accessorText); + if (accessor instanceof aureliaBinding.AccessScope + || accessor instanceof aureliaBinding.AccessMember && accessor.object instanceof aureliaBinding.AccessScope) { + return accessor.name; } + throw new Error(`Invalid property expression: "${accessor}"`); + } + } + PropertyAccessorParser.inject = [aureliaBinding.Parser]; + function getAccessorExpression(fn) { + /* tslint:disable:max-line-length */ + const classic = /^function\s*\([$_\w\d]+\)\s*\{(?:\s*"use strict";)?\s*(?:[$_\w\d.['"\]+;]+)?\s*return\s+[$_\w\d]+\.([$_\w\d]+)\s*;?\s*\}$/; + /* tslint:enable:max-line-length */ + const arrow = /^\(?[$_\w\d]+\)?\s*=>\s*[$_\w\d]+\.([$_\w\d]+)$/; + const match = classic.exec(fn) || arrow.exec(fn); + if (match === null) { + throw new Error(`Unable to parse accessor function:\n${fn}`); } + return match[1]; + } + + /*! ***************************************************************************** + Copyright (c) Microsoft Corporation. All rights reserved. + Licensed under the Apache License, Version 2.0 (the "License"); you may not use + this file except in compliance with the License. You may obtain a copy of the + License at http://www.apache.org/licenses/LICENSE-2.0 + + THIS CODE IS PROVIDED ON AN *AS IS* BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + KIND, EITHER EXPRESS OR IMPLIED, INCLUDING WITHOUT LIMITATION ANY IMPLIED + WARRANTIES OR CONDITIONS OF TITLE, FITNESS FOR A PARTICULAR PURPOSE, + MERCHANTABLITY OR NON-INFRINGEMENT. + + See the Apache Version 2.0 License for specific language governing permissions + and limitations under the License. + ***************************************************************************** */ + + function __decorate(decorators, target, key, desc) { + var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d; + if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc); + else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r; + return c > 3 && r && Object.defineProperty(target, key, r), r; + } + + class ValidateEvent { + constructor( /** - * Validates the property associated with a binding. + * The type of validate event. Either "validate" or "reset". */ - validateBinding(binding) { - if (!binding.isBound) { - return; - } - const propertyInfo = getPropertyInfo(binding.sourceExpression, binding.source); - let rules; - const registeredBinding = this.bindings.get(binding); - if (registeredBinding) { - rules = registeredBinding.rules; - registeredBinding.propertyInfo = propertyInfo; - } - if (!propertyInfo) { - return; - } - const { object, propertyName } = propertyInfo; - this.validate({ object, propertyName, rules }); - } + type, /** - * Resets the results for a property associated with a binding. + * The controller's current array of errors. For an array containing both + * failed rules and passed rules, use the "results" property. */ - resetBinding(binding) { - const registeredBinding = this.bindings.get(binding); - let propertyInfo = getPropertyInfo(binding.sourceExpression, binding.source); - if (!propertyInfo && registeredBinding) { - propertyInfo = registeredBinding.propertyInfo; - } - if (registeredBinding) { - registeredBinding.propertyInfo = null; - } - if (!propertyInfo) { - return; - } - const { object, propertyName } = propertyInfo; - this.reset({ object, propertyName }); - } + errors, /** - * Changes the controller's validateTrigger. - * @param newTrigger The new validateTrigger + * The controller's current array of validate results. This + * includes both passed rules and failed rules. For an array of only failed rules, + * use the "errors" property. */ - changeTrigger(newTrigger) { - this.validateTrigger = newTrigger; - const bindings = Array.from(this.bindings.keys()); - for (const binding of bindings) { - const source = binding.source; - binding.unbind(); - binding.bind(source); - } - } + results, /** - * Revalidates the controller's current set of errors. + * The instruction passed to the "validate" or "reset" event. Will be null when + * the controller's validate/reset method was called with no instruction argument. */ - revalidateErrors() { - for (const { object, propertyName, rule } of this.errors) { - if (rule.__manuallyAdded__) { - continue; - } - const rules = [[rule]]; - this.validate({ object, propertyName, rules }); - } - } - invokeCallbacks(instruction, result) { - if (this.eventCallbacks.length === 0) { - return; - } - const event = new ValidateEvent(result ? 'validate' : 'reset', this.errors, this.results, instruction || null, result); - for (let i = 0; i < this.eventCallbacks.length; i++) { - this.eventCallbacks[i](event); - } + instruction, + /** + * In events with type === "validate", this property will contain the result + * of validating the instruction (see "instruction" property). Use the controllerValidateResult + * to access the validate results specific to the call to "validate" + * (as opposed to using the "results" and "errors" properties to access the controller's entire + * set of results/errors). + */ + controllerValidateResult) { + this.type = type; + this.errors = errors; + this.results = results; + this.instruction = instruction; + this.controllerValidateResult = controllerValidateResult; } - } - ValidationController.inject = [Validator, PropertyAccessorParser]; + } /** - * Binding behavior. Indicates the bound property should be validated. + * Orchestrates validation. + * Manages a set of bindings, renderers and objects. + * Exposes the current list of validation results for binding purposes. */ - class ValidateBindingBehaviorBase { - constructor(taskQueue) { - this.taskQueue = taskQueue; + class ValidationController { + constructor(validator, propertyParser, config) { + this.validator = validator; + this.propertyParser = propertyParser; + // Registered bindings (via the validate binding behavior) + this.bindings = new Map(); + // Renderers that have been added to the controller instance. + this.renderers = []; + /** + * Validation results that have been rendered by the controller. + */ + this.results = []; + /** + * Validation errors that have been rendered by the controller. + */ + this.errors = []; + /** + * Whether the controller is currently validating. + */ + this.validating = false; + // Elements related to validation results that have been rendered. + this.elements = new Map(); + // Objects that have been added to the controller instance (entity-style validation). + this.objects = new Map(); + // Promise that resolves when validation has completed. + this.finishValidating = Promise.resolve(); + this.eventCallbacks = []; + this.validateTrigger = config instanceof GlobalValidationConfiguration + ? config.getDefaultValidationTrigger() + : GlobalValidationConfiguration.DEFAULT_VALIDATION_TRIGGER; } - bind(binding, source, rulesOrController, rules) { - // identify the target element. - const target = getTargetDOMElement(binding, source); - // locate the controller. - let controller; - if (rulesOrController instanceof ValidationController) { - controller = rulesOrController; + /** + * Subscribe to controller validate and reset events. These events occur when the + * controller's "validate"" and "reset" methods are called. + * @param callback The callback to be invoked when the controller validates or resets. + */ + subscribe(callback) { + this.eventCallbacks.push(callback); + return { + dispose: () => { + const index = this.eventCallbacks.indexOf(callback); + if (index === -1) { + return; + } + this.eventCallbacks.splice(index, 1); + } + }; + } + /** + * Adds an object to the set of objects that should be validated when validate is called. + * @param object The object. + * @param rules Optional. The rules. If rules aren't supplied the Validator implementation will lookup the rules. + */ + addObject(object, rules) { + this.objects.set(object, rules); + } + /** + * Removes an object from the set of objects that should be validated when validate is called. + * @param object The object. + */ + removeObject(object) { + this.objects.delete(object); + this.processResultDelta('reset', this.results.filter(result => result.object === object), []); + } + /** + * Adds and renders an error. + */ + addError(message, object, propertyName = null) { + let resolvedPropertyName; + if (propertyName === null) { + resolvedPropertyName = propertyName; } else { - controller = source.container.get(aureliaDependencyInjection.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); - // tslint:disable-next-line:no-bitwise - if (trigger & exports.validateTrigger.change) { - binding.vbbUpdateSource = binding.updateSource; - // tslint:disable-next-line:only-arrow-functions - // tslint:disable-next-line:space-before-function-paren - binding.updateSource = function (value) { - this.vbbUpdateSource(value); - this.validationController.validateBinding(this); - }; - } - // tslint:disable-next-line:no-bitwise - if (trigger & exports.validateTrigger.blur) { - binding.validateBlurHandler = () => { - this.taskQueue.queueMicroTask(() => controller.validateBinding(binding)); - }; - binding.validateTarget = target; - target.addEventListener('blur', binding.validateBlurHandler); - } - if (trigger !== exports.validateTrigger.manual) { - binding.standardUpdateTarget = binding.updateTarget; - // tslint:disable-next-line:only-arrow-functions - // tslint:disable-next-line:space-before-function-paren - binding.updateTarget = function (value) { - this.standardUpdateTarget(value); - this.validationController.resetBinding(this); - }; + resolvedPropertyName = this.propertyParser.parse(propertyName); } + const result = new ValidateResult({ __manuallyAdded__: true }, object, resolvedPropertyName, false, message); + this.processResultDelta('validate', [], [result]); + return result; } - unbind(binding) { - // reset the binding to it's original state. - if (binding.vbbUpdateSource) { - binding.updateSource = binding.vbbUpdateSource; - binding.vbbUpdateSource = 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; + /** + * Removes and unrenders an error. + */ + removeError(result) { + if (this.results.indexOf(result) !== -1) { + this.processResultDelta('reset', [result], []); } - binding.validationController.unregisterBinding(binding); - binding.validationController = null; - } - } - - /** - * Binding behavior. Indicates the bound property should be validated - * when the validate trigger specified by the associated controller's - * validateTrigger property occurs. - */ - exports.ValidateBindingBehavior = class ValidateBindingBehavior extends ValidateBindingBehaviorBase { - getValidateTrigger(controller) { - return controller.validateTrigger; - } - }; - exports.ValidateBindingBehavior.inject = [aureliaTaskQueue.TaskQueue]; - exports.ValidateBindingBehavior = __decorate([ - aureliaBinding.bindingBehavior('validate') - ], exports.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. - */ - exports.ValidateManuallyBindingBehavior = class ValidateManuallyBindingBehavior extends ValidateBindingBehaviorBase { - getValidateTrigger() { - return exports.validateTrigger.manual; - } - }; - exports.ValidateManuallyBindingBehavior.inject = [aureliaTaskQueue.TaskQueue]; - exports.ValidateManuallyBindingBehavior = __decorate([ - aureliaBinding.bindingBehavior('validateManually') - ], exports.ValidateManuallyBindingBehavior); - /** - * Binding behavior. Indicates the bound property should be validated - * when the associated element blurs. - */ - exports.ValidateOnBlurBindingBehavior = class ValidateOnBlurBindingBehavior extends ValidateBindingBehaviorBase { - getValidateTrigger() { - return exports.validateTrigger.blur; } - }; - exports.ValidateOnBlurBindingBehavior.inject = [aureliaTaskQueue.TaskQueue]; - exports.ValidateOnBlurBindingBehavior = __decorate([ - aureliaBinding.bindingBehavior('validateOnBlur') - ], exports.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. - */ - exports.ValidateOnChangeBindingBehavior = class ValidateOnChangeBindingBehavior extends ValidateBindingBehaviorBase { - getValidateTrigger() { - return exports.validateTrigger.change; + /** + * Adds a renderer. + * @param renderer The renderer. + */ + addRenderer(renderer) { + this.renderers.push(renderer); + renderer.render({ + kind: 'validate', + render: this.results.map(result => ({ result, elements: this.elements.get(result) })), + unrender: [] + }); } - }; - exports.ValidateOnChangeBindingBehavior.inject = [aureliaTaskQueue.TaskQueue]; - exports.ValidateOnChangeBindingBehavior = __decorate([ - aureliaBinding.bindingBehavior('validateOnChange') - ], exports.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. - */ - exports.ValidateOnChangeOrBlurBindingBehavior = class ValidateOnChangeOrBlurBindingBehavior extends ValidateBindingBehaviorBase { - getValidateTrigger() { - return exports.validateTrigger.changeOrBlur; + /** + * Removes a renderer. + * @param renderer The renderer. + */ + removeRenderer(renderer) { + this.renderers.splice(this.renderers.indexOf(renderer), 1); + renderer.render({ + kind: 'reset', + render: [], + unrender: this.results.map(result => ({ result, elements: this.elements.get(result) })) + }); } - }; - exports.ValidateOnChangeOrBlurBindingBehavior.inject = [aureliaTaskQueue.TaskQueue]; - exports.ValidateOnChangeOrBlurBindingBehavior = __decorate([ - aureliaBinding.bindingBehavior('validateOnChangeOrBlur') - ], exports.ValidateOnChangeOrBlurBindingBehavior); - - /** - * Creates ValidationController instances. - */ - class ValidationControllerFactory { - constructor(container) { - this.container = container; + /** + * Registers a binding with the controller. + * @param binding The binding instance. + * @param target The DOM element. + * @param rules (optional) rules associated with the binding. Validator implementation specific. + */ + registerBinding(binding, target, rules) { + this.bindings.set(binding, { target, rules, propertyInfo: null }); } - static get(container) { - return new ValidationControllerFactory(container); + /** + * Unregisters a binding with the controller. + * @param binding The binding instance. + */ + unregisterBinding(binding) { + this.resetBinding(binding); + this.bindings.delete(binding); } /** - * Creates a new controller instance. + * Interprets the instruction and returns a predicate that will identify + * relevant results in the list of rendered validation results. */ - create(validator) { - if (!validator) { - validator = this.container.get(Validator); + getInstructionPredicate(instruction) { + if (instruction) { + const { object, propertyName, rules } = instruction; + let predicate; + if (instruction.propertyName) { + predicate = x => x.object === object && x.propertyName === propertyName; + } + else { + predicate = x => x.object === object; + } + if (rules) { + return x => predicate(x) && this.validator.ruleExists(rules, x.rule); + } + return predicate; + } + else { + return () => true; } - const propertyParser = this.container.get(PropertyAccessorParser); - return new ValidationController(validator, propertyParser); } /** - * 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. + * Validates and renders results. + * @param instruction Optional. Instructions on what to validate. If undefined, all + * objects and bindings will be validated. */ - createForCurrentScope(validator) { - const controller = this.create(validator); - this.container.registerInstance(ValidationController, controller); - return controller; - } - } - ValidationControllerFactory['protocol:aurelia:resolver'] = true; - - exports.ValidationErrorsCustomAttribute = class ValidationErrorsCustomAttribute { - constructor(boundaryElement, controllerAccessor) { - this.boundaryElement = boundaryElement; - this.controllerAccessor = controllerAccessor; - this.controller = null; - this.errors = []; - this.errorsInternal = []; - } - static inject() { - return [aureliaPal.DOM.Element, aureliaDependencyInjection.Lazy.of(ValidationController)]; - } - sort() { - this.errorsInternal.sort((a, b) => { - if (a.targets[0] === b.targets[0]) { - return 0; - } - // tslint:disable-next-line:no-bitwise - return a.targets[0].compareDocumentPosition(b.targets[0]) & 2 ? 1 : -1; - }); - } - interestingElements(elements) { - return elements.filter(e => this.boundaryElement.contains(e)); - } - render(instruction) { - for (const { result } of instruction.unrender) { - const index = this.errorsInternal.findIndex(x => x.error === result); - if (index !== -1) { - this.errorsInternal.splice(index, 1); - } - } - for (const { result, elements } of instruction.render) { - if (result.valid) { - continue; + validate(instruction) { + // Get a function that will process the validation instruction. + let execute; + if (instruction) { + // tslint:disable-next-line:prefer-const + let { object, propertyName, rules } = instruction; + // if rules were not specified, check the object map. + rules = rules || this.objects.get(object); + // property specified? + if (instruction.propertyName === undefined) { + // validate the specified object. + execute = () => this.validator.validateObject(object, rules); } - const targets = this.interestingElements(elements); - if (targets.length) { - this.errorsInternal.push({ error: result, targets }); + else { + // validate the specified property. + execute = () => this.validator.validateProperty(object, propertyName, rules); } } - this.sort(); - this.errors = this.errorsInternal; - } - bind() { - if (!this.controller) { - this.controller = this.controllerAccessor(); - } - // this will call render() with the side-effect of updating this.errors - this.controller.addRenderer(this); - } - unbind() { - if (this.controller) { - this.controller.removeRenderer(this); + else { + // validate all objects and bindings. + execute = () => { + const promises = []; + for (const [object, rules] of Array.from(this.objects)) { + promises.push(this.validator.validateObject(object, rules)); + } + for (const [binding, { rules }] of Array.from(this.bindings)) { + const propertyInfo = getPropertyInfo(binding.sourceExpression, binding.source); + if (!propertyInfo || this.objects.has(propertyInfo.object)) { + continue; + } + promises.push(this.validator.validateProperty(propertyInfo.object, propertyInfo.propertyName, rules)); + } + return Promise.all(promises).then(resultSets => resultSets.reduce((a, b) => a.concat(b), [])); + }; } + // Wait for any existing validation to finish, execute the instruction, render the results. + this.validating = true; + const returnPromise = this.finishValidating + .then(execute) + .then((newResults) => { + const predicate = this.getInstructionPredicate(instruction); + const oldResults = this.results.filter(predicate); + this.processResultDelta('validate', oldResults, newResults); + if (returnPromise === this.finishValidating) { + this.validating = false; + } + const result = { + instruction, + valid: newResults.find(x => !x.valid) === undefined, + results: newResults + }; + this.invokeCallbacks(instruction, result); + return result; + }) + .catch(exception => { + // recover, to enable subsequent calls to validate() + this.validating = false; + this.finishValidating = Promise.resolve(); + return Promise.reject(exception); + }); + this.finishValidating = returnPromise; + return returnPromise; } - }; - __decorate([ - aureliaTemplating.bindable({ defaultBindingMode: aureliaBinding.bindingMode.oneWay }) - ], exports.ValidationErrorsCustomAttribute.prototype, "controller", void 0); - __decorate([ - aureliaTemplating.bindable({ primaryProperty: true, defaultBindingMode: aureliaBinding.bindingMode.twoWay }) - ], exports.ValidationErrorsCustomAttribute.prototype, "errors", void 0); - exports.ValidationErrorsCustomAttribute = __decorate([ - aureliaTemplating.customAttribute('validation-errors') - ], exports.ValidationErrorsCustomAttribute); - - exports.ValidationRendererCustomAttribute = class ValidationRendererCustomAttribute { - created(view) { - this.container = view.container; - } - bind() { - this.controller = this.container.get(ValidationController); - this.renderer = this.container.get(this.value); - this.controller.addRenderer(this.renderer); - } - unbind() { - this.controller.removeRenderer(this.renderer); - this.controller = null; - this.renderer = null; - } - }; - exports.ValidationRendererCustomAttribute = __decorate([ - aureliaTemplating.customAttribute('validation-renderer') - ], exports.ValidationRendererCustomAttribute); - - /** - * Sets, unsets and retrieves rules on an object or constructor function. - */ - class Rules { /** - * Applies the rules to a target. + * Resets any rendered validation results (unrenders). + * @param instruction Optional. Instructions on what to reset. If unspecified all rendered results + * will be unrendered. */ - static set(target, rules) { - if (target instanceof Function) { - target = target.prototype; - } - Object.defineProperty(target, Rules.key, { enumerable: false, configurable: false, writable: true, value: rules }); + reset(instruction) { + const predicate = this.getInstructionPredicate(instruction); + const oldResults = this.results.filter(predicate); + this.processResultDelta('reset', oldResults, []); + this.invokeCallbacks(instruction, null); } /** - * Removes rules from a target. + * Gets the elements associated with an object and propertyName (if any). */ - static unset(target) { - if (target instanceof Function) { - target = target.prototype; + getAssociatedElements({ object, propertyName }) { + const elements = []; + for (const [binding, { target }] of Array.from(this.bindings)) { + const propertyInfo = getPropertyInfo(binding.sourceExpression, binding.source); + if (propertyInfo && propertyInfo.object === object && propertyInfo.propertyName === propertyName) { + elements.push(target); + } } - target[Rules.key] = null; - } - /** - * Retrieves the target's rules. - */ - static get(target) { - return target[Rules.key] || null; - } - } - /** - * The name of the property that stores the rules. - */ - Rules.key = '__rules__'; - - // tslint:disable:no-empty - class ExpressionVisitor { - visitChain(chain) { - this.visitArgs(chain.expressions); - } - visitBindingBehavior(behavior) { - behavior.expression.accept(this); - this.visitArgs(behavior.args); - } - visitValueConverter(converter) { - converter.expression.accept(this); - this.visitArgs(converter.args); - } - visitAssign(assign) { - assign.target.accept(this); - assign.value.accept(this); - } - visitConditional(conditional) { - conditional.condition.accept(this); - conditional.yes.accept(this); - conditional.no.accept(this); - } - visitAccessThis(access) { - access.ancestor = access.ancestor; - } - visitAccessScope(access) { - access.name = access.name; - } - visitAccessMember(access) { - access.object.accept(this); - } - visitAccessKeyed(access) { - access.object.accept(this); - access.key.accept(this); - } - visitCallScope(call) { - this.visitArgs(call.args); - } - visitCallFunction(call) { - call.func.accept(this); - this.visitArgs(call.args); - } - visitCallMember(call) { - call.object.accept(this); - this.visitArgs(call.args); - } - visitPrefix(prefix) { - prefix.expression.accept(this); + return elements; } - visitBinary(binary) { - binary.left.accept(this); - binary.right.accept(this); + processResultDelta(kind, oldResults, newResults) { + // prepare the instruction. + const instruction = { + kind, + render: [], + unrender: [] + }; + // create a shallow copy of newResults so we can mutate it without causing side-effects. + newResults = newResults.slice(0); + // create unrender instructions from the old results. + for (const oldResult of oldResults) { + // get the elements associated with the old result. + const elements = this.elements.get(oldResult); + // remove the old result from the element map. + this.elements.delete(oldResult); + // create the unrender instruction. + instruction.unrender.push({ result: oldResult, elements }); + // determine if there's a corresponding new result for the old result we are unrendering. + const newResultIndex = newResults.findIndex(x => x.rule === oldResult.rule && x.object === oldResult.object && x.propertyName === oldResult.propertyName); + if (newResultIndex === -1) { + // no corresponding new result... simple remove. + this.results.splice(this.results.indexOf(oldResult), 1); + if (!oldResult.valid) { + this.errors.splice(this.errors.indexOf(oldResult), 1); + } + } + else { + // there is a corresponding new result... + const newResult = newResults.splice(newResultIndex, 1)[0]; + // get the elements that are associated with the new result. + const elements = this.getAssociatedElements(newResult); + this.elements.set(newResult, elements); + // create a render instruction for the new result. + instruction.render.push({ result: newResult, elements }); + // do an in-place replacement of the old result with the new result. + // this ensures any repeats bound to this.results will not thrash. + this.results.splice(this.results.indexOf(oldResult), 1, newResult); + if (!oldResult.valid && newResult.valid) { + this.errors.splice(this.errors.indexOf(oldResult), 1); + } + else if (!oldResult.valid && !newResult.valid) { + this.errors.splice(this.errors.indexOf(oldResult), 1, newResult); + } + else if (!newResult.valid) { + this.errors.push(newResult); + } + } + } + // create render instructions from the remaining new results. + for (const result of newResults) { + const elements = this.getAssociatedElements(result); + instruction.render.push({ result, elements }); + this.elements.set(result, elements); + this.results.push(result); + if (!result.valid) { + this.errors.push(result); + } + } + // render. + for (const renderer of this.renderers) { + renderer.render(instruction); + } } - visitLiteralPrimitive(literal) { - literal.value = literal.value; + /** + * Validates the property associated with a binding. + */ + validateBinding(binding) { + if (!binding.isBound) { + return; + } + const propertyInfo = getPropertyInfo(binding.sourceExpression, binding.source); + let rules; + const registeredBinding = this.bindings.get(binding); + if (registeredBinding) { + rules = registeredBinding.rules; + registeredBinding.propertyInfo = propertyInfo; + } + if (!propertyInfo) { + return; + } + const { object, propertyName } = propertyInfo; + this.validate({ object, propertyName, rules }); } - visitLiteralArray(literal) { - this.visitArgs(literal.elements); + /** + * Resets the results for a property associated with a binding. + */ + resetBinding(binding) { + const registeredBinding = this.bindings.get(binding); + let propertyInfo = getPropertyInfo(binding.sourceExpression, binding.source); + if (!propertyInfo && registeredBinding) { + propertyInfo = registeredBinding.propertyInfo; + } + if (registeredBinding) { + registeredBinding.propertyInfo = null; + } + if (!propertyInfo) { + return; + } + const { object, propertyName } = propertyInfo; + this.reset({ object, propertyName }); } - visitLiteralObject(literal) { - this.visitArgs(literal.values); + /** + * Changes the controller's validateTrigger. + * @param newTrigger The new validateTrigger + */ + changeTrigger(newTrigger) { + this.validateTrigger = newTrigger; + const bindings = Array.from(this.bindings.keys()); + for (const binding of bindings) { + const source = binding.source; + binding.unbind(); + binding.bind(source); + } } - visitLiteralString(literal) { - literal.value = literal.value; + /** + * Revalidates the controller's current set of errors. + */ + revalidateErrors() { + for (const { object, propertyName, rule } of this.errors) { + if (rule.__manuallyAdded__) { + continue; + } + const rules = [[rule]]; + this.validate({ object, propertyName, rules }); + } } - visitArgs(args) { - for (let i = 0; i < args.length; i++) { - args[i].accept(this); + invokeCallbacks(instruction, result) { + if (this.eventCallbacks.length === 0) { + return; + } + const event = new ValidateEvent(result ? 'validate' : 'reset', this.errors, this.results, instruction || null, result); + for (let i = 0; i < this.eventCallbacks.length; i++) { + this.eventCallbacks[i](event); } } - } + } + ValidationController.inject = [Validator, PropertyAccessorParser, GlobalValidationConfiguration]; - class ValidationMessageParser { - constructor(bindinqLanguage) { - this.bindinqLanguage = bindinqLanguage; - this.emptyStringExpression = new aureliaBinding.LiteralString(''); - this.nullExpression = new aureliaBinding.LiteralPrimitive(null); - this.undefinedExpression = new aureliaBinding.LiteralPrimitive(undefined); - this.cache = {}; + /** + * Binding behavior. Indicates the bound property should be validated. + */ + class ValidateBindingBehaviorBase { + constructor(taskQueue) { + this.taskQueue = taskQueue; } - parse(message) { - if (this.cache[message] !== undefined) { - return this.cache[message]; + bind(binding, source, rulesOrController, rules) { + // identify the target element. + const target = getTargetDOMElement(binding, source); + // locate the controller. + let controller; + if (rulesOrController instanceof ValidationController) { + controller = rulesOrController; } - const parts = this.bindinqLanguage.parseInterpolation(null, message); - if (parts === null) { - return new aureliaBinding.LiteralString(message); + else { + controller = source.container.get(aureliaDependencyInjection.Optional.of(ValidationController)); + rules = rulesOrController; } - let expression = new aureliaBinding.LiteralString(parts[0]); - for (let i = 1; i < parts.length; i += 2) { - expression = new aureliaBinding.Binary('+', expression, new aureliaBinding.Binary('+', this.coalesce(parts[i]), new aureliaBinding.LiteralString(parts[i + 1]))); + 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); + // tslint:disable-next-line:no-bitwise + if (trigger & exports.validateTrigger.change) { + binding.vbbUpdateSource = binding.updateSource; + // tslint:disable-next-line:only-arrow-functions + // tslint:disable-next-line:space-before-function-paren + binding.updateSource = function (value) { + this.vbbUpdateSource(value); + this.validationController.validateBinding(this); + }; + } + // tslint:disable-next-line:no-bitwise + if (trigger & exports.validateTrigger.blur) { + binding.validateBlurHandler = () => { + this.taskQueue.queueMicroTask(() => controller.validateBinding(binding)); + }; + binding.validateTarget = target; + target.addEventListener('blur', binding.validateBlurHandler); + } + if (trigger !== exports.validateTrigger.manual) { + binding.standardUpdateTarget = binding.updateTarget; + // tslint:disable-next-line:only-arrow-functions + // tslint:disable-next-line:space-before-function-paren + binding.updateTarget = function (value) { + this.standardUpdateTarget(value); + this.validationController.resetBinding(this); + }; } - MessageExpressionValidator.validate(expression, message); - this.cache[message] = expression; - return expression; - } - coalesce(part) { - // part === null || part === undefined ? '' : part - return new aureliaBinding.Conditional(new aureliaBinding.Binary('||', new aureliaBinding.Binary('===', part, this.nullExpression), new aureliaBinding.Binary('===', part, this.undefinedExpression)), this.emptyStringExpression, new aureliaBinding.CallMember(part, 'toString', [])); - } - } - ValidationMessageParser.inject = [aureliaTemplating.BindingLanguage]; - class MessageExpressionValidator extends ExpressionVisitor { - constructor(originalMessage) { - super(); - this.originalMessage = originalMessage; - } - static validate(expression, originalMessage) { - const visitor = new MessageExpressionValidator(originalMessage); - expression.accept(visitor); } - visitAccessScope(access) { - if (access.ancestor !== 0) { - throw new Error('$parent is not permitted in validation message expressions.'); + unbind(binding) { + // reset the binding to it's original state. + if (binding.vbbUpdateSource) { + binding.updateSource = binding.vbbUpdateSource; + binding.vbbUpdateSource = null; } - if (['displayName', 'propertyName', 'value', 'object', 'config', 'getDisplayName'].indexOf(access.name) !== -1) { - LogManager.getLogger('aurelia-validation') - // tslint:disable-next-line:max-line-length - .warn(`Did you mean to use "$${access.name}" instead of "${access.name}" in this validation message template: "${this.originalMessage}"?`); + 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; } } /** - * Dictionary of validation messages. [messageKey]: messageExpression + * Binding behavior. Indicates the bound property should be validated + * when the validate trigger specified by the associated controller's + * validateTrigger property occurs. */ - const validationMessages = { - /** - * The default validation message. Used with rules that have no standard message. - */ - default: `\${$displayName} is invalid.`, - required: `\${$displayName} is required.`, - matches: `\${$displayName} is not correctly formatted.`, - email: `\${$displayName} is not a valid email.`, - minLength: `\${$displayName} must be at least \${$config.length} character\${$config.length === 1 ? '' : 's'}.`, - maxLength: `\${$displayName} cannot be longer than \${$config.length} character\${$config.length === 1 ? '' : 's'}.`, - minItems: `\${$displayName} must contain at least \${$config.count} item\${$config.count === 1 ? '' : 's'}.`, - maxItems: `\${$displayName} cannot contain more than \${$config.count} item\${$config.count === 1 ? '' : 's'}.`, - min: `\${$displayName} must be at least \${$config.constraint}.`, - max: `\${$displayName} must be at most \${$config.constraint}.`, - range: `\${$displayName} must be between or equal to \${$config.min} and \${$config.max}.`, - between: `\${$displayName} must be between but not equal to \${$config.min} and \${$config.max}.`, - equals: `\${$displayName} must be \${$config.expectedValue}.`, + exports.ValidateBindingBehavior = class ValidateBindingBehavior extends ValidateBindingBehaviorBase { + getValidateTrigger(controller) { + return controller.validateTrigger; + } + }; + exports.ValidateBindingBehavior.inject = [aureliaTaskQueue.TaskQueue]; + exports.ValidateBindingBehavior = __decorate([ + aureliaBinding.bindingBehavior('validate') + ], exports.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. + */ + exports.ValidateManuallyBindingBehavior = class ValidateManuallyBindingBehavior extends ValidateBindingBehaviorBase { + getValidateTrigger() { + return exports.validateTrigger.manual; + } + }; + exports.ValidateManuallyBindingBehavior.inject = [aureliaTaskQueue.TaskQueue]; + exports.ValidateManuallyBindingBehavior = __decorate([ + aureliaBinding.bindingBehavior('validateManually') + ], exports.ValidateManuallyBindingBehavior); + /** + * Binding behavior. Indicates the bound property should be validated + * when the associated element blurs. + */ + exports.ValidateOnBlurBindingBehavior = class ValidateOnBlurBindingBehavior extends ValidateBindingBehaviorBase { + getValidateTrigger() { + return exports.validateTrigger.blur; + } + }; + exports.ValidateOnBlurBindingBehavior.inject = [aureliaTaskQueue.TaskQueue]; + exports.ValidateOnBlurBindingBehavior = __decorate([ + aureliaBinding.bindingBehavior('validateOnBlur') + ], exports.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. + */ + exports.ValidateOnChangeBindingBehavior = class ValidateOnChangeBindingBehavior extends ValidateBindingBehaviorBase { + getValidateTrigger() { + return exports.validateTrigger.change; + } + }; + exports.ValidateOnChangeBindingBehavior.inject = [aureliaTaskQueue.TaskQueue]; + exports.ValidateOnChangeBindingBehavior = __decorate([ + aureliaBinding.bindingBehavior('validateOnChange') + ], exports.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. + */ + exports.ValidateOnChangeOrBlurBindingBehavior = class ValidateOnChangeOrBlurBindingBehavior extends ValidateBindingBehaviorBase { + getValidateTrigger() { + return exports.validateTrigger.changeOrBlur; + } }; + exports.ValidateOnChangeOrBlurBindingBehavior.inject = [aureliaTaskQueue.TaskQueue]; + exports.ValidateOnChangeOrBlurBindingBehavior = __decorate([ + aureliaBinding.bindingBehavior('validateOnChangeOrBlur') + ], exports.ValidateOnChangeOrBlurBindingBehavior); + /** - * Retrieves validation messages and property display names. + * Creates ValidationController instances. */ - class ValidationMessageProvider { - constructor(parser) { - this.parser = parser; + class ValidationControllerFactory { + constructor(container) { + this.container = container; + } + static get(container) { + return new ValidationControllerFactory(container); } /** - * Returns a message binding expression that corresponds to the key. - * @param key The message key. + * Creates a new controller instance. */ - getMessage(key) { - let message; - if (key in validationMessages) { - message = validationMessages[key]; - } - else { - message = validationMessages['default']; + create(validator) { + if (!validator) { + validator = this.container.get(Validator); } - return this.parser.parse(message); + const propertyParser = this.container.get(PropertyAccessorParser); + const config = this.container.get(GlobalValidationConfiguration); + return new ValidationController(validator, propertyParser, config); } /** - * Formulates a property display name using the property name and the configured - * displayName (if provided). - * Override this with your own custom logic. - * @param propertyName The property name. + * 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. */ - getDisplayName(propertyName, displayName) { - if (displayName !== null && displayName !== undefined) { - return (displayName instanceof Function) ? displayName() : displayName; - } - // split on upper-case letters. - const words = propertyName.toString().split(/(?=[A-Z])/).join(' '); - // capitalize first letter. - return words.charAt(0).toUpperCase() + words.slice(1); + createForCurrentScope(validator) { + const controller = this.create(validator); + this.container.registerInstance(ValidationController, controller); + return controller; } } - ValidationMessageProvider.inject = [ValidationMessageParser]; + ValidationControllerFactory['protocol:aurelia:resolver'] = true; - /** - * Validates. - * Responsible for validating objects and properties. - */ - class StandardValidator extends Validator { - constructor(messageProvider, resources) { - super(); - this.messageProvider = messageProvider; - this.lookupFunctions = resources.lookupFunctions; - this.getDisplayName = messageProvider.getDisplayName.bind(messageProvider); - } - /** - * Validates the specified property. - * @param object The object to validate. - * @param propertyName The name of the property to validate. - * @param rules Optional. If unspecified, the rules will be looked up using the metadata - * for the object created by ValidationRules....on(class/object) - */ - validateProperty(object, propertyName, rules) { - return this.validate(object, propertyName, rules || null); + exports.ValidationErrorsCustomAttribute = class ValidationErrorsCustomAttribute { + constructor(boundaryElement, controllerAccessor) { + this.boundaryElement = boundaryElement; + this.controllerAccessor = controllerAccessor; + this.controller = null; + this.errors = []; + this.errorsInternal = []; } - /** - * Validates all rules for specified object and it's properties. - * @param object The object to validate. - * @param rules Optional. If unspecified, the rules will be looked up using the metadata - * for the object created by ValidationRules....on(class/object) - */ - validateObject(object, rules) { - return this.validate(object, null, rules || null); + static inject() { + return [aureliaPal.DOM.Element, aureliaDependencyInjection.Lazy.of(ValidationController)]; } - /** - * Determines whether a rule exists in a set of rules. - * @param rules The rules to search. - * @parem rule The rule to find. - */ - ruleExists(rules, rule) { - let i = rules.length; - while (i--) { - if (rules[i].indexOf(rule) !== -1) { - return true; + sort() { + this.errorsInternal.sort((a, b) => { + if (a.targets[0] === b.targets[0]) { + return 0; } - } - return false; + // tslint:disable-next-line:no-bitwise + return a.targets[0].compareDocumentPosition(b.targets[0]) & 2 ? 1 : -1; + }); } - getMessage(rule, object, value) { - const expression = rule.message || this.messageProvider.getMessage(rule.messageKey); - // tslint:disable-next-line:prefer-const - let { name: propertyName, displayName } = rule.property; - if (propertyName !== null) { - displayName = this.messageProvider.getDisplayName(propertyName, displayName); - } - const overrideContext = { - $displayName: displayName, - $propertyName: propertyName, - $value: value, - $object: object, - $config: rule.config, - // returns the name of a given property, given just the property name (irrespective of the property's displayName) - // split on capital letters, first letter ensured to be capitalized - $getDisplayName: this.getDisplayName - }; - return expression.evaluate({ bindingContext: object, overrideContext }, this.lookupFunctions); + interestingElements(elements) { + return elements.filter(e => this.boundaryElement.contains(e)); } - validateRuleSequence(object, propertyName, ruleSequence, sequence, results) { - // are we validating all properties or a single property? - const validateAllProperties = propertyName === null || propertyName === undefined; - const rules = ruleSequence[sequence]; - let allValid = true; - // validate each rule. - const promises = []; - for (let i = 0; i < rules.length; i++) { - const rule = rules[i]; - // is the rule related to the property we're validating. - // tslint:disable-next-line:triple-equals | Use loose equality for property keys - if (!validateAllProperties && rule.property.name != propertyName) { - continue; + render(instruction) { + for (const { result } of instruction.unrender) { + const index = this.errorsInternal.findIndex(x => x.error === result); + if (index !== -1) { + this.errorsInternal.splice(index, 1); } - // is this a conditional rule? is the condition met? - if (rule.when && !rule.when(object)) { + } + for (const { result, elements } of instruction.render) { + if (result.valid) { continue; } - // validate. - const value = rule.property.name === null ? object : object[rule.property.name]; - let promiseOrBoolean = rule.condition(value, object); - if (!(promiseOrBoolean instanceof Promise)) { - promiseOrBoolean = Promise.resolve(promiseOrBoolean); + const targets = this.interestingElements(elements); + if (targets.length) { + this.errorsInternal.push({ error: result, targets }); } - promises.push(promiseOrBoolean.then(valid => { - const message = valid ? null : this.getMessage(rule, object, value); - results.push(new ValidateResult(rule, object, rule.property.name, valid, message)); - allValid = allValid && valid; - return valid; - })); } - return Promise.all(promises) - .then(() => { - sequence++; - if (allValid && sequence < ruleSequence.length) { - return this.validateRuleSequence(object, propertyName, ruleSequence, sequence, results); - } - return results; - }); + this.sort(); + this.errors = this.errorsInternal; } - validate(object, propertyName, rules) { - // rules specified? - if (!rules) { - // no. attempt to locate the rules. - rules = Rules.get(object); + bind() { + if (!this.controller) { + this.controller = this.controllerAccessor(); } - // any rules? - if (!rules || rules.length === 0) { - return Promise.resolve([]); + // this will call render() with the side-effect of updating this.errors + this.controller.addRenderer(this); + } + unbind() { + if (this.controller) { + this.controller.removeRenderer(this); } - return this.validateRuleSequence(object, propertyName, rules, 0, []); } - } - StandardValidator.inject = [ValidationMessageProvider, aureliaTemplating.ViewResources]; + }; + __decorate([ + aureliaTemplating.bindable({ defaultBindingMode: aureliaBinding.bindingMode.oneWay }) + ], exports.ValidationErrorsCustomAttribute.prototype, "controller", void 0); + __decorate([ + aureliaTemplating.bindable({ primaryProperty: true, defaultBindingMode: aureliaBinding.bindingMode.twoWay }) + ], exports.ValidationErrorsCustomAttribute.prototype, "errors", void 0); + exports.ValidationErrorsCustomAttribute = __decorate([ + aureliaTemplating.customAttribute('validation-errors') + ], exports.ValidationErrorsCustomAttribute); + + exports.ValidationRendererCustomAttribute = class ValidationRendererCustomAttribute { + created(view) { + this.container = view.container; + } + bind() { + this.controller = this.container.get(ValidationController); + this.renderer = this.container.get(this.value); + this.controller.addRenderer(this.renderer); + } + unbind() { + this.controller.removeRenderer(this.renderer); + this.controller = null; + this.renderer = null; + } + }; + exports.ValidationRendererCustomAttribute = __decorate([ + aureliaTemplating.customAttribute('validation-renderer') + ], exports.ValidationRendererCustomAttribute); /** * Part of the fluent rule API. Enables customizing property rules. @@ -1684,27 +1717,6 @@ } // Exports - /** - * Aurelia Validation Configuration API - */ - class AureliaValidationConfiguration { - constructor() { - this.validatorType = StandardValidator; - } - /** - * Use a custom Validator implementation. - */ - customValidator(type) { - this.validatorType = type; - } - /** - * Applies the configuration. - */ - apply(container) { - const validator = container.get(this.validatorType); - container.registerInstance(Validator, validator); - } - } /** * Configures the plugin. */ @@ -1717,7 +1729,7 @@ const propertyParser = frameworkConfig.container.get(PropertyAccessorParser); ValidationRules.initialize(messageParser, propertyParser); // configure... - const config = new AureliaValidationConfiguration(); + const config = new GlobalValidationConfiguration(); if (callback instanceof Function) { callback(config); } @@ -1728,8 +1740,8 @@ } } - exports.AureliaValidationConfiguration = AureliaValidationConfiguration; exports.configure = configure; + exports.GlobalValidationConfiguration = GlobalValidationConfiguration; exports.getTargetDOMElement = getTargetDOMElement; exports.getPropertyInfo = getPropertyInfo; exports.PropertyAccessorParser = PropertyAccessorParser; diff --git a/dist/umd/aurelia-validation.js b/dist/umd/aurelia-validation.js index c251878a..d9784e45 100644 --- a/dist/umd/aurelia-validation.js +++ b/dist/umd/aurelia-validation.js @@ -1,114 +1,17 @@ (function (global, factory) { - typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports, require('aurelia-pal'), require('aurelia-binding'), require('aurelia-dependency-injection'), require('aurelia-task-queue'), require('aurelia-templating'), require('aurelia-logging')) : - typeof define === 'function' && define.amd ? define(['exports', 'aurelia-pal', 'aurelia-binding', 'aurelia-dependency-injection', 'aurelia-task-queue', 'aurelia-templating', 'aurelia-logging'], factory) : - (factory((global.au = global.au || {}, global.au.validation = {}),global.au,global.au,global.au,global.au,global.au,global.au.LogManager)); -}(this, (function (exports,aureliaPal,aureliaBinding,aureliaDependencyInjection,aureliaTaskQueue,aureliaTemplating,LogManager) { 'use strict'; + typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports, require('aurelia-binding'), require('aurelia-templating'), require('aurelia-logging'), require('aurelia-pal'), require('aurelia-dependency-injection'), require('aurelia-task-queue')) : + typeof define === 'function' && define.amd ? define(['exports', 'aurelia-binding', 'aurelia-templating', 'aurelia-logging', 'aurelia-pal', 'aurelia-dependency-injection', 'aurelia-task-queue'], factory) : + (factory((global.au = global.au || {}, global.au.validation = {}),global.au,global.au,global.au.LogManager,global.au,global.au,global.au)); +}(this, (function (exports,aureliaBinding,aureliaTemplating,LogManager,aureliaPal,aureliaDependencyInjection,aureliaTaskQueue) { 'use strict'; /** - * 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. - */ - function getTargetDOMElement(binding, view) { - var target = binding.target; - // DOM element - if (target instanceof Element) { - return target; - } - // custom element or custom attribute - // tslint:disable-next-line:prefer-const - 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(aureliaPal.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 + "\"."); - } - - function getObject(expression, objectExpression, source) { - var value = objectExpression.evaluate(source, null); - if (value === null || value === undefined || value instanceof Object) { - return value; - } - // tslint:disable-next-line:max-line-length - throw new Error("The '" + objectExpression + "' part of '" + expression + "' evaluates to " + value + " instead of an object, null or undefined."); - } - /** - * Retrieves the object and property name for the specified expression. - * @param expression The expression - * @param source The scope + * Validates objects and properties. */ - function getPropertyInfo(expression, source) { - var originalExpression = expression; - while (expression instanceof aureliaBinding.BindingBehavior || expression instanceof aureliaBinding.ValueConverter) { - expression = expression.expression; - } - var object; - var propertyName; - if (expression instanceof aureliaBinding.AccessScope) { - object = aureliaBinding.getContextFor(expression.name, source, expression.ancestor); - propertyName = expression.name; - } - else if (expression instanceof aureliaBinding.AccessMember) { - object = getObject(originalExpression, expression.object, source); - propertyName = expression.name; - } - else if (expression instanceof aureliaBinding.AccessKeyed) { - object = getObject(originalExpression, expression.object, source); - propertyName = expression.key.evaluate(source); - } - else { - throw new Error("Expression '" + originalExpression + "' is not compatible with the validate binding-behavior."); - } - if (object === null || object === undefined) { - return null; - } - return { object: object, propertyName: propertyName }; - } - - function isString(value) { - return Object.prototype.toString.call(value) === '[object String]'; - } - function isNumber(value) { - return Object.prototype.toString.call(value) === '[object Number]'; - } - - var PropertyAccessorParser = /** @class */ (function () { - function PropertyAccessorParser(parser) { - this.parser = parser; - } - PropertyAccessorParser.prototype.parse = function (property) { - if (isString(property) || isNumber(property)) { - return property; - } - var accessorText = getAccessorExpression(property.toString()); - var accessor = this.parser.parse(accessorText); - if (accessor instanceof aureliaBinding.AccessScope - || accessor instanceof aureliaBinding.AccessMember && accessor.object instanceof aureliaBinding.AccessScope) { - return accessor.name; - } - throw new Error("Invalid property expression: \"" + accessor + "\""); - }; - PropertyAccessorParser.inject = [aureliaBinding.Parser]; - return PropertyAccessorParser; - }()); - function getAccessorExpression(fn) { - /* tslint:disable:max-line-length */ - var classic = /^function\s*\([$_\w\d]+\)\s*\{(?:\s*"use strict";)?\s*(?:[$_\w\d.['"\]+;]+)?\s*return\s+[$_\w\d]+\.([$_\w\d]+)\s*;?\s*\}$/; - /* tslint:enable:max-line-length */ - var arrow = /^\(?[$_\w\d]+\)?\s*=>\s*[$_\w\d]+\.([$_\w\d]+)$/; - var match = classic.exec(fn) || arrow.exec(fn); - if (match === null) { - throw new Error("Unable to parse accessor function:\n" + fn); + var Validator = /** @class */ (function () { + function Validator() { } - return match[1]; - } + return Validator; + }()); /*! ***************************************************************************** Copyright (c) Microsoft Corporation. All rights reserved. @@ -144,41 +47,16 @@ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc); else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r; return c > 3 && r && Object.defineProperty(target, key, r), r; + } + + function __spreadArrays() { + for (var s = 0, i = 0, il = arguments.length; i < il; i++) s += arguments[i].length; + for (var r = Array(s), k = 0, i = 0; i < il; i++) + for (var a = arguments[i], j = 0, jl = a.length; j < jl; j++, k++) + r[k] = a[j]; + return r; } - /** - * Validation triggers. - */ - (function (validateTrigger) { - /** - * Manual validation. Use the controller's `validate()` and `reset()` methods - * to validate all bindings. - */ - validateTrigger[validateTrigger["manual"] = 0] = "manual"; - /** - * Validate the binding when the binding's target element fires a DOM "blur" event. - */ - validateTrigger[validateTrigger["blur"] = 1] = "blur"; - /** - * Validate the binding when it updates the model due to a change in the view. - */ - validateTrigger[validateTrigger["change"] = 2] = "change"; - /** - * 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. - */ - validateTrigger[validateTrigger["changeOrBlur"] = 3] = "changeOrBlur"; - })(exports.validateTrigger || (exports.validateTrigger = {})); - - /** - * Validates objects and properties. - */ - var Validator = /** @class */ (function () { - function Validator() { - } - return Validator; - }()); - /** * The result of validating an individual validation rule. */ @@ -205,1102 +83,1266 @@ return ValidateResult; }()); - var ValidateEvent = /** @class */ (function () { - function ValidateEvent( - /** - * The type of validate event. Either "validate" or "reset". - */ - type, - /** - * The controller's current array of errors. For an array containing both - * failed rules and passed rules, use the "results" property. - */ - errors, - /** - * The controller's current array of validate results. This - * includes both passed rules and failed rules. For an array of only failed rules, - * use the "errors" property. - */ - results, - /** - * The instruction passed to the "validate" or "reset" event. Will be null when - * the controller's validate/reset method was called with no instruction argument. - */ - instruction, - /** - * In events with type === "validate", this property will contain the result - * of validating the instruction (see "instruction" property). Use the controllerValidateResult - * to access the validate results specific to the call to "validate" - * (as opposed to using the "results" and "errors" properties to access the controller's entire - * set of results/errors). - */ - controllerValidateResult) { - this.type = type; - this.errors = errors; - this.results = results; - this.instruction = instruction; - this.controllerValidateResult = controllerValidateResult; - } - return ValidateEvent; - }()); - /** - * Orchestrates validation. - * Manages a set of bindings, renderers and objects. - * Exposes the current list of validation results for binding purposes. + * Sets, unsets and retrieves rules on an object or constructor function. */ - var ValidationController = /** @class */ (function () { - function ValidationController(validator, propertyParser) { - this.validator = validator; - this.propertyParser = propertyParser; - // Registered bindings (via the validate binding behavior) - this.bindings = new Map(); - // Renderers that have been added to the controller instance. - this.renderers = []; - /** - * Validation results that have been rendered by the controller. - */ - this.results = []; - /** - * Validation errors that have been rendered by the controller. - */ - this.errors = []; - /** - * Whether the controller is currently validating. - */ - this.validating = false; - // Elements related to validation results that have been rendered. - this.elements = new Map(); - // Objects that have been added to the controller instance (entity-style validation). - this.objects = new Map(); - /** - * The trigger that will invoke automatic validation of a property used in a binding. - */ - this.validateTrigger = exports.validateTrigger.blur; - // Promise that resolves when validation has completed. - this.finishValidating = Promise.resolve(); - this.eventCallbacks = []; + var Rules = /** @class */ (function () { + function Rules() { } /** - * Subscribe to controller validate and reset events. These events occur when the - * controller's "validate"" and "reset" methods are called. - * @param callback The callback to be invoked when the controller validates or resets. + * Applies the rules to a target. */ - ValidationController.prototype.subscribe = function (callback) { - var _this = this; - this.eventCallbacks.push(callback); - return { - dispose: function () { - var index = _this.eventCallbacks.indexOf(callback); - if (index === -1) { - return; - } - _this.eventCallbacks.splice(index, 1); - } - }; + Rules.set = function (target, rules) { + if (target instanceof Function) { + target = target.prototype; + } + Object.defineProperty(target, Rules.key, { enumerable: false, configurable: false, writable: true, value: rules }); }; /** - * Adds an object to the set of objects that should be validated when validate is called. - * @param object The object. - * @param rules Optional. The rules. If rules aren't supplied the Validator implementation will lookup the rules. + * Removes rules from a target. */ - ValidationController.prototype.addObject = function (object, rules) { - this.objects.set(object, rules); + Rules.unset = function (target) { + if (target instanceof Function) { + target = target.prototype; + } + target[Rules.key] = null; }; /** - * Removes an object from the set of objects that should be validated when validate is called. - * @param object The object. + * Retrieves the target's rules. */ - ValidationController.prototype.removeObject = function (object) { - this.objects.delete(object); - this.processResultDelta('reset', this.results.filter(function (result) { return result.object === object; }), []); + Rules.get = function (target) { + return target[Rules.key] || null; }; /** - * Adds and renders an error. + * The name of the property that stores the rules. */ - ValidationController.prototype.addError = function (message, object, propertyName) { - if (propertyName === void 0) { propertyName = null; } - var resolvedPropertyName; - if (propertyName === null) { - resolvedPropertyName = propertyName; - } - else { - resolvedPropertyName = this.propertyParser.parse(propertyName); + Rules.key = '__rules__'; + return Rules; + }()); + + // tslint:disable:no-empty + var ExpressionVisitor = /** @class */ (function () { + function ExpressionVisitor() { + } + ExpressionVisitor.prototype.visitChain = function (chain) { + this.visitArgs(chain.expressions); + }; + ExpressionVisitor.prototype.visitBindingBehavior = function (behavior) { + behavior.expression.accept(this); + this.visitArgs(behavior.args); + }; + ExpressionVisitor.prototype.visitValueConverter = function (converter) { + converter.expression.accept(this); + this.visitArgs(converter.args); + }; + ExpressionVisitor.prototype.visitAssign = function (assign) { + assign.target.accept(this); + assign.value.accept(this); + }; + ExpressionVisitor.prototype.visitConditional = function (conditional) { + conditional.condition.accept(this); + conditional.yes.accept(this); + conditional.no.accept(this); + }; + ExpressionVisitor.prototype.visitAccessThis = function (access) { + access.ancestor = access.ancestor; + }; + ExpressionVisitor.prototype.visitAccessScope = function (access) { + access.name = access.name; + }; + ExpressionVisitor.prototype.visitAccessMember = function (access) { + access.object.accept(this); + }; + ExpressionVisitor.prototype.visitAccessKeyed = function (access) { + access.object.accept(this); + access.key.accept(this); + }; + ExpressionVisitor.prototype.visitCallScope = function (call) { + this.visitArgs(call.args); + }; + ExpressionVisitor.prototype.visitCallFunction = function (call) { + call.func.accept(this); + this.visitArgs(call.args); + }; + ExpressionVisitor.prototype.visitCallMember = function (call) { + call.object.accept(this); + this.visitArgs(call.args); + }; + ExpressionVisitor.prototype.visitPrefix = function (prefix) { + prefix.expression.accept(this); + }; + ExpressionVisitor.prototype.visitBinary = function (binary) { + binary.left.accept(this); + binary.right.accept(this); + }; + ExpressionVisitor.prototype.visitLiteralPrimitive = function (literal) { + literal.value = literal.value; + }; + ExpressionVisitor.prototype.visitLiteralArray = function (literal) { + this.visitArgs(literal.elements); + }; + ExpressionVisitor.prototype.visitLiteralObject = function (literal) { + this.visitArgs(literal.values); + }; + ExpressionVisitor.prototype.visitLiteralString = function (literal) { + literal.value = literal.value; + }; + ExpressionVisitor.prototype.visitArgs = function (args) { + for (var i = 0; i < args.length; i++) { + args[i].accept(this); } - var result = new ValidateResult({ __manuallyAdded__: true }, object, resolvedPropertyName, false, message); - this.processResultDelta('validate', [], [result]); - return result; }; - /** - * Removes and unrenders an error. - */ - ValidationController.prototype.removeError = function (result) { - if (this.results.indexOf(result) !== -1) { - this.processResultDelta('reset', [result], []); + return ExpressionVisitor; + }()); + + var ValidationMessageParser = /** @class */ (function () { + function ValidationMessageParser(bindinqLanguage) { + this.bindinqLanguage = bindinqLanguage; + this.emptyStringExpression = new aureliaBinding.LiteralString(''); + this.nullExpression = new aureliaBinding.LiteralPrimitive(null); + this.undefinedExpression = new aureliaBinding.LiteralPrimitive(undefined); + this.cache = {}; + } + ValidationMessageParser.prototype.parse = function (message) { + if (this.cache[message] !== undefined) { + return this.cache[message]; + } + var parts = this.bindinqLanguage.parseInterpolation(null, message); + if (parts === null) { + return new aureliaBinding.LiteralString(message); + } + var expression = new aureliaBinding.LiteralString(parts[0]); + for (var i = 1; i < parts.length; i += 2) { + expression = new aureliaBinding.Binary('+', expression, new aureliaBinding.Binary('+', this.coalesce(parts[i]), new aureliaBinding.LiteralString(parts[i + 1]))); } + MessageExpressionValidator.validate(expression, message); + this.cache[message] = expression; + return expression; }; - /** - * Adds a renderer. - * @param renderer The renderer. - */ - ValidationController.prototype.addRenderer = function (renderer) { - var _this = this; - this.renderers.push(renderer); - renderer.render({ - kind: 'validate', - render: this.results.map(function (result) { return ({ result: result, elements: _this.elements.get(result) }); }), - unrender: [] - }); + ValidationMessageParser.prototype.coalesce = function (part) { + // part === null || part === undefined ? '' : part + return new aureliaBinding.Conditional(new aureliaBinding.Binary('||', new aureliaBinding.Binary('===', part, this.nullExpression), new aureliaBinding.Binary('===', part, this.undefinedExpression)), this.emptyStringExpression, new aureliaBinding.CallMember(part, 'toString', [])); }; - /** - * Removes a renderer. - * @param renderer The renderer. - */ - ValidationController.prototype.removeRenderer = function (renderer) { - var _this = this; - this.renderers.splice(this.renderers.indexOf(renderer), 1); - renderer.render({ - kind: 'reset', - render: [], - unrender: this.results.map(function (result) { return ({ result: result, elements: _this.elements.get(result) }); }) - }); + ValidationMessageParser.inject = [aureliaTemplating.BindingLanguage]; + return ValidationMessageParser; + }()); + var MessageExpressionValidator = /** @class */ (function (_super) { + __extends(MessageExpressionValidator, _super); + function MessageExpressionValidator(originalMessage) { + var _this = _super.call(this) || this; + _this.originalMessage = originalMessage; + return _this; + } + MessageExpressionValidator.validate = function (expression, originalMessage) { + var visitor = new MessageExpressionValidator(originalMessage); + expression.accept(visitor); }; - /** - * Registers a binding with the controller. - * @param binding The binding instance. - * @param target The DOM element. - * @param rules (optional) rules associated with the binding. Validator implementation specific. - */ - ValidationController.prototype.registerBinding = function (binding, target, rules) { - this.bindings.set(binding, { target: target, rules: rules, propertyInfo: null }); + MessageExpressionValidator.prototype.visitAccessScope = function (access) { + if (access.ancestor !== 0) { + throw new Error('$parent is not permitted in validation message expressions.'); + } + if (['displayName', 'propertyName', 'value', 'object', 'config', 'getDisplayName'].indexOf(access.name) !== -1) { + LogManager.getLogger('aurelia-validation') + // tslint:disable-next-line:max-line-length + .warn("Did you mean to use \"$" + access.name + "\" instead of \"" + access.name + "\" in this validation message template: \"" + this.originalMessage + "\"?"); + } }; + return MessageExpressionValidator; + }(ExpressionVisitor)); + + /** + * Dictionary of validation messages. [messageKey]: messageExpression + */ + var validationMessages = { /** - * Unregisters a binding with the controller. - * @param binding The binding instance. + * The default validation message. Used with rules that have no standard message. */ - ValidationController.prototype.unregisterBinding = function (binding) { - this.resetBinding(binding); - this.bindings.delete(binding); - }; + default: "${$displayName} is invalid.", + required: "${$displayName} is required.", + matches: "${$displayName} is not correctly formatted.", + email: "${$displayName} is not a valid email.", + minLength: "${$displayName} must be at least ${$config.length} character${$config.length === 1 ? '' : 's'}.", + maxLength: "${$displayName} cannot be longer than ${$config.length} character${$config.length === 1 ? '' : 's'}.", + minItems: "${$displayName} must contain at least ${$config.count} item${$config.count === 1 ? '' : 's'}.", + maxItems: "${$displayName} cannot contain more than ${$config.count} item${$config.count === 1 ? '' : 's'}.", + min: "${$displayName} must be at least ${$config.constraint}.", + max: "${$displayName} must be at most ${$config.constraint}.", + range: "${$displayName} must be between or equal to ${$config.min} and ${$config.max}.", + between: "${$displayName} must be between but not equal to ${$config.min} and ${$config.max}.", + equals: "${$displayName} must be ${$config.expectedValue}.", + }; + /** + * Retrieves validation messages and property display names. + */ + var ValidationMessageProvider = /** @class */ (function () { + function ValidationMessageProvider(parser) { + this.parser = parser; + } /** - * Interprets the instruction and returns a predicate that will identify - * relevant results in the list of rendered validation results. + * Returns a message binding expression that corresponds to the key. + * @param key The message key. */ - ValidationController.prototype.getInstructionPredicate = function (instruction) { - var _this = this; - if (instruction) { - var object_1 = instruction.object, propertyName_1 = instruction.propertyName, rules_1 = instruction.rules; - var predicate_1; - if (instruction.propertyName) { - predicate_1 = function (x) { return x.object === object_1 && x.propertyName === propertyName_1; }; - } - else { - predicate_1 = function (x) { return x.object === object_1; }; - } - if (rules_1) { - return function (x) { return predicate_1(x) && _this.validator.ruleExists(rules_1, x.rule); }; - } - return predicate_1; + ValidationMessageProvider.prototype.getMessage = function (key) { + var message; + if (key in validationMessages) { + message = validationMessages[key]; } else { - return function () { return true; }; + message = validationMessages['default']; } + return this.parser.parse(message); }; /** - * Validates and renders results. - * @param instruction Optional. Instructions on what to validate. If undefined, all - * objects and bindings will be validated. + * Formulates a property display name using the property name and the configured + * displayName (if provided). + * Override this with your own custom logic. + * @param propertyName The property name. */ - ValidationController.prototype.validate = function (instruction) { - var _this = this; - // Get a function that will process the validation instruction. - var execute; - if (instruction) { - // tslint:disable-next-line:prefer-const - var object_2 = instruction.object, propertyName_2 = instruction.propertyName, rules_2 = instruction.rules; - // if rules were not specified, check the object map. - rules_2 = rules_2 || this.objects.get(object_2); - // property specified? - if (instruction.propertyName === undefined) { - // validate the specified object. - execute = function () { return _this.validator.validateObject(object_2, rules_2); }; - } - else { - // validate the specified property. - execute = function () { return _this.validator.validateProperty(object_2, propertyName_2, rules_2); }; - } - } - else { - // validate all objects and bindings. - execute = function () { - var promises = []; - for (var _i = 0, _a = Array.from(_this.objects); _i < _a.length; _i++) { - var _b = _a[_i], object = _b[0], rules = _b[1]; - promises.push(_this.validator.validateObject(object, rules)); - } - for (var _c = 0, _d = Array.from(_this.bindings); _c < _d.length; _c++) { - var _e = _d[_c], binding = _e[0], rules = _e[1].rules; - var propertyInfo = getPropertyInfo(binding.sourceExpression, binding.source); - if (!propertyInfo || _this.objects.has(propertyInfo.object)) { - continue; - } - promises.push(_this.validator.validateProperty(propertyInfo.object, propertyInfo.propertyName, rules)); - } - return Promise.all(promises).then(function (resultSets) { return resultSets.reduce(function (a, b) { return a.concat(b); }, []); }); - }; + ValidationMessageProvider.prototype.getDisplayName = function (propertyName, displayName) { + if (displayName !== null && displayName !== undefined) { + return (displayName instanceof Function) ? displayName() : displayName; } - // Wait for any existing validation to finish, execute the instruction, render the results. - this.validating = true; - var returnPromise = this.finishValidating - .then(execute) - .then(function (newResults) { - var predicate = _this.getInstructionPredicate(instruction); - var oldResults = _this.results.filter(predicate); - _this.processResultDelta('validate', oldResults, newResults); - if (returnPromise === _this.finishValidating) { - _this.validating = false; - } - var result = { - instruction: instruction, - valid: newResults.find(function (x) { return !x.valid; }) === undefined, - results: newResults - }; - _this.invokeCallbacks(instruction, result); - return result; - }) - .catch(function (exception) { - // recover, to enable subsequent calls to validate() - _this.validating = false; - _this.finishValidating = Promise.resolve(); - return Promise.reject(exception); - }); - this.finishValidating = returnPromise; - return returnPromise; + // split on upper-case letters. + var words = propertyName.toString().split(/(?=[A-Z])/).join(' '); + // capitalize first letter. + return words.charAt(0).toUpperCase() + words.slice(1); + }; + ValidationMessageProvider.inject = [ValidationMessageParser]; + return ValidationMessageProvider; + }()); + + /** + * Validates. + * Responsible for validating objects and properties. + */ + var StandardValidator = /** @class */ (function (_super) { + __extends(StandardValidator, _super); + function StandardValidator(messageProvider, resources) { + var _this = _super.call(this) || this; + _this.messageProvider = messageProvider; + _this.lookupFunctions = resources.lookupFunctions; + _this.getDisplayName = messageProvider.getDisplayName.bind(messageProvider); + return _this; + } + /** + * Validates the specified property. + * @param object The object to validate. + * @param propertyName The name of the property to validate. + * @param rules Optional. If unspecified, the rules will be looked up using the metadata + * for the object created by ValidationRules....on(class/object) + */ + StandardValidator.prototype.validateProperty = function (object, propertyName, rules) { + return this.validate(object, propertyName, rules || null); }; /** - * Resets any rendered validation results (unrenders). - * @param instruction Optional. Instructions on what to reset. If unspecified all rendered results - * will be unrendered. + * Validates all rules for specified object and it's properties. + * @param object The object to validate. + * @param rules Optional. If unspecified, the rules will be looked up using the metadata + * for the object created by ValidationRules....on(class/object) */ - ValidationController.prototype.reset = function (instruction) { - var predicate = this.getInstructionPredicate(instruction); - var oldResults = this.results.filter(predicate); - this.processResultDelta('reset', oldResults, []); - this.invokeCallbacks(instruction, null); + StandardValidator.prototype.validateObject = function (object, rules) { + return this.validate(object, null, rules || null); }; /** - * Gets the elements associated with an object and propertyName (if any). + * Determines whether a rule exists in a set of rules. + * @param rules The rules to search. + * @parem rule The rule to find. */ - ValidationController.prototype.getAssociatedElements = function (_a) { - var object = _a.object, propertyName = _a.propertyName; - var elements = []; - for (var _i = 0, _b = Array.from(this.bindings); _i < _b.length; _i++) { - var _c = _b[_i], binding = _c[0], target = _c[1].target; - var propertyInfo = getPropertyInfo(binding.sourceExpression, binding.source); - if (propertyInfo && propertyInfo.object === object && propertyInfo.propertyName === propertyName) { - elements.push(target); + StandardValidator.prototype.ruleExists = function (rules, rule) { + var i = rules.length; + while (i--) { + if (rules[i].indexOf(rule) !== -1) { + return true; } } - return elements; + return false; }; - ValidationController.prototype.processResultDelta = function (kind, oldResults, newResults) { - // prepare the instruction. - var instruction = { - kind: kind, - render: [], - unrender: [] + StandardValidator.prototype.getMessage = function (rule, object, value) { + var expression = rule.message || this.messageProvider.getMessage(rule.messageKey); + // tslint:disable-next-line:prefer-const + var _a = rule.property, propertyName = _a.name, displayName = _a.displayName; + if (propertyName !== null) { + displayName = this.messageProvider.getDisplayName(propertyName, displayName); + } + var overrideContext = { + $displayName: displayName, + $propertyName: propertyName, + $value: value, + $object: object, + $config: rule.config, + // returns the name of a given property, given just the property name (irrespective of the property's displayName) + // split on capital letters, first letter ensured to be capitalized + $getDisplayName: this.getDisplayName }; - // create a shallow copy of newResults so we can mutate it without causing side-effects. - newResults = newResults.slice(0); - var _loop_1 = function (oldResult) { - // get the elements associated with the old result. - var elements = this_1.elements.get(oldResult); - // remove the old result from the element map. - this_1.elements.delete(oldResult); - // create the unrender instruction. - instruction.unrender.push({ result: oldResult, elements: elements }); - // determine if there's a corresponding new result for the old result we are unrendering. - var newResultIndex = newResults.findIndex(function (x) { return x.rule === oldResult.rule && x.object === oldResult.object && x.propertyName === oldResult.propertyName; }); - if (newResultIndex === -1) { - // no corresponding new result... simple remove. - this_1.results.splice(this_1.results.indexOf(oldResult), 1); - if (!oldResult.valid) { - this_1.errors.splice(this_1.errors.indexOf(oldResult), 1); - } + return expression.evaluate({ bindingContext: object, overrideContext: overrideContext }, this.lookupFunctions); + }; + StandardValidator.prototype.validateRuleSequence = function (object, propertyName, ruleSequence, sequence, results) { + var _this = this; + // are we validating all properties or a single property? + var validateAllProperties = propertyName === null || propertyName === undefined; + var rules = ruleSequence[sequence]; + var allValid = true; + // validate each rule. + var promises = []; + var _loop_1 = function (i) { + var rule = rules[i]; + // is the rule related to the property we're validating. + // tslint:disable-next-line:triple-equals | Use loose equality for property keys + if (!validateAllProperties && rule.property.name != propertyName) { + return "continue"; } - else { - // there is a corresponding new result... - var newResult = newResults.splice(newResultIndex, 1)[0]; - // get the elements that are associated with the new result. - var elements_1 = this_1.getAssociatedElements(newResult); - this_1.elements.set(newResult, elements_1); - // create a render instruction for the new result. - instruction.render.push({ result: newResult, elements: elements_1 }); - // do an in-place replacement of the old result with the new result. - // this ensures any repeats bound to this.results will not thrash. - this_1.results.splice(this_1.results.indexOf(oldResult), 1, newResult); - if (!oldResult.valid && newResult.valid) { - this_1.errors.splice(this_1.errors.indexOf(oldResult), 1); - } - else if (!oldResult.valid && !newResult.valid) { - this_1.errors.splice(this_1.errors.indexOf(oldResult), 1, newResult); - } - else if (!newResult.valid) { - this_1.errors.push(newResult); - } + // is this a conditional rule? is the condition met? + if (rule.when && !rule.when(object)) { + return "continue"; + } + // validate. + var value = rule.property.name === null ? object : object[rule.property.name]; + var promiseOrBoolean = rule.condition(value, object); + if (!(promiseOrBoolean instanceof Promise)) { + promiseOrBoolean = Promise.resolve(promiseOrBoolean); } + promises.push(promiseOrBoolean.then(function (valid) { + var message = valid ? null : _this.getMessage(rule, object, value); + results.push(new ValidateResult(rule, object, rule.property.name, valid, message)); + allValid = allValid && valid; + return valid; + })); }; - var this_1 = this; - // create unrender instructions from the old results. - for (var _i = 0, oldResults_1 = oldResults; _i < oldResults_1.length; _i++) { - var oldResult = oldResults_1[_i]; - _loop_1(oldResult); + for (var i = 0; i < rules.length; i++) { + _loop_1(i); } - // create render instructions from the remaining new results. - for (var _a = 0, newResults_1 = newResults; _a < newResults_1.length; _a++) { - var result = newResults_1[_a]; - var elements = this.getAssociatedElements(result); - instruction.render.push({ result: result, elements: elements }); - this.elements.set(result, elements); - this.results.push(result); - if (!result.valid) { - this.errors.push(result); + return Promise.all(promises) + .then(function () { + sequence++; + if (allValid && sequence < ruleSequence.length) { + return _this.validateRuleSequence(object, propertyName, ruleSequence, sequence, results); } + return results; + }); + }; + StandardValidator.prototype.validate = function (object, propertyName, rules) { + // rules specified? + if (!rules) { + // no. attempt to locate the rules. + rules = Rules.get(object); } - // render. - for (var _b = 0, _c = this.renderers; _b < _c.length; _b++) { - var renderer = _c[_b]; - renderer.render(instruction); + // any rules? + if (!rules || rules.length === 0) { + return Promise.resolve([]); } + return this.validateRuleSequence(object, propertyName, rules, 0, []); }; + StandardValidator.inject = [ValidationMessageProvider, aureliaTemplating.ViewResources]; + return StandardValidator; + }(Validator)); + + /** + * Validation triggers. + */ + (function (validateTrigger) { /** - * Validates the property associated with a binding. + * Manual validation. Use the controller's `validate()` and `reset()` methods + * to validate all bindings. */ - ValidationController.prototype.validateBinding = function (binding) { - if (!binding.isBound) { - return; - } - var propertyInfo = getPropertyInfo(binding.sourceExpression, binding.source); - var rules; - var registeredBinding = this.bindings.get(binding); - if (registeredBinding) { - rules = registeredBinding.rules; - registeredBinding.propertyInfo = propertyInfo; - } - if (!propertyInfo) { - return; - } - var object = propertyInfo.object, propertyName = propertyInfo.propertyName; - this.validate({ object: object, propertyName: propertyName, rules: rules }); - }; + validateTrigger[validateTrigger["manual"] = 0] = "manual"; /** - * Resets the results for a property associated with a binding. + * Validate the binding when the binding's target element fires a DOM "blur" event. */ - ValidationController.prototype.resetBinding = function (binding) { - var registeredBinding = this.bindings.get(binding); - var propertyInfo = getPropertyInfo(binding.sourceExpression, binding.source); - if (!propertyInfo && registeredBinding) { - propertyInfo = registeredBinding.propertyInfo; - } - if (registeredBinding) { - registeredBinding.propertyInfo = null; - } - if (!propertyInfo) { - return; - } - var object = propertyInfo.object, propertyName = propertyInfo.propertyName; - this.reset({ object: object, propertyName: propertyName }); - }; + validateTrigger[validateTrigger["blur"] = 1] = "blur"; /** - * Changes the controller's validateTrigger. - * @param newTrigger The new validateTrigger + * Validate the binding when it updates the model due to a change in the view. */ - ValidationController.prototype.changeTrigger = function (newTrigger) { - this.validateTrigger = newTrigger; - var bindings = Array.from(this.bindings.keys()); - for (var _i = 0, bindings_1 = bindings; _i < bindings_1.length; _i++) { - var binding = bindings_1[_i]; - var source = binding.source; - binding.unbind(); - binding.bind(source); - } - }; + validateTrigger[validateTrigger["change"] = 2] = "change"; /** - * Revalidates the controller's current set of errors. + * 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. */ - ValidationController.prototype.revalidateErrors = function () { - for (var _i = 0, _a = this.errors; _i < _a.length; _i++) { - var _b = _a[_i], object = _b.object, propertyName = _b.propertyName, rule = _b.rule; - if (rule.__manuallyAdded__) { - continue; - } - var rules = [[rule]]; - this.validate({ object: object, propertyName: propertyName, rules: rules }); - } - }; - ValidationController.prototype.invokeCallbacks = function (instruction, result) { - if (this.eventCallbacks.length === 0) { - return; - } - var event = new ValidateEvent(result ? 'validate' : 'reset', this.errors, this.results, instruction || null, result); - for (var i = 0; i < this.eventCallbacks.length; i++) { - this.eventCallbacks[i](event); - } - }; - ValidationController.inject = [Validator, PropertyAccessorParser]; - return ValidationController; - }()); + validateTrigger[validateTrigger["changeOrBlur"] = 3] = "changeOrBlur"; + })(exports.validateTrigger || (exports.validateTrigger = {})); /** - * Binding behavior. Indicates the bound property should be validated. + * Aurelia Validation Configuration API */ - var ValidateBindingBehaviorBase = /** @class */ (function () { - function ValidateBindingBehaviorBase(taskQueue) { - this.taskQueue = taskQueue; + var GlobalValidationConfiguration = /** @class */ (function () { + function GlobalValidationConfiguration() { + this.validatorType = StandardValidator; + this.validationTrigger = GlobalValidationConfiguration.DEFAULT_VALIDATION_TRIGGER; } - ValidateBindingBehaviorBase.prototype.bind = function (binding, source, rulesOrController, rules) { - var _this = this; - // identify the target element. - var target = getTargetDOMElement(binding, source); - // locate the controller. - var controller; - if (rulesOrController instanceof ValidationController) { - controller = rulesOrController; - } - else { - controller = source.container.get(aureliaDependencyInjection.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); - // tslint:disable-next-line:no-bitwise - if (trigger & exports.validateTrigger.change) { - binding.vbbUpdateSource = binding.updateSource; - // tslint:disable-next-line:only-arrow-functions - // tslint:disable-next-line:space-before-function-paren - binding.updateSource = function (value) { - this.vbbUpdateSource(value); - this.validationController.validateBinding(this); - }; - } - // tslint:disable-next-line:no-bitwise - if (trigger & exports.validateTrigger.blur) { - binding.validateBlurHandler = function () { - _this.taskQueue.queueMicroTask(function () { return controller.validateBinding(binding); }); - }; - binding.validateTarget = target; - target.addEventListener('blur', binding.validateBlurHandler); - } - if (trigger !== exports.validateTrigger.manual) { - binding.standardUpdateTarget = binding.updateTarget; - // tslint:disable-next-line:only-arrow-functions - // tslint:disable-next-line:space-before-function-paren - binding.updateTarget = function (value) { - this.standardUpdateTarget(value); - this.validationController.resetBinding(this); - }; - } + /** + * Use a custom Validator implementation. + */ + GlobalValidationConfiguration.prototype.customValidator = function (type) { + this.validatorType = type; + return this; }; - ValidateBindingBehaviorBase.prototype.unbind = function (binding) { - // reset the binding to it's original state. - if (binding.vbbUpdateSource) { - binding.updateSource = binding.vbbUpdateSource; - binding.vbbUpdateSource = 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; + GlobalValidationConfiguration.prototype.defaultValidationTrigger = function (trigger) { + this.validationTrigger = trigger; + return this; }; - return ValidateBindingBehaviorBase; + GlobalValidationConfiguration.prototype.getDefaultValidationTrigger = function () { + return this.validationTrigger; + }; + /** + * Applies the configuration. + */ + GlobalValidationConfiguration.prototype.apply = function (container) { + var validator = container.get(this.validatorType); + container.registerInstance(Validator, validator); + container.registerInstance(GlobalValidationConfiguration, this); + }; + GlobalValidationConfiguration.DEFAULT_VALIDATION_TRIGGER = exports.validateTrigger.blur; + return GlobalValidationConfiguration; }()); /** - * Binding behavior. Indicates the bound property should be validated - * when the validate trigger specified by the associated controller's - * validateTrigger property occurs. + * 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. */ - var ValidateBindingBehavior = /** @class */ (function (_super) { - __extends(ValidateBindingBehavior, _super); - function ValidateBindingBehavior() { - return _super !== null && _super.apply(this, arguments) || this; + function getTargetDOMElement(binding, view) { + var target = binding.target; + // DOM element + if (target instanceof Element) { + return target; } - ValidateBindingBehavior.prototype.getValidateTrigger = function (controller) { - return controller.validateTrigger; - }; - ValidateBindingBehavior.inject = [aureliaTaskQueue.TaskQueue]; - ValidateBindingBehavior = __decorate([ - aureliaBinding.bindingBehavior('validate') - ], ValidateBindingBehavior); - 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. - */ - var ValidateManuallyBindingBehavior = /** @class */ (function (_super) { - __extends(ValidateManuallyBindingBehavior, _super); - function ValidateManuallyBindingBehavior() { - return _super !== null && _super.apply(this, arguments) || this; + // custom element or custom attribute + // tslint:disable-next-line:prefer-const + 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(aureliaPal.DOM.Element); + if (element) { + return element; + } + throw new Error("Unable to locate target element for \"" + binding.sourceExpression + "\"."); + } } - ValidateManuallyBindingBehavior.prototype.getValidateTrigger = function () { - return exports.validateTrigger.manual; - }; - ValidateManuallyBindingBehavior.inject = [aureliaTaskQueue.TaskQueue]; - ValidateManuallyBindingBehavior = __decorate([ - aureliaBinding.bindingBehavior('validateManually') - ], ValidateManuallyBindingBehavior); - return ValidateManuallyBindingBehavior; - }(ValidateBindingBehaviorBase)); - /** - * Binding behavior. Indicates the bound property should be validated - * when the associated element blurs. - */ - var ValidateOnBlurBindingBehavior = /** @class */ (function (_super) { - __extends(ValidateOnBlurBindingBehavior, _super); - function ValidateOnBlurBindingBehavior() { - return _super !== null && _super.apply(this, arguments) || this; + throw new Error("Unable to locate target element for \"" + binding.sourceExpression + "\"."); + } + + function getObject(expression, objectExpression, source) { + var value = objectExpression.evaluate(source, null); + if (value === null || value === undefined || value instanceof Object) { + return value; } - ValidateOnBlurBindingBehavior.prototype.getValidateTrigger = function () { - return exports.validateTrigger.blur; - }; - ValidateOnBlurBindingBehavior.inject = [aureliaTaskQueue.TaskQueue]; - ValidateOnBlurBindingBehavior = __decorate([ - aureliaBinding.bindingBehavior('validateOnBlur') - ], ValidateOnBlurBindingBehavior); - return ValidateOnBlurBindingBehavior; - }(ValidateBindingBehaviorBase)); + // tslint:disable-next-line:max-line-length + throw new Error("The '" + objectExpression + "' part of '" + expression + "' evaluates to " + value + " instead of an object, null or undefined."); + } /** - * Binding behavior. Indicates the bound property should be validated - * when the associated element is changed by the user, causing a change - * to the model. + * Retrieves the object and property name for the specified expression. + * @param expression The expression + * @param source The scope */ - var ValidateOnChangeBindingBehavior = /** @class */ (function (_super) { - __extends(ValidateOnChangeBindingBehavior, _super); - function ValidateOnChangeBindingBehavior() { - return _super !== null && _super.apply(this, arguments) || this; + function getPropertyInfo(expression, source) { + var originalExpression = expression; + while (expression instanceof aureliaBinding.BindingBehavior || expression instanceof aureliaBinding.ValueConverter) { + expression = expression.expression; } - ValidateOnChangeBindingBehavior.prototype.getValidateTrigger = function () { - return exports.validateTrigger.change; - }; - ValidateOnChangeBindingBehavior.inject = [aureliaTaskQueue.TaskQueue]; - ValidateOnChangeBindingBehavior = __decorate([ - aureliaBinding.bindingBehavior('validateOnChange') - ], ValidateOnChangeBindingBehavior); - 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. - */ - var ValidateOnChangeOrBlurBindingBehavior = /** @class */ (function (_super) { - __extends(ValidateOnChangeOrBlurBindingBehavior, _super); - function ValidateOnChangeOrBlurBindingBehavior() { - return _super !== null && _super.apply(this, arguments) || this; + var object; + var propertyName; + if (expression instanceof aureliaBinding.AccessScope) { + object = aureliaBinding.getContextFor(expression.name, source, expression.ancestor); + propertyName = expression.name; } - ValidateOnChangeOrBlurBindingBehavior.prototype.getValidateTrigger = function () { - return exports.validateTrigger.changeOrBlur; + else if (expression instanceof aureliaBinding.AccessMember) { + object = getObject(originalExpression, expression.object, source); + propertyName = expression.name; + } + else if (expression instanceof aureliaBinding.AccessKeyed) { + object = getObject(originalExpression, expression.object, source); + propertyName = expression.key.evaluate(source); + } + else { + throw new Error("Expression '" + originalExpression + "' is not compatible with the validate binding-behavior."); + } + if (object === null || object === undefined) { + return null; + } + return { object: object, propertyName: propertyName }; + } + + function isString(value) { + return Object.prototype.toString.call(value) === '[object String]'; + } + function isNumber(value) { + return Object.prototype.toString.call(value) === '[object Number]'; + } + + var PropertyAccessorParser = /** @class */ (function () { + function PropertyAccessorParser(parser) { + this.parser = parser; + } + PropertyAccessorParser.prototype.parse = function (property) { + if (isString(property) || isNumber(property)) { + return property; + } + var accessorText = getAccessorExpression(property.toString()); + var accessor = this.parser.parse(accessorText); + if (accessor instanceof aureliaBinding.AccessScope + || accessor instanceof aureliaBinding.AccessMember && accessor.object instanceof aureliaBinding.AccessScope) { + return accessor.name; + } + throw new Error("Invalid property expression: \"" + accessor + "\""); }; - ValidateOnChangeOrBlurBindingBehavior.inject = [aureliaTaskQueue.TaskQueue]; - ValidateOnChangeOrBlurBindingBehavior = __decorate([ - aureliaBinding.bindingBehavior('validateOnChangeOrBlur') - ], ValidateOnChangeOrBlurBindingBehavior); - return ValidateOnChangeOrBlurBindingBehavior; - }(ValidateBindingBehaviorBase)); + PropertyAccessorParser.inject = [aureliaBinding.Parser]; + return PropertyAccessorParser; + }()); + function getAccessorExpression(fn) { + /* tslint:disable:max-line-length */ + var classic = /^function\s*\([$_\w\d]+\)\s*\{(?:\s*"use strict";)?\s*(?:[$_\w\d.['"\]+;]+)?\s*return\s+[$_\w\d]+\.([$_\w\d]+)\s*;?\s*\}$/; + /* tslint:enable:max-line-length */ + var arrow = /^\(?[$_\w\d]+\)?\s*=>\s*[$_\w\d]+\.([$_\w\d]+)$/; + var match = classic.exec(fn) || arrow.exec(fn); + if (match === null) { + throw new Error("Unable to parse accessor function:\n" + fn); + } + return match[1]; + } + + var ValidateEvent = /** @class */ (function () { + function ValidateEvent( + /** + * The type of validate event. Either "validate" or "reset". + */ + type, + /** + * The controller's current array of errors. For an array containing both + * failed rules and passed rules, use the "results" property. + */ + errors, + /** + * The controller's current array of validate results. This + * includes both passed rules and failed rules. For an array of only failed rules, + * use the "errors" property. + */ + results, + /** + * The instruction passed to the "validate" or "reset" event. Will be null when + * the controller's validate/reset method was called with no instruction argument. + */ + instruction, + /** + * In events with type === "validate", this property will contain the result + * of validating the instruction (see "instruction" property). Use the controllerValidateResult + * to access the validate results specific to the call to "validate" + * (as opposed to using the "results" and "errors" properties to access the controller's entire + * set of results/errors). + */ + controllerValidateResult) { + this.type = type; + this.errors = errors; + this.results = results; + this.instruction = instruction; + this.controllerValidateResult = controllerValidateResult; + } + return ValidateEvent; + }()); /** - * Creates ValidationController instances. + * Orchestrates validation. + * Manages a set of bindings, renderers and objects. + * Exposes the current list of validation results for binding purposes. */ - var ValidationControllerFactory = /** @class */ (function () { - function ValidationControllerFactory(container) { - this.container = container; + var ValidationController = /** @class */ (function () { + function ValidationController(validator, propertyParser, config) { + this.validator = validator; + this.propertyParser = propertyParser; + // Registered bindings (via the validate binding behavior) + this.bindings = new Map(); + // Renderers that have been added to the controller instance. + this.renderers = []; + /** + * Validation results that have been rendered by the controller. + */ + this.results = []; + /** + * Validation errors that have been rendered by the controller. + */ + this.errors = []; + /** + * Whether the controller is currently validating. + */ + this.validating = false; + // Elements related to validation results that have been rendered. + this.elements = new Map(); + // Objects that have been added to the controller instance (entity-style validation). + this.objects = new Map(); + // Promise that resolves when validation has completed. + this.finishValidating = Promise.resolve(); + this.eventCallbacks = []; + this.validateTrigger = config instanceof GlobalValidationConfiguration + ? config.getDefaultValidationTrigger() + : GlobalValidationConfiguration.DEFAULT_VALIDATION_TRIGGER; } - ValidationControllerFactory.get = function (container) { - return new ValidationControllerFactory(container); + /** + * Subscribe to controller validate and reset events. These events occur when the + * controller's "validate"" and "reset" methods are called. + * @param callback The callback to be invoked when the controller validates or resets. + */ + ValidationController.prototype.subscribe = function (callback) { + var _this = this; + this.eventCallbacks.push(callback); + return { + dispose: function () { + var index = _this.eventCallbacks.indexOf(callback); + if (index === -1) { + return; + } + _this.eventCallbacks.splice(index, 1); + } + }; }; /** - * Creates a new controller instance. + * Adds an object to the set of objects that should be validated when validate is called. + * @param object The object. + * @param rules Optional. The rules. If rules aren't supplied the Validator implementation will lookup the rules. */ - ValidationControllerFactory.prototype.create = function (validator) { - if (!validator) { - validator = this.container.get(Validator); + ValidationController.prototype.addObject = function (object, rules) { + this.objects.set(object, rules); + }; + /** + * Removes an object from the set of objects that should be validated when validate is called. + * @param object The object. + */ + ValidationController.prototype.removeObject = function (object) { + this.objects.delete(object); + this.processResultDelta('reset', this.results.filter(function (result) { return result.object === object; }), []); + }; + /** + * Adds and renders an error. + */ + ValidationController.prototype.addError = function (message, object, propertyName) { + if (propertyName === void 0) { propertyName = null; } + var resolvedPropertyName; + if (propertyName === null) { + resolvedPropertyName = propertyName; } - var propertyParser = this.container.get(PropertyAccessorParser); - return new ValidationController(validator, propertyParser); + else { + resolvedPropertyName = this.propertyParser.parse(propertyName); + } + var result = new ValidateResult({ __manuallyAdded__: true }, object, resolvedPropertyName, false, message); + this.processResultDelta('validate', [], [result]); + return result; }; /** - * 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. + * Removes and unrenders an error. */ - ValidationControllerFactory.prototype.createForCurrentScope = function (validator) { - var controller = this.create(validator); - this.container.registerInstance(ValidationController, controller); - return controller; + ValidationController.prototype.removeError = function (result) { + if (this.results.indexOf(result) !== -1) { + this.processResultDelta('reset', [result], []); + } }; - return ValidationControllerFactory; - }()); - ValidationControllerFactory['protocol:aurelia:resolver'] = true; - - var ValidationErrorsCustomAttribute = /** @class */ (function () { - function ValidationErrorsCustomAttribute(boundaryElement, controllerAccessor) { - this.boundaryElement = boundaryElement; - this.controllerAccessor = controllerAccessor; - this.controller = null; - this.errors = []; - this.errorsInternal = []; - } - ValidationErrorsCustomAttribute.inject = function () { - return [aureliaPal.DOM.Element, aureliaDependencyInjection.Lazy.of(ValidationController)]; + /** + * Adds a renderer. + * @param renderer The renderer. + */ + ValidationController.prototype.addRenderer = function (renderer) { + var _this = this; + this.renderers.push(renderer); + renderer.render({ + kind: 'validate', + render: this.results.map(function (result) { return ({ result: result, elements: _this.elements.get(result) }); }), + unrender: [] + }); }; - ValidationErrorsCustomAttribute.prototype.sort = function () { - this.errorsInternal.sort(function (a, b) { - if (a.targets[0] === b.targets[0]) { - return 0; - } - // tslint:disable-next-line:no-bitwise - return a.targets[0].compareDocumentPosition(b.targets[0]) & 2 ? 1 : -1; + /** + * Removes a renderer. + * @param renderer The renderer. + */ + ValidationController.prototype.removeRenderer = function (renderer) { + var _this = this; + this.renderers.splice(this.renderers.indexOf(renderer), 1); + renderer.render({ + kind: 'reset', + render: [], + unrender: this.results.map(function (result) { return ({ result: result, elements: _this.elements.get(result) }); }) }); }; - ValidationErrorsCustomAttribute.prototype.interestingElements = function (elements) { + /** + * Registers a binding with the controller. + * @param binding The binding instance. + * @param target The DOM element. + * @param rules (optional) rules associated with the binding. Validator implementation specific. + */ + ValidationController.prototype.registerBinding = function (binding, target, rules) { + this.bindings.set(binding, { target: target, rules: rules, propertyInfo: null }); + }; + /** + * Unregisters a binding with the controller. + * @param binding The binding instance. + */ + ValidationController.prototype.unregisterBinding = function (binding) { + this.resetBinding(binding); + this.bindings.delete(binding); + }; + /** + * Interprets the instruction and returns a predicate that will identify + * relevant results in the list of rendered validation results. + */ + ValidationController.prototype.getInstructionPredicate = function (instruction) { var _this = this; - return elements.filter(function (e) { return _this.boundaryElement.contains(e); }); + if (instruction) { + var object_1 = instruction.object, propertyName_1 = instruction.propertyName, rules_1 = instruction.rules; + var predicate_1; + if (instruction.propertyName) { + predicate_1 = function (x) { return x.object === object_1 && x.propertyName === propertyName_1; }; + } + else { + predicate_1 = function (x) { return x.object === object_1; }; + } + if (rules_1) { + return function (x) { return predicate_1(x) && _this.validator.ruleExists(rules_1, x.rule); }; + } + return predicate_1; + } + else { + return function () { return true; }; + } + }; + /** + * Validates and renders results. + * @param instruction Optional. Instructions on what to validate. If undefined, all + * objects and bindings will be validated. + */ + ValidationController.prototype.validate = function (instruction) { + var _this = this; + // Get a function that will process the validation instruction. + var execute; + if (instruction) { + // tslint:disable-next-line:prefer-const + var object_2 = instruction.object, propertyName_2 = instruction.propertyName, rules_2 = instruction.rules; + // if rules were not specified, check the object map. + rules_2 = rules_2 || this.objects.get(object_2); + // property specified? + if (instruction.propertyName === undefined) { + // validate the specified object. + execute = function () { return _this.validator.validateObject(object_2, rules_2); }; + } + else { + // validate the specified property. + execute = function () { return _this.validator.validateProperty(object_2, propertyName_2, rules_2); }; + } + } + else { + // validate all objects and bindings. + execute = function () { + var promises = []; + for (var _i = 0, _a = Array.from(_this.objects); _i < _a.length; _i++) { + var _b = _a[_i], object = _b[0], rules = _b[1]; + promises.push(_this.validator.validateObject(object, rules)); + } + for (var _c = 0, _d = Array.from(_this.bindings); _c < _d.length; _c++) { + var _e = _d[_c], binding = _e[0], rules = _e[1].rules; + var propertyInfo = getPropertyInfo(binding.sourceExpression, binding.source); + if (!propertyInfo || _this.objects.has(propertyInfo.object)) { + continue; + } + promises.push(_this.validator.validateProperty(propertyInfo.object, propertyInfo.propertyName, rules)); + } + return Promise.all(promises).then(function (resultSets) { return resultSets.reduce(function (a, b) { return a.concat(b); }, []); }); + }; + } + // Wait for any existing validation to finish, execute the instruction, render the results. + this.validating = true; + var returnPromise = this.finishValidating + .then(execute) + .then(function (newResults) { + var predicate = _this.getInstructionPredicate(instruction); + var oldResults = _this.results.filter(predicate); + _this.processResultDelta('validate', oldResults, newResults); + if (returnPromise === _this.finishValidating) { + _this.validating = false; + } + var result = { + instruction: instruction, + valid: newResults.find(function (x) { return !x.valid; }) === undefined, + results: newResults + }; + _this.invokeCallbacks(instruction, result); + return result; + }) + .catch(function (exception) { + // recover, to enable subsequent calls to validate() + _this.validating = false; + _this.finishValidating = Promise.resolve(); + return Promise.reject(exception); + }); + this.finishValidating = returnPromise; + return returnPromise; + }; + /** + * Resets any rendered validation results (unrenders). + * @param instruction Optional. Instructions on what to reset. If unspecified all rendered results + * will be unrendered. + */ + ValidationController.prototype.reset = function (instruction) { + var predicate = this.getInstructionPredicate(instruction); + var oldResults = this.results.filter(predicate); + this.processResultDelta('reset', oldResults, []); + this.invokeCallbacks(instruction, null); + }; + /** + * Gets the elements associated with an object and propertyName (if any). + */ + ValidationController.prototype.getAssociatedElements = function (_a) { + var object = _a.object, propertyName = _a.propertyName; + var elements = []; + for (var _i = 0, _b = Array.from(this.bindings); _i < _b.length; _i++) { + var _c = _b[_i], binding = _c[0], target = _c[1].target; + var propertyInfo = getPropertyInfo(binding.sourceExpression, binding.source); + if (propertyInfo && propertyInfo.object === object && propertyInfo.propertyName === propertyName) { + elements.push(target); + } + } + return elements; }; - ValidationErrorsCustomAttribute.prototype.render = function (instruction) { - var _loop_1 = function (result) { - var index = this_1.errorsInternal.findIndex(function (x) { return x.error === result; }); - if (index !== -1) { - this_1.errorsInternal.splice(index, 1); + ValidationController.prototype.processResultDelta = function (kind, oldResults, newResults) { + // prepare the instruction. + var instruction = { + kind: kind, + render: [], + unrender: [] + }; + // create a shallow copy of newResults so we can mutate it without causing side-effects. + newResults = newResults.slice(0); + var _loop_1 = function (oldResult) { + // get the elements associated with the old result. + var elements = this_1.elements.get(oldResult); + // remove the old result from the element map. + this_1.elements.delete(oldResult); + // create the unrender instruction. + instruction.unrender.push({ result: oldResult, elements: elements }); + // determine if there's a corresponding new result for the old result we are unrendering. + var newResultIndex = newResults.findIndex(function (x) { return x.rule === oldResult.rule && x.object === oldResult.object && x.propertyName === oldResult.propertyName; }); + if (newResultIndex === -1) { + // no corresponding new result... simple remove. + this_1.results.splice(this_1.results.indexOf(oldResult), 1); + if (!oldResult.valid) { + this_1.errors.splice(this_1.errors.indexOf(oldResult), 1); + } + } + else { + // there is a corresponding new result... + var newResult = newResults.splice(newResultIndex, 1)[0]; + // get the elements that are associated with the new result. + var elements_1 = this_1.getAssociatedElements(newResult); + this_1.elements.set(newResult, elements_1); + // create a render instruction for the new result. + instruction.render.push({ result: newResult, elements: elements_1 }); + // do an in-place replacement of the old result with the new result. + // this ensures any repeats bound to this.results will not thrash. + this_1.results.splice(this_1.results.indexOf(oldResult), 1, newResult); + if (!oldResult.valid && newResult.valid) { + this_1.errors.splice(this_1.errors.indexOf(oldResult), 1); + } + else if (!oldResult.valid && !newResult.valid) { + this_1.errors.splice(this_1.errors.indexOf(oldResult), 1, newResult); + } + else if (!newResult.valid) { + this_1.errors.push(newResult); + } } }; var this_1 = this; - for (var _i = 0, _a = instruction.unrender; _i < _a.length; _i++) { - var result = _a[_i].result; - _loop_1(result); + // create unrender instructions from the old results. + for (var _i = 0, oldResults_1 = oldResults; _i < oldResults_1.length; _i++) { + var oldResult = oldResults_1[_i]; + _loop_1(oldResult); } - for (var _b = 0, _c = instruction.render; _b < _c.length; _b++) { - var _d = _c[_b], result = _d.result, elements = _d.elements; - if (result.valid) { - continue; - } - var targets = this.interestingElements(elements); - if (targets.length) { - this.errorsInternal.push({ error: result, targets: targets }); + // create render instructions from the remaining new results. + for (var _a = 0, newResults_1 = newResults; _a < newResults_1.length; _a++) { + var result = newResults_1[_a]; + var elements = this.getAssociatedElements(result); + instruction.render.push({ result: result, elements: elements }); + this.elements.set(result, elements); + this.results.push(result); + if (!result.valid) { + this.errors.push(result); } } - this.sort(); - this.errors = this.errorsInternal; - }; - ValidationErrorsCustomAttribute.prototype.bind = function () { - if (!this.controller) { - this.controller = this.controllerAccessor(); - } - // this will call render() with the side-effect of updating this.errors - this.controller.addRenderer(this); - }; - ValidationErrorsCustomAttribute.prototype.unbind = function () { - if (this.controller) { - this.controller.removeRenderer(this); + // render. + for (var _b = 0, _c = this.renderers; _b < _c.length; _b++) { + var renderer = _c[_b]; + renderer.render(instruction); } }; - __decorate([ - aureliaTemplating.bindable({ defaultBindingMode: aureliaBinding.bindingMode.oneWay }) - ], ValidationErrorsCustomAttribute.prototype, "controller", void 0); - __decorate([ - aureliaTemplating.bindable({ primaryProperty: true, defaultBindingMode: aureliaBinding.bindingMode.twoWay }) - ], ValidationErrorsCustomAttribute.prototype, "errors", void 0); - ValidationErrorsCustomAttribute = __decorate([ - aureliaTemplating.customAttribute('validation-errors') - ], ValidationErrorsCustomAttribute); - return ValidationErrorsCustomAttribute; - }()); - - var ValidationRendererCustomAttribute = /** @class */ (function () { - function ValidationRendererCustomAttribute() { - } - ValidationRendererCustomAttribute.prototype.created = function (view) { - this.container = view.container; - }; - ValidationRendererCustomAttribute.prototype.bind = function () { - this.controller = this.container.get(ValidationController); - this.renderer = this.container.get(this.value); - this.controller.addRenderer(this.renderer); - }; - ValidationRendererCustomAttribute.prototype.unbind = function () { - this.controller.removeRenderer(this.renderer); - this.controller = null; - this.renderer = null; - }; - ValidationRendererCustomAttribute = __decorate([ - aureliaTemplating.customAttribute('validation-renderer') - ], ValidationRendererCustomAttribute); - return ValidationRendererCustomAttribute; - }()); - - /** - * Sets, unsets and retrieves rules on an object or constructor function. - */ - var Rules = /** @class */ (function () { - function Rules() { - } /** - * Applies the rules to a target. + * Validates the property associated with a binding. */ - Rules.set = function (target, rules) { - if (target instanceof Function) { - target = target.prototype; + ValidationController.prototype.validateBinding = function (binding) { + if (!binding.isBound) { + return; } - Object.defineProperty(target, Rules.key, { enumerable: false, configurable: false, writable: true, value: rules }); - }; - /** - * Removes rules from a target. - */ - Rules.unset = function (target) { - if (target instanceof Function) { - target = target.prototype; + var propertyInfo = getPropertyInfo(binding.sourceExpression, binding.source); + var rules; + var registeredBinding = this.bindings.get(binding); + if (registeredBinding) { + rules = registeredBinding.rules; + registeredBinding.propertyInfo = propertyInfo; } - target[Rules.key] = null; + if (!propertyInfo) { + return; + } + var object = propertyInfo.object, propertyName = propertyInfo.propertyName; + this.validate({ object: object, propertyName: propertyName, rules: rules }); }; /** - * Retrieves the target's rules. + * Resets the results for a property associated with a binding. */ - Rules.get = function (target) { - return target[Rules.key] || null; + ValidationController.prototype.resetBinding = function (binding) { + var registeredBinding = this.bindings.get(binding); + var propertyInfo = getPropertyInfo(binding.sourceExpression, binding.source); + if (!propertyInfo && registeredBinding) { + propertyInfo = registeredBinding.propertyInfo; + } + if (registeredBinding) { + registeredBinding.propertyInfo = null; + } + if (!propertyInfo) { + return; + } + var object = propertyInfo.object, propertyName = propertyInfo.propertyName; + this.reset({ object: object, propertyName: propertyName }); }; /** - * The name of the property that stores the rules. + * Changes the controller's validateTrigger. + * @param newTrigger The new validateTrigger */ - Rules.key = '__rules__'; - return Rules; - }()); - - // tslint:disable:no-empty - var ExpressionVisitor = /** @class */ (function () { - function ExpressionVisitor() { - } - ExpressionVisitor.prototype.visitChain = function (chain) { - this.visitArgs(chain.expressions); - }; - ExpressionVisitor.prototype.visitBindingBehavior = function (behavior) { - behavior.expression.accept(this); - this.visitArgs(behavior.args); - }; - ExpressionVisitor.prototype.visitValueConverter = function (converter) { - converter.expression.accept(this); - this.visitArgs(converter.args); - }; - ExpressionVisitor.prototype.visitAssign = function (assign) { - assign.target.accept(this); - assign.value.accept(this); - }; - ExpressionVisitor.prototype.visitConditional = function (conditional) { - conditional.condition.accept(this); - conditional.yes.accept(this); - conditional.no.accept(this); - }; - ExpressionVisitor.prototype.visitAccessThis = function (access) { - access.ancestor = access.ancestor; - }; - ExpressionVisitor.prototype.visitAccessScope = function (access) { - access.name = access.name; - }; - ExpressionVisitor.prototype.visitAccessMember = function (access) { - access.object.accept(this); - }; - ExpressionVisitor.prototype.visitAccessKeyed = function (access) { - access.object.accept(this); - access.key.accept(this); - }; - ExpressionVisitor.prototype.visitCallScope = function (call) { - this.visitArgs(call.args); - }; - ExpressionVisitor.prototype.visitCallFunction = function (call) { - call.func.accept(this); - this.visitArgs(call.args); - }; - ExpressionVisitor.prototype.visitCallMember = function (call) { - call.object.accept(this); - this.visitArgs(call.args); - }; - ExpressionVisitor.prototype.visitPrefix = function (prefix) { - prefix.expression.accept(this); - }; - ExpressionVisitor.prototype.visitBinary = function (binary) { - binary.left.accept(this); - binary.right.accept(this); - }; - ExpressionVisitor.prototype.visitLiteralPrimitive = function (literal) { - literal.value = literal.value; - }; - ExpressionVisitor.prototype.visitLiteralArray = function (literal) { - this.visitArgs(literal.elements); - }; - ExpressionVisitor.prototype.visitLiteralObject = function (literal) { - this.visitArgs(literal.values); + ValidationController.prototype.changeTrigger = function (newTrigger) { + this.validateTrigger = newTrigger; + var bindings = Array.from(this.bindings.keys()); + for (var _i = 0, bindings_1 = bindings; _i < bindings_1.length; _i++) { + var binding = bindings_1[_i]; + var source = binding.source; + binding.unbind(); + binding.bind(source); + } }; - ExpressionVisitor.prototype.visitLiteralString = function (literal) { - literal.value = literal.value; + /** + * Revalidates the controller's current set of errors. + */ + ValidationController.prototype.revalidateErrors = function () { + for (var _i = 0, _a = this.errors; _i < _a.length; _i++) { + var _b = _a[_i], object = _b.object, propertyName = _b.propertyName, rule = _b.rule; + if (rule.__manuallyAdded__) { + continue; + } + var rules = [[rule]]; + this.validate({ object: object, propertyName: propertyName, rules: rules }); + } }; - ExpressionVisitor.prototype.visitArgs = function (args) { - for (var i = 0; i < args.length; i++) { - args[i].accept(this); + ValidationController.prototype.invokeCallbacks = function (instruction, result) { + if (this.eventCallbacks.length === 0) { + return; + } + var event = new ValidateEvent(result ? 'validate' : 'reset', this.errors, this.results, instruction || null, result); + for (var i = 0; i < this.eventCallbacks.length; i++) { + this.eventCallbacks[i](event); } }; - return ExpressionVisitor; + ValidationController.inject = [Validator, PropertyAccessorParser, GlobalValidationConfiguration]; + return ValidationController; }()); - var ValidationMessageParser = /** @class */ (function () { - function ValidationMessageParser(bindinqLanguage) { - this.bindinqLanguage = bindinqLanguage; - this.emptyStringExpression = new aureliaBinding.LiteralString(''); - this.nullExpression = new aureliaBinding.LiteralPrimitive(null); - this.undefinedExpression = new aureliaBinding.LiteralPrimitive(undefined); - this.cache = {}; + /** + * Binding behavior. Indicates the bound property should be validated. + */ + var ValidateBindingBehaviorBase = /** @class */ (function () { + function ValidateBindingBehaviorBase(taskQueue) { + this.taskQueue = taskQueue; } - ValidationMessageParser.prototype.parse = function (message) { - if (this.cache[message] !== undefined) { - return this.cache[message]; + ValidateBindingBehaviorBase.prototype.bind = function (binding, source, rulesOrController, rules) { + var _this = this; + // identify the target element. + var target = getTargetDOMElement(binding, source); + // locate the controller. + var controller; + if (rulesOrController instanceof ValidationController) { + controller = rulesOrController; } - var parts = this.bindinqLanguage.parseInterpolation(null, message); - if (parts === null) { - return new aureliaBinding.LiteralString(message); + else { + controller = source.container.get(aureliaDependencyInjection.Optional.of(ValidationController)); + rules = rulesOrController; } - var expression = new aureliaBinding.LiteralString(parts[0]); - for (var i = 1; i < parts.length; i += 2) { - expression = new aureliaBinding.Binary('+', expression, new aureliaBinding.Binary('+', this.coalesce(parts[i]), new aureliaBinding.LiteralString(parts[i + 1]))); + 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); + // tslint:disable-next-line:no-bitwise + if (trigger & exports.validateTrigger.change) { + binding.vbbUpdateSource = binding.updateSource; + // tslint:disable-next-line:only-arrow-functions + // tslint:disable-next-line:space-before-function-paren + binding.updateSource = function (value) { + this.vbbUpdateSource(value); + this.validationController.validateBinding(this); + }; + } + // tslint:disable-next-line:no-bitwise + if (trigger & exports.validateTrigger.blur) { + binding.validateBlurHandler = function () { + _this.taskQueue.queueMicroTask(function () { return controller.validateBinding(binding); }); + }; + binding.validateTarget = target; + target.addEventListener('blur', binding.validateBlurHandler); + } + if (trigger !== exports.validateTrigger.manual) { + binding.standardUpdateTarget = binding.updateTarget; + // tslint:disable-next-line:only-arrow-functions + // tslint:disable-next-line:space-before-function-paren + binding.updateTarget = function (value) { + this.standardUpdateTarget(value); + this.validationController.resetBinding(this); + }; } - MessageExpressionValidator.validate(expression, message); - this.cache[message] = expression; - return expression; - }; - ValidationMessageParser.prototype.coalesce = function (part) { - // part === null || part === undefined ? '' : part - return new aureliaBinding.Conditional(new aureliaBinding.Binary('||', new aureliaBinding.Binary('===', part, this.nullExpression), new aureliaBinding.Binary('===', part, this.undefinedExpression)), this.emptyStringExpression, new aureliaBinding.CallMember(part, 'toString', [])); - }; - ValidationMessageParser.inject = [aureliaTemplating.BindingLanguage]; - return ValidationMessageParser; - }()); - var MessageExpressionValidator = /** @class */ (function (_super) { - __extends(MessageExpressionValidator, _super); - function MessageExpressionValidator(originalMessage) { - var _this = _super.call(this) || this; - _this.originalMessage = originalMessage; - return _this; - } - MessageExpressionValidator.validate = function (expression, originalMessage) { - var visitor = new MessageExpressionValidator(originalMessage); - expression.accept(visitor); }; - MessageExpressionValidator.prototype.visitAccessScope = function (access) { - if (access.ancestor !== 0) { - throw new Error('$parent is not permitted in validation message expressions.'); + ValidateBindingBehaviorBase.prototype.unbind = function (binding) { + // reset the binding to it's original state. + if (binding.vbbUpdateSource) { + binding.updateSource = binding.vbbUpdateSource; + binding.vbbUpdateSource = null; } - if (['displayName', 'propertyName', 'value', 'object', 'config', 'getDisplayName'].indexOf(access.name) !== -1) { - LogManager.getLogger('aurelia-validation') - // tslint:disable-next-line:max-line-length - .warn("Did you mean to use \"$" + access.name + "\" instead of \"" + access.name + "\" in this validation message template: \"" + this.originalMessage + "\"?"); + 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 MessageExpressionValidator; - }(ExpressionVisitor)); + return ValidateBindingBehaviorBase; + }()); /** - * Dictionary of validation messages. [messageKey]: messageExpression + * Binding behavior. Indicates the bound property should be validated + * when the validate trigger specified by the associated controller's + * validateTrigger property occurs. */ - var validationMessages = { - /** - * The default validation message. Used with rules that have no standard message. - */ - default: "${$displayName} is invalid.", - required: "${$displayName} is required.", - matches: "${$displayName} is not correctly formatted.", - email: "${$displayName} is not a valid email.", - minLength: "${$displayName} must be at least ${$config.length} character${$config.length === 1 ? '' : 's'}.", - maxLength: "${$displayName} cannot be longer than ${$config.length} character${$config.length === 1 ? '' : 's'}.", - minItems: "${$displayName} must contain at least ${$config.count} item${$config.count === 1 ? '' : 's'}.", - maxItems: "${$displayName} cannot contain more than ${$config.count} item${$config.count === 1 ? '' : 's'}.", - min: "${$displayName} must be at least ${$config.constraint}.", - max: "${$displayName} must be at most ${$config.constraint}.", - range: "${$displayName} must be between or equal to ${$config.min} and ${$config.max}.", - between: "${$displayName} must be between but not equal to ${$config.min} and ${$config.max}.", - equals: "${$displayName} must be ${$config.expectedValue}.", - }; + var ValidateBindingBehavior = /** @class */ (function (_super) { + __extends(ValidateBindingBehavior, _super); + function ValidateBindingBehavior() { + return _super !== null && _super.apply(this, arguments) || this; + } + ValidateBindingBehavior.prototype.getValidateTrigger = function (controller) { + return controller.validateTrigger; + }; + ValidateBindingBehavior.inject = [aureliaTaskQueue.TaskQueue]; + ValidateBindingBehavior = __decorate([ + aureliaBinding.bindingBehavior('validate') + ], ValidateBindingBehavior); + return ValidateBindingBehavior; + }(ValidateBindingBehaviorBase)); /** - * Retrieves validation messages and property display names. + * 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 ValidationMessageProvider = /** @class */ (function () { - function ValidationMessageProvider(parser) { - this.parser = parser; + var ValidateManuallyBindingBehavior = /** @class */ (function (_super) { + __extends(ValidateManuallyBindingBehavior, _super); + function ValidateManuallyBindingBehavior() { + return _super !== null && _super.apply(this, arguments) || this; } - /** - * Returns a message binding expression that corresponds to the key. - * @param key The message key. - */ - ValidationMessageProvider.prototype.getMessage = function (key) { - var message; - if (key in validationMessages) { - message = validationMessages[key]; - } - else { - message = validationMessages['default']; - } - return this.parser.parse(message); + ValidateManuallyBindingBehavior.prototype.getValidateTrigger = function () { + return exports.validateTrigger.manual; }; - /** - * Formulates a property display name using the property name and the configured - * displayName (if provided). - * Override this with your own custom logic. - * @param propertyName The property name. - */ - ValidationMessageProvider.prototype.getDisplayName = function (propertyName, displayName) { - if (displayName !== null && displayName !== undefined) { - return (displayName instanceof Function) ? displayName() : displayName; - } - // split on upper-case letters. - var words = propertyName.toString().split(/(?=[A-Z])/).join(' '); - // capitalize first letter. - return words.charAt(0).toUpperCase() + words.slice(1); + ValidateManuallyBindingBehavior.inject = [aureliaTaskQueue.TaskQueue]; + ValidateManuallyBindingBehavior = __decorate([ + aureliaBinding.bindingBehavior('validateManually') + ], ValidateManuallyBindingBehavior); + return ValidateManuallyBindingBehavior; + }(ValidateBindingBehaviorBase)); + /** + * Binding behavior. Indicates the bound property should be validated + * when the associated element blurs. + */ + var ValidateOnBlurBindingBehavior = /** @class */ (function (_super) { + __extends(ValidateOnBlurBindingBehavior, _super); + function ValidateOnBlurBindingBehavior() { + return _super !== null && _super.apply(this, arguments) || this; + } + ValidateOnBlurBindingBehavior.prototype.getValidateTrigger = function () { + return exports.validateTrigger.blur; + }; + ValidateOnBlurBindingBehavior.inject = [aureliaTaskQueue.TaskQueue]; + ValidateOnBlurBindingBehavior = __decorate([ + aureliaBinding.bindingBehavior('validateOnBlur') + ], ValidateOnBlurBindingBehavior); + 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. + */ + var ValidateOnChangeBindingBehavior = /** @class */ (function (_super) { + __extends(ValidateOnChangeBindingBehavior, _super); + function ValidateOnChangeBindingBehavior() { + return _super !== null && _super.apply(this, arguments) || this; + } + ValidateOnChangeBindingBehavior.prototype.getValidateTrigger = function () { + return exports.validateTrigger.change; + }; + ValidateOnChangeBindingBehavior.inject = [aureliaTaskQueue.TaskQueue]; + ValidateOnChangeBindingBehavior = __decorate([ + aureliaBinding.bindingBehavior('validateOnChange') + ], ValidateOnChangeBindingBehavior); + 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. + */ + var ValidateOnChangeOrBlurBindingBehavior = /** @class */ (function (_super) { + __extends(ValidateOnChangeOrBlurBindingBehavior, _super); + function ValidateOnChangeOrBlurBindingBehavior() { + return _super !== null && _super.apply(this, arguments) || this; + } + ValidateOnChangeOrBlurBindingBehavior.prototype.getValidateTrigger = function () { + return exports.validateTrigger.changeOrBlur; }; - ValidationMessageProvider.inject = [ValidationMessageParser]; - return ValidationMessageProvider; - }()); + ValidateOnChangeOrBlurBindingBehavior.inject = [aureliaTaskQueue.TaskQueue]; + ValidateOnChangeOrBlurBindingBehavior = __decorate([ + aureliaBinding.bindingBehavior('validateOnChangeOrBlur') + ], ValidateOnChangeOrBlurBindingBehavior); + return ValidateOnChangeOrBlurBindingBehavior; + }(ValidateBindingBehaviorBase)); /** - * Validates. - * Responsible for validating objects and properties. + * Creates ValidationController instances. */ - var StandardValidator = /** @class */ (function (_super) { - __extends(StandardValidator, _super); - function StandardValidator(messageProvider, resources) { - var _this = _super.call(this) || this; - _this.messageProvider = messageProvider; - _this.lookupFunctions = resources.lookupFunctions; - _this.getDisplayName = messageProvider.getDisplayName.bind(messageProvider); - return _this; + var ValidationControllerFactory = /** @class */ (function () { + function ValidationControllerFactory(container) { + this.container = container; } - /** - * Validates the specified property. - * @param object The object to validate. - * @param propertyName The name of the property to validate. - * @param rules Optional. If unspecified, the rules will be looked up using the metadata - * for the object created by ValidationRules....on(class/object) - */ - StandardValidator.prototype.validateProperty = function (object, propertyName, rules) { - return this.validate(object, propertyName, rules || null); + ValidationControllerFactory.get = function (container) { + return new ValidationControllerFactory(container); }; /** - * Validates all rules for specified object and it's properties. - * @param object The object to validate. - * @param rules Optional. If unspecified, the rules will be looked up using the metadata - * for the object created by ValidationRules....on(class/object) + * Creates a new controller instance. */ - StandardValidator.prototype.validateObject = function (object, rules) { - return this.validate(object, null, rules || null); + ValidationControllerFactory.prototype.create = function (validator) { + if (!validator) { + validator = this.container.get(Validator); + } + var propertyParser = this.container.get(PropertyAccessorParser); + var config = this.container.get(GlobalValidationConfiguration); + return new ValidationController(validator, propertyParser, config); }; /** - * Determines whether a rule exists in a set of rules. - * @param rules The rules to search. - * @parem rule The rule to find. + * 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. */ - StandardValidator.prototype.ruleExists = function (rules, rule) { - var i = rules.length; - while (i--) { - if (rules[i].indexOf(rule) !== -1) { - return true; - } - } - return false; + ValidationControllerFactory.prototype.createForCurrentScope = function (validator) { + var controller = this.create(validator); + this.container.registerInstance(ValidationController, controller); + return controller; }; - StandardValidator.prototype.getMessage = function (rule, object, value) { - var expression = rule.message || this.messageProvider.getMessage(rule.messageKey); - // tslint:disable-next-line:prefer-const - var _a = rule.property, propertyName = _a.name, displayName = _a.displayName; - if (propertyName !== null) { - displayName = this.messageProvider.getDisplayName(propertyName, displayName); - } - var overrideContext = { - $displayName: displayName, - $propertyName: propertyName, - $value: value, - $object: object, - $config: rule.config, - // returns the name of a given property, given just the property name (irrespective of the property's displayName) - // split on capital letters, first letter ensured to be capitalized - $getDisplayName: this.getDisplayName - }; - return expression.evaluate({ bindingContext: object, overrideContext: overrideContext }, this.lookupFunctions); + return ValidationControllerFactory; + }()); + ValidationControllerFactory['protocol:aurelia:resolver'] = true; + + var ValidationErrorsCustomAttribute = /** @class */ (function () { + function ValidationErrorsCustomAttribute(boundaryElement, controllerAccessor) { + this.boundaryElement = boundaryElement; + this.controllerAccessor = controllerAccessor; + this.controller = null; + this.errors = []; + this.errorsInternal = []; + } + ValidationErrorsCustomAttribute.inject = function () { + return [aureliaPal.DOM.Element, aureliaDependencyInjection.Lazy.of(ValidationController)]; }; - StandardValidator.prototype.validateRuleSequence = function (object, propertyName, ruleSequence, sequence, results) { - var _this = this; - // are we validating all properties or a single property? - var validateAllProperties = propertyName === null || propertyName === undefined; - var rules = ruleSequence[sequence]; - var allValid = true; - // validate each rule. - var promises = []; - var _loop_1 = function (i) { - var rule = rules[i]; - // is the rule related to the property we're validating. - // tslint:disable-next-line:triple-equals | Use loose equality for property keys - if (!validateAllProperties && rule.property.name != propertyName) { - return "continue"; - } - // is this a conditional rule? is the condition met? - if (rule.when && !rule.when(object)) { - return "continue"; + ValidationErrorsCustomAttribute.prototype.sort = function () { + this.errorsInternal.sort(function (a, b) { + if (a.targets[0] === b.targets[0]) { + return 0; } - // validate. - var value = rule.property.name === null ? object : object[rule.property.name]; - var promiseOrBoolean = rule.condition(value, object); - if (!(promiseOrBoolean instanceof Promise)) { - promiseOrBoolean = Promise.resolve(promiseOrBoolean); + // tslint:disable-next-line:no-bitwise + return a.targets[0].compareDocumentPosition(b.targets[0]) & 2 ? 1 : -1; + }); + }; + ValidationErrorsCustomAttribute.prototype.interestingElements = function (elements) { + var _this = this; + return elements.filter(function (e) { return _this.boundaryElement.contains(e); }); + }; + ValidationErrorsCustomAttribute.prototype.render = function (instruction) { + var _loop_1 = function (result) { + var index = this_1.errorsInternal.findIndex(function (x) { return x.error === result; }); + if (index !== -1) { + this_1.errorsInternal.splice(index, 1); } - promises.push(promiseOrBoolean.then(function (valid) { - var message = valid ? null : _this.getMessage(rule, object, value); - results.push(new ValidateResult(rule, object, rule.property.name, valid, message)); - allValid = allValid && valid; - return valid; - })); }; - for (var i = 0; i < rules.length; i++) { - _loop_1(i); + var this_1 = this; + for (var _i = 0, _a = instruction.unrender; _i < _a.length; _i++) { + var result = _a[_i].result; + _loop_1(result); } - return Promise.all(promises) - .then(function () { - sequence++; - if (allValid && sequence < ruleSequence.length) { - return _this.validateRuleSequence(object, propertyName, ruleSequence, sequence, results); + for (var _b = 0, _c = instruction.render; _b < _c.length; _b++) { + var _d = _c[_b], result = _d.result, elements = _d.elements; + if (result.valid) { + continue; } - return results; - }); + var targets = this.interestingElements(elements); + if (targets.length) { + this.errorsInternal.push({ error: result, targets: targets }); + } + } + this.sort(); + this.errors = this.errorsInternal; }; - StandardValidator.prototype.validate = function (object, propertyName, rules) { - // rules specified? - if (!rules) { - // no. attempt to locate the rules. - rules = Rules.get(object); + ValidationErrorsCustomAttribute.prototype.bind = function () { + if (!this.controller) { + this.controller = this.controllerAccessor(); } - // any rules? - if (!rules || rules.length === 0) { - return Promise.resolve([]); + // this will call render() with the side-effect of updating this.errors + this.controller.addRenderer(this); + }; + ValidationErrorsCustomAttribute.prototype.unbind = function () { + if (this.controller) { + this.controller.removeRenderer(this); } - return this.validateRuleSequence(object, propertyName, rules, 0, []); }; - StandardValidator.inject = [ValidationMessageProvider, aureliaTemplating.ViewResources]; - return StandardValidator; - }(Validator)); + __decorate([ + aureliaTemplating.bindable({ defaultBindingMode: aureliaBinding.bindingMode.oneWay }) + ], ValidationErrorsCustomAttribute.prototype, "controller", void 0); + __decorate([ + aureliaTemplating.bindable({ primaryProperty: true, defaultBindingMode: aureliaBinding.bindingMode.twoWay }) + ], ValidationErrorsCustomAttribute.prototype, "errors", void 0); + ValidationErrorsCustomAttribute = __decorate([ + aureliaTemplating.customAttribute('validation-errors') + ], ValidationErrorsCustomAttribute); + return ValidationErrorsCustomAttribute; + }()); + + var ValidationRendererCustomAttribute = /** @class */ (function () { + function ValidationRendererCustomAttribute() { + } + ValidationRendererCustomAttribute.prototype.created = function (view) { + this.container = view.container; + }; + ValidationRendererCustomAttribute.prototype.bind = function () { + this.controller = this.container.get(ValidationController); + this.renderer = this.container.get(this.value); + this.controller.addRenderer(this.renderer); + }; + ValidationRendererCustomAttribute.prototype.unbind = function () { + this.controller.removeRenderer(this.renderer); + this.controller = null; + this.renderer = null; + }; + ValidationRendererCustomAttribute = __decorate([ + aureliaTemplating.customAttribute('validation-renderer') + ], ValidationRendererCustomAttribute); + return ValidationRendererCustomAttribute; + }()); /** * Part of the fluent rule API. Enables customizing property rules. @@ -1411,12 +1453,12 @@ * @param args The rule's arguments. */ FluentRuleCustomizer.prototype.satisfiesRule = function (name) { + var _a; var args = []; for (var _i = 1; _i < arguments.length; _i++) { args[_i - 1] = arguments[_i]; } - var _a; - return (_a = this.fluentRules).satisfiesRule.apply(_a, [name].concat(args)); + return (_a = this.fluentRules).satisfiesRule.apply(_a, __spreadArrays([name], args)); }; /** * Applies the "required" rule to the property. @@ -1556,14 +1598,14 @@ // standard rule? rule = this[name]; if (rule instanceof Function) { - return rule.call.apply(rule, [this].concat(args)); + return rule.call.apply(rule, __spreadArrays([this], args)); } throw new Error("Rule with name \"" + name + "\" does not exist."); } var config = rule.argsToConfig ? rule.argsToConfig.apply(rule, args) : undefined; return this.satisfies(function (value, obj) { var _a; - return (_a = rule.condition).call.apply(_a, [_this, value, obj].concat(args)); + return (_a = rule.condition).call.apply(_a, __spreadArrays([_this, value, obj], args)); }, config) .withMessageKey(name); }; @@ -1808,28 +1850,6 @@ }()); // Exports - /** - * Aurelia Validation Configuration API - */ - var AureliaValidationConfiguration = /** @class */ (function () { - function AureliaValidationConfiguration() { - this.validatorType = StandardValidator; - } - /** - * Use a custom Validator implementation. - */ - AureliaValidationConfiguration.prototype.customValidator = function (type) { - this.validatorType = type; - }; - /** - * Applies the configuration. - */ - AureliaValidationConfiguration.prototype.apply = function (container) { - var validator = container.get(this.validatorType); - container.registerInstance(Validator, validator); - }; - return AureliaValidationConfiguration; - }()); /** * Configures the plugin. */ @@ -1842,7 +1862,7 @@ var propertyParser = frameworkConfig.container.get(PropertyAccessorParser); ValidationRules.initialize(messageParser, propertyParser); // configure... - var config = new AureliaValidationConfiguration(); + var config = new GlobalValidationConfiguration(); if (callback instanceof Function) { callback(config); } @@ -1853,8 +1873,8 @@ } } - exports.AureliaValidationConfiguration = AureliaValidationConfiguration; exports.configure = configure; + exports.GlobalValidationConfiguration = GlobalValidationConfiguration; exports.getTargetDOMElement = getTargetDOMElement; exports.getPropertyInfo = getPropertyInfo; exports.PropertyAccessorParser = PropertyAccessorParser;