Stop Digest Errors in Angular Filters

Lately I have been working on a fairly large project for a very important client that is basically a large search application with all sorts of custom filters. Filters are built of different topics which all include a large data set with a lot of properties (basically results of an SQL query) and a lot of custom filters to filter that data set in real time on frontend as user changes these custom filters.

The biggest issue of all was how to implement a totally dynamic filter function that can filter on the entire data set at once without impact on performance.

Basically a custom filter has been born:

JavaScript

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

(function(){

'use strict';

/**

* @ngdoc overview

* @name app.filters: filterMultiple

* @description

* Filter a large data set with different field types

*/

angular

.module('app.filters.filterMultiple',[])

.filter('filterMultiple',['$filter',function($filter){

function(items,keyObj){

varfilterObj={

filteredData:[],

applyFilter:function(obj,key,display){

...

...

}

};

if(keyObj){

angular.forEach(keyObj,function(obj,key){

filterObj.applyFilter(obj.value,key,obj.display);

});

}

returnfilterObj.filteredData;

}

}]);

})();

If I explain this filter a bit… with and screenshot of the functionality.

Here we have a list of Filters on the right and a list of components that are represented in a large list. The components posses values that can be filtered using the filters on the left. Angular has pretty decent support for filter out of the box, but it offers only a one-on-one mapping of objects. I needed a way to filter components on the right by range, date range, boolean, or any other supported type. So an idea of Multiple Filter was born.

In the script above you can see that first I set a filteredObject, which holds all the needed filter methods, filteredData and applyFilter method. Then every time the filter is called I run a “for loop” and apply filters on the entire data set, based on the filters array that is passed a parameter into the function. Clever huh! Well it turns out not so much!

A critical mistake:

XHTML

1

2

3

<ul>

<li ng-repeat='option in options track by $index'></li>

</ul>

I thought I was being pretty clever, but unfortunately my filter was producing – aaaarrrrrghhhhhh! My Chrome console log was so red I tought it was bleeding 😀 It was the famous 10 $digest() iterations reached. Aborting! error. Why was it happening?

I digged around the web and found a solution!

Here’s what was going on. In each Angular digest cycle, filters get executed, taking in some input and sending out some output. Angular knows it doesn’t need to run another digest cycle if and only if the output of the filter on this digest cycle is identical to the output of the filter from the previous one (assuming the input is the same.) In other words, the digests repeat until things settle down.

What I learned was priceless. As it turned out my custom filter was outputting different objects each time it was run even if the input (the data set) never changed.

To illustrate the example, check out the following two examples:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

/**

* Two objects can have identical properties,

* with identical values, and still not be identical

*/

varobject1={val:5};

varobject2={val:5};

console.log(object1===object2);//false.

/**

* This kind of mimics a pointer...

*/

varobject1={val:5};

varobject2=object1;

object2.val=10;

console.log(object1===object2);//true

console.log(object1.val);// 10

So this means that object or array input would yield an object or array output every time the filter was run, but the output objects would be different every time (despite the properties being exactly the same.) Angular would detect this difference and repeat the digest cycle. You basically get an infinite loop of digest operations, that is smartly prevented by Angular by stopping it after 10 iterations and making your console log bleed 😀

Using Underscore’s Memoize To Make Things Right Again

For identical input, a good function returns identical output. Unfortunately, if your function creates objects from its inputs, it will never return identical output for identical input.

The solution is to use underscore’s memoize method. What we want is that our filters keeps a record of its inputs and outputs by caching hashed values. So if an input was already seen before the output must be the same as before as this will break the digest cycle of Angular. This is where
_.memoize() comes in handy. Feed it a function, and it returns a version of that function that keeps track of inputs and outputs. By using memoize my filter returns exactly the same objects it did the first time it was run, as long as the input to the filter was the same.

So the modified filter code would be (notice the bolded memoize function):

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

(function(){

'use strict';

/**

* @ngdoc overview

* @name app.filters: filterMultiple

* @description

* Filter a large data set with different field types

*/

angular

.module('app.filters.filterMultiple',[])

.filter('filterMultiple',['$filter',function($filter){

return_.memoize(

function(items,keyObj){

varfilterObj={

filteredData:[],

applyFilter:function(obj,key,display){

...

...

}

};

if(keyObj){

angular.forEach(keyObj,function(obj,key){

if(!angular.isUndefined(obj.values)){

varv=obj.values.split(' - ');

varlimits={min:v[0],max:v[1]};

filterObj.applyFilter(limits,key,obj.display);

}

else{

filterObj.applyFilter(obj.value,key,obj.display);

}

});

}

returnfilterObj.filteredData;

});

}]);

})();

But another problem quickly comes out. My filter is now not responding to any changes filters. So as it turns out, the memoize function accepts two parameters
_.memoize(function,[hashFunction]) . The hash function looks only the first argument and my filter always receives identical first argument, so naturally memoize returns the identical output. To fix that final issue, a custom hash function must be provided: