ps0604
ps0604

Reputation: 1071

Approach to avoid loop in watched variable

In this plunk I have a controller with a scope variable var1 and a directive with a scope variable var2. var2 should mirror the value of var1 and vice versa. I watch the variable in the directive to know when it's changed in the controller.

The problem is that this generates a loop when the watched variable is changed in the directive (see the plunk console).

To prevent this loop, I could create in the directive two scope variables var2Input and var2Output to read/write var1 in the controller. Only var2Input would be watched and whatever I change in the directive I change in var2Output (that is read in the controller). But I don't want to create two variables as they would always have the same value. Any ideas how to approach this?

Javascript:

app.directive('someDirective', function () {

    var directive = {};

    directive.restrict = 'EA';

    directive.scope = {    
            var2: '='
    };

    directive.template = 'This is var2: {{var2}} <br/> ' + 
    '<button ng-click="add1()">Add 1</button>  <br/> ' + 
    '{{log}}';

    directive.link = {};

    directive.link.pre = function (scope, element, attrs) {};

    directive.link.post = function (scope, element, attrs) {

        scope.log = '';

        scope.$watch('var2', function (newValue, oldValue) {
               if (typeof newValue === 'undefined')
                  return;
          scope.var2 = newValue * 10;
          scope.log = scope.log + '<watched ' + newValue + '>';
        });

        scope.add1 = function() {
          scope.var2++;
        };
    };

    return directive;

});

Upvotes: 1

Views: 133

Answers (2)

Intervalia
Intervalia

Reputation: 10945

OK. Here is some code that should do what you are looking for. Look at the code then my explanation below:

<!DOCTYPE html>
<html ng-app="myApp">
  <head>
    <title>AngularJS Example</title>
    <script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.2.23/angular.min.js"></script>
    <script>
    var myApp = angular.module("myApp", []);

    myApp.controller('appController', function($scope) {
      $scope.var1 = 1;

      $scope.add = function(amount) {
        $scope.var1 += amount;
      }
    });

    myApp.controller('watcherController', watcherController);
    watcherController.$inject = ['$scope'];
    function watcherController($scope) {
      $scope.log = '';

      $scope.$watch('var2', function (newValue, oldValue) {
        if (typeof newValue === 'undefined') {
          return;
        }

        $scope.log = $scope.log + '\nwatched ' + newValue;
      });

      $scope.add1 = function() {
        $scope.var2++;
      };
    }

    myApp.directive('watcher', watcherDirective );
    function watcherDirective() {
      return {
        'restrict': 'EA',
        'template': 'This is var2: {{var2}} <br/><hr/><button ng-click="add1()">Add 1</button><br/><hr/><h4>LOG:</h4><pre>{{log}}</pre>',
        'controller': 'watcherController',
        'scope': {
          var2: '@'
        },
        'link': function ($scope, $element, $attrs, ctrl, transclude) {
          $attrs.$observe('var2', function(newValue, one, two) {
            $scope.var2 = parseInt(newValue,10)*10;
            $scope.log += "\nChanged on the outside"+$scope.var2+' - '+(typeof $scope.var2);
          });
        }
      };
    }
    </script>
  </head>
  <body ng-controller="appController">
    <button ng-click="add(5)">Add from outer controller</button><br/>Var1:{{var1}}<br/><hr/>
    <watcher var2="{{var1}}"></watcher>
  </body>
</html>

The $scope.$watch is called every time $scope.var2 is changed no matter who changes it so we have to be careful what we do inside of that watcher.

I now display the outer controller's value for $scope.var1 and I added a second button that changes the outer controller's $scope.var1 That value gets passed down into the directive every time it changes.

Instead of using 2-way binding 'var2': '=' I am using 1-way binding 'var2': '@' This converts the value of $scope.var1 into a string and passes that into the directive.

Then, in the directive I added $attrs.$observe to let me know when the outer value changes. I have to convert that value back into a number by using parseInt(). The $attrs.$observer function is only called when the outside world changes the value for the directive's attribute var2.

If you need 2-way binding then there are different things you would need to do. So if the directive needs to be able to modify the value of the outer controller's $scope.var1 then I will need to demo something else.

