0

(post edited again, new comments follow this line)

I'm changing the title of this posting since it was misleading - I was trying to fix a symptom.

I was unable to figure out why the code was breaking with a $digest() iterations error. A plunk of my code worked fine. I was totally stuck, so I decided to make my code a little more Angular-like. One anti-pattern I had implemented was to hide my model behind my controller by adding getters/setters to the controller. I tore all that out and instead put the model into the $scope since I had read that was proper Angular.

To my surprise, the $digest() iterations error went away. I do not exactly know why and I do not have the intestinal fortitude to put the old code back and figure it out. I surmise that by involving the controller in the get/put of the data I added a dependency under the hood. I do not understand it.

edit #2 ends here.

(post edited, see EDIT below)

I was working through my first Error: 10 $digest() iterations reached. Aborting! error today.

I solved it this way:

<div ng-init="lineItems = ctrl.getLineItems()">
    <tr ng-repeat="r in lineItems">
        <td>{{r.text}}</td>
        <td>...</td>
        <td>{{r.price | currency}}</td>
    </tr
</div>

Now a new issue has arisen - the line items I'm producing can be modified by another control on the page. It's a text box for a promo code. The promo code adds a discount to the lineItem array. It would show up if I could ng-repeat over ctrl.getLineItems().

Since the ng-repeat is looking at a static variable, not the actual model, it doesn't see that the real line items have changed and thus the promotional discount doesn't get displayed until I refresh the browser.

Here's the HTML for the promo code:

<input type="text" name="promo" ng-model="ctrl.promoCode"/>
<button ng-click="ctrl.applyPromoCode()">apply promo code</button>

The input tag is writing the value to the model. The bg-click in the button is invoking a function that will apply the code. This could change the data behind the lineItems.

I have been advised to use $scope.apply(...). However, since this is applied as a matter of course by ng-click is isn't going to do anything. Indeed, if I add it to ctrl.applyPromoCode(), I get an error since an .apply() is already in progress.

I'm at a loss.

EDIT

The issue above is probably the result of me fixing of symptom, not a problem. Here is the original HTML that was dying with the 10 $digest() iterations error.

<table>
    <tr ng-repeat="r in ctrl.getLineItems()">
        <td>{{r.text}}</td>
        <td>...</td>
        <td>{{r.price | currency}}</td>
    </tr>
</table>

The ctrl.getLineItems() function doesn't do much but invoke a model. I decided to keep the model out of the HTML as much as I could.

this.getLineItems = function() {
    var total = 0;
    this.lineItems = [];


    this.lineItems.push({text:"Your quilt will be "+sizes[this.size].block_size+" squares", price:sizes[this.size].price});
    total = sizes[this.size].price;

    this.lineItems.push({text: threads[this.thread].narrative, price:threads[this.thread].price});
    total = total + threads[this.thread].price;

    if (this.sashing) {
        this.lineItems.push({text:"Add sashing", price: this.getSashingPrice()});
        total = total + sizes[this.size].sashing;
        }
    else {
        this.lineItems.push({text:"No sashing", price:0});
        }

    if(isNaN(this.promo)) {
        this.lineItems.push({text:"No promo code", price:0});
        }
    else {
        this.lineItems.push({text:"Promo code", price: promos[this.promo].price});
        total = total + promos[this.promo].price;
        }

    this.lineItems.push({text:"Shipping", price:this.shipping});
    total = total + this.shipping;

    this.lineItems.push({text:"Order Total", price:total});

    return this.lineItems;

};

And the model code assembled an array of objects based upon the items selected. I'll abbreviate the class as it croaks as long as the array has a row.

