Matt's DOM Utils Matt's DOM Utils (Utils)

HOWTO: Graceful Degradation

Table of Contents

  1. Introduction
    1. Relation to Progressive Enhancement
  2. Variables in the Web Client
    1. HTTP
      1. Host
      2. 4xx and 5xx Status Codes
    2. HTML
      1. Doctype
      2. Alien Features
    3. CSS
      1. Alien Properties
      2. Proprietary Properties
    4. JavaScript
      1. Native Features
      2. Host Features
  3. Defense
    1. HTML
      1. Default State
      2. Default Style
    2. CSS
      1. Media Queries
    3. JavaScript
      1. isHostMethod
  4. Conclusions
    1. JavaScript Library
  5. Footnotes

Introduction

“Graceful Degradation” is a phrase that describes a process triggered by a Web client at runtime. The process enables a page to continue working even after erring. A page thereby often reverts to static content because insufficient support was detected. To grasp Graceful Degradation, one must first grasp the Web client.

There is only a trade off in accommodating ancient browsers if it is necessary to write code to actively accommodate them. If a system is being designed to facilitate [Graceful Degradation] then the work needed to accommodate the most modern browsers with [JavaScript] disabled has effectively accommodated those ancient browsers in some sense.

—Richard Cornford (on Graceful Degradation)[0]

The Web client is one of the most hostile environments that a software developer can face. Many variables can cause a page to falter. Each layer of the Web client has variables which can trigger differing behaviour in a page. They are outlined in detail below.

Relation to Progressive Enchancement

Contrary to what some[1] have written, “Progressive Enhancement” differs from Graceful Degradation. Progressive Enhancement is a phrase that describes a build process undertaken by a developer while building a Web page. A default state is first created with HTML; CSS is then layered on to decorate. Finally, JavaScript is added to give the page dynamic behaviour. This incremental process allows Web pages to be stable should variables change.


Variables in the Web Client

HTTP

On the Internet, HTTP is how text (née “hypertext”) is transmitted. Colloquially, it's how resources (read: files) are transmitted. However, there are many ways that a user or client can modify transmission of resources. The following list contains some common caveats.

Host

The Host header (specified in HTTP/1.1)[2] is specified to contain the host of the resource requested. Colloquially, one might refer to a “host” as a “domain” or “domain name”. Hosts can be blocked by Web clients through a preferences panel or the user through a hosts file. Quite often, this feature is used to block advertising hosts, and by proxy, advertising. Therefore, it is wise to anticipate an external resource being blocked (Google Analytics is a common target). Some examples would include: images, CSS stylesheets and JavaScript files. Sometimes, a client will block a host without the user's explicit consent. This can be caused by a “Web filter”, which is used by many administrators to prevent users from browsing undesired or lewd content.

4xx and 5xx Status Codes

The 4xx range of status codes (specified in HTTP/1.1)[3] denotes an error on the part of the Web client. Conversely, the 5xx range of status codes (also specified in HTTP/1.1)[4] denote errors on the part of the Web server. When applied to external resources in a Web page, they can encumber an entire page unless dealt with judiciously. One such example is the following JavaScript snippet:

/*
	NOTE: use absolute paths to avoid problems
	with the `src` property (absolute) not
	reflecting the `src` attribute
	(relative or absolute).
*/
var img = document.createElement("img");
img.onload = function (evt) {
	this.alt = "Image loaded successfully";
	this.src = "http://localhost/success.gif";
};
img.onerror = function (evt) {
	this.alt = "Image could not be loaded";
	// necessary to avoid infinite event loop
	if (img.src !== "http://localhost/error.gif") {
		this.src = "http://localhost/error.gif";
	}
};
img.alt = "Image is loading";
img.width = 250;
img.height = 250;
img.src = "http://localhost/placeholder.gif";
document.body.appendChild(img);

which will replace an image's src property with an image indicative of its current state. The alt property is populated with a message for each state. In the event that the error image doesn't load, a message will still be available for the user to read. Another example is an external script file that fails to load. This snippet is an example:

if (typeof Library === "object" && Library) {
	// `Library` script was loaded.
	(function () {
		// application code here
	}());
}

which prevents errors caused by referencing an object (read: API) that is undefined. Utils uses this pattern for both modules and unit tests.

HTML

HTML is a markup language derived from SGML that allows one to author Web content. It contains variables which can be modified by the Web client. Some examples follow.

Doctype

Within an HTML (along with XHTML and XML) document, a doctype is used to notify the client of what type the document is to be parsed as. Doctypes usually contain the version of the mark-up language used along with a category (such as “transitional” or “strict”). With the entrance of HTML 5, a version number and category are no longer necessary. This snippet:

<!DOCTYPE HTML>

is a sufficient identifier of an HTML 5 document. Omission of a doctype can cause a client to parse the page in “Quirks Mode”. Quirks Mode is a parsing mode wherein a client attempts to parse a document to conform to a legacy, or outdated style.

Please don’t [design for Quirks Mode]. Willfully designing for the Quirks [Mode] will come and haunt you, your coworkers or your successors in the future [...] . Designing for the Quirks [Mode] is a bad idea. Trust me.

—Henri Sivonen (on Quirks Mode)[5]
Alien Features

Environments that do not recognise a particular element will render textual content inside. This behaviour can be used to leave messages to the user that the feature is unsupported. In HTML 5, a snippet such as:

<video src="eggn.ogg">
	The video element is unsupported in this environment.
</video>

is a good starting point. In supported environments, the textual content is not rendered.

Environments that do not recognise a particular property will ignore it. This snippet:

<input name="first_name"
	placeholder="First Name"
	type="text">

is an example wherein an unrecognised attribute (the placeholder attribute) will often be ignored.

CSS

CSS is a style sheet language used to visually format HTML. Like HTML, it also contains variables which can by modified by the Web client. What follows is a set of examples.

Alien Properties

Unrecognised properties will be ignored by an environment. This facilitates the use of newer features while having little effect on outdated environments. For example, this snippet:

.featured
{
	background-color: #FFEE77;
	/* ignored in older browsers such as IE 7 */
	border-radius: 1em;
}

exemplifies how visual effects can be stacked to accommodate supportive environments.

Proprietary Properties

Proprietary properties pertain to a specific environment. They are non-standard and are thus discouraged unless only that particular environment is desired. Quite often, they are prefixed with a prefix representing the environment to be targeted (e.g. "-ms" for Microsoft). The following snippet:

.highlighted
{
	/* proprietary webkit property */
	-webkit-text-stroke: 1px #FF0000;
}

is an example of a proprietary property (specific to the Webkit engine) that is non-standard and tailored for a specific environment.

JavaScript

JavaScript is a scripting language and a subset of the ECMAScript language. Unfortunately, it contains many variables that can by modified by a Web client. Common problem points are outlined below.

Native Features

Unrecognised native features will err upon use in an environment. As a result, newer features should be used with caution. The following snippet:

// errs in IE 8
var keys = Object.keys({});

is an example of a newer feature that will cause problems for older environments. As a result, older core features with the widest amount of support are encouraged over newer features. This snippet:

function grabKeys(
	obj
)
{
	var keys = [],
		key;
	for (key in obj) {
		if (obj.hasOwnProperty(key)) {
			keys[keys.length] = key;
		}
	}
	return keys;
}

could be used as an alternative in order to support more environments, though its behaviour deviates slightly from the native method.

Host Features

Like native features, unrecognised host features will err upon use in an environment. However, host features are even more volatile as their implementation in ECMAScript is largely undefined. A common example is the Document Object Model (DOM), which is designed to be language-independent. Furthermore, host objects are very unreliable when used for “type” inferences, as their “types” vary between environments. Code such as this:

// "function" in Opera 8
// "object" in Opera 9
typeof document.childNodes;

illustrates just one inconsistency between environments. There are numerous predatory bugs lying underneath older environments, but due dilligence can defeat many of them. One example is this snippet:

// "Type Mismatch" in IE 6
xhr.onreadystatechange = null;

which can be replaced with the following snippet:

// type match
xhr.onreadystatechange = function () {};

thereby removing the “type mismatch”, as a function was expected. However, some are more difficult, such as this snippet:

// "object" in IE 5
typeof document.createDocumentFragment;
// Error: "Not Implemented" in IE 5
document.createDocumentFragment();

which would also be called an “uncallable function”. One solution to the preceding is to use the following snippet:

var canCallDocFrag = (function () {
	var result = true;
	try {
		document.createDocumentFragment();
	} catch (err) {
		result = false;
	}
	return result;
}());

which will catch the error thrown from the invocation of the specified method. An immediate closure is used to reduce repetition. A derivative of this strategy is used in Utils to complete IE 5 support.


Defense

Once one has learned of the follies that populate the Web client, one can then learn how to safeguard against them. A wide array of strategies exist to author defensively for the Web. They are explained below.

