Skip to content
This repository was archived by the owner on Apr 12, 2024. It is now read-only.

Commit 0814a6d

Browse files
committed
feat(ngModel): support object equality watch via ngModelOptions
1 parent b34769e commit 0814a6d

File tree

2 files changed

+109
-27
lines changed

2 files changed

+109
-27
lines changed

src/ng/directive/ngModel.js

Lines changed: 65 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -709,7 +709,7 @@ var NgModelController = ['$scope', '$exceptionHandler', '$attrs', '$element', '$
709709
};
710710

711711
this.$$writeModelToScope = function() {
712-
ngModelSet($scope, ctrl.$modelValue);
712+
ngModelSet($scope, modelValueGetter(ctrl.$modelValue));
713713
forEach(ctrl.$viewChangeListeners, function(listener) {
714714
try {
715715
listener();
@@ -806,43 +806,80 @@ var NgModelController = ['$scope', '$exceptionHandler', '$attrs', '$element', '$
806806
}
807807
};
808808

809-
// model -> value
810-
// Note: we cannot use a normal scope.$watch as we want to detect the following:
811-
// 1. scope value is 'a'
812-
// 2. user enters 'b'
813-
// 3. ng-change kicks in and reverts scope value to 'a'
814-
// -> scope value did not change since the last digest as
815-
// ng-change executes in apply phase
816-
// 4. view should be changed back to 'a'
817-
$scope.$watch(function ngModelWatch() {
809+
this.$$setupModelWatch = function() {
810+
// model -> value
811+
// Note: we cannot use a normal scope.$watch as we want to detect the following:
812+
// 1. scope value is 'a'
813+
// 2. user enters 'b'
814+
// 3. ng-change kicks in and reverts scope value to 'a'
815+
// -> scope value did not change since the last digest as
816+
// ng-change executes in apply phase
817+
// 4. view should be changed back to 'a'
818+
819+
// options.deepWatch
820+
// options.collection
821+
822+
setModelValueHelpers();
823+
$scope.$watch(ngModelWatch);
824+
};
825+
826+
var modelValueGetter = function modelValueGetter(modelValue) {
827+
return modelValue;
828+
};
829+
830+
var modelValueChanged = function modelValueChanged(newModelValue, currentModelValue) {
831+
return newModelValue !== currentModelValue;
832+
};
833+
834+
/**
835+
* If ngModelOptions deepWatch is true, then the model must be copied after every view / scope
836+
* change, so we can correctly detect changes to it with .equals(). Otherwise, the ctrl.$modelValue
837+
* and the scope value are the same, and we cannot detect differences to them properly
838+
*/
839+
function setModelValueHelpers() {
840+
if (ctrl.$options && ctrl.$options.deepWatch) {
841+
modelValueGetter = function modelValueGetter(modelValue) {
842+
return copy(modelValue);
843+
};
844+
845+
modelValueChanged = function modelValueChanged(newModelValue, currentModelValue) {
846+
return !equals(newModelValue, currentModelValue);
847+
};
848+
}
849+
}
850+
851+
function ngModelWatch() {
818852
var modelValue = ngModelGet($scope);
819853

820854
// if scope model value and ngModel value are out of sync
821-
// TODO(perf): why not move this to the action fn?
822-
if (modelValue !== ctrl.$modelValue &&
855+
if (modelValueChanged(modelValue, ctrl.$modelValue) &&
823856
// checks for NaN is needed to allow setting the model to NaN when there's an asyncValidator
824857
(ctrl.$modelValue === ctrl.$modelValue || modelValue === modelValue)
825858
) {
826-
ctrl.$modelValue = ctrl.$$rawModelValue = modelValue;
827-
parserValid = undefined;
859+
modelToViewAction(modelValue);
860+
}
861+
return modelValue;
862+
}
828863

829-
var formatters = ctrl.$formatters,
830-
idx = formatters.length;
864+
function modelToViewAction(modelValue) {
865+
ctrl.$modelValue = ctrl.$$rawModelValue = modelValueGetter(modelValue);
866+
parserValid = undefined;
831867

832-
var viewValue = modelValue;
833-
while (idx--) {
834-
viewValue = formatters[idx](viewValue);
835-
}
836-
if (ctrl.$viewValue !== viewValue) {
837-
ctrl.$viewValue = ctrl.$$lastCommittedViewValue = viewValue;
838-
ctrl.$render();
868+
var formatters = ctrl.$formatters,
869+
idx = formatters.length;
839870

840-
ctrl.$$runValidators(modelValue, viewValue, noop);
841-
}
871+
var viewValue = modelValue;
872+
while (idx--) {
873+
viewValue = formatters[idx](viewValue);
842874
}
875+
if (ctrl.$viewValue !== viewValue) {
876+
ctrl.$viewValue = ctrl.$$lastCommittedViewValue = viewValue;
877+
ctrl.$render();
878+
879+
ctrl.$$runValidators(modelValue, viewValue, noop);
880+
}
881+
}
843882

844-
return modelValue;
845-
});
846883
}];
847884

848885

@@ -1032,6 +1069,7 @@ var ngModelDirective = ['$rootScope', function($rootScope) {
10321069
formCtrl = ctrls[1] || modelCtrl.$$parentForm;
10331070

10341071
modelCtrl.$$setOptions(ctrls[2] && ctrls[2].$options);
1072+
modelCtrl.$$setupModelWatch();
10351073

10361074
// notify others, especially parent forms
10371075
formCtrl.$addControl(modelCtrl);

test/ng/directive/ngModelSpec.js

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ describe('ngModel', function() {
3030

3131
//Assign the mocked parentFormCtrl to the model controller
3232
ctrl.$$parentForm = parentFormCtrl;
33+
ctrl.$$setupModelWatch();
3334
}));
3435

3536

@@ -1790,6 +1791,26 @@ describe('ngModelOptions attributes', function() {
17901791

17911792
var helper, $rootScope, $compile, $timeout, $q;
17921793

1794+
beforeEach(module(function($compileProvider) {
1795+
$compileProvider.directive('formatObject', function() {
1796+
return {
1797+
require: 'ngModel',
1798+
link: function(scope, element, attrs, ngModelCtrl) {
1799+
ngModelCtrl.$formatters.push(function(value) {
1800+
return value.a + '-' + value.b;
1801+
});
1802+
ngModelCtrl.$parsers.push(function(value) {
1803+
var split = value.split('-');
1804+
return {
1805+
a: split[0],
1806+
b: split[1]
1807+
};
1808+
});
1809+
}
1810+
};
1811+
});
1812+
}));
1813+
17931814
beforeEach(function() {
17941815
helper = getInputCompileHelper(this);
17951816
});
@@ -2402,4 +2423,27 @@ describe('ngModelOptions attributes', function() {
24022423
expect($rootScope.value).toBe('modelValue');
24032424
expect($rootScope.changed).toHaveBeenCalledOnce();
24042425
});
2426+
2427+
2428+
it('should watch the model with object equality if deepWatch is true', function() {
2429+
$rootScope.value = {
2430+
a: 'alpha',
2431+
b: 'beta',
2432+
};
2433+
2434+
var input = helper.compileInput('<input type="text" format-object ng-model="value" ' +
2435+
'ng-model-options="{deepWatch: true}" >');
2436+
2437+
expect(input.val()).toBe('alpha-beta');
2438+
2439+
helper.changeInputValueTo('alpha-omega');
2440+
expect($rootScope.value).toEqual({
2441+
a: 'alpha',
2442+
b: 'omega'
2443+
});
2444+
2445+
$rootScope.value.b = 'gamma';
2446+
$rootScope.$digest();
2447+
expect(input.val()).toBe('alpha-gamma');
2448+
});
24052449
});

0 commit comments

Comments
 (0)