Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

AngularJS Group By Directive without External Dependencies

I'm new to Angular and would like to learn the best way to handle a problem. My goal is to have a reusable means to create group by headers. I created a solution which works, but I think this should be a directive instead of a scope function within my controller, but I'm not sure how to accomplish this, or if a directive is even the right way to go. Any inputs would be greatly appreciated.

See my current approach working on jsFiddle

In the HTML it's a simple list using ng-repeat where I call my newGrouping() function on ng-show. The function passes a reference to the full list, the field I want to group by, and the current index.

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

In my controller I have my newGrouping() function which simply compares the current to the previous, except on the first item, and returns true or false depending upon a match.

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

The output will look like this.

Group 1

  • abc
  • def

Group 2

  • ghi
  • jkl
  • mno

It feels like there should be a better way. I want this to be a common utility function that I can reuse. Should this be a directive? Is there a better way to reference the previous item in the list than my method of passing the full list and the current index? How would I approach a directive for this?

Any advice is greatly appreciated.

UPDATE: Looking for an answer that does not require external dependencies. There are good solutions using underscore/lodash or the angular-filter module.

Darryl

like image 834
Darryl Avatar asked Nov 15 '13 01:11

Darryl


People also ask

How do I group data in AngularJS?

To group data, users can drag and drop column headers onto and from the area above the grid. This area is called the groupPanel. Set its visible property to true to show it.

What is $inject in AngularJS?

Dependency Injection in AngularJS can be defines as the software design pattern which defines the way the software components are dependent on each other. AngularJS provides a set of components that can be injected in the form of dependencies such as factory, value, constant, service, and provider.

What is $scope in AngularJS?

AngularJS Scope The scope is the binding part between the HTML (view) and the JavaScript (controller). The scope is an object with the available properties and methods. The scope is available for both the view and the controller.

What is $rootScope in AngularJS?

The $rootScope is the top-most scope. An app can have only one $rootScope which will be shared among all the components of an app. Hence it acts like a global variable. All other $scopes are children of the $rootScope.


2 Answers

This is a modification of Darryl's solution above, that allows multiple group by parameters. In addition it makes use of $parse to allow the use of nested properties as group by parameters.

Example using multiple, nested parameters

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>  

Filter (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;
    };
}]);
like image 115
JoshMB Avatar answered Oct 21 '22 05:10

JoshMB


If you are already using LoDash/Underscore, or any functional library, you can do this using _.groupBy() (or similarly named) function.


In controller:

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

In template:

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

This will renders:

English

  • Edge of Tomorrow
  • X-MEN

Telugu

  • Gabbar Singh 2
  • Resu Gurram

Even better, this can be also converted into a filter very easily, without much of boilerplate code to group elements by a property.

Update: Group by multiple keys

Often grouping using multiple keys is very useful. Ex, using LoDash (source):

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

Update on why I recommend this approach: Using filters on ng-repeat/ng-options causes serious perf issues unless that filter executes quickly. Google for the filters perf problem. You'll know!

like image 24
manikanta Avatar answered Oct 21 '22 04:10

manikanta