2

In a directive, I am using ngRepeat that gets its items from a parametrized function on the scope.

In a simplified manner, it looks like this:

<div data-ng-repeat="data in containerItems">
    <div data-ng-repeat="myItem in getItems(data) track by myItem.Id">
        <!-- some content -->
    </div>
</div>

My problem is that I cannot seem to get rid of the following exception:

Error: [$rootScope:infdig] 10 $digest() iterations reached. Aborting!

As far as I could figure out based upon several other questions (such as this, or this), ngRepeat appears to think the items are updated each time it re-requests them, causing it prompt yet another digest cycle, eventually ending up with too many consecutive of them and giving up.

In particular, one comment by user g00fy was insightful to me: Indeed, I am using lodash's _.map function to generate the list of items displayed by ngRepeat, thus creating new object instances each time ngRepeat re-requests its items.

At first, I thought I could solve this by saving the return value of my item generation function into a variable on the scope. This did away with the above error.

Unfortunately, it is not a suitable solution for me, as the scope is shared by several instances of my directive within which ngRepeat is used. I really need each ngRepeat retrieve the items from the generation function with the appropriate parameter.

At first, I was using

track by $index

but I have also tried something like

track by myItem.Id

I had hoped that this would force ngRepeat to assume the item was changed only when an ID changes. However, none of this seems to prevent the above exception from happening.

Is there any way I can force AngularJS to only consider an item in ngRepeat changed when its values have changed, rather than the instance?

Structurally, it seems like the return value of getItems(data) could be saved on data. However, code in the framework of the application I am working on (which means it is relied upon to behave as it does by many modules of the application and must not be changed) will directly take the object graph that contains data and send that to the server-side backend. Hence, data is not the right place for computed runtime data.


To provide a more complete example, the getItems function looks roughly like this:

$scope.getItems = function (data) {
    var itemSet = itemSets[data.itemSetId];
    if (itemSet.customItems) {
        return itemSet.customItems.slice(1);
    } else {
        return _.map(standardItems.slice(1), function (si) {
            return {
                Id: si.code,
                Name: si.Description + ' ' + si.Owner
            };
        });
    }
};

The specifics here are not relevant. The important part is that I retrieve the items from elsewhere depending on something in the parameter data, and then the items may be in a format that requires transformation into a canonical format that is processed by my ngRepeat-ed template.

O. R. Mapper
  • 20,083
  • 9
  • 69
  • 114

2 Answers2

2
  1. If getItems(data) never changes, you can use one-time binding: ng-repeat="a in ::getItems(data)"
  2. If it changes, you should store it result in $scope and update it manually -- this is probably best way. Using methods in ng-repeat is bad style and would most probably cause perfomance issues later. So, this looks like:
<div data-ng-repeat="myItem in myItems track by myItem.Id">
$scope.myItems = getItems(data);
//somewhere you need to update myItems -- can be not easy
  1. Your problem is that your getItems should not create objects, so you may fix it by caching object or full result (see Deblaton answer), but this is no good in general.So, this looks like:

    <div data-ng-repeat="myItem in getItems(data) track by myItem.Id">
    
    $scope.objectCache = {};
    $scope.getItems = function (data) {
        var itemSet = itemSets[data.itemSetId];
        if (itemSet.customItems) {
            return itemSet.customItems.slice(1);
        } else {
            return _.map(standardItems.slice(1), function (si) {
                if (!objectCache[si.code]) { 
                    objectCache[si.code] = {
                       Id: si.code,
                       Name: si.Description + ' ' + si.Owner
                    }; 
                }
                return objectCache[si.code];
            });
        }
    };
    
Petr Averyanov
  • 9,327
  • 3
  • 20
  • 38
  • Wait, how is 2 ("probably best way") different from 3 ("no good in general")? You mean I should not use data sources that supply data in different formats? Well, that's simply the way it is; I have no control about how data from other components is shaped. – O. R. Mapper Dec 12 '17 at 11:18
  • One time binding is the best option (better than mine). However, regarding OP needs to have a single method to fetch its data, 3 is better than 2 ;-) – Deblaton Jean-Philippe Dec 12 '17 at 11:26
  • "somewhere you need to update myItems -- can be not easy" - kind of, yes. Basically, I need to update `myItems` between two executions of `
    ` that would use different `myItems` and that are shown next to one another.
    – O. R. Mapper Dec 12 '17 at 11:28
  • @O.R.Mapper No, if you have 2 different getItems(1), getItems(2) -> you should have myItems1, myItems2 in scope. I mean other changes – Petr Averyanov Dec 12 '17 at 11:30
1

So, I understand you have a complex mechanism to fetch your data, that you need to share between a lot of ng-repeat.

Because the way dirty checking works with angularJS (simple === between hold value, and $scope value), you have to assign your ng-repeat to a single reference (and then use an immutable object if ever the collection is updated).

Is there a reason why you couldn't do something like the following to fetch your data?

myItems = {};
$scope.getItems = function (data) {
    if(!myItems[data.itemSetId]) {
        var itemSet = itemSets[data.itemSetId];
        if (itemSet.customItems) {
            myItems[data.itemSetId] = itemSet.customItems.slice(1);
        } else {
            myItems[data.itemSetId] _.map(standardItems.slice(1), function (si) {
                return {
                    Id: si.code,
                    Name: si.Description + ' ' + si.Owner
                };
            });
        }
    }
    return myItems[data.itemSetId];
};
Deblaton Jean-Philippe
  • 11,188
  • 3
  • 49
  • 66
  • That seems pretty hacky (why cache something that can, without any effort, be computed), but if the direct answer to my titular question is "You cannot.", it seems I'll have to take this route. I'll accept a bit later, just in case another, better solution turns up. – O. R. Mapper Dec 12 '17 at 11:27
  • @O.R.Mapper I hope you'll try `::` before accepting my answer. – Deblaton Jean-Philippe Dec 12 '17 at 13:00
  • @O.R.Mapper your component is also maybe too big for a presentation component. You could probably split your code into sub components to improve your clarity. – Deblaton Jean-Philippe Dec 12 '17 at 13:01
  • The component was written by someone else and is used in various places in the application. I am carefully trying to weave in a new feature without breaking the other parts, so large reorganizations are probably not feasible. With that said, the part that I shortened to `` in my original sample *is* already just invoking some other directives of ours. – O. R. Mapper Dec 12 '17 at 13:05
  • As for `::`, that doesn't seem to work: The result of `getItems` changes when something in `data` changes, and if I write `::getItems(data)`, the `ngRepeat` does not reevaluate `getItems` any more once that something in `data` has changed. – O. R. Mapper Dec 12 '17 at 13:08
  • @O.R.Mapper `::` will be re-evaluated until the response from `getItems()` is not undefined. You can maybe use it at your advantage. – Deblaton Jean-Philippe Dec 12 '17 at 13:11