AngularJS: $rootScope:infdig error when calling a ng-style function inside ng-repeat
Asked Answered
M

4

8

I'm trying to build an animation on some phrases that will be displayed on the site main page, in a random position and with fade and translate effects.

I would achieve this using ng-style attribute inside an ng-repeat attribute and setting the ng-style value calling a JavaScript function defined inside the HomeController.

Using this approch cause angular to throw the exception: $rootScope:infdig error 10 $digest() iterations reached. Aborting! Watchers fired in the last 5 iterations

I read so much about this but no solution has solved my case. Anyone could help me?

Here is a part of index.html:

<div class="phrasesContainer" animate-phrases="">
      <h3 class="flying-text" ng-repeat="phrase in Phrases" ng-style="getTopLeftPosition()">{{phrase}}</h3>
    </div>

Here is the controller function:

$scope.getTopLeftPosition = function() {
var top = randomBetween(10, 90);
var left = getRandomTwoRange(5, 30, 70, 80);

return {
  top: top + '%',
  left: left + '%'
};

}

Here is a demo: http://plnkr.co/edit/8sYks589agtLbZCGJ08B?p=preview

Muffle answered 22/9, 2015 at 16:11 Comment(1)
getTopLeftPosition() is being called on every digest because it never stabilizes. It always returns a different value. Why not move the style related code into your directive?Triceratops
T
2

Here's a solution where I moved your style generation into the directive. The position is being set right before showing the element. Since this is a CSS change, I modified the styling as well so that the position does not transition.

Here's the directive. The code I've excluded has not been changed:

app.directive('animatePhrases', function() {
  return {
    restrict: 'A',
    link: function(scope, element, attrs) {
      setTimeout(function() {
        ...
      }, 1000);

      function changeText() {
        var currentActive = $('.phrasesContainer .active');
        var nextActive = currentActive.next();
        currentActive.toggleClass('active');

        if (nextActive.length == 0)
          nextActive = $('.phrasesContainer .flying-text').first();

        nextActive.css(getTopLeftPosition()); // Add this
        nextActive.toggleClass('active');

        setTimeout(changeText, 5000);
      }

      function getTopLeftPosition() {
        ...
      }

      function getRandomTwoRange(firstStart, firstEnd, secondStart, secondEnd) {
        ...
      }

      function randomBetween(min, max) {
        ...
      }
    }
  };
});

CSS:

.flying-text {
    transition: opacity 2s ease-in-out, margin 2s ease-in-out;
    position: absolute;
    opacity: 0;
    font-size: 1.5em;
}

In your HTML, simply remove the ng-style.

Plunker: http://plnkr.co/edit/bZB3A5hD7Bc4r4pp1g7V?p=preview

