Skip to main content

Dynamically Loading an AngularJS UI.Router

When starting out on my AngularJS journey, I couldn't get a good handle on the router native to the framework. So I adopted the use of the wonderful angular-ui / ui-router. During the past few years of development, I've honed in (for better or worse) my paradigm for setting new applications and nearly every AngularJS app has a routes.js file.

Without going into background, I wanted a way to load the ui-router dynamically. Typically, the routes gets defined in a .js file and typically looks something like this:

angular.module('core').config(function($stateProvider, $urlRouterProvider) {
    $urlRouterProvider.otherwise("/landing/201820");
    $stateProvider
        .state('landing', {url: "/landing/:term", templateUrl: "apps/noted/templates/landing.html"})
        .state('uploadCalendar', {url: "/uploadCalendar", templateUrl: "apps/noted/templates/uploadCalendar.html"})
        .state('noteTakers', {url: "/noteTakers", templateUrl: "apps/noted/templates/notetakers.html"})
        .state('noteReceivers', {url: "/noteReceivers", templateUrl: "apps/noted/templates/notereceivers.html"})
        .state('studentAssignments', {url: "/studentAssignments", templateUrl: "apps/noted/templates/studentAssignments.html"})

});

I thought it would be cool if I could somehow dynamically load the router via a JSON file produced from a database. This would allow, over time, the root application to become more standardized yet flexible when delivering routes to the end user.

My first step was to set up a data table to the routing information. This table contains information for both the router and the menu system.  Each record in the table represents a route for a specific application in the overarching framework. In this case, a Function is equivalent to application.


The fields that will be utilized for the dynamic loading are:
  • Function ID - determines the application to load
  • ROUTE_NAME - name of the route
  • URL - url of the route as it appears in the browser
  • TEMPLATE_URL - path to the HTML template of the route
  • CONTROLLER - path to the controller's Javascript file for the given template/route
The FrameController is doing a few more things than just loading the routes. The first part of the code sets $rootScope state variables because I am going to need the routing information for the frame template and I wanted a quick and easy way to reference $state values.

The controller dependencies are as follows:
  • $rootScope, $scope
  • $ocLazyLoad - needed to lazyLoad the controller Javascript 
  • $state, $stateParams
  • $stateRegistry - this is the key dependency to registering the state
  • frameDataService - interface to the database
The $scope.getUrlParameters function does what it says, which is to pull in url parameters. The overarching application that the AngularJS code sits in has not been angularized (yet), so I needed a way for the AngularJS app to know which user function was being called.

The $scope.goto function is just a quick way to navigate to the default route after routes have been loaded.
angular.module('core').controller('FrameController', function($rootScope, $ocLazyLoad, $scope, $timeout, $state,$stateParams, $stateRegistry, frameDataService){
    $rootScope.$state = $state;
    $rootScope.$stateParams = $stateParams;

    $scope.getUrlParameter = function(sParam) {
     var sPageURL = decodeURI(window.location.search.substring(1));
     var sURLVariables = sPageURL.split('&');

     for (var i = 0; i < sURLVariables.length; i++)
     {
         var sParameterName = sURLVariables[i].split('=');
         if (sParameterName[0] == sParam)
         {
             return sParameterName[1];
         }
     }
 }

    $scope.goto = function(_route, _params){
        console.log(_route);
        console.log(_params)
        $state.go(_route, _params);
    }

    frameDataService.call('getFunctionRoutes&_f=' + $scope.getUrlParameter('f')).then(function(data){
        $scope.routes = data.data;
        for (var i=0; i < $scope.routes.length; i++){
            console.log( $scope.routes[i])
            if ( $scope.routes[i].name.length == 0){
                $ocLazyLoad.load([$scope.routes[i].controller]);
            }
            if ( $scope.routes[i].name.length > 0){
                var _state = {
                    name: $scope.routes[i].name,
                    url: $scope.routes[i].url,
                    templateUrl: $scope.routes[i].templateUrl,
                    icon: $scope.routes[i].icon,
                    displayName: $scope.routes[i].displayName,
                    instructions: $scope.routes[i].instructions,
                    c:  $scope.routes[i].controller ,
                    lazyLoad : function($transition$,_controller){
                        return $transition$.injector().get('$ocLazyLoad').load(_controller.c);
                    }
                }
                $stateRegistry.register(_state)

                if ($scope.routes[i].status == 'ROOT'){
                    $scope.goto($scope.routes[i].defaultRoute,JSON.parse($scope.routes[i].defaultParams));
                }
            }
        }
    })
});
But the intended core of this post revolves around the next section of code: frameDataService.call(). frameDataService is a simple service that uses the $http functionality of AngularJS to query that database via Coldfusion. The end result is a data that is in JSON format - so it really doesn't matter what is used. A sample of the data looks like the following:
Once the frameDataService.call returns, I loop over the results. Keep in mind that I massage the route properties from their respective database names.
for (var i=0; i < $scope.routes.length; i++){
    console.log( $scope.routes[i])
    if ( $scope.routes[i].name.length == 0){
        $ocLazyLoad.load([$scope.routes[i].controller]);
    }
    if ( $scope.routes[i].name.length > 0){
        var _state = {
            name: $scope.routes[i].name,
            url: $scope.routes[i].url,
            templateUrl: $scope.routes[i].templateUrl,
            icon: $scope.routes[i].icon,
            displayName: $scope.routes[i].displayName,
            instructions: $scope.routes[i].instructions,
            c:  $scope.routes[i].controller ,
            lazyLoad : function($transition$,_controller){
                return $transition$.injector().get('$ocLazyLoad').load(_controller.c);
            }
        }
        $stateRegistry.register(_state)
        if ($scope.routes[i].status == 'ROOT'){
            $scope.goto($scope.routes[i].defaultRoute,JSON.parse($scope.routes[i].defaultParams));
        }
    }
}
First I test to see if the .name (ROUTE_NAME) exists. If not, I assume that this is a file to be included into the application, but no route is needed. For example, this could be a dataService. If there is a .name, then I create an object called _state. For the ui-router, the key properties are the name, url, templateUrl and to lazyLoad the corresponding controller. For the lazyLoad function, I discovered (accidentally) that the second parameter passed in is the _state. This allows us to easily pass in the path to the controller file.

The next step, after the _state is defined, is to register the _state by using the $stateRegistery.register function. The final step is to navigate to the ROOT route. This is in lieu of setting the $urlProvider.otherwise property which I haven't figured out to do yet.

So that's it in a nutshell. Get a JSON list of the routes; iterate through building a state object for each route; and then registering each state.

Just as a note: You'll see additional properties defined in the _state that are not actually part of the ui-router. These properties are used later in the framework to help standardize the look and feel of the content.

Hope this wasn't too confusing. As time permits, I may add some clarity later.

Cheers!

Comments

Post a Comment

Popular posts from this blog

The Conversion: Introduction

While working as the senior level developer for a small liberal arts college, I've had the luxury of deciding the technology to be used internally for custom applications. When I first started, the college was using Coldfusion 7, HTML, and many MS Access databases. Over the course of the past 11 years, we've moved to Oracle, are on CF11, and utilizing AngularJS / Angular for our front ends.  Coldfusion is still doing the heavy lifting in interfacing with Oracle. During those 11 years, we also found ourselves developing Adobe (now Apache) Flex applications.  Flex was a great tool - easy to develop in and fairly easy to deploy when Flash was ubiquitous. But Flex was not great on mobile platforms.  Hence our move to Angular due to the idea that we wanted to give our users a rich web experience. So over the course of the past two years, we've been either retiring Flex applications altogether due to the direction that some departments have taken (read outside vendor); or r...

The Conversion: Lay of the Landing Page - Part 1

In the last post  the groundwork has been laid for real development to begin. The data service, router, and landing pages have been created and are ready for code. The last thing to add for this part of the process is the Coldfusion component. A data folder has been added to store any cfcs relevant to the TRIPS application. The first is to write the CFFUNCTION that will return the list of trips. My development pattern for creating CF functions is fairly routine.  First, decide on a name, parameters and return type.  Sometimes the naming of a function is the hardest part.  I've settled on actionDataObject.  So in this case, the function will be named: getTrips.    At moment, the landing controller (landing.js) is empty. angular.module('core').controller('LandingController',['$scope','dataService', function($scope,dataService){ }]); The basic task of this controller is getting the list of trips.  First is the establishment of an empty tr...

The Conversion: Getting Started

So enough about me and my employer from the first two blog posts.  It's time to get some real work started.  This project is about converting the Adobe Flex based TRIPS application to and AngularJS front end.  Now, I have done some work in Angular 2.x (Typescript), but that was a separate deployment into a different (but similar) framework.  Because of the time frame I have to convert this application and it's overall complexity, I am going to stick with AngularJS. Before I get into details, just a quick note about the application framework.  In order to make the management applications easier, I built a small portal based on the concept of users, roles and functions.  All employees can be users of the system provided they accept the FERPA agreement. Access to functionality depends on the roles the user has and which functions are assigned that role. For example, our faculty users have access to class lists and course information via their FACULTY role. ...