11

I am trying to get getter/setter support for ng-model by implementing a directive that will take care of getting and setting the values to/from the view/model. I am almost there, but I end up in infinite $digest loops.

The idea is to set ng-model="$someFieldToStoreInTheScope", and then have the getter/setter directive do the updates between that field and the getter/setter functions.

I use $watch to update the model using the setter expression when the ngModelController updates the field in the scope, and another watch to update that field when the getter expression changes.

Have a look at: http://jsfiddle.net/BDyAs/10/

Html:

<div ng-app="myApp">
<body>
<form name="form">    
    <input type="text" ng-model="$ngModelValue" ng-model-getter-setter="get=getValue();set=setValue($value)"/> {{myDerivedValue}}
</form>
</body>
</div>

JS:

var myApp = angular.module('myApp', []);

myApp.directive(
    {
        'ngModelGetterSetter': function () {
            return {
                require: "ngModel",
                controller: ctrl,
                link:  function(scope, element, attrs, ngModelCtrl)
                {
                    var getterSetterExpression = attrs.ngModelGetterSetter;
                    var tokens = getterSetterExpression.split(";");
                    var getExpression = tokens[0].split("=")[1];
                    var setExpression = tokens[1].split("=")[1];

                    function updateViewValue()
                    {
                        var updateExpression = attrs.ngModel + "=" + getExpression;
                        scope.$eval(updateExpression);
                    }

                    function updateModelValue()
                    {
                        scope.$value = ngModelCtrl.$viewValue;
                        scope.$eval(setExpression);
                    }

                    updateViewValue();    

                    scope.$watch(getExpression, updateViewValue);
                    scope.$watch(attrs.ngModel, updateModelValue);
                }
            };
        }
    });

function ctrl($scope) {
    $scope.getValue = function ()
    {
        return $scope.myValue;
    }

    $scope.setValue = function (val)
    {
        $scope.myValue = val;
        $scope.myDerivedValue = $scope.myValue * 2;
    }

    $scope.setValue(5);

    setInterval(function () { $scope.setValue($scope.getValue() + 1); $scope.$apply(); }, 1000);
}

I have put a setInterval() in my code to modify the model and see if it propagates correctly in the view.

Any idea why there is an infinite digest loop, and how to remove it?

sboisse
  • 4,860
  • 3
  • 37
  • 48
  • Yes, the question is that it doesn't work well because of the infinite digest loop. Why is that loop there and how to remove it? – sboisse Jan 22 '14 at 17:53
  • The infinite digest loop seemed to be caused by angularjs having a hard time propagating the new value everywhere before it propagates the old value as well, in the same loop. I could break the loop by adding calls to ngModelCtrl.$setViewValue() and ngModelCtrl.$render() in the updateViewValue() function (Fiddle: http://jsfiddle.net/BDyAs/12/), but it would be interesting to know more precisely what was going on, and if the way I broke the loop is the best way to do it. – sboisse Jan 22 '14 at 18:04
  • I think the infinite loop is because on changeValue the watch is calling the set method which actually is modifying the model... so the watch is called again. Maybe you can try something like "if actual!=new --> update else --> do nothing". – Carlos Verdes Mar 24 '14 at 12:38

2 Answers2

8

NOTE AngularJs 1.3 now supports Getter/Setter for ng-model. Refer to http://docs.angularjs.org/api/ng/directive/ngModelOptions for more information.


I could break the infinite loop with extra calls to

ngModelCtrl.$setViewValue()

and

ngModelCtrl.$render()

in the event handlers. Not sure if it's the best way to do it though.

See fiddle: http://jsfiddle.net/BDyAs/12/

EDIT:

I improved the code even more in

http://jsfiddle.net/BDyAs/15/

by separating the directive in separate ones for the getter and the setter.

sboisse
  • 4,860
  • 3
  • 37
  • 48
  • according to the angularjs weekly newsletter, getter/setter support has been merged in - cant find any great info on it atm though – DrogoNevets Jul 16 '14 at 07:49
  • That is very good news. If you could provide a link to the newsletter that would be great! – sboisse Jul 17 '14 at 09:03
  • 1
    I think this might be what @DrogoNevets was referring to (note that it is angular 1.3, not included in 1.2): https://docs.angularjs.org/api/ng/directive/ngModelOptions – EverPresent Jul 29 '14 at 20:25
  • IT looks like it is. I added a note to the answer. Thanks for sharing. – sboisse Jul 30 '14 at 15:34
2

I think the question about breaking the digest loop has been answered. Here is a different, much cleaner approach to the same problem that doesn't involve $watch.

When you don't need to support legacy browsers, use ECMAScript 5 accessors.

Just add a property to your angular controller:

Object.defineProperty(
    $scope, 
    "accessorWrappedMyValue",            
    {
        get : function() { return $scope.myValue; },  
        set : function(newValue) { 
                  $scope.myValue = newValue; 
                  $scope.myDerivedValue = $scope.myValue * 2;
              },
        configurable: true
    });

Now all you need to do is reference accessorWrappedMyValue from ng-model like so:

<input type="text" ng-model="accessorWrappedMyValue" />

Further reading

This blog has a nice introduction to ES5 accessors.

Use this feature matrix to decide whether you can go with ES5 or not. The interesting lines are "Getter / Setter in property initializer" and "Object.defineProperty".

theDmi
  • 17,546
  • 6
  • 71
  • 138