2

I've created a custom filter in angularjs, which groups my elements by date.

This is the HTML part:

<table ng-repeat="(index, groupData) in something = (recentTasks | groupBy:dataGroupBy) track by $index">

The filter looks like this:

module.filter('groupBy', function () {
    return function(items, field) {
        var groups = [];

        switch (field) {
            case 'week':
                angular.forEach(items, function(item) {
                    var parsed = parseDateTime(item.date);
                    var date = new Date(parsed.year, parsed.month - 1, parsed.day);
                    var back = calculateWeeksBack(date);

                    if (groups[back] == undefined)
                    {
                        groups[back] = {
                            time_back: calculateWeeksBack(date),
                            tasks: []
                        };
                    }

                    groups[back].tasks.push(item);
                    groups[back].total_time += item.time;
                });

                break;

            case 'month':
                angular.forEach(items, function(item) {
                    var parsed = parseDateTime(item.date);
                    var date = new Date(parsed.year, parsed.month - 1, parsed.day);
                    var back = calculateMonthsBack(date);

                    if (groups[back] == undefined)
                    {
                        groups[back] = {
                            time_back: calculateMonthsBack(date),
                            tasks: []
                        };
                    }

                    groups[back].tasks.push(item);
                    groups[back].total_time += item.time;
                });

                break;

            default:
                angular.forEach(items, function(item) {
                    var parsed = parseDateTime(item.date);
                    var date = new Date(parsed.year, parsed.month - 1, parsed.day);
                    var back = calculateDaysBack(date);

                    if (groups[back] == undefined)
                    {
                        groups[back] = {
                            time_back: calculateDaysBack(date),
                            tasks: []
                        };
                    }

                    groups[back].tasks.push(item);
                    groups[back].total_time += item.time;
                });
        }
        return groups;
    }
});

What it does - the input (recentTasks) is an array of tasks. Every task has a 'date' parameter defined. I need to separate these tasks by date - every day will be in separate table. It works, but I get infinite digest loop.

Could you help me how to solve my problem? Or is there a better solution?

EDIT: Examples of input and output:

$scope.items = [
    {name: 'Abc', date: '2014-03-12'},
    {name: 'Def', date: '2014-03-13'},
    {name: 'Ghi', date: '2014-03-11'},
    {name: 'Jkl', date: '2014-03-12'}
]

The output has to be grouped like this:

[
    '2013-03-11': [
        {name: 'Ghi'}
    ],
    '2013-03-12': [
        {name: 'Abc'},
        {name: 'Jkl'}
    ],
    '2013-03-13': [
        {name: 'Def'}
    ]
]

Because items for every single day are in separated table in HTML structure.

<table ng-repeat="dayData in groupBy(items, 'day')">
    <thead>
        <tr><td>{{ dayData.date }}</td> </tr>
    </thead>
    <tbody>
        <tr ng-repeat="item in dayData.items">
            <td>{{ item.name }}</td>
        </tr>
    </tbody>
</table>

3 Answers 3

9

To understand why this happens, you need to understand the digest cycle. Angular is based on "dirty checking", and a digest cycle is when Angular iterates over all properties on the scope to see which has changed. If any properties has changed, it fires all watches for those properties to let them know that something has happened. And since a watch can change properties on the scope, Angular runs another round of dirty checking after the watches are done. The digest cycle stops when it has iterated over all properties and it sees that none of them has changed. An infinite digest occurs when a watch is always setting a new value to a property. Something like this might explain it better:

$scope.myNumber = Math.random();
$scope.$watch('myNumber', function() {
  $scope.myNumber = Math.random();
});

That is, our watch will never stop being called, since it's always changing the value of myNumber. Another common cause of the error is when you have two properties and two watches, like this:

$scope.prop1 = Math.random();
$scope.prop2 = Math.random();

$scope.$watch('prop1', function() {
  $scope.prop2 = Math.random();
});
$scope.$watch('prop2', function() {
  $scope.prop1 = Math.random();
});

Those watches will trigger each other in an endless loop.

So what happens in your case is that your filter is always returning a new array with new objects in them. And Angular doesn't compare objects by inspecting all properties and comparing them, instead it adds a $$hashkey property and uses that to compare to other objects. If two objects have the same $$hashkey property, they are considered equal. So even if you're returning an identical data structure each time, Angular sees that as a new object and runs another digest cycle until it gives up.

So for your filter to work, you need to change your code so that for the same arguments passed in, it returns an array with the same objects, as in, the same references to those objects. It should be enough to make sure that you don't always create a new object for groups[back] but reuse a previous object.

EDIT:

Well, I'm going to disappoint you and say that I recommend you to rethink your filter. It's going to be a bit confusing for other developers (or yourself a year from now) that you have a filter on an ng-repeat that doesn't return a subset of the list to repeat, but a new data structure instead. Take this example:

// Controller
$scope.items = [
  {name: 'Buy groceries', time: 10},
  {name: 'Clean the kitchen', time: 20}
];

// Template
<li ng-repeat="item in items | groupBy:something">
  {{item.total_time}}
</li>

Someone is going to look at the controller to see what an item contains, and they're going to see that it has a name and a time property. Then they look in the template, and see that there's a total_time property, and they won't know that it comes from the groupBy filter since filters in Angular normally doesn't mutate the objects.

What I think you should to is to extract your grouping code into a function on your scope (or create a service), and only use the filter to filter out unrelated items.

That way, your template will look like this instead:

<li ng-repeat="item in groupBy(items, 'something')">
  {{item.total_time}}
</li>

That will be much less confusing, since it's obvious that the groupBy() method returns a new data structure. It'll also fix the infinite digest error for you.

Sign up to request clarification or add additional context in comments.

2 Comments

Thanks for the explanation, now I understand why I'm getting the errors. But I still don't know, how to solve it. Could you please try to help me a little bit more? Thanks forward!
Thanks a lot for your answer! Actually, I don't really know, how to do it without changing the structure. The thing is, that in my input (the 'items') are unsorted tasks. In the output, I need to have the tasks sorted and grouped. I've added some examples how it should look like to my first post. Could you please help me how to find a solution now?
3

Multiple sources will tell you not to create new mutable objects on every call of a function in ngRepeat, as the digest cycle will check several times to see if the object is the same before deciding it's a stable model, and each new call will create an equivalent but not identical object. As mentioned by others, "The proper solution is to use a stable model, which means assigning the array to the scope/controller instead of using a getter." I ran into this problem when working with nested ngRepeats, which made it difficult to do this without a getter.

The easiest solution I found was to use ngInit to initialize a variable to reference the result of your function call, and then ngRepeat through that stable variable. For example:

<div ng-init="issues=myCtrl.getUniqueIssues()">
    <div ng-repeat="issue in issues">{{issue.name}}</div>
</div>

My source has an example with a filter as well: https://tahsinrahit.wordpress.com/2015/01/04/solution-of-infinite-digest-loop-error-for-ng-repeat-with-object-property-filter-in-angularjs/

Comments

1

There are 2 ways to solve this problem :

  1. Use a $$hashKey attribute with each element of you array. Angular doesn't compare objects by inspecting all properties and comparing them, instead it adds a $$hashkey property and uses that to compare to other objects. If two objects have the same $$hashkey property, they are considered equal. So even if you're returning an identical data structure each time, Angular sees that as a new object and runs another digest cycle until it gives up. See :

    • Old fiddle with $digest problem - jsfiddle.net/LE6Ay

    • $$hashKey property added- jsfiddle.net/LE6Ay/1

  2. Do not create new objects on every getList() call. The proper solution is to use stable model, which means assigning the array to the scope/controller instead of using a getter.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.