Search code examples
javascriptangularjsangular-ui-routerpromisengresource

Inject data from ngResource into controller with ui-router's resolve


In the angular ui-router documentation it is stated, that a controller will be instantiated only after all the promises have been resolved, including fetching data from a remote server.

I have a $resource instance:

angular.module('app')
    .factory('Item', item);

item.$inject = ['$resource'];

function item($resource) {
    return $resource('/items/:id/', {id: '@id'}, {
        update: {method: 'PATCH'}
    });
}

And I define a state which takes an id from url and retrieves data from a REST API:

     $stateProvider.state('item', {
            url: '/item/:itemId',
            templateUrl: '../components/item/item.html',
            controller: 'ItemController',
            resolve: {
                item: function ($stateParams, Item) {
                    return Item.get({id: $stateParams.itemId});
                }
            }
        })

In the controller I try to use an Item parameter, assuming that if the controller works, then all the data is already fetched and I can freely use it:

angular.module('app')
    .controller('ItemController', ItemController);

ItemController.$inject = ['$scope', 'item', 'Item'];

function ItemController($scope, item, Item) {

        $scope.item = item;
        $scope.similarItems = Item.query({item_type__id: $scope.item.item_type_detail.id});
} 

But the browser gives me this error:

TypeError: Cannot read property 'id' of undefined

And pointing to this exact line, which means that $scope.item.item_type_detail is still undefined, in other words - not resolved.

But if i wrap my second Item call (in the controller) in a $timeout for, say, 5000ms, it would work fine, meaning that the problem is not with the ajax call or improper dependency injection, but that the controller somehow gets instantiated before the promise resolves. How can this be explained and what can I do to fix it?


Solution

  • Return a promise to the resolver function:

    $stateProvider.state('item', {
        url: '/item/:itemId',
        templateUrl: '../components/item/item.html',
        controller: 'ItemController',
        resolve: {
            item: function ($stateParams, Item) {
                //return Item.get({id: $stateParams.itemId});
                return Item.get({id: $stateParams.itemId}).$promise;
                //return promise --------------------------^^^^^^^^^
            }
        }
    })
    

    It is important to realize that invoking a $resource object method immediately returns an empty reference. Once the data is returned from the server the existing reference is populated with the actual data.

    By returning the $promise property, the router will wait for the data from the server.