Jazzy
Jazzy

Reputation: 6129

Understanding using AngularJS promises, how would they help solve this issue

I am trying to load a JSON object from a server using $resource in the main controller of an Angular app. There are some routes that load a partial into ng-view where the data is displayed, paginated, etc.

This all works fine, but my next goal is to filter the data based on what link is clicked and also update the URL so that a user could link to that filtered data (domain.dev/products/brandname).

So long as the site loads from the root URL or any other URL that doesn't call the routes to the products, everything is fine, but as soon as I attempt to load the site from /products it breaks because the data isn't available before the route loads the partial into the view.

What I am trying to understand whether a promise would help with this issue and if so, where to start. I have been doing some reading on promises and I am fairly sure that is the answer, but I am unable to get it to work with the existing code.

I am using $resource because it's so clean looking, but perhaps $http would do better here because it has more options.

Simply put, when the site loads from /, everything is fine because the partials that use the JSON aren't there yet and when they are loaded, the data is ready. But when the controller that loads the partials is called at the time the site loads, the data isn't there and the ProductController throws an error when I try to set some vars. Since $scope.products doesn't exist it has not method of length. I have the entire code in the partial wrapped in a div with ng-show="products" but that is not working as I expected it to in this case, since the error seems to be thrown in the controller. The partial still loads in, but with {{ vars }} visible.

Any guidance on this would be appreciated.

Edit: I am not attempting to send the URL param to the endpoint, I am actually wanting all the data upfront and then filter is with Angular. Basically, I want the main app to download all of the data (it's not much) and have it ready regardless of whether or not /products/string is the path or not. My goal is to avoid making an HTTP call for each filter. I had that working, but it was too much delay for just a few items waiting for HTTP results. My main issue is that I can get the data, but by the time the data gets back from the server, the ProductController has ran and throws an error. Basically, I want the controller to wait for the data before trying to run.

Edit 2: I figured out the problem, it was more of the way I was attempting to construct my app. I thought the best way to separate the code was to have the products stuff in its own controller, but by also wanting to load the data up front, I need to have the call to the service in the main appController. That was my mistake. Once I moved all of the code into the appController and changed the controller in the products route, it worked as desired. I was prompted to learn more about promises, which is good and I was able to use what I learned. There is still the issue of filtering, but this solves this issue.

I got some help from this post: AngularJS: Waiting for an asynchronous call

Here is the working code:

elite.controller('productController', function($scope, $routeParams, $rootScope, $location, Products){
    $scope.brand = $routeParams.brand;

    $rootScope.productPagination = function(){

        $scope.currentPage = 1;
        $scope.numPerPage = 10;
        $scope.maxSize = 6;    
        $scope.totalItems = $rootScope.products.length;
        $scope.numPages =  Math.ceil($scope.totalItems / $scope.numPerPage); 

        $scope.$watch('currentPage + numPerPage', function() {
            var begin = (($scope.currentPage - 1) * $scope.numPerPage)
            , end = begin + $scope.numPerPage;

            $scope.filteredProducts = $rootScope.products.slice(begin, end);
        }); 
    }

    if (!$rootScope.products){
        Products.getProducts().then(function(data){
            $rootScope.products = data.products;
            $rootScope.productPagination();
        });
    }else{
        $rootScope.productPagination();
    }
});

Here is the app (condensed) so far:

var elite = angular.module("elite", ["ngResource", "ngSanitize", "ui.bootstrap"]);

elite.config(['$routeProvider', function($routeProvider){
    $routeProvider.when('/', {
        templateUrl: 'partials/home.html', 
        controller: 'homeController'
    })

    $routeProvider.when('/products', {
        templateUrl:'partials/products.html', 
        controller: 'productController'
    })

    $routeProvider.when('/products/:brand', {
        templateUrl:'partials/products.html', 
        controller: 'productController'
    })

    $routeProvider.otherwise({redirectTo :'/'})
}]);


elite.factory('Products', function($resource){
    return $resource("/products")
});


elite.controller('appController', function($scope, $location, $routeParams, Products){ 

    Products.get({},function (data){
        $scope.products = data.products;
    });  
});

elite.controller('productController', function($scope, $location, $routeParams, $resource){
    $scope.brand = $routeParams.brand;

    $scope.currentPage = 1;
    $scope.numPerPage = 10;
    $scope.maxSize = 6;

    $scope.$watch($scope.products, function(){
        $scope.numPages =  Math.ceil($scope.products.length / $scope.numPerPage); 

        $scope.$watch('currentPage + numPerPage', function() {
            var begin = (($scope.currentPage - 1) * $scope.numPerPage)
            , end = begin + $scope.numPerPage;

            $scope.totalItems = $scope.products.length;

            $scope.filteredProducts = $scope.products.slice(begin, end);
        }); 
    });

});




<div ng-show="products">
    <h2>{{ brand }} products...</h2>
    <h4>{{products.length}} items</h4>

    <pagination ng-show="numPages" total-items="totalItems" page="currentPage" max-size="maxSize" class="pagination-small" boundary-links="true" rotate="false" num-pages="numPages"></pagination>

    <table class="table table-striped">
        <tr ng-repeat="product in filteredProducts">
            <td>{{product.brand}}</td>
            <td>{{product.title}}</td>
            <td ng-bind="product.priceDealer | currency"></td>
            <td>{{product.msrp | currency}}<td>
        </tr>
    </table>
</div>

Upvotes: 1

Views: 383

Answers (2)

Chandermani
Chandermani

Reputation: 42669

You can use resolve on $routeProvider to fix this issue. Like controller property there is a resolve property available. As document mentions

An optional map of dependencies which should be injected into the controller. If any of these dependencies are promises, the router will wait for them all to be resolved or one to be rejected before the controller is instantiated. If all the promises are resolved successfully, the values of the resolved promises are injected and $routeChangeSuccess event is fired. If any of the promises are rejected the $routeChangeError event is fired.

Your definition becomes

$routeProvider.when('/products/:brand', {
        templateUrl:'partials/products.html', 
        controller: 'productController',
        resolve: {
                    brand:function($resource, $route) {
                       //Get the param values from $route.current.params
                       // implement the method that return a promise which resolves to brand data or product data.

                  }
        },
    })

Then you can get this data in your controller

function BrandController($scope,brand) {
   //access brand data here
}

Upvotes: 1

Maxim Shoustin
Maxim Shoustin

Reputation: 77904

I would change a bit your example:

Instead:

elite.factory('Products', function($resource){
    return $resource("/products")
}); 

we can write:

elite..factory('Products', function($q){

        var data = $resource('/products', {},
        {
          query: {method:'GET', params:{}}}
        );

       // however I prefer to use `$http.get` instead `$resource`

         var factory = {
            query: function (selectedSubject) {
                var deferred = $q.defer();              
                 deferred.resolve(data);                
                return deferred.promise;
            }
        }
        return factory;        
    });

After, controller side:

instead:

Products.get({},function (data){
        $scope.products = data.products;
    }); 

we get promise response:

    Products.query()
                    .then(function (result) {
                      $scope.products = data.products;
                    }, function (result) {
                        alert("Error: No data returned");
                    });

Reference

A promise represents a future value, usually a future result of an asynchronous operation, and allows us to define what will happen once this value becomes available, or when an error occurs.

I suggest you to read this good presentation hot promises work

Upvotes: 3

Related Questions