AngularJS Group by Directive without External Dependencies

时间:2013-11-15 01:52:50

标签: angularjs group-by angularjs-directive angularjs-ng-repeat ng-show

我是Angular的新手,想学习处理问题的最佳方法。我的目标是有一个可重用的方法来创建按标题分组。我创建了一个可行的解决方案,但我认为这应该是一个指令,而不是我的控制器中的范围函数,但我不知道如何实现这一点,或者指令是否是正确的方法。非常感谢任何投入。

请参阅我目前处理jsFiddle

的方法

在HTML中,这是一个使用ng-repeat的简单列表,我在ng-show上调用newGrouping()函数。该函数传递对完整列表的引用,我想要分组的字段和当前索引。

<div ng-app>
<div ng-controller='TestGroupingCtlr'>
    <div ng-repeat='item in MyList'>
        <div ng-show="newGrouping($parent.MyList, 'GroupByFieldName', $index);">
            <h2>{{item.GroupByFieldName}}</h2>
        </div>
        {{item.whatever}}
    </div>
</div>
</div>

在我的控制器中,我有newGrouping()函数,它只是将当前值与之前的值进行比较,第一项除外,并根据匹配返回true或false。

function TestGroupingCtlr($scope) {

  $scope.MyList = [
    {GroupByFieldName:'Group 1', whatever:'abc'},
    {GroupByFieldName:'Group 1', whatever:'def'},
    {GroupByFieldName:'Group 2', whatever:'ghi'},
    {GroupByFieldName:'Group 2', whatever:'jkl'},
    {GroupByFieldName:'Group 2', whatever:'mno'}
  ];

  $scope.newGrouping = function(group_list, group_by, index) {
  if (index > 0) {
    prev = index - 1;
    if (group_list[prev][group_by] !== group_list[index][group_by]) {
      return true;
    } else {
      return false;
    }
  } else {
    return true;
  }
  };
}

输出将如下所示。

第1组

  • ABC
  • DEF

第2组

  • GHI
  • JKL
  • mno

感觉应该有更好的方法。我希望这是一个可以重用的常用实用函数。这应该是指令吗?是否有更好的方法来引用列表中的上一项而不是传递完整列表和当前索引的方法?我将如何处理这个指令?

非常感谢任何建议。

更新:寻找不需要外部依赖的答案。 使用下划线/ lodash或角度滤波器模块有很好的解决方案。

的Darryl

9 个答案:

答案 0 :(得分:34)

这是对上述Darryl解决方案的修改,允许多个参数组。另外,它使用$ parse来允许使用嵌套属性作为分组参数。

使用多个嵌套参数的示例

http://jsfiddle.net/4Dpzj/6/

HTML

<h1>Multiple Grouping Parameters</h1>
<div ng-repeat="item in MyList  | orderBy:'groupfield' | groupBy:['groupfield', 'deep.category']">
    <h2 ng-show="item.group_by_CHANGED">{{item.groupfield}} {{item.deep.category}}</h2>
     <ul>
        <li>{{item.whatever}}</li>
     </ul>
</div>  

过滤(Javascript)

app.filter('groupBy', ['$parse', function ($parse) {
    return function (list, group_by) {

        var filtered = [];
        var prev_item = null;
        var group_changed = false;
        // this is a new field which is added to each item where we append "_CHANGED"
        // to indicate a field change in the list
        //was var new_field = group_by + '_CHANGED'; - JB 12/17/2013
        var new_field = 'group_by_CHANGED';

        // loop through each item in the list
        angular.forEach(list, function (item) {

            group_changed = false;

            // if not the first item
            if (prev_item !== null) {

                // check if any of the group by field changed

                //force group_by into Array
                group_by = angular.isArray(group_by) ? group_by : [group_by];

                //check each group by parameter
                for (var i = 0, len = group_by.length; i < len; i++) {
                    if ($parse(group_by[i])(prev_item) !== $parse(group_by[i])(item)) {
                        group_changed = true;
                    }
                }


            }// otherwise we have the first item in the list which is new
            else {
                group_changed = true;
            }

            // if the group changed, then add a new field to the item
            // to indicate this
            if (group_changed) {
                item[new_field] = true;
            } else {
                item[new_field] = false;
            }

            filtered.push(item);
            prev_item = item;

        });

        return filtered;
    };
}]);

