Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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?

like image 722
cashmere Avatar asked Jul 16 '13 03:07

cashmere


2 Answers

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.

like image 108
OverZealous Avatar answered Sep 20 '22 12:09

OverZealous


<li class="divider" ng-if="FormsList[$index].FORM_GROUP!=FormsList[$index-1].FORM_GROUP">

Works perfectly for me. like this

<input type="text" class="form-control" placeholder="Search Form..." ng-model="selectedForm">

<ul class="nav nav-pills nav-stacked" role="menu" ng-repeat="Form in FormsList | filter:selectedForm">
    <li class="divider" ng-if="FormsList[$index].FORM_GROUP!=FormsList[$index-1].FORM_GROUP">{{Form.FORM_GROUP}}</li>
    <li><a href="#" ng-click="LoadPDF(Form.LOCATION)">{{Form.LINK}}</a></li>
</ul>
like image 29
Somnath Avatar answered Sep 21 '22 12:09

Somnath