From 7f59fa1c57ee7535bd8818c6b908ce48e51791a2 Mon Sep 17 00:00:00 2001 From: Martin Staffa Date: Wed, 29 Nov 2017 14:57:43 +0100 Subject: [PATCH] fix(ngModelController): allow $overrideModelOptions to set updateOn Also adds more docs about "default" events and how to override ModelOptions. Closes #16351 --- src/ng/directive/ngModel.js | 35 ++++++- src/ng/directive/ngModelOptions.js | 125 +++++++++++++++++++++++- test/ng/directive/ngModelOptionsSpec.js | 37 +++++++ 3 files changed, 191 insertions(+), 6 deletions(-) diff --git a/src/ng/directive/ngModel.js b/src/ng/directive/ngModel.js index 8afa3da7f64a..2dbede979fd5 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; @@ -877,11 +880,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(); }, /** @@ -1029,6 +1043,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); } }; @@ -1320,11 +1349,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 2defcee0d128..03c7e5945b5c 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,106 @@ defaultModelOptions = new ModelOptions({ * * * + * ### Default events, extra triggers, and catch-all debounce values + * + * This example shows the relationship between "default" update events and + * additional `updateOn` triggers. + * + * `default` events are those that are bound to the control, and when fired, update the `$viewValue` + * via {@link ngModel.NgModelController#$setViewValue $setViewValue}. Every event that is not listed + * in `updateOn` is considered a "default" event, since different control types have different + * default events. + * + * The control in this example updates by "default", "click", and "blur", with different `debounce` + * values. You can see that "click" doesn't have an individual `debounce` value - + * therefore it uses the `*` debounce value. + * + * There is also a button that calls {@link ngModel.NgModelController#$setViewValue $setViewValue} + * directly with a "custom" event. Since "custom" is not defined in the `updateOn` list, + * it is considered a "default" event and will update the + * control if "default" is defined in `updateOn`, and will receive the "default" debounce value. + * Note that this is just to illustrate how custom controls would possibly call `$setViewValue`. + * + * You can change the `updateOn` and `debounce` configuration to test different scenarios. This + * is done with {@link ngModel.NgModelController#$overrideModelOptions $overrideModelOptions}. + * + + + + + + angular.module('optionsExample', []) + .component('modelUpdateDemo', { + templateUrl: 'template.html', + controller: function() { + this.name = 'Chinua'; + + this.options = { + updateOn: 'default blur click', + debounce: { + default: 2000, + blur: 0, + '*': 1000 + } + }; + + this.updateEvents = function() { + var eventList = this.options.updateOn.split(' '); + eventList.push('*'); + var events = {}; + + for (var i = 0; i < eventList.length; i++) { + events[eventList[i]] = this.options.debounce[eventList[i]]; + } + + this.events = events; + }; + + this.updateOptions = function() { + var options = angular.extend(this.options, { + updateOn: Object.keys(this.events).join(' ').replace('*', ''), + debounce: this.events + }); + + this.form.input.$overrideModelOptions(options); + }; + + // Initialize the event form + this.updateEvents(); + } + }); + + +
+ Input: +
+ Model: {{$ctrl.name}} +
+ + +
+
+ updateOn
+ + + + + + + + + + + +
OptionDebounce value
{{key}}
+ +
+ +
+
+
+ * + * * ## Model updates and validation * * The default behaviour in `ngModel` is that the model value is set to `undefined` when the @@ -307,11 +411,30 @@ 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}. See also the example for + * {@link ngModelOptions#default-events-extra-triggers-and-catch-all-debounce-values + * Default events, extra triggers, and catch-all debounce values}. + * + * * @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: diff --git a/test/ng/directive/ngModelOptionsSpec.js b/test/ng/directive/ngModelOptionsSpec.js index 3814ba2bc5ca..09a9ad5f4a7c 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'); + }); + });