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

Commit 940fcb4

Browse files
shahatapetebacondarwin
authored andcommitted
fix(ngModelController): introduce $cancelUpdate to cancel pending updates
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
1 parent b389cfc commit 940fcb4

File tree

2 files changed

+56
-26
lines changed

2 files changed

+56
-26
lines changed

src/ng/directive/input.js

+23-19
Original file line numberDiff line numberDiff line change
@@ -1577,7 +1577,8 @@ var NgModelController = ['$scope', '$exceptionHandler', '$attrs', '$element', '$
15771577

15781578
var ngModelGet = $parse($attr.ngModel),
15791579
ngModelSet = ngModelGet.assign,
1580-
pendingDebounce = null;
1580+
pendingDebounce = null,
1581+
ctrl = this;
15811582

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

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

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

1772-
that.$cancelDebounce();
1773-
if ( debounceDelay ) {
1779+
$timeout.cancel(pendingDebounce);
1780+
if (debounceDelay) {
17741781
pendingDebounce = $timeout(function() {
1775-
pendingDebounce = null;
1776-
that.$$realSetViewValue(value);
1782+
ctrl.$$realSetViewValue(value);
17771783
}, debounceDelay);
17781784
} else {
1779-
that.$$realSetViewValue(value);
1785+
this.$$realSetViewValue(value);
17801786
}
17811787
};
17821788

17831789
// model -> value
1784-
var ctrl = this;
1785-
17861790
$scope.$watch(function ngModelWatch() {
17871791
var value = ngModelGet($scope);
17881792

@@ -2293,4 +2297,4 @@ var ngModelOptionsDirective = function() {
22932297
}
22942298
}]
22952299
};
2296-
};
2300+
};

test/ng/directive/inputSpec.js

+33-7
Original file line numberDiff line numberDiff line change
@@ -847,22 +847,48 @@ describe('input', function() {
847847
dealoc(doc);
848848
}));
849849

850-
851-
it('should allow cancelling pending updates', inject(function($timeout) {
850+
it('should allow canceling pending updates', inject(function($timeout) {
852851
compileInput(
853-
'<form name="test">'+
854-
'<input type="text" ng-model="name" name="alias" '+
855-
'ng-model-options="{ debounce: 10000 }" />'+
856-
'</form>');
852+
'<input type="text" ng-model="name" name="alias" '+
853+
'ng-model-options="{ debounce: 10000 }" />');
854+
857855
changeInputValueTo('a');
858856
expect(scope.name).toEqual(undefined);
859857
$timeout.flush(2000);
860-
scope.test.alias.$cancelDebounce();
858+
scope.form.alias.$cancelUpdate();
861859
expect(scope.name).toEqual(undefined);
862860
$timeout.flush(10000);
863861
expect(scope.name).toEqual(undefined);
864862
}));
865863

864+
it('should reset input val if cancelUpdate called during pending update', function() {
865+
compileInput(
866+
'<input type="text" ng-model="name" name="alias" '+
867+
'ng-model-options="{ updateOn: \'blur\' }" />');
868+
scope.$digest();
869+
870+
changeInputValueTo('a');
871+
expect(inputElm.val()).toBe('a');
872+
scope.form.alias.$cancelUpdate();
873+
expect(inputElm.val()).toBe('');
874+
browserTrigger(inputElm, 'blur');
875+
expect(inputElm.val()).toBe('');
876+
});
877+
878+
it('should reset input val if cancelUpdate called during debounce', inject(function($timeout) {
879+
compileInput(
880+
'<input type="text" ng-model="name" name="alias" '+
881+
'ng-model-options="{ debounce: 2000 }" />');
882+
scope.$digest();
883+
884+
changeInputValueTo('a');
885+
expect(inputElm.val()).toBe('a');
886+
scope.form.alias.$cancelUpdate();
887+
expect(inputElm.val()).toBe('');
888+
$timeout.flush(3000);
889+
expect(inputElm.val()).toBe('');
890+
}));
891+
866892
});
867893

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

0 commit comments

Comments
 (0)