141

I have a list of players which belong to a group each. How can I use a filter to list the users per group?

[{name: 'Gene', team: 'team alpha'},
 {name: 'George', team: 'team beta'},
 {name: 'Steve', team: 'team gamma'},
 {name: 'Paula', team: 'team beta'},
 {name: 'Scruath of the 5th sector', team: 'team gamma'}];

I'm looking for this result:

  • team alpha
    • Gene
  • team beta
    • George
    • Paula
  • team gamma
    • Steve
    • Scruath of the 5th sector
0

8 Answers 8

184

You can use groupBy of angular.filter module.
so you can do something like this:

JS:

$scope.players = [
  {name: 'Gene', team: 'alpha'},
  {name: 'George', team: 'beta'},
  {name: 'Steve', team: 'gamma'},
  {name: 'Paula', team: 'beta'},
  {name: 'Scruath', team: 'gamma'}
];

HTML:

<ul ng-repeat="(key, value) in players | groupBy: 'team'">
  Group name: {{ key }}
  <li ng-repeat="player in value">
    player: {{ player.name }} 
  </li>
</ul>

RESULT:
Group name: alpha
* player: Gene
Group name: beta
* player: George
* player: Paula
Group name: gamma
* player: Steve
* player: Scruath

UPDATE: jsbin Remember the basic requirements to use angular.filter, specifically note you must add it to your module's dependencies:

(1) You can install angular-filter using 4 different methods:

  1. clone & build this repository
  2. via Bower: by running $ bower install angular-filter from your terminal
  3. via npm: by running $ npm install angular-filter from your terminal
  4. via cdnjs http://www.cdnjs.com/libraries/angular-filter

(2) Include angular-filter.js (or angular-filter.min.js) in your index.html, after including Angular itself.

(3) Add 'angular.filter' to your main module's list of dependencies.

Sign up to request clarification or add additional context in comments.

19 Comments

Great example. However, the key returns the group name and not the actual key... how can we solve that?
Don't forget to include the angular.filter module.
you can use order-by with group-by @erfling, PTAL on: github.com/a8m/angular-filter/wiki/…
Oh wow. Thanks. I didn't expect ordering the nested loop to affect the outer one in that way. That's really useful. +1
@Xyroid even i'm looking for the same i want to make key as object . any luck from you side
|
25

In addition to the accepted answers above I created a generic 'groupBy' filter using the underscore.js library.

JSFiddle (updated): http://jsfiddle.net/TD7t3/

The filter

app.filter('groupBy', function() {
    return _.memoize(function(items, field) {
            return _.groupBy(items, field);
        }
    );
});

Note the 'memoize' call. This underscore method caches the result of the function and stops angular from evaluating the filter expression every time, thus preventing angular from reaching the digest iterations limit.

The html

<ul>
    <li ng-repeat="(team, players) in teamPlayers | groupBy:'team'">
        {{team}}
        <ul>
            <li ng-repeat="player in players">
                {{player.name}}
            </li>
        </ul>
    </li>
</ul>

We apply our 'groupBy' filter on the teamPlayers scope variable, on the 'team' property. Our ng-repeat receives a combination of (key, values[]) that we can use in our following iterations.

Update June 11th 2014 I expanded the group by filter to account for the use of expressions as the key (eg nested variables). The angular parse service comes in quite handy for this:

The filter (with expression support)

app.filter('groupBy', function($parse) {
    return _.memoize(function(items, field) {
        var getter = $parse(field);
        return _.groupBy(items, function(item) {
            return getter(item);
        });
    });
});

The controller (with nested objects)

app.controller('homeCtrl', function($scope) {
    var teamAlpha = {name: 'team alpha'};
    var teamBeta = {name: 'team beta'};
    var teamGamma = {name: 'team gamma'};

    $scope.teamPlayers = [{name: 'Gene', team: teamAlpha},
                      {name: 'George', team: teamBeta},
                      {name: 'Steve', team: teamGamma},
                      {name: 'Paula', team: teamBeta},
                      {name: 'Scruath of the 5th sector', team: teamGamma}];
});

The html (with sortBy expression)

<li ng-repeat="(team, players) in teamPlayers | groupBy:'team.name'">
    {{team}}
    <ul>
        <li ng-repeat="player in players">
            {{player.name}}
        </li>
    </ul>
</li>

JSFiddle: http://jsfiddle.net/k7fgB/2/

4 Comments

one thing to note with this - by default memoize uses the first param (i.e. 'items') as the cache key - so if you pass it the same 'items' with a different 'field' it will return the same cached value. Solutions welcome.
I think you can use the $id value to get around this: item in items track by $id(item)
why include underscore as a dep when its not nesc
@TomCarver You can create cache keys that include the 'field' using a resolver function as the second argument of _.memoize. This function let's you define your own cache keys. e.g. _.memoize( ... , function (items, field) { return _.pluck(items,'name').toString() + '-' + field}). This SO answer has some more detail: stackoverflow.com/a/16513030/490592
19

First do a loop using a filter that will return only unique teams, and then a nested loop that returns all players per current team:

http://jsfiddle.net/plantface/L6cQN/

html:

