Day 69: EJS is Basically PHP for JavaScript?

Paused on the final project in the web security section of the Node.js course from The Odin Project. I Took some time to write notes on the ejs package since I am using it for this project. I previously was using the morgan library for building template views. I was not the biggest fan of the python-like indentation errors. It wasn’t bad I did not know how to change the automatic indentation settings in vim to prevent it from using tabs for the auto indentations. Since I started using vim I started using spaces more than tabs. So ejs seems to be a better options.

I was previously using ejs at the surface of surface levels LOL. I would only reference the docs to look at some simple example code that would get me what I wanted. Now I have a better understanding of how to do a few core things with the package. For example, I wanted to better understand how to add layouts to my views for a consistent header block, etc. Going over the “Get Started” section of the docs explained that pretty well.

Overall the package seems like the JavaScript equivalent to PHP. It is actually kind of funny that I went all this way to learn about backend development with JavaScript and end up using the equivalent to PHP. Obviously there is a big difference with how the languages actually operate under the hood but still, at the surface level it does look similar.

TLDR;

Okay, so here are the highlights of what I did:

  • Backend -> Paused working on the Members Only Project from the web security section. I took the day to write out some notes on the ejs package. Included my rough notes before. The notes are entirely based on the ejs documentation. I have only rearranged some things and added a few changes at this point.

Rough Notes on ejs Package

What is ejs ?

EJS is a simple templating language that lets you generate HTML markup with plain JavaScript. It is comparable to the morgan package.

What Does ejs Do?

Allows us to provide data to a template string that will ultimately produce dynamic HTML code. The idea being that HTML is static by design but by using a templating language we can use base templates that can still produce dynamic content by passing in different data depending on what we want.

A common example of this functionality can be found in php. When served to the client it appears as just HTML but in reality the HTML was pre-processed with our desired data before being sent to the client as what seems to be a static HTML file.

Long story short. ejs seems to be a JavaScript version of what php does.

How Do You Use ejs Do?

There are three ways to use ejs with a descending order of usage (I think):

  1. Pass ejs a template string and some data in a JavaScript file to produce HTML code. (Use it with Node.js)
  2. Feed it a template file and a data file, and specify an output file on the CLI to fill that file with formatted HTML code. (CLI)
  3. Download a browser build from the latest release, and use it in an HTML script tag. (In the browser)

1. Using ejs with Node.js

Install ejs from the CLI with:

$ npm install ejs

Pass ejs a template string and some data. BOOM, you’ve got some HTML.

let ejs = require("ejs");
let people = ["geddy", "neil", "alex"];
let html = ejs.render('<%= people.join(", "); %>', { people: people });

2. Using ejs with the CLI

Feed it a template file and a data file, and specify an output file.

ejs ./template_file.ejs -f data_file.json -o ./output.html

EJS ships with a full-featured command-line interface. Options are similar to those used in JavaScript code:

Option:                            Description:
-------                            ------------
-o / --output-file FILE            Write the rendered output to FILE rather than stdout.
-f / --data-file FILE              Must be JSON-formatted. Use parsed input from FILE as data for rendering.
-i / --data-input STRING           Must be JSON-formatted and URI-encoded. Use parsed input from STRING as data for rendering.
-m / --delimiter CHARACTER         Use CHARACTER with angle brackets for open/close (defaults to %).
-p / --open-delimiter CHARACTER    Use CHARACTER instead of left angle bracket to open.
-c / --close-delimiter CHARACTER   Use CHARACTER instead of right angle bracket to close.
-s / --strict                      When set to `true`, generated function is in strict mode
-n / --no-with                     Use 'locals' object for vars rather than using `with` (implies --strict).
-l / --locals-name                 Name to use for the object storing local variables when not using `with`.
-w / --rm-whitespace               Remove all safe-to-remove whitespace, including leading and trailing whitespace.
-d / --debug                       Outputs generated function body
-h / --help                        Display this help message.
-V/v / --version                   Display the EJS version.

Some examples of use:

$ ejs -p [ -c ] ./template_file.ejs -o ./output.html

$ ejs ./test/fixtures/user.ejs name=Lerxst

$ ejs -n -l _ ./some_template.ejs -f ./data_file.json

3. Use ejs In The Browser

Download a browser build from the latest release, and use it in a script tag.

<script src="ejs.js"></script>
<script>
  let people = ["geddy", "neil", "alex"];
  let html = ejs.render('<%= people.join(", "); %>', { people: people });
</script>

Go to the latest release, download ./ejs.js or ./ejs.min.js. Alternately, you can compile it yourself by cloning the repository and running jake build (or $(npm bin)/jake build if jake is not installed globally).

Include one of these files on your page, and ejs should be available globally.

Example:

<div id="output"></div>
<script src="ejs.min.js"></script>
<script>
  let people = ['geddy', 'neil', 'alex'],
      html = ejs.render('<%= people.join(", "); %>', {people: people});
  // With jQuery:
  $('#output').html(html);
  // Vanilla JS:
  document.getElementById('output').innerHTML = html;
</script>

Caveats

Most of EJS will work as expected; however, there are a few things to note:

Obviously, since you do not have access to the filesystem, ejs.renderFile won’t work. For the same reason, includes do not work unless you use an include callback. Here is an example: let str = “Hello <%= include(‘file’, {person: ‘John’}); %>”, fn = ejs.compile(str, {client: true});

