JavaScript

How to create DOM elements efficiently with JavaScript

At some point, every web application needs an overlay. A sort of pop-up that obscures the background and asks the user to perform an action. Here is how you do it.

First you create the HTML, give the overlay an absolute position, and then set the display to none by default. But what if you don’t have access to the HTML? Luckily, JavaScript has a convenient method for that.

document.createElement(elementName);

You can create any element using this method. The challenge starts when you have a complicated nested hierarchy of elements. Here is an example of an overlay:

<div id="overlay">
    <div class="overlay__inner">
        <div class="overlay__box">
            <div class="overlay__hdr">
                <span class="overlay__close-btn">X</span>
                <h3>The Overlay Title</h3>
                <p>The purpose of this overlay</p>
            </div>
            <div class="overlay__content">
                ...
            </div>
        </div>
    </div>
</div>

Recreating this in JavaScript is easy, but not simple. You have to keep track of every element that needs to be nested. Applying the correct attributes is also tedious. Let's see what it looks like.

var overElem = document.createElement("div");
overElem.id = "overlay";
var overInner = document.createElement("div");
overInner.className = "overlay__inner";
var overBox = document.createElement("div");
overBox.className = "overlay__box";
var overHdr = document.createElement("div");
overHdr.className = "overlay__hdr";
var clsBtn = document.createElement("span");
clsBtn.className = "overlay__close-btn";
var title = document.createElement("h3");
title.innerText = "The Overlay Title";
var subtitle = document.createElement("p");
subtitle.innerText = "The purpose of this overlay";

// a long while later

overHdr.appendChild(clsBtn);
overHdr.appendChild(title);
overHdr.appendChild(subtitle);
overBox.appendChild(overHdr);
...

It is a tedious task. Not only you have to define all elements, you have to make sure you append them in the correct order. This can cause bugs that are hard to track. To make the process easier, I created a couple of functions that turn the process into a more manageable tree.

JavaScript Mark Language or jml

I was inspired by the syntax of a popular JavaScript framework called Hyperapp. Here is how I want my code to create DOM elements to look like.

var overlay = ml("div", { id: "overlay"}, 
    ml("div", { class: "overlay__inner"}, 
        ml("div", { class: "overlay__box"}, [
            ml("div", { class: "overlay__hdr"}, [
                ml("span", {
                    class: "overlay__close-btn",
                    onClick: function() {
                        console.log("closing the overlay")
                    },
                }, "X"),
                ml("h3", {}, "The Overlay Title"),
                ml("p", {}, "The purpose of this overlay"),
            ]),
            ml("div", { class: "overlay__content"}, ["more content"]),
        ])
    )
);

document.body.appendChild(overlay);

This elegant style looks similar enough to the resulting HTML and is flexible enough to debug. What we are doing in this code is we call a function ml(a,b,c) that can have 3 arguments.

The first argument is the type of element we want to create. It can be div, h1, span or any valid HTML tag.

The second argument is the list of attributes we want the element to have. Note that we can also use this to attach events to the element. You can have onClick, onMouseover or any valid element event.

Then the third argument can be 3 things. A string, a DOM element, or an array of strings and DOM elements. This third parameter is automatically converted to an element and then appended to the parent. Note that strings are converted to a TextNode. So including HTML in the string automatically converts it to the corresponding HTML entity.

The library

Let's write the code needed to support this functionality:

function ml(tagName, props, nest) {
    var el = document.createElement(tagName);
    if(props) {
        for(var name in props) {
            if(name.indexOf("on") === 0) {
                el.addEventListener(name.substr(2).toLowerCase(), props[name], false)
            } else {
                el.setAttribute(name, props[name]);
            }
        }
    }
    if (!nest) {
        return el;
    }
    return nester(el, nest)
}

function nester(el, n) {
    if (typeof n === "string") {
        var t = document.createTextNode(n);
        el.appendChild(t);
    } else if (n instanceof Array) {
        for(var i = 0; i < n.length; i++) {
            if (typeof n[i] === "string") {
                var t = document.createTextNode(n[i]);
                el.appendChild(t);
            } else if (n[i] instanceof Node){
                el.appendChild(n[i]);
            }
        }
    } else if (n instanceof Node){
        el.appendChild(n)
    }
    return el;
}

The ml() function creates the parent element and sets its attributes and events. The nester() function, reads the third parameter and appropriately creates the child elements. Then the results is appended to the parent.

These two very simple functions make it much easier to deal with DOM elements in JavaScript. Plus you don't have to worry about figuring out which elements is the child of which. The hierarchy is in the design.


Comments

There are no comments added yet.

Let's hear your thoughts

For my eyes only