True Dynamic Element Object Creation

After posting about my adaptation of the del.icio.us playtagger, I decided to reduce the code in size a bit… and maybe even do some comparisons. After looking at the previous lines of code, I concluded that the most space was occupied by DOM methods like setAttribute or document.createElement. So the best way of reducing all that space was by creating some sort of ‘element object creator’… surely, I wasn’t the only one to suffer from ‘manually-dynamically’ creating elements… but there were only a couple of initiatives,… nothing compatible that degraded gracefully when using ‘application/xhtml+xml’ or ‘text/html’.

So, after coding for a while, I came up with this:

function crEl() {
  var a = arguments; //arguments
   var nN = a[0].toLowerCase(); //node Name
   var tE = document.createElement(nN); //temporary element
   var aPL = (a.length - 1)/2; //length of attribute and value pairs
   var dM = (aPL%2)-(Math.floor(aPL%2)); //decision maker
   if(dM === 0) { //if there is a value for each attribute name
      if(nN == "param") { //this applies for the param element
         for(var i = 0; i < aPL; ++i) {
            var j = 1+(i*2);
            tE.setAttribute("name", a[j]);
            tE.setAttribute("value", a[j+1]);
         }
      } else { //if the element is not a param element...
         for(var k = 0; k < aPL; ++k) {
            var l = 1+(k*2);
            tE.setAttribute(a[l], a[l+1]);
         }
      }
      return tE; //returns the element
   } else {
      return false; //Perhaps an alert(nN); would be better?
   }
}

You can copy and paste the above function, and test it out at the Mini Javascript Editor. Here’s a small test case (copy and paste it in the editor along with the function):

var par = crEl("p");
var div = crEl("div","id","mydiv","style","background-color:#ffffff;");
out(par.nodeName+", "+div.nodeName);
out(typeof(par)+", "+typeof(div));
out(par.hasAttributes()+", "+div.hasAttributes());


Usage
The function crEl() accepts any number of arguments (all of them strings… for now), and it creates an element (a node) with the name of the first string. The rest of the arguments are pairs of attribute names and values. An example would be:

var image = crEl(“img”,”src”,”http://example.com/image.png”,”alt”,”great image”,”width”,”80″,”height”,”15″);

The previous line of code would create an object with a nodeName of img, with an attribute ‘src’ of ‘http://example.com/image.png’ and so on… In an XHTML document it would be:

<img src=”http:example.com/image.png” alt=”great image” width=”80″ height=”15″ />

The only thing left to do is append the image object somewhere. Like, document.getElementById(‘mydiv’).appendChild(image);.

If you revise the code, you’ll see that it has room for improvement (as in including special rules for elements that may have textNodes),… but for now it has a special case for ‘param’ elements…

I’ll compare the original del.icio.us playtagger code for adding an object with attributes and some children to a span element (they use the innerHTML method).

Original code:

Delicious.Mp3.player = document.createElement('span')
Delicious.Mp3.player.innerHTML = '<object style="vertical-align:bottom" classid="clsid:d27cdb6e-ae6d-11cf-96b8-444553540000"' +
'codebase="http://fpdownload.macromedia.com/pub/shockwave/cabs/flash/swflash.cab#version=6,0,0,0"' +
'width="100" height="14" id="player" align="middle">' +
'<param name="allowScriptAccess" value="sameDomain" />' +
'<param name="flashVars" value="theLink='+url+'" />' +
'<param name="movie" value="http://test.dev.del.icio.us/static/swf/playtagger.swf" /><param name="quality" value="high" />' +
'<param name="bgcolor" value="#ffffff" />' +
'<embed style="vertical-align:bottom" src="http://test.dev.del.icio.us/static/swf/playtagger.swf" flashVars="theLink='+url+'"'+
'quality="high" bgcolor="#ffffff" width="100" height="14" name="player"' +
'align="middle" allowScriptAccess="sameDomain" type="application/x-shockwave-flash"' +
' pluginspage="http://www.macromedia.com/go/getflashplayer" /></object>'

When I ported the del.icio.us playtagger to use document.createElement instead of using innerHTML, the code for creating an object, and appending some variables to it was:

Delicious.Mp3.player = document.createElement('span');
Delicious.Mp3.player.setAttribute("id", "delicious");
var tempobj = document.createElement('object');
tempobj.setAttribute("type", "application/x-shockwave-flash");
tempobj.setAttribute("data", "http://del.icio.us/static/swf/playtagger.swf");
tempobj.setAttribute("width", "100");
tempobj.setAttribute("height", "14");
tempobj.setAttribute("id", "player");
var param0 = document.createElement('param');
var param1 = document.createElement('param');
var param2 = document.createElement('param');
var param3 = document.createElement('param');
var param4 = document.createElement('param');
var param5 = document.createElement('param');
var param6 = document.createElement('param');
param0.setAttribute("name", "allowScriptAccess");
param0.setAttribute("value", "always");
param1.setAttribute("name", "flashVars");
var tlu = "theLink=" + url;
param1.setAttribute("value", tlu);
param2.setAttribute("name", "movie");
param2.setAttribute("value", "http://del.icio.us/static/swf/playtagger.swf");
param3.setAttribute("name", "quality");
param3.setAttribute("value", "high");
param4.setAttribute("name", "bgcolor");
param4.setAttribute("value", "#FFFFFF");
param5.setAttribute("name", "menu");
param5.setAttribute("value", "false");
param6.setAttribute("name", "wmode");
param6.setAttribute("value", "transparent");
tempobj.appendChild(param2);
tempobj.appendChild(param3);
tempobj.appendChild(param4);
tempobj.appendChild(param5);
tempobj.appendChild(param6);
tempobj.appendChild(param1);
tempobj.appendChild(param0);
Delicious.Mp3.player.appendChild(tempobj);

If you look closely, I ommited some attributes for the object, or decided to add some params of my own… this was because appending an embed element through javascript causes mysterious errors in IE6. With this function… a direct translation from del.icio.us’ innerHTML method would look like:


Delicious.Mp3.player = crEl("span","id","delicious");
var tempobj = crEl("object","type","application/x-shockwave-flash","data","http://del.icio.us/static/swf/playtagger.swf","style","vertical-align:bottom;","width","100","height","14","id","player");
var p0 = crEl("param","allowScriptAccess","sameDomain");
var p1 = crEl("param","flashVars","theLink="+url);
var p2 = crEl("param","movie","http://del.icio.us/static/swf/playtagger.swf");
var p3 = crEl("param","quality","high");
var p4 = crEl("param","bgcolor","#ffffff");
var p5 = crEl("param","wmode","transparent");
tempobj.appendChild(p0);
tempobj.appendChild(p1);
tempobj.appendChild(p2);
tempobj.appendChild(p3);
tempobj.appendChild(p4);
tempobj.appendChild(p5);
Delicious.Mp3.player.appendChild(tempobj);

So… as you see, the more code that is used, the more you save space.

What’s next
What will happen next… ? I guess I can try porting the code to have a switch/case model for the tags… or try to include a method for appending Children nodes on the fly… Or if anyone is interested in making an Ecmascript (Javascript) library… feel free to use the code (just let me know).

Regarding the more dynamic way of appending child nodes, I was thinking of a recursive pair of functions that could be called multiple times when detecting an argument type of a number… and that could create text nodes on the fly…

XHTML-PlayTagger
Here is some basic testing… modified playtagger (application/xhtml+xml), modified playtagger (text/html)… The del.icio.us playtagger finds links to mp3 files in a document and places a button next to them. When the button is clicked, it embeds a flash player. I still haven’t managed for the flash player to be displayed in IE6 (appending an embed causes a javascript error saying ‘Invalid argument’). Feedback as to how to work around this would be greatly appreciated.

The original playtagger is only 2.43 kilobytes in size, and my latest modified tagger is 2.87 kilobytes… but when both are compressed, the filesize is, respectively, 2.25 and 2.34 kilobytes. So, there’s not much harm done. Also, I think the size will decrease considerably once I find a workaround for using an embed element as alternate content… (just using objects and params should suffice).

Test Cases
I’m going to make a test file… to see if I could bypass the need to store the created element in a variable and then append that variable… and also test what happens if dM !== 0. Oh well… I might as well do that after lunch ;-)