答案 1 :(得分:23)

如果您已经在使用LoDash / Underscore或任何功能库,则可以使用 _。groupBy()(或类似名称)功能来执行此操作。


在控制器中

var movies = [{"movieId":"1","movieName":"Edge of Tomorrow","lang":"English"},
              {"movieId":"2","movieName":"X-MEN","lang":"English"},
              {"movieId":"3","movieName":"Gabbar Singh 2","lang":"Telugu"},
              {"movieId":"4","movieName":"Resu Gurram","lang":"Telugu"}];
$scope.movies = _.groupBy(movies, 'lang');

在模板中

<ul ng-repeat="(lang, langMovs) in movies">{{lang}}
  <li ng-repeat="mov in langMovs">{{mov.movieName}}</li>
</ul>

这将呈现

  • 明日边缘
  • X-MEN

泰卢固语

  • Gabbar Singh 2
  • Resu Gurram

更好的是,这也可以很容易地转换为过滤器,没有太多的样板代码来按属性对元素进行分组。

更新:按多个键分组

通常使用多个键进行分组非常有用。例如,使用LoDash(source):

$scope.movies = _.groupBy(movies, function(m) {
    return m.lang+ "-" + m.movieName;
});

更新我推荐此方法的原因:ng-repeat / ng-options上使用过滤器会导致严重的性能问题,除非过滤器快速执行。谷歌针对过滤器性能问题。你会知道的!

答案 2 :(得分:4)

这是我最终决定在ng-repeat中处理分组的内容。我阅读了更多关于指令和过滤器的内容,虽然你可以用其中任何一个解决这个问题,但过滤器方法似乎是更好的选择。原因是过滤器更适合于只需要操作数据的情况。当需要进行DOM操作时,指令会更好。在这个例子中,我真的只需要操纵数据并让DOM独自存在。我觉得这给了最大的灵活性。

请参阅我对jsFiddle处理的分组的最终方法。我还添加了一个小表单来演示动态添加数据时列表的工作方式。

这是HTML。

<div ng-app="myApp">
    <div ng-controller='TestGroupingCtlr'>
        <div ng-repeat="item in MyList  | orderBy:'groupfield' | groupBy:'groupfield'" >
            <h2 ng-show="item.groupfield_CHANGED">{{item.groupfield}}</h2>
            <ul>
                <li>{{item.whatever}}</li>
            </ul>
        </div>

        <form role="form" ng-submit="AddItem()">
            <input type="text" data-ng-model="item.groupfield" placeholder="Group">
            <input type="text" data-ng-model="item.whatever" placeholder="Item">
            <input class="btn" type="submit" value="Add Item">
        </form>
    </div>

</div>

这是Javascript。

var app=angular.module('myApp',[]);

app.controller('TestGroupingCtlr',function($scope) {

        $scope.MyList = [
            {groupfield: 'Group 1', whatever: 'abc'},
            {groupfield: 'Group 1', whatever: 'def'},
            {groupfield: 'Group 2', whatever: 'ghi'},
            {groupfield: 'Group 2', whatever: 'jkl'},
            {groupfield: 'Group 2', whatever: 'mno'}
        ];

        $scope.AddItem = function() {

            // add to our js object array
            $scope.MyList.push({
            groupfield:$scope.item.groupfield,
                    whatever:$scope.item.whatever
            });
        };


    })


/*
 * groupBy
 *
 * Define when a group break occurs in a list of items
 *
 * @param {array}  the list of items
 * @param {String} then name of the field in the item from the list to group by
 * @returns {array} the list of items with an added field name named with "_new"
 *                  appended to the group by field name
 *
 * @example     <div ng-repeat="item in MyList  | groupBy:'groupfield'" >
 *              <h2 ng-if="item.groupfield_CHANGED">{{item.groupfield}}</h2>
 *
 *              Typically you'll want to include Angular's orderBy filter first
 */

