0

I'm learning the AngularJs and I've created a dynamic list with ng-repeat, but now I cannot find the solution for the next step...

I have the following code:

HTML:

<div ng-app="app">
    <div class="list" ng-controller="ListController">

        <div class="row" ng-repeat="prod in prodList">
            <div class="prod-name cell">
                <input type="checkbox" id="prod-{{ prod.id }}" data-ng-model="orders.prods.prod[prod.id]" value="{{ prod.id }}" data-cb="" /><label for="prod-{{ prod.id }}">{{ prod.name }}</label>
            </div>
            <div class="prod-size cell">
                <label for="prodColor-@{{ prod.id }}">
                    <select id="prodColor-{{ prod.id }}" data-ng-model="orders.prods.color[prod.id]" data-ng-options="color.id as color.name for color in prod.colors">
                        <option value="">Select one</option>
                    </select>
                </label>
            </div>
            <div class="prod-price cell">
                {{ prod.price }}
            </div>

        </div>

        <div class="sum">
            {{ sum }}
        </div>

    </div>
</div>

JS:

var app = angular.module("app", [], function () {
});

app.controller('ListController', function ($scope) {
    init();

    function init () {
        $scope.prodList = [{"id":"1","name":"window","colors":[{"id":"9","name":"white"},{"id":"11","name":"black"}],"price":"100"},{"id":"2","name":"door","colors":[{"id":"22","name":"blue"},{"id":"23","name":"red"}],"price":"356"},{"id":"3","name":"table","colors":[{"id":"37","name":"white"},{"id":"51","name":"black"}],"price":"505"}];
        $scope.orders = {};
        $scope.orders.prods = {};
        $scope.orders.prods.prod = {};
        $scope.orders.prods.color = {};
        $scope.sum = 0;
    }
});

Working demo:

http://jsfiddle.net/HDrzR/

Question

How can I calculate the summary of selected product's price into the $scope.sum?

Edited:

So if you select the "window" and the "table" from the list, the sum should contains 606. But if you unselect the "table" then the sum should be 100.

Thank you in advance!

2
  • Could you rephrase/clarify your question? I'm not sure what you mean by "summary of selected product's price". Commented May 25, 2014 at 12:24
  • Each product has a price. I need the selected products' price in $scope.sum. So if you select a checkbox from the list then that price should added to sum (and if uncheck it, remove it from sum of course). Commented May 25, 2014 at 12:33

5 Answers 5

2

You could either $watch over $scope.orders.prod and re-calculate the sum or (if performance is not a major concern and the list of objects is not huge) you could use a sum() function and reference it in the view:

Total: {{sum()}}

$scope.sum = function () {
    return $scope.prodList.filter(function (prod) {
        return $scope.orders.prods.prod[prod.id];
    }).reduce(function (subtotal, selectedProd) {
        return subtotal + parseInt(selectedProd.price);
    }, 0);
};

/* If the above looks complicated, it is the equivalent of: */
$scope.sum = function () {
    var sum = 0;
    for (var i = 0; i < $scope.prodList.length, i++) {
        var prod = $scope.prodList[i];
        if ($scope.orders.prods.prod[prod.id]) {
            sum += parseInt(prod.price);
        }
    }
    return sum;
}

If you decide to go down the watching path, this is how the code should look like:

Total: {{watchedSum}}

$scope.sum = ...;   // same as above
$scope.$watchCollection('orders.prods.prod', function (newValue, oldValue) {
    if (newValue !== undefined) {
        $scope.watchedSum = $scope.sum();
    }
});

UPDATE:
"Stole" Quad's $watchCollection as this is more "cheap" than $watch(..., true).
As long as $scope.order.prods.prod contains only rimitive values (i.e. no objects), $watchCollection is enough to detect the changes.


See, also, this short demo illustrating both approaches.

Sign up to request clarification or add additional context in comments.

6 Comments

