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

Commit 776472b

Browse files
committed
feat(ngModel): add ngModelContext for getter/setter bindings
Adds an optional attribute, `ng-model-context`, that will be evaluated to provide the calling context used when invoking the ngModel getter/setter function. When not provided, falls back to the existing behavior of invoking getter/setter functions from the global context. Closes #9394
1 parent e4eb382 commit 776472b

File tree

2 files changed

+91
-2
lines changed

2 files changed

+91
-2
lines changed

src/ng/directive/input.js

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

17171717

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

1723+
if ($attr.ngModelContext) {
1724+
parsedNgModelContext = $parse($attr.ngModelContext);
1725+
}
1726+
17221727
var ngModelGet = function ngModelGet() {
17231728
var modelValue = parsedNgModel($scope);
17241729
if (ctrl.$options && ctrl.$options.getterSetter && isFunction(modelValue)) {
1725-
modelValue = modelValue();
1730+
var context = parsedNgModelContext ? parsedNgModelContext($scope) : this;
1731+
modelValue = modelValue.call(context);
17261732
}
17271733
return modelValue;
17281734
};
@@ -1732,7 +1738,8 @@ var NgModelController = ['$scope', '$exceptionHandler', '$attrs', '$element', '$
17321738
if (ctrl.$options && ctrl.$options.getterSetter &&
17331739
isFunction(getterSetter = parsedNgModel($scope))) {
17341740

1735-
getterSetter(ctrl.$modelValue);
1741+
var context = parsedNgModelContext ? parsedNgModelContext($scope) : this;
1742+
getterSetter.call(context, ctrl.$modelValue);
17361743
} else {
17371744
parsedNgModel.assign($scope, ctrl.$modelValue);
17381745
}
@@ -1745,6 +1752,10 @@ var NgModelController = ['$scope', '$exceptionHandler', '$attrs', '$element', '$
17451752
throw $ngModelMinErr('nonassign', "Expression '{0}' is non-assignable. Element: {1}",
17461753
$attr.ngModel, startingTag($element));
17471754
}
1755+
if (parsedNgModelContext && !parsedNgModelContext.assign) {
1756+
throw $ngModelMinErr('nonassign', "Expression '{0}' is non-assignable. Element: {1}",
1757+
$attr.ngModelContext, startingTag($element));
1758+
}
17481759
};
17491760

17501761
/**

test/ng/directive/inputSpec.js

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

1978+
it('should try to invoke a model with global context if getterSetter is true and ngModelContext is not provided', inject(function($window) {
1979+
$window.value = 'global';
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('global');
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($window.value).toBe('global');
1997+
1998+
changeInputValueTo('a');
1999+
expect(scope.someService.getterSetter).toHaveBeenCalledWith('a');
2000+
expect(scope.someService.value).toBe('b');
2001+
expect($window.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($window.value).toBe('a');
2009+
2010+
$window.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($window.value).toBe('d');
2016+
}));
2017+
2018+
it('should try to invoke a model with the provided context if getterSetter is true and ngModelContext is provided', function() {
2019+
compileInput(
2020+
'<input type="text" ng-model="someService.getterSetter" '+
2021+
'ng-model-context="someService" '+
2022+
'ng-model-options="{ getterSetter: true }" />');
2023+
2024+
scope.someService = {
2025+
value: 'b',
2026+
getterSetter: function(newValue) {
2027+
this.value = newValue || this.value;
2028+
return this.value;
2029+
}
2030+
};
2031+
spyOn(scope.someService, 'getterSetter').andCallThrough();
2032+
scope.$apply();
2033+
expect(inputElm.val()).toBe('b');
2034+
expect(scope.someService.getterSetter).toHaveBeenCalledWith();
2035+
expect(scope.someService.value).toBe('b');
2036+
2037+
changeInputValueTo('a');
2038+
expect(scope.someService.getterSetter).toHaveBeenCalledWith('a');
2039+
expect(scope.someService.value).toBe('a');
2040+
2041+
scope.someService.value = 'c';
2042+
scope.$apply();
2043+
expect(inputElm.val()).toBe('c');
2044+
expect(scope.someService.getterSetter).toHaveBeenCalledWith();
2045+
expect(scope.someService.value).toBe('c');
2046+
});
2047+
2048+
it('should fail on non-assignable ngModelContext binding', function() {
2049+
expect(function() {
2050+
compileInput('<input type="text" ng-model="someService.getterSetter" '+
2051+
'ng-model-context="someService.getInstance()" ' +
2052+
'ng-model-options="{ getterSetter: true }" />');
2053+
}).toThrowMinErr('ngModel', 'nonassign', 'Expression \'someService.getInstance()\' is non-assignable.');
2054+
});
2055+
19782056
it('should assign invalid values to the scope if allowInvalid is true', function() {
19792057
compileInput('<input type="text" name="input" ng-model="value" maxlength="1" ' +
19802058
'ng-model-options="{allowInvalid: true}" />');

0 commit comments

Comments
 (0)