I would like to perform a group by function inside of an ng-repeat
Given the following data:
var items = [];
items.push({ id: 1, widgetId: 54, colorId: 45 });
items.push({ id: 2, widgetId: 54, colorId: 72 });
items.push({ id: 3, widgetId: 54, colorId: 29 });
items.push({ id: 4, widgetId: 55, colorId: 67 });
items.push({ id: 5, widgetId: 55, colorId: 29 });
items.push({ id: 6, widgetId: 56, colorId: 29 });
items.push({ id: 7, widgetId: 56, colorId: 72 });
items.push({ id: 8, widgetId: 57, colorId: 75 });
I would like an ng-repeat that results in the following presentation
widgetId 54 colorId: 45 colorId: 72 colorId 29
widgetId 55 colorId: 67 colorId: 29
widgetId 56 colorId: 29 colorId: 72
widgetId 57 colorId: 75
...and markup
<div class="container">
<div class="row">
<div>widgetId: 54</div>
<div>
<div>colorId: 45</div>
<div>colorId: 72</div>
<div>colorId: 29</div>
</div>
</div>
<div class="row">
<div>widgetId: 55</div>
<div>
<div>colorId: 67</div>
<div>colorId: 29</div>
</div>
</div>
<div class="row">
<div>widgetId: 56</div>
<div>
<div>colorId: 29</div>
<div>colorId: 72</div>
</div>
</div>
<div class="row">
<div>widgetId: 57</div>
<div>
<div>colorId: 75</div>
</div>
</div>
</div>
Any suggestions that don't include creating separate arrays? The data is coming to me this way and it would be nice to avoid manipulating it.
Update - the simple, clean way:
Use npm modules! Lodash can handle the groupBy and the memoization needed to avoid an infinite loop as an Angular filter.
npm install lodash
var memoize = require('lodash/function/memoize');
var groupBy = require('lodash/collection/groupBy');
app.filter('groupBy', function() {
return memoize(groupBy);
});
You may need to use the resolver
function of lodash's memoize:
app.filter('groupBy', function() {
return memoize(function() {
return groupBy.apply(null, arguments);
}, function() {
return JSON.stringify([].slice.call(arguments));
});
});
But, I really believe you should just simplify all of this and filter in the controller:
$scope.foo = function() { // run this when user clicked button, etc
$scope.groupedItems = groupBy($scope.items, 'stuff');
};
Old Answer:
I suggest a groupBy
filter to modify the data used in the view on the fly. Here's what I came up with. This filter returns a new object each time which will cause an infinite digest cycle, so I wrapped it in my service that fixes those kinds of problems. This one is simply fixed by memoization
. Memoization
meeans that given the same parameters (input, prop), the exact same output will be returned from a cache, so the same object is returned again, rather than creating a new one that looks the same. This filter also supports nested property names, so you can easily group by a property nested within the objects.
Live Demo
<div class="container">
<div class="row" ng-repeat="(setKey, set) in items | groupBy:'widgetId'">
WidgetId: {{setKey}}
<div ng-repeat="item in set">
ColorId: {{item.colorId}}
</div>
</div>
</div>
The filter:
.filter('groupBy', [
'$parse',
'pmkr.filterStabilize',
function($parse, filterStabilize) {
function groupBy(input, prop) {
if (!input) { return; }
var grouped = {};
input.forEach(function(item) {
var key = $parse(prop)(item);
grouped[key] = grouped[key] || [];
grouped[key].push(item);
});
return grouped;
}
return filterStabilize(groupBy);
}])
My filterStabilize
service should fix any filter, but any good memoize
function will do fine in this case (and most cases).
Try like this:
<div class="container">
<div class="row" ng-repeat="(key, value) in items| groupBy: 'widgetId'">
<div>widgetId: {{ key }}</div>
<div>
<div ng-repeat="color in value">
colorId: {{color.colorId}}
</div>
</div>
</div>
</div>