Triceratops answered 22/9, 2015 at 17:11 Comment(1)
Thanks! I will use that solution, the controller is cleaner :) Also @Univalent solution solve my problem (#32722266), it does not allow randomness at each iteraction but it's still a great solution if you want random position only at the start!Muffle
U
4

@Hieu Le hinted to the problem, but your issue is that since you are always returning a random position in your getTopLeftPosition function, angularjs digest loop will get called every time to actually propagate the changes to the watchers. This caused it to keep running over and over.

What you can do is to pre-calculate your random positions and then use that in your html.

For example, in your activate function you can do something like this:

  function activate() {
    $scope.Phrases = ["Phrase 1", "Phrase 2", "Phrase 3", "Phrase 4", "Phrase 5", "Phrase 6"];
    $scope.PhrasesAndPositions = $scope.Phrases.map(function(phrase){
      return {
        phrase: phrase,
        position: getTopLeftPosition()
      }
    });
  }

And then you can change your html to something like this:

    <div class="phrasesContainer" animate-phrases="">
      <h3 class="flying-text" ng-repeat="pap in PhrasesAndPositions" ng-style="pap.position">{{pap.phrase}}</h3>
    </div>

Here is the working plunk with my changes: http://plnkr.co/edit/FD9hYX9Q5wUkW2q7y86M?p=preview

Univalent answered 22/9, 2015 at 16:56 Comment(3)
I think the OP wants a phrase to have a new position every time it reappears. Not clear from the question though. Nice answer!Triceratops
Great suggestion @JoseM, the problem with this solution is that it removes the randomness of the position. When it repeats again, all phrases will have the same position and not randomizedElsewhere
Thanks! This solution solve my problem!! @Hieu Le Also my solution does not set random positions at each iteraction (because of the ingdig exception). However, the Anid Monsur solution allow this behaviour. I can't accept both answers but I would it (#32722266)Muffle
T
2

Here's a solution where I moved your style generation into the directive. The position is being set right before showing the element. Since this is a CSS change, I modified the styling as well so that the position does not transition.

Here's the directive. The code I've excluded has not been changed:

app.directive('animatePhrases', function() {
  return {
    restrict: 'A',
    link: function(scope, element, attrs) {
      setTimeout(function() {
        ...
      }, 1000);

      function changeText() {
        var currentActive = $('.phrasesContainer .active');
        var nextActive = currentActive.next();
        currentActive.toggleClass('active');

        if (nextActive.length == 0)
          nextActive = $('.phrasesContainer .flying-text').first();

        nextActive.css(getTopLeftPosition()); // Add this
        nextActive.toggleClass('active');

        setTimeout(changeText, 5000);
      }

      function getTopLeftPosition() {
        ...
      }

      function getRandomTwoRange(firstStart, firstEnd, secondStart, secondEnd) {
        ...
      }

      function randomBetween(min, max) {
        ...
      }
    }
  };
});

CSS:

.flying-text {
    transition: opacity 2s ease-in-out, margin 2s ease-in-out;
    position: absolute;
    opacity: 0;
    font-size: 1.5em;
}

In your HTML, simply remove the ng-style.

Plunker: http://plnkr.co/edit/bZB3A5hD7Bc4r4pp1g7V?p=preview

Triceratops answered 22/9, 2015 at 17:11 Comment(1)
Thanks! I will use that solution, the controller is cleaner :) Also @Univalent solution solve my problem (#32722266), it does not allow randomness at each iteraction but it's still a great solution if you want random position only at the start!Muffle
I
0

this way you can manger your ng-style

$scope.getTopLeftPosition = function() {
$scope.top = randomBetween(20, 90);
$socpe.left = getRandomTwoRange(5, 30, 70, 80);

}

function getRandomTwoRange(firstStart, firstEnd, secondStart, secondEnd) {
var isLower = (Math.random() * 2) < 1;
if (isLower)
  return randomBetween(firstStart, firstEnd);
else
  return randomBetween(secondStart, secondEnd);
}

function randomBetween(min, max) {
 return Math.floor(Math.random() * (max - min + 1)) + min;
}


<body>
<div ng-controller="HomeController">
  <div class="row" style="height:100%">
    <div class="phrasesContainer" animate-phrases="">
      <h3 class="flying-text " ng-repeat="phrase in Phrases" ng-style="{ 'top' : top, 'left' : left }">{{phrase}}</h3>
    </div>
  </div>
</div>

Illa answered 22/9, 2015 at 16:34 Comment(3)
Please explain your solution.Triceratops
Where is $scope.getTopLeftPosition being called?Triceratops
using scope rather than calling a function you can solve your problem i guess.Illa
E
0

I think the problem is that you cannot bind ng-style like that. It thinks that it needs to call getTopLeftPosition constantly.

I got it to kind of work here. The timing is a bit off but there are no more errors. Here I used $interval to repeat things:

Please view it here: http://plnkr.co/edit/B4iMZXMvZFoYMCKNvVrc?p=preview

  $scope.phrase_style = getTopLeftPosition();

  function change_phrase_style() {
      $scope.phrase_style = getTopLeftPosition();
  }

  function start_interval() {
      change_phrase_style();
      $interval(function(){
          change_phrase_style();
      }.bind(this), 5000); 
  }

  setTimeout(start_interval, 1000);

This might be related as well: angularjs infinite $digest Loop when no scope changes

Elsewhere answered 22/9, 2015 at 16:46 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.