7

I'm running into an issue where I want to bind to the output of a function inside of an ng-repeat loop. I'm finding that the function is being called twice per item rather than once as I'd expect. Here's the ng-repeat section (notice the calcRowTotal() call at the end):

<tr ng-repeat="row in timesheetRows">
    <td>
        <select ng-model="row.categoryID">
            <option ng-repeat="category in categories" value="{{category.id}}">
                {{category.title}}
            </option>
        </select>
    </td>
    <td ng-repeat="day in row.dayValues">
        <input type="text" ng-model="day.hours" />
    </td>
    <td>{{calcRowTotal($index, row)}}</td>
</tr>

The calcRowTotal() function is shown next:

$scope.calcRowTotal = function (index, row) {
    console.log('calcRowTotal - Index: ' + index);
    var total = 0;
    for (var i = 0; i < row.dayValues.length; i++) {
        var num = parseFloat(row.dayValues[i].hours);
        if (isNaN(num)) {
            num = 0;
            //this.dayValues[i].hours = 0;
        }
        total += num;
    }
    //updateDayTotals();
    return total;
}

An example of one of the items being iterated through is shown next:

{
    categoryID: 2,
    dayValues: [
                    { day: $scope.days[0], hours: 5 },
                    { day: $scope.days[1], hours: 0 },
                    { day: $scope.days[2], hours: 3 },
                    { day: $scope.days[3], hours: 0 },
                    { day: $scope.days[4], hours: 2 },
                    { day: $scope.days[5], hours: 5 },
                    { day: $scope.days[6], hours: 8 }
    ]
}

I'm seeing the following in the console (two items are currently in the collection I'm looping through):

calcRowTotal - Index: 0 
calcRowTotal - Index: 1 
calcRowTotal - Index: 0 
calcRowTotal - Index: 1 

I could certainly make a "rowTotal" property but would prefer to bind to "live" data provided by the function shown above. Hopefully the duplication is something simple I'm missing so I appreciate any feedback on why I'm seeing the duplication. As a side note, as data in one of the textboxes changes I need to update the row totals as well so it may be I need a different approach. Interested in understanding this particular situation first though....definitely don't want the duplication because there could be a lot of rows potentially.

Here's an example: http://jsfiddle.net/dwahlin/Y7XbY/2/

3
  • This is a duplicate of stackoverflow.com/q/14973792/259038, though it may not have been immediately obvious. Commented Feb 20, 2013 at 18:32
  • Thanks Josh. Spent quite a bit of time searching but didn't see that one. Appreciate the info. Commented Feb 20, 2013 at 18:36
  • Yeah, this is definitely not a "delete this" kind of duplicate as it's not obvious that they're the same case. I just wanted to close the loop for future viewers. Commented Feb 20, 2013 at 18:42

3 Answers 3

13

It's because you're binding to a function expression here:

<td>{{calcRowTotal($index, row)}}</td>

What that does it force that function to be reevaluated on every item, on every digest. What you'll want to do to prevent that is pre-calculate that value and put it in your array to begin with.

One way to do that is to set up a watch on your array:

$scope.$watch('timesheetRows', function(rows) {
   for(var i = 0; i < value.length; i++) {
     var row = rows[i];
     row.rowTotal = $scope.calcRowTotal(row, i);
   }
}, true);

Then all you have to do is bind to that new value:

<td>{{row.rowTotal}}</td>
Sign up to request clarification or add additional context in comments.

8 Comments

Thanks blesh - didn't realize it would do that but it makes sense.
It's honestly probably not a "big deal". Since it's a client side app, and there's only one user, if it's not causing performance issues, you can leave it if it's easier for you to maintain. Regardless, I have some psuedo-code for a proposed fix in my answer.
+1 But why didn't you use angular.forEach here? Seems like it may have cut the code down a little bit. Also, you should mention that expressions will be called at least once during every digest and we cannot guarantee how many times.
Perfect - thanks again blesh (and Josh for the alternatives to look into). I was going down the $watch road but being relatively new to the framework the hardest part is knowing the best practice to follow. In some cases there are multiple approaches which is good - but challenging at the same time.
I have the new $watch mentioned above added into the controller but am still seeing the calcRowTotal() function called 2 times per item. I'm assuming based on what has been mentioned (digests) that this is just expected behavior and something to get used to? I'm definitely used to ensuring that duplication is removed but it doesn't appear I can control that in this type of scenario?
|
1

Its totally because you're binding to a function expression as @Ben Lesh suggested. Somehow using $watch also got me executing the same function twice. Solution to refrain from double execution that we used is by using ng-init for function call. Simply create an init variable with function return as value and use that in ur expression as below:

<tr ng-repeat="row in timesheetRows" ng-init="rowTotalValue=calcRowTotal($index, row)">
<td>
    <select ng-model="row.categoryID">
        <option ng-repeat="category in categories" value="{{category.id}}">
            {{category.title}}
        </option>
    </select>
</td>
<td ng-repeat="day in row.dayValues">
    <input type="text" ng-model="day.hours" />
</td>
<td>{{rowTotalValue}}</td>

This worked for me where I was passing a single attribute of row. Not tried with $index though. But I assume it should work.

Comments

0

I actually had a similar problem recently.

Ended up iterating through each sub-object and binding the totals to a model. Shouldn't be an issue, especially if you're only using the total for display purposes.

eg.

// Reset to Zero
$scope.rowTotal = 0;

jquery.each($scope.row, function(key, value) {
 $scope.totalRow += value.hours;
});

and iterate through each row.

1 Comment

Many AngularJS projects don't use jQuery, but they can use the functionally similar angular.forEach.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.