Hacker News new | past | comments | ask | show | jobs | submit login
Templating in JavaScript, from Zero Dependencies on Up (2021) (jim-nielsen.com)
26 points by tosh 6 months ago | hide | past | favorite | 27 comments



Using this pattern has been a gift. It is really freeing to write templates without a framework and worrying about SSR.

Years back I dove completely in on Svelte and even wrote my own SSG that helped kick off the Islands/partial hydration crazy... Elder.js. It was fun, but burnt me out of open source.

Today, I only pull for a template language and deal with SSR/Hydration for pages that really need it.

I want JS projects that don't break 6 months after the last build. The way you get that is by not adding more dependencies and not chasing the latest craze.


> I want JS projects that don't break 6 months after the last build

I was somewhat recently trying to get a 3 year old JS project working. I felt like I was alone in an Egyptian pyramid wondering if I had missed the funeral.


> I want JS projects that don't break 6 months after the last build. The way you get that is by not adding more dependencies and not chasing the latest craze.

I've settled on ``document.createElement()``: it's a bit rough visually, but that's pretty much the only downside (I've found the time gained with templates to be negligible in front of the overall development time).

A big bonus of dealing with genuine objects is that there's room to extend them to e.g. store additional state/data/handlers/etc., which might otherwise be cumbersome to be set via attributes & cie.


An even bigger and underappreciated bonus of dealing with objects is that you're working at the correct abstraction layer. Using templates that glue string together is doing the Wrong Thing, and this is how you get XSS (or outside of HTML, injection class vulnerabilities specific to what you're writing, e.g. SQL injection when gluing strings into queries).


By being imperative, you describe how to get to the state you desire. By being declarative, you split the concerns of “how it should be” and “how to get there”, which doesn’t come without downsides (abstractions are leaky) but does have its benefits: the latter logic you’d maintain in one place, and the former is usually easier to reason about.


I'm not talking about imperative vs. declarative, but about working at the level of language you're dealing with. Using string concatenation for building HTML or SQL is like doing arithmetic like "123" + "456" = "123456" -- you're using the wrong addition operator.


Describing the state by design implies not working at the same level as getting to that state, though—I’m not sure if there is some subtlety I’m missing—and to me there seem to be obvious benefits (and downsides, as mentioned above) to not explicitly manipulating DOM where I want to just describe how a page should be structured.


Perhaps this can help to fix the ideas: here's how you could create the HTML by manipulating the DOM (the code indentation mimics the HTML indentation):

   let p = document.createElement("span");
  
    let b = document.createElement("button");
    b.innerText = btnText;
    if (btnClass) b.classList.add(btnClass);
  
    let m = document.createElement("span");
    m.style.display = "none";
    m.classList.add(modalClass);
  
     let q = document.createElement("span");
     q.classList.add(modalContentClass);
  
      let c = document.createElement("button");
      c.innerText = "×";
      c.classList.add(modalBtnCloseClass);
  
      // r is an "external" HTMLElement
      r.classList.add(modalContentClass);
  
     q.append(c, r);
  
    m.appendChild(q);
  
   p.append(b, m);
It's not that different from writing the HTML (with or without a template). But IMO the great thing here is that, at the end of that chunk of code, you've got pointers to every node of interest. You can then register handlers, target arbitrary nodes in such handlers, without having to navigate through the HTML (e.g. by setting ``id`` or other attributes to help identify them, or by relying on a static HTML structure).

And as the parent points, this should prevent some amount of unexpected HTML injection.

There's still a separation between the view and the page structure.


I know how to manipulate DOM. You are not invalidating my point. This way involves a lot more mental overhead any time you need to define a structure for a page, compared to a declarative way of writing HTML as a string and delegating DOM manipulation to separate imperative logic (or indeed browser’s native parser).


> This way involves a lot more mental overhead any time you need to define a structure for a page,

How so?

The "imperative logic" of the previous example is parametrized by a few node pointers. Assuming I understand your approach correctly, your separate imperative logic would still need to be parametrized by some node pointers as well.

Except because you wouldn't have direct access to them, you'd need to use class names or IDs to retrieve them. Hence you would need an extra layer to join the two parts that you've separated, which in my opinion is more mental overhead: the management of those attribute comes at a cost (e.g. naming conflict, more class names to remember, confusion between class names).

Perhaps a more concrete example (bits of code) of your approach might help make your point clearer.


Why do you assume there is a requirement to access nodes directly, and why would that be a typical requirement?

(There are ways to achieve that more declaratively than by building the entire DOM imperatively, JSX and React’s refs is one approach, but I wonder why you assume direct access is always needed in the first place.)


