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

Commit

Permalink
fix(ngModelController): introduce $cancelUpdate to cancel pending upd…
Browse files Browse the repository at this point in the history
…ates

The `$cancelUpdate()` method on `NgModelController` cancels any pending debounce
action and resets the view value by invoking `$render()`.

This method should be invoked before programmatic update to the model of inputs
that might have pending updates due to `ng-model-options` specifying `updateOn`
or `debounce` properties.

Fixes #6994
Closes #7014
  • Loading branch information
shahata authored and petebacondarwin committed Apr 8, 2014
1 parent b389cfc commit 940fcb4
Show file tree
Hide file tree
Showing 2 changed files with 56 additions and 26 deletions.
42 changes: 23 additions & 19 deletions src/ng/directive/input.js
Original file line number Diff line number Diff line change
Expand Up @@ -1577,7 +1577,8 @@ var NgModelController = ['$scope', '$exceptionHandler', '$attrs', '$element', '$

var ngModelGet = $parse($attr.ngModel),
ngModelSet = ngModelGet.assign,
pendingDebounce = null;
pendingDebounce = null,
ctrl = this;

if (!ngModelSet) {
throw minErr('ngModel')('nonassign', "Expression '{0}' is non-assignable. Element: {1}",
Expand Down Expand Up @@ -1693,19 +1694,26 @@ var NgModelController = ['$scope', '$exceptionHandler', '$attrs', '$element', '$

/**
* @ngdoc method
* @name ngModel.NgModelController#$cancelDebounce
* @name ngModel.NgModelController#$cancelUpdate
*
* @description
* Cancel a pending debounced update.
* Cancel an update and reset the input element's value to prevent an update to the `$viewValue`,
* which may be caused by a pending debounced event or because the input is waiting for a some
* future event.
*
* This method should be called before directly update a debounced model from the scope in
* order to prevent unintended future changes of the model value because of a delayed event.
* If you have an input that uses `ng-model-options` to set up debounced events or events such
* as blur you can have a situation where there is a period when the value of the input element
* is out of synch with the ngModel's `$viewValue`. You can run into difficulties if you try to
* update the ngModel's `$modelValue` programmatically before these debounced/future events have
* completed, because Angular's dirty checking mechanism is not able to tell whether the model
* has actually changed or not. This method should be called before directly updating a model
* from the scope in case you have an input with `ng-model-options` that do not include immediate
* update of the default trigger. This is important in order to make sure that this input field
* will be updated with the new value and any pending operation will be canceled.
*/
this.$cancelDebounce = function() {
if ( pendingDebounce ) {
$timeout.cancel(pendingDebounce);
pendingDebounce = null;
}
this.$cancelUpdate = function() {
$timeout.cancel(pendingDebounce);
this.$render();
};

// update the view value
Expand Down Expand Up @@ -1764,25 +1772,21 @@ var NgModelController = ['$scope', '$exceptionHandler', '$attrs', '$element', '$
* @param {string} trigger Event that triggered the update.
*/
this.$setViewValue = function(value, trigger) {
var that = this;
var debounceDelay = this.$options && (isObject(this.$options.debounce)
? (this.$options.debounce[trigger] || this.$options.debounce['default'] || 0)
: this.$options.debounce) || 0;

that.$cancelDebounce();
if ( debounceDelay ) {
$timeout.cancel(pendingDebounce);
if (debounceDelay) {
pendingDebounce = $timeout(function() {
pendingDebounce = null;
that.$$realSetViewValue(value);
ctrl.$$realSetViewValue(value);
}, debounceDelay);
} else {
that.$$realSetViewValue(value);
this.$$realSetViewValue(value);
}
};

// model -> value
var ctrl = this;

$scope.$watch(function ngModelWatch() {
var value = ngModelGet($scope);

Expand Down Expand Up @@ -2293,4 +2297,4 @@ var ngModelOptionsDirective = function() {
}
}]
};
};
};
40 changes: 33 additions & 7 deletions test/ng/directive/inputSpec.js
Original file line number Diff line number Diff line change
Expand Up @@ -847,22 +847,48 @@ describe('input', function() {
dealoc(doc);
}));


it('should allow cancelling pending updates', inject(function($timeout) {
it('should allow canceling pending updates', inject(function($timeout) {
compileInput(
'<form name="test">'+
'<input type="text" ng-model="name" name="alias" '+
'ng-model-options="{ debounce: 10000 }" />'+
'</form>');
'<input type="text" ng-model="name" name="alias" '+
'ng-model-options="{ debounce: 10000 }" />');

changeInputValueTo('a');
expect(scope.name).toEqual(undefined);
$timeout.flush(2000);
scope.test.alias.$cancelDebounce();
scope.form.alias.$cancelUpdate();
expect(scope.name).toEqual(undefined);
$timeout.flush(10000);
expect(scope.name).toEqual(undefined);
}));

it('should reset input val if cancelUpdate called during pending update', function() {
compileInput(
'<input type="text" ng-model="name" name="alias" '+
'ng-model-options="{ updateOn: \'blur\' }" />');
scope.$digest();

changeInputValueTo('a');
expect(inputElm.val()).toBe('a');
scope.form.alias.$cancelUpdate();
expect(inputElm.val()).toBe('');
browserTrigger(inputElm, 'blur');
expect(inputElm.val()).toBe('');
});

it('should reset input val if cancelUpdate called during debounce', inject(function($timeout) {
compileInput(
'<input type="text" ng-model="name" name="alias" '+
'ng-model-options="{ debounce: 2000 }" />');
scope.$digest();

changeInputValueTo('a');
expect(inputElm.val()).toBe('a');
scope.form.alias.$cancelUpdate();
expect(inputElm.val()).toBe('');
$timeout.flush(3000);
expect(inputElm.val()).toBe('');
}));

});

it('should allow complex reference binding', function() {
Expand Down

0 comments on commit 940fcb4

Please sign in to comment.