Lesson Overview

A lot of web sites and web applications get their dynamic content from either databases or structured data files/objects such as plain text files, XML (Extensible Markup Language) or JSON (JavaScript Object Notation) files. Although XML is still used in many applications today, JSON is more popular with JavaScript applications. In this lesson we'll look at some simple applications you can create with dynamic data that is read in from JSON files. If you're interested, there's also a lesson on how to create dynamic content from a MySQL database, also.

Pre-Requisites

Before doing this lesson, make sure you've done the Express MVC lesson and that you've done the demonstration programs and exercises, as you'll continue building those in this lesson. You should also have read through the EJS Templates lesson.

It is also assumed that you've worked with JSON objects in JavaScript before. I recommend The Modern JavaScript Tutorial: Objects and Mozilla Developers' Network: Working with JSON.

Reading a JSON File

In a previous lesson on Express MVC lesson, you started working on a project that displayed a daily menu from a JSON file. If you didn't complete the project, you can access the daily menu project here.

Recall that in the menu.json file, there is a single object that contains 7 properties: Sunday, Monday, Tuesday, Wednesday, Thursday, and Friday. Each property contains its own object with 3 of its own properties (Breakfast, Lunch, and Dinner"). Our previous project used a URL Parameter to capture a day name (which ideally should correspond to one of the 7 object property names e.g. localhost:3000/mvc/Tuesday). We then passed that value to our controller function that rendered the index page. The controller function passed to the index page both the day of the week from the URL parameter, and the object for that day. In the index page, we displayed the day of the week and also displayed at least one of the day-object's 3 properties (you might have displayed all three, Breakfast, Lunch, and Dinner if you were exploring the example).

In the Embedded JavaScript lesson, you learned how to use scriptlets to include Javascript selections and loops in your .ejs/html pages. This means we could actually display our menu in a much nicer way!

Start a new project and copy over your original menu project (or copy my version from the link up above). Open the /views/index.ejs file in the editor.

level 3 headings for breakfast/lunch/dinner, each heading shows a paragraph with the contents for that meal item
Proposed output for our MVC program

Delete the contents of the <main> element. We're going to add a for-in loop (for..in allows you to iterate object properties, which you can't do with a for..of loop) to iterate through the 3 properties inside the object we passed into the index page:

<% for (let m in menuInput) { %>

  <h2><%=m%></h2>
  <p><%=menuInput[m]%></p>

<% } %>

Don't forget to initialize your application and install express and ejs.

Now run your program and try each day of the week (make sure you use title case). You should see the menu, similar to the earlier screen shot.

What happens if you don't enter a valid day of the week? How do you think you might fix this?

One easy way is to make the day URL parameter optional by adding a question mark (?) to the end of the parameter name in the request URL in the app.js file:

app.get("/menu/:day?", homeController.renderIndex);

In our handler in the controller, we can simply assume that if the day was not included in the URL, there is no menu for that day and pass a null value instead of an actual day value and menu object:

exports.renderIndex = (req, res) => {
  let menuInput = null; // assume empty
  let dayInput = null; // assume empty

  // if there is a day parameter:
  if (req.params.day) {  
      dayInput = req.params.day;
      menuInput = menu[req.params.day];
  }
  res.render("index", {
      dayInput: dayInput,
      menuInput: menuInput
  });
};

In the index.ejs file, we can then use scriptlets to decide what to display if the day was passed in or if null was passed in:

<header>
  <h1>MVC: Index Page</h1>
  <%if (dayInput) {%>
    <h2>Menu for <%=dayInput%></h2>
  <%} else {%>
    <h2>Menu Not Available</h2>
  <%}%>
</header>

The loop doesn't need to be modified at all: it will only iterate if there are properties, and a "null" object has no properties.

A second option could be to create a 404 error page when the URL doesn't contain the extra parameter segment, and this would also be useful to deal with an issue when the segment is not one of the 7 days of the week in title case. That will be covered in an upcoming lesson.

Creating Dynamic HTML

Another cool thing you can do by reading a JSON file is to create dynamic HTML elements such as menus, articles, etc. For example, say we wanted to add a simple menu to the top of our header, and we want the menu information to come from a JSON file:

[
  {
      "path": "/visitors",
      "text": "Visitor Information"
  },
  {
      "path": "/about/staff/staffinfo",
      "text": "Our Staff"
  },
  {
      "path": "/about/index",
      "text": "About Us"
  },
  {
      "path": "/covid",
      "text": "Covid Updates"
  }
]

This file contains an array of objects, and each object has 2 properties:

So, for example, the first menu item would appear as <a href="/visitors">Visitor Information</a> and when clicked, it will load /views/visitors.ejs. Similarly, the second link will appear as <a href="/about/staff/staffinfo">Our Staff</a> and when clicked, it will load /views/about/staff/staffinfo.ejs

Copy the JSON code up above and paste it into navigation.json in your project's /models directory.

First, let's create the actual links. Then we'll get them working.

Creating the Links

This is fairly simple: in the controller, add the statement to import the navigation.json file:

const navigation = require(path.join(__dirname, "../", "models", "navigation.json"));

Inside the renderIndex() handler, make sure you pass the navigation array to your index.ejs file (I passed mine is as the local variable "nav"):

exports.renderIndex = (req, res) => {
  let menuInput = null;
  let dayInput = null;
  if (req.params.day) {
      dayInput = req.params.day;
      menuInput = menu[req.params.day];
  }
  res.render("index", {
      dayInput: dayInput,
      menuInput: menuInput,
      nav: navigation
  });
};

