Understanding the IE DOM

Many developers I know don’t like working with IE because it is so “incompatible” with Firefox and Safari.  This is true, but what most people don’t know is that 90% of IE’s problems are actually isolated to just one part of it’s JavaScript implementation and if you work with that one part correctly, you can largely avoid the potholes that make cross-browser such a pain.

A Tail of Two Doms

The DOM is the in-memory representation of an HTML document.  Most people know the DOM from its familiar JavaScript API like insertBefore(), removeChild(), cloneNode() and so on.  But actually, the DOM is much more than that.  Whenever a web browser reads in an HTML document, it builds an in-memory representation of that document — a graph of objects called the “Document Object Tree”, but which people usually refer to as the “DOM”*.

Now the real DOM used by wedding browsers is not really JavaScript; it’s native code, written in C++.  This makes the browser very fast and parsing and displaying HTML but it presents a problem for the JavaScript.  How do JavaScript scripts get access to the objects in the DOM?  It turns out that IE and FireFox/Safari/Opera diverge significantly on this point and it is the single biggest cause of browser incompatibilities that I know.

The Peer Model

Intentional or not, both Webkit and Firefox as I understand handle this problem in a similar way.**  They create, in effect, a parallel tree of semi-pure JavaScript objects that match exactly the C++ DOM objects used to render and display the page.  The figure below shows how this works. (Using my handy Skitch whiteboard…)

(Interesting aside, btw, most browsers actually have a third parallel tree of peer objects called Display Tree or something like that.  This is a set of native objects written in C++ that actually handle drawing the DOM objects in your window.  A web page is really much more sophisticated than it looks!)

It turns out that the peer model for JavaScript is actually a very nice one because the DOM objects you see when you write JavaScript are real JavaScript objects.  They have prototypes, inherit methods, throw exceptions, and can even have their properties overridden.  This makes it easy to do nifty tricks like enhancing the root HTMLElement class to add the $() method like jQuery and Prototype do.  It also means that DOM objects tend to have largely the same behavior as other objects so you can reliably predict how they will behave in most situations.

The Wrapper Model

IE, on the other hand, takes a much different approach to exposing the DOM in JavaScript.  DOM objects in IE are essentially “wrapped” by a thin veneer in the JScript engine that makes it possible for you to query and set properties and call methods on these native C++ objects through script.  The figure below shows the difference with this model.

Now, actually, wrapping the DOM elements like this in IE does have its advantages.  In some ways you are much closer to the metal of the browser so to speak and therefore able to exert a bit more control.  The dreaded hasLayout property for example exposes some good internal information about how IE will treat the rendering of an element that would be useful in other browsers when trying to deal with complex layouts.  (The programming by side-effects model required to manipulate this property, of course, is another matter.)

On the downside, however, wrapping DOM elements makes IE subject to the effect of leaky abstractions.  Since DOM objects are not real JavaScript elements, sometimes they don’t behave just like you expect.  In fact, if you’ve ever hit one of these doozies, you’ve been bitten by the DOM-Wrapper IE bug:

 

  • You set a property on a DOM object and that property mysteriously changes to some other value; even if it is not part of the DOM’s published API.
  • You tried to modify the prototype of the DOM element (i.e. HTMLElement) and discover it doesn’t show up on existing DOM objects.
  • You added a method to the Function prototype, yet certain functions on DOM objects seem not to have them.
  • You try to iterate through the key/value pairs on a DOM element and certain keys you expect to see are not included (actually this one can happen in FF at times because of their own leaky-abstractions.)
  • You store a JS objects in some property on a DOM element and now it leaks when you leave the page. Yep, the dreaded IE memory leaks are caused by wrapping behavior in IE.

 

The list goes on and on.  Weird, seemingly unexplainable behavior suddenly makes sense when you realize the object you are working with is not JavaScript, even though it pretends to be so; it is a native C++ object only posing as one instead.

How to Program IE with Less Pain and More Joy

The models used by Firefox/Webkit and IE are plainly very different when it comes to the DOM.  How can we use this power to our advantage?  The answer is quite simple:  treat all DOM objects like foreign objects.  Forget that they are JavaScript in Firefox and Safari.  Don’t add properties or methods to DOM elements.  Don’t fiddle with their prototype definitions.  Only work with their published APIs.

