A nifty way to create HTML from JavaScript

In rewriting the folderlist for AssetCloud, I came across the need to generate quite a bit of HTML elements via JavaScript, and what I think is a pretty nifty way to do it.  Here’s the backstory:

The AC folderlist, at time of writing, is generated as most PHP pages are, a server-side PHP script which spits out an HTML string based on data from the database.  This works great for most content, since access to the database is very quick, and string data is pretty cheap in terms of bandwidth.  However, a sufficently complex project will have a lot of assets, which will create a folderlist described by a lot of html.  At time of writing, the Platformer folderlist comes in at around 3.2MB (this is just HTML, mind you, no images or anything), and the master folder list (which will crash Chrome if you don’t wait for it) is 5.82MB.  To put those numbers in context, the Platformer list takes 3 full seconds to download, and the master list (for whatever reason) takes a whopping 1.6 minutes to download!  That’s pretty unacceptable by pretty much everyone’s standards.  Read more to see my solution.

So, the solution?  Do it in JavaScript!  That is, use an AJAX query to get the data behind the folder list in JSON (I guess then shouldn’t it be AJAJ?  Blech, that’s a terrible acronym) and render the page itself via JS.  Sounds simple enough, but creating HTML elements in JavaScript is a major pain.  For reference, this is the code to create the following confirmation dialog:

// -*- mode: javascript; -*-
var q = options['q'];
if(q == null)	q = "Are you sure you want to do this?";
var confirmText = options['confirmText'];
if(confirmText == null)	confirmText = "Yes";
var denyText = options['denyText'];
if(denyText == null)	denyText = "No";
var confirm = options['confirm'];

$("#confirm_modal").remove();
var cont = document.createElement('div');
var confirmModal = document.createElement('div');
confirmModal.className = "rounded box";

var head = document.createElement('div');
head.className = "top-rounded whitetext gradient box_head drag_head";
head.innerHTML = q;
confirmModal.appendChild(head);

var body = document.createElement('div');
body.className = "modal_content";

var buttoncontainer = document.createElement('div');
buttoncontainer.className ="button_container";
var confirmButton = document.createElement('div');
confirmButton.className = "center w85 button text-center";
confirmButton.id = "confirm_button";
confirmButton.innerHTML = confirmText;
buttoncontainer.appendChild(confirmButton);

var spacer = document.createElement('div');
spacer.style.width = "30px";
buttoncontainer.appendChild(spacer);

var denyButton = document.createElement('div');
denyButton.className = "center w85 button text-center";
denyButton.innerHTML = denyText;
denyButton.id = "deny_button";
buttoncontainer.appendChild(denyButton);
body.appendChild(buttoncontainer);

confirmModal.appendChild(body);
cont.appendChild(confirmModal);

That’s about 45 lines for 7 elements (plus some crap at the top to determine the text).  Yuck!  I wasn’t about to do this crap for the folderlist, which is made up of quite a few elements, not 7.  So, in comes a helper function.  This actually turned out to be much much easier than I thought it would be.  As a note, I struggled with this for a while on account of the innerHTML property of DOM elements.  I’m not exactly sure how or why (and thanks to molgrew on the freenode javascript channel for pointing this out) but setting this property on an element that you’ve already added an event (onclick in this case) to will remove that event.  So, I no longer use innerHTML and add textNodes among the element’s children.  The code for the function is as follows:

 

// -*- mode: javascript; -*-
function Elm(type, attributes, children, properties){		//Short for "Element"
	//Create element of type "type"
	var el = document.createElement(type);
	//Set any attributes given by the attributes object
	for(var option in attributes)
		el.setAttribute(option, attributes[option]);
	//Add children from children array.  If the child is text, add it as a text node
	for(var child in children){
		if(typeof(children[child]) == "string")
			el.appendChild(document.createTextNode(children[child]));
		else
			el.appendChild(children[child]);
	}
	//Add any extra properties supplied
	for(var prop in properties){
		el[prop] = properties[prop];
	}
	return el;
}

 

All of the parameters but “type” are optional, and the fourth, “properties”, is there for the special purpose of keeping metadata along with the element. In my case, there’s a FolderListNode object which contains references to particular elements in the row, and some functions. Please excuse the presentation of the code above (the column is just not wide enough :( ), and see “raw code” to view the formatting correctly. I’m sure that I’m breaking some standards and this won’t work on every browser under the sun, but it simplifies the process greatly. As I come across issues, I’ll expand on the function to implement the necessary workarounds. Let’s rewrite that confirm dialog with the Elm function:

 

// -*- mode: javascript; -*-
var q = options['q'];
if(q == null)	q = "Are you sure you want to do this?";
var confirmText = options['confirmText'];
if(confirmText == null)	confirmText = "Yes";
var denyText = options['denyText'];
if(denyText == null)	denyText = "No";
var confirm = options['confirm'];

$("#confirm_modal").remove();

var cont =	Elm('div', {},
/*Box*/		[ Elm('div', { class: "rounded box" },
/*Head*/			[ Elm('div', { class: "top-rounded whitetext gradient box_head drag_head" }, [q]),
/*Body*/			  Elm('div', { class: "modal_content" },
/*ButtonContainer*/		[ Elm('div', { class: "button_container" },
/*ConfirmButton*/			[ Elm('div', {class: "center w85 button text-center", id: "confirm_button"}, [confirmText]),
/*Spacer*/					  Elm('div', {style: "width:30px"}),
/*DenyButton*/				  Elm('div', {class: "center w85 button text-center", id: "deny_button"}, [denyText])
						])
					])
				])
			]);
new_modal("confirm_modal", $(cont).html());

 

My appologies for the ugly presentation.  Once Jono gets up and I can login to the admin portion of the blog I’ll get a code highlighter installed.  Anyway, you can see that the code is not much shorter but more importantly readable.  It would take me a little while to read through the above example and figure out which elements were children of which, and what was actually going on.  I can read the above as pretty much straight HTML.  Formatting the code gets a little hairy (eclipse wanted to add an extra tab on top of what you see above, as if it were a normal broken statement) but I think if you stick to the convention I displayed above (one tab after variable declaration, and a new tab for each level of nesting) I think you’ll be able to keep from tearing your hair out.

 

I literally just came up with this last night, so by no means do I consider it standard or conventional in the least, but I’d like to hear it torn apart by some JavaScript snobs, so have at it!

~ by Schoen on June 10, 2011.

Leave a Reply

Your email address will not be published. Required fields are marked *