How to store/communicate global asynchronous data to controllers - javascript

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>

Related

Why move my $http calls into a service?

Currently I have calls like this all over my three controllers:
$scope.getCurrentUser = function () {
$http.post("/Account/CurrentUser", {}, postOptions)
.then(function(data) {
var result = angular.fromJson(data.data);
if (result != null) {
$scope.currentUser = result.id;
}
},
function(data) {
alert("Browser failed to get current user.");
});
};
I see lots of advice to encapsulate the $http calls into an HttpService, or some such, but that it is much better practice to return the promise than return the data. Yet if I return the promise, all but one line in my controller $http call changes, and all the logic of dealing with the response remains in my controllers, e.g:
$scope.getCurrentUser = function() {
RestService.post("/Account/CurrentUser", {}, postOptions)
.then(function(data) {
var result = angular.fromJson(data.data);
if (result != null) {
$scope.currentUser = result.id;
}
},
function(data) {
alert("Browser failed to get current user.");
});
};
I could create a RestService for each server side controller, but that would only end up calling a core service and passing the URL anyway.
There are a few reasons why it is good practice in non-trivial applications.
Using a single generic service and passing in the url and parameters doesn't add so much value as you noticed. Instead you would have one method for each type of fetch that you need to do.
Some benefits of using services:
Re-usability. In a simple app, there might be one data fetch for each controller. But that can soon change. For example, you might have a product list page with getProducts, and a detail page with getProductDetail. But then you want to add a sale page, a category page, or show related products on the detail page. These might all use the original getProducts (with appropriate parameters).
Testing. You want to be able to test the controller, in isolation from an external data source. Baking the data fetch in to the controller doesn't make that easy. With a service, you just mock the service and you can test the controller with stable, known data.
Maintainability. You may decide that with simple services, it's a similar amount of code to just put it all in the controller, even if you're reusing it. What happens if the back-end path changes? Now you need to update it everywhere it's used. What happens if some extra logic is needed to process the data, or you need to get some supplementary data with another call? With a service, you make the change in one place. With it baked in to controllers, you have more work to do.
Code clarity. You want your methods to do clear, specific things. The controller is responsible for the logic around a specific part of the application. Adding in the mechanics of fetching data confuses that. With a simple example the only extra logic you need is to decode the json. That's not bad if your back-end returns exactly the data your controllers need in exactly the right format, but that may not be the case. By splitting the code out, each method can do one thing well. Let the service get data and pass it on to the controller in exactly the right format, then let the controller do it's thing.
A controller carries out presentation logic (it acts as a viewmodel in Angular Model-View-Whatever pattern). Services do business logic (model). It is battle-proven separation of concerns and inherent part of OOP good practices.
Thin controllers and fat services guarantee that app units stay reusable, testable and maintainable.
There's no benefit in replacing $http with RestService if they are the same thing. The proper separation of business and presentation logic is expected to be something like this
$scope.getCurrentUser = function() {
return UserService.getCurrent()
.then(function(user) {
$scope.currentUser = user.id;
})
.catch(function(err) {
alert("Browser failed to get current user.");
throw err;
});
});
It takes care of result conditioning and returns a promise. getCurrentUser passes a promise, so it could be chained if needed (by other controller method or test).
It would make sense to have your service look like this:
app.factory('AccountService', function($http) {
return {
getCurrentUser: function(param1, param2) {
var postOptions = {}; // build the postOptions based on params here
return $http.post("/Account/CurrentUser", {}, postOptions)
.then(function(response) {
// do some common processing here
});
}
};
});
Then calling this method would look this way:
$scope.getCurrentUser = function() {
AccountService.getCurrentUser(param1, param2)
.then(function(currentUser){
// do your stuff here
});
};
Which looks much nicer and lets you avoid the repetition of the backend service url and postOptions variable construction in multiple controllers.
Simple. Write every function as a service so that you can reuse it. As this is an asynchronous call use angular promise to send the data back to controller by wrapping it up within a promise.

Angular Best practice: promise in a Factory or in a Controller?

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.

AngularJS Router invoke function when certain URL is loaded initially

