The DOM is a Mess
John Resig
http://ejohn.org/ - http://twitter.com/jeresig/
A Tour of the DOM
A messy DOM
✦
Writing Cross-Browser Code
✦
Common Features
✦
✦ CSS Selector Engine
✦ DOM Modification
✦ Events
Messy
Nearly every DOM method is broken in
✦
some way, in some browser.
Some old:
✦
✦ getElementById
✦ getElementsByTagName
Some new:
✦
✦ getElementsByClassName
✦ querySelectorAll
getElementById
Likely the most commonly used DOM
✦
method
A couple weird bits:
✦
✦ IE and older versions of Opera returning
elements with a name == id
✦ Does not easily work in XML
documents
getElementsByTagName
Likely tied for most-commonly-used
✦
DOM method
Riddled with bugs in IE:
✦
✦ “*” returns no elements in IE 5.5
✦ “*” returns no elements on <object>
elements in IE 7
✦ .length gets overwritten in IE if an
element with an ID=”length” is found
getElementsByClassName
Landed in Firefox 3, Safari 3, Opera 9.6
✦
A few knotty issues:
✦
✦ HTMLElement.prototype
.getElementsByClassName
couldn’t be overwritten in Firefox
✦ Opera doesn’t match a second-specified
class (e.g. class=”a b”, b isn’t found)
querySelectorAll
Find DOM elements using CSS selectors
✦
In Firefox 3.1, Safari 3.1, Opera 10, IE 8
✦
Birthing pains:
✦
✦ Doesn’t exist in quirks mode, in IE 8
✦ Safari 3.1 had memory out of bounds
problems
✦ Safari 3.2 can’t match uppercase
characters in quirks mode
✦ #id doesn’t match in XML documents
Moral
If there’s a DOM method, there’s probably
✦
a problem with it somewhere, in some
capacity.
Browser Support Grid
IE Firefox Safari Opera Chrome
Previous 6.0 2.0 3.0 9.5
Current 7.0 3.0 3.2 9.6 Current
Next 8.0 3.1 4.0 10.0
jQuery Browser Support
Browser Support Grid
IE Firefox Safari Opera Chrome
Previous 3.0 9.5
6.0 2.0
Current 7.0 3.0 3.2 9.6 Current
Next 3.1 4.0
8.0 10.0
jQuery 1.3 Browser Support
Know Your Enemies
Points of Concern for JavaScript Code
Browser Bugs
Regressions
Missing Features JavaScript Code
Bug Fixes
External
Code, Markup
Know Your Enemies
Points of Concern for JavaScript Code
Browser Bugs
Regressions
Missing Features JavaScript Code
Bug Fixes
External
Code, Markup
Browser Bugs
Generally your primary concern
✦
Your defense is a good test suite
✦
✦ Prevent library regressions
✦ Analyze upcoming browser releases
Your offense is feature simulation
✦
What is a bug?
✦
✦ Is unspecified, undocumented, behavior
capable of being buggy?
Know Your Enemies
Points of Concern for JavaScript Code
Browser Bugs
Regressions
Missing Features JavaScript Code
Bug Fixes
External
Code, Markup
External Code
Making your code resistant to any
✦
environment
✦ Found through trial and error
✦ Integrate into your test suite
✦ Other libraries
✦ Strange code uses
Environment Testing
100% Passing:
✦
✦ Standards Mode
✦ Quirks Mode
✦ Inline with Prototype + Scriptaculous
✦ Inline with MooTools
Work in Progress:
✦
✦ XHTML w/ correct mimetype
✦ With Object.prototype
✦ In XUL (Firefox Extensions)
✦ In Rhino (with Env.js)
Order of Stylesheets
Putting stylesheets before code guarantees
✦
that they’ll load before the code runs.
Putting them after can create an
✦
indeterminate situation.
Pollution
Make sure your code doesn’t break
✦
outside code
✦ Use strict code namespacing
✦ Don’t extend outside objects, elements
Bad:
✦
✦ Introducing global variables
✦ Extending native objects (Array, Object)
✦ Extending DOM natives
Know Your Enemies
Points of Concern for JavaScript Code
Browser Bugs
Regressions
Missing Features JavaScript Code
Bug Fixes
External
Code, Markup
Missing Features
Typically older browsers missing specific
✦
features
Optimal solution is to gracefully
✦
degrade
✦ Fall back to a simplified page
Can’t make assumptions about
✦
browsers that you can’t support
✦ If it’s impossible to test them, you must
provide a graceful fallback
Object detection works well here.
✦
Object Detection
Check to see if an object or property
✦
exists
Useful for detecting an APIs existence
✦
Doesn’t test the compatibility of an API
✦
✦ Bugs can still exist - need to test
those separately with feature simulation
Fallback
Figure out a way to reduce the
✦
experience
Opt to not execute any JavaScript
✦
✦ Guarantee no partial API
✦ (e.g. DOM traversal, but no Events)
Redirect to another page, or just work
✦
unobtrusively
Working on a ready() fallback for jQuery
✦
Know Your Enemies
Points of Concern for JavaScript Code
Browser Bugs
Regressions
Missing Features JavaScript Code
Bug Fixes
External
Code, Markup
Bug Fixes
Don’t make assumptions about browser
✦
bugs.
✦ Assuming that a browser will always
have a bug is foolhardy
✦ You will become susceptible to fixes
✦ Browsers will become less inclined to fix
bugs
Look to standards to make decisions about
✦
what are bugs
Failed Bug Fix in FF 3
// Shouldn't work
var node = documentA.createElement(quot;divquot;);
documentB.documentElement.appendChild( node );
// Proper way
var node = documentA.createElement(quot;divquot;);
documentB.adoptNode( node );
documentB.documentElement.appendChild( node );
Feature Simulation
More advanced than object detection
✦
Make sure an API works as advertised
✦
Able to capture bug fixes gracefully
✦
Know Your Enemies
Points of Concern for JavaScript Code
Browser Bugs
Regressions
Missing Features JavaScript Code
Bug Fixes
External
Code, Markup
Regressions
Removing or changing unspecified APIs
✦
Object detection helps here
✦
Monitor upcoming browser releases
✦
✦ All vendors provide access to beta
releases
✦ Diligence!
Example: IE 7 introduced
✦
XMLHttpRequest with file:// bug
Test Suite Integration
✦
Untestable Problems
Has an event handler been bound?
✦
Will an event fire?
✦
Do CSS properties like color or opacity
✦
actually affect the display?
Problems that cause a browser crash.
✦
Problems that cause an incongruous API.
✦
Impractical to Test
Performance-related issues
✦
Determining if Ajax requests will work
✦
Battle of Assumptions
Cross-browser development is all about
✦
reducing the number of assumptions
No assumptions indicates perfect code
✦
✦ Unfortunately that’s an unobtainable
goal
✦ Prohibitively expensive to write
Have to draw a line at some point
✦
DOM Traversal
Many methods of DOM traversal
✦
One unanimous solution:
✦
✦ CSS Selector Engine
Traditional DOM
getElementsByTagName
✦
getElementById
✦
getElementsByClassName
✦
✦ in FF3, Safari 3, Opera 9.6
.children
✦
✦ only returns elements (in all, and FF 3.1)
getElementsByName
✦
.all[id]
✦
✦ Match multiple elements by ID
Top-Down CSS Selector
Traditional style of traversal
✦
✦ Used by all major libraries
Work from left-to-right
✦
“div p”
✦
✦ Find all divs, find paragraphs inside
Requires a lot of result merging
✦
And removal of duplicates
✦
function find(selector, root){
root = root || document;
var parts = selector.split(quot; quot;),
query = parts[0],
rest = parts.slice(1).join(quot; quot;),
elems = root.getElementsByTagName( query ),
results = [];
for ( var i = 0; i < elems.length; i++ ) {
if ( rest ) {
results = results.concat( find(rest, elems[i]) );
} else {
results.push( elems[i] );
}
}
return results;
}
(function(){
var run = 0;
this.unique = function( array ) {
var ret = [];
run++;
for ( var i = 0, length = array.length; i < length; i++ ) {
var elem = array[ i ];
if ( elem.uniqueID !== run ) {
elem.uniqueID = run;
ret.push( array[ i ] );
}
}
return ret;
};
})();
Bottom-Up
Work from right-to-left
✦
✦ (How CSS Engines work in browsers.)
“div p”
✦
✦ Find all paragraphs, see if they have a div
ancestor, etc.
Fast for specific queries
✦
✦ “div #foo”
Deep queries get slow (in comparison)
✦
✦ “#foo p”
Bottom-Up
Some nice features:
✦
✦ Only one DOM query
✦ No merge/unique required (except for
“div, span”)
✦ Everything is a process of filtering
function find(selector, root){
root = root || document;
var parts = selector.split(quot; quot;),
query = parts[parts.length - 1],
rest = parts.slice(0,-1).join(quot;quot;).toUpperCase(),
elems = root.getElementsByTagName( query ),
results = [];
for ( var i = 0; i < elems.length; i++ ) {
if ( rest ) {
var parent = elems[i].parentNode;
while ( parent && parent.nodeName != rest ) {
parent = parent.parentNode;
}
if ( parent ) {
results.push( elems[i] );
}
} else {
results.push( elems[i] );
}
}
return results;
}
CSS to XPath
Browsers provide XPath functionality
✦
Collect elements from a document
✦
Works in all browsers
✦
✦ In IE it only works on HTML
documents
Fast for a number of selectors (.class, “div
✦
div div”)
✦ Slow for some others: #id
Currently used by Dojo and Prototype
✦
Goal CSS 3 XPath
All Elements * //*
All P Elements p //p
All Child Elements p>* //p/*
Element By ID #foo //*[@id='foo']
//*[contains(concat(quot; quot;,
Element By Class .foo
@class, quot; quot;),quot; foo quot;)]
Element With Attribute *[title] //*[@title]
First Child of All P p > *:first-child //p/*[0]
All P with an A descendant //p[a]
Not possible
Next Element p+* //p/following-sibling::*[0]
querySelectorAll
The Selectors API spec from the W3C
✦
Two methods:
✦
✦ querySelector (first element)
✦ querySelectorAll (all elements)
Works on:
✦
✦ document
✦ elements
✦ DocumentFragments
Implemented in:
✦
✦ Firefox 3.1, Safari 3, Opera 10, IE 8
Injecting HTML
HTML 5:
✦
insertAdjacentHTML
Already in IE, dicey support, at best
✦
What can we use instead?
✦
✦ We must generate our own HTML
injection
✦ Use innerHTML to generate a DOM
Element Mappings
option and optgroup need to be contained in a
✦
<select multiple=quot;multiplequot;>...</select>
legend need to be contained in a
✦
<fieldset>...</fieldset>
thead, tbody, tfoot, colgroup, and caption need to be
✦
contained in a <table>...</table>
tr need to be in a
✦
<table><thead>...</thead></table>,
<table><tbody>...</tbody></table>, or a
<table><tfoot>...</tfoot></table>
td and th need to be in a
✦
<table><tbody><tr>...</tr></tbody></table>
col in a
✦
<table><tbody></tbody><colgroup>...</
colgroup></table>
link and script need to be in a
✦
div<div>...</div>
DocumentFragment
Fragments can collect nodes
✦
Can be appended or cloned in bulk
✦
Super-fast (2-3x faster than normal)
✦
Inline Script Execution
.append(“<script>var foo = 5;</script>”);
✦
Must execute scripts globally
✦
window.execScript() (for IE)
✦
eval.call( window, “var foo = 5;” );
✦
Cross-browser way is to build a script
✦
element then inject it
✦ Executes globally
function globalEval( data ) {
data = data.replace(/^s+|s+$/g, quot;quot;);
if ( data ) {
var head = document.getElementsByTagName(quot;headquot;)[0] || document.documentElement,
script = document.createElement(quot;scriptquot;);
script.type = quot;text/javascriptquot;;
script.text = data;
head.insertBefore( script, head.firstChild );
head.removeChild( script );
}
}
Removing Elements
Have to clean up bound events
✦
✦ IE memory leaks
Easy to do if it’s managed.
✦
Events
Three big problems with Events:
✦
✦ Memory Leaks (in IE)
✦ Maintaining ‘this’ (in IE)
✦ Fixing event object inconsistencies
Leaks
Internet Explorer 6 leaks horribly
✦
✦ Other IEs still leak, not so badly
Attaching functions (that have a closure to
✦
another node) as properties
Makes a leak:
✦
elem.test = function(){
anotherElem.className = “foo”;
};
‘this’
Users like having their ‘this’ refer to the
✦
target element
Not the case in IE
✦
What’s the solution?
✦
Single Handler
Bind a single handler to an event
✦
Call each bound function individually
✦
Can fix ‘this’ and event object
✦
How do we store the bound functions in a
✦
way that won’t leak?
Central Data Store
Store all bound event handlers in a central
✦
object
Link elements to handlers
✦
Keep good separation
✦
One data store per library instance
✦
Easy to manipulate later
✦
✦ Trigger individual handlers
✦ Easy to remove again, later
Central Data Store
Events Data
Element function click(){}
#43
function mouseover(){}
Element
Data
Element
#67
Element
Events
Element function click(){}
#22
function click(){}
Data Store
Unique Element ID
The structure must hook to an element
✦
Elements don’t have unique IDs
✦
Must generate them and manage them
✦
jQuery.data( elem, “events” );
✦
Unique attribute name:
✦
✦ elem.jQuery123456789 = 45;
✦ Prevents collisions with other libraries
We can store all sorts of data in here
✦