Step Four (Building a SPA): Client-Side Routing

Routing is not an easy topic with a SPA. When we talk routing in a web application we are referring to how we match the delivery of content to a user’s action.  In Node (to include Angular), routing is done through the URL and is designed to be done via the server.  When a user clicks on an element, a URL is sent to the server.  Attributes are passed through the URL.   This results in a new page or data being sent back to the client.
In a SPA, the only calls to the server after the initial load should only be for data or messaging.  This interaction will be via a RESTful JSON call or a WebSocket call.  These differ between a call to a database or service using a REST call, while web sockets are normally used messaging.

Request and Render

SPA routing in angular is not native.  What is native in Angular is URL routing, but SPA routing requires us to create the ability to:

  1. Capture the Request
  2. Render the new Content

To accomplish this, I use a skeleton based on another post, “Nested Views, Routing, And Deep Linking With AngularJS”.   

This code leverages four local scripts to handle the routing, which are placed in the js directory:

  • manage-routes
  • render-context
  • request-context
  • loadash-service (used by the context scripts)

It also requires two types of templates:

  • Controller templates (placed in the public/controllers directory)
  • View templates (placed in the public/views directory)

This post is focused on how to leverage the “request-context” and “render-context” scripts.  The only modification to either of these scripts is to add the module name to associate the anonymous function to our module. Please view the referenced blog above for further details.

These files follows a pattern of how we define module additions using an anonymous function:

(function( ng, app ) { … })(angular, <module_name>)

In this example,  we are adding to the “Simple” module. It looks like this:

(function( ng, app ) { … })(angular, Simple)

This pattern of passing the module into an anonymous function is used throughout complex angular examples.  For example, you will see later that the “app.value” resolves to “Simple.value” for the render-context example.  We can also do this with controllers, services, factories and directives.

The code for this posting is on GitHub

Managing Routes (Request Context)

manage-routes.js will be our main script.  It will define the module, called in the example “Simple”.  It is this module that is actually what we think of as our application.  “var Simple = angular.module( “Simple”, []);” is the first line of the file. We will then add to this module.  For example, we will add the “.configure” to handle the routing.

In the configure module, we create an action string based on the route passed by the event.  This is the string that is used by the rendering functions to determine what content and controllers to use in rendering of the page.

The script uses a global variable in Angular called “$routeProvider”.  Each time a route event occurs, the page will evaluate this variable and update the action.  This update will cause the controllers and their related views to change.


// Create an application module for our release.
var Simple = angular.module( "Simple", []);
// Configure the routing. The $routeProvider will be automatically injected into
// the configurator.
// This is a skeleton based on AngularJS Routing (https://github.com/bennadel/AngularJS-Routing)

Simple.config(
function( $routeProvider ){
// We are mapping routes to render "Actions" rather than a template.
$routeProvider
// Here is were we define our routes on the load.
//
//  In this example we are going to create two main layouts
//     - single: A splash screen
//     - standard: A screen that has a menu
.when(
"/first",
{
action: "standard.first"
}
)
.when(
"/second",
{
action: "standard.second"
}
)
.when(
"/",
{
action: "single.login"
}
)
// Send a user the splash screen when we do not understand the route.
.otherwise(
{
redirectTo: "/"
}
);
}
);

Each “when” function is chained against the $routeProvider variable. This is checking the first argument of the when function against the $routeProvider.  When there is a match, the hash argument is returned.  Most cases this response updates the “action”.  The exception is the catch (otherwise) that redirects.  This forces the program to loop based on the redirect as the new route.

In short, the function converts the route into an action string.  The string is used by request-context and render-context to determine what is to be “shown” to the user, and what controllers are to be active.

We place this file (manage-routes.js) in our js directory.  We use this directory to place our client side javascript files that are not the controller of the views. This means we also place our factories, services and directives here.

Display View (Render Context)

Now that we have an action string that tells us what views and controllers are to used based on the route, we need to create the code to leverage it.

Render-context file