Use the best practice followed by the browsers and create a peer tree of pure JavaScript objects that can mange your DOM elements for you.  These view objects can have all of your special high-level methods for manipulating the DOM build in and manage the DOM objects in your tree for you.  Conveniently, this also creates a nice abstraction layer you can use to hide other browser differences as well.  

This is the strategy used by SproutCore Views.  Each SC.View manages a single DOM object and sometimes a few of its children.  They reimplement much of the API actually used by the DOM.  This way DOM objects remain pure DOM elements manipulated by a pure JS layer defined by the view.

Unfortunately, most JS libraries today follow exactly the opposite approach.  Almost all of them try to modify the root prototypes of elements to add their own methods.  This isn’t really possible to do in IE since DOM objects are not real JavaScript so instead these libraries will try to infect each element they come in contact with with their methods to try to simulate the effect instead.  This is not only slow but leads to a wide variety of bugs.

Less DOM, more JS

So there you have it.  There of course are a lot of other minor differences between how the JavaScript language in FireFox and Safari and JScript in IE work, but most of them are easy to paper over.  The real tricky bugs that keep you up all night, however, are often driven from the differences in the DOM models.  To solve this problem, simply treat your DOM objects like foreign objects.  Follow the published APIs on these objects but avoid adding your own properties or enhancements on top.   

Or use a framework that does this for you like SproutCore. ;-)

Either way, if you understand this difference you’ll find debugging IE is not only easier but sometimes it a little bit fun.  Like learning a foreign dialect of your own native tongue; a little weird but mostly harmless and interesting,

 

* OK, for the pedants out there, technically the Document Object Model, or DOM, is the API defined by the W3C that the Document Object Tree (DOT?) conforms to.  Most people I know however simply refer to the Document Object Tree as the DOM, so I’m going to stick with that convention here as well.

** Also for the pedants: I am aware that both FireFox and Safari do not strictly keep a “peer” tree of JavaScript objects for all DOM elements in page.  However, this is the direction they appear to be moving conceptually and from a developer’s standpoint it’s easier to keep this mental model in the head than to deal with the actual complexity of the situation that is FireFox and Safari both blend the peer-tree and the wrapper models to some extent, mostly for historical reasons.  Either way if you follow my suggestion and always 

 

SproutCore Session Time is Set for WWDC

As I have mentioned before, SproutCore will be a session at WWDC this year. But, Apple just posted the schedules of the sessions. Here is what I know from the WWDC site:

Friday, June 13: 9:00 AM - 10:15 AM in the Marina Room.

This session is being categorized as “iPhone and Mac” which makes sense because of the robust Safari browser that crosses all platforms.

How to write a fast (and readable) method in JavaScript

Good methods should read like a story.  They should have an introduction, a climax, and a conclusion.  Here is what a good method might look like:

/**
Returns the first content object in the receivers content array whose key
matches the passed value.  
*/
findContentForValue: function(key, value) {
//
  // INTRODUCTION:  Collect data you will need into local variables.
  // Any data you might reference repeatedly should be collected here (such
// as the array length)
// Normalize your data here when possible to make the rest of the method
// flow...
  var content = Array.from(this.get('content')) ;  // The source content array
  var loc = content.get('length') ;  // The current search location.
  var ret = null ;  // The return value (or null if none found)
//
// CLIMAX:  Do the operation.
  while(!ret && (--loc >= 0)) {
    var obj = content.objectAt(loc) ;
    if (obj.get(key) === value) ret = obj;
  }
//
// CONCLUSION:  Cleanup and return the values.
  return ret ;
}

The Introduction

The introduction is where you collect all of your variables and setup your context.  A solid introduction is important to a good method because it both makes your method easier to read and improves performance.  

By declaring the variables you will work with up front, you help make clear to readers of your code what data you expect to work with.  This will prepare them to understand the rest of your code as it comes along.  It is also helpful to put commented explanations here describing what each method will do.  This way as a reader tries to digest your code, they will have a glossary to refer to when they see an unfamiliar variable.

