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

feat(ngModel): bind to getters/setters #7991

Merged
merged 1 commit into from
Jul 8, 2014
Merged
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
92 changes: 91 additions & 1 deletion src/ng/directive/input.js
Original file line number Diff line number Diff line change
Expand Up @@ -1855,7 +1855,15 @@ var NgModelController = ['$scope', '$exceptionHandler', '$attrs', '$element', '$
};

this.$$writeModelToScope = function() {
ngModelSet($scope, ctrl.$modelValue);
var getterSetter;

if (ctrl.$options && ctrl.$options.getterSetter &&
isFunction(getterSetter = ngModelGet($scope))) {

getterSetter(ctrl.$modelValue);
} else {
ngModelSet($scope, ctrl.$modelValue);
}
forEach(ctrl.$viewChangeListeners, function(listener) {
try {
listener();
Expand Down Expand Up @@ -1930,6 +1938,10 @@ var NgModelController = ['$scope', '$exceptionHandler', '$attrs', '$element', '$
$scope.$watch(function ngModelWatch() {
var modelValue = ngModelGet($scope);

if (ctrl.$options && ctrl.$options.getterSetter && isFunction(modelValue)) {
modelValue = modelValue();
}

// if scope model value and ngModel value are out of sync
if (ctrl.$modelValue !== modelValue &&
(isUndefined(ctrl.$$invalidModelValue) || ctrl.$$invalidModelValue != modelValue)) {
Expand Down Expand Up @@ -2062,6 +2074,55 @@ var NgModelController = ['$scope', '$exceptionHandler', '$attrs', '$element', '$
</form>
</file>
* </example>
*
* ## Binding to a getter/setter
*
* Sometimes it's helpful to bind `ngModel` to a getter/setter function. A getter/setter is a
* function that returns a representation of the model when called with zero arguments, and sets
* the internal state of a model when called with an argument. It's sometimes useful to use this
* for models that have an internal representation that's different than what the model exposes
* to the view.
*
* <div class="alert alert-success">
* **Best Practice:** It's best to keep getters fast because Angular is likely to call them more
* frequently than other parts of your code.
* </div>
*
* You use this behavior by adding `ng-model-options="{ getterSetter: true }"` to an element that
* has `ng-model` attached to it. You can also add `ng-model-options="{ getterSetter: true }"` to
* a `<form>`, which will enable this behavior for all `<input>`s within it. See
* {@link ng.directive:ngModelOptions `ngModelOptions`} for more.
*
* The following example shows how to use `ngModel` with a getter/setter:
*
* @example
* <example name="ngModel-getter-setter" module="getterSetterExample">
<file name="index.html">
<div ng-controller="ExampleController">
<form name="userForm">
Name:
<input type="text" name="userName"
ng-model="user.name"
ng-model-options="{ getterSetter: true }" />
</form>
<pre>user.name = <span ng-bind="user.name()"></span></pre>
</div>
</file>
<file name="app.js">
angular.module('getterSetterExample', [])
.controller('ExampleController', ['$scope', function($scope) {
var _name = 'Brian';
$scope.user = {
name: function (newName) {
if (angular.isDefined(newName)) {
_name = newName;
}
return _name;
}
};
}]);
</file>
* </example>
*/
var ngModelDirective = function() {
return {
Expand Down Expand Up @@ -2459,6 +2520,8 @@ var ngValueDirective = function() {
* value of 0 triggers an immediate update. If an object is supplied instead, you can specify a
* custom value for each event. For example:
* `ngModelOptions="{ updateOn: 'default blur', debounce: {'default': 500, 'blur': 0} }"`
* - `getterSetter`: boolean value which determines whether or not to treat functions bound to
`ngModel` as getters/setters.
*
* @example

Expand Down Expand Up @@ -2541,6 +2604,33 @@ var ngValueDirective = function() {
}]);
</file>
</example>

This one shows how to bind to getter/setters:

<example name="ngModelOptions-directive-getter-setter" module="getterSetterExample">
<file name="index.html">
<div ng-controller="ExampleController">
<form name="userForm">
Name:
<input type="text" name="userName"
ng-model="user.name"
ng-model-options="{ getterSetter: true }" />
</form>
<pre>user.name = <span ng-bind="user.name()"></span></pre>
</div>
</file>
<file name="app.js">
angular.module('getterSetterExample', [])
.controller('ExampleController', ['$scope', function($scope) {
var _name = 'Brian';
$scope.user = {
name: function (newName) {
return angular.isDefined(newName) ? (_name = newName) : _name;
}
};
}]);
</file>
</example>
*/
var ngModelOptionsDirective = function() {
return {
Expand Down
42 changes: 42 additions & 0 deletions test/ng/directive/inputSpec.js
Original file line number Diff line number Diff line change
Expand Up @@ -1173,6 +1173,48 @@ describe('input', function() {
expect(inputElm.val()).toBe('');
}));

it('should not try to invoke a model if getterSetter is false', function() {
compileInput(
'<input type="text" ng-model="name" '+
'ng-model-options="{ getterSetter: false }" />');
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

you should also add a test for the default behavior


var spy = scope.name = jasmine.createSpy('setterSpy');
changeInputValueTo('a');
expect(spy).not.toHaveBeenCalled();
expect(inputElm.val()).toBe('a');
});

it('should not try to invoke a model if getterSetter is not set', function() {
compileInput('<input type="text" ng-model="name" />');

var spy = scope.name = jasmine.createSpy('setterSpy');
changeInputValueTo('a');
expect(spy).not.toHaveBeenCalled();
expect(inputElm.val()).toBe('a');
});

it('should always try to invoke a model if getterSetter is true', function() {
compileInput(
'<input type="text" ng-model="name" '+
'ng-model-options="{ getterSetter: true }" />');

var spy = scope.name = jasmine.createSpy('setterSpy').andCallFake(function () {
return 'b';
});
scope.$apply();
expect(inputElm.val()).toBe('b');

changeInputValueTo('a');
expect(inputElm.val()).toBe('b');
expect(spy).toHaveBeenCalledWith('a');
expect(scope.name).toBe(spy);

scope.name = 'c';
changeInputValueTo('d');
expect(inputElm.val()).toBe('d');
expect(scope.name).toBe('d');
});

});

it('should allow complex reference binding', function() {
Expand Down