<div ng-app ng-controller="Main">
    <div ng-repeat="playerPerTeam in playersToFilter() | filter:filterTeams">
        <b>{{playerPerTeam.team}}</b>
        <li ng-repeat="player in players | filter:{team: playerPerTeam.team}">{{player.name}}</li>        
    </div>
</div>

script:

function Main($scope) {
    $scope.players = [{name: 'Gene', team: 'team alpha'},
                    {name: 'George', team: 'team beta'},
                    {name: 'Steve', team: 'team gamma'},
                    {name: 'Paula', team: 'team beta'},
                    {name: 'Scruath of the 5th sector', team: 'team gamma'}];

    var indexedTeams = [];

    // this will reset the list of indexed teams each time the list is rendered again
    $scope.playersToFilter = function() {
        indexedTeams = [];
        return $scope.players;
    }

    $scope.filterTeams = function(player) {
        var teamIsNew = indexedTeams.indexOf(player.team) == -1;
        if (teamIsNew) {
            indexedTeams.push(player.team);
        }
        return teamIsNew;
    }
}

1 Comment

just brilliant . but what if i want to push a new object to $scope.players on click ? as u are looping through a function will it get added ?
16

I originally used Plantface's answer, but I didn't like how the syntax looked in my view.

I reworked it to use $q.defer to post-process the data and return a list on unique teams, which is then uses as the filter.

http://plnkr.co/edit/waWv1donzEMdsNMlMHBa?p=preview

View

<ul>
  <li ng-repeat="team in teams">{{team}}
    <ul>
      <li ng-repeat="player in players | filter: {team: team}">{{player.name}}</li> 
    </ul>
  </li>
</ul>

Controller

app.controller('MainCtrl', function($scope, $q) {

  $scope.players = []; // omitted from SO for brevity

  // create a deferred object to be resolved later
  var teamsDeferred = $q.defer();

  // return a promise. The promise says, "I promise that I'll give you your
  // data as soon as I have it (which is when I am resolved)".
  $scope.teams = teamsDeferred.promise;

  // create a list of unique teams. unique() definition omitted from SO for brevity
  var uniqueTeams = unique($scope.players, 'team');

  // resolve the deferred object with the unique teams
  // this will trigger an update on the view
  teamsDeferred.resolve(uniqueTeams);

});

2 Comments

This answer isn't working with AngularJS > 1.1 as Promised are not unwrapped anymore for arrays. See the immigration notes
There is no need for the Promise in this solution, as you are not doing anything asynchronously. In this case, you can simply skip that step (jsFiddle).
11

Both answers were good so I moved them in to a directive so that it is reusable and a second scope variable doesn't have to be defined.

Here is the fiddle if you want to see it implemented

Below is the directive:

var uniqueItems = function (data, key) {
    var result = [];
    for (var i = 0; i < data.length; i++) {
        var value = data[i][key];
        if (result.indexOf(value) == -1) {
            result.push(value);
        }
    }
    return result;
};

myApp.filter('groupBy',
            function () {
                return function (collection, key) {
                    if (collection === null) return;
                    return uniqueItems(collection, key);
        };
    });

Then it can be used as follows:

<div ng-repeat="team in players|groupBy:'team'">
    <b>{{team}}</b>
    <li ng-repeat="player in players | filter: {team: team}">{{player.name}}</li>        
</div>

Comments

11

Update

I initially wrote this answer because the old version of the solution suggested by Ariel M. when combined with other $filters triggered an "Infite $diggest Loop Error" (infdig). Fortunately this issue has been solved in the latest version of the angular.filter.

I suggested the following implementation, that didn't have that issue:

angular.module("sbrpr.filters", [])
.filter('groupBy', function () {
  var results={};
    return function (data, key) {
        if (!(data && key)) return;
        var result;
        if(!this.$id){
            result={};
        }else{
            var scopeId = this.$id;
            if(!results[scopeId]){
                results[scopeId]={};
                this.$on("$destroy", function() {
                    delete results[scopeId];
                });
            }
            result = results[scopeId];
        }

        for(var groupKey in result)
          result[groupKey].splice(0,result[groupKey].length);

        for (var i=0; i<data.length; i++) {
            if (!result[data[i][key]])
                result[data[i][key]]=[];
            result[data[i][key]].push(data[i]);
        }

        var keys = Object.keys(result);
        for(var k=0; k<keys.length; k++){
          if(result[keys[k]].length===0)
            delete result[keys[k]];
        }
        return result;
    };
});

However, this implementation will only work with versions prior to Angular 1.3. (I will update this answer shortly providing a solution that works with all versions.)

I've actually wrote a post about the steps that I took to develop this $filter, the problems that I encountered and the things that I learned from it.

2 Comments

Hi @Josep, take a look on the new angular-filter version - 0.5.0, there's no more exception. groupBy can be chain with any filter. also, you're great test cases finish successfully - here's a plunker Thanks.
@Josep Having issues in Angular 1.3
3

In addition to the accepted answer you can use this if you want to group by multiple columns:

<ul ng-repeat="(key, value) in players | groupBy: '[team,name]'">

Comments

0

If you need that in js code. You can use injected method of angula-filter lib. Like this.

function controller($scope, $http, groupByFilter) {     

   var groupedData = groupByFilter(originalArray, 'groupPropName');

}

https://github.com/a8m/angular-filter/wiki/Common-Questions#inject-filters

Comments

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.