wings
wings

Reputation: 591

Looking for a better way to loop through and display one element at a time in AngularJS

I'm building an app that has animations in it and I need it to work better. I want to be able to go through a series of divs at a specified animation interval and display one at a time. And I want each series to have its own speed and its own divs.

Here's what I have so far (also copied below): http://jsfiddle.net/ollerac/shkq7/

Basically, I'm looking for a way to put the setInterval on a property of of the animatedBox so I can create a new animatedBox with custom properties. But every time I try to do this it breaks.

HTML

<div ng-app ng-controller="BoxController">

    <div class="layer" ng-repeat="layer in animatedBox.layers" ng-style="{ 'backgroundColor': layer.color}" ng-show="layer == animatedBox.selectedLayer"></div>

</div>


JAVASCRIPT

function buildBox () {
    return {
        color: '#' + (Math.random() * 0xFFFFFF << 0).toString(16)
    }
}

function BoxController ($scope) {
    $scope.animatedBox = {
        layers: [],
        selectedLayer: null
    };

    for (var i = 0; i < 5; i++) {
        $scope.animatedBox.layers.push(buildBox());
    }

    var i = -1;
    setInterval(function () {
        $scope.$apply(function() {
            i++;

            if (i < $scope.animatedBox.layers.length) {
                $scope.animatedBox.displayThisLayer = $scope.animatedBox.layers[i];
            } else {
              i = 0;
              $scope.animatedBox.selectedLayer = $scope.animatedBox.layers[i];
            }
        });
    }, 500);
}


CSS

.layer {
    width: 30px;
    height: 30px;
    position: absolute;
}

*Update*

Here's more along the lines of what I want to do:

updated jsFiddle: http://jsfiddle.net/ollerac/shkq7/2/

function buildBox () {
    return {
        color: '#' + (Math.random() * 0xFFFFFF << 0).toString(16)
    }
}

function BoxController ($scope) {
    $scope.animatedBox = {
        layers: [],
        selectedLayer: null,
        selectedLayerIndex: -1,
        updateSelectedLayer: function () {
            var self = this;

            if (self.layers.length) {
                $scope.$apply(function() {
                    self.selectedLayerIndex++;

                    if (self.selectedLayerIndex < self.layers.length) {
                        self.selectedLayer = self.layers[self.selectedLayerIndex];
                    } else {
                        self.selectedLayerIndex = 0;
                        self.selectedLayer = self.layers[self.selectedLayerIndex];
                    }
                });
            }
        }
    };

    for (var i = 0; i < 5; i++) {
        $scope.animatedBox.layers.push(buildBox());
    }

    setInterval(function () {
        $scope.animatedBox.updateSelectedLayer();
    }, 500);    
}

So now the object updates its own selectedLayer property. But I still need to call the setInterval that calls the update separately in order to get it to update. But I'd like the object to update itself and be completely independent. Can you think of a good way to do this because I'm really stuggling with it...

I guess this is more of a general javascript question, but I thought there might be an Angular way to handle this type of situation, like maybe using a directive or something would be appropriate.

Any suggestions would be much appreciated.

Upvotes: 1

Views: 939

Answers (1)

Michelle Tilley
Michelle Tilley

Reputation: 159105

You are correct, I believe a directive is the right solution here. (This one was a fun one to work on, by the way. :)

When approaching a problem like this, I usually start by writing the HTML and controller that I'd wish I could write, if everything already worked. For this example, here's what I ended up with.

<div ng-controller="BoxController">
  <div animated-boxes="colors"></div>
</div>
app.value('randomColor', function() {
  var red   = Math.floor(Math.random() * 255);
  var green = Math.floor(Math.random() * 255);
  var blue  = Math.floor(Math.random() * 255);
  return "rgb(" + red + "," + green + "," + blue + ")";
});

app.controller('BoxController', function($scope, randomColor) {
  $scope.colors = [ randomColor(), randomColor() ];
});

