2

I gave an object as followed

{
    key1: [{...}, {...} ....],
    key2: [{...}, {...} ....],
    .........so on .....
}

I have an ng-repeat ng-repeat="(key, values) in data" and then inside that ng-repeat="val in values"

I want to set up an filter based on some property of objects stored in the array. I have set up below filter

.filter('objFilter', function () {
                return function (input, search,field) {
                    if (!input || !search || !field)
                        return input;
                    var expected = ('' + search).toLowerCase();
                    var result = {};
                    angular.forEach(input, function (value, key) {
                        result[key] = [];
                        if(value && value.length !== undefined){
                            for(var i=0; i<value.length;i++){
                                var ip = value[i];
                                var actual = ('' + ip[field]).toLowerCase();
                                if (actual.indexOf(expected) !== -1) {
                                    result[key].push(value[i]);
                                }
                            }
                        }
                    });
                    console.log(result);
                    return result;
                };

The filter seems to work fine when I use ng-repeat="(date, values) in data| objFilter:search:'url'" but for some reason it is called too many times and causes Infinite $digest Loop. Any solutions??

Edit: I have created below plunker to show the issue. The filter works but look in the console for the errors. http://plnkr.co/edit/BXyi75kXT5gkK4E3F5PI

2
  • Please provide a fiddle with some dummy data so we can debug. Commented Apr 17, 2016 at 12:25
  • I have updated the question with a plunker to show the issue Commented Apr 17, 2016 at 18:08

1 Answer 1

2

Your filter causes an infinite $digest loop because it always returns a new object instance. With the same parameters it returns a new object instance (it doesn't matter if the data inside the object is the same as before).

Something causes a second digest phase. I'm guessing it's the nested ng-repeat. Angular calls filters on every digest phase and because you filter returns a new value it causes the framework to reevaluate the whole outer ng-repeat block which causes the same on the inner ng-repeat block.

Option 1 - modify the filter

One fix you can do is to "stabilize" the filter. If it's called 2 times in a row with the same value it should return the same result.

Replace your filter with the following code:

app.filter('objFilter', function () {
    var lastSearch = null;
    var lastField = null;
    var lastResult = null;

    return function (input, search, field) {
        if (!input || !search || !field) {
            return input;
        }

        if (search == lastSearch && field == lastField) {
            return lastResult;
        }

        var expected = ('' + search).toLowerCase();
        var result = {};

        angular.forEach(input, function (value, key) {
            result[key] = [];
            if(value && value.length !== undefined){
                for(var i=0; i<value.length;i++){
                    var ip = value[i];
                    var actual = ('' + ip[field]).toLowerCase();
                    if (actual.indexOf(expected) !== -1) {
                        //if(result[key]){
                        result[key].push(value[i]);
                        //}else{
                        //    result[key] = [value[i]];
                        //}
                    }
                }
            }
        });

        // Cache params and last result
        lastSearch = search;
        lastField = field;
        lastResult = result;

        return result;
    };
});

This code will work but it's bad and prone to errors. It might also cause memory leaks by keeping the last provided arguments in memory.

Option 2 - move the filter on model change

Better approach will be to remember the updated filtered data on model change. Keep you JavaScript as is. Change only the html:

<body ng-controller="MainCtrl">
  <div ng-if="data">
    Search: 
    <input type="text"  
           ng-model="search" 
           ng-init="filteredData = (data | objFilter:search:'url')"
           ng-change="filteredData = (data | objFilter:search:'url')">

    <div ng-repeat="(date, values) in filteredData">
      <div style="margin-top:30px;">{{date}}</div>
      <hr/>
      <div ng-repeat="val in values" class="item">
        <div class="h-url">{{val.url}}</div>
      </div>
    </div>
  </div>
</body>

First we add a wrapper ng-if with a requirement that data must have a value. This will ensure that our ng-init will have "data" in order to set the initial filteredData value.

We also change the outer ng-repeat to use filteredData instead of data. Then we update filtered data on the model change with the ng-change directive.

  • ng-init will fire once after data value is set
  • ng-change will be executed only when the user changes the input value

Now, no matter how many consecutive $digest phases you'll have, the filter won't fire again. It's attached on initialization (ng-init) and on user interaction (ng-change).

Notes

Filters fire on every digest phase. As a general rule try avoiding attaching complex filters directly on ng-repeat.

  • Every user interaction with a field that has ng-model causes a $digest phase
  • Every call of $timeout causes a $digest phase (by default).
  • Every time you load something with $http a digest phase will begin.

All those will cause the ng-repeat with attached filter to reevaluate, thus resulting in child scopes creation/destruction and DOM elements manipulations which is heavy. It might not lead to infinite $digest loop but will kill your app performance.

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

8 Comments

I thought of going with the second approach. This works but I get my data with an $http and by the time it loads data, ng-init is already initialized hence shows an empty list. But right after I search something it works because the filter is evaluated. Any idea how I can run the filter after I load data?
Also this will not update my mg-repeat when I change my data.
It should't show an empty list. In my example there is an ng-if="data" on a parent element. Ng-init won't be executed before data has a non null value. Try it.
Also, if you need to update data and get filteredData synced consider making your filter a service and then calling it from the controller when there is an update condition. This is what I would do. Your code is getting too compex for a filter.
I tried this $scope.filteredHistory = $filter('objFilter')($scope.data,$scope.search,'url') when ever I am changing data but for some reason it doesn't update the view. Putting $scope.filteredHistory in console shows the result tough.
|

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.