H is for Host

Host objects have no rules. They are defined in the language specifications as implementation-dependant, meaning that they are allowed to do virtually anything.

Detecting Host Objects

A famous example of this specification allowance (taken to a perverse extreme) is the case of host objects in Internet Explorer that are implemented as ActiveX objects. Simply evaluating their methods (as well as some properties) will cause an exception to be thrown.

var el = document.createElement('div');
var parent = el.offsetParent; // IE throws an exception here
if (window.external && window.external.addFavorite) {

  // Though the method exists, IE will never get here

  window.alert('Found it!');
} else {
  window.alert('No such object or method');
}

Fortunately, there are easy ways to sidestep these issues.

Methods

Found at the heart of My Library, the isHostMethod function tests if the specified host object property references an object. It is an extension of the isMethodType function promoted for many years by Thomas Lahn. The added bit allows for the safe detection of ActiveX methods, which have unknown types. This and related methods were later written about in a seminal blog post by Peter Michaux.

If the referenced object will not be called, use isHostObjectProperty.

Allowed properties may be of type function or object (IE and possibly others) or unknown (IE ActiveX methods). Object types exclude null.

This test does not assert that an arbitrary property is callable. Pass only names of properties universally implemented as methods.

This test does not support properties that are methods in some browsers but not others (e.g. childNodes). This test will not discriminate between such implementations and applications should never call such objects.

var reFeaturedMethod = new RegExp('^(function|object)$', 'i');

var isHostMethod = function(o, m) {
  var t = typeof o[m];
  return !!((reFeaturedMethod.test(t) && o[m]) || t == 'unknown');
};

Properties

The isHostObjectProperty function tests if the specified host object property references an object that is safe to evaluate. ActiveX methods and other properties of type unknown are excluded.

If the referenced object will be called, use isHostMethod.

Allowed properties may be of type function, object (IE and possibly others). Object types exclude null.

var reFeaturedMethod = new RegExp('^(function|object)$', 'i');

var isHostObjectProperty = function(o, p) {
  var t = typeof o[p];
  return !!(reFeaturedMethod.test(t) && o[p]);
};
Detect addFavorite Method

Detecting Style Support

Not necessarily what you can see, but what you can script. Avoids host object augmentation.

What Can Be Scripted

Browsers may support styles without allowing them to be changed programmatically. A common example is the display property.

if (typeof document.documentElement.style.display == 'string') {
  window.alert('Can script the display style.');
}
Detect Scriptable Styles

Note that in some older browsers, certain style properties (e.g. left, top, height and width) may reference numbers rather than strings. Furthermore, some newly implemented style properties may reference objects (e.g. transform).

Note that some styles that are defined in the DOM specifications (or drafts of specifications), but not yet widely implemented (or implemented properly) may have proprietary prefixes. This was the case with the opacity style in browsers past.

What Can Be Seen

There is no guarantee that styles changed through scripting will have any effect on the rendering. In many cases, this is not a concern (e.g. effects like opacity), but in others it can be critical (e.g. positioning). An easily demonstable case occurs when styling is disabled (or overriden) by the user.

Absolute Positioning

var top, el = document.createElement('div');
document.body.appendChild(el);
top = el.offsetTop;
if (top) {
  el.style.position = 'absolute';
  el.style.top = '0';
}
window.alert(top != el.offsetTop);

Fixed Positioning

var top, el = document.createElement('div');
document.body.appendChild(el);
top = el.offsetTop;
if (top) {
  try {
    el.style.position = 'fixed';
    el.style.top = '0';
    window.alert(top != el.offsetTop);
  } catch(e) {
    window.alert(false);
  }
} else {
   window.alert('Could not tell');
}
Detect Renderable Styles

Detecting Proprietary Styles

The basic pattern was first published by the author in early Fall of 2007 and has since become a staple of cross-browser scripting.

var prefixes = ['Moz', 'O', 'Webkit', 'Khtml', 'Ms'];

var findProprietaryStyle = function(style, el) {
  if (el && typeof el.style[style] == 'undefined') {
    style = style.charAt(0).toUpperCase() + style.substring(1);
    for (var i = prefixes.length; i--;) {
      if (typeof el.style[prefixes[i] + style] != 'undefined') {
        return prefixes[i] + style;
      }
    }
    return null;
  }
  return style;
};

window.alert(findProprietaryStyle('opacity', document.documentElement) || 'Not featured');
window.alert(findProprietaryStyle('transform', document.documentElement) || 'Not featured');
Detect Proprietary Styles

Detecting Event Support

First proposed by the author here, also mentioned mentioned here and later written about in the seminal blog post by Juriy Zaytsev (Kangax), this test infers specific event support by checking the reflection of DOM0 event handler attributes.

var supportedEvents = {};

var isEventSupported = function(eventName, el) {
  eventName = 'on' + eventName;
  var eventKey = eventName + (el && el.tagName || '');

  if (typeof supportedEvents[eventKey] == 'undefined') {
    supportedEvents[eventKey] = true;
    el = el || document.createElement('div');
    if (el && isHostMethod(el, 'setAttribute')) {
      if (typeof el[eventName] == 'undefined') {
        el.setAttribute(eventName, 'window.alert(" ");');
        supportedEvents[eventKey] = isHostMethod(el, eventName);
      }
    }
  }
  return supportedEvents[eventKey];
};

window.alert(isEventSupported('mouseover'));
window.alert(isEventSupported('touchstart'));
Detect Event Support

Feature Testing

Among professionals, feature testing has been the accepted best practice for dealing with DOM inconsistencies since approximately 2003. Unfortunately, the Ajax craze attracted mostly amateurs who proceeded to trumpet the execrable practice of browser sniffing almost to the exclusion of anything else. This is evidenced by this knock-down, drag-out discussion between the author, John Resig and other jQuery proponents. It is certainly no coincidence that attempts at feature testing showed up in jQuery a little over a year later. Richard Cornford had some insightful comments about this development.

Offset Includes Border

This example from My Library determines whether offsetLeft/Top properties include border dimensions (typically reflected by the clientLeft/Top properties). It was first demonstrated in this menu example from September of 2007.

var divOuter = createElement('div');
var divInner = createElement('div');

offsetIncludesBorder = (function() {
  setStyles(divOuter, {position:'absolute', visibility:'hidden', left:'0', top:'0', padding:'0', border:'solid 1px'});
  setStyles(divInner, {position:'absolute', left:'0', top:'0', margin:'0'});
  divOuter.appendChild(divInner);
  body.appendChild(divOuter);
  b = divInner.offsetLeft == 1;
  body.removeChild(divOuter);
  divOuter.removeChild(divInner);
  return b;
})();

It is interesting to note that this pattern showed up all over jQuery about a year later. It is now becoming standard practice for libraries to detect DOM quirks.

jQuery(function(){
  var div = document.createElement("div");
  div.style.width = div.style.paddingLeft = "1px";

  document.body.appendChild( div );
  jQuery.boxModel = jQuery.support.boxModel = div.offsetWidth === 2;
  document.body.removeChild( div ).style.display = 'none';
  div = null;
});

Script Injection

This abridged example, also from My Library, demonstrates how to determine the viability of dynamic script injection. It attempts to inject a snippet of script in two different ways, testing each in turn. If neither succeeds, the addScript function is not created (which signals the calling application that the functionality is unavailable in the current environment).

if (getHeadElement) {
  head = getHeadElement();
}
if (head && isHostMethod(head, 'appendChild') && createElement) {
  methods = [];
  s = createElement('script');
  if (s) {
    add = function(method, t, docNode) {
      method(s, t, docNode);
    };

    if (isHostMethod(global.document, 'createTextNode') && elementCanHaveChildren(s)) {
      methods[methods.length] = function(s, t, docNode) {
        s.appendChild((docNode || global.document).createTextNode(t));
      };
    }

    if (typeof s.text == 'string') {
      methods[methods.length] = function(s, t) {
        s.text = t;
      };
    }

    iMethod = methods.length;
    while (!API._testscriptinsertion && iMethod--) {
      head.appendChild(s);
      add(methods[iMethod], 'this.API._testscriptinsertion = true;');
      head.removeChild(s);
    }
    if (API._testscriptinsertion) {
      addScript = API.addScript = function(t, docNode) {
        head = getHeadElement(docNode);
        s = createElement('script');
        if (s && head) {
          head.appendChild(s);
          add(methods[iMethod], t, docNode);
          head.removeChild(s);
          s = null;
          head = null;
        }
      };
      delete API._testscriptinsertion;
    }
    s = null;
  }
}
head = null;

Here is the jQuery take on this. Note that this is a very bad example. It attempts to append a child to a script element, which will indeed cause an exception to be thrown in IE. The subsequent assumption that the text property exists and will result in a script evaluation is a very poor and ill-advised inference (particularly as it is crucial to know if an injected script will evaluate or not).

The comment about IE is the telltale sign of a multi-browser object inference that results in code that can only be expected to work in environments where it has been demonstrated to work (and this example can definitely be demonstrated to fail in several older browsers). This is programming by observation of specific browsers (rather than by understanding the underlying abstractions), which is quite the paradox for browser scripting, particularly for scripts that ostensibly save time by abstracting away the chore of keeping up with ever-changing browsers (today's observations are sure to become tomorrow's rewrites).

script.type = "text/javascript";
try {
  script.appendChild( document.createTextNode( "window." + id + "=1;" ) );
} catch(e){}

root.insertBefore( script, root.firstChild );

// Make sure that the execution of code works by injecting a script
// tag with appendChild/createTextNode
// (IE doesn't support this, fails, and uses .text instead)
if ( window[ id ] ) {
  jQuery.support.scriptEval = true;
  delete window[ id ];
}

if ( jQuery.support.scriptEval ) {
  script.appendChild( document.createTextNode( data ) );
} else {
  script.text = data;
}

Other Primers

Home
dmark@cinsoft.net