3.1 - Templating
🎯 Objectives
- Separate presentation code from logic code using a templating engine.
- Control the data that is displayed using conditionals and loops inside the template files.
- Nest templates within one another to reuse presentation code.
🔨 Setup
- Navigate to the template repository for this exercise and follow these directions to fork it.
- Assuming Docker is started, in VS Code, hit
CMD/CTRL + SHIFT + P
, search + rundev container: open folder in container
, and select the downloaded folder. - In the terminal of VS Code, hit the
+
icon to open a new terminal instance. Runls
to make sure you’re in the root directory of the exercise and that you seepackage.json
. - Run
npm install
to install all the dependencies. - Run
npm run server
inside a JavaScript debug terminal to start the server.
🔍 Context
We’ve done the M and C of MVC, which means we only have the V left! The View is the part of the application where the presentation code lives - the pretty part! 🤩 Views are files that contain the HTML/CSS/JS that the user will see and interact with. The main goal of templating is to separate logic from presentation.
The template will have placeholders of where data should be populated. Instead of having dedicated pages for Home and About:
<body> <h1>Home</h1></body>
<body> <h1>About</h1></body>
We can now make one template and pass in the value dynamically:
<body> <h1>{{heading}}</h1></body>
To populate the heading
variable, we’re going to make use of a templating engine. Specifically, we’re going to be using the Handlebars templating engine.
🚦 Let’s Go
Part 1: Simple Template
-
Inside
src/page.hbs
, write a simple HTML page. Simply type!
and use the autocomplete to generate the boilerplate HTML for you. Even though this file is not an.html
file, we can still write regular HTML syntax inside. The<body>
of the page should have:- An
<h1>
tag that contains a Handlebars expression tag which will output the value of{{ heading }}
. See the context section above if you’re not sure what that looks like. - An
<img>
that contains a Handlebars expression tag which will output the value ofimage
for itssrc
attribute:<img src="http://localhost:3000/{{ image }}">
. As you can see, you can use the Handlebars expression tag anywhere inside of HTML syntax.
- An
-
Make an
images/
folder in the root of the project, i.e. not insidesrc/
. This is where we’ll store all the images we want to display on our website. -
Download an image from the web to display on our website.
- It can be a Pokemon related image, or if you decided to use your own resource like books or movies, find an image related to that.
- Make sure to download it into the
images/
folder.
-
Inside
src/controller.ts
, let’s replace the JSON response insidegetHome
with an HTML response.-
The status code will remain the same (200).
-
The
Content-Type
header should be changed fromapplication/json
totext/html
to tell the client that we’ll now be sending back HTML data. -
Finally, instead of outputing JSON in
res.end()
, we now need to render the template to output the HTML we need.controller.ts res.end(await renderTemplate("src/page.hbs", {heading: "Homepage!",image: "images/image-from-previous-step.png",}));- We give it the name of the template file,
src/page.hbs
- We also give it the data that the template will need. The
heading
andimage
properties of this object map to the same ones we defined inside ofpage.hbs
.
- We give it the name of the template file,
-
-
Start the server by running
npm run server
. -
In a browser, navigate to
http://localhost:3000
and you should see a web page with the “Homepage!” and your image. -
Right-click and select “view page source” to see the HTML that was sent to the browser. You should see the HTML that you wrote in
page.hbs
, but with the{{ heading }}
and{{ image }}
placeholders replaced with the values you passed in.
Part 2: List View
-
Inside
page.hbs
, add a<ul>
tag that contains a Handlebarseach
tag. This will loop through all the objects in thedatabase
array frommodel.ts
, much like aforEach
loop, and will put each value inside of a<li>
tag:page.hbs <ul>{{#each pokemon}}<li>{{name}} is a {{type}} type Pokemon.</li>{{/each}}</ul> -
In the
getAllPokemon
controller method, make the same changes you made to thegetHome
method from the previous section. When callingres.end()
, you’ll pass inheading
: “All Pokemon” (or a title of your choice)image
: The same image as the homepage, or a new image if you’d like to display a different one for this page.pokemon
: The database imported frommodel.ts
.
controller.ts res.end(await renderTemplate("src/page.hbs", {heading: "All Pokemon!",image: "images/your-image.png",pokemon: pokemon,})); -
In a browser, navigate to
http://localhost:3000/pokemon
and you should see a web page with all the Pokemon (or your entities) listed.
Part 3: Partials
A more intermediate use of templates is splitting up “common” parts of the UI into their own, smaller, templates. The goal is to define all the UI components once and try to minimize any repeated code.
-
Create
views/partials/Header.hbs
andviews/partials/Footer.hbs
. Cut everything inpage.hbs
from the top of the file up to and including the opening of the<body>
tag, and paste it intoHeader.hbs
.Header.hbs <!DOCTYPE html><html lang="en"><head><title>Pokedex</title></head><body> -
Cut everything from the bottom of
page.hbs
up to and including the close</body>
tag and paste it intoFooter.hbs
. To make it more interesting, we’ll add a footer with a copyright message:Footer.hbs <footer>© Copyright 2024 Your Name</footer></body></html> -
Inside
page.hbs
we can use Handlebar’s partial tag to include the content fromHeader.hbs
andFooter.hbs
:page.hbs {{> Header }}<!-- h1, image, and list -->{{> Footer }} -
To get this to work, we need to register the partial template with Handlebars. I’ve provided a function for this called
registerPartialTemplate()
that takes the name of the partial and the path to where the template file lives:server.ts const hostname = "127.0.0.1";const port = 3000;registerPartialTemplate("Header", "src/views/partials/Header.hbs");registerPartialTemplate("Footer", "src/views/partials/Footer.hbs"); -
In your browser, navigate to all the routes to make sure they all still work.
Bonus: Single View
If you want an extra challenge, see if you can implement the getOne
controller function to serve an HTML page which displays one Pokemon and their image depending on the ID from the request URL.
- Download an image per Pokemon (or resource of your choice) into the
images
folder. - Add an
image
attribute to eachPokemon
object insidemodel.ts
. The value of this attribute should be the path to the image (ex.images/pikachu.png
). - In the controller method, use the attributes from the requested Pokemon to set:
heading
as the name of the Pokemonimage
as the Pokemon’s image
If done correctly, you should see a different Pokemon when you navigate to /pokemon/1
, /pokemon/2
, etc.
📥 Submission
Take a screenshot of your list page and submit it on Moodle.
If there’s one thing to remember from this exercise, it’s that templates/views exist to separate presentation code from logic code! Any developer can make the application work; What separates the novice from the experts is the organization and cleanliness of your code. 🧼