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

Commit 807d947

Browse files
committed
feat(ngModel): determine context for getter/setter bindings
When using a getter/setter function for ngModel, attempt to determine the appropriate context by parsing the ngModel expression. If the context cannot be found, fallback to using the current scope. For example, '<input ng-model="someObject.value" ng-model-options="{ getterSetter: true }">' will use 'someObject' as the calling context. Non-assignable ngModel expressions will always fallback to using the current scope as the context. For example, '<input ng-model="someObject.getValueFn()" ng-model-options="{ getterSetter: true }">' will invoke the result of 'someObject.getValueFn()' from the current scope. Closes #9394 BREAKING CHANGE: previously, getter/setter functions would always be called from the global context. This behaviour was unexpected by some users, as described in #9394, and is not particularly nice anyways. Applications that relied on this behaviour can use `$window` instead of `this` to access the global object... but they probably shouldn't be storing global state anyways!
1 parent e69c180 commit 807d947

File tree

2 files changed

+85
-2
lines changed

2 files changed

+85
-2
lines changed

src/ng/directive/input.js

+13-2
Original file line numberDiff line numberDiff line change
@@ -1716,13 +1716,14 @@ var NgModelController = ['$scope', '$exceptionHandler', '$attrs', '$element', '$
17161716

17171717

17181718
var parsedNgModel = $parse($attr.ngModel),
1719+
parsedNgModelContext = null,
17191720
pendingDebounce = null,
17201721
ctrl = this;
17211722

17221723
var ngModelGet = function ngModelGet() {
17231724
var modelValue = parsedNgModel($scope);
17241725
if (ctrl.$options && ctrl.$options.getterSetter && isFunction(modelValue)) {
1725-
modelValue = modelValue();
1726+
modelValue = modelValue.call(parsedNgModelContext ? parsedNgModelContext($scope) : $scope);
17261727
}
17271728
return modelValue;
17281729
};
@@ -1732,7 +1733,7 @@ var NgModelController = ['$scope', '$exceptionHandler', '$attrs', '$element', '$
17321733
if (ctrl.$options && ctrl.$options.getterSetter &&
17331734
isFunction(getterSetter = parsedNgModel($scope))) {
17341735

1735-
getterSetter(ctrl.$modelValue);
1736+
getterSetter.call(parsedNgModelContext ? parsedNgModelContext($scope) : $scope, ctrl.$modelValue);
17361737
} else {
17371738
parsedNgModel.assign($scope, ctrl.$modelValue);
17381739
}
@@ -1741,6 +1742,16 @@ var NgModelController = ['$scope', '$exceptionHandler', '$attrs', '$element', '$
17411742
this.$$setOptions = function(options) {
17421743
ctrl.$options = options;
17431744

1745+
if (ctrl.$options && ctrl.$options.getterSetter && parsedNgModel && parsedNgModel.assign) {
1746+
// Match everything before the last dot as the context. For example, if ngModel is
1747+
// 'someObject.getter', use 'someObject' as the context. This is safe, since we require
1748+
// ngModel to be assignable; therefore, it's impossible to have dots used for anything but
1749+
// an operator (e.g. 'someObject.getter("string.with.dots")' is non-assignable)
1750+
var match = $attr.ngModel.match(/(.+)\./);
1751+
if (match && match[1]) {
1752+
parsedNgModelContext = $parse(match[1]);
1753+
}
1754+
}
17441755
if (!parsedNgModel.assign && (!options || !options.getterSetter)) {
17451756
throw $ngModelMinErr('nonassign', "Expression '{0}' is non-assignable. Element: {1}",
17461757
$attr.ngModel, startingTag($element));

test/ng/directive/inputSpec.js

+72
Original file line numberDiff line numberDiff line change
@@ -1975,6 +1975,78 @@ describe('input', function() {
19751975
'ng-model-options="{ getterSetter: true }" />');
19761976
});
19771977

1978+
it('should try to invoke a model with default context if getterSetter is true and ngModel is non-assignable', function() {
1979+
scope.value = 'scopeContext';
1980+
compileInput(
1981+
'<input type="text" ng-model="someService.getGetterSetterFn()" '+
1982+
'ng-model-options="{ getterSetter: true }" />');
1983+
1984+
scope.someService = {
1985+
value: 'b',
1986+
getterSetter: function(newValue) {
1987+
this.value = newValue || this.value;
1988+
return this.value;
1989+
},
1990+
getGetterSetterFn: function() {
1991+
return this.getterSetter;
1992+
}
1993+
};
1994+
spyOn(scope.someService, 'getterSetter').andCallThrough();
1995+
scope.$apply();
1996+
expect(inputElm.val()).toBe('scopeContext');
1997+
expect(scope.someService.getterSetter).toHaveBeenCalledWith();
1998+
expect(scope.someService.value).toBe('b'); // 'this' is not bound to the service w/o ngModelContext
1999+
expect(scope.value).toBe('scopeContext');
2000+
2001+
changeInputValueTo('a');
2002+
expect(scope.someService.getterSetter).toHaveBeenCalledWith('a');
2003+
expect(scope.someService.value).toBe('b');
2004+
expect(scope.value).toBe('a');
2005+
2006+
scope.someService.value = 'c';
2007+
scope.$apply();
2008+
expect(inputElm.val()).toBe('a');
2009+
expect(scope.someService.getterSetter).toHaveBeenCalledWith();
2010+
expect(scope.someService.value).toBe('c');
2011+
expect(scope.value).toBe('a');
2012+
2013+
scope.value = 'd';
2014+
scope.$apply();
2015+
expect(inputElm.val()).toBe('d');
2016+
expect(scope.someService.getterSetter).toHaveBeenCalledWith();
2017+
expect(scope.someService.value).toBe('c');
2018+
expect(scope.value).toBe('d');
2019+
});
2020+
2021+
it('should try to invoke a model with the appropriate context if getterSetter is true and ngModel is assignable', function() {
2022+
compileInput(
2023+
'<input type="text" ng-model="someService.getterSetter" '+
2024+
'ng-model-options="{ getterSetter: true }" />');
2025+
2026+
scope.someService = {
2027+
value: 'b',
2028+
getterSetter: function(newValue) {
2029+
this.value = newValue || this.value;
2030+
return this.value;
2031+
}
2032+
};
2033+
spyOn(scope.someService, 'getterSetter').andCallThrough();
2034+
scope.$apply();
2035+
expect(inputElm.val()).toBe('b');
2036+
expect(scope.someService.getterSetter).toHaveBeenCalledWith();
2037+
expect(scope.someService.value).toBe('b');
2038+
2039+
changeInputValueTo('a');
2040+
expect(scope.someService.getterSetter).toHaveBeenCalledWith('a');
2041+
expect(scope.someService.value).toBe('a');
2042+
2043+
scope.someService.value = 'c';
2044+
scope.$apply();
2045+
expect(inputElm.val()).toBe('c');
2046+
expect(scope.someService.getterSetter).toHaveBeenCalledWith();
2047+
expect(scope.someService.value).toBe('c');
2048+
});
2049+
19782050
it('should assign invalid values to the scope if allowInvalid is true', function() {
19792051
compileInput('<input type="text" name="input" ng-model="value" maxlength="1" ' +
19802052
'ng-model-options="{allowInvalid: true}" />');

0 commit comments

Comments
 (0)