Here, the controller is only responsible for setting some basic data on the scope--an array of colors; the DOM is very simple, only passing in that array to something called animated-boxes. randomColor has been moved into a service so it can be reused and tested more easily. (I also changed it a bit so it doesn't result in bad hex values.)

Now, the only part that doesn't already work is this thing called animated-boxes. Any time you want to interact with the DOM, or to trigger some behavior when an HTML attribute is used, we move to a directive.

We'll define our directive, injecting the $timeout service since we know we want to do timer-based stuff. The result of the directive will just be an object.

app.directive('animatedBoxes', function($timeout) {
  return {
  };
});

Since we want the directive to be self-contained and not mess up the outer scope in which it's contained, we'll give it an isolate scope (see the directive docs for more information, but basically this just means we have a scope that's not attached to the scope in which the directive lives except through variables we specify.)

Since we want to have access to the value passed in to the directive via the HTML attribute, we'll set up a bi-directional scope binding on that value; we'll call it colors.

app.directive('animatedBoxes', function($timeout) {
  return {
    scope: {
      colors: '=animatedBoxes'
    }
  };
});

We'll give it a simple template that loops over colors and outputs one of our divs per each color. Our ng-show indicates that the div should only be shown if the scope value selected is equal to $index, which is the array index of the current iteration of the ng-repeat loop.

app.directive('animatedBoxes', function($timeout) {
  return {
    scope: {
      colors: '=animatedBoxes'
    },
    template: "<div><div class='layer' ng-repeat='color in colors' " +
      "ng-style='{backgroundColor: color}' ng-show='selected == $index'>" +
      "</div></div>"
  };
});

Now for the link function--the function that will handle the directive's logic. First, we want keep track of which box we're showing; in our ng-show, we used selected for this. We also want to keep track of how many boxes we have; we'll use $watch on our directive's scope to keep up with this.

link: function(scope, elem, attrs) {
  scope.selected = 0;
  var count = 0;

  scope.$watch('colors', function(value) {
    // whenever the value of `colors`, which is the array
    // of colors passed into the directive, changes, update
    // our internal count of colors
    if (value) count = value.length;
    else count = 0; // if `colors` is falsy, set count to 0
  }, true); // `true` ensures we watch the values in the array,
            // not just the object reference
}

Finally, we need to cycle through each box every so often. We'll do this with $timeout, which is a version of setTimeout that includes a scope $apply call (it does some other stuff, but we don't care about that now).

var nextBox = function() {
  if (scope.selected >= count - 1) scope.selected = 0;
  else scope.selected++;
  // recursively use `$timeout` instead of `setInterval`
  $timeout(nextBox, 500);
};

// kick off the directive by launching the first `nextBox`
nextBox();

If you put the entire directive so far together, you'll end up with this code (comments removed):

app.directive('animatedBoxes', function($timeout) {
  return {
    scope: {
      colors: '=animatedBoxes'
    },
    template: "<div><div class='layer' ng-repeat='color in colors' " +
      "ng-style='{backgroundColor: color}' ng-show='selected == $index'>" +
      "</div></div>",
    link: function(scope, elem, attrs) {
      scope.selected = 0;
      var count = 0;

      scope.$watch('colors', function(value) {
        if (value) count = value.length;
        else count = 0;
      }, true);

      var nextBox = function() {
        if (scope.selected >= count - 1) scope.selected = 0;
        else scope.selected++;
        $timeout(nextBox, 500);
      };

      nextBox();
    }
  };
});

A full working example, including comments and a little debugging area where you can see the value of colors and interact with it (so you can see how the directive responds to changes in the controller) can be found here: http://jsfiddle.net/BinaryMuse/g6A6Y/

Now that you have this, consider trying to apply this knowledge to allow the directive to have a variable speed by passing it in via the DOM, like we did with colors. Here's my result: http://jsfiddle.net/BinaryMuse/cHHKn/

Upvotes: 2

Related Questions