The ngRepeat
directive instantiates a template once per item from a collection. Each template
instance gets its own scope, where the given loop variable is set to the current collection item,
and $index
is set to the item index or key.
Special properties are exposed on the local scope of each template instance, including:
Variable | Type | Details |
---|---|---|
$index |
number | iterator offset of the repeated element (0..length-1) |
$first |
boolean | true if the repeated element is first in the iterator. |
$middle |
boolean | true if the repeated element is between the first and last in the iterator. |
$last |
boolean | true if the repeated element is last in the iterator. |
$even |
boolean | true if the iterator position $index is even (otherwise false). |
$odd |
boolean | true if the iterator position $index is odd (otherwise false). |
ngInit
.
This may be useful when, for instance, nesting ngRepeats.
It is possible to get ngRepeat
to iterate over the properties of an object using the following
syntax:
<div ng-repeat="(key, value) in myObj"> ... </div>
However, there are a few limitations compared to array iteration:
The JavaScript specification does not define the order of keys
returned for an object, so AngularJS relies on the order returned by the browser
when running for key in myObj
. Browsers generally follow the strategy of providing
keys in the order in which they were defined, although there are exceptions when keys are deleted
and reinstated. See the
MDN page on delete
for more info.
ngRepeat
will silently ignore object keys starting with $
, because
it's a prefix used by AngularJS for public ($
) and private ($$
) properties.
The built-in filters orderBy and filter do not work with objects, and will throw an error if used with one.
If you are hitting any of these limitations, the recommended workaround is to convert your object into an array
that is sorted into the order that you prefer before providing it to ngRepeat
. You could
do this with a filter such as toArrayFilter
or implement a $watch
on the object yourself.
ngRepeat
uses $watchCollection to detect changes in
the collection. When a change happens, ngRepeat
then makes the corresponding changes to the DOM:
To minimize creation of DOM elements, ngRepeat
uses a function
to "keep track" of all items in the collection and their corresponding DOM elements.
For example, if an item is added to the collection, ngRepeat
will know that all other items
already have DOM elements, and will not re-render them.
All different types of tracking functions, their syntax, and their support for duplicate items in collections can be found in the ngRepeat expression description.
item in items track by item.id
.
Should you reload your data later, ngRepeat
will not have to rebuild the DOM elements for items
it has already rendered, even if the JavaScript objects in the collection have been substituted
for new ones. For large collections, this significantly improves rendering performance.
When DOM elements are re-used, ngRepeat updates the scope for the element, which will automatically update any active bindings on the template. However, other functionality will not be updated, because the element is not re-created:
The above affects all kinds of element re-use due to tracking, but may be especially visible
when tracking by $index
due to the way ngRepeat re-uses elements.
The following example shows the effects of different actions with tracking:
angular.module('ngRepeat', ['ngAnimate']).controller('repeatController', function($scope) {
var friends = [
{name:'John', age:25},
{name:'Mary', age:40},
{name:'Peter', age:85}
];
$scope.removeFirst = function() {
$scope.friends.shift();
};
$scope.updateAge = function() {
$scope.friends.forEach(function(el) {
el.age = el.age + 5;
});
};
$scope.copy = function() {
$scope.friends = angular.copy($scope.friends);
};
$scope.reset = function() {
$scope.friends = angular.copy(friends);
};
$scope.reset();
});
<div ng-controller="repeatController">
<ol>
<li>When you click "Update Age", only the first list updates the age, because all others have
a one-time binding on the age property. If you then click "Copy", the current friend list
is copied, and now the second list updates the age, because the identity of the collection items
has changed and the list must be re-rendered. The 3rd and 4th list stay the same, because all the
items are already known according to their tracking functions.
</li>
<li>When you click "Remove First", the 4th list has the wrong age on both remaining items. This is
due to tracking by $index: when the first collection item is removed, ngRepeat reuses the first
DOM element for the new first collection item, and so on. Since the age property is one-time
bound, the value remains from the collection item which was previously at this index.
</li>
</ol>
<button ng-click="removeFirst()">Remove First</button>
<button ng-click="updateAge()">Update Age</button>
<button ng-click="copy()">Copy</button>
<br><button ng-click="reset()">Reset List</button>
<br>
<code>track by $id(friend)</code> (default):
<ul class="example-animate-container">
<li class="animate-repeat" ng-repeat="friend in friends">
{{friend.name}} is {{friend.age}} years old.
</li>
</ul>
<code>track by $id(friend)</code> (default), with age one-time binding:
<ul class="example-animate-container">
<li class="animate-repeat" ng-repeat="friend in friends">
{{friend.name}} is {{::friend.age}} years old.
</li>
</ul>
<code>track by friend.name</code>, with age one-time binding:
<ul class="example-animate-container">
<li class="animate-repeat" ng-repeat="friend in friends track by friend.name">
{{friend.name}} is {{::friend.age}} years old.
</li>
</ul>
<code>track by $index</code>, with age one-time binding:
<ul class="example-animate-container">
<li class="animate-repeat" ng-repeat="friend in friends track by $index">
{{friend.name}} is {{::friend.age}} years old.
</li>
</ul>
</div>
.example-animate-container {
background:white;
border:1px solid black;
list-style:none;
margin:0;
padding:0 10px;
}
.animate-repeat {
line-height:30px;
list-style:none;
box-sizing:border-box;
}
.animate-repeat.ng-move,
.animate-repeat.ng-enter,
.animate-repeat.ng-leave {
transition:all linear 0.5s;
}
.animate-repeat.ng-leave.ng-leave-active,
.animate-repeat.ng-move,
.animate-repeat.ng-enter {
opacity:0;
max-height:0;
}
.animate-repeat.ng-leave,
.animate-repeat.ng-move.ng-move-active,
.animate-repeat.ng-enter.ng-enter-active {
opacity:1;
max-height:30px;
}
To repeat a series of elements instead of just one parent element, ngRepeat (as well as other ng directives) supports extending the range of the repeater by defining explicit start and end points by using ng-repeat-start and ng-repeat-end respectively. The ng-repeat-start directive works the same as ng-repeat, but will repeat all the HTML code (including the tag it's defined on) up to and including the ending HTML tag where ng-repeat-end is placed.
The example below makes use of this feature:
<header ng-repeat-start="item in items">
Header {{ item }}
</header>
<div class="body">
Body {{ item }}
</div>
<footer ng-repeat-end>
Footer {{ item }}
</footer>
And with an input of ['A','B'] for the items variable in the example above, the output will evaluate to:
<header>
Header A
</header>
<div class="body">
Body A
</div>
<footer>
Footer A
</footer>
<header>
Header B
</header>
<div class="body">
Body B
</div>
<footer>
Footer B
</footer>
The custom start and end points for ngRepeat also support all other HTML directive syntax flavors provided in AngularJS (such as data-ng-repeat-start, x-ng-repeat-start and ng:repeat-start).
<ANY
ng-repeat="repeat_expression">
...
</ANY>
Param | Type | Details |
---|---|---|
ngRepeat | repeat_expression |
The expression indicating how to enumerate a collection. These formats are currently supported:
|
Animation | Occurs |
---|---|
enter | when a new item is added to the list or when an item is revealed after a filter |
leave | when an item is removed from the list or when an item is filtered out |
move | when an adjacent item is filtered out causing a reorder or when the item contents are reordered |
See the example below for defining CSS animations with ngRepeat.
Click here to learn more about the steps involved in the animation.This example uses ngRepeat
to display a list of people. A filter is used to restrict the displayed
results by name or by age. New (entering) and removed (leaving) items are animated.
<div ng-controller="repeatController">
I have {{friends.length}} friends. They are:
<input type="search" ng-model="q" placeholder="filter friends..." aria-label="filter friends" />
<ul class="example-animate-container">
<li class="animate-repeat" ng-repeat="friend in friends | filter:q as results track by friend.name">
[{{$index + 1}}] {{friend.name}} who is {{friend.age}} years old.
</li>
<li class="animate-repeat" ng-if="results.length === 0">
<strong>No results found...</strong>
</li>
</ul>
</div>
angular.module('ngRepeat', ['ngAnimate']).controller('repeatController', function($scope) {
$scope.friends = [
{name:'John', age:25, gender:'boy'},
{name:'Jessie', age:30, gender:'girl'},
{name:'Johanna', age:28, gender:'girl'},
{name:'Joy', age:15, gender:'girl'},
{name:'Mary', age:28, gender:'girl'},
{name:'Peter', age:95, gender:'boy'},
{name:'Sebastian', age:50, gender:'boy'},
{name:'Erika', age:27, gender:'girl'},
{name:'Patrick', age:40, gender:'boy'},
{name:'Samantha', age:60, gender:'girl'}
];
});
.example-animate-container {
background:white;
border:1px solid black;
list-style:none;
margin:0;
padding:0 10px;
}
.animate-repeat {
line-height:30px;
list-style:none;
box-sizing:border-box;
}
.animate-repeat.ng-move,
.animate-repeat.ng-enter,
.animate-repeat.ng-leave {
transition:all linear 0.5s;
}
.animate-repeat.ng-leave.ng-leave-active,
.animate-repeat.ng-move,
.animate-repeat.ng-enter {
opacity:0;
max-height:0;
}
.animate-repeat.ng-leave,
.animate-repeat.ng-move.ng-move-active,
.animate-repeat.ng-enter.ng-enter-active {
opacity:1;
max-height:30px;
}
var friends = element.all(by.repeater('friend in friends'));
it('should render initial data set', function() {
expect(friends.count()).toBe(10);
expect(friends.get(0).getText()).toEqual('[1] John who is 25 years old.');
expect(friends.get(1).getText()).toEqual('[2] Jessie who is 30 years old.');
expect(friends.last().getText()).toEqual('[10] Samantha who is 60 years old.');
expect(element(by.binding('friends.length')).getText())
.toMatch("I have 10 friends. They are:");
});
it('should update repeater when filter predicate changes', function() {
expect(friends.count()).toBe(10);
element(by.model('q')).sendKeys('ma');
expect(friends.count()).toBe(2);
expect(friends.get(0).getText()).toEqual('[1] Mary who is 28 years old.');
expect(friends.last().getText()).toEqual('[2] Samantha who is 60 years old.');
});