From bdd74b838f0a479296de07fb8e4eb1ee6b35e63e Mon Sep 17 00:00:00 2001 From: Martin Staffa Date: Tue, 5 Dec 2017 20:07:45 +0100 Subject: [PATCH] fix(ngModelController): allow $overrideModelOptions to set updateOn Also adds more docs about "default" events and how to override ngModelController options. Closes #16351 Closes #16352 --- src/ng/directive/ngModel.js | 35 +++++++++++++++++++---- src/ng/directive/ngModelOptions.js | 30 ++++++++++++++++++-- test/ng/directive/ngModelOptionsSpec.js | 37 +++++++++++++++++++++++++ 3 files changed, 95 insertions(+), 7 deletions(-) diff --git a/src/ng/directive/ngModel.js b/src/ng/directive/ngModel.js index d1d8e1821e67..3ea1e92887f9 100644 --- a/src/ng/directive/ngModel.js +++ b/src/ng/directive/ngModel.js @@ -270,6 +270,9 @@ function NgModelController($scope, $exceptionHandler, $attr, $element, $parse, $ this.$name = $interpolate($attr.name || '', false)($scope); this.$$parentForm = nullFormCtrl; this.$options = defaultModelOptions; + this.$$updateEvents = ''; + // Attach the correct context to the event handler function for updateOn + this.$$updateEventHandler = this.$$updateEventHandler.bind(this); this.$$parsedNgModel = $parse($attr.ngModel); this.$$parsedNgModelAssign = this.$$parsedNgModel.assign; @@ -875,11 +878,22 @@ NgModelController.prototype = { * See {@link ngModelOptions} for information about what options can be specified * and how model option inheritance works. * + *
+ * **Note:** this function only affects the options set on the `ngModelController`, + * and not the options on the {@link ngModelOptions} directive from which they might have been + * obtained initially. + *
+ * + *
+ * **Note:** it is not possible to override the `getterSetter` option. + *
+ * * @param {Object} options a hash of settings to override the previous options * */ $overrideModelOptions: function(options) { this.$options = this.$options.createChild(options); + this.$$setUpdateOnEvents(); }, /** @@ -1027,6 +1041,21 @@ NgModelController.prototype = { this.$modelValue = this.$$rawModelValue = modelValue; this.$$parserValid = undefined; this.$processModelValue(); + }, + + $$setUpdateOnEvents: function() { + if (this.$$updateEvents) { + this.$$element.off(this.$$updateEvents, this.$$updateEventHandler); + } + + this.$$updateEvents = this.$options.getOption('updateOn'); + if (this.$$updateEvents) { + this.$$element.on(this.$$updateEvents, this.$$updateEventHandler); + } + }, + + $$updateEventHandler: function(ev) { + this.$$debounceViewValueCommit(ev && ev.type); } }; @@ -1318,11 +1347,7 @@ var ngModelDirective = ['$rootScope', function($rootScope) { }, post: function ngModelPostLink(scope, element, attr, ctrls) { var modelCtrl = ctrls[0]; - if (modelCtrl.$options.getOption('updateOn')) { - element.on(modelCtrl.$options.getOption('updateOn'), function(ev) { - modelCtrl.$$debounceViewValueCommit(ev && ev.type); - }); - } + modelCtrl.$$setUpdateOnEvents(); function setTouched() { modelCtrl.$setTouched(); diff --git a/src/ng/directive/ngModelOptions.js b/src/ng/directive/ngModelOptions.js index f5d04ca0be40..93cb56af0d2f 100644 --- a/src/ng/directive/ngModelOptions.js +++ b/src/ng/directive/ngModelOptions.js @@ -177,6 +177,8 @@ defaultModelOptions = new ModelOptions({ * `submit` event. Note that `ngClick` events will occur before the model is updated. Use `ngSubmit` * to have access to the updated model. * + * ### Overriding immediate updates + * * The following example shows how to override immediate updates. Changes on the inputs within the * form will update the model only when the control loses focus (blur event). If `escape` key is * pressed while the input field is focused, the value is reset to the value in the current model. @@ -236,6 +238,8 @@ defaultModelOptions = new ModelOptions({ * * * + * ### Debouncing updates + * * The next example shows how to debounce model changes. Model will be updated only 1 sec after last change. * If the `Clear` button is pressed, any debounced action is canceled and the value becomes empty. * @@ -260,6 +264,7 @@ defaultModelOptions = new ModelOptions({ * * * + * * ## Model updates and validation * * The default behaviour in `ngModel` is that the model value is set to `undefined` when the @@ -307,20 +312,41 @@ defaultModelOptions = new ModelOptions({ * You can specify the timezone that date/time input directives expect by providing its name in the * `timezone` property. * + * + * ## Programmatically changing options + * + * The `ngModelOptions` expression is only evaluated once when the directive is linked; it is not + * watched for changes. However, it is possible to override the options on a single + * {@link ngModel.NgModelController} instance with + * {@link ngModel.NgModelController#$overrideModelOptions `NgModelController#$overrideModelOptions()`}. + * + * * @param {Object} ngModelOptions options to apply to {@link ngModel} directives on this element and * and its descendents. Valid keys are: * - `updateOn`: string specifying which event should the input be bound to. You can set several * events using an space delimited list. There is a special event called `default` that - * matches the default events belonging to the control. + * matches the default events belonging to the control. These are the events that are bound to + * the control, and when fired, update the `$viewValue` via `$setViewValue`. + * + * `ngModelOptions` considers every event that is not listed in `updateOn` a "default" event, + * since different control types use different default events. + * + * See also the section {@link ngModelOptions#triggering-and-debouncing-model-updates + * Triggering and debouncing model updates}. + * * - `debounce`: integer value which contains the debounce model update value in milliseconds. A * value of 0 triggers an immediate update. If an object is supplied instead, you can specify a * custom value for each event. For example: * ``` * ng-model-options="{ - * updateOn: 'default blur', + * updateOn: 'default blur click', * debounce: { 'default': 500, 'blur': 0 } * }" * ``` + * + * "default" also applies to all events that are listed in `updateOn` but are not + * listed in `debounce`, i.e. "click" would also be debounced by 500 milliseconds. + * * - `allowInvalid`: boolean value which indicates that the model can be set with values that did * not validate correctly instead of the default behavior of setting the model to undefined. * - `getterSetter`: boolean value which determines whether or not to treat functions bound to diff --git a/test/ng/directive/ngModelOptionsSpec.js b/test/ng/directive/ngModelOptionsSpec.js index f4142ae217ee..eb2b0de9993d 100644 --- a/test/ng/directive/ngModelOptionsSpec.js +++ b/test/ng/directive/ngModelOptionsSpec.js @@ -391,6 +391,43 @@ describe('ngModelOptions', function() { browserTrigger(inputElm[2], 'click'); expect($rootScope.color).toBe('blue'); }); + + it('should re-set the trigger events when overridden with $overrideModelOptions', function() { + var inputElm = helper.compileInput( + ''); + + var ctrl = inputElm.controller('ngModel'); + + helper.changeInputValueTo('a'); + expect($rootScope.name).toBeUndefined(); + browserTrigger(inputElm, 'blur'); + expect($rootScope.name).toEqual('a'); + + helper.changeInputValueTo('b'); + expect($rootScope.name).toBe('a'); + browserTrigger(inputElm, 'click'); + expect($rootScope.name).toEqual('b'); + + $rootScope.$apply('name = undefined'); + expect(inputElm.val()).toBe(''); + ctrl.$overrideModelOptions({updateOn: 'blur mousedown'}); + + helper.changeInputValueTo('a'); + expect($rootScope.name).toBeUndefined(); + browserTrigger(inputElm, 'blur'); + expect($rootScope.name).toEqual('a'); + + helper.changeInputValueTo('b'); + expect($rootScope.name).toBe('a'); + browserTrigger(inputElm, 'click'); + expect($rootScope.name).toBe('a'); + + browserTrigger(inputElm, 'mousedown'); + expect($rootScope.name).toEqual('b'); + }); + });