I am using angular router` to track the state of my web app like this:
when('/', {
controller: "AController",
templateUrl: "APanel.html"
}).
when('/subpage/:id', {
controller: "BController",
templateUrl: "BPanel.html"
}).
And I am using Angular Service to track some shared values:
app.service('stateService', function() {
this.someSwitch = false;
this.someLongDataArray = [x, y, z];
});
Currently, before changing path to \subpage\:id url from AController, I will assign new values to members of the service, so they can be referenced in subpages.
Now the question is, if user directly launching the subpage url \subpage\:id, or hit the refresh button on browser on subpage, BController will be invoked, and I will lost the values in the service which are supposed to be prepared by AController.
I am wondering what I should do in this case. is there any way I can get called when user launch the subpage directly, so I have a chance to prepare the data? (Maybe I can watch for html onload event, but not sure that's the best answer).
Thanks
It appears, BController is dependent on AController.
Ideally, Controller should not contain any data/dom manipulaton, state maintenance. It is simply a glue between view and the $scope model.
Being said so, you need not create any such dependency between controllers. The service can be invoked from both controllers.
OR
If indeed there is a requirement that APanel.html must be loaded/initialized before BPanel.html is loaded, then you must check for some flag in BContoller and redirect user to APanel.html.
like
if(<check some flag>){
$location.path('/');
}
But then you have to find the way to redirect the user again to BPanel.html. I guess this is not a recommended approach.
I am not sure I get your question completely. But if there is a possibility that the user might hit BPanel.html directly then you should do something like this.
if(serviceExists()){
//Do your usual Bcontroller stuff here if the services was initialized
}
else{
//Show a warning/error like "Oops, something is wrong go back to '/'" OR
// Initialize services in BController
}
This should be in your BController if initializing your service before BController is that important. You basically force people to stay on AController.

Notify a directive about data receiving

I have this simple angular directive:
angular.module("directives").directive("loading", function($rootScope) {
return {
link: function(scope, element, attrs) {
console.log("loading directive", scope, element, attrs);
element.addClass("hidden");
$rootScope.$on("$routeChangeStart", function(event, next, current) {
element.removeClass("hidden");
});
// how do I notify data have been received in the controller? <---
}
};
});
As you can see it allows me to show a loading message to be displayed when the route of the app changes. Like this:
Loading...
My problem is that I dunno how to notify the directive in order to get it to disappear.
Let's say the route /list/ is associated to ListController. When the url /list/ gets hit, the controller will load some data and only when such data have been received I would like the loading message to go away. Clearly the directive have no idea when the controller's data gets delivered so my question remains:
How the heck do I come up with a decent design for this?
To basically simplify the design and structure it in a way, what I do is have a 'messaging' service defined as part of an Angular module that takes care of this. Whenever there's a route change, it sets its most current message to 'Loading…' and when a controller is finished loading (finishes all Ajax requests, for example), it calls a clear() on the messaging service to clear all recent messages.
In your directive (in my case a navbar), you can watch for latestMessages from your messaging service and change the alert depending on what the latestMessage changes to. If its blank, you clear the message and so forth.
A little more work, I guess, but its a lot more structured and modular this way. Plus, since the messaging service is central, it can accept messages from as many modules as you'd want.

Resolve $http request before running app and switching to a route or state

I have written an app where I need to retrieve the currently logged in user's info when the application runs, before routing is handled. I use ui-router to support multiple/nested views and provide richer, stateful routing.
When a user logs in, they may store a cookie representing their auth token. I include that token with a call to a service to retrieve the user's info, which includes what groups they belong to. The resulting identity is then set in a service, where it can be retrieved and used in the rest of the application. More importantly, the router will use that identity to make sure they are logged in and belong to the appropriate group before transitioning them to the requested state.
I have code something like this:
app
.config(['$stateProvider', function($stateProvider) {
// two states; one is the protected main content, the other is the sign-in screen
$stateProvider
.state('main', {
url: '/',
data: {
roles: ['Customer', 'Staff', 'Admin']
},
views: {} // omitted
})
.state('account.signin', {
url: '/signin',
views: {} // omitted
});
}])
.run(['$rootScope', '$state', '$http', 'authority', 'principal', function($rootScope, $state, $http, authority, principal) {
$rootScope.$on('$stateChangeStart', function (event, toState) { // listen for when trying to transition states...
var isAuthenticated = principal.isAuthenticated(); // check if the user is logged in
if (!toState.data.roles || toState.data.roles.length == 0) return; // short circuit if the state has no role restrictions
if (!principal.isInAnyRole(toState.data.roles)) { // checks to see what roles the principal is a member of
event.preventDefault(); // role check failed, so...
if (isAuthenticated) $state.go('account.accessdenied'); // tell them they are accessing restricted feature
else $state.go('account.signin'); // or they simply aren't logged in yet
}
});
$http.get('/svc/account/identity') // now, looks up the current principal
.success(function(data) {
authority.authorize(data); // and then stores the principal in the service (which can be injected by requiring "principal" dependency, seen above)
}); // this does its job, but I need it to finish before responding to any routes/states
}]);
It all works as expected if I log in, navigate around, log out, etc. The issue is that if I refresh or drop on a URL while I am logged in, I get sent to the signin screen because the identity service call has not finished before the state changes. After that call completes, though, I could feasibly continue working as expected if there is a link or something to- for example- the main state, so I'm almost there.
I am aware that you can make states wait to resolve parameters before transitioning, but I'm not sure how to proceed.
OK, after much hair pulling, here is what I figured out.
As you might expect, resolve is the appropriate place to initiate any async calls and ensure they complete before the state is transitioned to.
You will want to make an abstract parent state for all states that ensures your resolve takes place, that way if someone refreshes the browser, your async resolution still happens and your authentication works properly. You can use the parent property on a state to make another state that would otherwise not be inherited by naming/dot notation. This helps in preventing your state names from becoming unmanageable.
While you can inject whatever services you need into your resolve, you can't access the toState or toStateParams of the state it is trying to transition to. However, the $stateChangeStart event will happen before your resolve is resolved. So, you can copy toState and toStateParams from the event args to your $rootScope, and inject $rootScope into your resolve function. Now you can access the state and params it is trying to transition to.
Once you have resolved your resource(s), you can use promises to do your authorization check, and if it fails, use $state.go() to send them to the login page, or do whatever you need to do. There is a caveat to that, of course.
Once resolve is done in the parent state, ui-router won't resolve it again. That means your security check won't occur! Argh! The solution to this is to have a two-part check. Once in resolve as we've already discussed. The second time is in the $stateChangeStart event. The key here is to check and see if the resource(s) are resolved. If they are, do the same security check you did in resolve but in the event. if the resource(s) are not resolved, then the check in resolve will pick it up. To pull this off, you need to manage your resources within a service so you can appropriately manage state.
Some other misc. notes:
Don't bother trying to cram all of the authz logic into $stateChangeStart. While you can prevent the event and do your async resolution (which effectively stops the change until you are ready), and then try and resume the state change in your promise success handler, there are some issues preventing that from working properly.
You can't change states in the current state's onEnter method.
This plunk is a working example.
We hit a similar issue. We felt we needed to make the logic which makes the HTTP
call accessible to the logic that handles the response. They're separate in the
code, so a service is a good way to do this.
We resolved that separation by wrapping the $http.get call in a service which
caches the response and calls the success callback immediately if the cache is
already populated. E.g.
app.service('authorizationService', ['authority', function (authority) {
var requestData = undefined;
return {
get: function (successCallback) {
if (typeof requestData !== 'undefined') {
successCallback(requestData);
}
else {
$http.get('/svc/account/identity').success(function (data) {
requestData = data;
successCallback(data);
});
}
}
};
}]);
This acts as a guard around the request being successful. You could then call authorizationService.get() within your $stateChangeStart
handler safely.
This approach is vulnerable to a race condition if a request is in already progress when authorizationService.get() is called. It might be possible to introduce some XHR bookkeeping to prevent that.
Alternatively you could publish a custom event once the HTTP request has
completed and register a subscriber for that event within the
$stateChangeStart handler. You would need to unregister that handler later
though, perhaps in a $stateChangeEnd handler.
None of this can complete until the authorisation is done, so you should inform
the user that they need to wait by showing a loading view.
Also, there's some interesting discussion of authentication with ui-router in
How to Filter Routes? on the AngularJS Google Group.
I think a cleaner way to handle this situation is t$urlRouterProvider. deferIntercept() together with $urlRouter.listen() in order to stop uiRouter until some data is retrieved from server.
See this answer and docs for more information.

Categories

Resources