If readability alone weren’t enough, JavaScript actually rewards you for using this style by making your code faster as well.  Every time you reference a variable (’foo’), JavaScript will search up a “scope” chain to find the value.  This chain begins with your current function and terminates with the “window” object, where global values will go.  By bringing all of the values you will be working with into local variable, you help to short-circuit this process for most calls.

The Climax

This is the part of your code that actual does the work.  Perform some processing, search through some data, whatever.  This code should generally try to work mostly with variable declared in your introduction and it should avoid returning early.  Instead, you should store the return variable somewhere and save it for your conclusion.

I’ve instituted a no-early-return policy for many years and often get a lot of pushback on it; but there are some good reasons for it.  I contend that early returns are one of the biggest barriers to maintaining code over the long term for two reasons.  First, early returns make your code a lot harder to follow.  There is nothing more frustrating than adding a console log or tracing some code only to have it return early before it gets to the part you wanted to investigate.  Second, early returns make it hard to re-factor your code.  How many times have you had to readjust a method because you needed to add some extra step to the process but some earlier code tried to exit before it ever got there?

That said, there are some good reasons to do an early return and I do use them in code when it makes sense.  For example, if you use a cached value to determine if a method needs to run at all, then an early return is fine.  I often put this code into the introduction-portion of the method in fact so that it is obvious to everyone when a method might not run at all.  Second, sometimes your code will have so many exit conditions that not using early returns will make the code harder to read.  This kind of code usually means you need to refactor anyway, but if it is unavoidable, early returns are preferrable to unreadable code.

The Conclusion

Conclusions should be simple.  They should not contain critical logic to your method and they should wrap up any loose ends so a reader can clearly see what they will get back when the function exits.  If you need to notify anyone of property changes or perform any other general housekeeping like that, do it here as well.  You should clearly indicate when you go into your cleanup phase, though, through comments so the reader knows they should not expect to see more application logic there.

My Conclusion

So that’s how you write a good method.  Pretend it is a story.  Introduce your variables at the beginning, do something interesting in the middle and return at the end.  Avoid referencing variables that are not in a local scope and avoid early returns.  If you do this, your JavaScript code will not only be far more readable, but it will perform better too.

 

SproutCore 0.9.5

I just released SproutCore 0.9.5 today.  Get it by running:

sudo gem update sproutcore

This includes some major new features for both the build tools and the JavaScript framework.  Here is the list:

Build Tools

This release includes some major new functionality to support localization.

  • Now you can load any localized version of your project simply by visiting http://localhost:4020/appname/lang.  For example, to get the french version you might do:  http://localhost:4020/appname/fr.  This functionality was planned from the beginning but a few bugs kept it from functioning properly.  Now it should work for your project.
  • The build tools now support build-time localization.  To localize a string in your RHTML files, just use the ruby code loc(”KeyName”).  This works just like “KeyName”.loc() in the JavaScript world except that the key will be localized when your HTML file is built instead of later once the file is downloaded to the client.  Just like the JavaScript, this helper will pull its localization key from the strings.js file found in your .lproj directories.
  • In support of the build-time localization, the build tools now also support special build-only language keys.  Any loc key beginning with “@@” in your strings.js file will be removed when the strings.js file is built for your web browser.  This way you can add loc strings intended only for use at build time and they will not be sent to the web browser with the rest of the loc strings.
  • Along the lines of localization, the build tools now automatically set the String.preferredLanguage property in your JavaScript to the current active language.  This is different from String.browserLanguage, which returns the language that is auto-detected from the browser.  The JavaScript loc() method has been updated to use the strings from the preferred language instead of the browser language by default.
  • You can use the same URLs that will be used at production time to manually request resources and sc-server will build them for you.  This makes it easy to reference resources without using the static_url() helper.  This approach is not recommended but sometimes in is necessary for various reasons and now it will work.
  • The build tools no longer automatically merge .html files into the index.html file.  They only merge files ending in .rhtml and .html.erb.  This allows you to include static .html files if you need.

JavaScript Framework

The framework now supports a variety of new features related to localization and a delegate for the collection view that can give you a way to provide fine grained control over how the collection view will handle drag and drop, deletion, etc:

 

  • Collection View now supports selectOnMouseDown which can be turned off to provide better drag and drop behavior on SourceList.  SC.SourceListView now turns this off by default.
  • SC.CollectionView now supports selectAll (Ctrl+A)
  • SC.CollectionView now supports the delete key to remove items.  Also added delegate methods to the collection view delegate to give you control over how deletions happen.
  • SC.window can now properly capture the backspace key in Firefox.  To activate this feature you must declare SC.CAPTURE_BACKSPACE_KEY = YES in your core.js file.  Capturing the backspace key will prevent the browser from going to previous page when the user hits backspace, which can lead to data loss. To capture this key, SC.window will directly set the document.onkeypress handler.  
  • SC.GridView now supports dropping ON items.
  • SC.ListView now supports dropping ON items.
  • Removed the try/catch() that was placed around property notifiers.  This is not only faster but it will make it easier to debug these exceptions in Firebug and IE.
  • [FIX] SC.InlineTextFieldView was using the _frame property even though that is used by a parent class.  Changed to _optframe
  • Improved some documentation here and there.
  • [FIX] SC.View will recache its frames when isVisibleInWindow changes.  This will help to ensure we always have the correct dimensions when bringing views on and offscreen. — All unit tests now pass again.
  • Collection View now supports dropping items ON item views as well as between them. 
  • Collection Views now support a delegate object that can be used to control  drag and drop and selection behavior.  See mixins/collection_view_delegate.js  for a complete description of the new methods.
  • SC.ArrayController now supports the useControllersForContent property.  If set to YES, then getting an objectAt() will return a controller for the value instead of the value itself.  This is useful for those times you are using an array controller to manage a set of objects you want to control.  Previously this feature was always used by array controllers and could not be disabled.  This is now off by default.
  • [FIX] SC.ArrayController and SC.ObjectController now will properly observe their own content, even when the content is set on init.

 

 

Other

Miscellaneous improvements to the packaging system and sample apps.

 

 

  • Added mongrel as a required dependency of SproutCore.  If you have something like thin installed this is technically not required, but several people were experiencing trouble installing the gem.
  • Lots of Safari-specific features for Photos just to demo some of its capabilities.  Client-side storage support is also provided but currently does not save changes you make.
  • Initial changes to SampleControls to add a form-view demo.  None of the controls are wired up yet.
  • Improves Photos sample to include support for adding/deleting albums and drag and drop into albums.

 

 

 

 

 

 

 

SproutCore 0.9.4 ReleasedS

New release of the ruby gem today.  This gem includes a few important fixes for the SproutCore build tools as well as some enhancements to the JS framework.  The specific list is included below.

As usual, you should update the SproutCore gem if you want to get the latest build tools.  You can always get the very latest JavaScript code by running ’sproutcore freeze:edge’ from within your SproutCore project.

SproutCore 0.9.4 Changes

  • [FIX] Build system now generates index.html files for client bundles, even if they do not include .rhtml resources.
  • [FIX] Typos in the readme docs for the client generator.
  • Added automaticOutletFor() that will cause a view to be configured automatically instead of forcing you to use an outlets array.  Useful for when you are manually constructing your views.
  • Initial changes required to eventually implement improved observer notification system.
  • Improved documentation for a variety of classes
  • [FIX] Only fixtures ending in “.js” will now be loaded.  This allows you to include fixtures named .json or whatever and actually load them via XHR.
  • [FIX] The build system now ignored any rhtml files that does not appear in an lproj directory since this is where templates are stored.

 

Another Reason Not to Clone Desktop Frameworks

It’s tempting to claim desktop frameworks since they come with 20 years of experience built in.  But 20 years of experience also means 20 years of baggage. When Apple created the UIKit (AppKit for the iPhone), even they didn’t clone Cocoa exactly:

One of the biggest differences is the extensive use of properties throughout the UIKit class declarations. Properties were introduced to Mac OS X in version 10.5 and thus came along after the creation of many classes in the AppKit framework. Rather than simply mimic the same getter and setter methods in AppKit, UIKit employs properties as a way to simplify the class interfaces. For information about properties, see Properties in The Objective-C 2.0 Programming Language.

From iPhone Getting Started Docs