How can I group data with an Angular filter?
Asked Answered
L

8

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
Loxodrome answered 10/2, 2013 at 18:6 Comment(0)
P
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.

Pruritus answered 22/7, 2014 at 5:28 Comment(19)
Great example. However, the key returns the group name and not the actual key... how can we solve that?Pothead
What is the "actual key" for you ? can you provide please some fiddle/jsbin example ?Pruritus
I'm trying to figure out how add checkbox for group and items, if groupd name checkbox is clicked all item under it should be selected.Lagrange
here's some quick example - jsbin.com/mazeye/1/edit?html,js,output @LagrangePruritus
I have created sample demo with checkbox feature, its kind of complex plnkr.co/edit/1fNicDw4W7hidKB6DgPa?p=preview @ArielM.Lagrange
I have updated my demo with a button to dynamically update model value plnkr.co/edit/1fNicDw4W7hidKB6DgPa?p=preview, but model change does not trigger ng change event of checkbox. How can we fix this issue?Lagrange
@lcrespofalco val.length ? see jsbin: jsbin.com/bixoqe/3/edit?html,js,outputPruritus
Don't forget to include the angular.filter module.Tradesman
@ArielM. can i use multiple values like 'team,age' if age is avalable with 'team' and 'name' in: <ul ng-repeat="(key, value) in players | groupBy: 'team'">Equation
You can pass an "angular-expression" as an argument. for example: groupBy: 'team || age'. here's some fiddle to demonstrate it. Thanks.Pruritus
So I'd like to add the very sad caveat I just learned about, which is that when you loop through object keys with ng-repeat, the object keys are output in alphabetical order, and you also can't use orderBy. Watch for this issue when using the groupBy filter, which returns the array you want to loop over as an object, with the specified property as its keys. github.com/angular/angular.js/issues/6210Sesquipedalian
you can use order-by with group-by @erfling, PTAL on: github.com/a8m/angular-filter/wiki/…Pruritus
Oh wow. Thanks. I didn't expect ordering the nested loop to affect the outer one in that way. That's really useful. +1Sesquipedalian
@ArielM. how can I group using multiple property like this https://mcmap.net/q/161669/-can-i-group-by-multiple-fields-using-angular-filter/1230188 ? Suppose I have groupId and groupName then I want to include those properties in "key", so I can access key.groupId and key.groupNameBowie
Use this lib that has a groupBy method: github.com/a8m/angular-filter. The custom method created here has problems chaining filters on the diggest loopHeadboard
@Xyroid even i'm looking for the same i want to make key as object . any luck from you sideAssembled
hi adding to your fiddle, imagine you have filter textbox to filter out list of category group. My requirment is that i couldn't able to hide groupName from UI when corresponding filtered list is empty. <p ng-hide="filteredValue.length == 0">Group name: {{ key }}</p> <li ng-repeat="player in filteredValue = value | filter: sort"> player: {{ player.name }} </li> . I want to hide groupName when value is empty. i tried ngHide no luckBriony
How can I get the real index of player in this case?Shoreline
How we can use this in angular 2+?Compulsion
K
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/

Koa answered 27/5, 2014 at 6:43 Comment(4)
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.Felicity
I think you can use the $id value to get around this: item in items track by $id(item)Puckery
why include underscore as a dep when its not nescUrethra
@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: https://mcmap.net/q/161670/-angular-filter-works-but-causes-quot-10-digest-iterations-reached-quotSaucier
L
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;
    }
}
Loxodrome answered 10/2, 2013 at 18:6 Comment(1)
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 ?Assembled
L
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);

});
Louls answered 7/4, 2013 at 16:5 Comment(2)
This answer isn't working with AngularJS > 1.1 as Promised are not unwrapped anymore for arrays. See the immigration notesLoxodrome
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).Loxodrome
F
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>
Flirt answered 23/10, 2013 at 9:23 Comment(0)
T
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.

Tidy answered 20/10, 2014 at 3:18 Comment(2)
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.Pruritus
@Tidy Having issues in Angular 1.3Urethra
V
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]'">
Vickivickie answered 10/4, 2017 at 19:34 Comment(0)
B
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

Brahe answered 4/2, 2019 at 9:37 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.