Dollar E: A document.createElement Wrapper

2006-03-29 17:11 - Programming

First, a lead in: I work for Root Markets, as part of the Root Vaults team. The CEO of my company, Seth Goldstein presented at this year's ETech conference, and my team went through a lot of work to get some spiffy new features built for our site to demo at that conference.

The primary new feature ( from my perspective ;-) ) was a brand new UI that made use of AJAX, which I built on top of the Prototype JavaScript library. There was a lot of dynamic element creation based on the data coming back from the AJAX calls, and thus a lot of document.createElement calls, along with the appendChild and insertBefore and the element-property-setting statements. It's very verbose.

At one point early on, I decided to write a wrapper function for document.createElement to avoid typing that out so often, and ended up realizing I should reduce more typing by figuring out a way to pass in attributes like the class and ID to be set. And then I realized, and it feels obvious in retrospect, that it should be able to handle creating a whole tree. I'm sure I'm not the first person to think of this, or even to write such a function. But I have found this to be very useful, and so after getting permission to do so from my company, I came here to release it as open source.

I called the function $E() or "Dollar E". I see it as the reverse of the $() function in prototype. The $() function gets an element that exists, the $E() function creates a new element that doesn't. It is passed a custom data structure, and it returns a (tree of) DOM element(s). Here's the source code and an example, and the HTML string produced by running the example.


Comments:

Here is something else to add
2006-03-29 23:32 - lm_mario
As some have said a clone of an element is faster than creating a new one. So I have created a replacement for document.createElement and document.createDocumentFragment which caches the first call to create and element then all future calls use its clone. I also use a ref to the 'document' object becuase thats faster than working with the straight document object. Your use of document.createElement is not altered and should work great with the $E shortcut.
//CODE IS BELOW

var doc = document;

doc._elementCache = {};
doc._createElement = doc.createElement;
doc._createDocumentFragment = doc.createDocumentFragment;

doc.createElement = function(elementName){
	elementName = elementName.toLowerCase();

	if(doc._elementCache[elementName+'Element'] == null){
		doc._elementCache[elementName+'Element'] = doc._createElement(elementName.toUpperCase());
	}
	
	return doc._elementCache[elementName+'Element'].cloneNode(false);
};

doc.createDocumentFragment = function(){
	if(doc._elementCache.documentFragment == null){
		doc._elementCache.documentFragment = doc._createDocumentFragment();
	}
	return doc._elementCache['documentFragment'].cloneNode(false);
};
License?
2006-03-30 00:08 - jdunck
Say MIT or similar. ;)
some notes
2006-03-30 01:21 - kae_verens
I've been working on a similar function for some time. My own version is newEl(tag,id,classname,innerText). I assign the id to both el.id and el.name (to take care of inputs and such). The code can be seen here: http://borderaction-new.webworkstest.com/j/js.js

IE, as usual, throws up gotchas. For instance, if you create an iframe with the newEl() method (and presumably with your $E() method), IE ignores the .name set, making it tricky to target iframes with generated forms. To get around that, I keep an ie-specific version in a separate file: http://borderaction-new.webworkstest.com/j/js-ie.js

I hope this is useful to you. I'm really considering using your code in future - it looks much cleaner than mine!

Syntactic elegance
2006-03-30 06:23 - ecmanaut
The Mochikit way is even more elegant, cutting away some of your property names and object nestings, making your example look like this (created attribute-less nodes getting a null first parameter instead of the attributes objects passed here):

