From 7db9943e380c8520274d1cfdb967035245ddde15 Mon Sep 17 00:00:00 2001 From: Martin Staffa Date: Wed, 20 Sep 2017 13:28:43 +0200 Subject: [PATCH] feat(ngModel): expose function to run model -> view pipeline Closes #3407 Closes #10764 --- src/ng/directive/ngModel.js | 49 ++++++++++++++++------------- test/ng/directive/ngModelSpec.js | 53 ++++++++++++++++++++++++++++++++ 2 files changed, 80 insertions(+), 22 deletions(-) diff --git a/src/ng/directive/ngModel.js b/src/ng/directive/ngModel.js index cdba3c11de28..86eaebbc241e 100644 --- a/src/ng/directive/ngModel.js +++ b/src/ng/directive/ngModel.js @@ -878,6 +878,27 @@ NgModelController.prototype = { */ $overrideModelOptions: function(options) { this.$options = this.$options.createChild(options); + }, + + $setModelValue: function(modelValue) { + this.$modelValue = this.$$rawModelValue = modelValue; + this.$$parserValid = undefined; + + var formatters = this.$formatters, + idx = formatters.length; + + var viewValue = modelValue; + while (idx--) { + viewValue = formatters[idx](viewValue); + } + if (this.$viewValue !== viewValue) { + this.$$updateEmptyClasses(viewValue); + this.$viewValue = this.$$lastCommittedViewValue = viewValue; + this.$render(); + + // It is possible that model and view value have been updated during render + this.$$runValidators(this.$modelValue, this.$viewValue, noop); + } } }; @@ -894,30 +915,14 @@ function setupModelWatcher(ctrl) { var modelValue = ctrl.$$ngModelGet(scope); // if scope model value and ngModel value are out of sync - // TODO(perf): why not move this to the action fn? + // This cannot be moved to the action function, because it would not catch the + // case where the model is changed in the ngChange function or the model setter if (modelValue !== ctrl.$modelValue && - // checks for NaN is needed to allow setting the model to NaN when there's an asyncValidator - // eslint-disable-next-line no-self-compare - (ctrl.$modelValue === ctrl.$modelValue || modelValue === modelValue) + // checks for NaN is needed to allow setting the model to NaN when there's an asyncValidator + // eslint-disable-next-line no-self-compare + (ctrl.$modelValue === ctrl.$modelValue || modelValue === modelValue) ) { - ctrl.$modelValue = ctrl.$$rawModelValue = modelValue; - ctrl.$$parserValid = undefined; - - var formatters = ctrl.$formatters, - idx = formatters.length; - - var viewValue = modelValue; - while (idx--) { - viewValue = formatters[idx](viewValue); - } - if (ctrl.$viewValue !== viewValue) { - ctrl.$$updateEmptyClasses(viewValue); - ctrl.$viewValue = ctrl.$$lastCommittedViewValue = viewValue; - ctrl.$render(); - - // It is possible that model and view value have been updated during render - ctrl.$$runValidators(ctrl.$modelValue, ctrl.$viewValue, noop); - } + ctrl.$setModelValue(modelValue); } return modelValue; diff --git a/test/ng/directive/ngModelSpec.js b/test/ng/directive/ngModelSpec.js index 7157d4aded87..371ce91be6bf 100644 --- a/test/ng/directive/ngModelSpec.js +++ b/test/ng/directive/ngModelSpec.js @@ -603,6 +603,59 @@ describe('ngModel', function() { expect(ctrl.$modelValue).toBeNaN(); })); + + describe('$setModelValue', function() { + it('should run the model -> view pipeline', function() { + var log = []; + + ctrl.$formatters.unshift(function(value) { + log.push(value); + return value + 2; + }); + + ctrl.$formatters.unshift(function(value) { + log.push(value); + return value + ''; + }); + + spyOn(ctrl, '$render'); + + ctrl.$setModelValue(3); + + expect(ctrl.$modelValue).toBe(3); + expect(log).toEqual([3, 5]); + expect(ctrl.$viewValue).toBe('5'); + expect(ctrl.$render).toHaveBeenCalledOnce(); + }); + + it('should run the model -> view pipeline even if the value has not changed', function() { + // this is analogue to $setViewValue + spyOn(ctrl, '$render'); + + ctrl.$setModelValue(3); + + expect(ctrl.$modelValue).toBe(3); + expect(ctrl.$render).toHaveBeenCalledOnce(); + + ctrl.$setModelValue(3); + expect(ctrl.$modelValue).toBe(3); + expect(ctrl.$render).toHaveBeenCalledOnce(); + }); + + it('should not modify the scope value', function() { + // this is analogue to $setViewValue, which does not modify the rendered (DOM) value + scope.$apply('value = 10'); + expect(ctrl.$modelValue).toBe(10); + expect(ctrl.$viewValue).toBe(10); + + ctrl.$setModelValue(3); + + expect(scope.value).toBe(10); + expect(ctrl.$modelValue).toBe(3); + expect(ctrl.$viewValue).toBe(3); + }); + + }); });