In the preceding chapter, Scopes, we learned that Angular creates a new scope each time you invoke a controller constructor via ng-controller. There are other cases in which Angular creates new scopes, and perhaps the most common is when working with collections of similar objects. Angular, unlike Backbone, does not have a component named Collection. However, its extensive support for working with collections of like objects merits a chapter, as you will see.

Set up

In addition to loading Angular from Google Hosted Libraries, the live examples in this page also load the Bootstrap styles for better-looking tables and lists.

<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.6/css/bootstrap.min.css">
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.5.5/angular.js"></script>

We then load our app module by passing its name to the ng-app directive. Our choice of the name app is a convention, but not otherwise significant.

<body ng-app="app">
  <!-- Other examples to be inserted here. -->
</body>

As explained earlier, a bit of boilerplate configuration is necessary to avoid creating the controller examples in this chapter using modules, which will be covered later in this book.

angular.module('app', []);
angular.module('app').config(['$controllerProvider', function($controllerProvider) {
  $controllerProvider.allowGlobals();
}]);

We are ready to proceed with our interactive exploration of collections, iteration, and Angular.

Iteration

In regular JavaScript, when you iterate over a collection of similar objects in a for loop, you might declare a local variable to reference the current element. For example:

var items = [{name: "Item 1"},{name: "Item 2"}];
for (var i = 0; i < items.length; i++) {
  var item = items[i];
}
document.body.innerHTML = item.name;

While you might like to believe (hope? wish? beg?) that the item variable in the example exists within a lexical scope that JavaScript creates for each iteration of the for loop, it doesn't, as demonstrated by the final line in the example. The item property is available outside of the loop! For further reading on this topic, please see Mozilla's JavaScript Guide.

With its built-in support for iteration, Angular avoids this pitfall. To see how, let's first move the items array shown above inside a controller.

function ItemsController($scope) {
  $scope.items = [
    {name: 'Item 1', price: 1.99},
    {name: 'Item 2', price: 2.99}
  ];
}

Imagine that items is a collection of unknown length, and that we need to iterate over its members, displaying the name property of each.

ng-repeat

Just as Angular creates a top-level Angular scope for expressions to protect us from creating variables on JavaScript's global scope, the built-in ng-repeat directive protects us from the situation shown in the first example, by creating an Angular scope for each iteration of the loop.

<ol ng-controller="ItemsController">
  <li ng-repeat="item in items" ng-bind="item.name"></li>
  <li ng-bind="item.name"></li>
</ol>

As you can see in this example, the item property is not available outside of the ng-repeat loop.

For each element in the comprehension passed to it, ng-repeat creates a new child scope with a property as specified in the comprehension. In this case, the property name is item, but it could be anything. Edit the example above, changing the usages of item to something different, like i or x.

Object properties

The (key, value) in object syntax lets you loop over the properties of an object. This is handy if you need to write the entire contents of an object to your view.

<table class="table table-condensed">
  <tr ng-repeat="(propertyName, propertyValue) in {b: 'two', a: 1.0, c: 3}">
    <td ng-bind="propertyName"></td>
    <td ng-bind="propertyValue"></td>
  </tr>
</table>

What if you want to extract just one specific, named property value from each object in a collection?

The item in items syntax that we used in our first ng-repeat example looks a lot like a list comprehension, but unfortunately ng-repeat does not permit returning anything other than the members of the object or array on the right-hand side. Let's try it anyway.

<ol ng-controller="ItemsController">
  <!-- Invalid code! Syntax error, because 'for' is not supported! -->
  <li ng-repeat="item.name for item in items" ng-bind="item.name"></li>
</ol>

It doesn't work. True list comprehensions let you return anything you want from the original enumeration, typically with the use of a for keyword. Python provides a nice example.

$index

In addition to the property holding the element, ng-repeat sets the index for the current element on the created scope as a special property, $index. If, for example, we decide to re-invent the HTML ordered list, we can render $index in the output.

<div ng-controller="ItemsController"> <div ng-repeat="item in items"> {{$index + 1}}. {{item.name}} </div> </div>

Let's try nesting usages of ng-repeat. First, let's create a more complex data model in which each top-level member contains an array of children.

function ItemsController($scope) {
  $scope.items = [
    {name: 'Item 1',
      items: [
       {name: 'Nested Item 1.1'},
       {name: 'Nested Item 1.2'}
      ]
    },
    {name: 'Item 2',
      items: [
       {name: 'Nested Item 2.1'},
       {name: 'Nested Item 2.2'}
      ]
    }
  ];
}

Now, let's nest an inner loop within the outer one. It will do nearly the same thing, adding the name of each item to an ordered list.

<div ng-controller="ItemsController"> <ol> <li ng-repeat="item in items"> {{item.name}} <ol> <li ng-repeat="item in item.items"> {{item.name}} </li> </ol> </li> </ol> </div>

What if we want outline-style nested counters? How do we prevent the $index property set by the outer loop from being shadowed by the property of the same name set by the inner loop?

ng-init

You may remember from the first chapter, Basics, that Angular allows us to initialize scope properties in the view using the ng-init directive. The solution to our current issue is to use ng-init to re-assign, or alias, the outer loop's $index before it is shadowed.

<div ng-controller="ItemsController"> <div ng-repeat="item in items" ng-init="outerCount = $index"> {{outerCount + 1}}. {{item.name}} <div ng-repeat="item in item.items"> {{outerCount + 1}}.{{$index + 1}}. {{item.name}} </div> </div> </div>

