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

Project Setup

For this demonstration, create a new project (e.g. /dynamic) and add the /models, /views, /controllers, and /public directories. You can download a .zip file of the contents of the /public directory from GitHub. Unzip the file inside the /public directory of your project.

Add two ESJ files to your /views directory: index.ejs and results.ejs.

You can add this base code to your index.ejs:

<!doctype html>
<html lang="en">

  <head>
    <meta charset="utf-8">
    <title>ABC Company Main Page</title>
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <link rel="stylesheet" href="./css/main.css">
  </head>

  <body>
    <header>
      <nav>
      </nav>
      <h1>ABC Company</h1>
      <h2>Main Page</h2>
    </header>

    <main>
      <h2>Welcome to ABC Company</h2>
      
    </main>

    <footer>
      <address>&copy; 2023 Firstname Lastname</address>
    </footer>
  </body>

</html>

You can add this code to your page2.ejs:

<!doctype html>
<html lang="en">

  <head>
    <meta charset="utf-8">
    <title>ABC Company Page 2</title>
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <link rel="stylesheet" href="./css/main.css">
  </head>

  <body>
    <header>
      <h1>ABC Company</h1>
      <h2>Secondary Page Page</h2>
    </header>

    <main>
      <h2>An Example Secondary Page</h2>
      <p><%=sampleData%></p>
      <p><a href="/dynamic">Home</a></p>
    </main>

    <footer>
      <address>&copy; 2023 Firstname Lastname</address>
    </footer>
  </body>

</html>

Creating Dynamic Navigation from JSON

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.html",
        "text": "Visitor Information"
    },
    {
        "path": "hiring.html",
        "text": "Join Our Team"
    },
    {
        "path": "page2",
        "text": "Secondary Page"
    },
    {
        "path": "about/index.html",
        "text": "About Us"
    },
    {
        "path": "about/staff/index.html",
        "text": "Our Staff"
    }
]

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.html">Visitor Information</a> and when clicked, it will load /public/visitors.html file. Similarly, the last link will appear as <a href="about/staff/index.html">Our Staff</a> and when clicked, it will load /public/about/staff/index.html file.

Copy the JSON code up above and paste it into links.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 links.json file:

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

Add a request handler renderIndex() to exports: make sure you pass the links array to your index.ejs file (I passed mine is as the local variable "links"):

exports.renderIndex = (req, res) => {
  res.render("index", {links: links});
};

Add another handler to render page2.ejs. This one requires a simple variable "sampleData" - set it to whatever value you like.

exports.renderPage2 = (req, res) => {
    res.render("page2", {sampleData: "foobar"});
}

Lastly, update your index.ejs file to adda for..of loop to your <nav> element:

<header>
  <nav>
   <% for (let link of links) {%>
      <a href="<%=link.path%>"><%=link.text%></a>
    <%}%>
  </nav>
  ....

Notice that for the link's href attribute, I used <%=link.path%> - This places the path property value of each link object into the href attribute. I also set the link text to the object's text property value.

Add the app.js file to your project and add the necessary code to add express, set the view engine to EJS, set up a middleware to log all requests, and start the server:

"use strict";

const express = require("express"),
    app = express(),
    path = require("path"),
    mainController = require("./controllers/mainController");
app.set("port", process.env.PORT || 3000);
app.set("view engine", "ejs");

app.use((req, res, next) => {
    console.log(req.method, req.url);
    next();
});

app.listen(app.get("port"), () => {
    console.log(`server running on port ${app.get("port")}`);
});

We have types of requests to handle:

We can add the three middleware in the order listed above, under the middleware to log all requests:

app.use((req, res, next) => {
    console.log(req.method, req.url);
    next();
});

app.use("/dynamic", express.static(path.join(__dirname, "public")));

app.get("/dynamic", mainController.renderIndex);

app.get("/dynamic/page2", mainController.renderPage2);

Notice that the last middleware uses "/dynamic/page2" as the URL pattern. That's because the path property for that object in the links array is set to "page2". You should also recall that all the rest of the objects in the links array are static files and will be handled by the express.static() middleware.

GET-requests to /dynamic will all load the index.ejs file. You might have noticed in all the static HTML files provided, there is a paragraph with a link written as
<a href="/dynamic">Home</a> That will make a request to /dynamic, which is hanled by the same middleware that loads the index page.

Also make note of the path used to the main.css file in each static HTML file: visitors.html and hiring.html use "./css/main.css", which causes a request to /dynamic/css/main.css (express.static() ensures that this loads the css/main.css file in the /public directory). The about/index.html uses the path "../css/main.css". If we used "./css/main.css", then the request would be "/dynamic/about/css/main.css" which is incorrect, so we add use "../" (previous directory) instead of "./" (current directory) so that the request becomes "/dynamic/css/main.css". This is also why the about/staff/index.html path to the css file is "../../css/main.css" (it has to go up 2 directories to get to the correct location of the /css directory).

If you haven't already, init your program and install express, then run the project:

npm init
npm install express ejs -s
nodemon app [or node app]

Load up localhost:3000/dynamic in your browser: 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

Also check each sub page and make sure the CSS is loading for each one and that the "Home" on each one brings you back to the main page.

Further Modifications

What if you wanted the same menu on each page of the project? To do this, each page would have to be a dynamic page, since the menu is written in EJS code. Feel free to modify your project. Is there a way you can easily load all those "previously static" EJS files with one single middleware function? This is something you should try!