I have a problem that's cropped up in more cases than one lately, and I'm wondering the best way to go about this. To state it simply:
I have data being displayed in an ng-repeat, sorted by a particular item. Say it's sorted by Name, for example. My goal is to have headers at the letter breaks in the alphabetized list:
----A----
Abe Lincoln
Adam Smith
----B----
Barack Obama
Barry Zuckercorn
----C----
...
and so on.
Things I've tried include:
- Having the controller pletely re-build the model's data as it es in, manually putting it into an array of letter groups. For example, my service has an array of "posts" and my controller, as the service updates, manually shuffles those "posts" into an array of "letterGroups". Then it's possible to just have two nested ng-repeats
However, this relies on heavy manipulation of the data, and it's not easy to change what the data is sorted by on the fly.
- When the model updates, have the controller "dope" the data by manually stepping through it, checking when the letter changes, and piggybacking a "header" property on that element (
post.header = true
). Then the ng-repeat can check if the element it's currently on is a header, and use ng-if to insert something else into the DOM.
This feels slightly cleaner but because of the way ng-repeat works, the header element has to be "included" at the same level as the ng-repeating element. For example, if you do the ng-repeat on <tr>
elements, that would mean it would be impossible to insert another <tr>
for the header, since the special element has to occur inside the <tr>
And finally, along the same lines as above:
- Have the controller maintain a list of "key indexes" -- this has the advantage of not modifying the data like the second method above, but works generally the same way.
EDIT:
I've written a blog post here explaining how I eventually handled this issue - thanks to the Google Groups discussion linked by Ian's answer below.
I have a problem that's cropped up in more cases than one lately, and I'm wondering the best way to go about this. To state it simply:
I have data being displayed in an ng-repeat, sorted by a particular item. Say it's sorted by Name, for example. My goal is to have headers at the letter breaks in the alphabetized list:
----A----
Abe Lincoln
Adam Smith
----B----
Barack Obama
Barry Zuckercorn
----C----
...
and so on.
Things I've tried include:
- Having the controller pletely re-build the model's data as it es in, manually putting it into an array of letter groups. For example, my service has an array of "posts" and my controller, as the service updates, manually shuffles those "posts" into an array of "letterGroups". Then it's possible to just have two nested ng-repeats
However, this relies on heavy manipulation of the data, and it's not easy to change what the data is sorted by on the fly.
- When the model updates, have the controller "dope" the data by manually stepping through it, checking when the letter changes, and piggybacking a "header" property on that element (
post.header = true
). Then the ng-repeat can check if the element it's currently on is a header, and use ng-if to insert something else into the DOM.
This feels slightly cleaner but because of the way ng-repeat works, the header element has to be "included" at the same level as the ng-repeating element. For example, if you do the ng-repeat on <tr>
elements, that would mean it would be impossible to insert another <tr>
for the header, since the special element has to occur inside the <tr>
And finally, along the same lines as above:
- Have the controller maintain a list of "key indexes" -- this has the advantage of not modifying the data like the second method above, but works generally the same way.
EDIT:
I've written a blog post here explaining how I eventually handled this issue - thanks to the Google Groups discussion linked by Ian's answer below.
Share Improve this question edited Jul 8, 2013 at 2:51 cemulate asked Jun 28, 2013 at 19:27 cemulatecemulate 2,3331 gold badge34 silver badges49 bronze badges2 Answers
Reset to default 5Create an Angular filter. It can accept the sorted list, chunk it by first letter and return an object for each chunk with the letter and an array of objects that start with that letter.
See for example the chunk by size filter here. Note in particular the discussion around creating hash values for your chunked values.
I wrote a small directive that watches the objects and automatically adds a $header property to the first one in the list. Use it like this:
<div ng-repeat="item in data.items | orderBy:... | filter:... as filteredItems" header-list objects="filteredItems" header="getHeader">
<div ng-show="item.$header">{{item.$header}}</div>
<div>Your normal content</div>
</div>
You can pass a function that build the header as you wish. The function can be an instance method or function in your scope. To get the first letter, define in your controller.
$scope.getHeader = function(item) {
return item.name.slice(0, 1);
},
The list will automatically update when filters and orders are changed.
app.module("...").directive('headerList', function () {
return {
restrict: 'A',
scope: {
objects: '=',
header: '=' // This must be a function or instance method
},
link: function(scope) {
scope.$watch('objects', function() {
var lastHeader, currentHeader;
scope.objects.forEach(function (obj) {
// We pass obj twice, so it can be used is instance method or regular function that takes the object as argument.
currentHeader = scope.header.call(obj, obj);
// in order to display a header per type, we mark the first event of each kind as header
if (currentHeader !== lastHeader) {
obj.$header = currentHeader;
lastHeader = currentHeader;
} else {
obj.$header = null;
}
});
});
}
}
});