Prevent Angular controller from loading using UI Router resolve - javascript

Im trying to use a promise to prevent a state from loading in UI Router $resolve.
$stateProvider.state('base.restricted', {
url:'/restricted',
templateUrl: 'views/restricted.html',
controller: 'restrictedCtrl',
resolve: { // user must be logged-in to proceed
// authentication service checks session on back-end
authenticate: function ($q, $state, $timeout, AuthService){
AuthService.validate(token).then(function(res){
if( res.data.response.status===1 ) { // authenticated!
return $q.resolve(); // load state
} else { // authentication failed :(
$timeout(function() { $state.go('base.notfound') }); // load not found page
return $q.reject(); // do not load state
}
});
}
}
})
This scheme works in terms of redirecting the user based on authentication results, but it looks like, upon failed authentication, the controller is still being loaded for a brief instant before the user is re-directed to the "not found" state.
I believe the reason is because Im running AuthService within resolve, but in an asynchronous manner. So the controller is loaded while AuthService does its thing but is then redirected once the callback function executes.
But isnt resolve inherently supposed to be blocking? That is, the controller doesnt load until resolve is "resolved"?
AuthService is basically this, fyi:
.service('AuthService', function($http){
this.validate = function(token){
return $http.get('http://my.api/validate/'+token);
}
});
How can I modify this code to prevent loading until AuthService's callback function has completed.

