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

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...