More erognomic createElement function

I like to hang around the code review stack exchange. One of the most common issues I see with beginner Javascript developers is their constant reliance on using .innerHTML, without properly sanitizing the data. Sometimes more seasoned developers use it too, despite the risk, simply because it’s much simpler to use than alternatives. In some cases this can just lead to potential bugs, but in the worst scenarios, these developers introduce XSS issues into their code.

I can understand the desire to use .innerHTML. The alternative is very unfriendly and cumbersome to use. Compare these two examples of creating a very small chunk of HTML (and imagine how much more verbose it gets as the amount of generated HTML grows, or how unreadable it gets when a little nesting gets involved)

Without .innerHTML:

const heading = document.createElement('a')
heading.classList.add('heading')
heading.href = './over-there.html'

const img = document.createElement('img')
img.classList.add('img')
img.src = './my-image.png'
img.alt = 'some alt text'
heading.appendChild(img)

const text = document.createElement('h1')
text.classList.add('heading-text')
text.innerText = 'Information'
heading.appendChild(text)

const body = document.createElement('p')
body.innerText = 'Lorum Ipsum'
heading.appendChild(body)

document.getElementById('main').appendChild(heading)

With .innerHTML

document.getElementById('main').innerHTML = `
  <a class="heading" href="./over-there.html">
    <img class="img" src="./my-image.png" alt="some alt text"/>
    <h1 class="heading-text">Information</h1>
    <p>Lorum Ipsum</p>
  </a>
`

What if we provided a more ergonomic create-element function that tried to compete with how user-friendly .innerHTML is? This could help drive people away from relying on .innerHTML so much, and guide them to better and safer alternatives. For example, we could provide an Element.create() function that was defined somewhat like this:

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

And here’s a usage example

const heading = Element.create('a', { class: 'heading', href: './over-there.html' }, [
  Element.create('img', { class: 'img', src: './my-image.png', alt: 'some alt text' }),
  Element.create('h1', { class: 'heading-text' }, ['Information']),
  Element.create('p', {}, ['Lorum Ipsum']),
])

document.getElementById('main').appendChild(heading)

Yes, this isn’t as concise as .innerHTML, but it’s certainly orders of magnitude better than the current DOM API functions we’ve got, and will hopefully be enticing enough to lead people away from .innerHTML.

And yes, I acknowledge that it’s not that hard to just define this Element.create() in userland code, but newer Javascript programmers aren’t going to think to do that.

4 Likes

Yes. Over the years, I also added such a similar function to most of my front projects.

Without surprise, it turns out that one the core pillar of React, the createElement function (the one which JSX desugars to), shares almost the same concept: https://reactjs.org/docs/react-api.html#createelement

I’m extremely in favor of such a method, and have written this function numerous times when dealing with more involved web pages where a web framework is too much, but static HTML is…well…too static.

I would like to request a couple changes for usability:

  1. The tagName should admit a subset of simple CSS selectors, with the conditions that 1. a tag name must be specified, 2. universal selectors and pseudoclasses must not be present, and 3. attribute selectors can only use =, not ~=, *=, or similar.
  2. !f attrs is either a node or non-object, it should instead be treated as the first child.

That way, I can just do Element.create("div.app#root", "child") or Element.create("input[type=number][name=count]"). There is precedent for both of these, in the form of Mithril and react-hyperscript, and I find myself frequently a least adding class and ID support if nothing else to my helpers.

A pattern I’ve seen that can kinda fix this is

const heading = Object.assign(document.createElement('a'), {
  className: 'heading',
  href: './over-there.html'
})
heading.append(
  Object.assign(document.createElement('img', {
    className: 'img',
    src: './my-image.png',
    alt: 'some alt text'
  }),
  Object.assign(document.createElement('h1', {
    className: 'heading-text'
    textContent: 'Information'
  }),
  Object.assign(document.createElement('p', {
    textContent: "Lorem Ipsum"
  }),
])
document.getElementById('main').appendChild(heading)

See also htm, a small library that makes creating element trees easier.

import htm from 'htm';
function h(type, props, ...children) {
  let ele = Object.assign(document.createElement(type), props)
  ele.append(...children)
  return ele
}
const html = htm.bind(h);

document.getElementById('main').appendChild(html`
  <a class="heading" href="./over-there.html">
    <img class="img" src="./my-image.png" alt="some alt text"/>
    <h1 class="heading-text">Information</h1>
    <p>Lorum Ipsum</p>
  </a>
`)

That’s a pretty neet pattern @easrng, and would certainly help, as would that htm library.

However, my primary concern is with newer to mid-level developers. More advanced developers are probably just using a framework, or some templating library (like htm or handlebars), or they’re smart enough to figure out how to create a more ergonomic createElement function themself, etc. But, a newer developer is not going to figure out that clever pattern themself, nor would I recommend that they go off and install little libraries to solve day-to-day problems - and it’s these people who keep running off to use .innerHTML, often in unsafe ways.

@isiahmeadows those are good points worth considering. For point 1, I could see value in having a shorthand like that. It might be a little confusing that the shorthand uses query syntax to create an element, but it’s certainly an avenue worth considering. For point 2, that’s an excellent option worth considering - It’s common to have only one child, and I can see value in allowing someone to just pass in a straight value instead of an array.

There is substantial library precedent for my suggestion, actually. (I’m admittedly a bit biased. :wink:)

1 Like

Wow, apparently there is a number of libraries out there that support it. I guess that form of shorthand isn’t uncommon or unintuitive.