Variant of conditional routing in AngularJS

    I am new to AngularJS, I recently decided to use it in my hobby project. Quite quickly, I faced the task of setting up routing according to certain conditions, the simplest and most obvious of such conditions is whether the user is authorized or not. The application contains pages open to any user, and pages where you can go only by logging in. If there is no authorization, you need to try to get it transparently for the user, if this is not possible, correct it on the login page.

    As far as I understand, this task is quite common, however, I did not find a simple ready-made way to do this out of the box. Having spent a fair amount of time googling, reading documentation and experimenting, I ended up finding a fairly elegant solution for my case. I hasten to share my bike, I hope this helps to save time for the same novice users of AngularJS. Perhaps there is a guru who will point to the very standard solution that for some reason I did not find. For example, I have not yet figured out ui-router .

    Task


    It’s worth writing a little more about my task. There is a single 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 log in. Upon successful login, the application receives an authorization token and user profile. A profile is a spreading data structure and, to reduce the load on the server, I want to download it once with login, and not in parts on each page. The authorization token can be stored for a long time in the local storage of the browser, but the profile data is downloaded again each time the page is updated. Having received the token once, I can freely go through all closed pages, reload any of them, add to bookmarks, etc. the profile is loaded transparently for me. But if the token goes bad or I give a link to a closed page of a site to a friend, the site should send me (or a friend) to the main page.

    Search for a solution


    The only useful thing that Google issued on the request "Angularjs conditional routing" is this question on stackoverflow . Two solutions were proposed there:

    First, to send 401 status from the server and intercept it through the $ http service — a request to the server from each protected page is assumed. Maybe it’s suitable for someone, but not for me - I upload the data once and dont want to finish the server for routing on the client.

    The second is to intercept the message $ routeChangeStart, check where we are going, whether there is authorization and, if not, redirect. Alternatively, listen for 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 routing paths; it’s not very convenient to understand where we are going; this message will also be thrown on redirects from non-existent pages, as a result, expressions in the conditions of routing 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 routing until the end of the download. At the same time, routing may depend on the data in the user profile itself, for example, it has a new super offer and you need to drop everything and urgently go to the page of this offer.

    I had an idea with redundant data to redirect to a separate “download page”, there to upload data and redirect according to the results, but firstly, the routing logic is spread out in two places - in one we look at the path, in the other the data; secondly, the user will have this intermediate page in history. The history can be erased using $ location.replace (), but if for some reason the download is delayed and the user has time to click Back, the wrong page will be erased, but it will also be corrected from another page, you need to somehow process this case that does not add simplicity to the solution. Thirdly, we need to remember somewhere where we went in order to correct it correctly, taking into account the situation from the "second." This decision did not inspire me and I continued to search.

    Decision


    AngularJS provides a service with the interesting name $ q. You can read in the documentation why q and the defered / promise specification are a fairly simple and interesting concept. In short, we ask the service to make a special object. From this object we obtain a promise object and give our client the code. The client hangs on the promise callbacks of the success and failure of the operation. Now when we do on our object or

    var defered = $q.defer();




    return defered.promise;




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




    defered.resolve(result);




    defered.reject();


    the client is called the appropriate callback. What is better than regular callbacks? promises can be chained (for details, see the documentation) and, which is important for my task, many AngularJS services can work with them, including $ routerProvider in the route configuration, you can specify a resolve field and pass a function that returns a 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 resolved, and if reject occurs, it will be completely canceled. Then everything is simple - we write a function that loads data, if necessary, does all the checks and redirects. If you need to load the data, the promise is returned, if you need to make a redirect, the promise is redirected before it so that the old route does 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’s even strange why I didn’t find this right away on the network (now, I hope, it will be easier to find). Among the shortcomings, it is necessary to specify resolve in each route, but on the other hand it gives configuration explicitness 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: The comments encouraged me to write code with passing promises in a chain from $ http.post (), which, like other methods of the $ http service, returns a promise. With this kind of natural use of promises, we get a cool clear separation of the process into stages and functions with a clear contract.

    The mechanism of promise chains is this: the then promise method returns another "derivative" promise, which is resolved with the value that one of the handlers specified in then returns - either a resolver or a regex - or will be detected 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, in order to register a derivative promise, it is enough 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);
                    });
            }
        }
    };
    

    Also popular now: