如何使用Angular过滤器对数据进行分组?

141

我有一个球员列表,每个球员都属于一个团体。如何使用过滤器列出每个团体的用户?

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

我正在寻找这个结果:

  • Alpha团队
    • 吉恩
  • Beta团队
    • 乔治
    • 宝拉
  • Gamma团队
    • 史蒂夫
    • 第五区的斯克鲁斯
8个回答

184
你可以使用angular.filter模块的groupBy函数。
这样,你就可以像这样做:

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>

结果:
组名: alpha
* 玩家: Gene
组名: beta
* 玩家: George
* 玩家: Paula
组名: gamma
* 玩家: Steve
* 玩家: Scruath

更新: jsbin 记住使用angular.filter的基本要求,特别是注意您必须将其添加到模块的依赖项中:

(1) 您可以使用4种不同的方法安装angular-filter:

  1. 克隆并构建此存储库
  2. 通过Bower:在终端上运行$ bower install angular-filter
  3. 通过npm:在终端上运行$ npm install angular-filter
  4. 通过cdnjs http://www.cdnjs.com/libraries/angular-filter

(2) 在index.html中包含angular-filter.js(或angular-filter.min.js),在包含Angular本身之后。

(3) 将'angular.filter'添加到您的主模块的依赖列表中。


8
别忘了加入 angular.filter 模块。 - Puce
你可以将一个“angular-expression”作为参数传递。例如:groupBy: 'team || age'。这里有一些fiddle来演示它。谢谢。 - a8m
1
你可以在 group by 中使用 order by。请查看:https://github.com/a8m/angular-filter/wiki/Common-Questions#order-groupby-result - a8m
1
哦,哇。谢谢。我没想到排序嵌套循环会以那种方式影响外部循环。这真的很有用。+1 - erfling
1
@Xyroid,我也在寻找同样的东西,我想把“key”作为对象。你有什么好的建议吗? - super cool
显示剩余14条评论

25

除了上面接受的答案,我使用 underscore.js 库创建了一个通用的 'groupBy' 过滤器。

JSFiddle(已更新):http://jsfiddle.net/TD7t3/

该过滤器:

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

注意'memoize'函数的调用。这个underscore方法缓存了函数的结果,防止Angular在每次评估筛选器表达式时都重新计算结果,从而防止Angular达到digest迭代限制。

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>
我们在teamPlayers作用域变量上的“groupBy”筛选器上,应用于“team”属性。我们的ng-repeat接收到一个(key, values[])组合,我们可以在接下来的迭代中使用它们。 2014年6月11日更新 我扩展了group by筛选器来考虑将表达式作为键(例如嵌套变量)的情况。Angular解析服务非常有用: 带有表达式支持的筛选器
app.filter('groupBy', function($parse) {
    return _.memoize(function(items, field) {
        var getter = $parse(field);
        return _.groupBy(items, function(item) {
            return getter(item);
        });
    });
});

控制器(带有嵌套对象)

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

使用sortBy表达式的HTML代码

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


3
需要注意的一点是,memoize默认使用第一个参数(即“items”)作为缓存键,因此如果您传递具有不同“field”的相同“items”,它将返回相同的缓存值。欢迎提供解决方案。 - Tom Carver
我认为你可以使用 $id 值来解决这个问题:在 items 中的 item 上通过 $id(item) 进行跟踪。 - Caspar Harmer
为什么在不必要的情况下将下划线作为依赖项? - amcdnl
@TomCarver 您可以创建包含“字段”的缓存键,使用解析器函数作为 _.memoize 的第二个参数。该函数允许您定义自己的缓存键。例如 _.memoize( ... , function (items, field) { return _.pluck(items,'name').toString() + '-' + field})。这个 SO 答案有更多细节:https://dev59.com/2mQn5IYBdhLWcg3w36TV#16513030 - Chad von Nau

19

首先使用一个过滤器进行循环,该过滤器将仅返回唯一的团队,然后使用嵌套循环返回当前团队的所有球员:

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>

脚本:

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

太棒了。但是如果我想在点击时将一个新对象推送到$scope.players中怎么办?当您通过函数循环时,它会被添加吗? - super cool

16

我最初采用了Plantface的答案,但我不喜欢在我的视图中看到的语法。

我修改了它,使用$q.defer来后处理数据并返回唯一团队列表,然后将其用作过滤器。

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

视图

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

});

1
这个答案不适用于AngularJS> 1.1,因为承诺(Promised)不再为数组解包。请参阅[移民说明](https://docs.angularjs.org/guide/migration)。 - Benny Bottema
6
在这个解决方案中,由于您没有执行任何异步操作,因此不需要 Promise。在这种情况下,您可以简单地跳过该步骤(jsFiddle)。 - Benny Bottema

11

两个答案都很好,所以我将它们移动到一个指令中,使其可以重复使用,而不必定义第二个作用域变量。

这里是示例,如果你想看它的实现

下面是指令:

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

那么它可以按如下方式使用:

<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

更新

我最初写下了这个答案,是因为Ariel M.建议的旧版本解决方案与其他$filters结合使用会触发一个"无限$digest循环错误"(infdig)。幸运的是,在最新版本的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;
    };
});

然而,这个实现只适用于Angular 1.3之前的版本。(我很快会更新这个答案,提供适用于所有版本的解决方案。)
我实际上写了一篇关于我开发这个$filter所采取的步骤、遇到的问题以及从中学到的东西的文章

嗨@Josep,看看新的angular-filter版本-0.5.0,不再有异常了。 groupBy可以与任何过滤器链接。 此外,您的测试用例非常出色,已成功完成-这是一个plunker。 谢谢。 - a8m
1
@Josep 在 Angular 1.3 中遇到了问题。 - amcdnl

3

除了已接受的答案外,如果您想按多列分组,也可以使用此方法:

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

0

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接