Refining the Creation of DOM Elements
by Jason S. Kerchner on May 13, 2009 (filed in Development)
If you recall from my first stab at creating the MVC View, I had a method of the View class called tag that would take some arguments and create a DOM element from them. This tag method would be called from higher-level methods designed to make creating a DOM element for the View easier. For example, the form method below (also a member of the View class) is used to create a FORM element. The tag method is called in line 9, and it does the actual work of creating the FORM element:
1 2 3 4 5 6 7 8 9 10 | form: function(id, url, method, attribs /*, elements*/) { if (!attribs) attribs = { }; attribs.id = id; attribs.action = url; attribs.method = method; return this.tag('<form %a></form>', attribs, this.extractArgs(arguments, 4)); } |
Now, let me explain a few things. First, I had envisioned calling tag with more options, like this:
1 | return this.tag('<form id="%i" action="%v" %a>%e</form>', id, url, attribs, elements); |
The %i would be replace with the id, %v would be the value (the url argument in this example), %a would be additional attributes, and %e would be the other elements contained by this element. All elements would include these arguments, so an INPUT element would be created something like this:
1 | return this.tag('<input id="%i" value="%v" %a />', id, value, attribs, null); |
What I realized, however, was that %i and %v were superfluous, and could be eliminated. Since these were just attributes, they could simply be added as part of %a. And this is exactly what I did in the first version of the tag function that I posted.
The %e I realized would require me to work strictly with HTML strings. In other words, I could replace the %e with an HTML string, but not with an actual DOM element (I can’t insert an object into the format string). The better plan, I figured, was to convert any HTML strings into DOM elements, then add the DOM elements to the containing element created by the tag method. That gave me more flexibility with what I passed as elements to the tag method. I could pass in an HTML string and convert it, or just pass in a DOM element. It also eliminated the need for the %e. Hence it was also dropped in the previous version of the tag method.
That left a much simpler structure, though it still had its roots in working with HTML strings. The attributes were expected to be passed into the tag method as an object of name/value pairs. That object would then be converted into an attributes string, and the %a in the format argument would be replaced with it. So essentially, a call to the tag method, like in line 9 below…
1 2 3 4 5 6 7 8 9 10 | text: function(id, value, attribs) { if (!attribs) attribs = { }; attribs.type = 'text'; attribs.id = id; attribs.value = value; return this.tag('<input %a />', attribs); }, |
…would result in the following code being executed inside the tag method…
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | // Convert the attribs object to an HTML string. var attribsHtml = ''; var singleAttrib = '.compact.checked.declare.readonly.disabled.selected.defer.ismap.nohref.noshade.nowrap.multiple.noresize.'; for (var a in attribs) { if (singleAttrib.indexOf('.' + a + '.') > -1) { if ((attribs[a] == 1) || (attribs[a] == true) || (attribs[a] == 'true') || (attribs[a] == a)) attribsHtml += ' ' + a; } else { if (attribs[a]) // Make sure its not null. attribsHtml += ' ' + a + '="' + attribs[a] + '"'; } } // Insert the attributes into the tag format format = format.replace('%a', attribsHtml); |
…which would create an attributes string (lines 1-13) that would then be inserted into the format argument at the %a position (line 16). In this particular example, this would result in an HTML string like the following:
1 | format = '<input type="text" id="username" value="jdoe" />' |
I could then take this HTML string, this complete tag, and convert it into a DOM element with a call to the element method. In fact, this is exactly what the tag method did next.
1 2 | // Convert the HTML string to a DOM object var elem = this.element(format); |
So that’s where it stood as per my last post, and up until recently, it worked nicely.
However, I began working on a method to create a TABLE element. I got as far as attempting to create the TR and TD elements. Turns out, these cannot be created anywhere except inside a TABLE element, which meant creating them using the tag method was impossible. You see, the element method (which the tag method uses to actually create the DOM element) creates a DIV, sets the innerHTML of the DIV to the HTML string that was created, then extracts the DOM element that the browser created from the HTML. But TR and TD elements can’t sit inside a DIV element, so the browser (at least Firefox) will not create them using the element method.
I really dislike making special cases and wanted to avoid that if at all possible. In other words, I didn’t want to check if the tag passed in was a TR or a TD and then work some different magic to create them as DOM elements. I wanted to use the same mechanism, regardless of the element being created.
I could create the TABLE element as a single string, with all the TR and TD elements inside it, but that would mean separate code to create the attribute strings, or pulling the attribute string generating code into a separate method. It would also mean I could wind up concatenating a huge HTML string, depending on how big the table was. Also, in order to keep the same mechanism where the programmer could pass in HTML strings or DOM elements, I would potentially have to convert a DOM element (one that might appear in a table cell, for example) into a string so that I could build it into the table. The more I thought about it, the more convoluted the whole processes seemed.
It was at this point that I realized five things. First, all the tags had the same format: '<tag %a></tag>'. Only the tag name was different. Second, any element can be created using the document.createElement function, even if that element can only sit inside specific other elements (like TR and TD can only sit inside a TABLE element). Third, all you need is the tag name to use document.createElement. Fourth, attributes can be added directly to a DOM element; they don’t need to be name/value pair strings. Fifth, I really didn’t like the name “tag” for the method, since it does not describe what the method really does.
So, I set out to refactor the tag method. First, I changed the name of the method to createElement, which more accurately describes what it does. Next, I
renamed the first argument from format to tag. Now, it expects only a tag name, not a tag format. And %a is no longer needed.
1 2 3 4 | // Original tag: function(format, attribs, elements) // New createElement: function(tag, attribs, elements) |
So, the part of the form method that calls the tag method changes, too.
1 2 3 4 5 6 7 8 9 10 11 12 13 | form: function(id, url, method, attribs /*, elements*/) { if (!attribs) attribs = { }; attribs.id = id; attribs.action = url; attribs.method = method; // Original return this.tag('<form %a></form>', attribs, this.extractArgs(arguments, 4)); // New return this.createElement('form', attribs, this.extractArgs(arguments, 4)); } |
I still have concerns about the method name. I can see where some people would confuse the View’s createElement method for the document’s createElement function. They are not the same, but the name accurately describes what they do. So for now, I will leave it with the new name.
Incidentally, if you are not familiar with the extractArgs method, you can learn about it in my first post on creating the MVC view.
Finally, the tag method, now renamed to createElement, got some internal changes as well. It now uses the document.createElement function to create elements, and it adds attributes directly to the newly created DOM element. The following is the updated function in its entirety.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 | createElement: function(tag, attribs, elements) { // Create the element var elem = document.createElement(tag); // Add the attributes to the element. HTML attribute names do not always match // the name that the DOM uses, so we check for them here. This may not be all // of them yet! for (var a in attribs) { attr = a.toLowerCase(); if (attr == 'class') elem.className = attribs[a]; else if (attr == 'for') elem.htmlFor = attribs[a]; else elem.setAttribute(a, attribs[a]); } // Add any inner element to the newly created DOM object if (elements) { for (var i = 0, len = elements.length; i < len; i++) { if (isString(elements[i])) elem.appendChild(this.element(elements[i])); else if (elements[i]) // Make sure not null or undefined elem.appendChild(elements[i]); } } // Lastly, return our new DOM element return elem; } |
First, it creates the DOM element (line 4). Then it adds the attributes to the element (lines 6-17). It is expected that these attributes are passed in with names that map to HTML attributes. The DOM property names may be different. For example, the HTML class attribute maps to the DOM element className property. I haven’t gone through all of the attributes yet, so this is probably not a complete list. As a bonus, now that I’m not creating a string of attributes, the code is much simpler.
The rest of the method is the same as the original, and uses the element method where necessary to convert HTML strings into DOM elements. I think this will be fine since it is unlikely that we will be passing HTML strings in for inner elements that cannot be created inside a DIV element. The table rendering method takes care of creating those elements that can only appear inside a TABLE element, and anything that we might pass in to be created inside a TD element should be able to be created inside a DIV, just the way the element method does it.
In my next post, I’ll take a closer look at that table method that started this whole refactoring process. For those who are interested in taking a look at it now, it’s available if you download the source code.
One final note. The test scripts for this are by no means complete and are, quite frankly, rather sloppy at the moment. I’ll get them cleaned up soon.
Leave a Reply