diff --git a/src/ng/directive/ngModel.js b/src/ng/directive/ngModel.js index aa3a6f532141..116d7b11e286 100644 --- a/src/ng/directive/ngModel.js +++ b/src/ng/directive/ngModel.js @@ -709,7 +709,7 @@ var NgModelController = ['$scope', '$exceptionHandler', '$attrs', '$element', '$ }; this.$$writeModelToScope = function() { - ngModelSet($scope, ctrl.$modelValue); + ngModelSet($scope, modelValueGetter(ctrl.$modelValue)); forEach(ctrl.$viewChangeListeners, function(listener) { try { listener(); @@ -806,43 +806,80 @@ var NgModelController = ['$scope', '$exceptionHandler', '$attrs', '$element', '$ } }; - // model -> value - // Note: we cannot use a normal scope.$watch as we want to detect the following: - // 1. scope value is 'a' - // 2. user enters 'b' - // 3. ng-change kicks in and reverts scope value to 'a' - // -> scope value did not change since the last digest as - // ng-change executes in apply phase - // 4. view should be changed back to 'a' - $scope.$watch(function ngModelWatch() { + this.$$setupModelWatch = function() { + // model -> value + // Note: we cannot use a normal scope.$watch as we want to detect the following: + // 1. scope value is 'a' + // 2. user enters 'b' + // 3. ng-change kicks in and reverts scope value to 'a' + // -> scope value did not change since the last digest as + // ng-change executes in apply phase + // 4. view should be changed back to 'a' + + // options.deepWatch + // options.collection + + setModelValueHelpers(); + $scope.$watch(ngModelWatch); + }; + + var modelValueGetter = function modelValueGetter(modelValue) { + return modelValue; + }; + + var modelValueChanged = function modelValueChanged(newModelValue, currentModelValue) { + return newModelValue !== currentModelValue; + }; + + /** + * If ngModelOptions deepWatch is true, then the model must be copied after every view / scope + * change, so we can correctly detect changes to it with .equals(). Otherwise, the ctrl.$modelValue + * and the scope value are the same, and we cannot detect differences to them properly + */ + function setModelValueHelpers() { + if (ctrl.$options && ctrl.$options.deepWatch) { + modelValueGetter = function modelValueGetter(modelValue) { + return copy(modelValue); + }; + + modelValueChanged = function modelValueChanged(newModelValue, currentModelValue) { + return !equals(newModelValue, currentModelValue); + }; + } + } + + function ngModelWatch() { var modelValue = ngModelGet($scope); // if scope model value and ngModel value are out of sync - // TODO(perf): why not move this to the action fn? - if (modelValue !== ctrl.$modelValue && + if (modelValueChanged(modelValue, ctrl.$modelValue) && // checks for NaN is needed to allow setting the model to NaN when there's an asyncValidator (ctrl.$modelValue === ctrl.$modelValue || modelValue === modelValue) ) { - ctrl.$modelValue = ctrl.$$rawModelValue = modelValue; - parserValid = undefined; + modelToViewAction(modelValue); + } + return modelValue; + } - var formatters = ctrl.$formatters, - idx = formatters.length; + function modelToViewAction(modelValue) { + ctrl.$modelValue = ctrl.$$rawModelValue = modelValueGetter(modelValue); + parserValid = undefined; - var viewValue = modelValue; - while (idx--) { - viewValue = formatters[idx](viewValue); - } - if (ctrl.$viewValue !== viewValue) { - ctrl.$viewValue = ctrl.$$lastCommittedViewValue = viewValue; - ctrl.$render(); + var formatters = ctrl.$formatters, + idx = formatters.length; - ctrl.$$runValidators(modelValue, viewValue, noop); - } + var viewValue = modelValue; + while (idx--) { + viewValue = formatters[idx](viewValue); } + if (ctrl.$viewValue !== viewValue) { + ctrl.$viewValue = ctrl.$$lastCommittedViewValue = viewValue; + ctrl.$render(); + + ctrl.$$runValidators(modelValue, viewValue, noop); + } + } - return modelValue; - }); }]; @@ -1032,6 +1069,7 @@ var ngModelDirective = ['$rootScope', function($rootScope) { formCtrl = ctrls[1] || modelCtrl.$$parentForm; modelCtrl.$$setOptions(ctrls[2] && ctrls[2].$options); + modelCtrl.$$setupModelWatch(); // notify others, especially parent forms formCtrl.$addControl(modelCtrl); diff --git a/test/ng/directive/ngChangeSpec.js b/test/ng/directive/ngChangeSpec.js index fc4990e14425..2be942543ed2 100644 --- a/test/ng/directive/ngChangeSpec.js +++ b/test/ng/directive/ngChangeSpec.js @@ -58,4 +58,19 @@ describe('ngChange', function() { helper.changeInputValueTo('a'); expect(inputElm.val()).toBe('b'); }); + + + it('should set the view if the model is changed by ngChange', function() { + $rootScope.reset = function() { + $rootScope.value = 'a'; + }; + $rootScope.value = 'a'; + var input = helper.compileInput(''); + var inputController = input.controller('ngModel'); + + $rootScope.$digest(); + + helper.changeInputValueTo('b'); + expect(input.val()).toBe('a'); + }); }); diff --git a/test/ng/directive/ngModelSpec.js b/test/ng/directive/ngModelSpec.js index b45ea2e0c0e0..d26bd5958e49 100644 --- a/test/ng/directive/ngModelSpec.js +++ b/test/ng/directive/ngModelSpec.js @@ -30,6 +30,7 @@ describe('ngModel', function() { //Assign the mocked parentFormCtrl to the model controller ctrl.$$parentForm = parentFormCtrl; + ctrl.$$setupModelWatch(); })); @@ -1790,6 +1791,26 @@ describe('ngModelOptions attributes', function() { var helper, $rootScope, $compile, $timeout, $q; + beforeEach(module(function($compileProvider) { + $compileProvider.directive('formatObject', function() { + return { + require: 'ngModel', + link: function(scope, element, attrs, ngModelCtrl) { + ngModelCtrl.$formatters.push(function(value) { + return value.a + '-' + value.b; + }); + ngModelCtrl.$parsers.push(function(value) { + var split = value.split('-'); + return { + a: split[0], + b: split[1] + }; + }); + } + }; + }); + })); + beforeEach(function() { helper = getInputCompileHelper(this); }); @@ -2402,4 +2423,27 @@ describe('ngModelOptions attributes', function() { expect($rootScope.value).toBe('modelValue'); expect($rootScope.changed).toHaveBeenCalledOnce(); }); + + + it('should watch the model with object equality if deepWatch is true', function() { + $rootScope.value = { + a: 'alpha', + b: 'beta', + }; + + var input = helper.compileInput(''); + + expect(input.val()).toBe('alpha-beta'); + + helper.changeInputValueTo('alpha-omega'); + expect($rootScope.value).toEqual({ + a: 'alpha', + b: 'omega' + }); + + $rootScope.value.b = 'gamma'; + $rootScope.$digest(); + expect(input.val()).toBe('alpha-gamma'); + }); });