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

fix(ngModelController): allow $overrideModelOptions to set updateOn #16352

Merged
merged 1 commit into from
Dec 5, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 30 additions & 5 deletions src/ng/directive/ngModel.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -877,11 +880,22 @@ NgModelController.prototype = {
* See {@link ngModelOptions} for information about what options can be specified
* and how model option inheritance works.
*
* <div class="alert alert-warning">
* **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.
* </div>
*
* <div class="alert alert-danger">
* **Note:** it is not possible to override the `getterSetter` option.
* </div>
*
* @param {Object} options a hash of settings to override the previous options
*
*/
$overrideModelOptions: function(options) {
this.$options = this.$options.createChild(options);
this.$$setUpdateOnEvents();
},

/**
Expand Down Expand Up @@ -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);
}
};

Expand Down Expand Up @@ -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();
Expand Down
125 changes: 124 additions & 1 deletion src/ng/directive/ngModelOptions.js
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -236,6 +238,8 @@ defaultModelOptions = new ModelOptions({
* </file>
* </example>
*
* ### 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.
*
Expand All @@ -260,6 +264,106 @@ defaultModelOptions = new ModelOptions({
* </file>
* </example>
*
* ### 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}.
*
<example name="ngModelOptions-advanced" module="optionsExample">
<file name="index.html">
<model-update-demo></model-update-demo>
</file>
<file name="app.js">
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();
}
});
</file>
<file name="template.html">
<form name="$ctrl.form">
Input: <input type="text" name="input" ng-model="$ctrl.name" ng-model-options="$ctrl.options" />
</form>
Model: <tt>{{$ctrl.name}}</tt>
<hr>
<button ng-click="$ctrl.form.input.$setViewValue('some value', 'custom')">Trigger setViewValue with 'some value' and 'custom' event</button>

<hr>
<form ng-submit="$ctrl.updateOptions()">
<b>updateOn</b><br>
<input type="text" ng-model="$ctrl.options.updateOn" ng-change="$ctrl.updateEvents()" ng-model-options="{debounce: 500}">

<table>
<tr>
<th>Option</th>
<th>Debounce value</th>
</tr>
<tr ng-repeat="(key, value) in $ctrl.events">
<td>{{key}}</td>
<td><input type="number" ng-model="$ctrl.events[key]" /></td>
</tr>
</table>

<br>
<input type="submit" value="Update options">
</form>
</file>
</example>
*
*
* ## Model updates and validation
*
* The default behaviour in `ngModel` is that the model value is set to `undefined` when the
Expand Down Expand Up @@ -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,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

😞

* 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:
Expand Down
37 changes: 37 additions & 0 deletions test/ng/directive/ngModelOptionsSpec.js
Original file line number Diff line number Diff line change
Expand Up @@ -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(
'<input type="text" ng-model="name" name="alias" ' +
'ng-model-options="{ updateOn: \'blur click\' }"' +
'/>');

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');
});

});


Expand Down