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.
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.
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:
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:
This file contains an array of objects, and each object has 2
properties:
path - this will be the path to the file, relative
to the /views directory of the project. For now, I'm assuming all
navigation links are going to .ejs files, but in future you could
modify this example to load other static HTML files, also
text - the link text for the link
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:
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:
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:
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:
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:
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.
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.
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>:
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:
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.