0

I'm trying to wrap my head around Factories and Services in AngularJS and keep getting stuck. Here is a basic example similar to problems i'm trying to solve.

HTML

<div ng-controller="productGridController">

    <div ng-repeat="category in categories" ng-controller="productCategoryController">

        <input type="text" ng-model="category.name">

        <div ng-repeat="product in products">
            <input type="text" ng-model="product.name">
        </div>

        <a class="btn btn-small" ng-click="addProduct()">Add Product</a>

    </div>

    <a class="btn" ng-click="addCategory()">Add Category</a>
</div>

Now let's say I want to get a total count of all products that are currently instantiated inside of the root controller productGridController and store it in a variable I can output inside of the productGridController in a variable such as $scope.totalProducts

Since each productCategoryController has it's own scope, I'm not sure how to do global counters and sums like that? I think ia service or factory could do something like this but I'm not sure how to implement it.

I tried putting a root scope variable in the productGridController like this:

app.controller("productGridController", function($scope, $http, $rootScope, $timeout) {
    $scope.totalProducts = 0;

    $scope.categories = [];
    $scope.addCategory = function() {
        $scope.categories.push({name:''});
    }
});

And then watching whenever something changed in the productCategoryController

app.controller("productCategoryController", function($scope, $http, $rootScope, $timeout) {
    $scope.products = [];

    $scope.addProduct = function() {
        $scope.products.push({name:''});
    }

    $scope.$watch('products', function() {
        $scope.totalProducts = $scope.products.length;
    }, true);
});

This works when there's only been one category added, but when there is multiple categories it only factors the most recent one.

Also keep in mind in my real application I'm pulling in the data from a database so this will also need to be able to work in realtime, so the counter function can be called at any point to reflect the current status of how many products there currently is.

What would be the simplest way to do this in Angular JS?

Jordash
  • 2,926
  • 8
  • 38
  • 77
  • *What would be the simplest way to do this in Angular JS?* I think you are making life complicated by instantiating a controller for each item in an `ng-repeat`. In general, a Controller shouldn't try to do too much. It should contain only the business logic needed for a single view. For more information, see [AngularJS Developer Guide -- Using Controllers Correctly](https://docs.angularjs.org/guide/controller#using-controllers-correctly). – georgeawg Jan 10 '17 at 06:30

2 Answers2

0

First:

You are putting a watch on 'products'. When you call 'products.push()' in 'addProduct', the value of products doesn't change; so your watch will not be invoked. If you had instead said:

$scope.addProduct = function() {
    $scope.products = $scope.products.concat([{name:''}]);
}

then your watch would have been invoked as you expected (because then the value of 'products' has changed).

Second:

Don't forget you can also use the functional form of $watch:

$scope.$watch(function() {return $scope.products.length;}, function() {
    $scope.totalProducts = $scope.products.length;
});

But mostly: why use a $watch at all? You could have said:

$scope.addProduct = function() {
    $scope.products.push({name:''});
    $scope.totalProducts = $scope.products.length;
}

since you know that this is the sort of action which has a side effect.

The idea of using a service is to go a bit further, and package all of this data-specific logic in one place where we don't have to think about how and when it is rendered. Then, you inject that service into your controller, and the controller exposes the parts of the service (via scope) that are relevant.

Chas Brown
  • 386
  • 1
  • 10
  • I have to use a watch because this data could be pulled from a database, in which case it wouldn't ever call an `addProduct` method – Jordash Jan 09 '17 at 23:34
  • Also even though `products` is adding a blank object the `watch` still is triggered – Jordash Jan 09 '17 at 23:35
  • Also, note that `$scope.products.length` is the length of products inside of that product category, so it will at most give the total # of products for that specific `productCategoryController` I want the total products of all product categories summed up, not just one of the categories. – Jordash Jan 09 '17 at 23:36
  • If you're pulling this from a database, then the results should 'live' in a service (instead of as a $scope var). When your service fetches the data, it is then responsible for updating things like 'totalProducts' (which I guess you want to be the SUM of the lengths of the product array for all the different categories. – Chas Brown Jan 09 '17 at 23:41
0

When persistent or shared state is needed, implement a factory or service to manage that state - here's a good SO Q/A about the differences (along with providers).

$watch is great, but it's not always the best choice. For example if you can't cache the products dataset (maybe it's a huge) then change events via $broadcast may be a better choice.

Here's a plnkr demonstrating your scenario with a product service that broadcast's an event for add/remove operations and a directive that listens for that change event and updating its content accordingly.

The important parts of ProductService are its $broadcast'ing of a change event when products are added or removed:

app.service('ProductService', function($rootScope) {
  let self = {
      PRODUCT_COUNT_DELTA: "PRODUCT_COUNT_DELTA",
      $inject: ['$rootScope']
    }
  // broadcast a PRODUCT_COUNT_DELTA event with the latest product count.
  function changes(changeType) {
    if (changeType == 0)
      $rootScope.$broadcast(self.PRODUCT_COUNT_DELTA, self.getProductCount());
  }

In the demo, the service methods addProduct and removeProduct call the internal changes function when a product is added or removed.

The directive productCount renders the current product count and updates when notified of product count changes:

app.directive("productCount", ['ProductService', function(productService) {
  let render = function(count) {
    count = count || productService.getProductCount();
    return "<span>" + count + "</span>";
  }
  return {
    restrict: 'E',      
    template: function(elem, attr) {
      return render();
    },
    link: function($scope, elem, attrs) {
      $scope.$on(productService.PRODUCT_COUNT_DELTA, function($event, productCount) {
        elem.html(render(productCount));
      });
    }
  }
}]);

Again, $watch is handy but isn't always the right choice.

Community
  • 1
  • 1
RamblinRose
  • 4,883
  • 2
  • 21
  • 33
  • Thanks, but this example doesn't answer the question, in the question their can be multiple product category controllers, each with their own scope, i'm trying to get a sum of total products across all controllers, not just one. In order to do that I need to iterate through all currently active controllers of a specific type. – Jordash Jan 10 '17 at 16:52
  • Indeed - maybe [something like this](https://plnkr.co/edit/VColYfxi9ZsTXfaIUWLh?p=preview)? – RamblinRose Jan 10 '17 at 18:04