Dynamic Content and Template Engine - aakash14goplani/FullStack GitHub Wiki

Topics Covered


Sharing Data with Users

  • 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.


Templating Engine

  • 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 engine

  • 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.


Types and Installation

  • Three most used types and syntax templating engine

  • Installation (all 3 in one go...):

    npm install --save ejs pug express-handlebars@latest
    

Working with PUG

Topics Covered

Implementing

  • Set the PUG variable globally using app.set(). It takes configuration as key-value pair. Use reserved keywords views and view 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 case pug.
    • 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 defined pug as the default templating engine so it will look for .pug files.

Output Dynamic Content

  • 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 array adminData.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.
  • 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.


Managing Layouts

  • 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 and block content you can inject content dynamically
  • 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 folder layouts. .pug extension here is optional, if you don't specify one, engine will default it to pug, 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.
  • 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 be active 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: '/' });

Variable Accessibility

  • 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:
    1. To display these variable on HTML, we use syntax #{var_name}, e.g. #{title}
    2. 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

Working with Handlebars

Topics Covered

Implementing

  • 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. Name hbs 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 be file_name.hbs

  • We then set views and view-engine property

    app.set('views', path.join(rootDirectory, 'views'));
    app.set('view engine', 'hbs');
    • Note: value provided to view-engine should be the key set in app.engine()
  • Handlebars uses the same HTML template and not any custom templates like PUG


Output Dynamic Content

  • 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 in views\layouts directory. Since we don't want to use layouts now, we can disable this by

    1. pass layout:false in data object
      res.status(404).render('404', { layout: false, title: 'Page Not Found' });
    2. Set default configuration in app.engine()
       app.engine('hbs', expressHbs({
          extname: "hbs",
          defaultLayout: "",
          layoutsDir: "",
       }));
  • 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. So this.title refers to title of current book in the loop.

Managing Layouts

  • 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>
  • Then you have to adjust app.engine() property to define default layout
    app.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 is views/layouts/ however you can configure it per your needs
    • extname is the extension name (set in app.engine). By default it search for views/layouts/main.handlebars
    • Be sure to comment layout: false as we are now using a default layout.

Working with EJS

Topics Covered

Implementing

  • Configuration is same as PUG i.e. set views and view-engine property

    app.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


Output Dynamic Content

  • 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)

Managing Layouts

  • 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
  • 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.

Reference Documentation

⚠️ **GitHub.com Fallback** ⚠️