+1 I liked this one, it seems the right way to do it.
I know @SystemExpert is sharing an alternate solution but why a watch would be necessary here? I do not see it
@Dalorzo: As mentioned in the answer, the $watch alternative has its merits (in terms of performance). The 1st approach will run sum() with every digest loop (even if the prod list hasn't changed). The approach using $watch will run sum() only if prod has changed. Usually this is not a concern as the impact is unnoticable, but if sum is computationally expensive (e.g. too many items or too large prices or whatever), then saving those extra computation miht have noticable impact on app responsiveness (with 0 cost).
Sum occurs during UI Action trigger on change MVVM suggests ngChage: <input type="checkbox" ng-model="" ng-change="addItUp()">
Yeah, that's a good point. One thing to keepin mind though is that initialization should be manually taken care of (in a real-world example, prodList wouldn't be hard-coded in the controller, but rather fetched from a backend, so initialization should be manual (probably with a $watch over $scope.prodList). But, that said, ngChange should be the most performant solution.
|
1

here is an updated fiddle. http://jsfiddle.net/HRQ9u/

what I did is add a $scope.$watch on the orders:

         $scope.$watch('orders', function (newValue, oldValue) {
            var findProdById = function (products, id) {
                for (var i = 0; i < products.length; i++) {
                    if (parseInt(products[i].id, 10) === parseInt(id, 10)) {
                        return products[i];
                    }
                }
                return null;
            };
            var orders = newValue;

            var usedProducts = newValue.prods.prod;
            var sum = 0;
            for ( var prop in usedProducts ){
               var prodList = $scope.prodList;
               var id = prop;
               if ( usedProducts[id] ){
                   var product = findProdById(prodList, id);
                   if ( product !== null ){
                       sum += parseInt(product.price, 10);
                   }
               }
            }
            $scope.sum = sum;
        }, true);

Edit:

With that said, you can probably simplify your model a lot. And I mean A LOT.

Also, I have used only regular js as opposed to lodash or underscore.

2 Comments

Thanks, but the problem is, that it summarise everything which is in prodList. But what I need is only the selected ones from the list.
@baba I read some where in SO that it is always better to share the code as part of the answer.
1

Register an ng-click with checkbox and pass $event & prod.id references to a custom function that will be called on checkbox click event.

 <input type="checkbox" id="prod-{{ prod.id }}" data-ng-model="orders.prods.prod[prod.id]" value="{{ prod.price }}" ng-click="calcSum($event, prod.id)" data-cb="" />

Within controller define custom function as:

$scope.calcSum = function($event, id) {
         var checkbox = $event.target;

        if (checkbox.checked  ) {
            $scope.sum += parseInt(checkbox.value);   
         } else {
            $scope.sum -= parseInt(checkbox.value);    
         }  
}

Change the Checkbox value attribute to store {{ prod.price }} instead of {{ prod.id }}, it makes more sense.

Comments

1

From my POV it looks more appropriate to use is ngChange from the input[checkbox] itself. Like:

ng-change="addMe(prod.price);"

We need a way to differentiate checked from not checked and in order to change less your app we will use the existence of the object by it doing this way:

ng-change="addMe(orders.prods.prod[prod.id],prod.price);"
ng-true-value="{{ prod.id }}" 
ng-false-value=""

The full HTML will look like this:

<input type="checkbox" id="prod-{{ prod.id }}" data-ng-model="orders.prods.prod[prod.id]" ng-change="addMe(orders.prods.prod[prod.id],prod.price);" ng-true-value="{{ prod.id }}" ng-false-value="" />

From your Controller all you need is to add or substract when necessary:

$scope.addMe=function(checked,val){
  val = 1*val
  if (!checked){ $scope.sum = $scope.sum-val;}
  else { $scope.sum =  $scope.sum +val; }
}

Here is the Online Demo

Comments

1

Updated your fiddle FIDDLE

You can $scope.$watchCollection your scope to calcuate new sum. I chaged your prodList prices to number instead of string, and used underscore.js for easier looping in your objects/arrays.

 $scope.$watchCollection("orders.prods.prod",function(){
    var sum = 0;
    for(var i = 0;i<$scope.prodList.length;i++){
        if($scope.orders.prods.prod[$scope.prodList[i].id]){
            sum += $scope.prodList[i].price;
        }
    }
    $scope.sum = sum; 
});

watchCollection docs are here

edit: changed with regular js answer

4 Comments

$watch and underscore is adding 2 layers for simple sum.
underscore can simply be replaced with regular js, used here just for convenience
It would have been a better answer if you did it regular js instead
Well, here it is. removed underscorejs dependency.

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.