public

Making Elements

Trying my hand at "working smarter, not harder" in order to easily create multiple elements to insert into the DOM.

2 years ago

Latest Post As The CRO Flies (Part 1) by Tracy Parks public

I work in the field of optimizing and testing websites for the purposes of lifting conversion rates. A long winded way of saying, I make websites better. Well...."better".

This field of web development centers around client side execution - meaning that all the things you want done fire off within the user's browser as the page is loading. I'll go into the finer point of that in a later post, but suffice it to say, there is no back end or server side integration. If we want to make a new CTA (call to action - you know, the "buy now" buttons), we have to physically create them. Well when you are doing something a bit more complicated than a single CTA on the page it can be teadious.

How you would normally go about making an element in the DOM is by the document.createElement('tag_name') command. This would create an element with the tag name of "tag_name" - not entirely sure this name is valid in HTML, but you can replace "tag_name" with "div" or "a", etc. This is all usually assigned to a variable and then inserted into the DOM via appendChild() or insertBefore() or insertAdjacentElement() - what I refere to as the usual suspects.

Now, if I'm just making one element with not much to it (i.e. classes, innerHTML, ID, href, src, and other various attributes), I'll just make an element with the normal createElement method. But there are many times in my day that I need to make a few elements and they need a bunch of attributes, or I neeed to create a hundred new elements. Usually, the reason revolves around a carousel or the like. But regardless of the reasons, it can get a bit too much.

It's all too much for me to take

So being the "work smarter, not harder" person that I am, I devised a way to make this whole process as easy as possible. To start with I had to come up with a way to create an element and deliver a set of instructions to read some data input and populate the element accordingly. I also wanted it to be intuative with its syntax so that anyone who knows HTML would be able to use this. This was important as I've gone through Angular and REACT and although its a "really neat idea", the execution is horrible. They all seem to be written for people that just couldn't be bothered to learn HTML.

My first thought was to create a function - since really that is overly obvious as where to start - and this function would take in a parameter to create the HTML element.

function make_element(type){
	return document.createElement(type);
}
it's a simple redundant function

Too obvious? A bit too on the nose? Well, yes it is. This is just the foundation. With this I know that if I input a string as the type the output will be that string as the tagname. make_element('div') will create a div. make_element('span') will create a span. I mean it is fairly obvious and hopefully you are continuing to read this as this is just the start.

REACT, don't do it. When you want to go to it

The whole one-liner script is really just "re-inventing" the wheel. It's a function that replicates an already pre-existing function. Usefulness: 0.

Now the fun begins. How do we deliver information into a function that isn't just an endless amount of args? make_element(type, arg1, arg2, arg3...) is too cumbersome. Well my goto for delivering data as a single unit (i.e. a variable) is to use a JSON object. JSON is a very robust way of delivering all the data you need. But how do we tell the JSON to behave in a way that we can easily format the element.

Ideally, I'd like to use the keys as the attributes and the values as... well... their values.

const element_data = {
    classes:'classes value',
    styles:'inline-styles value',
    src: 'src vale',
    alt: 'alt value',
    innerHTML: 'innerHTML structure',
}
structure example for the JSON data

The problem with the previous layout is that it is a bit too much JS and not HTML syntax. It's close! but yeah not quite. Also, how do I approach populating the element with said info? While you can have an attribute of "classes" on an element, it really isn't valid as the "class" attribute. Same with "styles".

Overall, this is what we as devs technically refer to as "a really bad idea".

const element_data = {
    class:'classes value',
    style:'inline-styles value',
    src: 'src vale',
    alt: 'alt value',
    html: 'innerHTML structure',
}
a much better example for the JSON data

A better approach. Not a lot changed, but it makes all the difference in the world. A thing to remember too, this is not the only attributes we want to use. What about hyperlinks, titles, IDs, etc. And how do we broach assimilating all that and inserting it into our element?

I got them keys, keys, keys

Reading data and giving back the requested data is something that computers were built for! Well, no they weren't really, but let's pretend that was the case so that we can all be smug with how smart that statement was supposed to sound.

For those that don't know - and it is ok to not know something - JSON stores data values in keys. Many modern databases to this. It makes for construction, manipulation, and reading of said data easier. In the examples above we stored the src value in the src key. If we ever needed to read that bit of information, we'd just need to access it by "dot notation": element_data.src.

Now we come to the issue that is "how do we access something we don't know exists"? If we know the data structure is always going to be the same then we really don't need to worry about it.  We just use if declarations to go about and retrieve the values:

if(typeof element_data.class !== 'undefined'){
    x = element_data.class;
}

Now what happens when the data we want to feed is something that wasn't anticipated? Class, HTML, href, and all that are easy to think about, but what about data-src or avid-x? Nearly anything can be assigned as an attribute. But we can't anticipate everything. Populating every eventuality would be a tediously long and impossible function to use. Millins of lines of code dedicated to if statements in a long chain! We just couldn't do it.

So how do we do it?

Arrays have already preset "keys" in the form of item numbers: 0, 1, 2, 3 ... If you want to access a certain item in the array you can just loop through it till you find the correct value, but that defeats the purpose of using the key to populate the attributes. One could have an array of attribute that you want to assign and a secondary array with all the values, but that is just convoluted and unnecessarily prone to errors. Imagine if the key array got a push and the value array got skipped. No, that way is just asking for trouble.

