8000 feat(ngModel): add ngModelContext for getter/setter bindings by NevilleS · Pull Request #9865 · angular/angular.js · GitHub
[go: up one dir, main page]

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

feat(ngModel): add ngModelContext for getter/setter bindings #9865

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
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!
  • Loading branch information
NevilleS committed Nov 7, 2014
commit c0747ddb63fe12509cf462c221f89fe9443d07c3
10 changes: 8 additions & 2 deletions src/ng/directive/input.js
Original file line number Diff line number Diff line change
Expand Up @@ -1716,13 +1716,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 = modelValue.call(parsedNgModelContext ? parsedNgModelContext($scope) : $scope);
}
return modelValue;
};
Expand All @@ -1732,7 +1733,7 @@ var NgModelController = ['$scope', '$exceptionHandler', '$attrs', '$element', '$
if (ctrl.$options && ctrl.$options.getterSetter &&
isFunction(getterSetter = parsedNgModel($scope))) {

getterSetter(ctrl.$modelValue);
getterSetter.call(parsedNgModelContext ? parsedNgModelContext($scope) : $scope, ctrl.$modelValue);
} else {
parsedNgModel.assign($scope, ctrl.$modelValue);
}
Expand All @@ -1741,6 +1742,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
77 changes: 77 additions & 0 deletions test/ng/directive/inputSpec.js
Original file line number Diff line number Diff line change
Expand Up @@ -1975,6 +1975,83 @@ describe('input', function() {
'ng-model-options="{ getterSetter: true }" />');
});

it('should try to invoke a model with default context if getterSetter is true and getterSetterContext is not provided', function() {
scope.value = 'scopeContext';
compileInput(
'<input type="text" ng-model="someService.getterSetter" '+
'ng-model-options="{ getterSetter: true }" />');

scope.someService = {
value: 'b',
getterSetter: function(newValue) {
this.value = newValue || this.value;
return this.value;
}
};
spyOn(scope.someService, 'getterSetter').andCallThrough();
scope.$apply();
expect(inputElm.val()).toBe('scopeContext');
expect(scope.someService.getterSetter).toHaveBeenCalledWith();
expect(scope.someService.value).toBe('b'); // 'this' is not bound to the service w/o ngModelContext
expect(scope.value).toBe('scopeContext');

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

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

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

it('should try to invoke a model with the provided context if getterSetter is true and getterSetterContext is an expression', function() {
compileInput(
'<input type="text" ng-model="someService.getterSetter" '+
'ng-model-options="{ getterSetter: true, getterSetterContext: \'someService\' }" />');

scope.someService = {
value: 'b',
getterSetter: function(newValue) {
this.value = newValue || this.value;
return this.value;
}
};
spyOn(scope.someService, 'getterSetter').andCallThrough();
scope.$apply();
expect(inputElm.val()).toBe('b');
expect(scope.someService.getterSetter).toHaveBeenCalledWith();
expect(scope.someService.value).toBe('b');

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

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

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