Comment puis-je regrouper des données avec un filtre angulaire?


136

J'ai une liste de joueurs qui appartiennent chacun à un groupe. Comment puis-je utiliser un filtre pour répertorier les utilisateurs par groupe?

[{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'}];

Je recherche ce résultat:

  • équipe alpha
    • Gène
  • bêta d'équipe
    • George
    • Paula
  • gamma d'équipe
    • Steve
    • Scruath du 5ème secteur

Réponses:


182

Vous pouvez utiliser groupBy du module angular.filter .
afin que vous puissiez faire quelque chose comme ceci:

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>

RÉSULTAT:
Nom du groupe: alpha
* joueur: Gene
Nom du groupe: beta
* joueur: George
* joueur: Paula
Nom du groupe: gamma
* joueur: Steve
* joueur: Scruath

UPDATE: jsbin Rappelez-vous les exigences de base à utiliser angular.filter, notez en particulier que vous devez l'ajouter aux dépendances de votre module:

(1) Vous pouvez installer un filtre angulaire en utilisant 4 méthodes différentes:

  1. cloner et construire ce référentiel
  2. via Bower: en exécutant $ bower install angular-filter depuis votre terminal
  3. via npm: en exécutant $ npm, installez angular-filter depuis votre terminal
  4. via cdnjs http://www.cdnjs.com/libraries/angular-filter

(2) Incluez angular-filter.js (ou angular-filter.min.js) dans votre index.html, après avoir inclus Angular lui-même.

(3) Ajoutez 'angular.filter' à la liste des dépendances de votre module principal.


Excellent exemple. Cependant, la clé renvoie le nom du groupe et non la clé réelle ... comment pouvons-nous résoudre cela?
JohnAndrews

7
N'oubliez pas d'inclure le angular.filtermodule.
Puce

1
vous pouvez utiliser order-by avec group-by @erfling, PTAL sur: github.com/a8m/angular-filter/wiki/…
a8m

1
Oh wow. Merci. Je ne m'attendais pas à ce que l'ordre de la boucle imbriquée affecte la boucle externe de cette manière. C'est vraiment utile. +1
erfling le

1
@Xyroid même si je cherche le même que je veux faire keycomme objet. aucune chance de votre part
super cool

25

En plus des réponses acceptées ci-dessus, j'ai créé un filtre générique 'groupBy' en utilisant la bibliothèque underscore.js.

JSFiddle (mis à jour): http://jsfiddle.net/TD7t3/

Le filtre

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

Notez l'appel «mémoize». Cette méthode de soulignement met en cache le résultat de la fonction et empêche angular d'évaluer l'expression du filtre à chaque fois, empêchant ainsi angular d'atteindre la limite d'itérations de résumé.

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

Nous appliquons notre filtre 'groupBy' sur la variable de portée teamPlayers, sur la propriété 'team'. Notre ng-repeat reçoit une combinaison de (clé, valeurs []) que nous pouvons utiliser dans nos itérations suivantes.

Mise à jour du 11 juin 2014 J'ai élargi le groupe par filtre pour tenir compte de l'utilisation d'expressions comme clé (par exemple des variables imbriquées). Le service d'analyse angulaire est très pratique pour cela:

Le filtre (avec support d'expression)

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

Le contrôleur (avec des objets imbriqués)

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

Le html (avec l'expression sortBy)

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


Vous avez raison, le lien violon a été mis à jour. Merci de me prévenir.
chrisv

3
C'est plutôt chouette en fait! Moins de code.
Benny Bottema

3
une chose à noter avec ceci - par défaut, memoize utilise le premier paramètre (ie 'items') comme clé de cache - donc si vous lui passez les mêmes 'items' avec un 'field' différent, il retournera la même valeur en cache. Les solutions sont les bienvenues.
Tom Carver

Je pense que vous pouvez utiliser la valeur $ id pour contourner ceci: élément dans le suivi des éléments par $ id (élément)
Caspar Harmer

2
Quelles «réponses acceptées»? Sur Stack Overflow, il ne peut y avoir qu'une seule réponse acceptée.
Sebastian Mach

19

Faites d'abord une boucle en utilisant un filtre qui ne retournera que des équipes uniques, puis une boucle imbriquée qui renvoie tous les joueurs par équipe actuelle:

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>

scénario:

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

Si simple. Nice one @Plantface.
Jeff Yates

juste génial. mais que faire si je veux pousser un nouvel objet vers $ scope.players en cliquant? lorsque vous parcourez une fonction, sera-t-il ajouté?
super cool

16

J'ai utilisé à l'origine la réponse de Plantface, mais je n'aimais pas à quoi ressemblait la syntaxe à mon avis.

Je l'ai retravaillé pour utiliser $ q.defer pour post-traiter les données et renvoyer une liste d'équipes uniques, qui est ensuite utilisée comme filtre.

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

Vue

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

Manette

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

});

1
Cette réponse ne fonctionne pas avec AngularJS> 1.1 car Promised n'est plus déballé pour les tableaux. Voir les notes d'immigration
Benny Bottema

6
Il n'y a pas besoin de la promesse dans cette solution, car vous ne faites rien de manière asynchrone. Dans ce cas, vous pouvez simplement sauter cette étape ( jsFiddle ).
Benny Bottema

11

Les deux réponses étaient bonnes, je les ai donc déplacées dans une directive afin qu'elle soit réutilisable et qu'une deuxième variable de portée ne doive pas être définie.

Voici le violon si vous voulez le voir implémenté

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

Ensuite, il peut être utilisé comme suit:

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

11

Mettre à jour

J'ai d'abord écrit cette réponse parce que l'ancienne version de la solution suggérée par Ariel M. lorsqu'elle était combinée avec d'autres $filters déclenchait une " Infite $ diggest Loop Error " ( infdig) . Heureusement, ce problème a été résolu dans la dernière version de angular.filter .

J'ai suggéré l'implémentation suivante, qui n'avait pas ce problème :

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

Cependant, cette implémentation ne fonctionnera qu'avec les versions antérieures à Angular 1.3. (Je mettrai à jour cette réponse sous peu en fournissant une solution qui fonctionne avec toutes les versions.)

J'ai en fait écrit un article sur les étapes que j'ai suivies pour développer cela $filter, les problèmes que j'ai rencontrés et les choses que j'en ai tirées .


Salut @Josep, jetez un œil à la nouvelle angular-filterversion - 0.5.0, il n'y a plus d'exception. groupBypeut être en chaîne avec n'importe quel filtre. De plus, vos cas de test sont excellents et terminez avec succès - voici un plunker Merci.
a8m

1
@Josep Ayant des problèmes dans Angular 1.3
amcdnl

2

En plus de la réponse acceptée, vous pouvez l'utiliser si vous souhaitez regrouper par plusieurs colonnes :

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

En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.