We leave the “render-context” file alone (just like the request-context.js), but we do need to update the last line so that it knows the module name for our application.  This makes the function definition look like this:

(function( ng, app ) {
"use strict";
app.value(… )
})(angular, Simple)

As noted before, the app.value will resolve to Simple.value.  This structure makes the modification of code for a new project simpler.

Context Rendering Controllers

There is symmetry between the controllers and the views.  For each controller, there is a view for the exception of the base controller, which is placed in the root of the controller directory.
This means we are adding two directories.  One directory is for the controllers of the application (controllers) and the other for the views that are rendered (views).
In the “controllers” directory we are going to place a controller for each level of the nested view.  The context render function will evaluate at each layer.  This allows us to make the outermost part of the view dynamic.  The action reads from left to right, with the outermost template to the left, which will be defined as a layout view.
What does the controller look like in this structure?   There are two parts,

  • Part of the controllers relates to the controllers for the data binding and functions for interaction
  • The other part handles the rendering.

The base templates only contain the render aspect of the controller, as the binding and function are unique to the application.  There are commented areas to show where to put the application’s controllers.

We create the controller using the format we saw when we created the default values:

(function( ng, app ) {
"use strict";
app.controller(
“<controller name>”,
function( $scope, $route, $routeParams, $location, requestContext, _ ) {
… }
)
})(angular, Simple)

App.controller resolves to Simple.controller.  This is given a name and an associated anonymous function.  We pass the global variables as to contain their scope against its associated view.  We place a controller at each level, like so:
When creating a controller for a view, there are three variables we need to be aware of:

  • The controller name needs to be defined,  The view will call this name (First.FirstController)
  • The context name needs to be defines.  This is the action variable from the route.  (standard.first)
  • Since you will be using a template, make sure that the module name passed in is correct. (Simple)

(function( ng, app ){
"use strict";
app.controller(
"first.FirstController",
function( $scope, requestContext, _ ) {
// --- Define Controller, Scope Methods and Variables. -------- //
// Get the render context local to this controller (and relevant params).
var renderContext = requestContext.getRenderContext( "standard.first" )
// The subview indicates which view is going to be rendered on the page.
$scope.subview = renderContext.getNextSection();
// --- Bind To Scope Events. ------------------------ //
// Handle changes to the request context.
$scope.$on(
"requestContextChanged",
function() {
// Make sure this change is relevant to this controller.
if ( ! renderContext.isChangeRelevant() ) {
return;
}

// Update the view that is being rendered.

$scope.subview = renderContext.getNextSection();
});

// — Initialize. ———————————- //

$scope.setWindowTitle( “Welcome to Simple” );
}
);
})( angular, Simple );

Despite the amount of code here, there really is not much going on.  In fact, there are only two things going on that involve the context:

  • Define the scoped “getNextSeciton” by assigning it to subview
  • We watch to see a change in requestContextChange.  If this occurs we check to see if there needs to be an update to the rendering.

We do a controller to each page in the “views”, even the layouts.  The layout is the outermost part of the view and appears left most in the action.

Defining the Views

There are two views we want to take a look at: a layout and a view.  We want to look at a layout view and then a complete page with a subview.  I will save putting one subview into another subview for a future post.

What of the main index page? In a SPA, the first page that is downloaded loads the application and establishes the DOM.  The application will then leverage this DOM to render the page for the user.  I will show this at the end of the post when we put this all together.

Layout (Standard)

In the clip, I’ve removed the menu html so we can focus on the view.  The code uses an ng-switch attribute.  When a controller is runs, it will de-activate unused views and activate the ones it needs.  In this example, the ng-value of  first is activated and angular will then place the ‘views/first/index.html’ into this location.


<!-- BEGIN: Layout Container. -->
<div>
<!-- BEGIN: Menu Layout. We place the menu here -->

<!-- BEGIN: Layout Header. -->
<!-- Include Body Content. -->

<div ng-switch=”subview”>
<div ng-switch-when=”first”  ng-include=” ‘views/first/index.html’  “></div>
<div ng-switch-when=”second” ng-include=” ‘views/second/index.html’ “></div>
</div>
</div>

