AngularDart custom filter call() method required to be idempotent?
Asked Answered
A

1

3

The main running example of the Angular Dart tutorial is a Recipe Book app. The exercise at the end of the Chapter 5 on filters and services suggests trying to "create a [custom] filter that will multiply all the amounts [of each ingredient listed] in the recipes" thus allowing a "user to double, triple, or quadruple the recipe." E.g. an ingredient of "1/2 cup of flour" would become "1 cup of flour" when doubled.

I have written such a custom filter: it takes a list of Ingredients (consisting of a quantity and a description) and returns a new list of new Ingredients (with increased quantities), but I am getting the following error:

5 $digest() iterations reached. Aborting!

My question is: what is the required and/or permitted behavior of an AngularDart custom filter call() method? E.g., clearly it is permitted to remove (i.e. filter) elements from its input list, but can it also add new or replace elements? The Dart angular.core NgFilter documentation simply says that a "filter is a class with a call method". I have not found more details.

Extrapolating from the answer to this AngularJS post, it would seem that repeated invocations of call() should (eventually?) yield "the same result". If so, this would be a reasonable constraint.

Yielding "the same result" could mean that call() needs to be idempotent, but in the case of Dart such idempotence should be relative to == (object equivalence) not identical() (object identity), IMHO. I ran a few tests using the following small example to illustrate the issues:

  • main.dart
    import 'package:angular/angular.dart';

    class A { }

    @NgFilter(name:'myFilter') class MutatingCustomFilter {
      final A _a = new A();
      call(List list) => new List.from(list)..add(_a); // runs ok.
      // call(List list) => new List.from(list)..add(new A()); // gives error
    }

    class MyAppModule extends Module {
      MyAppModule() { type(MutatingCustomFilter); }
    }

    main() => ngBootstrap(module: new MyAppModule());
  • index.html excerpt
    <ul>
      <li ng-repeat="x in [1,2,3] | myFilter">{{x}}</li>
    </ul>

If I change the body of class A to be

@override bool operator==(other) => true;
@override int get hashCode => 1;

which makes all instances of A considered ==, then the second implementation of call() in main.dart (the one with add(new A())) still gives an error (though a different one).

I can see how to solve the tutorial exercise without use of a custom filter, but I am trying to not give up on the challenge of finding a filter that will work as requested. I am new to Angular and decided to jump in with AngularDart, so any help in explaining the effects of the various flavors of call(), or in finding documentation for the expected behavior of call(), (or letting me know if you think such a custom filter simply cannot be written!) would be appreciated.

Abeabeam answered 24/1, 2014 at 1:53 Comment(0)
P
5

Too many iterations

When angular detects a change in the model, it executes a reaction function. The reaction function can further change the model. This would leave the model in inconsistent state. For this reason we re-run the change detection, which can further create more changes. For this reason we keep re-running the changes until the model stabilizes. But how many times should we rerun the change detection before giving up? By default it is 5 times. If the model does not stabilize after 5 iteration we give up. This is what is going on in your case.

Change Detection

When has object changed? one can use identical or == (equals). Good arguments can be made for each, but we have chosen to use identical because it is fast and consistent. Using == (equals) is tricky and it would negatively impact the change detection algorithm.

Filters and arrays

When a filter which operates an an array, executes it has no choice but to create a new instance of the array. This breaks identical, but luckily it is fed into ng-repeat which uses its own algorithm for array contents detection rather the array detection. While the array does not have to be identical between runs, its content must be. Otherwise ng-repeat can not tell the difference between insertions and changes, which it needs to do proper animations.

Your code

The issue with your filter is that it creates new instance on each iteration of the digest loop. These new instances prevent the model from stabilizing and hence the error. (There are plans to solve this issue, but it will be few weeks before we get there.)

Solution

Your solutions is attempting to create a filter which consumes the whole array and then attempts to create a new array, for the ng-repeat. A different (prefered) solution would be to leave the ng-repeat iteration as is, and instead place the filter on the binding which is creating the qty and apply it there.

<span>{{recipe.qty | myFilter:multiply}}</span>
Playmate answered 27/1, 2014 at 4:48 Comment(3)
Thank you for the detailed response Misko. I am looking forward to learning more about the "plans to solve this issue".Abeabeam
Based on your feedback I gather that the proposed solution would only work if myFilter was idempotent w.r.t. identical (or, more accurately, converged within 5 iterations). This seems to be the case for nums. In an implementation where the type of an ingredient quantity is a special numeric class, say Rational, supporting whole number treatment of numbers like 1 1/3 (like in "1 1/3 cups of flour", then such a filter-based solution would still not work (modulo fancy interning). Or am I missing something?Abeabeam
Until the "plans to solve this issue" are executed, I have found a work around: pipe ingredient values into a (custom) toString filter.Abeabeam

© 2022 - 2024 — McMap. All rights reserved.