3

I have a custom filter that takes in an Object, subjectBin, and returns a brand new Object, result. The filter is invoked from a ng-repeat. My custom filter works, but throws infinite $digest errors. Here is my filter:

   return function(subjectBin, field) {

    var result = {},
        faculty,
        subject;

    angular.forEach(subjectBin, function (value, key) {
        faculty = key;
        angular.forEach(value, function (value, key) {
            subject = key;
            value.forEach(function (course) {
                // Check "field" against some of the object's properties
                //
                if (course.asString.toUpperCase().indexOf(field) > -1 ||
                    course.subjectTitle.toUpperCase().indexOf(field) > -1 ||
                    faculty.toUpperCase().indexOf(field) > -1 ) {
                    if (result.hasOwnProperty(faculty)) {
                        if (result[faculty].hasOwnProperty(subject)) {
                            result[faculty][subject].push(course);
                        }
                        else {
                            result[faculty][subject] = [course];
                        }
                    }
                    else {
                        result[faculty] = {};
                        result[faculty][subject] = {};
                        result[faculty][subject] = [course];
                    }
                }
            });
        });
    });

    return result;
    };

To my understanding, this is giving infinite $digest errors because my filter is returning a brand new Object every time the $digest cycle happens. And this causes the dirty-bit to be set again, and again and again...

But then I get confused when I see examples like this:

  .filter('reverse', function() {
return function(input, uppercase) {
  input = input || '';
  var out = "";
  for (var i = 0; i < input.length; i++) {
    out = input.charAt(i) + out;
  }
  // conditional based on optional argument
  if (uppercase) {
    out = out.toUpperCase();
  }
  return out;
};
})

This is an example from the Angular doc's, and it clearly takes in input and returns a brand new string out. It also does not throw any infinite $digest errors. In my eyes, this is analogous to my filter in that it returns a brand new Object.

Any insight on why my filter is throwing infinite $digest errors, but then this other filter does not?

2
  • Check out my answer here (I marked your question as a duplicate to this post) stackoverflow.com/a/25315679/1435655 I explain the problem a bit there and in the GitHub link. You can keep your filter function as is and just wrap it in a memoize function return memoize(function() { // your filter });, though I prefer my stabilize service: return stabilize(function() { // your filter }); Commented Nov 15, 2014 at 5:19
  • The example you posted does not return an object per se, returns a string (or string object) which is slightly different. Commented Nov 15, 2014 at 5:55

1 Answer 1

7

In your snippet of code, it seems you're radically changing the structure of your data.

Angular filters are not meant to perform deep changes in objects. They are meant, as the name says, to filter information. If your data manipulation requires you to change the data structure, you're probably doing it wrong.

In fact, in it's cycle digest, Angular is smart enough to successfully compare, by itself:

  • strings of any kind (literals and string objects) -- "foo" equals new String("foo")

  • 1 dimensional arrays by their elements ['a', 'b', 'c'] equals new Array(['a', 'b', 'c'])

  • 1 dimensional objects by their elements foo={a:'a', b:'b'} equals bar={a:'a', b:'b'}

However, things start get messy with multidimensional arrays or objects. For instance, returning something a NEW array like this one ['a', 'b', ['c', 'd']] from a filter will cause an infinite loop in the digest cycle, because comparing oldValue with NewValue is ALWAYS false. You can say Angular performs a shallow comparisons.

To sum up, returning NEW multidimensional objects or arrays in a filter will reach the infinite $digest error.


So what if I have a complex multidimensional data structure and I need to perform some comparison in a deep level?

If you don't create new objects in each cycle, then nothing prevents you from doing deep comparisons. However, data should be correctly structured .

For instance, in your case it seems you're using objects as a keymap, like this:

var subjectBin = {
        faculty1: {
            subject1: ['math'],
            subject2: ['science', 'history'],
            subject3: ['foo', 'blabla'],
            subject4: ['unraveling', 'the mistery']
        },
        faculty2: {
            subject1: ['that', 'all'],
            subject2: ['started', 'with a'],
            subject3: ['foo', 'blabla'],
            subject4: ['bigbang', 'BANG!']
        }
    };

This is a hard structure to filter because there is no common pattern (in fact, in your filter you're using 3 loops!!!).

You can restructure the information like this:

var subjectBin = [{
        facultyName: 'faculty1',
        subjects: [{
            subjectName: 'subject1',
            courses: ['math']
        }, {
            subjectName: 'subject2',
            courses: ['science', 'history']
        }]
    }];

This will be much easier to use. In fact, you don't even need custom filters, you can use the default angular filters for simple comparisons

here's an example with data restructuration logic too(fiddle).

var app = angular.module('app', []);

app.controller('fooCtrl', ['$scope',
  function($scope) {
    var bla = {
      faculty1: {
        subject1: ['math'],
        subject2: ['science', 'history'],
        subject3: ['foo', 'blabla'],
        subject4: ['unraveling', 'the mistery']
      },
      faculty2: {
        subject1: ['that', 'all'],
        subject2: ['started', 'with a'],
        subject3: ['foo', 'blabla'],
        subject4: ['bigbang', 'BANG!']
      }
    };


    $scope.items = [];

    angular.forEach(bla, function(value, key) {
      var faculty = {
        name: key,
        subjects: []
      };

      angular.forEach(value, function(value, key) {
        var subject = {
          name: key,
          courses: value
        };
        faculty.subjects.push(subject);
      });
      $scope.items.push(faculty);
    });

  }
]);
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.2.23/angular.min.js"></script>
<div ng-app='app'>
  <div ng-controller="fooCtrl">
    <ul>
      <li ng-repeat="faculty in items">Faculty Name: {{faculty.name}}
        <br/>
        <ul>
          <li ng-repeat="subject in faculty.subjects | filter:{courses:['foo']}">Subject name: {{subject.name}}
            <br/>
            <ul>
              <li ng-repeat="course in subject.courses">{{course}}</li>
            </ul>
          </li>
        </ul>Subjects: {{item.subjects}}</li>
    </ul>
  </div>

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

1 Comment

Thanks you @Tivie. Those are some good suggestions. It's strange Angular's documentation doesn't mention the shallow comparison when returning new Objects from filters.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.