diff --git a/bower.json b/bower.json index 29ef928e..a2f4985f 100644 --- a/bower.json +++ b/bower.json @@ -1,6 +1,6 @@ { "name": "aurelia-validation", - "version": "1.6.0", + "version": "2.0.0-rc1", "description": "Validation for Aurelia applications", "keywords": [ "aurelia", diff --git a/dist/amd/aurelia-validation.js b/dist/amd/aurelia-validation.js index db769a8f..e5c571df 100644 --- a/dist/amd/aurelia-validation.js +++ b/dist/amd/aurelia-validation.js @@ -454,6 +454,16 @@ define('aurelia-validation', ['exports', 'aurelia-binding', 'aurelia-templating' * when it updates the model due to a change in the view. */ validateTrigger[validateTrigger["changeOrBlur"] = 3] = "changeOrBlur"; + /** + * Validate the binding when the binding's target element fires a DOM "focusout" event. + * Unlike "blur", this event bubbles. + */ + validateTrigger[validateTrigger["focusout"] = 4] = "focusout"; + /** + * Validate the binding when the binding's target element fires a DOM "focusout" event or + * when it updates the model due to a change in the view. + */ + validateTrigger[validateTrigger["changeOrFocusout"] = 6] = "changeOrFocusout"; })(exports.validateTrigger || (exports.validateTrigger = {})); /** @@ -1041,6 +1051,7 @@ define('aurelia-validation', ['exports', 'aurelia-binding', 'aurelia-templating' return ValidationController; }()); + // tslint:disable:no-bitwise /** * Binding behavior. Indicates the bound property should be validated. */ @@ -1067,23 +1078,48 @@ define('aurelia-validation', ['exports', 'aurelia-binding', 'aurelia-templating' controller.registerBinding(binding, target, rules); binding.validationController = controller; var trigger = this.getValidateTrigger(controller); - // tslint:disable-next-line:no-bitwise - if (trigger & exports.validateTrigger.change) { + var event = (trigger & exports.validateTrigger.blur) === exports.validateTrigger.blur ? 'blur' + : (trigger & exports.validateTrigger.focusout) === exports.validateTrigger.focusout ? 'focusout' + : null; + var hasChangeTrigger = (trigger & exports.validateTrigger.change) === exports.validateTrigger.change; + binding.isDirty = !hasChangeTrigger; + // validatedOnce is used to control whether controller should validate upon user input + // + // always true when validation trigger doesn't include "blur" event (blur/focusout) + // else it will be set to true after (a) the first user input & loss of focus or (b) validation + binding.validatedOnce = hasChangeTrigger && event === null; + if (hasChangeTrigger) { 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); + this.isDirty = true; + if (this.validatedOnce) { + 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); }); + if (event !== null) { + binding.focusLossHandler = function () { + _this.taskQueue.queueMicroTask(function () { + if (binding.isDirty) { + controller.validateBinding(binding); + binding.validatedOnce = true; + } + }); }; + binding.validationTriggerEvent = event; binding.validateTarget = target; - target.addEventListener('blur', binding.validateBlurHandler); + target.addEventListener(event, binding.focusLossHandler); + if (hasChangeTrigger) { + var propertyName_1 = getPropertyInfo(binding.sourceExpression, binding.source).propertyName; + binding.validationSubscription = controller.subscribe(function (event) { + if (!binding.validatedOnce && event.type === 'validate') { + binding.validatedOnce = event.errors.findIndex(function (e) { return e.propertyName === propertyName_1; }) > -1; + } + }); + } } if (trigger !== exports.validateTrigger.manual) { binding.standardUpdateTarget = binding.updateTarget; @@ -1105,13 +1141,19 @@ define('aurelia-validation', ['exports', 'aurelia-binding', 'aurelia-templating' binding.updateTarget = binding.standardUpdateTarget; binding.standardUpdateTarget = null; } - if (binding.validateBlurHandler) { - binding.validateTarget.removeEventListener('blur', binding.validateBlurHandler); - binding.validateBlurHandler = null; + if (binding.focusLossHandler) { + binding.validateTarget.removeEventListener(binding.validationTriggerEvent, binding.focusLossHandler); + binding.focusLossHandler = null; binding.validateTarget = null; } + if (binding.validationSubscription) { + binding.validationSubscription.dispose(); + binding.validationSubscription = null; + } binding.validationController.unregisterBinding(binding); binding.validationController = null; + binding.isDirty = null; + binding.validatedOnce = null; }; return ValidateBindingBehaviorBase; }()); @@ -1209,6 +1251,34 @@ define('aurelia-validation', ['exports', 'aurelia-binding', 'aurelia-templating' aureliaBinding.bindingBehavior('validateOnChangeOrBlur') ], ValidateOnChangeOrBlurBindingBehavior); return ValidateOnChangeOrBlurBindingBehavior; + }(ValidateBindingBehaviorBase)); + var ValidateOnFocusoutBindingBehavior = /** @class */ (function (_super) { + __extends(ValidateOnFocusoutBindingBehavior, _super); + function ValidateOnFocusoutBindingBehavior() { + return _super !== null && _super.apply(this, arguments) || this; + } + ValidateOnFocusoutBindingBehavior.prototype.getValidateTrigger = function () { + return exports.validateTrigger.focusout; + }; + ValidateOnFocusoutBindingBehavior.inject = [aureliaTaskQueue.TaskQueue]; + ValidateOnFocusoutBindingBehavior = __decorate([ + aureliaBinding.bindingBehavior('validateOnFocusout') + ], ValidateOnFocusoutBindingBehavior); + return ValidateOnFocusoutBindingBehavior; + }(ValidateBindingBehaviorBase)); + var ValidateOnChangeOrFocusoutBindingBehavior = /** @class */ (function (_super) { + __extends(ValidateOnChangeOrFocusoutBindingBehavior, _super); + function ValidateOnChangeOrFocusoutBindingBehavior() { + return _super !== null && _super.apply(this, arguments) || this; + } + ValidateOnChangeOrFocusoutBindingBehavior.prototype.getValidateTrigger = function () { + return exports.validateTrigger.changeOrFocusout; + }; + ValidateOnChangeOrFocusoutBindingBehavior.inject = [aureliaTaskQueue.TaskQueue]; + ValidateOnChangeOrFocusoutBindingBehavior = __decorate([ + aureliaBinding.bindingBehavior('validateOnChangeOrFocusout') + ], ValidateOnChangeOrFocusoutBindingBehavior); + return ValidateOnChangeOrFocusoutBindingBehavior; }(ValidateBindingBehaviorBase)); /** @@ -1865,7 +1935,7 @@ define('aurelia-validation', ['exports', 'aurelia-binding', 'aurelia-templating' config.apply(frameworkConfig.container); // globalize the behaviors. if (frameworkConfig.globalResources) { - frameworkConfig.globalResources(ValidateBindingBehavior, ValidateManuallyBindingBehavior, ValidateOnBlurBindingBehavior, ValidateOnChangeBindingBehavior, ValidateOnChangeOrBlurBindingBehavior, ValidationErrorsCustomAttribute, ValidationRendererCustomAttribute); + frameworkConfig.globalResources(ValidateBindingBehavior, ValidateManuallyBindingBehavior, ValidateOnBlurBindingBehavior, ValidateOnFocusoutBindingBehavior, ValidateOnChangeBindingBehavior, ValidateOnChangeOrBlurBindingBehavior, ValidateOnChangeOrFocusoutBindingBehavior, ValidationErrorsCustomAttribute, ValidationRendererCustomAttribute); } } @@ -1880,6 +1950,8 @@ define('aurelia-validation', ['exports', 'aurelia-binding', 'aurelia-templating' exports.ValidateOnBlurBindingBehavior = ValidateOnBlurBindingBehavior; exports.ValidateOnChangeBindingBehavior = ValidateOnChangeBindingBehavior; exports.ValidateOnChangeOrBlurBindingBehavior = ValidateOnChangeOrBlurBindingBehavior; + exports.ValidateOnFocusoutBindingBehavior = ValidateOnFocusoutBindingBehavior; + exports.ValidateOnChangeOrFocusoutBindingBehavior = ValidateOnChangeOrFocusoutBindingBehavior; exports.ValidateEvent = ValidateEvent; exports.ValidateResult = ValidateResult; exports.ValidationController = ValidationController; diff --git a/dist/aurelia-validation.d.ts b/dist/aurelia-validation.d.ts index ba8b1cf4..a8cd039e 100644 --- a/dist/aurelia-validation.d.ts +++ b/dist/aurelia-validation.d.ts @@ -74,7 +74,17 @@ export declare enum validateTrigger { * 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 + changeOrBlur = 3, + /** + * Validate the binding when the binding's target element fires a DOM "focusout" event. + * Unlike "blur", this event bubbles. + */ + focusout = 4, + /** + * Validate the binding when the binding's target element fires a DOM "focusout" event or + * when it updates the model due to a change in the view. + */ + changeOrFocusout = 6 } export declare type ValidatorCtor = new (...args: any[]) => Validator; /** @@ -426,6 +436,14 @@ export declare class ValidateOnChangeOrBlurBindingBehavior extends ValidateBindi static inject: (typeof TaskQueue)[]; getValidateTrigger(): validateTrigger; } +export declare class ValidateOnFocusoutBindingBehavior extends ValidateBindingBehaviorBase { + static inject: (typeof TaskQueue)[]; + getValidateTrigger(): validateTrigger; +} +export declare class ValidateOnChangeOrFocusoutBindingBehavior extends ValidateBindingBehaviorBase { + static inject: (typeof TaskQueue)[]; + getValidateTrigger(): validateTrigger; +} /** * Creates ValidationController instances. */ diff --git a/dist/commonjs/aurelia-validation.js b/dist/commonjs/aurelia-validation.js index 75c9ef1c..fdb59581 100644 --- a/dist/commonjs/aurelia-validation.js +++ b/dist/commonjs/aurelia-validation.js @@ -463,6 +463,16 @@ var StandardValidator = /** @class */ (function (_super) { * when it updates the model due to a change in the view. */ validateTrigger[validateTrigger["changeOrBlur"] = 3] = "changeOrBlur"; + /** + * Validate the binding when the binding's target element fires a DOM "focusout" event. + * Unlike "blur", this event bubbles. + */ + validateTrigger[validateTrigger["focusout"] = 4] = "focusout"; + /** + * Validate the binding when the binding's target element fires a DOM "focusout" event or + * when it updates the model due to a change in the view. + */ + validateTrigger[validateTrigger["changeOrFocusout"] = 6] = "changeOrFocusout"; })(exports.validateTrigger || (exports.validateTrigger = {})); /** @@ -1050,6 +1060,7 @@ var ValidationController = /** @class */ (function () { return ValidationController; }()); +// tslint:disable:no-bitwise /** * Binding behavior. Indicates the bound property should be validated. */ @@ -1076,23 +1087,48 @@ var ValidateBindingBehaviorBase = /** @class */ (function () { controller.registerBinding(binding, target, rules); binding.validationController = controller; var trigger = this.getValidateTrigger(controller); - // tslint:disable-next-line:no-bitwise - if (trigger & exports.validateTrigger.change) { + var event = (trigger & exports.validateTrigger.blur) === exports.validateTrigger.blur ? 'blur' + : (trigger & exports.validateTrigger.focusout) === exports.validateTrigger.focusout ? 'focusout' + : null; + var hasChangeTrigger = (trigger & exports.validateTrigger.change) === exports.validateTrigger.change; + binding.isDirty = !hasChangeTrigger; + // validatedOnce is used to control whether controller should validate upon user input + // + // always true when validation trigger doesn't include "blur" event (blur/focusout) + // else it will be set to true after (a) the first user input & loss of focus or (b) validation + binding.validatedOnce = hasChangeTrigger && event === null; + if (hasChangeTrigger) { 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); + this.isDirty = true; + if (this.validatedOnce) { + 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); }); + if (event !== null) { + binding.focusLossHandler = function () { + _this.taskQueue.queueMicroTask(function () { + if (binding.isDirty) { + controller.validateBinding(binding); + binding.validatedOnce = true; + } + }); }; + binding.validationTriggerEvent = event; binding.validateTarget = target; - target.addEventListener('blur', binding.validateBlurHandler); + target.addEventListener(event, binding.focusLossHandler); + if (hasChangeTrigger) { + var propertyName_1 = getPropertyInfo(binding.sourceExpression, binding.source).propertyName; + binding.validationSubscription = controller.subscribe(function (event) { + if (!binding.validatedOnce && event.type === 'validate') { + binding.validatedOnce = event.errors.findIndex(function (e) { return e.propertyName === propertyName_1; }) > -1; + } + }); + } } if (trigger !== exports.validateTrigger.manual) { binding.standardUpdateTarget = binding.updateTarget; @@ -1114,13 +1150,19 @@ var ValidateBindingBehaviorBase = /** @class */ (function () { binding.updateTarget = binding.standardUpdateTarget; binding.standardUpdateTarget = null; } - if (binding.validateBlurHandler) { - binding.validateTarget.removeEventListener('blur', binding.validateBlurHandler); - binding.validateBlurHandler = null; + if (binding.focusLossHandler) { + binding.validateTarget.removeEventListener(binding.validationTriggerEvent, binding.focusLossHandler); + binding.focusLossHandler = null; binding.validateTarget = null; } + if (binding.validationSubscription) { + binding.validationSubscription.dispose(); + binding.validationSubscription = null; + } binding.validationController.unregisterBinding(binding); binding.validationController = null; + binding.isDirty = null; + binding.validatedOnce = null; }; return ValidateBindingBehaviorBase; }()); @@ -1218,6 +1260,34 @@ var ValidateOnChangeOrBlurBindingBehavior = /** @class */ (function (_super) { aureliaBinding.bindingBehavior('validateOnChangeOrBlur') ], ValidateOnChangeOrBlurBindingBehavior); return ValidateOnChangeOrBlurBindingBehavior; +}(ValidateBindingBehaviorBase)); +var ValidateOnFocusoutBindingBehavior = /** @class */ (function (_super) { + __extends(ValidateOnFocusoutBindingBehavior, _super); + function ValidateOnFocusoutBindingBehavior() { + return _super !== null && _super.apply(this, arguments) || this; + } + ValidateOnFocusoutBindingBehavior.prototype.getValidateTrigger = function () { + return exports.validateTrigger.focusout; + }; + ValidateOnFocusoutBindingBehavior.inject = [aureliaTaskQueue.TaskQueue]; + ValidateOnFocusoutBindingBehavior = __decorate([ + aureliaBinding.bindingBehavior('validateOnFocusout') + ], ValidateOnFocusoutBindingBehavior); + return ValidateOnFocusoutBindingBehavior; +}(ValidateBindingBehaviorBase)); +var ValidateOnChangeOrFocusoutBindingBehavior = /** @class */ (function (_super) { + __extends(ValidateOnChangeOrFocusoutBindingBehavior, _super); + function ValidateOnChangeOrFocusoutBindingBehavior() { + return _super !== null && _super.apply(this, arguments) || this; + } + ValidateOnChangeOrFocusoutBindingBehavior.prototype.getValidateTrigger = function () { + return exports.validateTrigger.changeOrFocusout; + }; + ValidateOnChangeOrFocusoutBindingBehavior.inject = [aureliaTaskQueue.TaskQueue]; + ValidateOnChangeOrFocusoutBindingBehavior = __decorate([ + aureliaBinding.bindingBehavior('validateOnChangeOrFocusout') + ], ValidateOnChangeOrFocusoutBindingBehavior); + return ValidateOnChangeOrFocusoutBindingBehavior; }(ValidateBindingBehaviorBase)); /** @@ -1874,7 +1944,7 @@ frameworkConfig, callback) { config.apply(frameworkConfig.container); // globalize the behaviors. if (frameworkConfig.globalResources) { - frameworkConfig.globalResources(ValidateBindingBehavior, ValidateManuallyBindingBehavior, ValidateOnBlurBindingBehavior, ValidateOnChangeBindingBehavior, ValidateOnChangeOrBlurBindingBehavior, ValidationErrorsCustomAttribute, ValidationRendererCustomAttribute); + frameworkConfig.globalResources(ValidateBindingBehavior, ValidateManuallyBindingBehavior, ValidateOnBlurBindingBehavior, ValidateOnFocusoutBindingBehavior, ValidateOnChangeBindingBehavior, ValidateOnChangeOrBlurBindingBehavior, ValidateOnChangeOrFocusoutBindingBehavior, ValidationErrorsCustomAttribute, ValidationRendererCustomAttribute); } } @@ -1889,6 +1959,8 @@ exports.ValidateManuallyBindingBehavior = ValidateManuallyBindingBehavior; exports.ValidateOnBlurBindingBehavior = ValidateOnBlurBindingBehavior; exports.ValidateOnChangeBindingBehavior = ValidateOnChangeBindingBehavior; exports.ValidateOnChangeOrBlurBindingBehavior = ValidateOnChangeOrBlurBindingBehavior; +exports.ValidateOnFocusoutBindingBehavior = ValidateOnFocusoutBindingBehavior; +exports.ValidateOnChangeOrFocusoutBindingBehavior = ValidateOnChangeOrFocusoutBindingBehavior; exports.ValidateEvent = ValidateEvent; exports.ValidateResult = ValidateResult; exports.ValidationController = ValidationController; diff --git a/dist/es2015/aurelia-validation.js b/dist/es2015/aurelia-validation.js index f4d9fa19..97f7c101 100644 --- a/dist/es2015/aurelia-validation.js +++ b/dist/es2015/aurelia-validation.js @@ -393,6 +393,16 @@ var validateTrigger; * when it updates the model due to a change in the view. */ validateTrigger[validateTrigger["changeOrBlur"] = 3] = "changeOrBlur"; + /** + * Validate the binding when the binding's target element fires a DOM "focusout" event. + * Unlike "blur", this event bubbles. + */ + validateTrigger[validateTrigger["focusout"] = 4] = "focusout"; + /** + * Validate the binding when the binding's target element fires a DOM "focusout" event or + * when it updates the model due to a change in the view. + */ + validateTrigger[validateTrigger["changeOrFocusout"] = 6] = "changeOrFocusout"; })(validateTrigger || (validateTrigger = {})); /** @@ -979,6 +989,7 @@ class ValidationController { } ValidationController.inject = [Validator, PropertyAccessorParser, GlobalValidationConfiguration]; +// tslint:disable:no-bitwise /** * Binding behavior. Indicates the bound property should be validated. */ @@ -1004,23 +1015,48 @@ class ValidateBindingBehaviorBase { controller.registerBinding(binding, target, rules); binding.validationController = controller; const trigger = this.getValidateTrigger(controller); - // tslint:disable-next-line:no-bitwise - if (trigger & validateTrigger.change) { + const event = (trigger & validateTrigger.blur) === validateTrigger.blur ? 'blur' + : (trigger & validateTrigger.focusout) === validateTrigger.focusout ? 'focusout' + : null; + const hasChangeTrigger = (trigger & validateTrigger.change) === validateTrigger.change; + binding.isDirty = !hasChangeTrigger; + // validatedOnce is used to control whether controller should validate upon user input + // + // always true when validation trigger doesn't include "blur" event (blur/focusout) + // else it will be set to true after (a) the first user input & loss of focus or (b) validation + binding.validatedOnce = hasChangeTrigger && event === null; + if (hasChangeTrigger) { 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); + this.isDirty = true; + if (this.validatedOnce) { + this.validationController.validateBinding(this); + } }; } - // tslint:disable-next-line:no-bitwise - if (trigger & validateTrigger.blur) { - binding.validateBlurHandler = () => { - this.taskQueue.queueMicroTask(() => controller.validateBinding(binding)); + if (event !== null) { + binding.focusLossHandler = () => { + this.taskQueue.queueMicroTask(() => { + if (binding.isDirty) { + controller.validateBinding(binding); + binding.validatedOnce = true; + } + }); }; + binding.validationTriggerEvent = event; binding.validateTarget = target; - target.addEventListener('blur', binding.validateBlurHandler); + target.addEventListener(event, binding.focusLossHandler); + if (hasChangeTrigger) { + const { propertyName } = getPropertyInfo(binding.sourceExpression, binding.source); + binding.validationSubscription = controller.subscribe((event) => { + if (!binding.validatedOnce && event.type === 'validate') { + binding.validatedOnce = event.errors.findIndex((e) => e.propertyName === propertyName) > -1; + } + }); + } } if (trigger !== validateTrigger.manual) { binding.standardUpdateTarget = binding.updateTarget; @@ -1042,13 +1078,19 @@ class ValidateBindingBehaviorBase { binding.updateTarget = binding.standardUpdateTarget; binding.standardUpdateTarget = null; } - if (binding.validateBlurHandler) { - binding.validateTarget.removeEventListener('blur', binding.validateBlurHandler); - binding.validateBlurHandler = null; + if (binding.focusLossHandler) { + binding.validateTarget.removeEventListener(binding.validationTriggerEvent, binding.focusLossHandler); + binding.focusLossHandler = null; binding.validateTarget = null; } + if (binding.validationSubscription) { + binding.validationSubscription.dispose(); + binding.validationSubscription = null; + } binding.validationController.unregisterBinding(binding); binding.validationController = null; + binding.isDirty = null; + binding.validatedOnce = null; } } @@ -1120,7 +1162,25 @@ let ValidateOnChangeOrBlurBindingBehavior = class ValidateOnChangeOrBlurBindingB ValidateOnChangeOrBlurBindingBehavior.inject = [TaskQueue]; ValidateOnChangeOrBlurBindingBehavior = __decorate([ bindingBehavior('validateOnChangeOrBlur') -], ValidateOnChangeOrBlurBindingBehavior); +], ValidateOnChangeOrBlurBindingBehavior); +let ValidateOnFocusoutBindingBehavior = class ValidateOnFocusoutBindingBehavior extends ValidateBindingBehaviorBase { + getValidateTrigger() { + return validateTrigger.focusout; + } +}; +ValidateOnFocusoutBindingBehavior.inject = [TaskQueue]; +ValidateOnFocusoutBindingBehavior = __decorate([ + bindingBehavior('validateOnFocusout') +], ValidateOnFocusoutBindingBehavior); +let ValidateOnChangeOrFocusoutBindingBehavior = class ValidateOnChangeOrFocusoutBindingBehavior extends ValidateBindingBehaviorBase { + getValidateTrigger() { + return validateTrigger.changeOrFocusout; + } +}; +ValidateOnChangeOrFocusoutBindingBehavior.inject = [TaskQueue]; +ValidateOnChangeOrFocusoutBindingBehavior = __decorate([ + bindingBehavior('validateOnChangeOrFocusout') +], ValidateOnChangeOrFocusoutBindingBehavior); /** * Creates ValidationController instances. @@ -1738,8 +1798,8 @@ frameworkConfig, callback) { config.apply(frameworkConfig.container); // globalize the behaviors. if (frameworkConfig.globalResources) { - frameworkConfig.globalResources(ValidateBindingBehavior, ValidateManuallyBindingBehavior, ValidateOnBlurBindingBehavior, ValidateOnChangeBindingBehavior, ValidateOnChangeOrBlurBindingBehavior, ValidationErrorsCustomAttribute, ValidationRendererCustomAttribute); + frameworkConfig.globalResources(ValidateBindingBehavior, ValidateManuallyBindingBehavior, ValidateOnBlurBindingBehavior, ValidateOnFocusoutBindingBehavior, ValidateOnChangeBindingBehavior, ValidateOnChangeOrBlurBindingBehavior, ValidateOnChangeOrFocusoutBindingBehavior, ValidationErrorsCustomAttribute, ValidationRendererCustomAttribute); } } -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 }; +export { configure, GlobalValidationConfiguration, getTargetDOMElement, getPropertyInfo, PropertyAccessorParser, getAccessorExpression, ValidateBindingBehavior, ValidateManuallyBindingBehavior, ValidateOnBlurBindingBehavior, ValidateOnChangeBindingBehavior, ValidateOnChangeOrBlurBindingBehavior, ValidateOnFocusoutBindingBehavior, ValidateOnChangeOrFocusoutBindingBehavior, 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 f4d9fa19..97f7c101 100644 --- a/dist/es2017/aurelia-validation.js +++ b/dist/es2017/aurelia-validation.js @@ -393,6 +393,16 @@ var validateTrigger; * when it updates the model due to a change in the view. */ validateTrigger[validateTrigger["changeOrBlur"] = 3] = "changeOrBlur"; + /** + * Validate the binding when the binding's target element fires a DOM "focusout" event. + * Unlike "blur", this event bubbles. + */ + validateTrigger[validateTrigger["focusout"] = 4] = "focusout"; + /** + * Validate the binding when the binding's target element fires a DOM "focusout" event or + * when it updates the model due to a change in the view. + */ + validateTrigger[validateTrigger["changeOrFocusout"] = 6] = "changeOrFocusout"; })(validateTrigger || (validateTrigger = {})); /** @@ -979,6 +989,7 @@ class ValidationController { } ValidationController.inject = [Validator, PropertyAccessorParser, GlobalValidationConfiguration]; +// tslint:disable:no-bitwise /** * Binding behavior. Indicates the bound property should be validated. */ @@ -1004,23 +1015,48 @@ class ValidateBindingBehaviorBase { controller.registerBinding(binding, target, rules); binding.validationController = controller; const trigger = this.getValidateTrigger(controller); - // tslint:disable-next-line:no-bitwise - if (trigger & validateTrigger.change) { + const event = (trigger & validateTrigger.blur) === validateTrigger.blur ? 'blur' + : (trigger & validateTrigger.focusout) === validateTrigger.focusout ? 'focusout' + : null; + const hasChangeTrigger = (trigger & validateTrigger.change) === validateTrigger.change; + binding.isDirty = !hasChangeTrigger; + // validatedOnce is used to control whether controller should validate upon user input + // + // always true when validation trigger doesn't include "blur" event (blur/focusout) + // else it will be set to true after (a) the first user input & loss of focus or (b) validation + binding.validatedOnce = hasChangeTrigger && event === null; + if (hasChangeTrigger) { 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); + this.isDirty = true; + if (this.validatedOnce) { + this.validationController.validateBinding(this); + } }; } - // tslint:disable-next-line:no-bitwise - if (trigger & validateTrigger.blur) { - binding.validateBlurHandler = () => { - this.taskQueue.queueMicroTask(() => controller.validateBinding(binding)); + if (event !== null) { + binding.focusLossHandler = () => { + this.taskQueue.queueMicroTask(() => { + if (binding.isDirty) { + controller.validateBinding(binding); + binding.validatedOnce = true; + } + }); }; + binding.validationTriggerEvent = event; binding.validateTarget = target; - target.addEventListener('blur', binding.validateBlurHandler); + target.addEventListener(event, binding.focusLossHandler); + if (hasChangeTrigger) { + const { propertyName } = getPropertyInfo(binding.sourceExpression, binding.source); + binding.validationSubscription = controller.subscribe((event) => { + if (!binding.validatedOnce && event.type === 'validate') { + binding.validatedOnce = event.errors.findIndex((e) => e.propertyName === propertyName) > -1; + } + }); + } } if (trigger !== validateTrigger.manual) { binding.standardUpdateTarget = binding.updateTarget; @@ -1042,13 +1078,19 @@ class ValidateBindingBehaviorBase { binding.updateTarget = binding.standardUpdateTarget; binding.standardUpdateTarget = null; } - if (binding.validateBlurHandler) { - binding.validateTarget.removeEventListener('blur', binding.validateBlurHandler); - binding.validateBlurHandler = null; + if (binding.focusLossHandler) { + binding.validateTarget.removeEventListener(binding.validationTriggerEvent, binding.focusLossHandler); + binding.focusLossHandler = null; binding.validateTarget = null; } + if (binding.validationSubscription) { + binding.validationSubscription.dispose(); + binding.validationSubscription = null; + } binding.validationController.unregisterBinding(binding); binding.validationController = null; + binding.isDirty = null; + binding.validatedOnce = null; } } @@ -1120,7 +1162,25 @@ let ValidateOnChangeOrBlurBindingBehavior = class ValidateOnChangeOrBlurBindingB ValidateOnChangeOrBlurBindingBehavior.inject = [TaskQueue]; ValidateOnChangeOrBlurBindingBehavior = __decorate([ bindingBehavior('validateOnChangeOrBlur') -], ValidateOnChangeOrBlurBindingBehavior); +], ValidateOnChangeOrBlurBindingBehavior); +let ValidateOnFocusoutBindingBehavior = class ValidateOnFocusoutBindingBehavior extends ValidateBindingBehaviorBase { + getValidateTrigger() { + return validateTrigger.focusout; + } +}; +ValidateOnFocusoutBindingBehavior.inject = [TaskQueue]; +ValidateOnFocusoutBindingBehavior = __decorate([ + bindingBehavior('validateOnFocusout') +], ValidateOnFocusoutBindingBehavior); +let ValidateOnChangeOrFocusoutBindingBehavior = class ValidateOnChangeOrFocusoutBindingBehavior extends ValidateBindingBehaviorBase { + getValidateTrigger() { + return validateTrigger.changeOrFocusout; + } +}; +ValidateOnChangeOrFocusoutBindingBehavior.inject = [TaskQueue]; +ValidateOnChangeOrFocusoutBindingBehavior = __decorate([ + bindingBehavior('validateOnChangeOrFocusout') +], ValidateOnChangeOrFocusoutBindingBehavior); /** * Creates ValidationController instances. @@ -1738,8 +1798,8 @@ frameworkConfig, callback) { config.apply(frameworkConfig.container); // globalize the behaviors. if (frameworkConfig.globalResources) { - frameworkConfig.globalResources(ValidateBindingBehavior, ValidateManuallyBindingBehavior, ValidateOnBlurBindingBehavior, ValidateOnChangeBindingBehavior, ValidateOnChangeOrBlurBindingBehavior, ValidationErrorsCustomAttribute, ValidationRendererCustomAttribute); + frameworkConfig.globalResources(ValidateBindingBehavior, ValidateManuallyBindingBehavior, ValidateOnBlurBindingBehavior, ValidateOnFocusoutBindingBehavior, ValidateOnChangeBindingBehavior, ValidateOnChangeOrBlurBindingBehavior, ValidateOnChangeOrFocusoutBindingBehavior, ValidationErrorsCustomAttribute, ValidationRendererCustomAttribute); } } -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 }; +export { configure, GlobalValidationConfiguration, getTargetDOMElement, getPropertyInfo, PropertyAccessorParser, getAccessorExpression, ValidateBindingBehavior, ValidateManuallyBindingBehavior, ValidateOnBlurBindingBehavior, ValidateOnChangeBindingBehavior, ValidateOnChangeOrBlurBindingBehavior, ValidateOnFocusoutBindingBehavior, ValidateOnChangeOrFocusoutBindingBehavior, 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 4d995906..7128a24c 100644 --- a/dist/native-modules/aurelia-validation.js +++ b/dist/native-modules/aurelia-validation.js @@ -460,6 +460,16 @@ var validateTrigger; * when it updates the model due to a change in the view. */ validateTrigger[validateTrigger["changeOrBlur"] = 3] = "changeOrBlur"; + /** + * Validate the binding when the binding's target element fires a DOM "focusout" event. + * Unlike "blur", this event bubbles. + */ + validateTrigger[validateTrigger["focusout"] = 4] = "focusout"; + /** + * Validate the binding when the binding's target element fires a DOM "focusout" event or + * when it updates the model due to a change in the view. + */ + validateTrigger[validateTrigger["changeOrFocusout"] = 6] = "changeOrFocusout"; })(validateTrigger || (validateTrigger = {})); /** @@ -1047,6 +1057,7 @@ var ValidationController = /** @class */ (function () { return ValidationController; }()); +// tslint:disable:no-bitwise /** * Binding behavior. Indicates the bound property should be validated. */ @@ -1073,23 +1084,48 @@ var ValidateBindingBehaviorBase = /** @class */ (function () { controller.registerBinding(binding, target, rules); binding.validationController = controller; var trigger = this.getValidateTrigger(controller); - // tslint:disable-next-line:no-bitwise - if (trigger & validateTrigger.change) { + var event = (trigger & validateTrigger.blur) === validateTrigger.blur ? 'blur' + : (trigger & validateTrigger.focusout) === validateTrigger.focusout ? 'focusout' + : null; + var hasChangeTrigger = (trigger & validateTrigger.change) === validateTrigger.change; + binding.isDirty = !hasChangeTrigger; + // validatedOnce is used to control whether controller should validate upon user input + // + // always true when validation trigger doesn't include "blur" event (blur/focusout) + // else it will be set to true after (a) the first user input & loss of focus or (b) validation + binding.validatedOnce = hasChangeTrigger && event === null; + if (hasChangeTrigger) { 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); + this.isDirty = true; + if (this.validatedOnce) { + 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); }); + if (event !== null) { + binding.focusLossHandler = function () { + _this.taskQueue.queueMicroTask(function () { + if (binding.isDirty) { + controller.validateBinding(binding); + binding.validatedOnce = true; + } + }); }; + binding.validationTriggerEvent = event; binding.validateTarget = target; - target.addEventListener('blur', binding.validateBlurHandler); + target.addEventListener(event, binding.focusLossHandler); + if (hasChangeTrigger) { + var propertyName_1 = getPropertyInfo(binding.sourceExpression, binding.source).propertyName; + binding.validationSubscription = controller.subscribe(function (event) { + if (!binding.validatedOnce && event.type === 'validate') { + binding.validatedOnce = event.errors.findIndex(function (e) { return e.propertyName === propertyName_1; }) > -1; + } + }); + } } if (trigger !== validateTrigger.manual) { binding.standardUpdateTarget = binding.updateTarget; @@ -1111,13 +1147,19 @@ var ValidateBindingBehaviorBase = /** @class */ (function () { binding.updateTarget = binding.standardUpdateTarget; binding.standardUpdateTarget = null; } - if (binding.validateBlurHandler) { - binding.validateTarget.removeEventListener('blur', binding.validateBlurHandler); - binding.validateBlurHandler = null; + if (binding.focusLossHandler) { + binding.validateTarget.removeEventListener(binding.validationTriggerEvent, binding.focusLossHandler); + binding.focusLossHandler = null; binding.validateTarget = null; } + if (binding.validationSubscription) { + binding.validationSubscription.dispose(); + binding.validationSubscription = null; + } binding.validationController.unregisterBinding(binding); binding.validationController = null; + binding.isDirty = null; + binding.validatedOnce = null; }; return ValidateBindingBehaviorBase; }()); @@ -1215,6 +1257,34 @@ var ValidateOnChangeOrBlurBindingBehavior = /** @class */ (function (_super) { bindingBehavior('validateOnChangeOrBlur') ], ValidateOnChangeOrBlurBindingBehavior); return ValidateOnChangeOrBlurBindingBehavior; +}(ValidateBindingBehaviorBase)); +var ValidateOnFocusoutBindingBehavior = /** @class */ (function (_super) { + __extends(ValidateOnFocusoutBindingBehavior, _super); + function ValidateOnFocusoutBindingBehavior() { + return _super !== null && _super.apply(this, arguments) || this; + } + ValidateOnFocusoutBindingBehavior.prototype.getValidateTrigger = function () { + return validateTrigger.focusout; + }; + ValidateOnFocusoutBindingBehavior.inject = [TaskQueue]; + ValidateOnFocusoutBindingBehavior = __decorate([ + bindingBehavior('validateOnFocusout') + ], ValidateOnFocusoutBindingBehavior); + return ValidateOnFocusoutBindingBehavior; +}(ValidateBindingBehaviorBase)); +var ValidateOnChangeOrFocusoutBindingBehavior = /** @class */ (function (_super) { + __extends(ValidateOnChangeOrFocusoutBindingBehavior, _super); + function ValidateOnChangeOrFocusoutBindingBehavior() { + return _super !== null && _super.apply(this, arguments) || this; + } + ValidateOnChangeOrFocusoutBindingBehavior.prototype.getValidateTrigger = function () { + return validateTrigger.changeOrFocusout; + }; + ValidateOnChangeOrFocusoutBindingBehavior.inject = [TaskQueue]; + ValidateOnChangeOrFocusoutBindingBehavior = __decorate([ + bindingBehavior('validateOnChangeOrFocusout') + ], ValidateOnChangeOrFocusoutBindingBehavior); + return ValidateOnChangeOrFocusoutBindingBehavior; }(ValidateBindingBehaviorBase)); /** @@ -1871,8 +1941,8 @@ frameworkConfig, callback) { config.apply(frameworkConfig.container); // globalize the behaviors. if (frameworkConfig.globalResources) { - frameworkConfig.globalResources(ValidateBindingBehavior, ValidateManuallyBindingBehavior, ValidateOnBlurBindingBehavior, ValidateOnChangeBindingBehavior, ValidateOnChangeOrBlurBindingBehavior, ValidationErrorsCustomAttribute, ValidationRendererCustomAttribute); + frameworkConfig.globalResources(ValidateBindingBehavior, ValidateManuallyBindingBehavior, ValidateOnBlurBindingBehavior, ValidateOnFocusoutBindingBehavior, ValidateOnChangeBindingBehavior, ValidateOnChangeOrBlurBindingBehavior, ValidateOnChangeOrFocusoutBindingBehavior, ValidationErrorsCustomAttribute, ValidationRendererCustomAttribute); } } -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 }; +export { configure, GlobalValidationConfiguration, getTargetDOMElement, getPropertyInfo, PropertyAccessorParser, getAccessorExpression, ValidateBindingBehavior, ValidateManuallyBindingBehavior, ValidateOnBlurBindingBehavior, ValidateOnChangeBindingBehavior, ValidateOnChangeOrBlurBindingBehavior, ValidateOnFocusoutBindingBehavior, ValidateOnChangeOrFocusoutBindingBehavior, 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 44730c6b..312ee050 100644 --- a/dist/system/aurelia-validation.js +++ b/dist/system/aurelia-validation.js @@ -497,6 +497,16 @@ System.register(['aurelia-binding', 'aurelia-templating', 'aurelia-logging', 'au * when it updates the model due to a change in the view. */ validateTrigger[validateTrigger["changeOrBlur"] = 3] = "changeOrBlur"; + /** + * Validate the binding when the binding's target element fires a DOM "focusout" event. + * Unlike "blur", this event bubbles. + */ + validateTrigger[validateTrigger["focusout"] = 4] = "focusout"; + /** + * Validate the binding when the binding's target element fires a DOM "focusout" event or + * when it updates the model due to a change in the view. + */ + validateTrigger[validateTrigger["changeOrFocusout"] = 6] = "changeOrFocusout"; })(validateTrigger || (validateTrigger = exports('validateTrigger', {}))); /** @@ -1084,6 +1094,7 @@ System.register(['aurelia-binding', 'aurelia-templating', 'aurelia-logging', 'au return ValidationController; }())); + // tslint:disable:no-bitwise /** * Binding behavior. Indicates the bound property should be validated. */ @@ -1110,23 +1121,48 @@ System.register(['aurelia-binding', 'aurelia-templating', 'aurelia-logging', 'au controller.registerBinding(binding, target, rules); binding.validationController = controller; var trigger = this.getValidateTrigger(controller); - // tslint:disable-next-line:no-bitwise - if (trigger & validateTrigger.change) { + var event = (trigger & validateTrigger.blur) === validateTrigger.blur ? 'blur' + : (trigger & validateTrigger.focusout) === validateTrigger.focusout ? 'focusout' + : null; + var hasChangeTrigger = (trigger & validateTrigger.change) === validateTrigger.change; + binding.isDirty = !hasChangeTrigger; + // validatedOnce is used to control whether controller should validate upon user input + // + // always true when validation trigger doesn't include "blur" event (blur/focusout) + // else it will be set to true after (a) the first user input & loss of focus or (b) validation + binding.validatedOnce = hasChangeTrigger && event === null; + if (hasChangeTrigger) { 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); + this.isDirty = true; + if (this.validatedOnce) { + 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); }); + if (event !== null) { + binding.focusLossHandler = function () { + _this.taskQueue.queueMicroTask(function () { + if (binding.isDirty) { + controller.validateBinding(binding); + binding.validatedOnce = true; + } + }); }; + binding.validationTriggerEvent = event; binding.validateTarget = target; - target.addEventListener('blur', binding.validateBlurHandler); + target.addEventListener(event, binding.focusLossHandler); + if (hasChangeTrigger) { + var propertyName_1 = getPropertyInfo(binding.sourceExpression, binding.source).propertyName; + binding.validationSubscription = controller.subscribe(function (event) { + if (!binding.validatedOnce && event.type === 'validate') { + binding.validatedOnce = event.errors.findIndex(function (e) { return e.propertyName === propertyName_1; }) > -1; + } + }); + } } if (trigger !== validateTrigger.manual) { binding.standardUpdateTarget = binding.updateTarget; @@ -1148,13 +1184,19 @@ System.register(['aurelia-binding', 'aurelia-templating', 'aurelia-logging', 'au binding.updateTarget = binding.standardUpdateTarget; binding.standardUpdateTarget = null; } - if (binding.validateBlurHandler) { - binding.validateTarget.removeEventListener('blur', binding.validateBlurHandler); - binding.validateBlurHandler = null; + if (binding.focusLossHandler) { + binding.validateTarget.removeEventListener(binding.validationTriggerEvent, binding.focusLossHandler); + binding.focusLossHandler = null; binding.validateTarget = null; } + if (binding.validationSubscription) { + binding.validationSubscription.dispose(); + binding.validationSubscription = null; + } binding.validationController.unregisterBinding(binding); binding.validationController = null; + binding.isDirty = null; + binding.validatedOnce = null; }; return ValidateBindingBehaviorBase; }()); @@ -1252,6 +1294,34 @@ System.register(['aurelia-binding', 'aurelia-templating', 'aurelia-logging', 'au bindingBehavior('validateOnChangeOrBlur') ], ValidateOnChangeOrBlurBindingBehavior); return ValidateOnChangeOrBlurBindingBehavior; + }(ValidateBindingBehaviorBase))); + var ValidateOnFocusoutBindingBehavior = exports('ValidateOnFocusoutBindingBehavior', /** @class */ (function (_super) { + __extends(ValidateOnFocusoutBindingBehavior, _super); + function ValidateOnFocusoutBindingBehavior() { + return _super !== null && _super.apply(this, arguments) || this; + } + ValidateOnFocusoutBindingBehavior.prototype.getValidateTrigger = function () { + return validateTrigger.focusout; + }; + ValidateOnFocusoutBindingBehavior.inject = [TaskQueue]; + ValidateOnFocusoutBindingBehavior = __decorate([ + bindingBehavior('validateOnFocusout') + ], ValidateOnFocusoutBindingBehavior); + return ValidateOnFocusoutBindingBehavior; + }(ValidateBindingBehaviorBase))); + var ValidateOnChangeOrFocusoutBindingBehavior = exports('ValidateOnChangeOrFocusoutBindingBehavior', /** @class */ (function (_super) { + __extends(ValidateOnChangeOrFocusoutBindingBehavior, _super); + function ValidateOnChangeOrFocusoutBindingBehavior() { + return _super !== null && _super.apply(this, arguments) || this; + } + ValidateOnChangeOrFocusoutBindingBehavior.prototype.getValidateTrigger = function () { + return validateTrigger.changeOrFocusout; + }; + ValidateOnChangeOrFocusoutBindingBehavior.inject = [TaskQueue]; + ValidateOnChangeOrFocusoutBindingBehavior = __decorate([ + bindingBehavior('validateOnChangeOrFocusout') + ], ValidateOnChangeOrFocusoutBindingBehavior); + return ValidateOnChangeOrFocusoutBindingBehavior; }(ValidateBindingBehaviorBase))); /** @@ -1908,7 +1978,7 @@ System.register(['aurelia-binding', 'aurelia-templating', 'aurelia-logging', 'au config.apply(frameworkConfig.container); // globalize the behaviors. if (frameworkConfig.globalResources) { - frameworkConfig.globalResources(ValidateBindingBehavior, ValidateManuallyBindingBehavior, ValidateOnBlurBindingBehavior, ValidateOnChangeBindingBehavior, ValidateOnChangeOrBlurBindingBehavior, ValidationErrorsCustomAttribute, ValidationRendererCustomAttribute); + frameworkConfig.globalResources(ValidateBindingBehavior, ValidateManuallyBindingBehavior, ValidateOnBlurBindingBehavior, ValidateOnFocusoutBindingBehavior, ValidateOnChangeBindingBehavior, ValidateOnChangeOrBlurBindingBehavior, ValidateOnChangeOrFocusoutBindingBehavior, ValidationErrorsCustomAttribute, ValidationRendererCustomAttribute); } } diff --git a/dist/umd-es2015/aurelia-validation.js b/dist/umd-es2015/aurelia-validation.js index 67e8c019..2fa16f86 100644 --- a/dist/umd-es2015/aurelia-validation.js +++ b/dist/umd-es2015/aurelia-validation.js @@ -391,6 +391,16 @@ * when it updates the model due to a change in the view. */ validateTrigger[validateTrigger["changeOrBlur"] = 3] = "changeOrBlur"; + /** + * Validate the binding when the binding's target element fires a DOM "focusout" event. + * Unlike "blur", this event bubbles. + */ + validateTrigger[validateTrigger["focusout"] = 4] = "focusout"; + /** + * Validate the binding when the binding's target element fires a DOM "focusout" event or + * when it updates the model due to a change in the view. + */ + validateTrigger[validateTrigger["changeOrFocusout"] = 6] = "changeOrFocusout"; })(exports.validateTrigger || (exports.validateTrigger = {})); /** @@ -977,6 +987,7 @@ } ValidationController.inject = [Validator, PropertyAccessorParser, GlobalValidationConfiguration]; + // tslint:disable:no-bitwise /** * Binding behavior. Indicates the bound property should be validated. */ @@ -1002,23 +1013,48 @@ controller.registerBinding(binding, target, rules); binding.validationController = controller; const trigger = this.getValidateTrigger(controller); - // tslint:disable-next-line:no-bitwise - if (trigger & exports.validateTrigger.change) { + const event = (trigger & exports.validateTrigger.blur) === exports.validateTrigger.blur ? 'blur' + : (trigger & exports.validateTrigger.focusout) === exports.validateTrigger.focusout ? 'focusout' + : null; + const hasChangeTrigger = (trigger & exports.validateTrigger.change) === exports.validateTrigger.change; + binding.isDirty = !hasChangeTrigger; + // validatedOnce is used to control whether controller should validate upon user input + // + // always true when validation trigger doesn't include "blur" event (blur/focusout) + // else it will be set to true after (a) the first user input & loss of focus or (b) validation + binding.validatedOnce = hasChangeTrigger && event === null; + if (hasChangeTrigger) { 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); + this.isDirty = true; + if (this.validatedOnce) { + this.validationController.validateBinding(this); + } }; } - // tslint:disable-next-line:no-bitwise - if (trigger & exports.validateTrigger.blur) { - binding.validateBlurHandler = () => { - this.taskQueue.queueMicroTask(() => controller.validateBinding(binding)); + if (event !== null) { + binding.focusLossHandler = () => { + this.taskQueue.queueMicroTask(() => { + if (binding.isDirty) { + controller.validateBinding(binding); + binding.validatedOnce = true; + } + }); }; + binding.validationTriggerEvent = event; binding.validateTarget = target; - target.addEventListener('blur', binding.validateBlurHandler); + target.addEventListener(event, binding.focusLossHandler); + if (hasChangeTrigger) { + const { propertyName } = getPropertyInfo(binding.sourceExpression, binding.source); + binding.validationSubscription = controller.subscribe((event) => { + if (!binding.validatedOnce && event.type === 'validate') { + binding.validatedOnce = event.errors.findIndex((e) => e.propertyName === propertyName) > -1; + } + }); + } } if (trigger !== exports.validateTrigger.manual) { binding.standardUpdateTarget = binding.updateTarget; @@ -1040,13 +1076,19 @@ binding.updateTarget = binding.standardUpdateTarget; binding.standardUpdateTarget = null; } - if (binding.validateBlurHandler) { - binding.validateTarget.removeEventListener('blur', binding.validateBlurHandler); - binding.validateBlurHandler = null; + if (binding.focusLossHandler) { + binding.validateTarget.removeEventListener(binding.validationTriggerEvent, binding.focusLossHandler); + binding.focusLossHandler = null; binding.validateTarget = null; } + if (binding.validationSubscription) { + binding.validationSubscription.dispose(); + binding.validationSubscription = null; + } binding.validationController.unregisterBinding(binding); binding.validationController = null; + binding.isDirty = null; + binding.validatedOnce = null; } } @@ -1118,7 +1160,25 @@ exports.ValidateOnChangeOrBlurBindingBehavior.inject = [aureliaTaskQueue.TaskQueue]; exports.ValidateOnChangeOrBlurBindingBehavior = __decorate([ aureliaBinding.bindingBehavior('validateOnChangeOrBlur') - ], exports.ValidateOnChangeOrBlurBindingBehavior); + ], exports.ValidateOnChangeOrBlurBindingBehavior); + exports.ValidateOnFocusoutBindingBehavior = class ValidateOnFocusoutBindingBehavior extends ValidateBindingBehaviorBase { + getValidateTrigger() { + return exports.validateTrigger.focusout; + } + }; + exports.ValidateOnFocusoutBindingBehavior.inject = [aureliaTaskQueue.TaskQueue]; + exports.ValidateOnFocusoutBindingBehavior = __decorate([ + aureliaBinding.bindingBehavior('validateOnFocusout') + ], exports.ValidateOnFocusoutBindingBehavior); + exports.ValidateOnChangeOrFocusoutBindingBehavior = class ValidateOnChangeOrFocusoutBindingBehavior extends ValidateBindingBehaviorBase { + getValidateTrigger() { + return exports.validateTrigger.changeOrFocusout; + } + }; + exports.ValidateOnChangeOrFocusoutBindingBehavior.inject = [aureliaTaskQueue.TaskQueue]; + exports.ValidateOnChangeOrFocusoutBindingBehavior = __decorate([ + aureliaBinding.bindingBehavior('validateOnChangeOrFocusout') + ], exports.ValidateOnChangeOrFocusoutBindingBehavior); /** * Creates ValidationController instances. @@ -1736,7 +1796,7 @@ config.apply(frameworkConfig.container); // globalize the behaviors. if (frameworkConfig.globalResources) { - frameworkConfig.globalResources(exports.ValidateBindingBehavior, exports.ValidateManuallyBindingBehavior, exports.ValidateOnBlurBindingBehavior, exports.ValidateOnChangeBindingBehavior, exports.ValidateOnChangeOrBlurBindingBehavior, exports.ValidationErrorsCustomAttribute, exports.ValidationRendererCustomAttribute); + frameworkConfig.globalResources(exports.ValidateBindingBehavior, exports.ValidateManuallyBindingBehavior, exports.ValidateOnBlurBindingBehavior, exports.ValidateOnFocusoutBindingBehavior, exports.ValidateOnChangeBindingBehavior, exports.ValidateOnChangeOrBlurBindingBehavior, exports.ValidateOnChangeOrFocusoutBindingBehavior, exports.ValidationErrorsCustomAttribute, exports.ValidationRendererCustomAttribute); } } diff --git a/dist/umd/aurelia-validation.js b/dist/umd/aurelia-validation.js index d9784e45..7efcd39b 100644 --- a/dist/umd/aurelia-validation.js +++ b/dist/umd/aurelia-validation.js @@ -458,6 +458,16 @@ * when it updates the model due to a change in the view. */ validateTrigger[validateTrigger["changeOrBlur"] = 3] = "changeOrBlur"; + /** + * Validate the binding when the binding's target element fires a DOM "focusout" event. + * Unlike "blur", this event bubbles. + */ + validateTrigger[validateTrigger["focusout"] = 4] = "focusout"; + /** + * Validate the binding when the binding's target element fires a DOM "focusout" event or + * when it updates the model due to a change in the view. + */ + validateTrigger[validateTrigger["changeOrFocusout"] = 6] = "changeOrFocusout"; })(exports.validateTrigger || (exports.validateTrigger = {})); /** @@ -1045,6 +1055,7 @@ return ValidationController; }()); + // tslint:disable:no-bitwise /** * Binding behavior. Indicates the bound property should be validated. */ @@ -1071,23 +1082,48 @@ controller.registerBinding(binding, target, rules); binding.validationController = controller; var trigger = this.getValidateTrigger(controller); - // tslint:disable-next-line:no-bitwise - if (trigger & exports.validateTrigger.change) { + var event = (trigger & exports.validateTrigger.blur) === exports.validateTrigger.blur ? 'blur' + : (trigger & exports.validateTrigger.focusout) === exports.validateTrigger.focusout ? 'focusout' + : null; + var hasChangeTrigger = (trigger & exports.validateTrigger.change) === exports.validateTrigger.change; + binding.isDirty = !hasChangeTrigger; + // validatedOnce is used to control whether controller should validate upon user input + // + // always true when validation trigger doesn't include "blur" event (blur/focusout) + // else it will be set to true after (a) the first user input & loss of focus or (b) validation + binding.validatedOnce = hasChangeTrigger && event === null; + if (hasChangeTrigger) { 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); + this.isDirty = true; + if (this.validatedOnce) { + 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); }); + if (event !== null) { + binding.focusLossHandler = function () { + _this.taskQueue.queueMicroTask(function () { + if (binding.isDirty) { + controller.validateBinding(binding); + binding.validatedOnce = true; + } + }); }; + binding.validationTriggerEvent = event; binding.validateTarget = target; - target.addEventListener('blur', binding.validateBlurHandler); + target.addEventListener(event, binding.focusLossHandler); + if (hasChangeTrigger) { + var propertyName_1 = getPropertyInfo(binding.sourceExpression, binding.source).propertyName; + binding.validationSubscription = controller.subscribe(function (event) { + if (!binding.validatedOnce && event.type === 'validate') { + binding.validatedOnce = event.errors.findIndex(function (e) { return e.propertyName === propertyName_1; }) > -1; + } + }); + } } if (trigger !== exports.validateTrigger.manual) { binding.standardUpdateTarget = binding.updateTarget; @@ -1109,13 +1145,19 @@ binding.updateTarget = binding.standardUpdateTarget; binding.standardUpdateTarget = null; } - if (binding.validateBlurHandler) { - binding.validateTarget.removeEventListener('blur', binding.validateBlurHandler); - binding.validateBlurHandler = null; + if (binding.focusLossHandler) { + binding.validateTarget.removeEventListener(binding.validationTriggerEvent, binding.focusLossHandler); + binding.focusLossHandler = null; binding.validateTarget = null; } + if (binding.validationSubscription) { + binding.validationSubscription.dispose(); + binding.validationSubscription = null; + } binding.validationController.unregisterBinding(binding); binding.validationController = null; + binding.isDirty = null; + binding.validatedOnce = null; }; return ValidateBindingBehaviorBase; }()); @@ -1213,6 +1255,34 @@ aureliaBinding.bindingBehavior('validateOnChangeOrBlur') ], ValidateOnChangeOrBlurBindingBehavior); return ValidateOnChangeOrBlurBindingBehavior; + }(ValidateBindingBehaviorBase)); + var ValidateOnFocusoutBindingBehavior = /** @class */ (function (_super) { + __extends(ValidateOnFocusoutBindingBehavior, _super); + function ValidateOnFocusoutBindingBehavior() { + return _super !== null && _super.apply(this, arguments) || this; + } + ValidateOnFocusoutBindingBehavior.prototype.getValidateTrigger = function () { + return exports.validateTrigger.focusout; + }; + ValidateOnFocusoutBindingBehavior.inject = [aureliaTaskQueue.TaskQueue]; + ValidateOnFocusoutBindingBehavior = __decorate([ + aureliaBinding.bindingBehavior('validateOnFocusout') + ], ValidateOnFocusoutBindingBehavior); + return ValidateOnFocusoutBindingBehavior; + }(ValidateBindingBehaviorBase)); + var ValidateOnChangeOrFocusoutBindingBehavior = /** @class */ (function (_super) { + __extends(ValidateOnChangeOrFocusoutBindingBehavior, _super); + function ValidateOnChangeOrFocusoutBindingBehavior() { + return _super !== null && _super.apply(this, arguments) || this; + } + ValidateOnChangeOrFocusoutBindingBehavior.prototype.getValidateTrigger = function () { + return exports.validateTrigger.changeOrFocusout; + }; + ValidateOnChangeOrFocusoutBindingBehavior.inject = [aureliaTaskQueue.TaskQueue]; + ValidateOnChangeOrFocusoutBindingBehavior = __decorate([ + aureliaBinding.bindingBehavior('validateOnChangeOrFocusout') + ], ValidateOnChangeOrFocusoutBindingBehavior); + return ValidateOnChangeOrFocusoutBindingBehavior; }(ValidateBindingBehaviorBase)); /** @@ -1869,7 +1939,7 @@ config.apply(frameworkConfig.container); // globalize the behaviors. if (frameworkConfig.globalResources) { - frameworkConfig.globalResources(ValidateBindingBehavior, ValidateManuallyBindingBehavior, ValidateOnBlurBindingBehavior, ValidateOnChangeBindingBehavior, ValidateOnChangeOrBlurBindingBehavior, ValidationErrorsCustomAttribute, ValidationRendererCustomAttribute); + frameworkConfig.globalResources(ValidateBindingBehavior, ValidateManuallyBindingBehavior, ValidateOnBlurBindingBehavior, ValidateOnFocusoutBindingBehavior, ValidateOnChangeBindingBehavior, ValidateOnChangeOrBlurBindingBehavior, ValidateOnChangeOrFocusoutBindingBehavior, ValidationErrorsCustomAttribute, ValidationRendererCustomAttribute); } } @@ -1884,6 +1954,8 @@ exports.ValidateOnBlurBindingBehavior = ValidateOnBlurBindingBehavior; exports.ValidateOnChangeBindingBehavior = ValidateOnChangeBindingBehavior; exports.ValidateOnChangeOrBlurBindingBehavior = ValidateOnChangeOrBlurBindingBehavior; + exports.ValidateOnFocusoutBindingBehavior = ValidateOnFocusoutBindingBehavior; + exports.ValidateOnChangeOrFocusoutBindingBehavior = ValidateOnChangeOrFocusoutBindingBehavior; exports.ValidateEvent = ValidateEvent; exports.ValidateResult = ValidateResult; exports.ValidationController = ValidationController; diff --git a/doc/CHANGELOG.md b/doc/CHANGELOG.md index 23562e05..5a392612 100644 --- a/doc/CHANGELOG.md +++ b/doc/CHANGELOG.md @@ -1,25 +1,42 @@ -# Change Log - -All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines. - -# [1.6.0](https://github.com/aurelia/validation/compare/1.5.0...1.6.0) (2019-12-18) - - -### Bug Fixes - -* **all:** update binding library and fix TS errors ([33c91a1](https://github.com/aurelia/validation/commit/33c91a1120c0cc41a3a1bb093aca7fd682d56e34)) -* **ExpressionVisitor:** not redeclare imports ([15109b3](https://github.com/aurelia/validation/commit/15109b3a82f0d0ecebaf19c7b1490c4373731c13)), closes [#537](https://github.com/aurelia/validation/issues/537) - - -### Features - -* **config:** make config constructor param optional and place default trigger in static property ([a52f4c4](https://github.com/aurelia/validation/commit/a52f4c403bb68ed895fd0b7a3ea217c67ce22e2d)) -* **config:** make the setters chainable ([bd118a6](https://github.com/aurelia/validation/commit/bd118a61c9cb283cbb9db36712660a0362f5994c)) -* **config:** rename global config class ([c4e5fe2](https://github.com/aurelia/validation/commit/c4e5fe29e2a9bcd11cfdaba0f70050ef9c894be6)) -* **config:** support global config option for default validation trigger ([39a4e67](https://github.com/aurelia/validation/commit/39a4e679513e3fa716b46cedce24931b3b57028f)) - - - +# [2.0.0-rc1](https://github.com/aurelia/validation/compare/1.6.0...2.0.0-rc1) (2020-03-26) + + +### Features + +* focusout trigger + changeOrX behavior change ([62f0579](https://github.com/aurelia/validation/commit/62f05791327716399644bceb410372f53d2da6dc)), closes [#509](https://github.com/aurelia/validation/issues/509) [#543](https://github.com/aurelia/validation/issues/543) + + +### BREAKING CHANGES + +* This commit changes the default behavior for the +changeOrBlur trigger. +- The change trigger is ineffective till the + associated property is validated once, either by manual validation or + by blur-triggered validation. This prevents showing validation failure + immediately in case of an incomplete input. Note the distinction made + between *incomplete* and *invalid* input. +- The blur trigger is ineffective until the property is dirty; i.e. any + changes were made to the property. This prevents showing a failure + message when there is a blur event w/o changing the property. + +# [1.6.0](https://github.com/aurelia/validation/compare/1.5.0...1.6.0) (2019-12-18) + + +### Bug Fixes + +* **all:** update binding library and fix TS errors ([33c91a1](https://github.com/aurelia/validation/commit/33c91a1120c0cc41a3a1bb093aca7fd682d56e34)) +* **ExpressionVisitor:** not redeclare imports ([15109b3](https://github.com/aurelia/validation/commit/15109b3a82f0d0ecebaf19c7b1490c4373731c13)), closes [#537](https://github.com/aurelia/validation/issues/537) + + +### Features + +* **config:** make config constructor param optional and place default trigger in static property ([a52f4c4](https://github.com/aurelia/validation/commit/a52f4c403bb68ed895fd0b7a3ea217c67ce22e2d)) +* **config:** make the setters chainable ([bd118a6](https://github.com/aurelia/validation/commit/bd118a61c9cb283cbb9db36712660a0362f5994c)) +* **config:** rename global config class ([c4e5fe2](https://github.com/aurelia/validation/commit/c4e5fe29e2a9bcd11cfdaba0f70050ef9c894be6)) +* **config:** support global config option for default validation trigger ([39a4e67](https://github.com/aurelia/validation/commit/39a4e679513e3fa716b46cedce24931b3b57028f)) + + + ## [1.5.0](https://github.com/aurelia/validation/compare/1.4.0...1.5.0) (2019-08-09) * Update type definitions for compatibility with latest dependency-injection release. diff --git a/package-lock.json b/package-lock.json index 5cde2e52..3e77c673 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "aurelia-validation", - "version": "1.6.0", + "version": "2.0.0-rc1", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/package.json b/package.json index 7d626f89..d18108a6 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "aurelia-validation", - "version": "1.6.0", + "version": "2.0.0-rc1", "description": "Validation for Aurelia applications", "keywords": [ "aurelia", @@ -43,6 +43,7 @@ "doc": "cross-env typedoc --json doc/api.json --excludeExternals --includeDeclarations --mode modules --target ES6 --name aurelia-validation-docs dist/doc-temp/", "postdoc": "cross-env node doc/shape-doc && rimraf dist/doc-temp", "precut-release": "cross-env npm run test", + "changelog": "conventional-changelog -p angular -i doc/CHANGELOG.md -s", "cut-release": "standard-version -t \"\" -i doc/CHANGELOG.md && npm run build && npm run doc" }, "jspm": {