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: MAIN.CFM

Within the current development pattern I have for our portal system, the main.cfm is the starting point for an application. For the most part, this is really boilerplate and serves to get the application off the ground. The way the portal works is that the user lands on the main menu page and provide with a menu of options. Each option is called a function and is assigned a unique id. When the user clicks on a menu option, the page is reloaded with the function id. The process checks to see if the user is actually allowed to have access to the function. It then accordingly looks up the path and displays the page via a . In the case of the TRIPS application, the included file is the main.cfm page for the application. So far the project looks like this: we have the folder structure defined with no files aside from the main.cfm. The main.cfm contains a few javascript includes, the overarching controller for the application as well as the route viewer. The next blog post will foc

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 ret

The Conversion: Trips for Keeps - Part 2

Ack! A few days off work and then updates to other applications that needed to be made has delayed more work on TRIPS. But we're back and ready to continue. In the last post , we started to build out the trip.html template and it's corresponding controller. The dataService was updated with an abstraction that would allow a CFC method to be called and data to be passed.  This post will focus on retrieving that data from the dataService to the CFC method. Before we get started, I thought it would be kind of cool to keep a count of the number of lines of code for the project.  I'm not sure if it will provide any insight, but it might be nice to compare the line count to the number of lines in the Flex application.  Rather than posting the counts on each blog post, I have created a separate post tracking the line count per post . In review, let's take a look at the dataService code that's going to call the Coldfusion function to save the trip information. this.p