My AngularJS resolve doesn't finish before the controller is loaded.
Background: Say I have a "my profile" page that is requested (user loads website.com/user). I would like to have angular intercept the request and ask for the appropriate data from the server.
The server will reply differently depending on whether the user is logged in/active/etc. I would like angular to get the data from the server before it loads the template, and before it initiates the appropriate controller. So far it does not.
App.js code:
.when('/user',
{
templateUrl:'/templates/user',
controller: 'my_profile',
resolve: {
some_cute_function_name: function($timeout){
$timeout(function(){
console.log('here123');
},5)
}
}
Meanwhile, controller code:
console.log('about to update scope');
It turns out that the controller is initiated before the resolve is done. I know this because the greater the number of seconds before the $timeout is done, the more likely I am to see this:
about to update scope
here123
as opposed to the other way around.
Is there any way to ensure that the resolve is resolved before the controller is initiated?
Much appreciated!
Your resolver needs to return a promise:
.when('/user',
{
templateUrl:'/templates/user',
controller: 'my_profile',
resolve: {
some_cute_function_name: function($timeout){
return $timeout(function(){
console.log('here123');
return { some: 'data' };
},5);
}
}
AngularJS describes resolve as:
"An optional map of dependencies which should be injected into the controller. If any of these dependencies are promises, the router will wait for them all to be resolved or one to be rejected before the controller is instantiated." (emphasis added)
I take it that unless any of the dependencies are promises, angular won't wait for resolve to finish before instantiating the controller (even if I have operations that take a long time to finish, like a $timeout).
Solution: you have to make sure the resolve involves a promise.
Related
I have a problem to initialize controller in AngularJS.
Below is the process which I want to implement.
Get data from mongoDB by $http before DOM is ready.
By Using the data, some div element should be created using ng-repeat.
But the problem is that the view is rendered before controller gets data from $http.
So I searched all over the stack-overflow and google, and found about ui-router's resolve function.
Below is my ui-router code.
.state('floor', {
url: '/floor/:domainId',
templateUrl: 'modules/floor/views/floor.client.view.html',
controller: 'FloorController',
resolve: {
initData: ['$http', '$stateParams', function($http, $stateParams) {
return $http.get('/users/getFloor/' + $stateParams.domainId).success(function(user) {
return $http.get('/users/' + user._id + '/data/get').success(function(data) {
return data;
});
});
}]
}
})
The first $http is to get user id from domain id. (e.g. User can connect to /floor/my_personal_domain_address), and the second $http is what I need for initial data.
This is my Controller code.
angular.module('floor').controller('FloorController', ['$scope', 'initData',
function($scope, initData) {
console.log(initData);
}]);
Small tip or search keyword or anything will be very thankful for me.
I'm still learning AngularJS so just give me a small tip please.. Thank you!
UPDATE
This was my misunderstanding of how controller works. As some people commented, I didn't have to use resolve to retrieve data before controller initialized. The problem was occurred because I declared array variable used in ng-repeat as [] for the first time and client shows error. If I declare the variable after I get value from database, controller data-bind it to view properly.
So the problem is solved. Thank you all for valuable comments and answers.
UPDATE 2
Anyway, ui-router's resolve can return a value even though it is promise. I worked for it for some hours, and what I found out is that if I return $http promise in resolve, controller can get its data when successful. But if $http error is occurred in resolve, nothing can catch it. So if there's someone who wants to use resolve to send data to controller before it is initialized, I think it must be used with care.
Get data from mongoDB by $http before DOM is ready.
In this case probably the simpler solution would be not to make any tricky $http requests before Angular initialization but instead just to embed your data as JavaScript global variable into the main HMTL page just before loading of angular.min.js script.
I don't know if I get your question correctly, but this should help you:
(from the ui-router docs https://github.com/angular-ui/ui-router/wiki)
// Another promise example. If you need to do some
// processing of the result, use .then, and your
// promise is chained in for free. This is another
// typical use case of resolve.
promiseObj2: function($http){
return $http({method: 'GET', url: '/someUrl'})
.then (function (data) {
return doSomeStuffFirst(data);
});
},
So you'd have to use .then() instead of .success() and it should work.
I have a basic factory in my app that handles API calls. Currently I'm using the form:
.factory('apiFactory', function($http){
var url = 'http://192.168.22.8:8001/api/v1/';
return {
getReports: function() {
return $http.get(url+'reports').then(function(result) {
return result;
});
},
getReport: function(id) {
return $http.get(url+'report/'+id).then(function(result) {
return result;
});
}
}
})
And in my controller I'm handling the promise like so:
.controller('exampleController', function($scope, apiFactory) {
apiFactory.getReports().then(
function(answer) {
if (answer.status==200){
if (answer.data.status == "error"){
// DISPLAY ERROR MESSAGE
console.log(answer.data.msg);
}
} else{
// THROW error
console.log('error: ', answer);
}
},
function(error){
console.log('error: ', answer);
}
);
}
}
})
It seems I could move the promise handling to my Factory instead of doing it in my controller, but I'm not sure if that would have any benefits others than a smaller controller.
Could somebody explain the best practices regarding this pattern?
It is ultimately up to you how much data you want to provide to the caller of the service. If needed, you could definitely return the HTTP response object to the caller, and have them process the response (which, btw, is always HTTP 2xx, if the promise is resolved rather than rejected).
But if you want to isolate the caller from the specifics of how the data got there (maybe it was cached, or supplied via another mechanism), and if you need to post-process the data, then it is advisable to handle the response in the service.
Here's an example:
.factory("apiService", function($http, $q){
var url = 'http://192.168.22.8:8001/api/v1/';
return {
getReports: function() {
return $http.get(url+'reports').then(function(result) {
var data = result.data;
if (data === "something I don't accept"){
return $q.reject("Invalid data");
}
var processedData = processData(data);
return processedData;
})
.catch(function(err){
// for example, "re-throw" to "hide" HTTP specifics
return $q.reject("Data not available");
})
},
// same idea for getReport
}
});
Then the controller wouldn't need to care about the underlying mechanism - all it gets is data or a rejection.
.controller('exampleController', function($scope, apiService) {
apiService.getReports()
.then(function(reports){
$scope.reports = reports; // actual reports data
});
})
Off-topic:
Notice how I changed the name of the service from "apiFactory" to "apiService". I wanted to point that out to remove a possible misconception. Whether you use .factory or .service or .value what you get as an injectable is always a service instance. .factory is just a mechanism of how this service is instantiated, so the name "apiFactory" is a misnomer. The only "factory" here is a function that you register with .factory (which could be anonymous, of course):
.factory("fooSvc", function fooSvcFactory(){
return {
getFoo: function(){...}
}
})
Better to keep all the data fetching inside the factory. This keeps the controller free from state, and it no longer cares how your factory works. If you change how you get data (e.g. not using $http) your controller shouldn't care, as it just calls getReport() and
A good explanation (see "Resolving Model data, no callback arg binding in Controllers"):
http://toddmotto.com/rethinking-angular-js-controllers/
Short answer: Handle promises in Factory.
Why?
Problems you face if you handle promises in Controller:
Let's say you have 5 Controllers that use the same Factory. Now let's say that you want to handle the errors when the promise does not get resolved correctly. So in the first controller, you write an error callback (or the catch(exception) more precisely, as you are dealing with promises), that shows you an alert message with the error. When the promise fails, this controller shows an alert with the error message. So far, so good? Right. But wait! What about the other 4 controllers? You haven't handled the errors in them. So now you end up copying the error handling code from the first controller & pasting it in the rest of the 4 controllers.
Now the fun starts. Imagine that you want to change your logic in the error state. Maybe you want to just log the error in console, or show a toaster message perhaps. So you go to the first controller & update the code. You think you are done? NO!!! There are 4 other controllers that are showing the alert message (remember that you pasted the code from the first controller previously???). So now, you try to update the rest of the controllers by pasting the new error message logic. Think about all the controllers that you need to update, if you have more!!! Tiring, isn't it???
Why to resolve promise in Factory:
Now let's say that you write your error handling logic in your service and all the controllers are using this service. You have written code to show an alert in case of a promise that fails in this service. All controllers will now start showing this alert, in case of a broken promise. In future, if you wish to update your error logic, you simply update this error logic in your service & all your controllers will start using this updated logic automatically, without any more effort on your part. This way, you have saved yourself tons of time.
So think about this:
1 change in Factory vs 1 change in all Controllers (which maybe 1 or more)
I would definitely vouch for 1 change in Factory as that helps reuse my logic with just a simple change and only once. All my controllers will start using the new logic. Controllers are supposed to be slim, lean & clean. Factories & Services are supposed to be reusable. For this reason of reusability, I strongly suggest you handle promise in Factory/Service.
I request some data from Firebase using AngularFire like below.
$rootScope.userTags = $firebase($fbCurrent.child('tags')).$asArray();
On logging this data in following ways I get the outputs as in the image below. So,
console.log($rootScope.userTags);
console.log($rootScope.userTags[0]);
$rootScope.userTags.$loaded().then(function () {
console.log($rootScope.userTags[0]);
});
In first log statement you can notice an $FirebaseArray object with five elements. In second log statement when I try to access one of the elements, I get undefined although you can clearly see those elements in the un$loaded array. Now in the last log statement, calling $loaded lets me access those elements without any issues.
Why does it behave this way? And is there a way I can access those elements, since those are already there, without calling $loaded on the array?
The $asArray() or $asObject() methods are asynchronous actions. The data has to be downloaded from Firebase before it can be accessed. This is just the nature of asynchronous data and not anything specific to Firebase.
The way we handle this issue of asynchrony is through the $loaded promise.
It seems though the real issue here ensuring data is loaded upon instantiation of the controller. You can solve this be resolving the data in the router.
Resolve require a promise to be returned. When the user routes to that page, Angular will not load the page until the promise has been resolved. The data resolved from the promise will injected into the controller.
.config(['$routeProvider',
function($routeProvider) {
$routeProvider
.when('/resolve', {
templateUrl: 'resolve.html',
controller: 'ResolveCtrl',
resolve: {
// data will be injected into the ResolveCtrl
data: function($firebase) {
// resolve requires us to return a promise
return $firebase(new Firebase('your-firebase')).$asArray().$loaded();
}
}
})
}
])
Now that we are resolving the data in the route we can access it in our controller.
.controller('ResolveCtrl', function($scope, data) {
$scope.userTags = data;
$scope.userTags[0]; // data has been resolved from the promise
});
The basic premise is this....
I have an application. When the user hits the application, it immediately fetches various information regarding the user from a sharepoint server through an ajax call. And depending on what kind of data is received from the user, the app has to display/hide certain information and set certain settings.
Each controller within the application is heavily dependent on the data that is returned from this sharepoint server.
I have several questions...
First, where should this ajax call be made? Ideally it should be run as soon as possible, so should it be executed in the app.run()?
Second, where should this data that gets returned from the sharepoint server be stored? I read that making a factory for the sole purpose of storing data is not best practice, and it is better to just use the $rootscope. Right now, I am just storing a User object in a factory call "User" which in hindsight I guess is a no no
Finally, I'm not sure if there is a way to suspend the loading of the controllers as they are heavily dependent on this on the data that gets returned, but if there isn't, how would one communicate the information that gets received to the controllers. Would this be a case to use the $broadcast method?
Right now I have a kind of hackish solution. It gets the job done, but I'm pretty sure it is less than ideal
Here is a part of one controller. I am injecting the factory User into it
if (User.HasLoadedUserProps == false)
{
User.registerObserverCallback(hasLoadedProperties);
User.GetUser("1111");
}
else
{
if (User.IsAdmin == true)
//do whatever
}
Once the necessary information has been returned from the ajax call, it calls this
var hasLoadedProperties = function ()
{
if (User.IsAdmin == true)
//do whatever
else
utilities.popupBox("You do not have permission to view this page", "Access Denied");
}
Any wisdom, insight, or advice is appreciated!
First:
When your ajax call should happen depends on a few things, but since you mention that you'd like to defer controller loading until the user data is pulled down, your best bet is to put the call in your service. More on that in my response to your last item. Placing that data in a service also makes it easier to share across controllers, which brings us to the next point...
Second:
Your user data absolutely should go in a service, and absolutely should not go in $rootScope. Think of $rootScope like you do window / globals in JavaScript. You want to avoid using it for much of anything. An exception would be where you really, really need to use events ($broadcast/$emit/$on) but even those cases should be rare.
Finally:
Look into the resolve option for $routeProvider (there are similar options for ui-router if you prefer that route (no pun intended).
This option allows you to defer the instantiation of a controller until a set of promises is resolved. In your case, you should return a promise from your User service, which is resolved once the user data is retrieved.
To help demonstrate these points, I made this simple demo. This code, along with the links to the Angular docs, should be enough to get you going ...
angular.module('myApp', ['ngRoute'])
.config(function($routeProvider) {
$routeProvider.when('/', {
templateUrl: 'beer.html',
controller: 'BeerController',
resolve: {
beer: function(Beer){ //injected into controller once promise is resolved
return Beer.getFavorite();
}
}
})
})
.controller('BeerController', function($scope, beer) { // Will load after 3s
$scope.favoriteBeer = beer; // beer comes from resolve in $routeProvider
})
.factory('Beer', function($timeout) {
var beer = {
favorite: 'porter'
};
beer.getFavorite = function() {
return $timeout(function() { // pretend this is an ajax call
return beer.favorite;
}, 3000);
}
return beer;
});
...where beer.html contains:
<div>
My favorite kind of beer is: {{favoriteBeer}}
</div>
I want my Angular application to resolve a promise before changing the route to a certain path:
va.config(['$routeProvider', function($routeProvider) {
$routeProvider.
when('/sendungen', {templateUrl: '../partials/sendungen.php', controller: 'OverviewCtrl',resolve: {
shipments: oc.fetchAllShipments
}}).
// ...
}]);
The funcion fetchAllShipments():
oc.fetchAllShipments = function(shipment){
shipment.fetchAllShipments().then(function(promise){
shipment.allShipments = promise.data;
});
};
The controller shall then copy the data from the shipment service to its $scope:
va.controller('OverviewCtrl',function($scope,$http,shipment){
$scope.allShipments = shipment.allShipments;
});
Everything is working fine as long as i change routes from within the application, e.g I load the mainpage, then switch to /sendungen
However, if i am already on that path and decide to refresh the page, the application is loaded before the data seems to be resolved. This happens only occasionally and seems to be depending on how fast he script was executed.
How do i prevent that behaviour?
The function in the resolve should return a promise, not like in your oc.fetchAllShipments method.
resolve - An optional map of
dependencies which should be injected into the controller. If any of
these dependencies are promises, the router will wait for them all to
be resolved or one to be rejected before the controller is
instantiated. If all the promises are resolved successfully, the
values of the resolved promises are injected and $routeChangeSuccess
event is fired.
For example:
resolve: {
shipments: ['$q', function($q){
var deffered = $q.defer();
shipment.fetchAllShipments().then(function(res){
deffered.resolve(res);
});
return deffered.promise;
}]
}
The quick and dirty fix will be to use $timeout:
va.controller('OverviewCtrl',function($scope,$http,shipment, $timeout){
$timeout(function(){$scope.allShipments = shipment.allShipments}, 1000);
});