dean.edwards.name/weblog/


2009/12/17

getElementsByTagName()

It turns out that document fragments do not implement the getElementsByTagName() method. But they do implement the Selectors API methods: querySelector/querySelectorAll.

Nobody cares about this except for people that write JavaScript selector engines. If you are one of those people then keep reading.

I can code around the absence of a lot of DOM methods but getElementsByTagName() seemed impossible to do without. So I had to fake it!

JavaScript performance

JavaScript is a nice enough scripting language but it is still a scripting language.

For high performance loops you should try to avoid the following:

  1. try/catch blocks
  2. function/method calls (even on native objects)
  3. accessing DOM properties (or anything that involves calling a complex getter/setter)

Please note that this is an ordered list. :)

The code

With all that in mind I wrote this simple implementation of getElementsByTagName() which I hope is fast enough for those edge cases where users execute DOM queries on document fragments:

function getElementsByTagName(node, tagName) {
  var elements = [], i = 0, anyTag = tagName === "*", next = node.firstChild;
  while ((node = next)) {
    if (anyTag ? node.nodeType === 1 : node.nodeName === tagName) elements[i++] = node;
    next = node.firstChild || node.nextSibling;
    while (!next && (node = node.parentNode)) next = node.nextSibling;
  }
  return elements;
};

Update: I’ve amended the original code based on the comments below.

Some things to consider:

  • Document fragments cannot have a parent node.
  • This method will only be used to query document fragments, so you can assume that the context node will not be an element.

Comments (22)

2009/10/22

Convert any colour value to hex in MSIE

The following function will convert any colour value (rgb, named colours, etc) to the hex equivalent in MSIE:

function toHex(color) {
  var body  = createPopup().document.body,
      range = body.createTextRange();
  body.style.color = color;
  var value = range.queryCommandValue("ForeColor");
  value = ((value & 0x0000ff) << 16) | (value & 0x00ff00) | ((value & 0xff0000) >>> 16);
  value = value.toString(16);
  return "#000000".slice(0, 7 - value.length) + value;
};

For other browsers you can use getComputedStyle() so that is already a solved problem.

Note: this method has been around for a while. I posted this adaptation because it does not suffer some of the drawbacks of the original.

If you are concerned about performance then you can always re-use the popup and range objects.

Comments (26)

2009/04/23

Back in the game

After some wrangling I’m back in control of the dean.edwards.name domain.

Comments (22)

2009/04/02

dean.edwards.name

Some of you may have noticed that my site is now hosted on a new domain:

http://deanedwards.me.uk/

The old domain got squatted. I whined about it on reddit at the time.

The squatter is currently hosting my copyrighted material. I wrote to the registrar to complain but haven’t received a reply (I should probably contact the hosting service too).

So, if you could update your backlinks/bookmarks etc that would be cool. I know that it’s my fault though. :-(

The old domain was actually worth quite a lot to me. It provided my email address which was the way that people contacted me for work. That email address has provided all of my work for the last five years. There’s no way I can replace it. I’ll just have to start again.

Update: I’m now in contact with the new owner of the domain and it looks like it may be returned to me. Fingers crossed!

Comments (19)

2009/03/24

Callbacks vs Events

Most of the major JavaScript libraries claim to support custom events in one form or another. For example, jQuery, YUI and Dojo all support a custom “document ready” event. However, the implementation of these custom events is always some form of callback system.

A callback system works by storing event handlers in an array. When the underlying event is detected the dispatch system loops through the array calling the callback functions in turn. So what’s wrong with that? Before I answer that, let’s look at some code.

Here is some simple code that uses the DOMContentLoaded event to perform two separate initialisations:

document.addEventListener("DOMContentLoaded", function() {
  console.log("Init: 1");
  DOES_NOT_EXIST++; // this will throw an error
}, false);

document.addEventListener("DOMContentLoaded", function() {
  console.log("Init: 2");
}, false);

What do you expect to see in the console when the document is loaded?

Well, you should see this (or something like it):

Init: 1

Error: DOES_NOT_EXIST is not defined

Init: 2

The point is, both functions are executed. You get an error in the first function but it does not stop the second function from executing.

The Problem

Now let’s look at some code based on a callback system. I’ll pick on jQuery because it’s the most popular:

$(document).ready(function() {
  console.log("Init: 1");
  DOES_NOT_EXIST++; // this will throw an error
});

$(document).ready(function() {
  console.log("Init: 2");
});

What do we see in the console?

Init: 1

Error: DOES_NOT_EXIST is not defined

The problem is clear. Callback systems are brittle. If any of the callback functions throws an error then the subsequent callbacks are not executed. In reality, this means that a poorly written plugin can prevent other plugins from initialising.

Dojo suffers exactly the same problem as jQuery. The YUI library takes a slightly different approach. It wraps a try/catch around its dispatch mechanism. The downside is that your errors occur silently:

YAHOO.util.Event.onDOMReady(function() {
  console.log("Init: 1");
  DOES_NOT_EXIST++; // this will throw an error
});

YAHOO.util.Event.onDOMReady(function() {
  console.log("Init: 2");
});

Produces:

Init: 1

Init: 2

Perfect initialisation! Nothing to worry about here! Except for the error that you don’t see.

So what’s the solution?

The Solution

The solution, is to use a hybrid of a callback system and real event dispatch. We can trigger a fake event and from within that event, run the callback function. Each event handler has its own execution context. If an error occurs in our fake event then it won’t affect our callback system.

That sounds a bit complicated. I’ll illustrate with some code.

var currentHandler;

if (document.addEventListener) {
  document.addEventListener("fakeEvents", function() {
    // execute the callback
    currentHandler();
  }, false);

  var dispatchFakeEvent = function() {
    var fakeEvent = document.createEvent("UIEvents");
    fakeEvent.initEvent("fakeEvents", false, false);
    document.dispatchEvent(fakeEvent);
  };
} else { // MSIE

  // I'll show this code later
}

var onLoadHandlers = [];
function addOnLoad(handler) {
  onLoadHandlers.push(handler);
};

onload = function() {
  for (var i = 0; i < onLoadHandlers.length; i++) {
    currentHandler = onLoadHandlers[i];
    dispatchFakeEvent();
  }
};

Now we’ll use the code above to attach our two troublesome event handlers:

addOnLoad(function() {
  console.log("Init: 1");
  DOES_NOT_EXIST++; // this will throw an error
});

addOnLoad(function() {
  console.log("Init: 2");
});

OK. Let’s run the code above and see what we get:

Init: 1

Error: DOES_NOT_EXIST is not defined

Init: 2

Perfect! Just what we want. Both event handlers are executed and we also get a message telling us about the error in the first handler. Great!

But what about Internet Explorer I hear you ask (I have good hearing). MSIE does not support the standard event dispatch system. It has its own method; fireEvent but that only works with real events (e.g. click).

Rather than explain the solution in words, here is the code:

var currentHandler;

if (document.addEventListener) {

  // We've seen this code already

} else if (document.attachEvent) { // MSIE

  document.documentElement.fakeEvents = 0; // an expando property

  document.documentElement.attachEvent("onpropertychange", function(event) {
    if (event.propertyName == "fakeEvents") {
      // execute the callback
      currentHandler();
    }
  });

  dispatchFakeEvent = function(handler) {
    // fire the propertychange event
    document.documentElement.fakeEvents++;
  };
}

A similar approach except that we use the proprietary propertychange event as the trigger.

Summary

I’ve shown a very simple example of how to use the uderlying event system to fire custom events. Library authors should be capable of seeing how this can be extended to fully support cross-browser custom events.

Update

Some commenters have suggested using setTimeout. Here is my response to that:

For this particular example, a timer will work fine. This is just an example to illustrate the technique. The real usefulness of this is for other custom events. Most libraries implement custom events using a callback system. As I illustrated, callback systems are brittle. Dispatching events with timers will work to a degree but it is not how a real event system works. In a real system, events are dispatched sequentially. There are other concerns, like cancelling an event and stopping the event from bubbling. This would be impossible with timers.

The important thing is that I’ve demonstrated a technique for wrapping a callback system in a real event dispatch system. That gives you the ability to fire real custom events in MSIE. If you are building an event system based on event delegation then this technique may also be interesting to you.

Update 2

It seems that Prototype uses an almost identical trick to fire custom events in MSIE:

http://andrewdupont.net/2009/03/24/link-dean-edwards/

Comments (59)

2008/03/22

Two steps forward one step back

Safari 3.1 fails the Acid2 test:

http://bugs.webkit.org/show_bug.cgi?id=13693

Comments (7)

2008/01/23

Quotes

..we worked together with The Web Standards Project (in the WaSP-Microsoft Task Force) on this problem. I can’t give them enough credit for this work;

Chris Wilson (Microsoft)

Although members of the WaSP Microsoft Task Force were very much involved in this proposal, it is important to re-emphasise that this proposal is not one that every member of the Web Standards Project necessarily backs by default.

Drew McLellan (WaSP co-lead)

I, for one, hope other browser vendors join Microsoft in implementing this functionality.

Aaron Gustafson (WaSP/MSTF)

I won’t support any more cruft added to HTML without hearing the reasons. “Don’t break the Web” is a way to befuddle us. Tell us what your real concerns are and we will try to help. We are not here to rubber-stamp the first crazy idea that you come up with.

Dean Edwards on the WaSP/MSTF private mailing list

Comments (66)

2008/01/06

IE7.js version 2.0 (beta)

I’ve finally updated my IE7 library. :-)

IE7 was in alpha for a long time. The last version was very stable but I always intended to issue a serious update. The release of the real IE7 browser threw me off course a little and then I got bogged down with base2.

I’ve made some important changes to the script which I’ll now outline.

  • The IE7 project is now hosted on googlecode (I got fed up with SourceForge).
  • IE7 is no longer modular. Instead I’ve merged the scripts into two: IE7.js and IE8.js
  • IE7.js includes only fixes that are included in the real MSIE7 browser.
  • All other enhancements are moved to IE8.js.
  • IE7 is now much smaller (11KB gzipped).
  • IE7 is now much faster (it uses the selector engine from base2.DOM)
  • There are no dependencies on other files (except blank.gif)
  • You can hotlink IE7/IE8.js directly from Google’s servers (usage instructions below)

Some fixes are removed completely:

Usage

You can link directly to the files on google:

http://ie7-js.googlecode.com/svn/version/

To upgrade MSIE5-6 to MSIE7 include the following in the <head> of your page:

<!--[if lt IE 7]>
<script src="http://ie7-js.googlecode.com/svn/version/2.0(beta3)/IE7.js" type="text/javascript"></script>
<![endif]-->

To upgrade MSIE5-7 with advanced CSS features missing from MSIE7 use the following:

<!--[if lt IE 8]>
<script src="http://ie7-js.googlecode.com/svn/version/2.0(beta3)/IE8.js" type="text/javascript"></script>
<![endif]-->

You do not need to include IE7.js if you are using IE8.js.

Demo

http://ie7-js.googlecode.com/svn/test/index.html

Links

The new home of the IE7 project is:

http://code.google.com/p/ie7-js/

And I’ve created a Google Group for discussion and questions:

http://groups.google.com/group/ie7-js

Comments (197)

Archives

Categories