DIV({id:"toolGroup_1",class:"toolGroup"},
DIV({class:"roundBarTop"},
DIV({class:"leftEdge"}),
DIV({class:"rightEdge"}),
DIV({class:"heading"},A({class:"collapser"}),"Group Heading")))
An even more prototypish way...
2006-03-30 08:18 - foca
Nice function, I've hacked it into prototype in a more prototypish way, adding it to Element.Methods as "create":
Element.Methods = {
  create: function(data) {
  	var el;
  	if (typeof data == 'string') {
  	  el = document.createTextNode(data);
  	  return el;
  	}
  	el = document.createElement(data.tag);
  	delete(data.tag);
  	if (typeof data.children != 'undefined') {
  	  if (typeof data.children == 'string' || typeof data.children.length == 'undefined') {
  	  	el.appendChild($E(data.children));
  	  } else {
  	  	data.children.each(function(child) {
  	  	  el.appendChild($E(child));
  	  	});
  	  }
  	  delete(data.children);
  	}
  	data.each(function(attr) {
  	  el[attr] = data[attr];
  	});
  	return el;
  },

  ...
}
And then adding, after Object.extend(Element, Element.Methods):
var $E = Element.create;
Thus integrating it into Prototype in a nicer way.
Thanks a lot :-D
Delete? Other Argument Formats?
2006-03-30 16:27 - logicnazi
It seems to me kinda counterintuitive to delete the attributes of the object that is passed in to $E. I think some programmers might be very surprised when they pass in what is supposed to be a persistant object they have saved somewhere and its properties get deleted. Javascript will do GC for you and if the user doesn't want that object in the future it will be collected (I don't think there are memory leak issues in just this simple case) so why delete the properties?

Also it would seem best to support calls of the kind (tag, classname, id) or perhaps (tag, id, classname). This could be implemented with a quick arguments.length and typeof check at the begining of the function.