app.filter('groupBy', function(){
    return function(list, group_by) {

    var filtered = [];
    var prev_item = null;
    var group_changed = false;
    // this is a new field which is added to each item where we append "_CHANGED"
    // to indicate a field change in the list
    var new_field = group_by + '_CHANGED';

    // loop through each item in the list
    angular.forEach(list, function(item) {

        group_changed = false;

        // if not the first item
        if (prev_item !== null) {

            // check if the group by field changed
            if (prev_item[group_by] !== item[group_by]) {
                group_changed = true;
            }

        // otherwise we have the first item in the list which is new
        } else {
            group_changed = true;
        }

        // if the group changed, then add a new field to the item
        // to indicate this
        if (group_changed) {
            item[new_field] = true;
        } else {
            item[new_field] = false;
        }

        filtered.push(item);
        prev_item = item;

    });

    return filtered;
    };
})

对于我正在使用它的应用程序,我在整个应用程序中将过滤器设置为可重复使用的过滤器。

我对指令方法不喜欢的是HTML在指令中,因此感觉不可重用。

我喜欢之前的过滤方法,但它似乎效率不高,因为列表必须在摘要周期中遍历两次。我处理长列表,所以这可能是一个问题。此外,它只是看起来不像对上一个项目的简单检查那样直观,看它是否发生了变化。此外,我希望能够轻松地对多个字段使用过滤器,这个新过滤器只需通过另一个字段名称再次使用过滤器来处理。

关于我的groupBy过滤器的另一个注释 - 我确实意识到多个分组会导致数组被遍历多次,所以我打算修改它以接受多个group by字段的数组,这样它只需要遍历数组一次。

非常感谢投入。它真的帮助我学习了更多关于Angular中的指令和过滤器。

欢呼声, 达里尔

答案 3 :(得分:1)

下面是一个基于指令的解决方案,以及一个演示它的JSFiddle的链接。该指令允许每个实例指定它应该分组的项的字段名称,因此有一个使用两个不同字段的示例。它具有项目数量的线性运行时间。

JSFiddle

<div ng-app='myApp'>
    <div ng-controller='TestGroupingCtlr'>
        <h1>Grouping by FirstFieldName</h1>
        <div group-with-headers to-group="MyList" group-by="FirstFieldName">
        </div>
        <h1>Grouping by SecondFieldName</h1>
        <div group-with-headers to-group="MyList" group-by="SecondFieldName">
        </div>
    </div>
</div>

angular.module('myApp', []).directive('groupWithHeaders', function() {
    return {
        template: "<div ng-repeat='(group, items) in groups'>" +
                    "<h2>{{group}}</h2>" +
                    "<div ng-repeat='item in items'>" +
                      "{{item.whatever}}" +   
                    "</div>" +
                  "</div>",
        scope: true,
        link: function(scope, element, attrs) {
            var to_group = scope.$eval(attrs.toGroup);
            scope.groups = {};
            for (var i = 0; i < to_group.length; i++) {
                var group = to_group[i][attrs.groupBy];
                if (group) {
                    if (scope.groups[group]) {
                        scope.groups[group].push(to_group[i]);
                    } else {
                        scope.groups[group] = [to_group[i]];
                    }
                }    
            }
        }
      };
});

function TestGroupingCtlr($scope) {

  $scope.MyList = [
    {FirstFieldName:'Group 1', SecondFieldName:'Group a', whatever:'abc'},
    {FirstFieldName:'Group 1', SecondFieldName:'Group b', whatever:'def'},
    {FirstFieldName:'Group 2', SecondFieldName:'Group c', whatever:'ghi'},
    {FirstFieldName:'Group 2', SecondFieldName:'Group a', whatever:'jkl'},
    {FirstFieldName:'Group 2', SecondFieldName:'Group b', whatever:'mno'}
  ];
}

