Question

I have a multi-step wizard that binds to data within a service (wizardStateSvc). I want to have a totals element on the screen that updates whenever base values for the wizard that affect the total are updated. The calculation is a bit complex (more than shown here on my sample) so I want to have it performed in the controller. I figured a $watch would work for this but what is occuring is that the $watch function is being called once during initialization and never triggering as I update items. How do I get this $watch to trigger properly?

Totals controller:

myApp.controller('wizardTotalsCtrl', ['wizardStateSvc', '$scope', function (wizardStateSvc, $scope) {
    $scope.products= wizardStateSvc.quote.products;

    $scope.$watch(function() { return wizardStateSvc.quote.products; }, function(products) {
        var total= 0;

        products.forEach(function(product) {
            total += product.price * (1 - (product.dealerDiscount * 0.01)) * product.quantity;
        });

        $scope.baseTotal = total;
    });
}])

State Service:

myApp.service("wizardStateSvc", [function () {
    var quote = {
        products: [],
        options: {},
        customer: {},
        shipping: {},
        other: {}
    }

    return {
        quote: quote
    }
}]);
Était-ce utile?

La solution

If the only thing that can change is the contents of the products array, i.e. products may be inserted or deleted from the array but their price does NOT change, then use $scope.$watchCollection. The code would be the same as yours, just replace $watch with $watchCollection.

Rationale: $watch checks for equality; since the products array itself does not change (i.e. products at time t1 === products at time t2), the watch is never triggered. On the other hand, $watchCollection watches the contents of the array, so it is what you want in this case.

If the price of the products may also change you need the costlier $scope.$watch(...,...,true). The true at the 3rd argument means deep watch, i.e. traverse the object hierarchy and check each nested property. Check out the docs.

Autres conseils

Your watch watches the array 'products'. When it's initialized, products is a reference to an array, and when you add values, products remains is still a reference to the same array, it's only the array content which is different, so there really is no reason for your watch to invoke the function again. This problem has two solution:

Not so good solution: Watch the length of products, which will make the watch get called whenever the length of products change.

$scope.$watch(function() { return wizardStateSvc.quote.products.length; }, ...);

This is problematic in the use case where you add one item and remove another immediately afterwards. If before this action the value of the watch is x, it will be x after your action, and thus won't invoke.

Better solution: Use watch collection instead, which handles also the use cases the watching the length doesn't.

$scope.$watchCollection('products', ...);

From the docs (scroll to the $watchCollection part):

https://docs.angularjs.org/api/ng/type/$rootScope.Scope

Licencié sous: CC-BY-SA avec attribution
Non affilié à StackOverflow
scroll top