> Why do you assume there is a requirement to access nodes directly, and why would that be a typical requirement?

I haven't made a case for it to be a requirement, but rather for direct access to be 1) trivial to achieve 2) more convenient than all of the (Vanilla JS) alternatives that I know of (attributes-based solutions).

> JSX and React’s refs

From a quick glance, I'm not sure this brings anything more than keeping a handful of node pointers around. But it should, at least in some ways, be better that attribute-based solutions.


Well, I have outlined why declarativity is good, so as far as I’m concerned the choice to go imperative and abandon the benefits of declarativity in order to make access to nodes trivial should probably be accompanied by understanding of why that access would be needed in the first place.


To perform any action on the DOM, you need access to the nodes: registering handlers, manually triggering events, setting/getting values, rebuilding them, etc.

I'd encourage you to toy with a concrete example, in case you (or I) are missing a subtle point; the previous piece of code is the "view" for a modal "component" which hosts an external HTMLElement (r). See how you'd implement it with both approaches.

With both, the "view" and the logic are still separated. The view is merely encoded differently.


> To perform any action on the DOM, you need access to the nodes

Not really. With declarative approach you can just give HTML to the browser (or some templater) and get your page completely laid out without having any access to any nodes.

In fact, declarative approach does also allow to connect handlers with ease (onclick and similar attributes are very much a thing).

Using onclick handlers doesn’t scale well if we are talking about Web apps with complex GUIs, but then the naive imperative approach of createElement scales potentially even worse. The approaches that do scale tend to favour declarative approach with isolated imperative logic (such as in event handlers).


> In fact, declarative approach does also allow to connect handlers with ease (onclick and similar attributes are very much a thing).

Yup, but it's more limited: consider the modal: you can't — without relying on attributes, or worse, the page's structure — hide the modal when clicking on the little cross, in an onclick attribute.

> The approaches that do scale tend to favour declarative approach with isolated imperative logic

I think it's key regardless of the approach: you want to break down your pages in pieces (e.g. "components"), have them eventually communicate with each others, while keeping those communications as local as possible.

Very much in the same way we design programs over small units (procedure, function, object, etc.).


Modals are generally involved in a full-blown Web app. For an average post or article, you don’t need node handles, pure declarative HTML is enough. At the same time, implementing a full-blown app imperatively directly with createElement would result in an unmaintainable mess. In other words, there seem to be very few use cases where such approach is justified.


> For an average post or article, you don’t need node handles

It goes without saying...

> At the same time, implementing a full-blown app imperatively directly with createElement would result in an unmaintainable mess

I disagree, and I've built non-trivial UIs with createElement; a key is to have the discipline to keep the "components" well-isolated.


For those looking for something more robust... this is where I'd look: https://github.com/kitajs/html.

Also Honojs (hono.dev) has a built in HTML function which is quite good and I've used extensively.


kitajs already being a fork of https://github.com/nicojs/typed-html . What makes this one more robust or stable then?


The fact that XSS isn't covered (or even mentioned!) in an article about building your own HTML templating system is a major, and dangerous, oversight


Nice article; I do this often with small server-side rendered Deno projects. I haven't felt the need to upgrade to a "proper" templating library yet.

I noticed the article doesn't mention escaping HTML, which you probably should do if you're expecting user input. Fortunately, such a utility is included in Deno's standard library: https://jsr.io/@std/html/doc/~/escape

Similarly, for XSS, the `xss` package on npm works great for Deno.


It all depends on the requirements and quality attributes.

For example, a plain string template will not help with selective DOM updates; that’s why we end with a tree DSL like JSX.

For many different reasons, you’ll like to have lazy component rendering. That’s why using `Component(props)` is not the same as `factory(Component, props)`.

I’m not saying that the author is wrong or right. If you only want a server-side template or one-off template for a simple page, then using JS/TS string templates is better than relying on a DSL like Handlebars or Pug. The client-side story is a bit different, and that’s why there are so many frameworks for it.


React-dom/server APIs are great for this. Writing jsx (as opposed to template literals as in OP) is generally a nicer DX, and you can keep the setup fairly minimal.


Isn't there a <template> and <slot> mechanism in HTML you can use now?


Last time I checked, you still have to manually clone templates, query slots to fill and imperatively manipulate DOM. Did this recently and found the process very annoying compared to frontend framework template experience. Compared to banging out template literal strings, sounds like too much hassle, especially in Node.js, where there's no DOM by default, so you'd have to import a DOM library.


good article. the point would have been more salient if the jimniels/html micro dependency had been inlined.




Consider applying for YC's Spring batch! Applications are open till Feb 11.

Guidelines | FAQ | Lists | API | Security | Legal | Apply to YC | Contact

Search: