Reputation: 647
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;
});
}]);
Upvotes: 6
Views: 3551
Reputation: 6940
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);
}
}
Upvotes: 4
Reputation: 20445
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 !
Upvotes: 2