You should return a promise immediately, instead of doing so after the response from the service comes back:
return AuthService.validate(token).then(function(res){ ... }
Otherwise the router sees no return value instead of a promise, so it thinks it's OK to load the state. Then later when your AJAX request returns you end up redirecting, which is why you see a flicker.

Related

Ui-router loads state, then how can I run a function?

I'm new to Angular and states and wrapping my head around ui-router. I've been doing it with jQuery for too long. In jQuery, I can load up something with ajax, then on the success, perhaps run another function. How do you do that with Angular?
For example, I have the following
var ivApp = angular.module('ivApp', ['ui.router']);
ivApp.config(function($urlRouterProvider, $stateProvider){
$urlRouterProvider.otherwise('/');
$stateProvider
.state('home', {
url: '/',
templateUrl: 'partials/partial-home.html'
})
});
Which simply loads up partial-home.html into my ui-view. But how to tell it to run a function once that is done? For example, I have authenticate.js and a function authenticate(). How do I run authenticate() once 'home' state has loaded?
Additionally, can I tell angular to only load authenticate.js for this state? Or should I have already loaded it in the template. I know that if I include the script in partial-home.html (e.g. <script src="authenticate.js"></script>) chrome throws me an error about synchronous xmlhttprest being deprecated. So somhow in the config, can I declare authenticat.js as a dependency of the state or something like that?
At the moment I have worked out I can do something like:
ivApp.controller('authenticate', function($scope) {
// start authorisation
authenticate();
});
And then define the controller authenticate in my ui-router states. But is that how it's done? It works basically. My authenticate function is doing things like changing things in the DOM, but I read controllers shouldn't be used for this.
Thanks for any pointers
Let's break down into parts.
If you just want to load authenticate.js in this particular home state, use ocLazyLoad. It's one of the best way to load a resource lazily. And it works really well if ui-router too!
$stateProvider.state('index', {
url: "/", // root route
views: {
"lazyLoadView": {
controller: 'AppCtrl', // This view will use AppCtrl loaded below in the resolve
templateUrl: 'partial-home.html'
}
},
resolve: { // Any property in resolve should return a promise and is executed before the view is loaded
loadMyCtrl: ['$ocLazyLoad', function($ocLazyLoad) {
// you can lazy load files for an existing module
return $ocLazyLoad.load('js/authenticate.js');
}]
}
});
If you want to run authenticate() once the state is loaded, there are quite a number of ways to do it. One way of course is listening to the $stateChangeSuccess event, but I would avoid using it since you know, global variables, and global variables are bad. I do not want to pollute my $rootScope just because I have a really specific use case.
You can use resolve in ui-router too. Resolve is executed after the state is loaded and before the controller is instantiated. I would recommend to use this method as you can chain your promises together with ocLazyLoad, if you are using it (which you should).
Manipulating DOMs after a state is loaded? Sure, that's what templateUrl for! Design your template such that it accomadates to your authenticate() functions. If you combine it with resolve, there isn't really much of a problem separating concerns as you would already have executed authenticate() before controller is loaded.
Edit: Adding in Plnkr
You want to first lazily-load authenticate.js, and then use the function inside authenticate.js to do something. Since resolve in ui.router executes promise chains in parallel, we have to chain them up, i.e, load your jsfiles first, and then return your status of authentication.
We need to declare a deferred promise using $q service. We then return this promise in the resolve, so that you controller is listening to one promise instead of two. Here is how:
$stateProvider
.state('Home', {
templateUrl: 'home.html',
controller: 'homeCtrl',
resolve: {
//need to chain our promises since we neeed to first load the authenticate.js
//and second, execute authenticate()
loadJsAndAuth: ['$ocLazyLoad', '$q', '$injector', function($ocLazyLoad, $q, $injector) {
//declare a deferred promise
var deferred = $q.defer();
//now load the authenticate.js
$ocLazyLoad.load('authenticate.js').then(
//load successful! proceed to use our authenticate function!
function(success) {
//since we already have loaded authenticatejs, now we can inject the service and use it
var authSvc = $injector.get('authenticateSvc');
//this is just a demo on how to authenticate.
//change this to banana to see the authenticate fail
var fruits = 'apple'
if (authSvc.authenticate(fruits)) {
//authenticate pass, resolve the promise!
deferred.resolve('authenticated!');
}
//authenticate fail, reject the promise
deferred.reject('authenticate failed');
},
//load of jsfiles failed! reject the promise.
function(error) {
deferred.reject('Cannot load authenticate.js')
})
return deferred.promise;
}]
}
})
And in your controller, you can get the resolved promises!
//you can get access to what is is being resolved by loadJsAndAuth
.controller('homeCtrl', ['$scope', 'loadJsAndAuth', function($scope, loadJsAndAuth) {
$scope.status = loadJsAndAuth // this is resolved promises.
}]);

AngularJS - Wait to Load data to display the view

I am really new to AngularJS and after reading several questions and some articles I am a little confused about the correct way to load data and wait till its loaded to display the view.
My controller looks like this
app.controller('ResultsController', ['$scope','$http', '$routeParams', function($scope, $http, $routeParams) {
$scope.poll = {};
$scope.$on('$routeChangeSuccess', function() {
showLoader();
$http.get("rest/visualizacion/" + $routeParams.id)
.success(function(data) {
$scope.poll = data;
hideLoader();
})
.error(function(data) {
// Handle error
});
});
}]);
I have seen there are people who create a service for $http calls, is it necessary? Why is it better?
The appropriate way to do that is to use the resolve property of the route. From the documentation:
resolve - {Object.<string, function>=} - 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. If any of the promises are rejected the $routeChangeError event is fired. The map object is:
key – {string}: a name of a dependency to be injected into the controller.
factory - {string|function}: If string then it is an alias for a service. Otherwise if function, then it is injected and the return value is treated as the dependency. If the result is a promise, it is resolved before its value is injected into the controller. Be aware that ngRoute.$routeParams will still refer to the previous route within these resolve functions. Use $route.current.params to access the new route parameters, instead.
So, if you want poneys to be retrieved from the backend before the router goes to the poney list page, you would have
resolve: {
poneys: function($http) {
return $http.get('/api/poneys').then(function(response) {
return response.data;
)};
}
}
And your controller would be defined as
app.controller('PoneyListCtrl", function($scope, poneys) {
$scope.poneys = poneys;
// ...
});
Of course, you could also put the code making the $http call and returning a list of poneys in a service, and use that service in the resolve.

