From 940fcb4090e96824a4abc50252aa36aaf239e937 Mon Sep 17 00:00:00 2001 From: Shahar Talmi Date: Sun, 6 Apr 2014 09:16:29 +0300 Subject: [PATCH] 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 --- src/ng/directive/input.js | 42 +++++++++++++++++++--------------- test/ng/directive/inputSpec.js | 40 ++++++++++++++++++++++++++------ 2 files changed, 56 insertions(+), 26 deletions(-) diff --git a/src/ng/directive/input.js b/src/ng/directive/input.js index 14bc4a03d3b6..7e6f338f62c6 100644 --- a/src/ng/directive/input.js +++ b/src/ng/directive/input.js @@ -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(); }; // 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) { 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); @@ -2293,4 +2297,4 @@ var ngModelOptionsDirective = function() { } }] }; -}; \ No newline at end of file +}; diff --git a/test/ng/directive/inputSpec.js b/test/ng/directive/inputSpec.js index 5046d4788fe5..389dd7bdede0 100644 --- a/test/ng/directive/inputSpec.js +++ b/test/ng/directive/inputSpec.js @@ -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( - '
'+ - ''+ - '
'); + ''); + 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( + ''); + 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( + ''); + 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() {