Dynamic Content and Template Engine - aakash14goplani/FullStack GitHub Wiki
-
Our current structure that logs incoming requests:
/* admin.js */ router.use(bodyParser.urlencoded({extended: false})); router.post('/product', (req, res, next) => { console.log('Product Added: ', req.body.title); res.redirect('/'); }); module.exports = router;
-
Instead of logging data, we can store that in array and access it in other files
/* admin.js */ const products = []; ... router.use(bodyParser.urlencoded({extended: false})); router.post('/product', (req, res, next) => { console.log('Product Added: ', req.body.title); products.push( { title : req.body.title} ); res.redirect('/'); }); exports.route = router; exports.product = products;
- Here we add an array where we store incoming data as an object
- Initially we used to export just our routes, now we also need to export our data too so that it can be accessible to other files as well. For this we use alternate export syntax that enables to export multiple stuff from file.
-
In the target file where we want to access this data, we can simply import the array and start using data:
/* in shop.js file */ const adminData = require('./admin'); router.get('/', (req, res, next) => { console.log('Data in shop.js: ', adminData.product); res.sendFile(path.join(rootDirectory, 'views', 'shop.html')); });
-
This is one way of sharing data but this has one disadvantage.
- If I reload the page, data still persists.
- If I open a new tab in the same browser, data still persists.
- If I open a different browser (say Mozilla and my initial browser was chrome), data still persists.
-
The data here is actually inherent to our node server as it is running and therefore, it's shared across all users. Sometimes this is what you may want but for most of times you want to fetch data for a specific request and if that happens to be the same data you show for all users that send this request, this is fine but sharing this data across requests, across users is typically something you don't want to do because if you now edit this with user A, user B will see the updated version even though you might not want to show that.
- For putting dynamic content into our html pages, we would use templating engines. Templating engines work like this:
- We got a html template that holds our HTML markup, related images, css and js files
- A node express content in your app, like a dummy array or any content/data
- A templating engine which understands a certain syntax for which it scans your html template and where it then replaces placeholders or certain snippets depending on the engine you're using with real html content on the fly.
-
Templating engines converts your templates (EJP/PUG etc) to normal html code. The entire conversion takes place on the server (and not browser) on fly
-
Templating engines also do some advanced stuff like caching some built templates for you if the data input didn't change, so that they can return the template even faster and don't have to rebuild it for every request even if the data didn't change.
-
Three most used types and syntax
-
Installation (all 3 in one go...):
npm install --save ejs pug express-handlebars@latest
Topics Covered
-
Set the PUG variable globally using
app.set()
. It takes configuration as key-value pair. Use reserved keywordsviews
andview engine
app.set('view engine', 'pug'); app.set('views', 'views');
-
view engine
allows us to use PUG configuration within Express. Its value will be the templating engine that we are going to use, in our casepug
. -
views
allows us to tell express where to find these PUG templates. It's value is the name of the folder that holds all PUG related templates. - So now we're telling express that we want to compile dynamic templates with the pug engine and where to find these templates.
- If you're starting the server from outside of the folder then you need to provide an absolute path to the views folder:
app.set('views', path.join(__dirname, 'views'))
-
-
PUG templates have extension
.pug
. These are bit different then regular HTML, they use minimal html syntax and later compile to actual HTML document. -
PUG example
<!DOCTYPE html>
html(lang="en")
head
meta(charset="UTF-8")
meta(name="viewport", content="width=device-width, initial-scale=1.0")
meta(http-equiv="X-UA-Compatible", content="ie=edge")
title #{title}
link(rel="stylesheet", href="/css/main.css")
link(rel="stylesheet", href="/css/product.css")
body
- Corresponding HTML
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Shop Product</title>
<link rel="stylesheet" href="/css/main.css">
</head>
<body>
-
Indentation matters in pug: you basically structure your nesting of html with indentation levels.
-
Last step is to configure express such that it can server PUG templates and we do that using
render()
method/* initial */ res.sendFile(path.join(rootDirectory, 'views', 'shop.html')); /* PUG configuration */ res.render('shop');
- we don't have to construct a path to that folder instead we can just say
shop
i.e. file_name. - We also don't need
shop.pug
because we definedpug
as the default templating engine so it will look for.pug
files.
- we don't have to construct a path to that folder instead we can just say
-
The
render()
method allows us to pass in data that should be added into our view. To add data we can pass a JavaScript object as second argument:res.render('shop', { prods: adminData.product, title: 'My Shop'});
- Here
prods
is the key which holds products arrayadminData.product
- We can pass multiple properties in k-v pair. Our second property is page title where
title
is the key and 'My Shop' is corresponding value.
- Here
-
We can bind this to our PUG templates using syntax
#{property_key_name}
i.e. a hashtag followed by two curly braces and between these curly braces, you can put any value you are passing into your view:if prods.length > 0 .grid each product in prods header.card__header h1.product__title #{product.title} else h1 No Products
-
If you only change something in the PUG template, you don't need to restart the server because the templates are not part of your server side code, they are basically just templates which are picked up on the fly.
-
If you have same code repeated over multiple files, you can export the common code in a separate file and use that piece everywhere:
<!DOCTYPE html> html(lang="en") head meta(charset="UTF-8") meta(name="viewport", content="width=device-width, initial-scale=1.0") meta(http-equiv="X-UA-Compatible", content="ie=edge") block title // dynamic content link(rel="stylesheet", href="/css/main.css") block styles // dynamic content body header.main-header nav.main-header__nav ul.main-header__item-list li.main-header__item a(href="/", class=(path === '/' ? 'active' : ''), name=title) Shop // dynamic content li.main-header__item a(href="/admin/add-product", class=(path === '/admin/add-product' ? 'active' : ''), name=title) Add Product // dynamic content main block content // dynamic content
- In the above code
block
keyword lets you inject dynamic content. A file extending above layout can inject content accordingly. Syntax:block <string_key>
. Example:block title
. Here different files can enter different titles. - Similarly with
block styles
andblock content
you can inject content dynamically
- In the above code
-
Importing this layout file:
extends layouts/main-layout.pug block title title #{title} block styles link(rel="stylesheet", href="/css/forms.css") link(rel="stylesheet", href="/css/product.css") block content h1 #{title}! form.product-form(action="/admin/product", method="post") div.form-control label(for="title") Title input#title(type="text", name="title") button(type="submit") Add Product
- Here we are first extending the layout file named
main-layout.pug
defined in folderlayouts
..pug
extension here is optional, if you don't specify one, engine will default it topug
, since this is the engine we are working on. - In layout file we have defined
block <key>
where-in we can inject dynamic content. To inject dynamic content, we provide exact same key or tag mentioned in layout followed by content to be injected. Be careful of indentation.block styles link(rel="stylesheet", href="/css/forms.css") link(rel="stylesheet", href="/css/product.css")
- Above code will inject dynamic content in layout file.
- Here we are first extending the layout file named
-
You can also inject content conditionally
a(href="/", class=(path === '/' ? 'active' : ''), name=title) Shop
- Here we are trying to attach class
active
conditionally based on the input path. So if path is/
, class will beactive
else it will be blank. - To set the above path variable, in our JS file, we can add path variable:
res.render('shop', { prods:adminData.product, title: 'My Shop', path: '/' });
- Here we are trying to attach class
- To define data to be shared, we use second argument of
render()
method.res.render('shop', { prods:adminData.product, title: 'My Shop', path: '/' });
- We can use these variables in two ways:
- To display these variable on HTML, we use syntax
#{var_name}
, e.g.#{title}
- To use these variables within pug file for applying any condition or filtering requests, we can access them directly by using variable name. e.g.
a(href="/", class=(path === '/' ? 'active' : ''), name=title) Shop
- To display these variable on HTML, we use syntax
Topics Covered
-
Express handlebars are by default not directly available in Express so you have to first register handlebars:
const expresHandlebars = require('express-handlebars'); app.engine('hbs', expresHandlebars());
-
We have to use
app.engine()
method to register any custom handlebars. Namehbs
is not required you can name it anything, going forward, any files you create will have extension of name you provide here, in our case it will befile_name.hbs
-
We then set
views
andview-engine
propertyapp.set('views', path.join(rootDirectory, 'views')); app.set('view engine', 'hbs');
-
Note: value provided to
view-engine
should be the key set inapp.engine()
-
Note: value provided to
-
Handlebars uses the same HTML template and not any custom templates like PUG
-
We can output content using syntax:
{{ var_name }}
. e.g.<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta http-equiv="X-UA-Compatible" content="ie=edge"> <title>{{ title }}</title> <link rel="stylesheet" href="/css/main.css"> </head>
-
In JS file, we pass the object in the same way as we did it for PUG
res.status(404).render('404', { title: 'Page Not Found' });
-
Note: above line will result in error:
Error: ENOENT: no such file or directory, open '<working_directory>\views\layouts\main.handlebars'
. By default handlebars search for default layout (main.handlebars
) that needs to be applied to your template inviews\layouts
directory. Since we don't want to use layouts now, we can disable this by- pass
layout:false
in data objectres.status(404).render('404', { layout: false, title: 'Page Not Found' });
- Set default configuration in
app.engine()
app.engine('hbs', expressHbs({ extname: "hbs", defaultLayout: "", layoutsDir: "", }));
- pass
-
Handlebars gives us functionality to loop and filter data
<main> <h1>My Products</h1> <p>List of all the products...</p> {{#if hasProducts}} <div class="grid"> {{#each prods}} <article class="card product-item"> <header class="card__header"> <h1 class="product__title">{{ this.title }}</h1> </header> </article> {{/each}} </div> {{#else}} <h1>No Products</h1> {{/else}} {{/if}} </main>
- We can filter data with
{{#if <true | false>}} ... {{#else}} ... {{else}}{{/if}}
. These are called as block statements i.e. statements that are used to evaluate logic and not just print data. They begin with#
- We cannot pass condition to if block directly, e.g.,
{{#if array.length > 0}}
will FAIL. We are only allowed to pass true or false values. - So we have to modify our js file to include additional property that evaluates to boolean value which we can use in if block statement:
res.render('shop', { prods:adminData.product, title: 'My Shop', path: '/', layout: false, hasProducts: adminData.product.length > 0 });
- Handlebars always forces you to write logic in JS file and allows on content to be displayed in template.
- We can iterate using
{{#each <array>}} ... {{/each}}
block statement - Within block statement , handlebars provide you access to current object
this
keyword. Sothis.title
refers to title of current book in the loop.
- We can filter data with
- If you have same code repeated over multiple files, you can export the common code in a separate file and use that piece everywhere:
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta http-equiv="X-UA-Compatible" content="ie=edge"> <title>{{ title }}</title> <link rel="stylesheet" href="/css/main.css"> {{#if productCSS}} <link rel="stylesheet" href="/css/product.css"> {{/if}} {{#if formsCSS}} <link rel="stylesheet" href="/css/forms.css"> {{/if}} </head> <body> <header class="main-header"> <nav class="main-header__nav"> <ul class="main-header__item-list"> <li class="main-header__item"><a class="{{#if activeShop}}active{{/if}}" href="/">Shop</a></li> <li class="main-header__item"><a class="{{#if activeProduct}}active{{/if}}" href="/admin/add-product">Add Product</a></li> </ul> </nav> </header> <main> {{{ body }}} </main> </body> </html>
- In the above code we can output dynamic simple constants using syntax
{{ var_name }}
e.g.{{ title }}
and for complex block of code we have to add 3 curly braces{{{ body }}}
- Handlebar does not provide facility to output block content like PUG. Here we have to output content conditionally where each condition must be evaluated to true or false from JS file. e.g. CSS files will load only if corresponding property is true
{{#if productCSS}} <link rel="stylesheet" href="/css/product.css"> {{/if}} {{#if formsCSS}} <link rel="stylesheet" href="/css/forms.css"> {{/if}}
res.render( 'add-product', { title: 'Add Product', path: '/admin/add-product', activeProduct: true, formsCSS: true, productCSS: true /*, layout: false */ } );
- Also to add/remove attributes you need to use conditional statements
<a class="{{#if activeShop}}active{{/if}}" href="/">Shop</a>
- In the above code we can output dynamic simple constants using syntax
- Then you have to adjust
app.engine()
property to define default layoutapp.engine('hbs', expresHandlebars({ defaultLayout: 'main-layout', layoutsDir: '2_ExpressJS_Intro/views/layouts/', // OR path.join(rootDirectory, 'views/layouts/') extname: 'hbs' }));
- Here
defaultLayout
is the file_name of your layout file. -
layoutsDir
is the directory where this file is stored. Default isviews/layouts/
however you can configure it per your needs -
extname
is the extension name (set in app.engine). By default it search forviews/layouts/main.handlebars
- Be sure to comment
layout: false
as we are now using a default layout.
- Here
Topics Covered
-
Configuration is same as PUG i.e. set
views
andview-engine
propertyapp.set('views', path.join(rootDirectory, 'views')); app.set('view engine', 'ejs');
-
EJS like handlebars uses the same HTML template and not any custom templates like PUG
-
We can output content using syntax:
<%= var_name %>
. e.g.<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta http-equiv="X-UA-Compatible" content="ie=edge"> <title><%= title %></title> <link rel="stylesheet" href="/css/main.css"> </head>
-
In JS file, we pass the object in the same way as we did it for PUG
res.status(404).render('404', { title: 'Page Not Found' });
-
EJS gives us functionality to loop and filter data
<% if (prods.length > 0) { %> <div class="grid"> <% for (let product of prods) { %> <article class="card product-item"> <header class="card__header"> <h1 class="product__title"><%=product.title %></h1> </header> <div class="card__image"> <img src="https://cdn.pixabay.com/photo/2016/03/31/20/51/book-1296045_960_720.png" alt="A Book"> </div> <div class="card__content"> <h2 class="product__price">$19.99</h2> <p class="product__description">A very interesting book about so many even more interesting things!</p> </div> <div class="card__actions"> <button class="btn">Add to Cart</button> </div> </article> <% } %> </div> <% } %>
- We can write the block statements within
<% ... %>
. Like PUG we can write if and for loops (any loop syntax supported by JS e.g. forEach, for/in etc)
- We can write the block statements within
-
EJS is bit different when compared to previous two methods. Instead of creating one common layout structure, you could have multiple common structures which you can latter import in your main file.
<!-- head.ejs layout --> <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta http-equiv="X-UA-Compatible" content="ie=edge"> <title><%=title %></title> <link rel="stylesheet" href="/css/main.css"> <!-- navigation.ejs layout --> <body> <header class="main-header"> <nav class="main-header__nav"> <ul class="main-header__item-list"> <li class="main-header__item"><a class="<%= path === '/' ? 'active' : '' %>" href="/">Shop</a></li> <li class="main-header__item"><a class="<%= path === '/admin/add-product' ? 'active' : '' %>" href="/admin/add-product">Add Product</a></li>Product</a></li> </ul> </nav> </header>
- In the above code we can output dynamic simple constants using syntax
<%= var_name %>
e.g.<%= title %>
. - To toggle class, use same method as of PUG
- In the above code we can output dynamic simple constants using syntax
-
To import these layout you could use
<%- include('file_name') %>
syntax of EJS<%- include('includes/head') %> <link rel="stylesheet" href="/css/forms.css"> <link rel="stylesheet" href="/css/product.css"> </head> <%- include('includes/navigation') %> <main> <h1><%=title %></h1> <form class="product-form" action="/admin/product" method="POST"> <div class="form-control"> <label for="title">Title</label> <input type="text" name="title" id="title"> </div> <button type="submit">Add Product</button> </form> </main> </body> </html>
-
Note:
-
<%= var_name %>
will print the var_name value as it is, if var_name is some JavaScript code, it will not process that code, instead dump the code as it is on web page. This is because of security reasons to prevent cross site scripting. -
<%- var_name %>
will process JavaScript code and won't print anything on browser.
-