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

Commit

Permalink
fix(ngModelOptions): preserve context of getter/setters
Browse files Browse the repository at this point in the history
Closes #9394

BREAKING CHANGE: previously, ngModel invoked getter/setters in the global context.

For example:

```js
<input ng-model="model.value" ng-model-options="{ getterSetter: true }">
```

would previously invoke `model.value()` in the global context.

Now, ngModel invokes `value` with `model` as the context.

It's unlikely that real apps relied on this behavior. If they did they can use `.bind` to explicilty
bind a getter/getter to the global context, or just reference globals normally without `this`.
  • Loading branch information
btford committed Nov 20, 2014
1 parent e3764e3 commit 72e7adf
Show file tree
Hide file tree
Showing 2 changed files with 45 additions and 3 deletions.
11 changes: 8 additions & 3 deletions src/ng/directive/input.js
Expand Up @@ -1740,13 +1740,14 @@ var NgModelController = ['$scope', '$exceptionHandler', '$attrs', '$element', '$


var parsedNgModel = $parse($attr.ngModel),
parsedNgModelContext = null,
pendingDebounce = null,
ctrl = this;

var ngModelGet = function ngModelGet() {
var modelValue = parsedNgModel($scope);
if (ctrl.$options && ctrl.$options.getterSetter && isFunction(modelValue)) {
modelValue = modelValue();
modelValue = $scope.$eval($attr.ngModel + '()');
}
return modelValue;
};
Expand All @@ -1755,8 +1756,7 @@ var NgModelController = ['$scope', '$exceptionHandler', '$attrs', '$element', '$
var getterSetter;
if (ctrl.$options && ctrl.$options.getterSetter &&
isFunction(getterSetter = parsedNgModel($scope))) {

getterSetter(ctrl.$modelValue);
$scope.$eval($attr.ngModel + '($$$p)', {$$$p: ctrl.$modelValue});
} else {
parsedNgModel.assign($scope, ctrl.$modelValue);
}
Expand All @@ -1765,6 +1765,11 @@ var NgModelController = ['$scope', '$exceptionHandler', '$attrs', '$element', '$
this.$$setOptions = function(options) {
ctrl.$options = options;

if (ctrl.$options && ctrl.$options.getterSetter && ctrl.$options.getterSetterContext) {
// Use the provided context expression to specify the context used when invoking the
// getter/setter function
parsedNgModelContext = $parse(ctrl.$options.getterSetterContext);
}
if (!parsedNgModel.assign && (!options || !options.getterSetter)) {
throw $ngModelMinErr('nonassign', "Expression '{0}' is non-assignable. Element: {1}",
$attr.ngModel, startingTag($element));
Expand Down
37 changes: 37 additions & 0 deletions test/ng/directive/inputSpec.js
Expand Up @@ -2093,6 +2093,43 @@ 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 fail to parse if getterSetterContext is an invalid expression', function() {
expect(function() {
compileInput(
'<input type="text" ng-model="someService.getterSetter" '+
'ng-model-options="{ getterSetter: true, getterSetterContext: \'throw error\' }" />');
}).toThrowMinErr("$parse", "syntax", "Syntax Error: Token 'error' is an unexpected token at column 7 of the expression [throw error] starting at [error].");
});

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

0 comments on commit 72e7adf

Please sign in to comment.