Rectangle 27 0

javascript How can I group data with an Angular filter?


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

@Josep Having issues in Angular 1.3

@downvoter could you please explain the down-vote? Thanks!

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.

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 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.

Note
Rectangle 27 0

javascript How can I group data with an Angular filter?


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

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:

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 ?

Note
Rectangle 27 0

javascript How can I group data with an Angular filter?


$scope.players = [
  {name: 'Gene', team: 'alpha'},
  {name: 'George', team: 'beta'},
  {name: 'Steve', team: 'gamma'},
  {name: 'Paula', team: 'beta'},
  {name: 'Scruath', team: 'gamma'}
];
<ul ng-repeat="(key, value) in players | groupBy: 'team'">
  Group name: {{ key }}
  <li ng-repeat="player in value">
    player: {{ player.name }} 
  </li>
</ul>
angular.filter

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

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

@ArielM. hey! sorry that it took me so long to answer, I've been very busy these last days. Yep, you are right! in Angular 1.3 the this keyword inside the $filter returned function is the window. So, the code of my solution won't work for Anbular 1.3, I will have to update my answer and my blog. Thanks for that. I've also noticed that you have changed the technique for stabilizing your groupBy filter, now you're using memoizing the results. That should solve all the issues. Good job! I will update my answer in the next hours/days. Thanks for your feedback!

@ArielM. sure, no problem! Please have a look at these tests that I've written, they could help you to debug this issue. Thanks!

@Josep Thx, I think I gonna refactor the watcherService and move from timeout to queue implementation.(more smarter, stable, etc.. )

I like this solution a lot. Any chance we can run into the digest issue mentioned elsewhere (where _.memoize was used)?

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:

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

Note
Rectangle 27 0

javascript How can I group data with an Angular filter?


<ul>
  <li ng-repeat="team in teams">{{team}}
    <ul>
      <li ng-repeat="player in players | filter: {team: team}">{{player.name}}</li> 
    </ul>
  </li>
</ul>
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);

});

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.

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).

This answer isn't working with AngularJS > 1.1 as Promised are not unwrapped anymore for arrays. See the immigration notes

Note
Rectangle 27 0

javascript How can I group data with an Angular filter?


<li ng-repeat="(team, players) in teamPlayers | groupBy:'team.name'">
    {{team}}
    <ul>
        <li ng-repeat="player in players">
            {{player.name}}
        </li>
    </ul>
</li>
<ul>
    <li ng-repeat="(team, players) in teamPlayers | groupBy:'team'">
        {{team}}
        <ul>
            <li ng-repeat="player in players">
                {{player.name}}
            </li>
        </ul>
    </li>
</ul>
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}];
});
app.filter('groupBy', function($parse) {
    return _.memoize(function(items, field) {
        var getter = $parse(field);
        return _.groupBy(items, function(item) {
            return getter(item);
        });
    });
});
app.filter('groupBy', function() {
    return _.memoize(function(items, field) {
            return _.groupBy(items, field);
        }
    );
});

The filter (with expression support)

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

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.

This is pretty neat actually! Least amount of code.

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:

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.

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.

Note
Rectangle 27 0

javascript How can I group data with an Angular filter?


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

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

Then it can be used as follows:

Which "both" answers? There are many now.

Note
Rectangle 27 0

javascript How can I group data with an Angular filter?


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

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:

Note
Rectangle 27 0

javascript How can I group data with an Angular filter?


$scope.players = [
  {name: 'Gene', team: 'alpha'},
  {name: 'George', team: 'beta'},
  {name: 'Steve', team: 'gamma'},
  {name: 'Paula', team: 'beta'},
  {name: 'Scruath', team: 'gamma'}
];
<ul ng-repeat="(key, value) in players | groupBy: 'team'">
  Group name: {{ key }}
  <li ng-repeat="player in value">
    player: {{ player.name }} 
  </li>
</ul>

@ArielM. hey! sorry that it took me so long to answer, I've been very busy these last days. Yep, you are right! in Angular 1.3 the this keyword inside the $filter returned function is the window. So, the code of my solution won't work for Anbular 1.3, I will have to update my answer and my blog. Thanks for that. I've also noticed that you have changed the technique for stabilizing your groupBy filter, now you're using memoizing the results. That should solve all the issues. Good job! I will update my answer in the next hours/days. Thanks for your feedback!

@ArielM. sure, no problem! Please have a look at these tests that I've written, they could help you to debug this issue. Thanks!

@Josep Thx, I think I gonna refactor the watcherService and move from timeout to queue implementation.(more smarter, stable, etc.. )

@Plantface Angular compels you to not change the model in the view... so it's causes some problems with couple filters, in angular-filter I used an helper watcher that solve this issue, and actually groupBy and the others works pretty fine and doesn't trigger the infinite digest loop

I like this solution a lot. Any chance we can run into the digest issue mentioned elsewhere (where _.memoize was used)?

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

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

Note
Rectangle 27 0

javascript How can I group data with an Angular filter?


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

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

Then it can be used as follows:

Which "both" answers? There are many now.

Note
Rectangle 27 0

javascript How can I group data with an Angular filter?


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

@Josep Having issues in Angular 1.3

@downvoter could you please explain the down-vote? Thanks!

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.

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 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.

Note