答案 4 :(得分:1)

AngularJS有三个指令可帮助您显示信息组。这些指令是ngRepeat,ngRepeatStart和ngRepeatEnd。我发现了一篇博文,展示了show groups in AngularJS的方式。它的要点是这样的:

<body ng-controller="OrdersCtrl">
  <div ng-repeat-start="customer in customers" class="header">{{customer.name}}</div>
  <div ng-repeat="order in customer.orders">{{order.total}} - {{order.description}}</div>
  <div ng-repeat-end><br /></div>
</body>

一旦你学会了如何使用它们,就会发现非常强大的指令。

答案 5 :(得分:1)

如果您在同一视图中的同一数据集上有多个过滤器,JoshMB的代码将无法正常工作。第二次对数据集的过滤版本进行分组时,它将更改原始对象中的相同属性,从而打破先前过滤版本中的分组中断。

我通过添加&#34; CHANGED&#34;的名称来解决这个问题。 attribute as en extra filter parameter。以下是我更新的代码版本。

/*
 * groupBy
 *
 * Define when a group break occurs in a list of items
 *
 * @param {array}  the list of items
 * @param {String} then name of the field in the item from the list to group by
 * @param {String} then name boolean attribute that indicated the group changed for this filtered version of the set

 * @returns {array} the list of items with an added field name named with "_new"
 *                  appended to the group by field name
 *
 * @example     <div ng-repeat="item in MyList | filter:'a' | groupBy:'groupfield':'Agroup_CHANGED'" >
 *              <h2 ng-if="item.Agroupfield_CHANGED">{{item.groupfield}}</h2>
 *              <!-- now a differen filtered subset -->
 *              <div ng-repeat="item in MyList | filter:'b' | groupBy:'groupfield':'Bgroup_CHANGED'" >
 *              <h2 ng-if="item.Bgroupfield_CHANGED">{{item.groupfield}}</h2>
 *
 *              Typically you'll want to include Angular's orderBy filter first
 */

app.filter('groupBy', ['$parse', function ($parse) {
    return function (list, group_by, group_changed_attr) {

        var filtered = [];
        var prev_item = null;
        var group_changed = false;
        // this is a new field which is added to each item where we append "_CHANGED"
        // to indicate a field change in the list
        //var new_field = group_by + '_CHANGED'; //- JB 12/17/2013
        var new_field = 'group_by_CHANGED';
        if(group_changed_attr != undefined) new_field = group_changed_attr;  // we need this of we want to group different filtered versions of the same set of objects !

        // loop through each item in the list
        angular.forEach(list, function (item) {

            group_changed = false;

            // if not the first item
            if (prev_item !== null) {

                // check if any of the group by field changed

                //force group_by into Array
                group_by = angular.isArray(group_by) ? group_by : [group_by];

                //check each group by parameter
                for (var i = 0, len = group_by.length; i < len; i++) {
                    if ($parse(group_by[i])(prev_item) !== $parse(group_by[i])(item)) {
                        group_changed = true;
                    }
                }


            }// otherwise we have the first item in the list which is new
            else {
                group_changed = true;
            }

            // if the group changed, then add a new field to the item
            // to indicate this
            if (group_changed) {
                item[new_field] = true;
            } else {
                item[new_field] = false;
            }

            filtered.push(item);
            prev_item = item;

        });

        return filtered;
    };
}]);

答案 6 :(得分:0)

编辑:这是一种自定义过滤方法。 Groups由范围内的过滤器函数创建,以从当前列表生成组数组。添加/删除列表项将绑定组数组的更新,因为它在每个摘要周期重置。

HTML

<div ng-app="myApp">
    <div ng-controller='TestGroupingCtlr'>
        <div ng-repeat='group in getGroups()'>
             <h2>{{group}}</h2>
              <ul>
                <!-- could use another scope variable as predicate -->
                <li ng-repeat="item in MyList |  groupby:group">{{item.whatever}}</li>
            </ul>
        </div>
    </div>