If this doesn't make sense let me know and I will amend my answer.

AMENDED ANSWER:

Here is some revised code. Below is an explanation:

<!DOCTYPE html>
<html ng-app="myApp">
  <head>
    <title>AngularJS Example</title>
    <script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.2.23/angular.min.js"></script>
    <script>
    var myApp = angular.module("myApp", []);

    myApp.controller('appController', function($scope) {
      $scope.var1 = 1;

      $scope.add = function() {
        $scope.var1++;
      }

      $scope.reset = function() {
        $scope.var1 = 1;
      }
    });


    //******************* BEGIN TWO WAY BINDING
    myApp.controller('watcherController', watcherController);
    watcherController.$inject = ['$scope'];
    function watcherController($scope) {
      $scope.log = '';
      var doWatchCode = true;

      $scope.$watch('var2', function (newValue, oldValue) {
        if (typeof newValue === 'undefined') {
          return;
        }

        if (doWatchCode) {
          $scope.var2 = newValue * 10;
          $scope.log = $scope.log + '\nvar2 watched newValue(' + newValue+') - $scope.var2('+$scope.var2+')';
          doWatchCode = false;
        }
        else {
          doWatchCode = true;
        }
      });

      $scope.add1 = function() {
        doWatchCode = false;
        $scope.var2++;
      };
    }

    myApp.directive('watcher', watcherDirective );
    function watcherDirective() {
      return {
        'restrict': 'EA',
        'template': 'This is var2: {{var2}} <br/><hr/><button ng-click="add1()">Add 1</button><br/><hr/><h4>LOG:</h4><pre>{{log}}</pre>',
        'controller': 'watcherController',
        'scope': {
          var2: '='
        },
        'link': function ($scope, $element, $attrs, ctrl, transclude) {
          $attrs.$observe('var2', function(newValue) {
            $scope.log += "\nChanged on the outside"+newValue+' - '+(typeof newValue);
          });
        }
      };
    }
    //******************* END TWO WAY BINDING
    </script>
  </head>
  <body ng-controller="appController">
    <button ng-click="reset()">Reset Var1</button><br/>
    <button ng-click="add()">Add from outer controller</button><br/>Var1:{{var1}}<br/><hr/>
    <watcher var2="var1"></watcher>
  </body>
</html>

What I am doing here is adding a gate variable doWatchCode that either allows your $watch function to perform its operations or not.

When doWatchCode is true then your $watch function can do anything it needs to do. If it changes the value of var2 then it must change the value of doWatchCode to false. The reason for this is that changing var2 will cause your $watch function to be called again. But this time doWatchCode will be false and prevent the value of var2 from being updated again. The value for doWatchCode must be set back to true before exiting the function.

In you add function you must set doWatchCode to false to prevent your internal functions from allowing your multiply to happen again.

When the value for var2 is changed from the outside doWatchCode should be true which will allow your $watch function to do its work.

Let me know if that works.

Upvotes: 2

Maxim Shoustin
Maxim Shoustin

Reputation: 77904

You need watcher only for 1st time to catch 1st text change. After that you can cancel it. Also CodeMirror is 3d party plugin and you can add empty $timeout to trigger digest cycle. Here is a working fixed demo:

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

app.controller('myCtl', function($scope) {

     $scope.text = "this is the text";

     $scope.showText = function() {
       alert($scope.text);
     };

});

app.directive('editor', function ($timeout) {

    var directive = {};

    directive.restrict = 'EA';

    directive.scope = {    
            text: '='
    };

    directive.template = '<textarea id="cm"> </textarea>';

    directive.link = function (scope, element, attrs) {

      scope.editor = CodeMirror.fromTextArea(document.getElementById('cm'), {
          lineNumbers: true
      });
      scope.editor.setSize(300, 100);


      var canceler = scope.$watch('text', function (newValue, oldValue) {
            if (typeof newValue === 'undefined')
                  return;

               scope.editor.setValue(newValue);
               canceler();
        });


      scope.editor.on("change", function(cm, change) {
        $timeout(function(){
           scope.text = scope.editor.getValue();
        });
      });
    };

    return directive;
});

Demo Plunker

Upvotes: 1

Related Questions