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

Commit c0747dd

Browse files
committed
feat(ngModelOptions): add getterSetterContext to specify context for getter/setter bindings
Along with getterSetter, allow users to provide an expression via the getterSetterContext option. This expression is evaluated to determine the context that should be used when invoking the ngModel as a getter/setter function. For example, <input ng-model="someObject.value" ng-model-options="{ getterSetter: true }"> would previously invoke 'someObject.value()' from the global context. Now, users can specify context, like ng-model-options="{ getterSetter: true, getterSetterContext: 'someObject'}", which would invoke 'someObject.value()' using 'someObject' as the calling context. If getterSetterContext is not provided, fallback to using the current scope as the context. 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 c0747dd

File tree

2 files changed

+85
-2
lines changed

2 files changed

+85
-2
lines changed

src/ng/directive/input.js

+8-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,11 @@ var NgModelController = ['$scope', '$exceptionHandler', '$attrs', '$element', '$
17411742
this.$$setOptions = function(options) {
17421743
ctrl.$options = options;
17431744

1745+
if (ctrl.$options && ctrl.$options.getterSetter && ctrl.$options.getterSetterContext) {
1746+
// Use the provided context expression to specify the context used when invoking the
1747+
// getter/setter function
1748+
parsedNgModelContext = $parse(ctrl.$options.getterSetterContext);
1749+
}
17441750
if (!parsedNgModel.assign && (!options || !options.getterSetter)) {
17451751
throw $ngModelMinErr('nonassign', "Expression '{0}' is non-assignable. Element: {1}",
17461752
$attr.ngModel, startingTag($element));

test/ng/directive/inputSpec.js

+77
Original file line numberDiff line numberDiff line change
@@ -1975,6 +1975,83 @@ 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 getterSetterContext is not provided', function() {
1979+
scope.value = 'scopeContext';
1980+
compileInput(
1981+
'<input type="text" ng-model="someService.getterSetter" '+
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+
};
1991+
spyOn(scope.someService, 'getterSetter').andCallThrough();
1992+
scope.$apply();
1993+
expect(inputElm.val()).toBe('scopeContext');
1994+
expect(scope.someService.getterSetter).toHaveBeenCalledWith();
1995+
expect(scope.someService.value).toBe('b'); // 'this' is not bound to the service w/o ngModelContext
1996+
expect(scope.value).toBe('scopeContext');
1997+
1998+
changeInputValueTo('a');
1999+
expect(scope.someService.getterSetter).toHaveBeenCalledWith('a');
2000+
expect(scope.someService.value).toBe('b');
2001+
expect(scope.value).toBe('a');
2002+
2003+
scope.someService.value = 'c';
2004+
scope.$apply();
2005+
expect(inputElm.val()).toBe('a');
2006+
expect(scope.someService.getterSetter).toHaveBeenCalledWith();
2007+
expect(scope.someService.value).toBe('c');
2008+
expect(scope.value).toBe('a');
2009+
2010+
scope.value = 'd';
2011+
scope.$apply();
2012+
expect(inputElm.val()).toBe('d');
2013+
expect(scope.someService.getterSetter).toHaveBeenCalledWith();
2014+
expect(scope.someService.value).toBe('c');
2015+
expect(scope.value).toBe('d');
2016+
});
2017+
2018+
it('should try to invoke a model with the provided context if getterSetter is true and getterSetterContext is an expression', function() {
2019+
compileInput(
2020+
'<input type="text" ng-model="someService.getterSetter" '+
2021+
'ng-model-options="{ getterSetter: true, getterSetterContext: \'someService\' }" />');
2022+
2023+
scope.someService = {
2024+
value: 'b',
2025+
getterSetter: function(newValue) {
2026+
this.value = newValue || this.value;
2027+
return this.value;
2028+
}
2029+
};
2030+
spyOn(scope.someService, 'getterSetter').andCallThrough();
2031+
scope.$apply();
2032+
expect(inputElm.val()).toBe('b');
2033+
expect(scope.someService.getterSetter).toHaveBeenCalledWith();
2034+
expect(scope.someService.value).toBe('b');
2035+
2036+
changeInputValueTo('a');
2037+
expect(scope.someService.getterSetter).toHaveBeenCalledWith('a');
2038+
expect(scope.someService.value).toBe('a');
2039+
2040+
scope.someService.value = 'c';
2041+
scope.$apply();
2042+
expect(inputElm.val()).toBe('c');
2043+
expect(scope.someService.getterSetter).toHaveBeenCalledWith();
2044+
expect(scope.someService.value).toBe('c');
2045+
});
2046+
2047+
it('should fail to parse if getterSetterContext is an invalid expression', function() {
2048+
expect(function() {
2049+
compileInput(
2050+
'<input type="text" ng-model="someService.getterSetter" '+
2051+
'ng-model-options="{ getterSetter: true, getterSetterContext: \'throw error\' }" />');
2052+
}).toThrowMinErr("$parse", "syntax", "Syntax Error: Token 'error' is an unexpected token at column 7 of the expression [throw error] starting at [error].");
2053+
});
2054+
19782055
it('should assign invalid values to the scope if allowInvalid is true', function() {
19792056
compileInput('<input type="text" name="input" ng-model="value" maxlength="1" ' +
19802057
'ng-model-options="{allowInvalid: true}" />');

0 commit comments

Comments
 (0)