diff --git a/src/ng/directive/input.js b/src/ng/directive/input.js index ab642338358e..558cce14fe7a 100644 --- a/src/ng/directive/input.js +++ b/src/ng/directive/input.js @@ -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(); @@ -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)) { @@ -2062,6 +2074,55 @@ var NgModelController = ['$scope', '$exceptionHandler', '$attrs', '$element', '$ * + * + * ## 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. + * + *
+ * **Best Practice:** It's best to keep getters fast because Angular is likely to call them more + * frequently than other parts of your code. + *
+ * + * 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 `
`, which will enable this behavior for all ``s within it. See + * {@link ng.directive:ngModelOptions `ngModelOptions`} for more. + * + * The following example shows how to use `ngModel` with a getter/setter: + * + * @example + * + +
+ + Name: + + +
user.name = 
+
+
+ + angular.module('getterSetterExample', []) + .controller('ExampleController', ['$scope', function($scope) { + var _name = 'Brian'; + $scope.user = { + name: function (newName) { + if (angular.isDefined(newName)) { + _name = newName; + } + return _name; + } + }; + }]); + + *
*/ var ngModelDirective = function() { return { @@ -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 @@ -2541,6 +2604,33 @@ var ngValueDirective = function() { }]); + + This one shows how to bind to getter/setters: + + + +
+
+ Name: + +
+
user.name = 
+
+
+ + angular.module('getterSetterExample', []) + .controller('ExampleController', ['$scope', function($scope) { + var _name = 'Brian'; + $scope.user = { + name: function (newName) { + return angular.isDefined(newName) ? (_name = newName) : _name; + } + }; + }]); + +
*/ var ngModelOptionsDirective = function() { return { diff --git a/test/ng/directive/inputSpec.js b/test/ng/directive/inputSpec.js index d39832f4c102..5c3784f50507 100644 --- a/test/ng/directive/inputSpec.js +++ b/test/ng/directive/inputSpec.js @@ -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( + ''); + + 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(''); + + 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( + ''); + + 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() {