Lastly, update your index.ejs file to add a navigation element with your links, using a for..of loop:

<header>
  <nav>
  <% for (let link of nav) {%>
    <a href="/menu<%=link.path%>"><%=link.text%></a>
  <%}%>
  </nav>
  <h1>MVC: Index Page</h1>
  ....

Notice that for the link's href attribute, I used /menu<%=link.path%> - This means every link's href will be "/menu" followed by the value of the object's path property. I also set the link text to the object's text property.

If you run your program, you should see the links at the top of the header. Inspect the page DOM and make sure it renders similar to the screen shot below:

nav element with four links
DOM for the navigation on the page

Now that we have the links appearing, how do we make them work? We need to add request handlers for each link in the navigation.

If you want to try the program we finish, go into the /views directory and create the files and directories for the four menu items (visitors.ejs, covid.ejs, /about directory and its index.ejs, and the /staff sub-directory inside /about with the staffinfo.ejs file).

Link Request Handlers

There are several ways to get the links to work: you just need request handlers for each request. However, this can be cumbersome: You would have to add an app.get() for each request URL (/menu/visitors, /menu/about/staffinfo, /menu/about/index, and /menu/covid) and you'd have to add a controller function for each one. Furthermore, the controller functions would all be pretty much the same:

// **** in app.js:
app.get("/menu/visitors", homeController.navToVisits);
app.get("/menu/about/staff/staffinfo", homeController.navToStaff);
app.get("/menu/about/index", homeController.navToAbout);  
app.get("/menu/covid", homeController.navToCovid);
  
app.get("/menu/:day?", homeController.renderIndex);

// **************************
// **** in homeController:
exports.navToVisits = (req, res) => {
  res.render("visitors");
};
exports.navToStaff = (req, res) => {
  res.render("about/staff/staffinfo");
};
exports.navToAbout = (req, res) => {
  res.render("about/index");
};
exports.navToCovid = (req, res) => {
  res.render("covid");
};

Surely there is a way to do this effectively without all the redundant code? You could use a single handler to render all the navigation pages, you just have to slice off the /menu from req.url using the JavaScript Array.slice() function:

exports.navToPage = (req, res) => {
  res.render(req.url.slice("/menu/".length));
};

For the slice() argument, I used "/menu/".length - this avoids counting errors and makes it easier to modify later (actually, using a constant for "/menu/" would have been even better), but also makes it more readable (slice(6) doesn't make it obvious what the 6 is for).

Then you would imagine you need to update each of your request handlers with the new navToPage() controller function:

app.get("/menu/visitors", homeController.navToPage);
app.get("/menu/about/staff/staffinfo", homeController.navToPage);
app.get("/menu/about/index", homeController.navToPage);  
app.get("/menu/covid", homeController.navToPage);

However, this code is still redundant. What if we were able to do something such as:

app.get("/menu/*", homeController.navToPage);

In this handler, we're saying that any URL that starts with /menu/ and ends with any segment or any number of segments should be handled by the navToPage() function. But unfortunately, this doesn't work. Try putting this statement above the request handler for /menu/:day? and run your program.

the two lines of code
The handler for page navigation is above the handler for menu day

When you try to reload your index page, you get an error that the nav object in your index.ejs is not defined. That's because the nav local variable was only passed via the renderIndex() handler, but now your page is loading via the navToPage() handler. Why? Because the request for /menu/ or /menu/Tuesday matches the URL pattern /menu/* - remember that the requests are routed in the same order as your route handlers. So then, shall we try putting the app.get("/menu/*") below the app.get("/menu/:day?")? Try it and then reload your index page.

the two lines of code in reverse order
The handler for menu day is above the handler for navigation

Now you'll find that your index page loads correctly.

Try the links in the navigation menu - do they work? You should find that your links to staffinfo and /about's index page work fine, but the links to visitors and covid udpates reload the index page, using the path in the "day of week" spot in the scripts. Why?

Because when your browser requests /about/index and about/staff/staffinfo, the route handler that appears first, /menu:day? doesn't match because there is more than one path segment after "/menu". So the second route handler for /menu/* is used. However, when the browser requests /visitors or /covid, these request URLs match the first route handler /menu:day?, and the value in the day parameter contains "visitors" or "covid". So the renderIndex() function executes and sends the value "visitors" or "covid" to the index.ejs file.

A simple fix for this is to make sure we can separate the navigation links from other requests by adding an extra segment to the href value. Go into your index.ejs file and modify the statement inside the scriptlet's for-of loop that creates each link inside the <nav>:

<a href="/menu/nav<%=link.path%>"><%=link.text%></a>

I added "/nav" to the href URL, so now all of the navigation links will start with the URL pattern /menu/nav

Now we can update our route handler that handles navigation links by adding /nav to the URL pattern:

app.get("/menu/nav/*", homeController.navToPage);

This handler says that when a request comes in for /menu/nav/ followed by anything else (any segment or any number of segments), execute the navToPage() function in the controller. This request handler can go above or below the handler for /menu/:day? since they're not the same URL pattern anymore.

Now go to your controller and update the navToPage() handler:

exports.navToPage = (req, res) => {
    res.render(req.url.slice("/menu/nav/".length));
};

In our handler, we render the page as defined by the request URL, minus the "/menu/nav/" path segments.

If you re-run your program now, everything should work: the index page loads and each of the navigation links load.

You can explore with this technique - what other page elements might be created from JSON data? What if you had more than one navigation menu (e.g. one in the header and one in the footer)? Can we use a similar technique to load static files such as CSS and static HTML? Yes! You'll learn how to load static files in the lesson on Loading Static Files.