Jak mogę pogrupować dane za pomocą filtru kątowego?

136

Mam listę graczy, którzy należą do grupy. Jak mogę użyć filtru, aby wyświetlić listę użytkowników według grupy?

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

Szukam tego wyniku:

  • zespół alfa
    • Gen
  • zespół beta
    • Jerzy
    • Paula
  • zespół gamma
    • Steve
    • Scruath z 5 sektora
Benny Bottema
źródło

Odpowiedzi:

182

Możesz użyć modułu groupBy z modułu angular.filter .
więc możesz zrobić coś takiego:

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>

WYNIK:
Nazwa grupy:
gracz alfa *: Gene
Nazwa grupy:
gracz beta *: George
* gracz: Paula
Nazwa grupy:
gracz gamma *: Steve
* gracz: Scruath

AKTUALIZACJA: jsbin Zapamiętaj podstawowe wymagania do użycia angular.filter, w szczególności pamiętaj, że musisz dodać je do zależności swojego modułu:

(1) Możesz zainstalować filtr kątowy używając 4 różnych metod:

  1. klonuj i buduj to repozytorium
  2. przez Bower: uruchamiając $ bower zainstaluj filtr kątowy z terminala
  3. przez npm: uruchamiając $ npm zainstaluj filtr kątowy z terminala
  4. przez cdnjs http://www.cdnjs.com/libraries/angular-filter

(2) Dołącz angular-filter.js (lub angular-filter.min.js) do pliku index.html, po dołączeniu samego Angulara.

(3) Dodaj „angular.filter” do listy zależności modułu głównego.

a8m
źródło
Świetny przykład. Jednak klucz zwraca nazwę grupy, a nie rzeczywisty klucz ... jak możemy to rozwiązać?
JohnAndrews
7
Nie zapomnij dołączyć angular.filtermodułu.
Puce
1
możesz użyć order-by z group-by @erfling, PTAL na: github.com/a8m/angular-filter/wiki/…
a8m
1
Oh wow. Dzięki. Nie spodziewałem się, że uporządkowanie zagnieżdżonej pętli wpłynie w ten sposób na zewnętrzną. To naprawdę przydatne. +1
erfling
1
@Xyroid, nawet ja szukam tego samego, co chcę zrobić keyjako obiekt. szczęście z twojej strony
super fajne
25

Oprócz zaakceptowanych odpowiedzi powyżej utworzyłem ogólny filtr „groupBy” przy użyciu biblioteki underscore.js.

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

Filtr

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

Zwróć uwagę na wezwanie „memoize”. Ta metoda podkreślenia zapisuje w pamięci podręcznej wynik funkcji i uniemożliwia angularowi ocenę wyrażenia filtru za każdym razem, uniemożliwiając w ten sposób osiągnięcie limitu iteracji skrótu przez angular.

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

Stosujemy nasz filtr „groupBy” w zmiennej zakresu teamPlayers we właściwości „team”. Nasze powtórzenie ng otrzymuje kombinację (klucz, wartości []), której możemy użyć w naszych kolejnych iteracjach.

Aktualizacja 11 czerwca 2014 r. Rozszerzyłem grupę o filtr, aby uwzględnić użycie wyrażeń jako klucza (np. Zmienne zagnieżdżone). Przydaje się do tego usługa analizy kątowej:

Filtr (z obsługą wyrażeń)

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

Kontroler (z zagnieżdżonymi obiektami)

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

HTML (z wyrażeniem 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
źródło
Masz rację, link do skrzypiec został zaktualizowany. Dziękuję za powiadomienie mnie.
chrisv
3
To jest całkiem fajne! Najmniej kodu.
Benny Bottema,
3
jedna rzecz, na którą należy zwrócić uwagę - domyślnie memoize używa pierwszego parametru (tj. „items”) jako klucza pamięci podręcznej - więc jeśli przekażesz mu te same „items” z innym „polem”, zwróci tę samą wartość z pamięci podręcznej. Mile widziane rozwiązania.
Tom Carver
Myślę, że możesz użyć wartości $ id, aby obejść ten problem: pozycja w śledzeniu przedmiotów według $ id (pozycja)
Caspar Harmer
2
Które „zaakceptowane odpowiedzi”? W przypadku przepełnienia stosu może być tylko jedna zaakceptowana odpowiedź.
Sebastian Mach
19

Najpierw wykonaj pętlę, używając filtru, który zwróci tylko unikalne drużyny, a następnie zagnieżdżoną pętlę, która zwróci wszystkich graczy z bieżącej drużyny:

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>

scenariusz:

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
źródło
Tak prosty. Niezły @Plantface.
Jeff Yates
po prostu genialne. ale co, jeśli po kliknięciu chcę wypchnąć nowy obiekt do $ scope.players? ponieważ przechodzisz przez funkcję, czy zostanie ona dodana?
super fajne
16

Pierwotnie użyłem odpowiedzi Plantface, ale nie podobało mi się, jak wyglądała składnia w moim widoku.

Przerobiłem go tak, aby używał $ q.defer do przetwarzania końcowego danych i zwracania listy unikalnych zespołów, która jest następnie używana jako filtr.

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

Widok

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

Kontroler

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
źródło
1
Ta odpowiedź nie działa z AngularJS> 1.1, ponieważ Promised nie jest już rozpakowywany dla tablic. Zobacz notatki imigracyjne
Benny Bottema,
6
Obietnica nie jest potrzebna w tym rozwiązaniu, ponieważ nie robisz niczego asynchronicznie. W takim przypadku możesz po prostu pominąć ten krok ( jsFiddle ).
Benny Bottema,
11

Obie odpowiedzi były dobre, więc przeniosłem je do dyrektywy, aby można ją było ponownie wykorzystać i nie trzeba było definiować drugiej zmiennej zakresu.

Oto skrzypce, jeśli chcesz zobaczyć, jak zostały wdrożone

Poniżej znajduje się dyrektywa:

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

Następnie można go użyć w następujący sposób:

<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
źródło
11

Aktualizacja

Początkowo napisałem tę odpowiedź, ponieważ stara wersja rozwiązania zaproponowana przez Ariela M. w połączeniu z innymi $filters wywołała Infite $ diggest Loop Error ” ( infdig) . Na szczęście ten problem został rozwiązany w najnowszej wersji pliku angular.filter .

Zaproponowałem następującą implementację, która nie miała tego problemu :

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

Jednak ta implementacja będzie działać tylko z wersjami starszymi od Angular 1.3. (Wkrótce zaktualizuję tę odpowiedź, zapewniając rozwiązanie, które działa ze wszystkimi wersjami).

Właściwie napisałem post o krokach, które podjąłem, aby to rozwinąć $filter, o problemach, które napotkałem i rzeczach, których się z tego nauczyłem .

Josep
źródło
Cześć @Josep, spójrz na nową angular-filterwersję - 0.5.0, nie ma więcej wyjątków. groupBymożna łączyć z dowolnym filtrem. również jesteś świetny, przypadki testowe zakończyły się pomyślnie - oto plunker Dzięki.
a8m
1
@Josep Problemy w Angular 1.3
amcdnl
2

Oprócz zaakceptowanej odpowiedzi możesz użyć tego, jeśli chcesz pogrupować według wielu kolumn :

<ul ng-repeat="(key, value) in players | groupBy: '[team,name]'">
Luis Teijon
źródło