-
Notifications
You must be signed in to change notification settings - Fork 27.5k
fix(ngModelOptions): introduce $cancelUpdate to cancel pending updates #7014
Changes from 1 commit
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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}", | ||
|
@@ -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(); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Both will work. When do you prefer There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I like the idea of using
What do you think? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Sure, I'll add this to this PR as a separate commit. |
||
}; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I love it when functions are this simple! It just makes the world seem right. |
||
|
||
// update the view value | ||
|
@@ -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) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Good spot on the parenthesis styling |
||
pendingDebounce = $timeout(function() { | ||
pendingDebounce = null; | ||
that.$$realSetViewValue(value); | ||
ctrl.$$realSetViewValue(value); | ||
}, debounceDelay); | ||
} else { | ||
that.$$realSetViewValue(value); | ||
this.$$realSetViewValue(value); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. should be There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why? It is in the context of BTW, I was wondering - what do you think about using There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If someone calls I think using ctrl everywhere is a good idea for general safety and consistency, although I suspect that a good minifier might swap out There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Uglify doesn't swap |
||
} | ||
}; | ||
|
||
// model -> value | ||
var ctrl = this; | ||
|
||
$scope.$watch(function ngModelWatch() { | ||
var value = ngModelGet($scope); | ||
|
||
|
@@ -2293,4 +2297,4 @@ var ngModelOptionsDirective = function() { | |
} | ||
}] | ||
}; | ||
}; | ||
}; |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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(''); | ||
}); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Magic! I guess that this fails before your fix? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It does. |
||
|
||
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(''); | ||
})); | ||
|
||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We need another unit test that works over the second case, where there is no debounce but an event that has not yet occurred before the programmatic reset occurs. |
||
}); | ||
|
||
it('should allow complex reference binding', function() { | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think this description would benefit from initially layout out the problem that this method solves. For instance:
Then you can go on with how to use this method, as you have described already.