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

Commit b9fcf01

Browse files
committed
feat(ngModel): bind to getters/setters
Closes #768
1 parent 3f2232b commit b9fcf01

File tree

2 files changed

+133
-1
lines changed

2 files changed

+133
-1
lines changed

src/ng/directive/input.js

+91-1
Original file line numberDiff line numberDiff line change
@@ -1855,7 +1855,15 @@ var NgModelController = ['$scope', '$exceptionHandler', '$attrs', '$element', '$
18551855
};
18561856

18571857
this.$$writeModelToScope = function() {
1858-
ngModelSet($scope, ctrl.$modelValue);
1858+
var getterSetter;
1859+
1860+
if (ctrl.$options && ctrl.$options.getterSetter &&
1861+
isFunction(getterSetter = ngModelGet($scope))) {
1862+
1863+
getterSetter(ctrl.$modelValue);
1864+
} else {
1865+
ngModelSet($scope, ctrl.$modelValue);
1866+
}
18591867
forEach(ctrl.$viewChangeListeners, function(listener) {
18601868
try {
18611869
listener();
@@ -1930,6 +1938,10 @@ var NgModelController = ['$scope', '$exceptionHandler', '$attrs', '$element', '$
19301938
$scope.$watch(function ngModelWatch() {
19311939
var modelValue = ngModelGet($scope);
19321940

1941+
if (ctrl.$options && ctrl.$options.getterSetter && isFunction(modelValue)) {
1942+
modelValue = modelValue();
1943+
}
1944+
19331945
// if scope model value and ngModel value are out of sync
19341946
if (ctrl.$modelValue !== modelValue &&
19351947
(isUndefined(ctrl.$$invalidModelValue) || ctrl.$$invalidModelValue != modelValue)) {
@@ -2062,6 +2074,55 @@ var NgModelController = ['$scope', '$exceptionHandler', '$attrs', '$element', '$
20622074
</form>
20632075
</file>
20642076
* </example>
2077+
*
2078+
* ## Binding to a getter/setter
2079+
*
2080+
* Sometimes it's helpful to bind `ngModel` to a getter/setter function. A getter/setter is a
2081+
* function that returns a representation of the model when called with zero arguments, and sets
2082+
* the internal state of a model when called with an argument. It's sometimes useful to use this
2083+
* for models that have an internal representation that's different than what the model exposes
2084+
* to the view.
2085+
*
2086+
* <div class="alert alert-success">
2087+
* **Best Practice:** It's best to keep getters fast because Angular is likely to call them more
2088+
* frequently than other parts of your code.
2089+
* </div>
2090+
*
2091+
* You use this behavior by adding `ng-model-options="{ getterSetter: true }"` to an element that
2092+
* has `ng-model` attached to it. You can also add `ng-model-options="{ getterSetter: true }"` to
2093+
* a `<form>`, which will enable this behavior for all `<input>`s within it. See
2094+
* {@link ng.directive:ngModelOptions `ngModelOptions`} for more.
2095+
*
2096+
* The following example shows how to use `ngModel` with a getter/setter:
2097+
*
2098+
* @example
2099+
* <example name="ngModel-getter-setter" module="getterSetterExample">
2100+
<file name="index.html">
2101+
<div ng-controller="ExampleController">
2102+
<form name="userForm">
2103+
Name:
2104+
<input type="text" name="userName"
2105+
ng-model="user.name"
2106+
ng-model-options="{ getterSetter: true }" />
2107+
</form>
2108+
<pre>user.name = <span ng-bind="user.name()"></span></pre>
2109+
</div>
2110+
</file>
2111+
<file name="app.js">
2112+
angular.module('getterSetterExample', [])
2113+
.controller('ExampleController', ['$scope', function($scope) {
2114+
var _name = 'Brian';
2115+
$scope.user = {
2116+
name: function (newName) {
2117+
if (angular.isDefined(newName)) {
2118+
_name = newName;
2119+
}
2120+
return _name;
2121+
}
2122+
};
2123+
}]);
2124+
</file>
2125+
* </example>
20652126
*/
20662127
var ngModelDirective = function() {
20672128
return {
@@ -2459,6 +2520,8 @@ var ngValueDirective = function() {
24592520
* value of 0 triggers an immediate update. If an object is supplied instead, you can specify a
24602521
* custom value for each event. For example:
24612522
* `ngModelOptions="{ updateOn: 'default blur', debounce: {'default': 500, 'blur': 0} }"`
2523+
* - `getterSetter`: boolean value which determines whether or not to treat functions bound to
2524+
`ngModel` as getters/setters.
24622525
*
24632526
* @example
24642527
@@ -2541,6 +2604,33 @@ var ngValueDirective = function() {
25412604
}]);
25422605
</file>
25432606
</example>
2607+
2608+
This one shows how to bind to getter/setters:
2609+
2610+
<example name="ngModelOptions-directive-getter-setter" module="getterSetterExample">
2611+
<file name="index.html">
2612+
<div ng-controller="ExampleController">
2613+
<form name="userForm">
2614+
Name:
2615+
<input type="text" name="userName"
2616+
ng-model="user.name"
2617+
ng-model-options="{ getterSetter: true }" />
2618+
</form>
2619+
<pre>user.name = <span ng-bind="user.name()"></span></pre>
2620+
</div>
2621+
</file>
2622+
<file name="app.js">
2623+
angular.module('getterSetterExample', [])
2624+
.controller('ExampleController', ['$scope', function($scope) {
2625+
var _name = 'Brian';
2626+
$scope.user = {
2627+
name: function (newName) {
2628+
return angular.isDefined(newName) ? (_name = newName) : _name;
2629+
}
2630+
};
2631+
}]);
2632+
</file>
2633+
</example>
25442634
*/
25452635
var ngModelOptionsDirective = function() {
25462636
return {

test/ng/directive/inputSpec.js

+42
Original file line numberDiff line numberDiff line change
@@ -1173,6 +1173,48 @@ describe('input', function() {
11731173
expect(inputElm.val()).toBe('');
11741174
}));
11751175

1176+
it('should not try to invoke a model if getterSetter is false', function() {
1177+
compileInput(
1178+
'<input type="text" ng-model="name" '+
1179+
'ng-model-options="{ getterSetter: false }" />');
1180+
1181+
var spy = scope.name = jasmine.createSpy('setterSpy');
1182+
changeInputValueTo('a');
1183+
expect(spy).not.toHaveBeenCalled();
1184+
expect(inputElm.val()).toBe('a');
1185+
});
1186+
1187+
it('should not try to invoke a model if getterSetter is not set', function() {
1188+
compileInput('<input type="text" ng-model="name" />');
1189+
1190+
var spy = scope.name = jasmine.createSpy('setterSpy');
1191+
changeInputValueTo('a');
1192+
expect(spy).not.toHaveBeenCalled();
1193+
expect(inputElm.val()).toBe('a');
1194+
});
1195+
1196+
it('should always try to invoke a model if getterSetter is true', function() {
1197+
compileInput(
1198+
'<input type="text" ng-model="name" '+
1199+
'ng-model-options="{ getterSetter: true }" />');
1200+
1201+
var spy = scope.name = jasmine.createSpy('setterSpy').andCallFake(function () {
1202+
return 'b';
1203+
});
1204+
scope.$apply();
1205+
expect(inputElm.val()).toBe('b');
1206+
1207+
changeInputValueTo('a');
1208+
expect(inputElm.val()).toBe('b');
1209+
expect(spy).toHaveBeenCalledWith('a');
1210+
expect(scope.name).toBe(spy);
1211+
1212+
scope.name = 'c';
1213+
changeInputValueTo('d');
1214+
expect(inputElm.val()).toBe('d');
1215+
expect(scope.name).toBe('d');
1216+
});
1217+
11761218
});
11771219

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

0 commit comments

Comments
 (0)