2
\$\begingroup\$

There are many template engines but my question is: Why do they still exist? Can we not achieve the same job with readily available JavaScript Template Literals?

So influenced from this blogpost, I have come up with something as follows. Do you think this is reasonable in daily JavaScript?

The following code is pure JavaScript. Lets start with a proper data.

var data = { labels: ['ID','Name']
           , rows  : [ {"id": "1", 'name': "richard"}
                     , {"id": "2", 'name': "santos"}
                     ]
           };

Now let's define a tableTemplate function which takes the data and gives us an HTML table.

function tableTemplate(data){
  return `
    <table>
      <tr> ${data.labels.map(label => html`
             <th>&${label}</th>`).join("")}
      </tr>${data.rows.map(row => html`
      <tr>
             <td>&${row.id}</td>
             <td>&${row.name}</td>
      </tr>`).join("")}
    </table>`;
}

So what is this html` thingy and also that &$?

  • First of all html` is just a generic tag function that we define only once and without modifying it, we can use it for all of our template functions.
  • &$ on the other hand is simple. & is just a simple string character and the second one is the escaping $ character for variables in template strings such as ${x}. We can use a tag function to validate our HTML code by escaping invalid HTML characters like &, >, <, ", ' or `. The version I give below looks a little convoluted but it's really simple and very useful. It basically escapes invalid HTML characters and provides a "raw" version of the template string.

Please follow above links for further information. Here it is:

function html(lits,...vars){
  var esc = { "&": "&amp;"
            , ">": "&gt;"
            , "<": "&lt;"
            , '"': "&quot;"
            , "'": "&#39;"
            , "`": "&#96;"
            };
  return lits.raw
             .reduce( (res,lit,i) => (res += lit.endsWith("&") ? lit.slice(0,-1) + [...vars[i].toString()].reduce((s,c) => s += esc[c] || c, "")
                                                               : lit + (vars[i] || ""))
                    , ""
                    );
                                                 
}

Now, it's time to put everything together for a working sample. Let's also add <thead> and <tbody> tags and some CSS this time.

function html(lits,...vars){
  var esc = { "&": "&amp;"
            , ">": "&gt;"
            , "<": "&lt;"
            , '"': "&quot;"
            , "'": "&#39;"
            , "`": "&#96;"
            };
  return lits.raw
             .reduce( (res,lit,i) => (res += lit.endsWith("&") ? lit.slice(0,-1) + [...vars[i].toString()].reduce((s,c) => s += esc[c] || c, "")
                                                               : lit + (vars[i] || ""))
                    , ""
                    );
                                                 
}

function tableTemplate(data){
  return `
    <table>
      <thead>
        <tr>${data.labels.map(label => html`
              <th>&${label}</th>`).join("")}
        </tr>
      </thead>
      <tbody>${data.rows.map(row => html`
        <tr>
              <td>&${row.id}</td>
              <td>&${row.name}</td>
        </tr>`).join("")}
      </tbody>
    </table>`;
}

var data = { labels: ['ID','Name']
           , rows  : [ {"id": 1, 'name': "richard"}
                     , {"id": 2, 'name': "santos"}
                     , {"id": 3, 'name': "<ömer & `x`>"}
                     ]
           };
document.write(tableTemplate(data));
table {
  background-color: LightPink;
  border          : 0.2em solid #00493E;
  border-radius   : 0.4em;
  padding         : 0.2em
  }
thead {
  background-color: #00493E;
  color           : #E6FFB6
  }
tbody {
  background-color: #E6FFB6;
  color           : #00493E
  }

Obviously you can now insert <style> tag or class="whatever" attribute into your quasi-HTML template literal in the tableTemplate function just the way you would normally do in an HTML text file.

Also please note that if you console.log the resulting HTML text string, thanks to the raw conversion, it would beautifully display (according to your indenting in the tableTemplate function) just like below:

<table>
  <thead>
    <tr>
           <th>ID</th>
           <th>Name</th>
    </tr>
  </thead>
  <tbody>
    <tr>
           <td>1</td>
           <td>richard</td>
    </tr>
    <tr>
           <td>2</td>
           <td>santos</td>
    </tr>
    <tr>
           <td>317</td>
           <td>&lt;ömer &amp; &#39;x&#39;&gt;</td>
    </tr>
  </tbody>
</table>
\$\endgroup\$
5
  • \$\begingroup\$ What does this do? The table looks nothing like data? \$\endgroup\$
    – Blindman67
    Nov 21, 2021 at 20:07
  • \$\begingroup\$ @Blindman67 Wow i am dealing with a very interesting site related bug here. I can swear that the tableTemplate function in the working sandbox is exactly the same one as shown in the second snippet. When i try to edit it just turns back into how i had written it and works just fine until i save it and it shows again that silly hash code <th>{cbe533db-4533-4fed-934e-f25c95923264}{row.name}</td>. What's happening..? Perhaps the $$ is colliding with some parsing library behind the curtain..? \$\endgroup\$
    – Redu
    Nov 21, 2021 at 20:59
  • \$\begingroup\$ @Blindman67 Replaced $$ with &$ in my code and the sandbox bug is resolved. So.. i guess somebody has to fix something. From this bug only i can conclude that the Stackoverflow and Codereview sandboxes are of different sources or versions of the same source since my code's original form works just fine @ SO. \$\endgroup\$
    – Redu
    Nov 21, 2021 at 21:08
  • \$\begingroup\$ I just tried adding your code with $$ and it worked fine,. Most likely something your end (extension or the like) inserting the GUID \$\endgroup\$
    – Blindman67
    Nov 22, 2021 at 3:26
  • \$\begingroup\$ @Blindman67 I don't think so, as you mention in your first comment it doesn't work fine at your end too. The thing is, when you edit the question and scroll down and click on edit the above snippet link and replace &$s with $$s in the sandbox everything still works just fine but only after you save it with $$ then things show up awry in the question. \$\endgroup\$
    – Redu
    Nov 23, 2021 at 9:30

