Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

AngularJS "headers" in ng-repeat

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 completely re-build the model's data as it comes 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.

like image 791
cemulate Avatar asked Jun 28 '13 19:06

cemulate


2 Answers

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.

like image 101
Ian Mercer Avatar answered Oct 30 '22 07:10

Ian Mercer


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;
          }
        });

      });
    }
  }
});
like image 1
Felix Avatar answered Oct 30 '22 08:10

Felix