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');
+ });
});