HTML

HTML does not explicitly contain tools for defensive authoring, but its status as the building blocks of a Web page allows it to be used defensively.

Default State

A default state is the first state in which a Web client is presented with upon loading a page. For HTML pages, this means static HTML. Default states are used to provide clients with forms of pages while not patronising users of these clients. Messages such as: “Please upgrade your browser; this one is insufficient” and “Please enable JavaScript to view this page” only insult the user's choice (or often a lack thereof) of environment.

An optimal approach is to provide a static alternative. For example, consider a proposal to create a rotating image slideshow for the home page of a large Web site. JavaScript would be the presumptive choice to rotate images at the behest of a timer. However, consider a scenario wherein scripting is disabled. Perhaps an administrator has disabled scripting out of security concerns. The user may be left with an empty “slide show” depending on the default state written. A defensive strategy would be to write one image directly into the HTML mark-up and provide a link to a page containing all proposed feature links in static form. Consider this snippet:

<div id="slideshow">
	<a href="slides.html" id="slide_link"
		title="Feature Slideshow"><img alt="Feature 1"
			height="480" id="current_slide"
			name="current_slide"
			src="images/feature1.jpg" width="640"></a>
<div>

which would then be followed by this theoretical JavaScript snippet:

var link = document.getElementById("slide_link"),
	slide = document.images.current_slide,
	slideIndex,
	slideTimer,
	maxIndex = 3;

function playSlide()
{
	slide.alt = "Feature" + slideIndex;
	slide.src = "images/feature" +
		slideIndex + ".jpg";
	link.href = "http://localhost/feature" +
		slideIndex + ".html";
	link.title = "Feature" + slideIndex;
}

function endShow()
{
	clearTimeout(slideTimer);
	slideTimer = null;
	startShow();
}

function runTimer()
{
	if (slideIndex <= maxIndex) {
		slideTimer = setTimeout(
			runTimer,
			3000
		);
		playSlide();
		slideIndex += 1;
	} else if (slideIndex > maxIndex) {
		endShow();
	}
}

function startShow()
{
	slideIndex = 1;
	runTimer();
}

startShow();

which is a naïve slide show that provides a default state for the user should the script fail to execute (be it a preference or a failed load).

Default Style

Default styles are often overlooked by developers. When respected, they can yield a defensive foundation for Web pages. One such case is the <p> element, which has a top and bottom margin specified[6] in CSS 2.1. It can be used to easily style and separate form controls. The following snippet is an example:

<form action="http://localhost/" id="test_form"
	method="post" name="test_form">
	<fieldset>
		<legend>Enter Your Credentials</legend>
		<p>
			<label>
				<input name="username_control" type="text">
				Username
			</label>
		</p>
		<p>
			<label>
				<input name="password_control" type="password">
				Password
			</label>
		</p>
		<p>
			<input name="submit_control" type="submit" value="Log in">
		</p>
	</fieldset>
</form>

which eschews <table> elements for <p> elements; this provides a quicker, flexible solution.

CSS

CSS can be written defensively to minimise loopholes created by deficient environments. By testing a wide range of browsers and studying standards, one can acquire knowledge of defensive CSS strategies. Some examples follow.

Media Queries

Media types were first introduced[7] in CSS 2.1. They have been expanded to provide complex logical assertions, or “queries”, and are specified as a module[8] of CSS 3.

Querying for the current display width can be used as a strategy to facilitate a “responsive” page. This means that the page will adjust to differing screen widths. For example, a page that uses pseudo-columns floated next to each other may remove those floats if a certain width range is detected. The result is one singular pseudo-column, which is optimal for cramped display widths. The project Web site for Utils uses this strategy; its use is most evident on the home page. An example would be the following snippet:

@media screen and (max-width: 20em)
{
	body
	{
		font-size: .75em;
	}
}

which will shrink the font size to 12px if the display's maximum width is 320px (given 16px ems).

JavaScript

“Defensive Programming” is a phrase that describes a style of programming that makes few assumptions and tests heavily. It has been studied and espoused by denizens of the comp.lang.javascript[9] newsgroup for over a decade. Most often, the practice is used in browser scripting with JavaScript, particularly HTML DOM scripting. By programming defensively, one makes few assumptions and tests frequently. By testing frequently, one acquires knowledge of environmental “quirks”, and how to avoid them.

There's lots of [browsers with unknown bugs] out there. You can't know all of their quirks and you can't code for all of their quirks. All you can do is follow standards, use sound practices, keep things simple as possible and test on as many agents as is feasible.

