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

Commit 9ad7d74

Browse files
committed
refactor(ngModel): remove $$invalidModelValue and refactor methods
- define `ngModelGet` and `ngModelSet` to already use the getter/setter semantics, so the rest of the code does not need to care about it. - remove `ctrl.$$invalidModelValue` to simplify the internal logic
1 parent 90cd1e0 commit 9ad7d74

File tree

2 files changed

+86
-42
lines changed

2 files changed

+86
-42
lines changed

src/ng/directive/input.js

+46-36
Original file line numberDiff line numberDiff line change
@@ -1651,15 +1651,33 @@ var NgModelController = ['$scope', '$exceptionHandler', '$attrs', '$element', '$
16511651
this.$name = $attr.name;
16521652

16531653

1654-
var ngModelGet = $parse($attr.ngModel),
1655-
ngModelSet = ngModelGet.assign,
1654+
var parsedNgModel = $parse($attr.ngModel),
16561655
pendingDebounce = null,
16571656
ctrl = this;
16581657

1658+
var ngModelGet = function ngModelGet() {
1659+
var modelValue = parsedNgModel($scope);
1660+
if (ctrl.$options && ctrl.$options.getterSetter && isFunction(modelValue)) {
1661+
modelValue = modelValue();
1662+
}
1663+
return modelValue;
1664+
};
1665+
1666+
var ngModelSet = function ngModelSet(newValue) {
1667+
var getterSetter;
1668+
if (ctrl.$options && ctrl.$options.getterSetter &&
1669+
isFunction(getterSetter = parsedNgModel($scope))) {
1670+
1671+
getterSetter(ctrl.$modelValue);
1672+
} else {
1673+
parsedNgModel.assign($scope, ctrl.$modelValue);
1674+
}
1675+
};
1676+
16591677
this.$$setOptions = function(options) {
16601678
ctrl.$options = options;
16611679

1662-
if (!ngModelSet && (!options || !options.getterSetter)) {
1680+
if (!parsedNgModel.assign && (!options || !options.getterSetter)) {
16631681
throw $ngModelMinErr('nonassign', "Expression '{0}' is non-assignable. Element: {1}",
16641682
$attr.ngModel, startingTag($element));
16651683
}
@@ -1875,26 +1893,17 @@ var NgModelController = ['$scope', '$exceptionHandler', '$attrs', '$element', '$
18751893
* Runs each of the registered validators (first synchronous validators and then asynchronous validators).
18761894
*/
18771895
this.$validate = function() {
1878-
// ignore $validate before model initialized
1879-
if (ctrl.$modelValue !== ctrl.$modelValue) {
1896+
// ignore $validate before model is initialized
1897+
if (isNumber(ctrl.$modelValue) && isNaN(ctrl.$modelValue)) {
18801898
return;
18811899
}
1882-
1883-
var prev = ctrl.$modelValue;
1884-
ctrl.$$runValidators(undefined, ctrl.$$invalidModelValue || ctrl.$modelValue, ctrl.$viewValue, function() {
1885-
if (prev !== ctrl.$modelValue) {
1886-
ctrl.$$writeModelToScope();
1887-
}
1888-
});
1900+
this.$$parseAndValidate();
18891901
};
18901902

18911903
this.$$runValidators = function(parseValid, modelValue, viewValue, doneCallback) {
18921904
currentValidationRunId++;
18931905
var localValidationRunId = currentValidationRunId;
18941906

1895-
// We can update the $$invalidModelValue immediately as we don't have to wait for validators!
1896-
ctrl.$$invalidModelValue = modelValue;
1897-
18981907
// check parser error
18991908
if (!processParseErrors(parseValid)) {
19001909
return;
@@ -1971,8 +1980,6 @@ var NgModelController = ['$scope', '$exceptionHandler', '$attrs', '$element', '$
19711980

19721981
function validationDone() {
19731982
if (localValidationRunId === currentValidationRunId) {
1974-
// set the validated model value
1975-
ctrl.$modelValue = ctrl.$valid ? modelValue : undefined;
19761983

19771984
doneCallback();
19781985
}
@@ -2011,31 +2018,31 @@ var NgModelController = ['$scope', '$exceptionHandler', '$attrs', '$element', '$
20112018
$animate.addClass($element, DIRTY_CLASS);
20122019
parentForm.$setDirty();
20132020
}
2021+
this.$$parseAndValidate();
2022+
};
20142023

2015-
var parserValid = true, modelValue = viewValue;
2024+
this.$$parseAndValidate = function() {
2025+
var parserValid = true,
2026+
viewValue = ctrl.$$lastCommittedViewValue,
2027+
modelValue = viewValue;
20162028
for(var i = 0; i < ctrl.$parsers.length; i++) {
20172029
modelValue = ctrl.$parsers[i](modelValue);
20182030
if (isUndefined(modelValue)) {
20192031
parserValid = false;
20202032
break;
20212033
}
20222034
}
2023-
2035+
var prevModelValue = ctrl.$modelValue;
20242036
ctrl.$$runValidators(parserValid, modelValue, viewValue, function() {
2025-
ctrl.$$writeModelToScope();
2037+
ctrl.$modelValue = ctrl.$valid ? modelValue : undefined;
2038+
if (ctrl.$modelValue !== prevModelValue) {
2039+
ctrl.$$writeModelToScope();
2040+
}
20262041
});
20272042
};
20282043

20292044
this.$$writeModelToScope = function() {
2030-
var getterSetter;
2031-
2032-
if (ctrl.$options && ctrl.$options.getterSetter &&
2033-
isFunction(getterSetter = ngModelGet($scope))) {
2034-
2035-
getterSetter(ctrl.$modelValue);
2036-
} else {
2037-
ngModelSet($scope, ctrl.$modelValue);
2038-
}
2045+
ngModelSet(ctrl.$modelValue);
20392046
forEach(ctrl.$viewChangeListeners, function(listener) {
20402047
try {
20412048
listener();
@@ -2123,17 +2130,20 @@ var NgModelController = ['$scope', '$exceptionHandler', '$attrs', '$element', '$
21232130
};
21242131

21252132
// model -> value
2133+
// Note: we cannot use a normal scope.$watch as we want to detect the following:
2134+
// 1. scope value is 'a'
2135+
// 2. user enters 'b'
2136+
// 3. ng-change kicks in and reverts scope value to 'a'
2137+
// -> scope value did not change since the last digest as
2138+
// ng-change executes in apply phase
2139+
// 4. view should be changed back to 'a'
21262140
$scope.$watch(function ngModelWatch() {
2127-
var modelValue = ngModelGet($scope);
2128-
2129-
if (ctrl.$options && ctrl.$options.getterSetter && isFunction(modelValue)) {
2130-
modelValue = modelValue();
2131-
}
2141+
var modelValue = ngModelGet();
21322142

21332143
// if scope model value and ngModel value are out of sync
21342144
// TODO(perf): why not move this to the action fn?
2135-
if (ctrl.$modelValue !== modelValue &&
2136-
(isUndefined(ctrl.$$invalidModelValue) || ctrl.$$invalidModelValue != modelValue)) {
2145+
if (modelValue !== ctrl.$modelValue) {
2146+
ctrl.$modelValue = modelValue;
21372147

21382148
var formatters = ctrl.$formatters,
21392149
idx = formatters.length;

test/ng/directive/inputSpec.js

+40-6
Original file line numberDiff line numberDiff line change
@@ -214,6 +214,33 @@ describe('NgModelController', function() {
214214
expect(ctrl.$modelValue).toBeUndefined();
215215
});
216216

217+
it('should not reset the view when the view is invalid', function() {
218+
// this test fails when the view changes the model and
219+
// then the model listener in ngModel picks up the change and
220+
// tries to update the view again.
221+
222+
// add a validator that will make any input invalid
223+
ctrl.$parsers.push(function() {return undefined;});
224+
spyOn(ctrl, '$render');
225+
226+
// first digest
227+
ctrl.$setViewValue('bbbb');
228+
expect(ctrl.$modelValue).toBeUndefined();
229+
expect(ctrl.$viewValue).toBe('bbbb');
230+
expect(ctrl.$render).not.toHaveBeenCalled();
231+
expect(scope.value).toBeUndefined();
232+
233+
// further digests
234+
scope.$apply('value = "aaa"');
235+
expect(ctrl.$viewValue).toBe('aaa');
236+
ctrl.$render.reset();
237+
238+
ctrl.$setViewValue('cccc');
239+
expect(ctrl.$modelValue).toBeUndefined();
240+
expect(ctrl.$viewValue).toBe('cccc');
241+
expect(ctrl.$render).not.toHaveBeenCalled();
242+
expect(scope.value).toBeUndefined();
243+
});
217244

218245
it('should call parentForm.$setDirty only when pristine', function() {
219246
ctrl.$setViewValue('');
@@ -385,18 +412,18 @@ describe('NgModelController', function() {
385412
describe('validations pipeline', function() {
386413

387414
it('should perform validations when $validate() is called', function() {
388-
ctrl.$validators.uppercase = function(value) {
389-
return (/^[A-Z]+$/).test(value);
415+
scope.$apply('value = ""');
416+
417+
var validatorResult = false;
418+
ctrl.$validators.someValidator = function(value) {
419+
return validatorResult;
390420
};
391421

392-
ctrl.$modelValue = 'test';
393-
ctrl.$$invalidModelValue = undefined;
394422
ctrl.$validate();
395423

396424
expect(ctrl.$valid).toBe(false);
397425

398-
ctrl.$modelValue = 'TEST';
399-
ctrl.$$invalidModelValue = undefined;
426+
validatorResult = true;
400427
ctrl.$validate();
401428

402429
expect(ctrl.$valid).toBe(true);
@@ -3936,6 +3963,13 @@ describe('input', function() {
39363963
browserTrigger(inputElm, 'click');
39373964
expect(scope.changeFn).toHaveBeenCalledOnce();
39383965
});
3966+
3967+
it('should be able to change the model and via that also update the view', function() {
3968+
compileInput('<input type="text" ng-model="value" ng-change="value=\'b\'" />');
3969+
3970+
changeInputValueTo('a');
3971+
expect(inputElm.val()).toBe('b');
3972+
});
39393973
});
39403974

39413975

0 commit comments

Comments
 (0)