Skip to content

Commit

Permalink
fix(ngModelController): allow $overrideModelOptions to set updateOn
Browse files Browse the repository at this point in the history
Also adds more docs about "default" events and how to override
ModelOptions.

Closes angular#16351
  • Loading branch information
Narretz committed Nov 29, 2017
1 parent 07e4751 commit 3984ce4
Show file tree
Hide file tree
Showing 3 changed files with 189 additions and 4 deletions.
31 changes: 28 additions & 3 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:** overriding the model options with this function only affects the
* current `ngModelController` and does not propagate the new options to
* `ngModelOptions` directives that have initially inherited from it.
* </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,19 @@ NgModelController.prototype = {
this.$modelValue = this.$$rawModelValue = modelValue;
this.$$parserValid = undefined;
this.$processModelValue();
},

$$setUpdateOnEvents: function() {
if (this.$$updateEvents.length > 0) {
this.$$element.off(this.$$updateEvents, this.$$updateEventHandler);
}

this.$$updateEvents = this.$options.getOption('updateOn');
this.$$element.on(this.$$updateEvents, this.$$updateEventHandler);
},

$$updateEventHandler: function(ev) {
this.$$debounceViewValueCommit(ev && ev.type);
}
};

Expand Down Expand Up @@ -1321,9 +1348,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() {
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 eventL = this.options.updateOn.split(' ');
eventL.push('*');
var events = {};
for (var i = 0; i < eventL.length; i++) {
events[eventL[i]] = this.options.debounce[eventL[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,
* 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

0 comments on commit 3984ce4

Please sign in to comment.