Angular route resolve for sessionStorage login variable

I've built a login system using Angular JS. When the user logs in, a session storage variable is set and they are redirected to a dashboard page (should only be accessible when logged in)"
$window.sessionStorage["isLoggedIn"] = true;
$location.path("/dashboard");
Now I want to use resolve on my any routes that required the user to be logged in. I find the documentation for this very confusing and can't understand it. If the user is not logged in and tries to access one of these pages, they need to be shown a message saying they can't access that page.
app.config(function($routeProvider) {
$routeProvider.when("/dashboard", {
templateUrl : "framework/views/dashboard.html",
controller : "DashboardCtrl",
title: "Dashboard",
resolve: {
//how does this work?!
}
});
app.factory("loginCheckService", function(){
//check sessionStorage and return?
});
You would rather listern for locationChangeStart event, perform validations (auth), prevent the route change (if required) and raise some events (unauthroized) to show the login form.
something like
app.run(function($rootScope,LoginService){
$rootScope.$on('$locationChangeStart',function(event){
if(!LoginService.isUserLoggedIn()){
event.preventDefault();
//LoginService.raiseUserNotLoggedIn(); OR
$rootScope.$broadcast('UserNotLoggedIn');
}
});
});
app.controller('LoginFormController',function($scope){
$scope.userLoggedIn=true;
$scope.on('UserNotLoggedIn',function(){
$scope.userLoggedIn=false;
});
});
Resolve allows you to define a set of tasks that will execute before the route is loaded. It is essential just a set of keys and functions, which allow you to do things like asynchronous http requests, run code snippets, set values, etc (really whatever you want), prior to page load.
So if you had a service that made a http get request and returned a promise to ensure a session exists on the server each time a route occurs, resolve guarantees that it will not load the page until the http request is fulfilled, and the promise is a success. In other words if the fulfilled promise fails the page will not load:
.config([ '$routeProvider', function( $routeProvide ) {
$routeProvider.when('/dashboard', {
templateUrl: 'framework/views/dashboard.html',
controller: 'DashboardCtrl',
controllerAs: 'dashCtrl',
resolve: {
DateOfBirth: ['Date', function( Date ) { // random example of other uses for resolve
return Date.getCurrentYear() - 37;
}],
AuthUser: ['$q', '$location', 'UserSession',
function( $q, $location, UserSession) {
return UserSession.getSession()
.then(function( success ) { // server response 200 = OK
// if you are in here the promise has succeeded and
// the page will load
// can do whatever you want
return success
}, function( error ) { // server response not 200 = Not OK
// if you are in here the promise has failed and
// the page will not load
// can do whatever you want
// if unauthenticated typically you'd:
$location.path('/login);
$location.replace();
// for this read up on promises, but promises can be
// chained, and it says move me to next error promise
// in the chain. if you don't use this it will assume
// the error was handled and pass you to the next
// success in chain. So good practice even if you're
// not chaining
return $q.reject( error );
});
}];
}
})
}]);
Another nice thing about resolve is the keys are injectable. So you can pass the result to your controller:
.controller('DashboardCtrl', ['AuthUser', 'UserSession', 'DateOfBirth'
function(AuthUser, UserSession, DateOfBirth) {
// show all the errors you want by accessing the AuthUser
// data contained in the server response, or just read the
// server response status
var self = this;
self.status = AuthUser.status;
self.response = AuthUser.data;
}]);
Then in your UI you can ngShow or ngBind blah blah on the result using dashCtrl.response or dashCtrl.status, or whatever you decide on doing with the resolved data, and show your errors knowing the page never loaded.
I'd suggest that on route you check the session instead of storing it on the client. Also, keep in mind the resolve only works on routes, but if you're making calls to the server that don't require routing you'll want to look up how to use interceptors. They allow you to peak at the outgoing and incoming server requests/responses unrelated to routing, so those which occur while you're currently on a specific page like /dashboard/home that don't trigger a route, and just simply update /home content.

How long does an angular promise live?

I'm trying to understand the life cycle of angular services and components.
Say I have a controller that uses an http service:
function MyController($scope, $http) {
$http.get('/service').success(function(data) {
// handle the response
});
}
The thing is that this controller may be attached to a view. And I want to make sure that the response is discarded if the view has been removed, this is to prevent conflicts with other requests thay may be triggered in other parts of the application. Will the instance of the controller be destroyed and with it, pending calls from the $http service be canceled if the view is removed? For example, when the user navigates away (without reloading) from the page causing a Javascript render of a new section?
[Edit] I created a jsfiddle that shows that, at least for the $timeout service, the pending operations are still running after the $scope is destroyed by navigating away. Is there a simple way to attach async operations to the scope so that they will be destroyed automatically?
First, attach a reference to your promise, then pass that reference to the cancel function. This resolves the promise with a rejections. So you could also just use promise.reject() in place of cancel(promise)
function MyController($scope, $http) {
var promise = $http.get('/service');
promise.success(function(data){
});
$scope.$on(
"$destroy",
function() {
promise.reject("scope destroyed, promise no longer available");
}
);
}

Angular: Default handler for unhandled http errors

In my angularjs app, I defined a default handler for http errors this way:
myapp.config([ '$httpProvider', function($httpProvider) {
$httpProvider.responseInterceptors.push('errorInterceptor')
}])
where errorInterceptor is a service that displays some details about the error in an alert field on the top of the current page.
Now, when I want to handle a specific error in a different way (say the query is triggered in a modal, and I want to display the alert only in this modal, and not at page level):
$http.get('/my/request').then(success, specificErrorHandling)
Angular does the specificErrorHandling but still triggers my errorInterceptor, so my error gets reported twice. Is there a way to avoid that?
More generically, is there an Angular way to handle only errors that aren't already taken care of along the promise chain, the same way the top-level error handler of a server app doesn't have to handle catched exceptions?
Edit: As requested by Beetroot-Beetroot in comments, here is the code for my interceptor:
#app.factory 'errorInterceptor', [ '$q', 'alertsHandler',
($q, alertsHandler) ->
success = (response) ->
response
failure = (response) ->
alertsHandler.raise(response)
(promise) ->
promise.then success, failure
]
We have something like that.
If we handle the http error, we pass a property on the request called errorHandled:true
$http({
method: 'GET',
url: '/my/url',
errorHandled:true
}).then(function(){ ... }, function(){ ... });
And then in the intercept for responseError: function(rejection){ ... } we can see if this flag is set by looking at rejection.config.errorHandled and if not - then we pop a toastr dialog with the error. the code looks something like this
function ( rejection ) {
if ( !rejection.config.errorHandled && rejection.data.message ){
toastr.error(rejection.data.message, 'Error');
}
return $q.reject(rejection);
}
The chances of someone writing "errorHandled:true" without adding a handler are slim. The chances of having 2 error indicators are also slim because we got used to it - but actually 2 indicators are better than none..
It would be great if we had the promise to query it if it has an error handler or not down the then chain, but we couldn't find this anywhere.
Assuming that you know which errors needs to be suppressed and which one need to be propagate. Also since the Response interceptor is a function that returns promise itself
You can catch the response for failure case and instead of propagating it up the stack you can return something such as empty response.
If you look at the sample example in angular documentation for interceptor
$provide.factory('myHttpInterceptor', function($q, dependency1, dependency2) {
return function(promise) {
return promise.then(function(response) {
// do something on success
}, function(response) {
// do something on error
if (canRecover(response)) {
return responseOrNewPromise; // This can suppress the error.
}
return $q.reject(response); // This propogates it.
});
}
});

Categories