—David Mark (on Defensive Programming)[10]
isHostMethod

“isHostMethod” is a method that has been referenced[11] in the comp.lang.javascript newsgroup innumerable times. It searches for a typeof result of "function", "object" or "unknown". The first two have been covered in an example earlier. If the specified property matches either of those two, it must be “truthy” to pass. The final “type” refers to a special case wherein ActiveX objects will return the preceding “type” in earlier versions of Internet Explorer. It cannot have its “truthiness” examined, as doing so can throw an error. Utils uses a derivative of isHostMethod called “isHostObject”. isHostMethod's seminal form is the following:

function isHostMethod(o, m) 
{ 
  var t = typeof o[m]; 
  return (/^(function|object)$/.test(t) && o[m]) 
    || t == "unknown"; 
}

which is a snippet from David Mark's “My Library”[12]. isHostMethod was derived from a function created by Thomas Lahn, entitled “isMethodType”.


Conclusions

Two different environments can, and probably will produce two different results. Instead of striving for a multiplicity of identical results, strive for a multiplicity of acceptable results that present a user with a respectable interface regardless of the environment.

JavaScript Library

Earlier, code for an example “slideshow” was written. However, it doesn't quite gracefully degrade. Using Utils, Graceful Degradation can be achieved. Here is a revised version:

var global = global || this;
if (typeof Utils === "object" && Utils) {
	(function () {
		var commonElements,
			slideIndex,
			slideTimer,
			maxIndex = 3;

		function grabById(
			id
		)
		{
			var result = null;
			// method is part of a "dynamic interface"
			if (Utils.select.byId) {
				result = Utils.select.byId(
					global.document,
					id
				);
			}
			return result;
		}

		function grabNamedImage(
			name
		)
		{
			var result = null;
			// method is part of a "dynamic interface"
			if (Utils.select.images) {
				result = Utils.select.images(
					global.document,
					name
				);
			}
			return result;
		}

		commonElements = (function () {
			var result = {};
			result.link = grabById("slide_link");
			result.slide = grabNamedImage("current_slide");
			return result;
		}());

		function playSlide()
		{
			var slide = commonElements.slide,
				link = commonElements.link;
			if (slide && link) {
				slide.alt = "Feature" + slideIndex;
				slide.src = "images/feature" +
					slideIndex + ".jpg";
				link.href = "http://localhost/feature" +
					slideIndex + ".html";
				link.title = "Feature" + slideIndex;
			}
		}

		function removeTimeout(
			ref
		)
		{
			var key = "clearTimeout",
				result = null;
			if (Utils.is.hostObject(global[key])) {
				result = global[key](
					ref
				);
			}
			return result;
		}

		function endShow()
		{
			removeTimeout(slideTimer);
			slideTimer = null;
			startShow();
		}

		function makeTimeout(
			callback,
			time
		)
		{
			var key = "setTimeout",
				result = null;
			if (Utils.is.hostObject(global[key])) {
				result = global[key](
					callback,
					time
				);
			}
			return result;
		}

		function runTimer()
		{
			var timeout;
			if (slideIndex <= maxIndex) {
				timeout = makeTimeout(
					runTimer,
					3000
				);
				if (timeout) {
					slideTimer = timeout;
					playSlide();
					slideIndex += 1;
				}
			} else if (slideIndex > maxIndex) {
				endShow();
			}
			timeout = null;
		}

		function startShow()
		{
			slideIndex = 1;
			runTimer();
		}

		startShow();
	}());
}

which is far more defensive and degrades cleanly. Note the use of wrapper functions and result checks, enabling a more defensive approach.

The preceding code is used in a functional demo[13].

for viewing. It has been tested successfully and degrades gracefully in IE 4+.


Footnotes

[0]
Richard Cornford on Graceful Degradation
[1]
Darcy Clarke: Front-end Job Interview Questions
[2]
HTTP/1.1: Host Header
[3]
HTTP/1.1: 4xx Status Codes
[4]
HTTP/1.1: 5xx Status Codes
[5]
Henri Sivonen on Quirks Mode
[6]
CSS 2.1: Default Stylesheet for HTML 4
[7]
CSS 2.1: Media Types
[8]
CSS 3: Media Queries
[9]
comp.lang.javascript Newsgroup
[10]
David Mark on Defensive Programming
[11]
Google Groups: Should isHostMethod be added to the FAQ?
[12]
My Library
[13]
Slideshow Test