Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Ways of loading data into controller via service in AngularJS

I have a service that loads data using $http and returns a promise (simplified for brevity):

angular.module('myApp').factory('DataService', ['$http', function($http) {
  function unwrapFriendList(data) {
    ...
    return unwrappedFriendList;
  }

  return {
    getFriendList: function() {
      return $http.get('/api/friends').then(unwrapFriendList);
    }
  }
}]);

Here is a view that uses that data, after promise is resolved and result is stored in $scope.friends:

<div ng-repeat='friend in friends'>
  {{friend.firstName}} {{friend.lastName}}
</div>

When it comes to loading that data into the controller, I've come across a couple of ways to do that.

Option 1: Controller that uses data loaded via ng-route resolve

angular.module('myApp').controller('FriendListCtrl', ['$scope', 'friendList', function($scope, friendList) {
  $scope.friends = friendList;
}]);

Route section:

angular.module('myApp', ...).config(function($routeProvider) {
  $routeProvider
    .when('/friends', {
      templateUrl: 'views/friends.html',
      controller: 'FriendListCtrl',
      resolve: {
        friendList: ['DataService', function(DataService) {
          return DataService.getFriendList();
        }]
      }
    })
    ...
});

Option 2: Controller that triggers data loading by itself

angular.module('myApp').controller('FriendListCtrl', ['$scope', 'DataService', function($scope, DataService) {
  DataService.getFriendList().then(function(friendList) {
    $scope.friends = friendList;
  });
}]);

Questions

  • Are there other commonly used ways of doing this? If so, please illustrate with a code example.
  • What are the limitations of each approach?
  • What are advantages of each approach?
  • Under what circumstances should I use each approach?
like image 947
pbkhrv Avatar asked Dec 18 '14 21:12

pbkhrv


1 Answers

Unit testing

Option 1: Using resolves makes mocking dependencies in controller unit tests very simple. In your first option:

$routeProvider
  .when('/friends', {
    templateUrl: 'views/friends.html',
    controller: 'FriendListCtrl',
    resolve: {
      friendList: ['DataService', function(DataService) {
        return DataService.getFriendList();
      }]
    }
  })

angular.module('myApp')
  .controller('FriendListCtrl', ['$scope', 'friendList',
    function($scope, friendList) {
      $scope.friends = friendList;
    }]);

Since friendList is injected into the controller, mocking it in a test is as simple as passing in a plain object to the $controller service:

var friendListMock = [
  // ...
];

$controller('FriendListCtrl', {
  $scope: scope,
  friendList: friendListMock
})

Option 2: You can't do this with the second option, and will have to spy on/stub the DataService. Since the data data requests in the second option are immediately invoked on controller creation, testing will get very tangled once you start doing multiple, conditional, or dependent (more on that later) data requests.

View initialisation

Option 1: Resolves prevent view initialisation until all resolves are fulfilled. This means that anything in the view expecting data (directives included) will have it immediately.

Option 2: If data requests happen in the controller, the view will display, but will not have any data until the requests are fulfilled (which will be at some unknown point in the future). This is akin to a flash of unstyled content and can be jarring but can be worked around.

The real complications come when you have components in your view expecting data and are not provided with it, because they're still being retrieved. You then have to hack around this by forcing each of your components to wait or delay initialisation for some unknown amount of time, or have them $watch some arbitrary variable before initialising. Very messy.

Prefer resolves

While you can do initial data loading in controllers, resolves already do it in a much cleaner and more declarative way.

The default ngRoute resolver, however, lacks a few key features, the most notable being dependent resolves. What if you wanted to provide 2 pieces of data to your controller: a customer, and the details of their usual store? This is not easy with ngRoute:

resolve: {
  customer: function($routeParams, CustomerService) {
    return CustomerService.get($routeParams.customerId);
  },
  usualStore: function(StoreService) {
    // can't access 'customer' object here, so can't get their usual store
    var storeId = ...;
    return StoreService.get(storeId);
  }
}

You can hack around this by loading the usualStore from the controller after the customer is injected, but why bother when it can be done cleanly in ui-router with dependent resolves:

resolve: {
  customer: function($stateParams, CustomerService) {
    return CustomerService.get($stateParams.customerId);
  },
  usualStore: function(StoreService, customer) {
    // this depends on the 'customer' resolve above
    return StoreService.get(customer.usualStoreId);
  }
}
like image 113
user2943490 Avatar answered Nov 15 '22 10:11

user2943490