As for the poster who wanted to cache the created DOM elements (though the code the posted doesn't seem to apply the class/id attributes) I'm not so sure what you did here is a good idea. For one it leaves a reference to that DOM element lying around so it won't get GCed. If it is just one object this isn't a big deal but what if if has a huge list of child nodes (maybe someone is using it to parse data from the server into the DOM). Also you need to count in the time of changing the id/classname/attributes as well as the overhead of all the caching functions.

It is quite possible that this will screw up the GC in more subtle ways as well. It can be really amazing what small changes in reference circularity/type can have on the memory footprint. Since it is far more important to avoid the worst case where you slow the browser to a crawl because of the memory usage than it is to save milliseconds in the $E function it seems overall to me to be a bad idea.
Why Delete?
2006-03-30 16:42 - arantius

Simple: I don't want to litter every DOM element I create with a "tag" and probably "children" attribute, the latter of which is rather large sometimes. It's a lot less code and a fair deal faster than checking the key name inside the attribute-assigning loop.

True, I hadn't considered the case where the user would intend to be keeping that element around, since all my use was inline objects, as in the example. Perhaps it should make a copy of the data object at the top of the function, and work from the copy.

If you are suggesting that the function accept tag, class, and id as direct function arguments, that would totally break the nestable children paradigm that gives this function it's true power. The reason this is useful is that it defines a simple object, which it can transform to a DOM element, but can do so recursively.

Clone vs Create
2006-03-30 16:54 - arantius

Mario, the following script:

<script type='text/javascript'>
var el, el2, start, end;

start=new Date();
for (var i=0; i<9999; i++) { 
	el=document.createElement('div');
}
end=new Date();
alert('createElement: '+ (end-start) );

start=new Date();
el=document.createElement('div');
for (var i=0; i<9999; i++) { 
	el2=el.cloneNode(false);
}
end=new Date();
alert('cloneNode: '+ (end-start) );
</script>

Tells me that Firefox executes cloneNode 26% slower than createElement. WinIE is 39% faster, but much faster in both cases. (IE's create is faster than FF's, by about 100%.)

But it also tells me that creating ten thousand elements only takes between 0.1 and 0.35 seconds. Seeing as we're usually talking dozens at a time, either way is perfectly quick for me!

Nice work
2006-03-31 05:08 - gingerhendrix
Hi anthony,

Nice piece of code. Nice enough to inspire me to blog about it. The guts of my post is disagreeing with ecmanaut that the mochikit way is more elegant, though i think this is just a matter of personal preference. Cheers for the code snippet.
thanx for the speed test
2006-04-01 17:58 - lm_mario5
Thanx arantius. (this is Mario, i forgot my password, wierd?)

I do remember when I made it that there was some time differences between the browsers. Moz is genrally speaking faster than IE in lots of things. For example try creating table elements (with and without using innerHTML to make them). Moz smokes IE for that. Also if you are interested... here is a faster 'each' method for prototype. It uses a reverse while loop (at least it hsould be faster hehe 8P)

Array.prototype._each = function(iterator, thisVal){
//using a faster reverse while loop instead of a for loop
var i = this.length,l=i;
if( i > 0 ) {do {
    var iIndex = l-i;
    iterator.call(thisVal, this[iIndex], iIndex, this); //(this[iIndex] || this.charAt && this.charAt(iIndex))
}while( --i );};
};
attributs
2006-07-27 10:41 - Blaataap

I had to change:

for (attr in data) {
    el[attr]=data[attr];
}
to:
for (attr in data) {
    el.setAttribute(attr, data[attr]);
}

Take it up a notch!
2007-03-18 10:08 - lskatz

Hi, this is the closest I have seen to solving my "template" problem with newer versions of Javascript. I'm not very good at Javascript, but it would be nice to see it solved.

Basically, I want to go from a template html string to adding it via createElement and appendChild. With firefox, I don't have to worry about it because it just lets me insert html. However, other browsers are sticklers. The reason behind the html string is that my backend php scripts can access it too. I outlined the problem better on google groups but no one could help me there.

I guess this is an open challenge to anyone who wants to solve it because it would be really, really helpful. I imagine the usage would be something like appendComplexElement(newEl,parentEl);

My post on google groups - the formatting on the template string looks better there.

My template file would be inserted into a table. As you can see, the second layer of table rows presents a problem in just getting the table element, followed by tr and td.

<tr> <td>{var1}</td> <td>{var2}</td>

<td> <table> <tr> <td>Name: {name3}</td> <td>Age: {age3}</td> </tr> </table> </td>

</tr>

-- Thanks! Lee

Looks great...can you make the example work?
2007-06-20 10:55 - 1Riptide

hi. Thanks for all your efforts first of all ... The problem I am having is that when I use the sample code provided - nothing happens. Nothing is written to the document at all, though my efforts to alert my way through your $E function seem to indicate that things are clicking right along.

I have taken this example into many different sandboxes with simpler and simpler implementations...but the result is the same. Nothing.

Is there something I am doing wrong (remember...I am copy/pasting your code verbatim...)

The example ...
2007-06-20 10:58 - arantius

... on screen is not what produces the code you see. It just produces an element, that your script can use. The display is powered by a slightly different script. View the source (of the iframe above), and look at the "runExample" function.

Thanx arantius
2007-06-20 12:18 - 1Riptide

forgot about appendChild() as well. Very good work sir!

Very nice; one tiny fix
2008-02-25 22:32 - lyricsboy

I changed this line:

for (attr in data) {
to this:
for (var attr in data) {
to avoid warnings from Rhino about attr being out of scope. Thanks for the useful code!

Post a comment:

Username
Password
  If you do not have an account to log in to yet, register your own account. You will not enter any personal info and need not supply an email address.
Subject:
Comment:

You may use Markdown syntax in the comment, but no HTML. Hints:

  • An empty line between text will create a paragraph boundary.
  • Use angle braces around a plain URL to auto-link it: <http://www.example.com/>.
  • Use this format to create a link with different text showing: [An Example](http://www.example.com/).
  • Use backticks (``), not leading spaces to enclose a code block.

If you are attempting to contact me, ask me a question, etc, please send me a message through the contact form rather than posting a comment here. Thank you. (If you post a comment anyway when it should be a message to me, I'll probably just delete your comment. I don't like clutter.)