Part 2: Navigation​ Setup with Node + Express ​

Chris Berry Articles, JavaScript, Microservices, Node.js, Single-Page Application, solidfoundationsseries, Tutorial, Vue.js Leave a Comment

Attention: The following article was published over 6 years ago, and the information provided may be aged or outdated. Please keep that in mind as you read the post.

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.

Related Posts:  Simplifying IAP Setup on GCP with Terraform

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

Related Posts:  Your Keyboard as an Output Device?

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!

Series Quick Links

5 1 vote
Article Rating
Subscribe
Notify of
guest
0 Comments
Oldest
Newest Most Voted
Inline Feedbacks
View all comments