fn(data, null, function(path, d){ // include callback // path -> ‘file’ // d -> {person: ‘John’} // Put your code here // Return the contents of file as a string }); // returns rendered string Using EJS with Express This GitHub Wiki page explains various ways of passing EJS options to Express.

Examples

This is how templating would be done in an ejs template file.

<% if (user) { %>
  <h2><%= user.name %></h2>
<% } %>

The data that contained the user value would need to be provided to the template for the if statement to pass.

let template = ejs.compile(str, options);
template(data);

// => Rendered HTML string

ejs.render(str, data, options);
// => Rendered HTML string

ejs.renderFile(filename, data, options, function (err, str) {
  // str => Rendered HTML string
});

Options:

Name:              Description:
-----              ------------
cache              Compiled functions are cached, requires filename

filename           Used by cache to key caches, and for includes

root               Set project root for includes with an absolute path (e.g, /file.ejs).
                   Can be array to try to resolve include from multiple directories.

views              An array of paths to use when resolving includes with relative paths.

context            Function execution context

compileDebug       When false no debug instrumentation is compiled

client             Returns standalone compiled function

delimiter          Character to use for inner delimiter, by default '%'

openDelimiter      Character to use for opening delimiter, by default '<'

closeDelimiter     Character to use for closing delimiter, by default '>'

debug              Outputs generated function body

strict             When set to `true`, generated function is in strict mode

_with              Whether or not to use with() {} constructs. If false then the locals
                   will be stored in the locals object. (Implies `--strict`)

localsName         Name to use for the object storing local variables when not using with Defaults to locals

rmWhitespace       Remove all safe-to-remove whitespace, including leading and trailing whitespace.
                   It also enables a safer version of -%> line slurping for all scriptlet tags (it does not strip
                   new lines of tags in the middle of a line).

escape             The escaping function used with <%= construct. It is used in rendering and is .toString()ed in
                   the generation of client functions. (By default escapes XML).

outputFunctionName Set to a string (e.g., 'echo' or 'print') for a function to print output inside scriptlet tags.

async              When true, EJS will use an async function for rendering. (Depends on async/await support in the JS runtime.

includer           Custom function to handle EJS includes, receives (originalPath, parsedPath) parameters, where
                   originalPath is the path in include as-is and parsedPath is the previously resolved path.
                   Should return an object { filename, template }, you may return only one of the properties,
		   where filename is the final parsed path and template is the included content.

This project uses JSDoc. For the full public API documentation, clone the repository and run jake doc. This will run JSDoc with the proper options and output the documentation to out/. If you want the both the public & private API docs, run jake devdoc instead.

Tags

Tag:        Description:
----        ------------
<%          'Scriptlet' tag, for control-flow, no output

<%_         ‘Whitespace Slurping’ Scriptlet tag, strips all whitespace before it

<%=         Outputs the value into the template (HTML escaped)

<%-         Outputs the unescaped value into the template

<%#         Comment tag, no execution, no output

<%%         Outputs a literal '<%'

%>          Plain ending tag

-%>         Trim-mode ('newline slurp') tag, trims following newline

_%>         ‘Whitespace Slurping’ ending tag, removes all whitespace after it

Includes

Includes are relative to the template with the include call. (This requires the ‘filename’ option.) For example if you have "./views/users.ejs" and "./views/user/show.ejs" you would use <%- include('user/show'); %>.

You’ll likely want to use the raw output tag (<%-) with your include to avoid double-escaping the HTML output.

<ul>
  <% users.forEach(function(user){ %>
    <%- include('user/show', {user: user}); %>
  <% }); %>
</ul>

Custom delimiters

Custom delimiters can be applied on a per-template basis, or globally by setting the delimeter property on the options object:

let ejs = require('ejs'),
    users = ['geddy', 'neil', 'alex'];

// Just one template
ejs.render('<?= users.join(" | "); ?>', {users: users},
    {delimiter: '?'});
// => 'geddy | neil | alex'

// Or globally
ejs.delimiter = '$';
ejs.render('<$= users.join(" | "); $>', {users: users});
// => 'geddy | neil | alex'

Caching

EJS ships with a basic in-process cache for caching the intermediate JavaScript functions used to render templates. It’s easy to plug in LRU caching using Node’s lru-cache library:

let ejs = require('ejs'),
    LRU = require('lru-cache');
ejs.cache = LRU(100); // LRU cache with 100-item limit

If you want to clear the EJS cache, call ejs.clearCache. If you’re using the LRU cache and need a different limit, simple reset ejs.cache to a new instance of the LRU.

Custom file loader

The default file loader is fs.readFileSync, if you want to customize it, you can set ejs.fileLoader.

let ejs = require('ejs');
let myFileLoader = function (filePath) {
  return 'myFileLoader: ' + fs.readFileSync(filePath);
};

ejs.fileLoader = myFileLoader;

With this feature, you can preprocess the template before reading it.

Layouts

EJS does not specifically support blocks, but layouts can be implemented by including headers and footers, like so:

<%- include('header'); -%>
<h1>
  Title
</h1>
<p>
  My page
</p>
<%- include('footer'); -%>

References:


Goal For Round 8 of the #100DaysofCode Challenge

This is my eighth round of the “#100daysofcode” challenge. I will be continuing my work from round five, six, and seven into round eight. I was working through the book “Cracking the Coding Interview” by Gayle Laakmann McDowell. My goal was to become more familiar with algorithms and data structures. This goal was derived from my goal to better understand operating systems and key programs that I use in the terminal regularly e.g. Git. This goal was in turn derived from my desire to better understand the fundamental tools used for coding outside of popular GUIs. This in turn was derived from my desire to be a better back-end developer.

I am currently putting a pause on the algorithm work to build some backend/full stack projects. I primarily want to improve my skills with the back-end from an implementation perspective. I have improved tremendously in terminal and CLI skills but I lost focus due to how abstract the algorithm concepts got. I wanted to work on things that were more tangible until I can get to a position where I could directly benefit from improving my algorithm skills and theoretical knowledge. So that’s the focus right now. Build my backend skills and prove my full stack capabilities by building some dope projects.

Again, I still have no idea if my path is correct but I am walking down this road anyways. Worst case scenario I learn a whole bunch of stuff that will help me out on my own personal projects. Best case scenario I actually become one of those unicorn developers that go on to start a billion dollar company… You never know LOL.