2

Trying to understand the nature of Angular filters. So I have this:

<p>RandomCase: {{ aString | randomCase }}</p>

and this:

.filter 'randomCase', () ->
    (input) ->
        input.replace /./g, (c) ->
            if Math.random() > 0.5 then c.toUpperCase() else c

Coffeescript makes for a cleaner code here, JS version is found in JSFiddle along with the complete example:

http://jsfiddle.net/nmakarov/5LdKV/

The point is to decorate a string by having random letters capitalized.

it works, but throws "10 $digest() iterations reached. Aborting!" most of the time. I figured that for some reason Angular would re-run the filter at least twice to see that outputs are the same. And if not, will run it again until the last two matches. Indeed, since the filter's code produces a random string, it is quite unlikely it will repeat itself twice in a row.

Now to the question: is it possible to tell Angular not to re-run this filter more than once? I do not need to observe the value of this filtered output in the code, so no need for Angular to watch the changes - even if a hardcoded "string" be used in place of an aString variable, the code behaves the same - 10 iterations reached...

And I know that I can put the randomizing logic in a controller and bind the result to a $scope.aString and it would just work - I'm trying to understand the Angular way of filters.

Cheers.

Marc Kline
  • 9,399
  • 1
  • 33
  • 36
Petrovich
  • 58
  • 5

2 Answers2

2

There is no way to use an non-idempotent filter in a watched expression without a hack. This the simplest one that I can think of, which will make the filter idempotent...

Use a memoizing function to ensure that subsequent calls to the filter passing the same arguments return the same result.

Example using Underscore:

myApp.filter('randomCase', function() {
    return _.memoize(function (input) {
        console.log("random");
        return input.replace(/./g, function(c) {
            if (Math.random() > 0.5) {
                return c.toUpperCase();
            } else {
                return c;
            }
        });
    });
});

Updated Fiddle

Marc Kline
  • 9,399
  • 1
  • 33
  • 36
  • Right, this is `external` hack of the problem, so to speak. And I was wondering if it is possible to do it from within Angular itself - without any hacks. – Petrovich May 29 '14 at 20:56
1

The filter itself will only run when an expression with the | operator (e.g. someVar | someFilter) is evaluated. It is Anuglar's dirty checking that causes the expression to be evaluated multiple times.

In short, Angular runs the expression aString | randomCase over and over until it doesn't change. At that point it knows what to put into the DOM. To prevent infinite looping when that value doesn't stop changing, it throws the infinite $digest error.

For this reason filters always run at least twice. Once to get the initial value, and then a second time to compare it against that first value.

By putting the randomizing logic in the controller, you would then have something like {{randomizedString}} in your HTML. The value of randomizedString wouldn't change from the first time it was evaluated, and thus would accomplish your end goal without hitting the infinite $digest error.

Community
  • 1
  • 1
dnc253
  • 39,967
  • 41
  • 141
  • 157
  • Here is another fork of the OP's Fiddle showing how this might work: http://jsfiddle.net/marcolepsy/ckKLm/ – Marc Kline May 29 '14 at 18:39
  • Marc, that's right - it is possible to move the `randomization` logic out of the filter and into controller. But I'm trying to utilize filters - that's what they are for, right? – Petrovich May 29 '14 at 20:58
  • It wasn't my idea - just my demonstration. Also, I'm not arguing for or against any of these solutions. As @dnc253 points out, this is just how Angular works when it comes to interpolated values. So any solution is going to seem hackish to you... because you're trying to do something that isn't supported out of the box. – Marc Kline May 29 '14 at 21:02
  • dnc253, I do realize that filter function would run over an dover again 'till output stabilizes, but I still have difficulty to understand - why? It is just a filter, for Earth's sake! One way road, so to speak. The (excellent) link you have provided has this line: `If there is a change in value, then it fires the change event`. So my question is: how on Earth I tell Angular not to bind listener to a filter output? And related question: why one wanted to have listeners on filter output? – Petrovich May 29 '14 at 21:05
  • Ultimately you're really telling Angular to watch the result of an expression which in this case happens to include a filter. But there are tons of other types of expressions handled the same way. Yes, there could be an exception for filters to accomodate what you're trying to achieve, but there isn't. I suspect it's in part because it isn't a popular feature request, coupled with the fact that it may not be as simple to implement as it'd seem. Still, it does sound possible that there [may be support](https://github.com/angular/angular.js/issues/6133) for non-idempotent filters in Angular 2. – Marc Kline May 29 '14 at 21:49
  • Seems like 1.3 has addressed this: "The filter function should be a pure function, which means that it should be stateless and idempotent. Angular relies on these properties and executes the filter only when the inputs to the function change." from https://code.angularjs.org/1.3.14/docs/guide/filter – Jamie Pate Mar 23 '15 at 23:47