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.
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();
}]
}
})
...
});
angular.module('myApp').controller('FriendListCtrl', ['$scope', 'DataService', function($scope, DataService) {
DataService.getFriendList().then(function(friendList) {
$scope.friends = friendList;
});
}]);
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.
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.
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);
}
}
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With