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

fix(ngModelOptions): preserve context of getter/setters #10136

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
49 changes: 25 additions & 24 deletions src/ng/directive/input.js
Original file line number Diff line number Diff line change
Expand Up @@ -1751,32 +1751,33 @@ var NgModelController = ['$scope', '$exceptionHandler', '$attrs', '$element', '$


var parsedNgModel = $parse($attr.ngModel),
parsedNgModelAssign = parsedNgModel.assign,
ngModelGet = parsedNgModel,
ngModelSet = parsedNgModelAssign,
pendingDebounce = null,
ctrl = this;

var ngModelGet = function ngModelGet() {
var modelValue = parsedNgModel($scope);
if (ctrl.$options && ctrl.$options.getterSetter && isFunction(modelValue)) {
modelValue = modelValue();
}
return modelValue;
};

var ngModelSet = function ngModelSet(newValue) {
var getterSetter;
if (ctrl.$options && ctrl.$options.getterSetter &&
isFunction(getterSetter = parsedNgModel($scope))) {

getterSetter(ctrl.$modelValue);
} else {
parsedNgModel.assign($scope, ctrl.$modelValue);
}
};

this.$$setOptions = function(options) {
ctrl.$options = options;

if (!parsedNgModel.assign && (!options || !options.getterSetter)) {
if (options && options.getterSetter) {
var invokeModelGetter = $parse($attr.ngModel + '()'),
invokeModelSetter = $parse($attr.ngModel + '($$$p)');

ngModelGet = function($scope) {
var modelValue = parsedNgModel($scope);
if (isFunction(modelValue)) {
modelValue = invokeModelGetter($scope);
}
return modelValue;
};
ngModelSet = function($scope, newValue) {
if (isFunction(parsedNgModel($scope))) {
invokeModelSetter($scope, {$$$p: ctrl.$modelValue});
} else {
parsedNgModelAssign($scope, ctrl.$modelValue);
}
};
} else if (!parsedNgModel.assign) {
throw $ngModelMinErr('nonassign', "Expression '{0}' is non-assignable. Element: {1}",
$attr.ngModel, startingTag($element));
}
Expand Down Expand Up @@ -2189,7 +2190,7 @@ var NgModelController = ['$scope', '$exceptionHandler', '$attrs', '$element', '$
}
if (isNumber(ctrl.$modelValue) && isNaN(ctrl.$modelValue)) {
// ctrl.$modelValue has not been touched yet...
ctrl.$modelValue = ngModelGet();
ctrl.$modelValue = ngModelGet($scope);
}
var prevModelValue = ctrl.$modelValue;
var allowInvalid = ctrl.$options && ctrl.$options.allowInvalid;
Expand Down Expand Up @@ -2217,7 +2218,7 @@ var NgModelController = ['$scope', '$exceptionHandler', '$attrs', '$element', '$
};

this.$$writeModelToScope = function() {
ngModelSet(ctrl.$modelValue);
ngModelSet($scope, ctrl.$modelValue);
forEach(ctrl.$viewChangeListeners, function(listener) {
try {
listener();
Expand Down Expand Up @@ -2313,7 +2314,7 @@ var NgModelController = ['$scope', '$exceptionHandler', '$attrs', '$element', '$
// ng-change executes in apply phase
// 4. view should be changed back to 'a'
$scope.$watch(function ngModelWatch() {
var modelValue = ngModelGet();
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?
Expand Down
37 changes: 36 additions & 1 deletion test/ng/directive/inputSpec.js
Original file line number Diff line number Diff line change
Expand Up @@ -2102,7 +2102,7 @@ describe('input', function() {
expect(inputElm.val()).toBe('a');
});

it('should always try to invoke a model if getterSetter is true', function() {
it('should try to invoke a function model if getterSetter is true', function() {
compileInput(
'<input type="text" ng-model="name" ' +
'ng-model-options="{ getterSetter: true }" />');
Expand All @@ -2117,6 +2117,12 @@ describe('input', function() {
expect(inputElm.val()).toBe('b');
expect(spy).toHaveBeenCalledWith('a');
expect(scope.name).toBe(spy);
});

it('should assign to non-function models if getterSetter is true', function() {
compileInput(
'<input type="text" ng-model="name" ' +
'ng-model-options="{ getterSetter: true }" />');

scope.name = 'c';
changeInputValueTo('d');
Expand All @@ -2136,6 +2142,35 @@ describe('input', function() {
'ng-model-options="{ getterSetter: true }" />');
});

it('should invoke a model in the correct context if getterSetter is true', function() {
compileInput(
'<input type="text" ng-model="someService.getterSetter" ' +
'ng-model-options="{ getterSetter: true }" />');

scope.someService = {
value: 'a',
getterSetter: function(newValue) {
this.value = newValue || this.value;
return this.value;
}
};
spyOn(scope.someService, 'getterSetter').andCallThrough();
scope.$apply();

expect(inputElm.val()).toBe('a');
expect(scope.someService.getterSetter).toHaveBeenCalledWith();
expect(scope.someService.value).toBe('a');

changeInputValueTo('b');
expect(scope.someService.getterSetter).toHaveBeenCalledWith('b');
expect(scope.someService.value).toBe('b');

scope.someService.value = 'c';
scope.$apply();
expect(inputElm.val()).toBe('c');
expect(scope.someService.getterSetter).toHaveBeenCalledWith();
});

it('should assign invalid values to the scope if allowInvalid is true', function() {
compileInput('<input type="text" name="input" ng-model="value" maxlength="1" ' +
'ng-model-options="{allowInvalid: true}" />');
Expand Down