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:
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.
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:
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.
Create 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;
}
});
});
}
}
});
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With