Enter: Object.keys(array)

What this little JavaScript function does is takes the keys of an object array and places them into an actual array. With this you can then access the value said key of the JSON array with bracket notation: array[key]. In order to do so you'll need to iterate through all the keys. For loops, while, etc can be used. I personally use forEach()

Object.keys(array).forEach((key) => {
    x = array[key];
});

Now that we know we can access nearly anything that is placed into the data we can move a bit forward with our function:

function make_element(type,settings){
    let element;
    if(type){
        element = document.createElement(type);
        if(typeof settings === 'object'){
            Object.keys(settings).forEach({
                element.setAttribute(key,settings[key]);
            });
        }
    }
    return element;
}

Now we can have entries within our data for class, id, src, href, etc and even ones that are specific to other users and uses like data-id or i-just-made-this-up.

BUT...

I like big buts and I cannot lie

How do we set the innerHTML. That isn't an attribute. Making a key labeled html is just going to create an attribute called html. That is just not going to work. Granted there is a fairly easy solution: "we just check to see if the key is html then do something else.

et, voilà

function make_element(type,settings){
    let element;
    if(type){
        element = document.createElement(type);
        if(typeof settings === 'object'){
            Object.keys(settings).forEach((key) =>{
                if(key == 'html'){
                    element.innerHTML = settings[key];
                } else {
                    element.setAttribute(key,settings[key]);
                }
            });
        }
    }
    return element;
}

Our full function completed. We could do a bit more, like checking to make sure HTML was used in place of html.

if(key.toLowerCase() == 'html'){
    element.innerHTML = settings[key];
}

Or if the full innerHTML was used in it's place

if(key.toLowerCase().indexOf('html') > -1){
    element.innerHTML = settings[key];
}

I could spend hours going through and modifying this to accommodate scenarios that might crop up, but in the end, I'm the one setting the settings so I don't bother. For larger team assignments, I might go through to allow for other scenarios.

This is the end, beautiful friend

In conclusion, to invoke the function all one would need to do is give the element type and layout the settings:

const data = {
    class:'make-red set-right-text font-arial',
    html:'This text should be red, right-aligned, and in the arial font'
}
const foo = make_element('div',data);

// or be more direct

const bar = make_element('a',{
    class:'button-link',
    id:'main-cta',
    html:'Learn More',
    href:'/links-here'
});

UPDATE 2023

I've used the above function quite a bit and over the course of months I've come to the realization that it needed some updating and tweaking. I realized that I needed to clean it up, make it a little more efficient but not lose any of it's functionality or interaction setup.

In place of Object.keys I substituted it for Object.entries which would allow me to deconstruct the key, value automatically rather than just going with the key and assuming the value with the use of settings[key].

Object.entries(settings).forEach(([key, value]) => {
	/* ... */
});

I also added the lookup for the key to be something for HTML and something for CLASS to better use cases where someone might use innerText or css as the key. I did this bay adding a switch statement looking at the key itself and factoring in the various "standard" uses that people might use dpepending on their background.

switch (key.toLowerCase()) {
	case 'html':
    case 'innerhtml':
    case 'text':
    case 'innertext':
    	element.innerHTML = value;
        break;
    case 'class':
    case 'classname':
    case 'classlist':
    case 'css':
        element.classList.add(...(!Array.isArray(value) ? value.replace(/,/g,' ').split(' ') : value));
        break;
    /* ... */
}

I made sure to look at the key in all lower case just in case someone used camelCase or ALL CAPS or SpOnGBoB cases for their JSON data object. I also took into account that the classes may or may not be input as an array. So with that I used the spread operator ... on the results of the ternary operator checking for the value to not be an array !Array.isArray(value). If it isn't, it splits the values at the space and makes them into an array. This is useful in case someone uses a string to represent the classes: 'this_class this_other_class' or if they flubbed on the string and made an array of classes into a string like: `'this_class, this_other_class' I honestly can't think of every permutation of failure so I stopped with just that.

Next I took care of all the other attributes that could be added. Now this was very straight forward and I just decided that the attribute added would just be the key and its value would just be the value.

default:
	element.setAttribute(key, value);

The only other change I made was to not return undefined if there was no type declared. Like if someone just typed: const foo = make_element(); or something like that. So instead of just returning 'undefined', I added a return short circuit:

return element || null;

This will return null if element is undefined. I know it's "truthy/falsey" the same thing, but it really isn't the same thing programmatically. So I added it there for cleaner results.

The 2023 updated code:

function make_element(type,settings){
    let element
    if (!!type && typeof type === 'string') {
        element = document.createElement(type);
        if (typeof settings === 'object') {
            Object.entries(settings).forEach(([key, value]) => {
                switch (key.toLowerCase()) {
                    case 'html':
                    case 'innerhtml':
                    case 'text':
                    case 'innertext':
                        element.innerHTML = value;
                        break;
                    case 'class':
                    case 'classname':
                    case 'classlist':
                    case 'css':
                        element.classList.add(...(!Array.isArray(value) ? value.replace(/,/g,' ').split(' ') : value));
                        break;
                    default:
                        element.setAttribute(key, value);
                }
            });
        }
    }

    return element || null;
}
Tracy Parks

Published 2 years ago