Part of the Solid Foundations Learning Series
This is an in-depth learning series focused on a specific application: a JavaScript-based suite of single-page applications optimized for use in a microservice environment. We focus on telling the story of “why” and “how” it was built.
—
In part one, we reviewed how the base application was created by using the Express Generator command line tool. We also reviewed the folder layout, look and feel, and an overview of what each section of the application does.
In this post, we’re going to dive a little deeper into the application and explain the why and how of the navigation setup for this application.
Why are we using pages to create the navigation?
Yes, when you look through the code, you will notice that we’re not using a database to bring URL information into the application for navigation. Nor will you see our navigation being hardcoded into the HTML markup. The goal of this application, in the beginning, was to investigate and proof-out the idea of using NodeJS and its file system tools to be able to scan through a folder and be able to dynamically create navigation hyperlinks based off of the files that were found.
Why would you want to do this you ask? Simple, I had an itch for some time to create an application where I could house multiple single-page applications and be able to switch between those applications as easily as you would any other application.
To begin with, I had tried just adding in the navigation links directly to the home pages of those applications. This worked, but it was a very manual process. And I always questioned, “how do I turn off an SPA if I don’t want access to it anymore?” Or, “How can I add in another SPA without rebuilding the entire application?” This thought process was the motivation for the navigation of this application.
How is this done?
First, we’re going to modify a few things within the app.js
file. Within this file, Express registers its View engine with the application and dictates the directories where the Handlebars Views will be found. By default, this would be within the views
folder.
By adding this code to the app.js
file:
// view engine setup app.set('views', path.join(__dirname, 'views')); app.set('view engine', 'hbs'); // partials directory setup const hbs = require('hbs'); hbs.registerPartials(__dirname + '/views/partials'); hbs.registerPartials(__dirname + '/views/pages');
I have been able to add two subdirectories to the dynamically found views
folder. The partials
folder will be the home for all smaller elements of the application’s views such as the header
, the footer
and the navigation
components. The pages
folder is where you will find each Handlebars view page
.
The basic setup for a Handlebars view page looks like this:
{{> header }} {{> navigation }} {{!-- triple curly braces for literal content --}} {{{data.contents}}} {{> footer }}
By using the double curly brace with right angle bracket syntax, we are telling Handlebars upon rendering that we would like to import those components from our partials folder and combine them on runtime. The triple curly braces are used to display literal dynamic content which comes from the server and is added during runtime. We’ll go more in depth about that in a later section.
We now have the view in the pages
folder, but we still haven’t touched the navigation yet. Let’s proceed to that step.
Reading the files
In our services
folder, you can find a file called, file.service.js
. This service utilizes some of Node’s built-in file manipulation modules to help create our global navigation. Here is the code which actually scans the views/pages
directory and finds all of the available files.
function createdFilelist(route) { let fileslist = []; fs.readdir("views/pages/", (err, files) => { files.forEach(file => { // remove extension and remove any underscores or hypens let filename = file.slice(0, -4).replace(/_/g, " ").replace(/-/g, ' '); let fileInfo = { fileName: filename, link: file.slice(0, -4), active: false }; if (fileInfo.link === "home") { // undefined will happen at the '/' root of the application if(route === "unknown"){ fileInfo.active = false; } else if (route == undefined || route === fileInfo.link) { fileInfo.active = true; } // went want the home link always first. fileslist.unshift(fileInfo); } else { if (route === fileInfo.link) { fileInfo.active = true; } fileslist.push(fileInfo); } }); }); return fileslist.sort(); }
I’ll break this function down from top to bottom. The function expects a parameter to be passed to it called route
. This is expected to be the current route and will be further discussed in another file.
Next, we create an array to hold all of the files found in the views/pages
directory. Using Node’s fs
module we read the directory. For each file found we’re going to strip out hyphens, underscores, and extensions. We’re going to create a simple object literal and add the filename, the information for the actual link to the link property, and a boolean flag to determine if this is the active link or not.
A series of if
statements follow that manipulate the found files based on the passed-in route
parameter to determine how to create the list. By default, we’re going to be looking for home
or an undefined
route for our initial route. Additionally, if that route is the link being created, we will set the active
property to true at this time.
Once the fileInfo
object has been created, it is added to the fileList
array. When everything is said and done, the list is returned out of the function.
So how is it used?
Well, we made an array of link objects, but now you’re wondering how it’s used. In the routes
folder, we have our actual index.js
file. This file contains the routes for our application and I’m going to go in-depth on two of them.
First, we have our root route, the index of the entire web application. The code for this route is:
app.get("/", function(req, res, next) { let data = { title: "Home", links: fileListing.CreatedFileList(), contents: "", user: user }; // LoadContents returns a promise markDown.LoadContents("home").then(function(html) { if (html) { data.contents = html; } res.render("pages/home", { data }); }); });
Notice that it’s just a simple GET
request as you would expect, but inside the request, we create a data object. The object is where the title of the page is set and the links collection is where we are going to store the navigation object array we just created. The contents and user properties will be discussed later. At the bottom of the function, we call Express’ response render function and pass in the physical path of the ‘pages/home` Handlebars view along with the data object. I’ll explain how the data object is used in just a minute, but let’s address the next route first.
The second GET
route is where most of the action will occur for the majority of the pages found in the application. Here is the code for the function:
app.get("/pages/:route", function(req, res, next) { let route = req.params.route; let data = { title: fileListing.CreateFileTitle(route), links: fileListing.CreatedFileList(route), contents: "", user: user }; // LoadContents returns a promise markDown.LoadContents(route).then(function(html) { if (html) { data.contents = html; } res.render("pages/" + route, { data }); }); });
Right from the start, we’re looking for a parameter from the requested URL. Notice that it is called route
. This is the same string which we used in the createdFileList
function earlier. We again create a data object, but this time when we create the links collection, we’re passing that route
parameter into the function (whereas the root
route did not have the parameter passed in).
This explains the need for the if
statements in the createdFileList
function. The additional sections of this route will be explained later, but let’s drop down to the render function. As you see, it’s very similar to the root
routes render, but here we are actually appending the route to the pages/
path so that we can now dynamically pull back the correct Handlebars view which is going to be rendered to the browser. Again, notice that the data
object has been passed along during the rendering.
Now for the links
So far we’ve just been scanning folders for files and building up data objects to be associated with Handlebars views, but as of yet, we still haven’t rendered the navigation of the application. Let’s speak on that.
Inside of the views
folder, we find that we have a partials
folder which holds a Handlebars component called navigation.hbs
. The code for that looks like this:
<div class="bg-dark mb-3 shadow rounded fixed-top"> <nav class="navbar navbar-expand-lg navbar-dark "> <a class="navbar-brand" href="/"><strong>Dynamic Data</strong></a> <button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarNavAltMarkup" aria-controls="navbarNavAltMarkup" aria-expanded="false" aria-label="Toggle navigation"> <span class="navbar-toggler-icon"></span> </button> <div class="collapse navbar-collapse" id="navbarNavAltMarkup"> <div class="navbar-nav mr-auto"> {{#each data.links}} <a class="nav-link {{#if this.active}}active{{/if}}" href="/pages/{{this.link}}">{{this.fileName}}</a> {{/each}} {{#if data.user.authenticated}} <a class="nav-link {{#if data.active}}active{{/if}}" href="/secure/">secure page</a> {{/if}} </div> <div class="navbar-nav"> {{#if data.user.authenticated}} <span class="text-light navbar-text">Welcome {{data.user.username}} </span> <a href="/logout" class="nav-link">Sign Out</a> {{else}} <a href="/login" class="nav-link">Sign In</a> {{/if}} </div> </div> </nav> </div> {{!-- the closing main tag is found in the footer.hbs file. --}} <main class="mr-5 ml-5">
As you can see, it’s mostly just straight up Bootstrap markup for a navigation bar across the top of the page. But if you look closer you will see a few additions which are Handlebars helpers. The real reason for using Handlebars as our view engine.
This section here is where we take the data
object from the render methods of each route and loop through the links
collection and create a hyperlink for each object found. Notice that we’re using an if
statement to set the URL and the display name for the link, as well as to determine if this particular link should be active
(and have a Bootstrap active class attached).
{{#each data.links}} <a class="nav-link {{#if this.active}}active{{/if}}" href="/pages/{{this.link}}">{{this.fileName}}</a> {{/each}}
The rest of the markup has additional Handlebars helpers for doing a few more manipulations of the markup for security purposes, which will be addressed later.
Wrapping it all up
So the goal was reached. We can add in a simple Handlebars markup page, and on runtime of the application the backing code will find the file, and dynamically add it to a navigation list which will, in turn, render that into a hyperlink which will allow the user to navigate to the page.
As a side benefit, it also allows you to easily remove a navigation link simply by moving the file from the pages
folder. There were several spots in this post where I said we will talk about more in depth, and in the following article we will address:
- Adding content to the pages
- Having secure sections of the application
- Adding in Single-Page Applications and the why behind adding those to this application.
Stay tuned!