Separate template literals from logic with module exports

Viewed 530

I'm experimenting with HTML String template literals (Electron which uses NodeJS).

It works well but it looks like a mess. What I miss is the separation between HTML and logic.

Here is what I've got

module.exports = {
  snippet: (value1, value2) => {
    return `
<div>
  <h1>Hello ${value1 + ' - ' + value2}</h1>
</div>
`;
  },
};

Suggested output

What I wish I could do is like below (which does not work):

module.exports = {
  snippet: (value1, value2) => {
    return require('snippet.html')(value1, value2);
  },
};
<div>
  <h1>Hello ${value1 + ' - ' + value2}</h1>
</div>

Why it does not work

A string is flat and can't take arguments

I could get the HTML from the HTML file as a String. The problem is that it will be "flat" and I can't use the logic inside the template literals anymore.

Eval is evil and slow

I've read it's possible to run HTML as a String through eval to make it work again. I've also read that it's not good for security and that it's slow.

Other notes

  • I use nodeJS so I need to use module.exports in the js file.
  • I use nested snippets so it's important that template literals still work as it should. It should be able to take variables, functions and so on.
  • I don't mind new ES6 syntax.
  • I'm aware of Vue, React and other frameworks. I want to keep it small and without the hassle to setup a server this time.

Update - A tiny bit better, but not enough

I've now split logic from the template. Still, both are in the same file which makes the html less readable.

module.exports = {
  snippet: (value1, value2) => {
    /* Possible to put whatever logic in here */
    return html(value1, value2);
  },
};

function html(value1, value2) {
  return `
<div>
  <h1>Hello ${value1 + ' - ' + value2}</h1>
</div>
`;
}
2 Answers
  1. I'm aware of Vue, React and other frameworks. I want to keep it small and without the hassle to setup a server this time.

    Developing your own template framework is in no way a hassle free task. I don't know what do you mean by other frameworks, but as you put this phrase along with monsters like Vue and React, I must guess that you do not know about small and simple template libraries. I'm not sure about current state of affairs, but last time I checked libraries like ones from this list or this list were quite small and simple to use

  2. Or maybe you'll find this approach compelling https://jonsuh.com/blog/javascript-templating-without-a-library/

  3. You don't really have to use module.exports on latest NodeJS versions

  4. If you OK with your Update but the only issue is that template is in the same file as snippet, then what stops you from splitting files?

    // app.js
    console.log(require('./snippet.js').snippet(1,2))
    
    // snippet.js
    module.exports = {
      snippet: (value1, value2) => {
        /* Possible to put whatever logic in here */
        return require('./template.js')(value1, value2)
      },
    }
    
    // template.js
    module.exports = (value1, value2) => `
    <div>
      <h1>Hello ${value1 + ' - ' + value2}</h1>
    </div>
    `
    

As a side note:

  1. Eval is evil and slow

    From MDN:

    eval() is a dangerous function, which executes the code it's passed with the privileges of the caller. If you run eval() with a string that could be affected by a malicious party, you may end up running malicious code on the user's machine with the permissions of your webpage / extension.

    So if you only run your own code through eval() it is perfectly save... But, of course, as you implementing templates, that probably means that at some point you would want to allow user's input to the template, and that would call for troubles. So probably, yes, eval is unsafe in your case.

    I just wanted to point out that eval is extremely dangerous, but not an absolute evil in all situation.

    And it not necessary slow, either. When you say slow you must always ask yourself: "Compared to what? And in which situation?". Read the article and you will see why it can be slow. But can doesn't mean must.

  2. From the same paragraph:

    More importantly, a third-party code can see the scope in which eval() was invoked, which can lead to possible attacks in ways to which the similar Function is not susceptible.

    You may consider using Function. It is still dangerous. But less so.


On the second thougth - you should use Function, or just drop the idea of writing your template engine altogether. Because it's insecurity doesn't matter: the idea to use template literals is as insecure as Functionis:

const template = (value1, value2) => `
<div>
  <h1>Hello ${value1 + ' - ' + value2}</h1>
</div>
`
some_node.innerHTML = template("</h1><script>console.log("malicious injection")</script><h1>", "something else")

And by the way innerHTML is also insecure

A template has variables, therefore it should be a function. You have given an example:

function html(value1, value2) {
  return `
<div>
  <h1>Hello ${value1 + ' - ' + value2}</h1>
</div>
`;
}

So far so good. You wanted to separate logic from templating. You cannot 100% separate them, because your logic needs to know what templates to use and your templates need to somehow apply the results of your logic. But you can make the meeting point very thin. Consider this:

function templateHello(data) {
  return `
<div>
  <h1>Hello ${computeValues(data)}</h1>
</div>
`;
}

function computeValues(data) {
    return data.value1 + "-" + data.value2;
}

Note that is have changed:

  • the name of the function, html is not a specific name, because all your templates return html
  • the content of your h1, because you are absolutely right that business logic should not be present in the template
  • created a function which computes the inner text of h1

Let's make one step further:

class Template{
    first(data) {return `...`;}
    second(data) {return `...`;}
}

Nice, isn't it? You can of course use much more meaningful names. Don't put business logic inside the template, except function calls. In that way:

  • you will have some helper functions of pure logic
  • your templates will not contain logic, except the invocations of those helper functions
  • you will be able to call a template function as a function
  • the layer of meeting point between your templates and logic will be the function calls inside the template

You can use a template function as a subtemplate of another template function, like:

function firstHelper(templateObject, data) {
    var output = "";
    for (let item of data.items) {
        output += templateObject.second(item);
    }
    return output;
}

class Template {
    first(data) {
        return `<p>${firstHelper(this, data)}</p>`;
    }
    second(item) {
        return `<a href="${item.href}">${item.text}</a>`;
    }
}

var t = new Template();
t.first({items: [{href: "a", text: "b"}, {href: "c", text: "d"}]});

Of course you can implement helper classes, inherit template classes and override methods as you like.

Related