<!– END: Layout Container. –>
<!– BEGIN: Layout Footer. –>
<!– END: Layout Footer. –>
</div>

When developing the pages, ensure that the switch names and include files are correct.

View (First.index)

The layout will call the subview.  Here we need to make sure that the html includes the proper name of the controller.  Anything placed inside the div of this controller will appear where the switch was defined.


<div ng-controller="second.SecondController">
<div>
<div class="col-md-3">
<div class="well">
<legend>Place Holder</legend>
<p> This is a place holder to we figure out what is the outside structure of the application.</p>
</div>
</div>
</div>
</div>

The two views combined (views/layout/stardard.html and views/single/index.html) create what to the user appears to be a new page:

page1

This is a powerful aspect of a SPA in angular.  When using the Angular development tools in chrome, you will see that there is a scope for each view.  This allows interaction for every element on the page.

Updating the main index.html page

Lastly, the index.html page initially loaded by the SPA needs to call all the scripts. We are going to replace the “hello” example from before and we are going to add the new scripts in the following order:

  • 3rd party modules (via bower, note we added lodash)
  • manage-routes
  • render-context
  • request-context
  • loadash-service


<!doctype html>
<html ng-app="Simple" ng-controller="AppController">
<head>
<meta charset="utf-8" />
<title ng-bind="windowTitle">Our First Angular Program</title>
<!--

Add our style sheets here.

-->
<link href="bower_components/bootstrap/dist/css/bootstrap.css" rel="stylesheet">
</head>
<body>
<div ng-switch="subview">
<!--

This subview will show while the AngularJS app is loading since it’s visible right in the page. Once the app kicks in, however, the calculated subview will determine the appropriate layout to render.


-->
<div ng-switch-when="loading">
<h4>Welcome to Simple.</h4>
<p>Loading Application</p>
</div>
<!-- Core layouts. -->
<div ng-switch-when="single"    ng-include=" 'views/layouts/single.html' "></div>
<div ng-switch-when="standard" ng-include=" 'views/layouts/standard.html' "></div>
</div>
<!-- Load Browser-Side JavaScript. -->
<script type="text/javascript" src="bower_components/jquery/jquery.js"></script>
<script type="text/javascript" src="bower_components/angular/angular.js"></script>
<script type="text/javascript" src="bower_components/lodash/dist/lodash.js"></script>
<!-- Load AngularJS application module. (Load this before page controllers) -->
<script type="text/javascript" src="js/manage-routes.js"></script>
<!-- Load Controllers. -->
<!-- Layouts. -->
<script type="text/javascript" src="controllers/layouts/single-controller.js"></script>
<script type="text/javascript" src="controllers/layouts/standard-controller.js"></script>
<!-- Pages and Subviews. -->
<script type="text/javascript" src="controllers/first/first-controller.js"></script>
<script type="text/javascript" src="controllers/second/second-controller.js"></script>
<script type="text/javascript" src="controllers/splash/splash-controller.js"></script>
<script type="text/javascript" src="controllers/app-controller.js"></script>
<!-- Load Routing Support. (Load these after the page controllers) -->
<script type="text/javascript" src="js/lodash-service.js"></script>
<script type="text/javascript" src="js/request-context.js"></script>
<script type="text/javascript" src="js/render-context.js"></script>
</body>
</html>

With the index.html updated, you can now run nodemon to run the application.  The route for “/” will match the action “single.splash”.  This will combine the two and allow the user to navigate to the other pages:

splash

Summary

This post introduces the idea of routing on the client side of the SPA.  This makes the application truly a SPA.  Next we can start to add REST calls to the application.

Comments

  1. This tutorial is great! Do you know when the next one will be done? I can’t wait to get the server side working.

    • Thanks for the comment. Just finishing the first part of the server side. After which, I will be focusing on how to secure the SPA data calls. There are still larger postings to cover robustness and unit testing. Cheers.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s

%d bloggers like this: