Copying DOM Element Properties and Handling Browser Differences
by Jason S. Kerchner on May 20, 2009 (filed in Development)
I’ve made another slight, but significant, change to the way in which DOM elements are created by the View. Going back to my original approach of building DOM elements from HTML strings, it made sense to refer directly to HTML and CSS attributes. But now that I’m creating DOM elements directly through document.createElement, there is no need to use HTML attributes, and I can just use the DOM element properties.
The first change, then, was to rename attribs (attributes) in all DOM element rendering functions to props (properties).
Next, I updated the view’s createElement function (in View.js, not document.createElement). This is the original version.
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; } |
In the new version (below) note that the second argument has been changed from attribs to props. It now expects DOM element property names, not HTML attribute names. This means I need to use names like className instead of class, and htmlFor instead of for. Since this function no longer has to do attribute name to property name conversions (since I’m now passing it property names and not attribute names), lines 6 through 10 (lines 6 through 17 in the original version) have been updated to use the copy function (in Globals.js, part of the JavaScript Extensions). I’ll explain that third argument to the copy function, the anonymous function with the data argument, in a moment.
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 | createElement: function(tag, props, elements) { // Create the element var elem = document.createElement(tag); // Apply the properties to the element, correct property names where necessary (Globals.js) window.copy(props, elem, function(data) { if (data.prop == 'cssFloat' && !elem.cssFloat) data.prop = 'styleFloat'; }); // 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; } |
There are some special cases to handle when copying properties, so I needed to alter the copy function, too. These special cases are due to browser differences. Like the fact that all browsers use the style.cssFloat property, but IE uses style.styleFloat. If I’m going to use the copy function, a change is required to handle these special scenarios. But since the copy function is an extension, I want to keep the change generic.
The copy function originally took two arguments. The first was the source object, and the second was the destination object. Now, there is a third argument. This argument is a function that will be called for every property in the source object just before it is added to the destination object. This allows the property name, value or data type to be changed before it is copied to the destination object.
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 | window.copy = function(src, dest, fn) { if (!dest) dest = {}; for (var prop in src) { var value = src[prop]; var type = window.typeOf(value); if (fn) { var data = { prop: prop, value: value, type: type }; fn(data); prop = data.prop; value = data.value; type = data.type; } if (type == 'object') { if (!dest[prop]) dest[prop] = {}; window.copy(value, dest[prop]); } else if (type == 'date') { dest[prop] = new Date(value.getTime()); } else if (type == 'array') { dest[prop] = value.slice(); } else { dest[prop] = value; } }; return dest; }; |
Lines 8 through 14 check if a callback function (fn) was passed in and, if so, creates an object that has three properties: the property name, value and data type. This data object is then passed to the callback function. Why use an object, rather than passing the values in as three separate arguments? Because objects are passed by reference. This means that the callback function data object points to the same data object as the copy function, so changing a value in the callback function also changes that value in the copy function (since both are pointing to the same data object). If I was passing in three separate arguments, they would be local only to the callback function, so changing their values would not change them in the copy function.
If you look back at the modified code for the View’s createElement function above, you’ll see that the callback function checks to see if the property is cssFloat and, if it is, checks to see if that property exists in the DOM element. If it doesn’t, it means we are running in IE and must use the styleFloat property name. In that case, it changes the property name.
So adding the callback function to copy allows me to change property names to suit different browsers, but still keep the copy function generic.
The other advantage to using the copy function that that it handles nested objects. So, specifying styles can be done with a props object like the following passed to the View’s createElement function. Note that these are DOM element property names, not CSS attribute names.
1 2 3 4 5 6 7 8 | { id: 'someId', style: { display: 'block', backgroundColor: '#cccccc', cssFloat: 'right' } } |
So, not any major changes here, but they are significant from the perspective of using the View. It also simplifies the View’s createElement function a little more, but still keeps it flexible.
Leave a Reply