Como posso agrupar dados com um filtro Angular?

136

Eu tenho uma lista de jogadores que pertencem a um grupo cada. Como posso usar um filtro para listar os usuários por grupo?

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

Estou procurando este resultado:

  • alfa da equipe
    • Gene
  • equipe beta
    • George
    • Paula
  • equipe gama
    • Steve
    • Escrutínio do 5º setor
Benny Bottema
fonte

Respostas:

182

Você pode usar o groupBy do módulo angular.filter .
para que você possa fazer algo assim:

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>

RESULTADO:
Nome do grupo: alpha
* jogador: Gene
Nome do grupo: beta
* jogador: George
* jogador: Paula
Nome do grupo: gama
* jogador: Steve
* jogador: Scruath

UPDATE: jsbin Lembre-se dos requisitos básicos a serem usados angular.filter, observe especificamente que você deve adicioná-lo às dependências do seu módulo:

(1) Você pode instalar o filtro angular usando 4 métodos diferentes:

  1. clonar e construir este repositório
  2. via Bower: executando $ bower, instale um filtro angular a partir do seu terminal
  3. via npm: executando $ npm instale o filtro angular do seu terminal
  4. via cdnjs http://www.cdnjs.com/libraries/angular-filter

(2) Inclua angular-filter.js (ou angular-filter.min.js) em seu index.html, depois de incluir o próprio Angular.

(3) Adicione 'angular.filter' à lista de dependências do seu módulo principal.

a8m
fonte
Ótimo exemplo. No entanto, a chave retorna o nome do grupo e não a chave real ... como podemos resolver isso?
JohnAndrews
7
Não esqueça de incluir o angular.filtermódulo.
Puce
1
você pode usar order-by com o grupo-by @erfling, PTAL em: github.com/a8m/angular-filter/wiki/...
a8m
1
Oh uau. Obrigado. Eu não esperava que a ordem do loop aninhado afetasse o externo dessa maneira. Isso é realmente útil. +1
erfling
1
@ Xyroid, mesmo eu estou procurando o mesmo que eu quero fazer keycomo objeto. alguma sorte do seu lado
super legal
25

Além das respostas aceitas acima, criei um filtro 'groupBy' genérico usando a biblioteca underscore.js.

JsFiddle (atualizado): http://jsfiddle.net/TD7t3/

O filtro

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

Observe a chamada 'memorizar'. Esse método de sublinhado armazena em cache o resultado da função e impede que angular avalie a expressão do filtro todas as vezes, impedindo que angular atinja o limite de iterações de compilação.

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

Aplicamos nosso filtro 'groupBy' na variável de escopo teamPlayers, na propriedade 'team'. Nosso ng-repeat recebe uma combinação de (key, values ​​[]) que podemos usar nas próximas iterações.

Atualização em 11 de junho de 2014 Expandi o grupo por filtro para considerar o uso de expressões como a chave (por exemplo, variáveis ​​aninhadas). O serviço de análise angular é bastante útil para isso:

O filtro (com suporte à expressão)

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

O controlador (com objetos aninhados)

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

O html (com expressão 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/

chrisv
fonte
Você está certo, o link do violino foi atualizado. Obrigado por me notificar.
ChrisV
3
Isso é bem legal, na verdade! Menor quantidade de código.
Benny Bottema 07/07
3
uma coisa a ser observada com isso - por padrão, o memoize usa o primeiro parâmetro (ou seja, 'itens') como chave de cache - portanto, se você passar os mesmos 'itens' com um 'campo' diferente, ele retornará o mesmo valor em cache. Soluções bem-vindas.
Tom Carver
Eu acho que você pode usar o valor $ id para contornar este problema: artigo em itens acompanhar por $ id (item)
Caspar Harmer
2
Quais "respostas aceitas"? No estouro de pilha, só pode haver uma resposta aceita.
Sebastian Mach
19

Primeiro, faça um loop usando um filtro que retornará apenas equipes únicas e, em seguida, um loop aninhado que retorna todos os jogadores por time atual:

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>

roteiro:

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;
    }
}
Benny Bottema
fonte
Tão simples. Nice @Plantface.
Jeff Yates
simplesmente brilhante. mas e se eu quiser enviar um novo objeto para $ scope.players ao clicar? Como você está percorrendo uma função, ela será adicionada?
Super cool
16

Originalmente, usei a resposta do Plantface, mas não gostei da aparência da sintaxe.

Reescrevi -o para usar $ q.defer para pós-processar os dados e retornar uma lista em equipes únicas, que são usadas como filtro.

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

Visão

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

Controlador

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

});
Walter Stabosz
fonte
1
Esta resposta não está funcionando com AngularJS> 1.1, pois o Promised não é mais desembrulhado para matrizes. Veja as notas de imigração
Benny Bottema
6
Não há necessidade da promessa nesta solução, pois você não está fazendo nada de forma assíncrona. Nesse caso, você pode simplesmente pular essa etapa ( jsFiddle ).
Benny Bottema
11

Ambas as respostas foram boas, então eu as mudei para uma diretiva para que seja reutilizável e uma segunda variável de escopo não precise ser definida.

Aqui está o violino, se você quiser vê-lo implementado

Abaixo está a diretiva:

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

Em seguida, pode ser usado da seguinte maneira:

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

Atualizar

Inicialmente, escrevi essa resposta porque a versão antiga da solução sugerida por Ariel M. quando combinada com outras $filters acionou um " Infite $ diggest Loop Error " ( infdig) . Felizmente, esse problema foi resolvido na versão mais recente do angular.filter .

Sugeri a seguinte implementação, que não tinha esse problema :

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

No entanto, essa implementação funcionará apenas com versões anteriores ao Angular 1.3. (Atualizarei esta resposta em breve, fornecendo uma solução que funcione com todas as versões.)

Na verdade, escrevi um post sobre as etapas que eu tomei para desenvolver isso $filter, os problemas que encontrei e as coisas que aprendi com isso .

Josep
fonte
Olá @Josep, dê uma olhada na nova angular-filterversão - 0.5.0, não há mais exceção. groupBypode ser corrente com qualquer filtro. Além disso, seus ótimos casos de teste são concluídos com êxito - aqui está uma vantagem Agradecemos.
a8m
1
@Josep Tendo problemas no Angular 1.3
amcdnl
2

Além da resposta aceita, você pode usar isso se desejar agrupar por várias colunas :

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