</div>

JS

var app=angular.module('myApp',[]);
app.filter('groupby', function(){
    return function(items,group){       
       return items.filter(function(element, index, array) {
            return element.GroupByFieldName==group;
        });        
    }        
})        

app.controller('TestGroupingCtlr',function($scope) {

    $scope.MyList = [{  GroupByFieldName: 'Group 1', whatever: 'abc'},
                     {GroupByFieldName: 'Group 1',whatever: 'def'}, 
                     {GroupByFieldName: 'Group 2',whatever: 'ghi' },
                     {GroupByFieldName: 'Group 2',whatever: 'jkl'}, 
                     {GroupByFieldName: 'Group 2',whatever: 'mno'  }
                    ];
    $scope.getGroups = function () {
        var groupArray = [];
        angular.forEach($scope.MyList, function (item, idx) {
            if (groupArray.indexOf(item.GroupByFieldName) == -1)
              groupArray.push(item.GroupByFieldName)
        });
        return groupArray.sort();
    }

})

DEMO

答案 7 :(得分:0)

http://blog.csdn.net/violet_day/article/details/17023219#t2

<!doctype html>  
<html ng-app>  
<head>  
    <script src="lib/angular/angular.min.js"></script>  
    <script>  
        function TestCtrl($scope) {  
            $scope.items = [  
                { id: 0, name: "Red"},  
                { id: 1, name: "Red"},  
                { id: 2, name: "Red"},  
                { id: 3, name: "Red"},  
                { id: 4, name: "Yellow"},  
                { id: 5, name: "Orange"}  
            ];  
        }  
    </script>  
</head>  
<body ng-controller="TestCtrl">  
<ul ng-repeat="a in items" ng-if="a.name!=items[$index-1].name">  
    {{ a.name }}  
    <li ng-repeat="b in items" ng-if="a.name==b.name">  
        {{ b.id }}  
    </li>  
</ul>  
</body>  
</html>  

答案 8 :(得分:-2)

try this:
<!doctype html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>Example - example-example58-production</title>


  <script src="//ajax.googleapis.com/ajax/libs/angularjs/1.3.0-beta.5/angular.min.js"></script>



</head>
<body ng-app="">
    <script>
    function Ctrl($scope) {
      $scope.friends =
          [{name:'John', phone:'555-1212', age:10},
           {name:'Mary', phone:'555-9876', age:19},
           {name:'Mike', phone:'555-4321', age:21},
           {name:'Adam', phone:'555-5678', age:35},
           {name:'John', phone:'555-1212', age:10},
           {name:'John', phone:'555-1212', age:10},
           {name:'John', phone:'555-1212', age:10},
           {name:'John', phone:'555-1212', age:10},
           {name:'Julie', phone:'555-8765', age:29},
           {name:'Mike', phone:'555-4321', age:21},
           {name:'Adam', phone:'555-5678', age:35},
           {name:'Mike', phone:'555-4321', age:21},
           {name:'Adam', phone:'555-5678', age:35},
           {name:'Mike', phone:'555-4321', age:21},
           {name:'Adam', phone:'555-5678', age:35}]
    }
  </script>
  <div ng-controller="Ctrl">
  <div ng-init="friendx=(friends|orderBy:'age')"> </div>
    <table class="friend" ng-repeat="friend in friendx">
    <tr>
      <td ng-if="friendx[$index].age!=friendx[$index-1].age">{{friend.age}}</td>
    </tr>
      <tr>
        <td>{{friend.name}}</td>
        <td>{{friend.phone}}</td>
        <td>{{friend.age==friendx[$index].age}}</td>
      </tr>
    </table>
  </div>
</body>enter code here
</html>
[http://plnkr.co/edit/UhqKwLx1yo2ua44HjY59?p=preview][1]


  [1]: http://plnkr.co/edit/UhqKwLx1yo2ua44HjY59?p=preview