Ways of loading data into controller via service in AngularJS
Asked Answered
P

2

6

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?
Prima answered 18/12, 2014 at 21:15 Comment(0)
A
4

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);
  }
}
Askew answered 19/12, 2014 at 1:14 Comment(2)
Thanks for a detailed answer. 1). Couldn't I inject a mock version of DataService? Why would that be worse than injecting a mock friendList? 2). Could I partially mitigate the dependent resolve issue by encapsulating those calls in the service logic and combining results into one object? Can you see an issue with that approach?Prima
1. You can, but it can also be argued that supplying mock data is easier and more straightforward than mocking out a service. 2. Yes, you can do this, but then you end up with functions created for the sole purpose of tying together a bunch of resolves. Fine if you will be using these elsewhere in the application, but otherwise quite excessive as the number of services in your app grows.Askew
T
2

Are there other commonly used ways of doing this?

Depends, If you have data that is on other domain and it can take time loading so you cant show the view until it get received so you will go for resolve one i.e first.

What are the limitations of each approach?

Limitation of using the first pattern the resolve one can be that the page won't display anything until all the data has loaded

Limitation of second one is that data may take longer to be recieved and your view will be like "{{}}" if you have not tackled it with css

What are advantages of each approach?

Advantage of first one is what i have said earlier that you will resolve the data and ensure it that it is present before view is rendered

Under what circumstances should I use each approach?

the resolve is very useful if we need to load some data loaded before the controller initialisation and rendering the view

And second one is when you dont have check ins and these loading problems expected and data is in you own hands !

Tetragram answered 18/12, 2014 at 21:36 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.