¿Cómo puedo agrupar datos con un filtro angular?

136

Tengo una lista de jugadores que pertenecen a un grupo cada uno. ¿Cómo puedo usar un filtro para enumerar los usuarios 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'}];

Estoy buscando este resultado:

  • equipo alfa
    • Gene
  • equipo beta
    • Jorge
    • Paula
  • equipo gamma
    • Steve
    • Scruath del 5to sector
Benny Bottema
fuente

Respuestas:

182

Puede usar groupBy del módulo angular.filter .
para que puedas hacer algo como esto:

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:
Nombre del grupo: alpha
* player: Gene
Nombre del grupo: beta
* player: George
* player: Paula
Nombre del grupo: gamma
* player: Steve
* player: Scruath

ACTUALIZACIÓN: jsbin Recuerde los requisitos básicos para usar angular.filter, tenga en cuenta específicamente que debe agregarlo a las dependencias de su módulo:

(1) Puede instalar el filtro angular usando 4 métodos diferentes:

  1. clonar y construir este repositorio
  2. a través de Bower: ejecutando $ bower instale un filtro angular desde su terminal
  3. a través de npm: ejecutando $ npm instala angular-filter desde tu terminal
  4. a través de cdnjs http://www.cdnjs.com/libraries/angular-filter

(2) Incluya angular-filter.js (o angular-filter.min.js) en su index.html, después de incluir Angular.

(3) Agregue 'angular.filter' a la lista de dependencias de su módulo principal.

a8m
fuente
Gran ejemplo Sin embargo, la clave devuelve el nombre del grupo y no la clave real ... ¿cómo podemos resolver eso?
JohnAndrews
77
No olvides incluir el angular.filtermódulo.
Puce
1
puede usar order-by con group-by @erfling, PTAL en: github.com/a8m/angular-filter/wiki/…
a8m el
1
Oh wow. Gracias. No esperaba que ordenar el bucle anidado afectara al externo de esa manera. Eso es realmente útil. +1
erfling
1
@Xyroid, incluso estoy buscando lo mismo que quiero hacer keycomo objeto. suerte de tu parte
súper genial
25

Además de las respuestas aceptadas anteriores, creé un filtro genérico 'groupBy' usando la biblioteca underscore.js.

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

El filtro

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

Tenga en cuenta la llamada 'memorizar'. Este método de subrayado almacena en caché el resultado de la función y evita que angular evalúe la expresión del filtro cada vez, evitando así que angular alcance el límite de iteraciones de resumen.

El 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 nuestro filtro 'groupBy' en la variable de alcance teamPlayers, en la propiedad 'team'. Nuestro ng-repeat recibe una combinación de (clave, valores []) que podemos usar en nuestras siguientes iteraciones.

Actualización 11 de junio de 2014 Expandí el grupo por filtro para tener en cuenta el uso de expresiones como clave (por ejemplo, variables anidadas). El servicio de análisis angular es bastante útil para esto:

El filtro (con soporte de expresión)

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

El controlador (con objetos anidados)

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

El html (con expresión 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
fuente
Tienes razón, el enlace de violín se ha actualizado. Gracias por avisarme.
chrisv
3
¡Esto es bastante bueno en realidad! La menor cantidad de código.
Benny Bottema
3
Una cosa a tener en cuenta con esto: por defecto, memoize usa el primer parámetro (es decir, 'elementos') como clave de caché, por lo que si le pasa los mismos 'elementos' con un 'campo' diferente, devolverá el mismo valor en caché. Soluciones bienvenidas.
Tom Carver
Creo que puede usar el valor de $ id para evitar esto: elemento en elementos rastreados por $ id (elemento)
Caspar Harmer
2
¿Qué "respuestas aceptadas"? En Stack Overflow, solo puede haber una respuesta aceptada.
Sebastian Mach
19

Primero haga un bucle usando un filtro que devolverá solo equipos únicos, y luego un bucle anidado que devuelva todos los jugadores por equipo actual:

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>

guión:

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
fuente
Tan sencillo. Nice one @Plantface.
Jeff Yates
sólo brillante . pero ¿qué pasa si quiero enviar un nuevo objeto a $ scope.players al hacer clic? a medida que recorres una función, ¿se agregará?
súper genial el
16

Originalmente usé la respuesta de Plantface, pero no me gustó cómo se veía la sintaxis en mi opinión.

Lo modifiqué para usar $ q.defer para procesar posteriormente los datos y devolver una lista de equipos únicos, que luego se usa como filtro.

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

Ver

<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
fuente
1
Esta respuesta no funciona con AngularJS> 1.1, ya que Promised ya no se desenvuelve para las matrices. Ver las notas de inmigración
Benny Bottema
66
No hay necesidad de Promise en esta solución, ya que no está haciendo nada de forma asincrónica. En este caso, simplemente puede omitir ese paso ( jsFiddle ).
Benny Bottema
11

Ambas respuestas fueron buenas, así que las moví a una directiva para que sea reutilizable y no se tenga que definir una segunda variable de alcance.

Aquí está el violín si quieres verlo implementado

A continuación se muestra la directiva:

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

Entonces se puede usar de la siguiente manera:

<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
fuente
11

Actualizar

Inicialmente escribí esta respuesta porque la versión anterior de la solución sugerida por Ariel M. cuando se combinaba con otras $filters provocaba un " Infite $ diggest Loop Error " ( infdig) . Afortunadamente, este problema se ha resuelto en la última versión de angular.filter .

Sugerí la siguiente implementación, que no tenía ese 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;
    };
});

Sin embargo, esta implementación solo funcionará con versiones anteriores a Angular 1.3. (Actualizaré esta respuesta en breve proporcionando una solución que funcione con todas las versiones).

De hecho, escribí una publicación sobre los pasos que tomé para desarrollar esto $filter, los problemas que encontré y las cosas que aprendí de ellos .

Josep
fuente
Hola @Josep, eche un vistazo a la nueva angular-filterversión: 0.5.0, no hay más excepciones. groupBySe puede encadenar con cualquier filtro. Además, sus excelentes casos de prueba terminan con éxito: aquí hay un plunker Gracias.
a8m el
1
@Josep Problemas en Angular 1.3
amcdnl
2

Además de la respuesta aceptada, puede usar esto si desea agrupar por varias columnas :

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