šŸ“œ ā¬†ļø ā¬‡ļø

Option of conditional routing in AngularJS

I am new to AngularJS, just recently decided to use it in my hobby project. Pretty quickly, I was faced with the task of setting up routing for certain conditions, the simplest and most obvious of these conditions - whether the user is authorized or not. The application contains pages that are open to any user, and pages that can only be accessed by logging in. If there is no authorization, you need to try to get it transparently for the user, if that fails - refer to the login page.

As far as I understand, this is a fairly common task, however I have not found a simple ready-made way to do it out of the box. Having spent a fair amount of time on googling, reading documentation and experiments, I finally found a rather elegant solution for my case. I hasten to share my bike, I hope this will help save time to the same novice AngularJS users. Perhaps there is a guru who will indicate the very standard solution that I somehow did not find. For example, I did not understand ui-router yet.

Task


It is worth a little more to write about my task. There is a one-page web application (Single Page Application). For simplicity, we assume that there is one publicly accessible "main" page along the path "/", that is, at the root of the site. On it, the user can register or login. If the login is successful, the application receives an authorization token and a user profile. The profile is a spreading data structure and, to reduce the load on the server, I want to load it once at login, and not in parts on each page. The authorization token can be stored for a long time in the browser’s local storage, but the profile data is reloaded each time the page is refreshed. Having received the token once, I can freely browse all closed pages, reload any of them, add to bookmarks, etc. The profile is being loaded transparently for me. But if the token becomes rotten or I give a link to a closed page of the site to a friend, the site should send me (or a friend) to the main page.

Search solutions


The only useful thing that Google issued to the request of ā€œAngularjs conditional routingā€ is this question on stackoverflow . Two solutions were proposed there:
')
The first is to send status 401 from the server and intercept it via the $ http service — a request to the server from each protected page is assumed. Maybe someone will do it, but not for me - I load the data once and I would not like to finish the server for the sake of routing on the client.

The second is to intercept the $ routeChangeStart message, check where we are going, whether there is authorization and, if not, redirect it. As an option, listen to path changes through $ scope. $ Watch (function () {return $ location.path ();}. The disadvantages of this solution are:

1. In the case of $ routeChangeStart, the next object does not provide a routing path; it is not very convenient to understand where we are going; the message will be thrown on redirects from non-existent pages, as a result, the expressions in the routing conditions will not be very beautiful, tied to the names of templates, the names of controllers and other strange things.
2. If you need to load data, as in my case, there is no way to delay the routing until the end of loading. At the same time, the routing may depend on the data itself in the user profile - for example, it has a new super-offer and you have to drop everything and go to the page of this offer immediately.

I had a thought with the missing data to redirect to a separate ā€œdownload pageā€, there to load data and redirect by results, but firstly the routing logic is spread in two places - in one we look the way, in the other data; secondly, the user in history will have this intermediate page. The history can be overwritten using $ location.replace (), but if the download is delayed for some reason and the user has time to press Back, the wrong page will be erased, but also among the other page, you need to somehow handle this case that does not add simplicity to the solution. Thirdly, we need to memorize somewhere where we were going in order to correctly correct, taking into account the situation from the ā€œsecondā€. This decision did not inspire me and I continued searching.

Decision


AngularJS provides a service with the interesting name $ q. The documentation can read why q and the specification for defered / promise is a fairly simple and interesting concept. In short, we ask the service to make a special object.

var defered = $q.defer();


From this object, we obtain a promise object and give it to the client of our code.

return defered.promise;


The client hangs on promise callbacks of success and failure of the operation

promise.then(function (result) {...}, function (reason) {...});


Now when we we do at our facility

defered.resolve(result);


or

defered.reject();


The client will call the appropriate callback. How is this better than ordinary callbacks? promises can be chained (for details in the documentation) and, importantly for my task, many AngularJS services can work with them, including in $ routerProvider in the route configuration you can specify the field field and pass the function returning promise . Moreover, if this function returns an object that is not a promise, it will be interpreted as a promise that has already been resolved. The route will wait until the promise is raised, and if a reject happens, it will be canceled altogether. Then everything is simple - we write a function that loads the data, if necessary, does all the checks and redirects. If you need to load the data, a promise is returned, if you need to make a redirect, the promise will be rejected in front of it so that the old route will not wait in vain.

Solution code:

 'use strict'; var app = angular.module('app', []) .config(['$routeProvider', function($routeProvider) { $routeProvider .when('/', { templateUrl: "login.html", controller: LoginController }) .when('/private', { templateUrl: "private.html", controller: PrivateController, resolve: { factory: checkRouting } }) .when('/private/anotherpage', { templateUrl:"another-private.html", controller: AnotherPriveController, resolve: { factory: checkRouting } }) .otherwise({ redirectTo: '/' }); }]); var checkRouting= function ($q, $rootScope, $location) { if ($rootScope.userProfile) { return true; } else { var defered = $q.defer(); $http.post("/loadUserProfile", { userToken: "blah" }) .success(function (response) { $rootScope.userProfile = response.userProfile; defered.resolve(true); }) .error(function () { defered.reject(); $location.path("/"); }); return defered.promise; } }; 


As a result, it turned out quite simply and transparently, it is even strange why I didn’t find one right away on the network (now, I hope, it will be easier to find). Among the shortcomings it can be noted that resolve should be specified in each route, but on the other hand, this gives a clear configuration and flexibility - you can write a couple more of the same check * functions (if the logic for different pages is completely different) and use where necessary.

UPDATE: Comments encouraged me to write code with the transfer of promises along the chain from $ http.post (), which, like other methods of the $ http service, returns a promise. With this, natural, use of promises we get a cool clear separation of the process in stages and functions with a clear contract.

The mechanism of the chain of promises is this: the then method of the promise returns another ā€œderivativeā€ promise, which is resolved with the value returned by one of the handlers specified in the then - resolv or reject - or ordered if one of the handlers throws an exception. Moreover, if the return value is the promise itself, then its result will determine the result of the derived promise. So, to book a derivative promise, it suffices to return $ q.reject ().

As a result, the solution looks like this:

 //    ,   resolve . var checkRouting = function ($q, $rootScope, $http, $location, localStorageService) { //     function redirect(path) { if ($location.path() != path) { $location.path(path); //  . return $q.reject(); //   . } else { return true; //   . } } return getUserDataPromise($q, $rootScope, $http, localStorageService) .then(function (userData) { //       . if (userData.sales.lenght > 0) { return redirect("/sales"); //   ,   ! } else { return true; //  ,  . } }, function (reason) { //      . console.error(reason); //     ; ) return redirect("/"); }); }; //  ,      userData,  . var getUserDataPromise = function ($q, $rootScope, $http, localStorageService) { if ($rootScope.userData) { return $q.when($rootScope.userData); //    ( ) . } else { var userToken = localStorageService.get("userToken"); if (!userToken) { return $q.reject("No user token."); //     . } else { //  ,        , //  ,     . return $http.post("/loadUserData", { userToken: userToken }) .then(function (result) { if (result.data.userData) { $rootScope.userData = result.data.userData; return result.data.userData; } else { return $q.reject("Got response from server but without user data."); } }, function (reason) { return $q.reject("Error requesting server: " + reason); }); } } }; 

Source: https://habr.com/ru/post/200662/


All Articles