function OrderModel() {
    this.lineItems = []; // Result of the lineItems call
    ...
    this.getLineItems = function() {
    var total = 0;
    this.lineItems = [];
        ...
        this.lineItems.push({text:"Order Total", price:total});
    return this.lineItems;
    };
}
Tony Ennis
  • 12,000
  • 7
  • 52
  • 73
  • 1
    I thought that this is what `$apply()` was for. – PM 77-1 Jun 09 '14 at 23:18
  • Have you already tried it? – PM 77-1 Jun 09 '14 at 23:29
  • Working through it now. Not having a lot of luck so far. I need to learn more about "$scope". – Tony Ennis Jun 09 '14 at 23:35
  • Found a post that confirms my suggestions: [How can I tell AngularJS to “refresh”](http://stackoverflow.com/questions/12304728/how-can-i-tell-angularjs-to-refresh) – PM 77-1 Jun 09 '14 at 23:40
  • @PM77-1, http://jimhoskins.com/2012/12/17/angularjs-and-apply.html is also helpful. However, now I am struggling to figure out from where I get `$scope`. – Tony Ennis Jun 09 '14 at 23:44
  • Regarding `$scope`, see ["Dependency Injection"](https://docs.angularjs.org/guide/di). Don't forget that "the Angular way" would be to immediately `$digest` or `$apply` changes coming from the "text box for a promo code". I'd recommend setting up an `ng-model="inputModel"` attribute on the `` element, then something like `$scope.$watch( 'inputModel', function() { $scope.lineItems = ...; });` in your controller. – Morgan Delaney Jun 09 '14 at 23:52
  • 1
    Apply won't help you here. The issue is that you had a problem and instead of solving the problem, you just changed your app's semantics. And now your app is not working (e.g. updating items in `ngRepeat`). It is best to solve the original problem instead. Show some code (the original code that produced the error). – gkalpak Jun 10 '14 at 01:37
  • I agree @ExpertSystem, when I added the `ng-init` thing, I was fixing a symptom. I have been fiddling with the app, it will take a few minutes to put it back. – Tony Ennis Jun 10 '14 at 02:02
  • @TonyEnnis: This is a typical issue (which has its roots in how `ngRepeat` checks for equality). Coulc you post the actual code of the `getLineItems()` function ? – gkalpak Jun 10 '14 at 07:24
  • @ExpertSystem, done - the full function has been posted. – Tony Ennis Jun 10 '14 at 13:09
  • @TonyEnnis: The problem is that with each $digest cycle, a new array is returned (even if it creates objects with equal values, new objects are created). Would it be possible to base ng-repeat on `lineItems` and call `getLineItems()` only when something might have changed ? – gkalpak Jun 10 '14 at 13:53
  • @ExpertSystem I've done what you said - I commented out the `setLineItems` method in the controller so nothing could call it. I hacked the code so the model's `setLineItems` is only called when the page is advanced to the problem page. Now it should be called only once. I changed `ng-repeat` to use the model's `lineItems` property directly. Unfortunately, I still get the `digest iterations` failure. – Tony Ennis Jun 10 '14 at 15:45
  • @TonyEnnis: Did you take a look at my answer below ? Does it work for you ? – gkalpak Jun 14 '14 at 18:11
  • @ExpertSystem No, it did not work. See the edit I made to the original post. – Tony Ennis Jun 15 '14 at 01:06
  • @TonyEnnis: I hadn't noticed the 2nd edit. It's strange that my solution didn't work for you, because it works fine in my fiddle. In any case, I doubt that moving stuff from controller to $scope solves the problem. As to what is more Angular: The `controller as` syntax is newer and seems to be promoted by the Angular-team... – gkalpak Jun 15 '14 at 06:05
  • @ExpertSystem your solution, or one very much like it, also worked in plunker. – Tony Ennis Jun 15 '14 at 12:36

1 Answers1

1

The problem is that with each $digest cycle, a new array is returned (even if it contains objects with equal values, new objects are created).

To circumvent this, you could associate ngRepeat with a lineItems property and call getLineItems() only when something might have changed.


A possible implementation is the following:

<!-- The VIEW -->
<table>
    <tr ng-repeat="r in ctrl.lineItems">...</tr>
</table>

/* The CONTROLLER */
.controller('myCtrl', function (OrderModel) {
    this.orderModel  = OrderModel;
    this.lineItems   = this.orderModel.lineItems;
    this.reloadItems = this.orderModel.getLineItems;

    // Initialization
    this.reloadItems();
});

/* The SERVICE */
app.service('OrderModel', function () {
    this.lineItems = [];
    this.getLineItems = function () {
        var total = 0;
        this.lineItems.splice(0, this.lineItems.length);
        ...
        for (var i = 0; i < 10; i++) {
            total++;
            this.lineItems.push({text: 'Order Total', price: total});
        }
    };
});

See, also, this short demo.

gkalpak
  • 47,844
  • 8
  • 105
  • 118