Angular JS and Dynamic Menu – Part 2

I have blogged previously on how to build a menu with Angular where menu data comes from the database.  In this post I will elaborate a bit on my technique, revise it some and make adjustments for log in process.  The basic premise is sill the same – I need to get my data from the database, but this time I want to postpone the process until after the login.

So, in my main module I only configure a single route – one to my login screen.

    angular.module('app',
        [
            'ngRoute',
            'app.globalsModule',
            'app.directives.Common',
            'app.directives.validation',
            'app.home.controllers',
            'app.home.services',
            'app.security.contollers',
            'app.security.services'
        ])
        .config(['$routeProvider', 'globalsServiceProvider',
            function ($routeProvider: ng.route.IRouteProvider, globalsServiceProvider: interfaces.IGlobalsProvider) {
                var globals: interfaces.IGLobals = globalsServiceProvider.$get();
                $routeProvider.when('/login', {
                    controller: 'loginController',
                    templateUrl: globals.baseUrl + 'login/logon'
                });
                $routeProvider.otherwise({
                    redirectTo: '/login'
                });
            }]);

This code is similar to the one I posted prior.  The only thing worth noting is that I am using absolute URL for my menus by attaching site root URL to relative view’s path from my database.  Next step is to create home controller with will do a few things:

  • Get menu from a service that talks to Web Api controller
  • Raise an event on the root scope that menu has been retrieved, passing the menu itself as an event argument.
  • Listen to an event that routes have been built and issue initial navigation to home page.

The reason I am using root scope is because it is visible everywhere, and I need a scope to exchange my events.  Seconds step is to use config() function on the same home module to build routes based on my menu from my database.  Of course, after I create route structure I want to navigate somewhere.  Hence, my config block does the following:

  • Listen to the event that menu has been retrieved
  • Populate routes
  • Raise an event that routes have been populated. 

Again, I am using root scope for all the events.  There is one interesting thing about config() function in the module.  I cannot inject instances into it, just providers.  Hence, I cannot even get the root scope.  To get around this issue I want to use angular.injector().  This presents a different problem.  With every call I get a new instance of the injector, hence my root scope from one call is not the same as the one that is injected into the controller by different injector intance.  So, I need to use another workaround.  To guarantee that I get the same injector instance, I need to scrub it off my root HTML element that is decorated with ng-app attribute.  This way I will get instance of exact same objects as the rest of my application.  So, let’s take a look at the controller:

    class homeController extends app.core.controllers.CoreController {
        constructor(
            $location: ng.ILocationService,
            $scope: IHomeScope,
            menuService: app.home.services.IMenuService,
            globalsService: interfaces.IGLobals,
            $rootScope: ng.IRootScopeService) {
            super($scope);
            $scope.navigate = function (menu: app.home.models.IMenuItem) {
                $location.path(menu.Route);
            };
            $scope.searchText = '';
            $scope.search = () => {
                console.log('looking for ' + $scope.searchText);
            };
            $scope.applicationName = globalsService.applicationName.toLocaleLowerCase();
            $scope.$on('loggedIn', () => {
                menuService.getMenu((menu: IMenuItem[]) => {
                    $scope.menuItems = menu;
                    $rootScope.$on('routesLoaded', () => {
                        $location.path('');
                    });
                    $rootScope.$broadcast('menuLoaded', menu);
                });
            });
        }
    }

The key thing to notice is that I get an instance of the root scope and subscribe to login event to kick off menu building. In the logged in event handler I get the menu via menu service.  In the handler for that (function that is passed into to getMenu call) I am saving my menu items, then subscribe to ‘routes loaded’ event, and finally broadcast (raise) an event that the menu has been retrieved.  In my ‘routes loaded’ hander I simply navigate to home page by providing an invalid route, thus going to home page specified in otherwise() call to route provider. 

Now, in config block I do the opposite subscriptions:

.config(['$routeProvider', function ($routeProvider: ng.route.IRouteProvider) {
            var injector: ng.auto.IInjectorService = angular.injector(['ng']);
            var timeout: ng.ITimeoutService = injector.get('$timeout');
            var stop;
            var wait = () => {
                stop = timeout(() => {
                    var finalInjector: ng.auto.IInjectorService = angular.element('[data-ng-app="app"]').injector();
                    if (finalInjector) {
                        timeout.cancel(stop);
                        var scope: ng.IScope = finalInjector.get('$rootScope');
                        scope.$on('menuLoaded', (event: ng.IAngularEvent, ...args: any[]) => {
                            var menu = <IMenuItem[]>args[0];
                            var globals: interfaces.IGLobals = finalInjector.get('globalsService');
                            angular.forEach(menu, (item: IMenuItem) => {
                                if (item.Route) {
                                    $routeProvider.when(item.Route, {
                                        controller: item.Controller,
                                        templateUrl: globals.baseUrl + item.View
                                    });
                                }
                            });
                            $routeProvider.otherwise({
                                redirectTo: '/home'
                            });
                            scope.$broadcast('routesLoaded');
                        });

                    } else {
                        wait();
                    }
                }, 10);
            };
        wait();
    }]);

This code is a bit more convoluted, and it has everything to do with the fact that I cannot inject instances, just providers, into config block.  First, I get an injector service by calling angular.injector().  The only thing I need is timeout service.  I kick off a function every 10 ms that does just one thing – monitors my root HTML element until it has injector populated, which will happen after Angular application is bootstrapped.  This injector is shared for entire application, hence I use it to get an instance or the root scope.  I use the root scope to subscribe and raise events to communicate with my controller.

To summarize, I do not want to have routes injected until after the login process because menu is dynamic for each user.  Because I postpone the routes creation until after the login, I can make sure that only the routes the user has rights to are created.  I also use events to communicate the state of the application between different objects – controller and configuration block.

Enjoy.

2 Comments

  1. Pingback: Angular Dynamic Menu and Initial URL | Sergey Barskiy's Blog

  2. Pingback: Angular JS and Dynamic Menu – Part 2 | Se...

Leave a Reply

Your email address will not be published. Required fields are marked *