Skip to content
This repository was archived by the owner on Apr 12, 2024. It is now read-only.

Commit da72477

Browse files
authored
fix(ngModelController): allow $overrideModelOptions to set updateOn
Also adds more docs about "default" events and how to override ngModelController options. Closes #16351 Closes #16352
1 parent 74b04c9 commit da72477

File tree

3 files changed

+191
-6
lines changed

3 files changed

+191
-6
lines changed

src/ng/directive/ngModel.js

+30-5
Original file line numberDiff line numberDiff line change
@@ -270,6 +270,9 @@ function NgModelController($scope, $exceptionHandler, $attr, $element, $parse, $
270270
this.$name = $interpolate($attr.name || '', false)($scope);
271271
this.$$parentForm = nullFormCtrl;
272272
this.$options = defaultModelOptions;
273+
this.$$updateEvents = '';
274+
// Attach the correct context to the event handler function for updateOn
275+
this.$$updateEventHandler = this.$$updateEventHandler.bind(this);
273276

274277
this.$$parsedNgModel = $parse($attr.ngModel);
275278
this.$$parsedNgModelAssign = this.$$parsedNgModel.assign;
@@ -884,11 +887,22 @@ NgModelController.prototype = {
884887
* See {@link ngModelOptions} for information about what options can be specified
885888
* and how model option inheritance works.
886889
*
890+
* <div class="alert alert-warning">
891+
* **Note:** this function only affects the options set on the `ngModelController`,
892+
* and not the options on the {@link ngModelOptions} directive from which they might have been
893+
* obtained initially.
894+
* </div>
895+
*
896+
* <div class="alert alert-danger">
897+
* **Note:** it is not possible to override the `getterSetter` option.
898+
* </div>
899+
*
887900
* @param {Object} options a hash of settings to override the previous options
888901
*
889902
*/
890903
$overrideModelOptions: function(options) {
891904
this.$options = this.$options.createChild(options);
905+
this.$$setUpdateOnEvents();
892906
},
893907

894908
/**
@@ -1036,6 +1050,21 @@ NgModelController.prototype = {
10361050
this.$modelValue = this.$$rawModelValue = modelValue;
10371051
this.$$parserValid = undefined;
10381052
this.$processModelValue();
1053+
},
1054+
1055+
$$setUpdateOnEvents: function() {
1056+
if (this.$$updateEvents) {
1057+
this.$$element.off(this.$$updateEvents, this.$$updateEventHandler);
1058+
}
1059+
1060+
this.$$updateEvents = this.$options.getOption('updateOn');
1061+
if (this.$$updateEvents) {
1062+
this.$$element.on(this.$$updateEvents, this.$$updateEventHandler);
1063+
}
1064+
},
1065+
1066+
$$updateEventHandler: function(ev) {
1067+
this.$$debounceViewValueCommit(ev && ev.type);
10391068
}
10401069
};
10411070

@@ -1327,11 +1356,7 @@ var ngModelDirective = ['$rootScope', function($rootScope) {
13271356
},
13281357
post: function ngModelPostLink(scope, element, attr, ctrls) {
13291358
var modelCtrl = ctrls[0];
1330-
if (modelCtrl.$options.getOption('updateOn')) {
1331-
element.on(modelCtrl.$options.getOption('updateOn'), function(ev) {
1332-
modelCtrl.$$debounceViewValueCommit(ev && ev.type);
1333-
});
1334-
}
1359+
modelCtrl.$$setUpdateOnEvents();
13351360

13361361
function setTouched() {
13371362
modelCtrl.$setTouched();

src/ng/directive/ngModelOptions.js

+124-1
Original file line numberDiff line numberDiff line change
@@ -177,6 +177,8 @@ defaultModelOptions = new ModelOptions({
177177
* `submit` event. Note that `ngClick` events will occur before the model is updated. Use `ngSubmit`
178178
* to have access to the updated model.
179179
*
180+
* ### Overriding immediate updates
181+
*
180182
* The following example shows how to override immediate updates. Changes on the inputs within the
181183
* form will update the model only when the control loses focus (blur event). If `escape` key is
182184
* 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({
236238
* </file>
237239
* </example>
238240
*
241+
* ### Debouncing updates
242+
*
239243
* The next example shows how to debounce model changes. Model will be updated only 1 sec after last change.
240244
* If the `Clear` button is pressed, any debounced action is canceled and the value becomes empty.
241245
*
@@ -260,6 +264,106 @@ defaultModelOptions = new ModelOptions({
260264
* </file>
261265
* </example>
262266
*
267+
* ### Default events, extra triggers, and catch-all debounce values
268+
*
269+
* This example shows the relationship between "default" update events and
270+
* additional `updateOn` triggers.
271+
*
272+
* `default` events are those that are bound to the control, and when fired, update the `$viewValue`
273+
* via {@link ngModel.NgModelController#$setViewValue $setViewValue}. Every event that is not listed
274+
* in `updateOn` is considered a "default" event, since different control types have different
275+
* default events.
276+
*
277+
* The control in this example updates by "default", "click", and "blur", with different `debounce`
278+
* values. You can see that "click" doesn't have an individual `debounce` value -
279+
* therefore it uses the `*` debounce value.
280+
*
281+
* There is also a button that calls {@link ngModel.NgModelController#$setViewValue $setViewValue}
282+
* directly with a "custom" event. Since "custom" is not defined in the `updateOn` list,
283+
* it is considered a "default" event and will update the
284+
* control if "default" is defined in `updateOn`, and will receive the "default" debounce value.
285+
* Note that this is just to illustrate how custom controls would possibly call `$setViewValue`.
286+
*
287+
* You can change the `updateOn` and `debounce` configuration to test different scenarios. This
288+
* is done with {@link ngModel.NgModelController#$overrideModelOptions $overrideModelOptions}.
289+
*
290+
<example name="ngModelOptions-advanced" module="optionsExample">
291+
<file name="index.html">
292+
<model-update-demo></model-update-demo>
293+
</file>
294+
<file name="app.js">
295+
angular.module('optionsExample', [])
296+
.component('modelUpdateDemo', {
297+
templateUrl: 'template.html',
298+
controller: function() {
299+
this.name = 'Chinua';
300+
301+
this.options = {
302+
updateOn: 'default blur click',
303+
debounce: {
304+
default: 2000,
305+
blur: 0,
306+
'*': 1000
307+
}
308+
};
309+
310+
this.updateEvents = function() {
311+
var eventList = this.options.updateOn.split(' ');
312+
eventList.push('*');
313+
var events = {};
314+
315+
for (var i = 0; i < eventList.length; i++) {
316+
events[eventList[i]] = this.options.debounce[eventList[i]];
317+
}
318+
319+
this.events = events;
320+
};
321+
322+
this.updateOptions = function() {
323+
var options = angular.extend(this.options, {
324+
updateOn: Object.keys(this.events).join(' ').replace('*', ''),
325+
debounce: this.events
326+
});
327+
328+
this.form.input.$overrideModelOptions(options);
329+
};
330+
331+
// Initialize the event form
332+
this.updateEvents();
333+
}
334+
});
335+
</file>
336+
<file name="template.html">
337+
<form name="$ctrl.form">
338+
Input: <input type="text" name="input" ng-model="$ctrl.name" ng-model-options="$ctrl.options" />
339+
</form>
340+
Model: <tt>{{$ctrl.name}}</tt>
341+
<hr>
342+
<button ng-click="$ctrl.form.input.$setViewValue('some value', 'custom')">Trigger setViewValue with 'some value' and 'custom' event</button>
343+
344+
<hr>
345+
<form ng-submit="$ctrl.updateOptions()">
346+
<b>updateOn</b><br>
347+
<input type="text" ng-model="$ctrl.options.updateOn" ng-change="$ctrl.updateEvents()" ng-model-options="{debounce: 500}">
348+
349+
<table>
350+
<tr>
351+
<th>Option</th>
352+
<th>Debounce value</th>
353+
</tr>
354+
<tr ng-repeat="(key, value) in $ctrl.events">
355+
<td>{{key}}</td>
356+
<td><input type="number" ng-model="$ctrl.events[key]" /></td>
357+
</tr>
358+
</table>
359+
360+
<br>
361+
<input type="submit" value="Update options">
362+
</form>
363+
</file>
364+
</example>
365+
*
366+
*
263367
* ## Model updates and validation
264368
*
265369
* The default behaviour in `ngModel` is that the model value is set to `undefined` when the
@@ -307,11 +411,30 @@ defaultModelOptions = new ModelOptions({
307411
* You can specify the timezone that date/time input directives expect by providing its name in the
308412
* `timezone` property.
309413
*
414+
*
415+
* ## Programmatically changing options
416+
*
417+
* The `ngModelOptions` expression is only evaluated once when the directive is linked; it is not
418+
* watched for changes. However, it is possible to override the options on a single
419+
* {@link ngModel.NgModelController} instance with
420+
* {@link ngModel.NgModelController#$overrideModelOptions}. See also the example for
421+
* {@link ngModelOptions#default-events-extra-triggers-and-catch-all-debounce-values
422+
* Default events, extra triggers, and catch-all debounce values}.
423+
*
424+
*
310425
* @param {Object} ngModelOptions options to apply to {@link ngModel} directives on this element and
311426
* and its descendents. Valid keys are:
312427
* - `updateOn`: string specifying which event should the input be bound to. You can set several
313428
* events using an space delimited list. There is a special event called `default` that
314-
* matches the default events belonging to the control.
429+
* matches the default events belonging to the control. These are the events that are bound to
430+
* the control, and when fired, update the `$viewValue` via `$setViewValue`.
431+
*
432+
* `ngModelOptions` considers every event that is not listed in `updateOn` a "default" event,
433+
* since different control types use different default events.
434+
*
435+
* See also the section {@link ngModelOptions#triggering-and-debouncing-model-updates
436+
* Triggering and debouncing model updates}.
437+
*
315438
* - `debounce`: integer value which contains the debounce model update value in milliseconds. A
316439
* value of 0 triggers an immediate update. If an object is supplied instead, you can specify a
317440
* custom value for each event. For example:

test/ng/directive/ngModelOptionsSpec.js

+37
Original file line numberDiff line numberDiff line change
@@ -391,6 +391,43 @@ describe('ngModelOptions', function() {
391391
browserTrigger(inputElm[2], 'click');
392392
expect($rootScope.color).toBe('blue');
393393
});
394+
395+
it('should re-set the trigger events when overridden with $overrideModelOptions', function() {
396+
var inputElm = helper.compileInput(
397+
'<input type="text" ng-model="name" name="alias" ' +
398+
'ng-model-options="{ updateOn: \'blur click\' }"' +
399+
'/>');
400+
401+
var ctrl = inputElm.controller('ngModel');
402+
403+
helper.changeInputValueTo('a');
404+
expect($rootScope.name).toBeUndefined();
405+
browserTrigger(inputElm, 'blur');
406+
expect($rootScope.name).toEqual('a');
407+
408+
helper.changeInputValueTo('b');
409+
expect($rootScope.name).toBe('a');
410+
browserTrigger(inputElm, 'click');
411+
expect($rootScope.name).toEqual('b');
412+
413+
$rootScope.$apply('name = undefined');
414+
expect(inputElm.val()).toBe('');
415+
ctrl.$overrideModelOptions({updateOn: 'blur mousedown'});
416+
417+
helper.changeInputValueTo('a');
418+
expect($rootScope.name).toBeUndefined();
419+
browserTrigger(inputElm, 'blur');
420+
expect($rootScope.name).toEqual('a');
421+
422+
helper.changeInputValueTo('b');
423+
expect($rootScope.name).toBe('a');
424+
browserTrigger(inputElm, 'click');
425+
expect($rootScope.name).toBe('a');
426+
427+
browserTrigger(inputElm, 'mousedown');
428+
expect($rootScope.name).toEqual('b');
429+
});
430+
394431
});
395432

396433

0 commit comments

Comments
 (0)