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.
Pingback: Angular Dynamic Menu and Initial URL | Sergey Barskiy's Blog
Pingback: Angular JS and Dynamic Menu – Part 2 | Se...