cashmere cashmere - 2 months ago 15
AngularJS Question

Ng-Repeat ng-show if previous item field value different

If have a list of items with date field which is ordered by date. I want to group these items by date somethings like;

Date1




  • item1



Date2




  • item2

  • item3



and so forth...

What I have is this;

<ul>
<li ng-repeat="listing in listings | filter:query">
<h1 ng-show="$index === 0 || listings[$index - 1].created !== listing.created">
{{listing.created | date:mediumDate}}
</h1>
{{listing.title}}
</li>
</ul>


But this way h1 element becomes the child of li element which messes up presentation. Is there some other way to write this ng-repeat the way I want?

Answer

I have a solution that might not work depending on the number of items in your list.

My idea was to apply a filter to transform a simple list into a hierarchical one.

Here's a working jsFiddle. (See the new one at the bottom, really!)

The idea is simple, take an existing list, and rearrange it on-the-fly:

<div ng-repeat="g in items | groupBy:'group' | orderBy:'group'">
    <h2>{{g.group}}</h2>
    <ul>
        <li ng-repeat="item in g.items | orderBy:'title'">{{item.id}}. {{item.title}}</li>
    </ul>
</div>

In this case, we've take the original array, and applied a groupBy filter on it. This filter is actually sort of complex, because it needs to make sure it doesn't modify the array more than necessary, so it has to store a deep copy of the original array to compare against. If you remove that deep copy and inspection, you will end up with $digest iteration errors every time.

Edit: See bottom for a modified version that performs shallower copies of the array

Here's the filter code:

app.filter("groupBy", function() {
    var mArr = null,
        mGroupBy = null,
        mRetArr = null;
    return function(arr, groupBy) {
        if(!angular.equals(mArr, arr) || mGroupBy !== groupBy) {
            mArr = angular.copy(arr);
            mGroupBy = groupBy;
            mRetArr = [];
            var groups = {};
            angular.forEach(arr, function(item) {
                var groupValue = item[groupBy]
                if(groups[groupValue]) {
                    groups[groupValue].items.push(item);
                } else {
                    groups[groupValue] = {
                        items: [item]
                    };
                    groups[groupValue][groupBy] = groupValue;
                    mRetArr.push(groups[groupValue]);
                }
            });
        }
        return mRetArr;
    };
});

(mArr, mGroupBy, and mRetArr are the "memoized" values, here.)

What's nice about this filter is that it doesn't require that the data come in already grouped. The jsFiddle linked above include a button to dynamically add items to the array at the end, but they still end up sorted into groups.

Again, the real caveat is that it could be slow and/or expensive based on the number of items in your list. Hundreds of moderately-complex items should be OK, but if you have thousands, you should pre-sort them somewhere else before they get to the scope.


Edit: Much better version

At the cost of more code complexity, I modified the filter to copy only the key information instead of deeply copying the whole array. This time, I make a hashmap of the groups containing the items. The items are still referenced, but this time it's an explicit reference, so modifying an item in the tree will not force a resort, but changing an item's group or adding or removing an item will.

New fiddle: http://jsfiddle.net/hhWaX/2/

And here's the new filter:

app.filter("groupBy", function() {
    var mArr = null,
        mGroupBy = null,
        mRetArr = null,
        getMemoArr = function(arr, groupBy) {
            var ret = {};
            angular.forEach(arr, function(item){
                var groupValue = item[groupBy];
                if(ret[groupValue]) {
                    ret[groupValue].push(item);
                } else {
                    ret[groupValue] = [item];
                }
            });
            return ret;
        };
    return function(arr, groupBy) {
        var newMemoArr = getMemoArr(arr, groupBy);
        if(mGroupBy !== groupBy || !angular.equals(mArr, newMemoArr)) {
            mArr = newMemoArr;
            mGroupBy = groupBy;
            mRetArr = [];
            var groups = {};
            angular.forEach(arr, function(item) {
                var groupValue = item[groupBy]
                if(groups[groupValue]) {
                    groups[groupValue].items.push(item);
                } else {
                    groups[groupValue] = {
                        items: [item]
                    };
                    groups[groupValue][groupBy] = groupValue;
                    mRetArr.push(groups[groupValue]);
                }
            });
        }
        return mRetArr;
    };
});

It's a lot more complicated, but this one should have significantly better performance over large sets, because there is the absolute minimum of duplication of the original array.

Comments