4 Comments

  • I was reading your post and had a few ideas how you might improve the function.

    First of all, you could pass in attribute values as an object, which lets you have natural name/value pairing, such as:

    function crEl(tagName, attributes) {
    tagName = tagName.toLowerCase();
    var element = document.createElement(tagName); //temporary element
    for (var property in attributes) {
    if (tagName == “param”) {
    element.setAttribute(“name”, property);
    element.setAttribute(“value”, attributes[property]);
    }
    else {
    element.setAttribute(property, attributes[property]);
    }
    }
    return element;
    }

    var div = crEl(“div”, { id: “mydiv”, style: “background-color: #ffffff;” });

    If you’re interested in a sophisticated element assembler, you could create an ElementBuilder object. Since I’m on the topic anyway, let me try:

    function ElementBuilder() {
    // maintain a stack of elements to push/pop
    this._elementStack = [document.createDocumentFragment()];
    }

    ElementBuilder.prototype.pushEl = function(tagName, attributes) {
    var element = crEl(tagName, attributes);
    this._elementStack[this._elementStack.length-1].appendChild(element);
    this._elementStack.push(element);
    return element;
    };

    ElementBuilder.prototype.addText = function(text) {
    var textNode = document.createTextNode(text);
    this._elementStack[this._elementStack.length-1].appendChild(textNode);
    };

    ElementBuilder.prototype.pushAndPopEl = function(tagName, attributes) {
    this.pushEl(tagName, attributes);
    this.popEl();
    };

    ElementBuilder.prototype.popEl = function() {
    if (this._elementStack.length > 1) {
    this._elementStack.pop();
    }
    };

    ElementBuilder.attach = function(parent) {
    parent.appendChild(this._elementStack[0]);
    };

    You would use it like this:

    var mp3builder = new ElementBuilder();
    mp3builder.pushEl(“object”, {type:”application/x-shockwave-flash”,data:”http://del.icio.us/static/swf/playtagger.swf”,style:”vertical-align:bottom;”,width:”100″,height:”14″,id:”player”);
    mp3builder.pushAndPopEl(“param”,{allowScriptAccess:”sameDomain”});
    mp3builder.pushAndPopEl(“param”,{flashVars:”theLink=”+url});
    mp3builder.pushAndPopEl(“param”,{movie:”http://del.icio.us/static/swf/playtagger.swf”});
    mp3builder.pushAndPopEl(“param”,{quality:”high”});
    mp3builder.pushAndPopEl(“param”,{bgcolor:”#ffffff”});
    mp3builder.pushAndPopEl(“param”,{wmode:”transparent”});
    mp3builder.popEl();

    Delicious.Mp3.player = crEl(“span”, {id: “delicious”});
    ElementBuilder.attachTo(Delicious.Mp3.player);

    I apologize for any errors. Feel free to edit this comment for formatting and coding errors.

  • Hello Matthias, and thank you for your insightful comment. Your idea of the assembler sounds really intriguing, I’ll give it a try and see what I can come up with…

    I did spend some time before reading your comment expanding the function, so that it could accept objects and append an onclick event (this last one was unsuccesful).

    Here is the formula I came up with: (dropped some conditions, and a for, and went with a while… but your object oriented arguments seem much better

    function crEl() {
    function cE(el) { //http://simon.incutio.com/archive/2003/06/15/javascriptWithXML
    if (typeof(document.createElementNS) != “undefined”) {
    return document.createElementNS(“http://www.w3.org/1999/xhtml”, el);
    }
    if (typeof(document.createElement) != “undefined”) {
    return document.createElement(el);
    }
    return false;
    }
    var a = arguments; //arguments
    var nN = a[0].toLowerCase(); //nodeName
    if(nN == “tn”) { //creates textNode with first argument after “tn”
    var tN = crTN(a[1]);
    return tN;
    } else if(a.length >= 1 && typeof(a[0]) == “string”) {
    var tE = cE(nN); //temporary Element
    var strs = new Array(); //array to store strings and other stuff
    for(var b = 0; b < a.length; ++b) {
    var tst = a[b]; //temporary storage
    if(typeof(tst) == “object”) {
    tE.appendChild(tst);
    } else {
    strs.push(tst); //these can actually be strings, numbers, functions… anything but objects
    }
    }
    a = strs; //new arguments… (after having appended objects, and removed them from the list)
    } else {
    return a[0]; //in case an object gets passed as the first argument
    }
    var al = a.length; //length of attribute and value pairs
    if(nN == “param”) { //this applies for the param element
    tE.setAttribute(“name”, a[1]);
    tE.setAttribute(“value”, a[2]);
    } else { //if the main element is not a param element…
    var s = 0; //how much to skip after parsing a method
    var l = 1;
    while(l<al) {
    var m = a[l].toLowerCase();
    if(m == “tn”){ //this checks for a delcaration of a textNode
    var txN = crTN(a[l+1]); // creates a textNode
    tE.appendChild(txN); //appends the textNode
    s=2;
    } else if(m == “param” && (a[l+2])) { //if there is a false attribute called ‘param’
    var tpm = cE(m); //creates the param
    tpm.setAttribute(“name”, a[l+1]); //param name and name value
    tpm.setAttribute(“value”,a[l+2]); //param value and value value :P
    tE.appendChild(tpm);
    s=3;
    } else if(m == “onclick”) {
    tE.onclick = a[l+1];
    s=2;
    } else {
    tE.setAttribute(a[l], a[l+1]);
    s=2;
    }
    l+=s;
    }
    }
    return tE; //returns the element
    }

    function crTN() { //creates a textNode from the first argument.
    var tT = arguments[0]; //temporary text
    var tTN = document.createTextNode(tT); //temporary text node
    return tTN;
    }

    I made some test cases for it (using another modified delicious playtagger)… I’m having trouble with the onclick attribute, I think it might be a mistake of mine… Anyhow, I’m looking for an argument called ‘onclick’, and I’m inferring the next argument to be a function… so that I can then use the onclick method and pass it a function. Here is the content creation function.

    function cc() { //create content
    var id0 = document.getElementById(“image”); //where I’m going to place some of the nodes
    var id1 = document.getElementById(“text”);
    var id2 = document.getElementById(“links”);
    //Here is an image
    var img = crEl(“img”,”src”,”http://rolandog.com/wp-content/r_g.png”,”alt”,”rolandog.com”,”width”,”80″,”height”,”15″);
    //The following test appends through three different methods an image…
    id0.appendChild(img); //works
    document.getElementById(“image”).appendChild(img); //works
    document.getElementById(“image”).appendChild(crEl(“img”,”src”,”http://rolandog.com/wp-content/r_g.png”,”alt”,”rolandog.com”,”width”,”80″,”height”,”15″)); //fails
    //Now we’re gonna try some methods of placing text.
    var bld = crEl(“strong”,”tn”,”I like toast… “); //creating bold text
    var par = crEl(“p”,”tn”,”Mmm, toast.”,bld) //a paragraph that should have a text node… if it was detected and that appends the bold text
    //Now I’ll append it
    id1.appendChild(par);
    //Here is the source of a document and it will be displayed as text/plain
    var src = crEl(“object”,”type”,”text/plain”,”data”,”http://rolandog.com/sounds/playtagger.html”); //source for the playtagger.html
    //now I’ll place it as a child in the id1.
    id1.appendChild(src); //maybe I can create fractal documents??
    //Now for the links…
    var link = crEl(“a”,”href”,”#”,”onclick”,alert(‘Hello dudes’),”tn”,”Hello world.”);
    id2.appendChild(link); //fails in IE… but it does work… the onclick attribute requires its value to be a function
    }

    I did however upload the tests with my latests modifications for object creation in xhtml and html

    Though I think I’ll head in the direction you’ve pointed. I’m currently in my polymer chemistry lab, but when I get home I’ll give your functions a try… they seem much better. I dunno if I have to leave a statement to declare my code as open source or GPL, but yeah… it is free, and anyone that stops by can pitch in and ‘steal’ it or improve it.

  • First of all, a correction on my first comment. The last line should be:

    mp3builder.attachTo(Delicious.Mp3.player);

    Just a tip on your onclick handler. Where you have:

    var link = crEl(”a”,”href”,”#”,”onclick”,alert(’Hello dudes’),”tn”,”Hello world.”);

    you are calling the alert function and passing its result into the function. Instead, you should pass in a reference to a function. You might do this:

    var link = crEl(”a”,”href”,”#”,”onclick”,function() { alert(’Hello dudes’) },”tn”,”Hello world.”);

    Or this (note that there are no parentheses after showAlert):

    function showAlert() {
    alert(’Hello dudes’);
    }
    var link = crEl(”a”,”href”,”#”,”onclick”,showAlert,”tn”,”Hello world.”);

    There is yet another approach that I considered presenting this morning. Essentially, instead of setting attributes, it sets properties on the JavaScript object just as you do for the onclick handler. For example:

    function crEl(tagName, properties) {
    function crE(tagName) { /*…*/ }

    function setObjectProperties(object, properties) {
    for (var propertyName in properties) {
    if (typeof properties[propertyName] === “object”) {
    // do a deep copy of objects; recurse here
    setObjectProperties(object[propertyName], properties[object]);
    }
    else if (tagName == “param”) {
    element["name"] = propertyName;
    element["value"] = properties[propertyName];
    }
    else {
    element[propertyName] = properties[propertyName];
    }
    }
    }

    tagName = tagName.toLowerCase();
    var element = crE(tagName); //temporary element
    setObjectProperties(object, properties);
    return element;
    }

    You might call it like this:

    function showAlert() {
    alert(’Hello dudes’);
    }
    var my_div = crElement(“div”, { onclick: showAlert, style: { backgroundColor: “#FF0000″ } });

    Let me know if more explanation would be helpful. I’m interested in seeing what you come up with.

  • [...] But, thanks to the great and insightful comments of Matthias Miller, I came up with this interesting piece of code: [...]

Post a Comment

Your email is never shared. Required fields are marked *