TesX
TesX

Reputation: 933

Angular.js: ng-bind nested dependencies

I have a problem with Angular.js.

I'm trying to build a simple invoice web app...
When editing invoices, the user should be able to insert and delete new items.

Each item has a readonly field which is the subtotal (unit price * qty) which is calculated by the function calcSubTotal(row)

At the end of the invoice, there is the GrandTotal, calculated by the function calcGrandTotal()

Ex:

HTML:

<div data-ng-repeat='row in rows'>
    Unit Price: <input type='text' name='price' data-ng-model='row.unit_price' />
    Qty: <input type='text' name='qty' data-ng-model='row.qty' />

    <input type='text' readonly value='{{ calcSubTotal(row) }}' />
</div>

<p>GRAND TOTAL: {{ calcGrandTotal() }}</p>

JS (EDIT):

$scope.calcSubTotal = function(row) {
    console.log(row);
    return (row.unit_price * row.qty);
}

$scope.calcGrandTotal = function() {

    var total = 0.00;

    $($scope.rows).each(function(i, row) {
        var x = $scope.calcSubTotal(row);
        total += x || 0.00;
    });

    return total;
};

Now if I put console.log("CALLED") inside the calcGrandTotal() function I see that it is being called lots of times.
EDIT:
Worst, if I put console.log(row) in the calcSubTotal(row) function, I can see that it is being called for all of the items even when I'm modifying a single item in the invoice.

I mean, if I modify item 1, it shouldn't recalculate item 2 but only update the grand total (based on the previously calculated subtotal for item 2, which hasn't changed...)

How can I optimize this thing?

Thanks.

Upvotes: 0

Views: 411

Answers (2)

Mike Quinlan
Mike Quinlan

Reputation: 2882

The problem with putting functions inside of ngBind brackets is that it it called every time the view is rendered. You should only be calculating totals when each one of the rows quantity or price changes. Consider this

EDIT: I didn't see that you were calculating subtotals for each line item. Here's a revised solution:

<div ng-repeat="row in rows">
  <input ng-model="row.qty" ng-change="calculateTotals()"/>
  <input ng-model="row.price" ng-change="calculateTotals()"/>
  <input readonly value="{{row.subTotal}}"/>
</div>

<div ng-bind="grandTotal"></div>

Then in your controller/directive do something like this

$scope.calculateTotals = function() {
  var grandTotal = 0;
  angular.forEach($scope.rows, function(row) {
    row.subTotal = row.qty * row.price;
    grandTotal += row.subTotal;
  });
  $scope.grandTotal = grandTotal;
}

NOTE: You might also have to call it somewhere in your controller after you've recieved the row data:

$http.get('/my/invoice/').success(function(rows){
  $scope.rows = rows;
  $scope.calculateTotals();
});

Upvotes: 1

Chetan
Chetan

Reputation: 56

You might want to do something like this,

HTML (same as yours)

<div data-ng-repeat='row in rows'>
      <p>row {{$index}}</p>
      Unit Price: <input type='text' name='price' data-ng-model='row.unit_price' />
      <br>
      Qty: <input type='text' name='qty' data-ng-model='row.qty' />
      <br>

      <input type='text' readonly value='{{ calcSubTotal(row) }}'>
    </div>

    <p>GRAND TOTAL: {{ calcGrandTotal() }}</p>

JS

angular.module("app", []).controller('ctrl', function($scope){
  $scope.rows = [{unit_price: 0, qty: 0}, {unit_price: 0, qty: 0}];

  $scope.calcSubTotal = function(row){
    return parseFloat(row.unit_price) * parseFloat(row.qty);
  }

  $scope.calcGrandTotal = function() {

      var total = 0.00;

      $($scope.rows).each(function(i, row) {
          var x = $scope.calcSubTotal(row);
          total += x || 0.00;
      });

      return total;
  };
});

I have put these together in a plunk here.

Upvotes: 0

Related Questions