Jesse Lawson

Software engineering, artificial intelligence, writing, and open-source tools

Jan 1, 0001 - AngularJS Code Snippets Tutorials

CMS-like Dynamic Routing in AngularJS

As I’m designing a CMS-like system in AngularJS, one of the things I find myself needing to do is create simple HTML pages stuffed full of content. What I don’t want to do is create a new partial view and add it to my route for every page that I create in the future. Ideally, we would employ a system that automagically understands to follow a specific set of rules in finding a file when one is requested.

In this article, we’re going to do just that.

A dynamic, flat-file routing methodology for AngularJS.

Let’s say you want to have the ability to dynamically process routes like this:

/#/:a/:b

where :a and :b are nothing more than route parameters. In a bare-bones CMS-style system, we can use route parameters like this to help us organize our content.

What we’ll do is start organizing our content in a file structure that resembles this:

{% highlight javascript %} \app \views \pages index.html another-page.html \forests trees.html plants.html \animals cats.html dogs.html index.html {% endhighlight %}

Notice how we’ve organized all of our files around file folders; our folder structure, written as a dynamic route, might look something like this:

/views/pages/:group/:page

So in theory, if we can create a single controller that will be called when a request is made to the above route, and then dynamically fetch content based on the values of :group and :page from within that controller (via a service), we will have a simplified (albeit daunting at first) CMS-style dynamic routing system in AngularJS.

Step 1: Setup Routes

Let’s go ahead and assign our routes now. In app.js, let’s configure our route provider:

angular.module('yourApp', []).config( function( $routeProvider )
    .config(function ($routeProvider) {
    $routeProvider
      .when('/', {
        templateUrl: 'views/home.html',
        controller: 'MainController',
        stylesheet: 'home.css',
      })
      .when('/:group', {
          templateUrl: 'views/pages/index.html',
          controller: 'PagesController',
      })
      .when('/:group/:page', {
          templateUrl: 'views/pages/index.html',
          controller: 'PagesController',
      })
      });

Let’s take a look at what we’ve done here by thinking about how we can access our different pages. We want to create a dynamic routing system, so we’ll need to have measures in place to take an array of different values (that’s supposed to be a very bad programming pun). Some of the URLs I’d like to be able to serve and their possible locations look like this:

/#/academics/math will draw from

  • /views/pages/academics/math.html

/#/academics could be any of the following:

  • /views/pages/academics/index.html
  • /views/pages/academics.html

Do you see what I’ve done here? In a url structure that looks like /#/:group/:page, the :group can be either a page or a sub-folder on my filesystem. Now, if someone tries to access /#/about/locations, they’ll be served up with the partial /views/pages/about/locations.html file; if they try to access /#/locations, then our system will first try /views/pages/locations/index.html, then /views/pages/locations.html before throwing a 404 error.

Now that we’ve got our routes defined, let’s go ahead and create our PagesController (the controller assigned to our variable routes).

Step 2: Pages Controller

I created a new file called app/controller/pages/PagesController.js, and it looks like this:

'use strict';

angular.module('coastlineApp')
  .controller('PagesController', function ($scope, $routeParams, PageLoader) {
      
    // Using our PageLoader factory, 
        // we create getPage() to be called in the home.html file 
    $scope.getPage = function() {
        return PageLoader.getThePage();
    }
      
    $scope.lastURL = function() {
        return PageLoader.getLastURL(); 
    }
  
    $scope.htmlReady();
    });

Nice and straight-forward: we are injecting a factory called PageLoader and then calling its getThePage() function. Simple? Simple. Exactly how controllers are supposed to be.1

Step 3: Page Loader service

Technically this is a factory, but we’ll still call it a service because that’s what it is; it’s a service for our PagesController to use. Remember: our PagesController’s sole responsibility is to connect the view (which is a page we’ll create in Step 4) and the back-end, which is what we’re creating right now.

I created a new file called app/services/PageLoader.js, and it looks like this:

angular.module('yourApp')
        .factory('PageLoader',['$http', '$routeParams', '$location',function($http, $routeParams, $location){
        var pageURL = "";
        var page404 = "/404";
        
      // Here's a simple function to see if our page exists.
      function PageExists(url)
      {
          var http = new XMLHttpRequest();
          http.open('HEAD', url, false);
          http.send();
          return http.status!=404;
      }
      
      function go404() {
          $location.path(page404);
      }
      
      return {

      getThePage: function() {
          // Pull information from the $routeParams, 
          // which should be the :group and :page objects.
          var pageGroup = '/'+$routeParams.group;
          var pageName = '/'+$routeParams.page;
          var is404 = false; // placeholder for checking whether our page exists
          
          // If both :group and :page are defined, we can see if that page exists.
          if(angular.isDefined($routeParams.group) &&  
             angular.isDefined($routeParams.page)) {
              
              // Build our url
              pageURL = 'views/pages'+pageGroup+pageName+'.html';
              
              // Test to see if it exists. If if does, we're done.
              console.log("Looking for "+pageURL+"..."); 
              if(PageExists(pageURL)) {
                  return pageURL;
              } else {
                  // Since :group and :page were set, 
                  // we know that if this file doesn't exist then there's no 
                  // mistake about it: it's not physically present on the server. 
                  go404();
              }
          } else {
              // One of the route parameters was not set. Most likely, 
              // it was the :page because someone is trying to
              // access /#/page and not /#/group/page
              if(angular.isUndefined($routeParams.page)) {
                  // If it is indeed the page that is undefined, 
                  // then let's look to see if they are looking for
                  // the index file in a group 
                  // (i.e., /#/academics -> /#/academics/index.html)
              
                  pageURL = 'views/pages'+pageGroup+'/index.html';
                  
                  console.log("Looking for "+pageURL+"..."); 
                  
                  if(PageExists(pageURL)) {
                     return pageURL; // We're done!
                  } else {
                      // The file /#/:group/index.html does not exist, 
                      // so maybe they meant to access /#/:group.html
                      // For example: /#/academics -> /#/academics.html
                      pageURL = 'views/pages'+pageGroup+'.html';

                      console.log("Looking for "+pageURL+"..."); 
                      if(PageExists(pageURL)) {
                          return pageURL; // Finally done!
                      } else {
                          go404(); // Nothing else possible; return 404
                      }
                  }
              } else {
                  // The only thing that could trigger this 
                  // is if the $routeParams.group was undefined but the :page
                  // was not. That would be odd indeed. 
                  // We'll throw a 404 error here for good measure.
                  go404();

              }
          
              go404();
          }
      }
    } // /return
    }]);

As with most JavaScript code that I’ve come across, it seems like there is a lot of stuff going on here but in reality it’s quite simple:

  1. We check to see if the :group and :route parameters were set. If so, we’ll try to serve that page and shoot out a 404 error if we can’t.
  2. If :page isn’t set, then we try to serve :group as if it were a page (and again, shoot out a 404 if we can’t find it)
  3. We shoot 404 errors if anything else goes wrong (because we want to strictly serve from our static partials).

If you head back up to our routes, you’ll notice that our dynamic routes are using the partial views/pages/index.html. We’ll need to build that next, and integrate it with our controller.

Step 4: The Partial

I created views/pages/index.html, and it looks like this:

<div ng-controller="PagesController">
        <div ng-include="getPage()"></div> 
    </div>

That’s it! Notice that all we’re doing is connecting our view (the partial) to our controller. Since the controller uses the PagesLoader service to find the appropriate partial’s url to include, all this partial does is include whatever dynamically generated2 partial our service tells our controller to tell this page.

Step 5: There is no step 5.

Technically this system is all setup. The only thing that’s left is for you to physically go into the filesystem and create these partial pages. You don’t need any controllers or fancy markup, just simple HTML files. If you wanted to extend what I’ve done here, you could add a part to the PagesLoader service that uses Angular’s $sce compilers and compiles javascript and even Angular markup before returning the partial to the controller.

While this system works well for the specific application that I’ve developed it for, a more commercially-aware (i.e., scalable, CRUDable, user-friendly-able, etc) would pull these partials from a MySQL database and render along with it the

variables that are necessary for SEO. A truly dynamic system would require this, so the only real dynamic thing about our system here is the routing schema;

It’s dynamic because you don’t have to edit the routes every time you want to add a new page. All you have to do is create the physical .html file and boom, you’ve got a new page.

I’ll be updating this code to be more dynamic across the stack, and you can look forward to a second version of a dynamic AngularJS routing system that pulls from a MySQL database in the future.

  • It’s generally good practice to keep the meat and potatoes of your application inside services and leave the controller’s to fusing together the views and the logic. If you are using your controllers for all of your logic, try pulling the heavy stuff out and sticking it into a service. Unfortunately, this type of bad habit is often latently reinforced by our reliance on things like JSFiddle et al as we teach ourselves how to master these web technologies. Keep this in mind the next time you’re looking at fiddles for your code. 
  • As you can see, it’s not technically dynamic only because our pages are all static. That being said, the routing methodology itself is considered dynamic because there is no set in stone routing schema. In a future article, I’ll go over how we can create a fully-dynamic system by drawing from a MySQL DB all the values for the page content and even header information (for SEO).