node.jsexpressbackendejs

Why do we use local.id in Node.js to fetch data from backend? Why not just res.id?


I am building a URL Shortner for which I am using Node.js and Express and MongodB as my database and plain html and css on the frontend. I am also using EJS as my view engine. But I am stuck up as how to display a short id (that is being generated) on the UI using EJS? And why only locals.id is a solution and how is it working??

//Code


<% if (locals.id) { %>
    <p>URL Generated: https://localhost:8000/url/<%= id%></p>
    <% } %>

This piece of code is working to display the id but I'm not able to get it.


Solution

  • TL;DR

    When rendering templates, be it on the server with res.render(...); ejs.render(); ... or in templates with include(), the variables passed to the engine are added to the locals variable. Those variables can be either called as a property of locals like locals.variable_name or by their name like variable_name. Variables defined in the template, like <% const title = "Default page" %>, can only be used by their name, as they are not added to the locals context.

    How do EJS local variables work

    EJS is a templating language, which means you can define templates, dynamically render these templates with different variables and finally send the rendered templates (which at this point is a normal string consisting of HTML markup) to the client. Let's look at an example:

    folder structure:

    |--public
    |  |--css
    |     | default.css
    |
    |--views
    |  |--partials
    |  |  | head.ejs
    |  |  | footer.ejs
    |  |
    |  |--pages
    |  |  | default.ejs
    | main.js
    

    The views folder doesn't need to contain the partials - pages structure, in fact, you can place the template files anywhere. But, generally it's good practice to have some folder to hold them, but that may vary, for example, if you work with the mvc pattern.

    head.ejs

    <!DOCTYPE html>
    <html lang="de">
    <head>
        <meta charset="UTF-8">
        <meta name="viewport" content="width=device-width, initial-scale=1.0">
        <title> <%= locals.title %> </title>
        <% if (locals.styles) { %>
            <% for(let style of styles) { %>
                <link rel="stylesheet" href="/css/<%= style %>.css">
            <% } %>
        <% } %>
    </head>
    

    footer.ejs

    </body>
    </html>
    

    default.ejs

    <% const title = "Default page" %>
    <%- include(../partials/head.ejs, {title: title, styles: ["default"]}) %>
    
    <body>
      <div class="container">
    
        <!-- This does not work -->
        <% if(locals.title) { %>
          <h1>The <%= title %></h1>
        <% } %>
        <!-- This works -->
        <% if(title) { %>
          <h1><%= title %> of GreenSaiko</h1>
        <% } %>
    
        <p>Welcome to the default page!</p>
    
        <% if(locals.message) { %>
          <p><%= message %></p>
        <% } %>
    
    <%- include("../partials/footer.ejs") %>
    

    main.js

    import express from 'express';
    import bodyParser from 'body-parser';
    import path, { dirname } from 'path'; 
    import { fileURLToPath } from 'url';
    const __dirname = dirname(fileURLToPath(import.meta.url));
    
    const APP = express();
    const PORT = 3000;
    
    // set ejs as the view engine
    APP.set('view engine', 'ejs');
    
    // tell ejs where to find your views (not necessary, but allows you
    // to write the paths to the template relative to the views folder)
    APP.set('views', path.join(__dirname, 'views'));
    
    // bodyparser tranforms the response body into a json string
    APP.use(bodyParser.json());
    // urlencoded makes sure that masked characters are translated properly
    APP.use(express.urlencoded({ extended: true }));
    // serve the public folder as static files
    APP.use(express.static(__dirname + '/public'));
    
    // request handler
    APP.get('*', (req, res) => {
        return res.render("pages/default", {message: "This is a message for you"});
    });
    
    const server = APP.listen(PORT, () => {
      console.log(`Server is running on port ${PORT}`);
    });
    

    First, let's talk about what exactly is happening when rendering templates in response to a request.

    In main.js we tell the app to do something on any route that is called: APP.get('*', (req, res) => {...}. Inside the anonymous function (the request handler) we call the render function of the response object res.render(). For the file path, we can just say "pages/default" because we told EJS where it should look for views (see APP.set("views", ...)). IMPORTANT: Do not add a slash at the beginning of “pages/default”!; the file extension, on the other hand, can be added, but isn't necessary. For the second parameter, we can pass variables to EJS which we want to use in our template. In the code here, we pass the message variable to the engine. This is an important part for your question, as the second parameter basically represents the locals variable. You can imagine it like so:

    let locals = {message: "This is a message for you"};
    APP.get('*', (req, res) => {
        return res.render("pages/default.ejs", locals);
    });
    

    If you console.log(res.render.toString()), you can see that the second parameter is passed along the function as the option object and in APP.render(...) is merged with the locals. This explains why the locals variable holds the values we passed in the res.render(...) function.

    Once the render function is called, the engine checks through the specified file if it can find code it is responsible for and runs it. In our case, it looks for any EJS tags and runs the javascript inside. If we look at the default.ejs template, there are plenty of things to do for the engine. First, let's look at what we're doing with the message variable we passed to the render function:

    Before we use the variable, we check that it isn't undefined, null or doesn't hold any value. If the variable has a valid value, we then display it in a <p> tag. As you can see in that snippet and as we have established before, the locals variable holds the message variable, or rather the message property because the typeof locals is object. Crucial at this point is that in the condition of the if statement and in the content of the <p> tag we can either use locals.message or just message. So both of these snippets are correct:

    <% if(locals.message) { %>
      <p><%= locals.message %></p>
    <% } %>
    
    <% if(message) { %>
      <p><%= message %></p>
    <% } %>
    

    But something else interesting can be observed if we look at the title variable we define in the template. That variable can be used by its name, as you can see where we include head.ejs. But if you look below in the body, there are two parts where we use the title variable. In the first part where we call it as locals.title will not work, while the second one, where we call it as just title does work. This is simply because variables defined in EJS tags, so where the render engine runs code, are not added to the locals object.

    On the other hand, if we take a look at the line where we include the head template, we pass ["default"] as the styles property. In head.ejs then, we use this property again in a similar fashion as the others, with an if statement. Here we can use locals.styles because it is added to the locals context when rendering head.ejs. It's basically the same as saying res.render("partials/head.ejs", {title: title, styles: ["default"]}), but instead of rendering the template and sending it to the client, it's added to the DOM after rendering.

    Noteworthy also, is that the properties of locals can also be manually set like:

    APP.get('*', (req, res) => {
        res.locals.test = "This is a test";
        res.render("pages/default.ejs", {message: "This is a message for you"});
    });
    

    Which can, of course, be called in the template by locals.variable_name and variable_name, so here it would be locals.test or test.

    So in conclusion, the locals variable is filled with the properties we pass to the engine when rendering a template and represents variables available to the current view. Those properties can then be called by either locals.[property_name] or just property_name. Variables defined in EJS tags, on the other hand, are not added to the locals context and must be called by their variable_name. So you can use locals.[property_name] for variables passed to the engine, but generally, you will simply call them by their name. Meaning, personally, I would write the if condition in your example just as <% if(id) { %>

    Edit

    Because I recently ran into some issues with dynamicaly passing data to partials and checking if they are defined or not, here's a little tip. Instead of checking if window has the variable name as an attribute <% if(window.myVar) {%>, which won't work in EJS scripts because there is no window object during rendering, check the locals object if(locals.myVar) {.