In addition to $index, ng-repeat sets a number of boolean properties on each iteration of the loop: $first, $middle, $last, $even, and $odd. You can try each of these in the example below, which uses the helpful ng-class directive to apply a green label when the conditional expression is true. Can you figure out a way to apply the label-success class to just the first and last elements? (Hint: It only requires adding the ! operator.)

<ol> <li ng-repeat="val in [1,2,3,4,5]"> <span class="label label-default" ng-class="{'label-success': $middle}"> {{val}} </span> </li> </ol>

Did you notice something when you applied styling using the $even and $odd properties? Angular sets the properties using the traditional zero-based index of a for loop. This is a sound choice overall, but it is at odds (pardon the pun) with the one-based count of the ordered list.

Uniqueness

As a side note, one thing to be careful of when you use primitives with ng-repeat is the directive's requirement that each element be unique, as defined by JavaScript's strict equality operator.

Strict equality

Let's run a few experiments to refresh our knowledge of how JavaScript's strict equality operator (===) defines equality. The template below tests equality for numbers, strings and objects.

<table class="table table-condensed"> <tr> <td>1 === 1</td> <td>{{1 === 1}}</td> </tr> <tr> <td>'1' === '1'</td> <td>{{'1' === '1'}}</td> </tr> <tr> <td>1 === '1'</td> <td>{{1 === '1'}}</td> </tr> <tr> <td>{} === {}</td> <td>{{ {} === {} }}</td> </tr> <tr> <td>{name: 1} === {name: 1}</td> <td>{{ {name: 1} === {name: 1} }}</td> </tr> </table>

Since equality is not permitted in a collection by default, the following usage of ng-repeat results in a duplicate element error because the value 1 is repeated.

<ol> <!-- Invalid code! Duplicate element error, because '1' is repeated! --> <li ng-repeat="val in [1,2,1]" ng-bind="val"></li> </ol>

Modify the example above to use a mixture of numbers and a string instead of just numbers by pasting in the following array: [1,2,'1']. What is the result? Next, change it to use objects instead of numbers, by pasting in the following array: [{name: 1},{name: 2},{name: 1}]. You will need to change ng-bind="val" to ng-bind="val.name" to see the values.

track by

The solution to the problem with the array of numbers above is to add a track by instruction to your ng-repeat expression in order to override the default equality test of the elements. You can track collection elements by any unique property. If you don't have a unique property (our primitive numeric values certainly don't), you can use $index.

<ol> <li ng-repeat="val in [1,2,1] track by $index" ng-bind="val"></li> </ol>

When possible, you should always use a unique property from your model objects, such as a unique id created by either the backend data store or some sort of client-side UUID generator. If you must use $index, be aware that changes to the collection may cause problems with DOM events.

Callback functions

Angular also makes it easy to get a reference to a collection element that you can use inside your controller. You simply pass the collection element property to the callback function in a directive that accepts one, such as ng-click.

It is very easy to wire up a user action for deleting elements from the collection, for example. On the controller, we need to define a callback function that accepts the element reference as its only parameter. This callback can be named anything, but destroy is a good choice.

function ItemsController($scope) {
  $scope.items = [
    {id: 1, name: 'Item 1'},
    {id: 2, name: 'Item 2'},
    {id: 3, name: 'Item 3'},
    {id: 4, name: 'Item 4'}
  ];

  $scope.destroy = function(item) {
    var index = $scope.items.indexOf(item);
    $scope.items.splice(index, 1);
  };
}

The code within our destroy callback is a bit obtuse, although not through any fault of Angular. It is merely the awkward syntax required by JavaScript to remove an array element by reference.

All that's left to do is to add ng-click="destroy(item)" to an element within the loop. Semantically, a button is probably the best choice, but with ng-click you can actually use any clickable element.

<div ng-controller="ItemsController">
  <h4 ng-pluralize count="items.length"
      when="{'one': '1 item', 'other': '{} items'}">
  </h4>
  <table class="table table-condensed">
    <tr ng-repeat="item in items">
      <td ng-bind="item.name"></td>
      <td>
        <button class="btn btn-xs btn-default" ng-click="destroy(item)">
          destroy
        </button>
      </td>
    </tr>
  </table>
</div>

The wiring of the destroy callback is a shining example of Angular's declarative approach. As a bonus, the example also demonstrates how to use ng-pluralize to conditionally pluralize a label for the number of items in a collection. The configuration required by ng-pluralize is a bit muddier, and shows the tradeoffs.

-start and -end

Although it's not common, you may need to render sibling elements for each member of your collection. An example of this is the description list, or dl element, which is populated with pairs of dt and dd elements. This poses a problem for directives like ng-repeat, which are designed to be applied to a single element. The solution is to extend the directive invocation across multiple elements with special -start and -end suffixes.

<dl ng-controller="ItemsController">
  <dt ng-repeat-start="item in items">name</dt>
  <dd ng-bind="item.name"></dd>
  <dt>price</dt>
  <dd ng-repeat-end ng-bind="item.price"></dd>
</dl>

These suffixes are not limited to ng-repeat and can be applied to other directives. When creating your own custom directives (as covered in the upcoming chapter Directives), be sure not to use names that end in either -start or -end.

Conclusion

The built-in support for collections that Angular provides via ng-repeat is both flexible and powerful, enabling us to quickly build typical user interfaces for CRUD applications. This chapter completes our survey of the rich functionality for web development that we can leverage without learning too much about Angular's internals. From this point forward, we will be delving deeper into extending Angular, and in order to do so we need to learn how Angular manages components. The next chapter covers Angular's home-grown module system.

I hope you have been enjoying Angular Basics.
You can join the mailing list to hear about updates to the book.
Please also let me know what you liked and what I can improve.
And please share the word using the social buttons below!