2 Answers 2

2
\$\begingroup\$

There are many template engines but my question is: Why do they still exist? Can we not achieve the same job with readily available JavaScript Template Literals?

Sure, that seems like it would work ok for smaller projects, especially if you remember to keep to the string encoding guidelines to prevent XSS attacks. From my quick glance over your function, I don't see any security issues with how you're encoding strings.

Many templating libraries were popularized before template literals existed. Had template literals existed earlier, I wouldn't be surprised if these libraries would have been made to support them from the start, though, perhaps they continue to not support them because they like keeping the HTML outside of your business logic.


I'll go ahead and point out some potential design considerations you can think about.

It's pretty easy to accidentally forget to precede an interpolated value with an "&" character, which could cause the interface to be buggy, or worse, open up XSS issues. One option would be to make string-escaping the default behavior and require putting a special character before an interpolated value to explicitly state that you trust the interpolated content, and are ok with it getting inserted into the template literal untouched.

Another option would be to have the interpolated values take object literals, which explains what kind of encoding you want to be performed. This could help make things more readable, especially if you want to support other forms of encoding in the future. For example:

html`
  <div id="${{ attr: 'All attr-unsafe characters will be encoded' }}">
    <p>${{ content: 'All content-unsafe characters will be encoded' }}</p>
    ${{ raw: 'This string will be added, unchanged' }}
  </div>
`

I'm not saying you should or should not switch to one of these options, just pointing out some alternative options to consider.

One more option (this one's my favorite), is to not ever use template literals. For small projects, I like to use this very simple helper function:

function el(tagName, attrs = {}, children = []) {
  const newElement = document.createElement(tagName)
  for (const [key, value] of Object.entries(attrs)) {
    newElement.setAttribute(key, value)
  }
  newElement.append(...children)
  return newElement
}

This allows you to use function calls to construct your HTML content. Because it's using native browser APIs, you won't have to worry about hand-writing escaping logic either, since the browser will automatically escape your data (even still, don't do something stupid like putting untrusted data into a script tag).

function el(tagName, attrs = {}, children = []) {
  const newElement = document.createElement(tagName)
  for (const [key, value] of Object.entries(attrs)) {
    newElement.setAttribute(key, value)
  }
  newElement.append(...children)
  return newElement
}

const renderTable = data =>
  el('table', { id: 'my-table' }, [
    el('thead', {}, [
      el('tr', {}, [
        ...data.labels.map(renderTableHeader),
      ]),
    ]),
    el('tbody', {}, [
      ...data.rows.map(renderRow)
    ]),
  ]);

const renderTableHeader = label =>
  el('th', {}, [label])
 
const renderRow = ({ id, name }) =>
  el('tr', {}, [
    el('td', {}, [id]),
    el('td', {}, [name]),
  ])

var data = { labels: ['ID','Name']
           , rows  : [ {"id": 1, 'name': "richard"}
                     , {"id": 2, 'name': "santos"}
                     , {"id": 3, 'name': "<ömer & `x`>"}
                     ]
           };

document.getElementById('main')
  .appendChild(renderTable(data))
#my-table {
  background-color: LightPink;
  border          : 0.2em solid #00493E;
  border-radius   : 0.4em;
  padding         : 0.2em
  }
thead {
  background-color: #00493E;
  color           : #E6FFB6
  }
tbody {
  background-color: #E6FFB6;
  color           : #00493E
  }
<div id="main"></div>

\$\endgroup\$
0
2
\$\begingroup\$

A short review;

Do you think this is reasonable in daily JavaScript?

Probably not, for a few reasons

  • You need a function per record type since this wires metadata and styling so tightly

  • There is a ton of HTML tooling out there, none of it will work with embedded html

  • The html function you provide is compacter but less readable than the one in the blog

  • The escaping code seems bare, I prefer to use

      function escapeHtml(html){
        var text = document.createTextNode(html);
        var p = document.createElement('p');
        p.appendChild(text);
        return p.innerHTML;
      }
    
  • If I had to write a templating routine myself I would have gone with

      <table>
        <head>
          <tr id="id"></tr>
          <tr id="name"></tr>
        </thead>
        <body>
          <tr>
                <td></td>
                <td></td>
          </tr>
        </tbody>
      </table>
    

and call something like

    tableTemplate(template, {"id":"ID","name":"Name"}, rows);

Which would have mapped the row labels to the proper id, and then put the data in the proper columns based off the location of the corresponding header id.

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge that you have read and understand our privacy policy and code of conduct.

Not the answer you're looking for? Browse other questions tagged or ask your own question.