Separating Behavior From Structure

"Separating Behavior from Structure" refers to the practice of maintaining clean semantic HTML markup that is free of any attributes or script that introduces custom behaviors. Any custom behaviors that are introduced into your HTML page should come from external files that unobtrusively attach/bind the behaviors to elements within your semantic markup. Like the practice of separating style from structure, this has several benefits which include:

This document will give you a brief introduction of the "unobtrusive javascript" technique and some utilities within Spry that aid with separating behavior from structure.

Unobtrusive JavaScript

Unobtrusive JavaScript is the practice of separating out any JavaScript behavior code from your content structure and presentation. With Unobtrusive JavaScript, only <script> tags that include external JavaScript files are allowed within your document. The goal is to eliminate the use of any <script> tags with inline JavaScript and the use of any HTML behavioral/event attributes, like onclick, onmouseover, etc, that make use of JavaScript from within the content markup itself, and externalize this code in a separate JavaScript file which gets included by a <script> tag with a "src" attribute. The idea here is that these externalized behaviors will get programmatically attached to the elements at some point during the document loading process, most likely after the window onload event fires, with the use of the DOM APIs which allow you to add/remove event handlers programmatically.

As an example, consider this markup which has its "obtrusive" JavaScript highlighted:

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<title>Obtrusive JavaScript Sample</title>
<link href="../../../widgets/collapsiblepanel/SpryCollapsiblePanel.css" rel="stylesheet" type="text/css" />
<script type="text/javascript" src="../../../widgets/collapsiblepanel/SpryCollapsiblePanel.js"></script>
</head>
<body>
<a href="#" onclick="cp.open(); return false;">Open Panel</a> |
<a href="#" onclick="cp.close(); return false;">Close Panel</a>
<div id="cp" class="CollapsiblePanel">
	<div class="CollapsiblePanelTab">Lorem Ipsum</div>
	<div class="CollapsiblePanelContent">
		...
	</div>
</div>
<script type="text/javascript">
var cp = new Spry.Widget.CollapsiblePanel('cp', { contentIsOpen: false });
</script>
</body>
</html>

(Click here to view the sample above.)

Applying the Unobtrusive JavaScript methodology to it, you would end up with something like this:

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<title>Obtrusive JavaScript Sample</title>
<link href="../../../widgets/collapsiblepanel/SpryCollapsiblePanel.css" rel="stylesheet" type="text/css" />
<script type="text/javascript" src="../../../widgets/collapsiblepanel/SpryCollapsiblePanel.js"></script>
<script type="text/javascript" src="ujs-02.js"></script>
</head>
<body>
<a id="openLink" href="#">Open Panel</a> |
<a id="closeLink" href="#">Close Panel</a>
<div id="cp" class="CollapsiblePanel">
	<div class="CollapsiblePanelTab">Lorem Ipsum</div>
	<div class="CollapsiblePanelContent">
		...
	</div>
</div>
</body>
</html>

(Click here to view the sample above.)

The onclick attributes that were on the links, and the <script> block that was at the bottom of the <body> element have been replaced with a <script> tag at the top which includes "ujs-02.js". A couple of id attributes were also added to the links on the page so they can be identified programmatically when we attempt to attach the onclick behaviors unobtrusively.

The contents of "ujs-02.js" looks something like this:

var cp;

function AddEventListener(element, eventType, handler, capture)
{
	if (element.addEventListener)
		element.addEventListener(eventType, handler, capture);
	else if (element.attachEvent)
		element.attachEvent("on" + eventType, handler);
}

window.onload = function()
{
	AddEventListener(document.getElementById("openLink"), "click", function(e)
	{
		cp.open();

		if (e.preventDefault) e.preventDefault();
		else e.returnResult = false;
		if (e.stopPropagation) e.stopPropagation();
		else e.cancelBubble = true;
	}, false);

	AddEventListener(document.getElementById("closeLink"), "click", function(e)
	{
		cp.close();

		if (e.preventDefault) e.preventDefault();
		else e.returnResult = false;
		if (e.stopPropagation) e.stopPropagation();
		else e.cancelBubble = true;
	}, false);

	cp = new Spry.Widget.CollapsiblePanel('cp', { contentIsOpen: false });
};

(Click here to view the sample above.)

The AddEventListener() function is a convenience function that encapsulates the add event listener functionality of the browser. IE uses a proprietary method called attachEvent(), while all other browsers use addEventListener as defined in the W3C DOM Event Spec.

After that, we have some code that defines a function to be called when the window's onload event fires. The window's onload event fires once the markup for the entire page, and all images have loaded. Most pages that attach behaviors unobtrusively, wait for this event to occur because then they know all of the elements they will need to access will be available. Our onload function does 3 things:

  1. Adds an onclick listener for "openLink".
  2. Adds an onclick listener for "closeLink".
  3. Adds the CollapsiblePanel behaviors to the 'cp' element.

The adding of the Collapsible panel behaviors is pretty straight forward, since it looks just as it did when embedded within the original HTML file, but, if you look at the event listener code for the "openLink", what used to look like this:

onclick="cp.open(); return false;"

gets translated to this:

function(e)
{
	cp.open();

	if (e.preventDefault) e.preventDefault();
	else e.returnResult = false;
	if (e.stopPropagation) e.stopPropagation();
	else e.cancelBubble = true;
}

In the original "untranslated" case, the "return false;" tells the browser that you want to prevent the event from being processed by any of the link's parent elements (propagation), and you also want to prevent the browser from executing the link's default action which is to load the page specified in the link's href attribute. When you programmatically attach an event handler with the DOM API, propagation and default actions are treated separately. The code after the "cp.open()" call in the "translated" case is basically the equivalent of returning false in an on* attribute. Once again IE has a proprietary way of preventing the default action and stopping propagation, so rather than being 2 simple DOM calls to e.preventDefault() and e.stopPropagation(), the code must check for the presence of the standard calls, and if they aren't present, fall back to the IE way of doing things.

Unobtrusive Utilities in Spry

In the examples above, we tried to stick to using the DOM APIs to illustrate the basic concepts of Unobtrusive JavaScript. Now, lets make use of some of the utilities within Spry that allow you to do the same thing with less code.

Starting with the markup from the last example above, the first thing we need to do is include some of our DOM manipulating utilities from "SpryDOMUtils.js".

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<title>Obtrusive JavaScript Sample</title>
<link href="../../../widgets/collapsiblepanel/SpryCollapsiblePanel.css" rel="stylesheet" type="text/css" />
<script type="text/javascript" src="../../../widgets/collapsiblepanel/SpryCollapsiblePanel.js"></script>
<script type="text/javascript" src="../../../includes/SpryDOMUtils.js"></script>
<script type="text/javascript" src="ujs-03.js"></script>
</head>
<body>
<a id="openLink" href="#">Open Panel</a> |
<a id="closeLink" href="#">Close Panel</a>
<div id="cp" class="CollapsiblePanel">
	<div class="CollapsiblePanelTab">Lorem Ipsum</div>
	<div class="CollapsiblePanelContent">
		...
	</div>
</div>
</body>
</html>

(Click here to view the sample above.)

SpryDOMUtils.js contains utility functions for adding/removing class names on elements, adding/removing event handlers, an element selector for retrieving elements within the page based on CSS 3 selectors, etc. If we make use of some of these utilities, the code in our externalized JS file (ujs-03.js) becomes:

var cp;

Spry.Utils.addLoadListener(function()
{
	Spry.Utils.addEventListener("openLink", "click", function(e) { cp.open(); return false; }, false);
	Spry.Utils.addEventListener("closeLink", "click", function(e) { cp.close(); return false; }, false);

	cp = new Spry.Widget.CollapsiblePanel('cp', { contentIsOpen: false });
});

(Click here to view the sample above.)

The code above does the same thing as our previous example, but this version makes use of some of the Spry utilities that hide some of the differences between browsers when it comes to dealing with registering the event handler and dealing with the actual event object. Once again, taking a look at the event listener code for the "openLink", what used to be:

onclick="cp.open(); return false;"

now gets translated to:

	Spry.Utils.addEventListener("openLink", "click", function(e) { cp.open(); return false; }, false);

which looks a bit more like the original code wrapped with some syntactic sugar.

You'll also notice that the example above makes use of Spry.Utils.addLoadListener() rather than setting the window.onload property directly. It's usually better to avoid setting the on* properties of an element directly, especially in an environment that makes use of several different JavaScript libraries from different vendors. The reason is that each library may want to override the window's onload property so if you include 3 libraries that did this, only the last one would work properly. Spry.Utils.addLoadListener() uses the DOM APIs underneath the hood to programmatically add an onload listener. Doing so allows multiple onload event listeners to exist on the same element, so even if one of the libraries you included sets the window.onload function directly, your code will still be called when the onload event fires.

One of the newest features in Spry 1.6 is the element selector which allows a developer to call a single function with a specified CSS selector and get back an array of matching elements. The array that the element selector returns is enhanced with utility methods that allow the developer to do frequent tasks, such as adding/removing class names or adding event listeners, on the resulting set of nodes with a single function call instead of having to code a loop that operates on individual elements within the array.

Consider the following markup that contains onmouseover and onmouseout attributes that change the background color of a table row as the user hovers the mouse over it:

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<title>Unobtrusive JavaScript Sample</title>
<link href="css/sample.css" rel="stylesheet" type="text/css" />
</head>
<body>
<table id="hoverExample" border="1">
	<tr onmouseover="this.className = 'hover';" onmouseout="this.className = '';">
		<td>row1</td>
		<td>These rows</td>
	</tr>
	<tr onmouseover="this.className = 'hover';" onmouseout="this.className = '';">
		<td>row2</td>
		<td>have a hover</td>
	</tr>
	<tr onmouseover="this.className = 'hover';" onmouseout="this.className = '';">
		<td>row3</td>
		<td>behavior</td>
	</tr>
	<tr onmouseover="this.className = 'hover';" onmouseout="this.className = '';">
		<td>row4</td>
		<td>attached to them</td>
	</tr>
</table>
</body>
</html>

(Click here to view the sample above.)

To separate the hover behavior from the actual markup, we would simply remove all of the onmouseover and onmouseout attributes, and add references to SpryDOMUtils.js and another JavaScript file (ujs-05.js) that will attach our hover behavior unobtrusively:

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<title>Unobtrusive JavaScript Sample</title>
<script type="text/javascript" src="../../../includes/SpryDOMUtils.js"></script>
<script type="text/javascript" src="ujs-05.js"></script>
<link href="css/sample.css" rel="stylesheet" type="text/css" />
</head>
<body>
<table id="hoverExample" border="1">
	<tr>
		<td>row1</td>
		<td>These rows</td>
	</tr>
	<tr>
		<td>row2</td>
		<td>have a hover</td>
	</tr>
	<tr>
		<td>row3</td>
		<td>behavior</td>
	</tr>
	<tr>
		<td>row4</td>
		<td>attached to them</td>
	</tr>
</table>
</body>
</html>

(Click here to view the sample above.)

The code in our JavaScript file (ujs-05.js) would look something like this:

Spry.Utils.addLoadListener(function()
{
	// Find the row elements in the table with an id of "hoverExample".

	var matchingElementsArray = Spry.$$("#hoverExample tr");

	// Add event handlers to every element in the resulting array.

	matchingElementsArray.addEventListener("mouseover", function(e){ this.className = "hover"; }, false);
	matchingElementsArray.addEventListener("mouseout", function(e){ this.className = ""; }, false);
});

Or, since the array returned by the element selector is extended with utility functions that return the array itself, the code above could be shortened to what looks like a "chained list" of function calls:

Spry.Utils.addLoadListener(function()
{
	Spry.$$("#hoverExample tr").addEventListener("mouseover", function(e){this.className="hover";}, false).addEventListener("mouseout", function(e){this.className="";}, false);
});

(Click here to view the sample above.)

Unobtrusive Behavior Attributes

The element selector can be very useful when you want to find a set of elements based on a class name, or even its position within the document and attach some behaviors to it. It is also useful for attaching attributes unobtrusively to elements that enable browser or Spry behaviors. An example of this would be the tabindex attribute. According to the XHTML 1.0 DTD, the tabindex attribute is only allowed on, anchor, map, object, and form input elements, but browsers like IE and Mozilla/FireFox allow it to be placed on other elements like divs and spans for the sake of accessibility. The W3C's WAI group has picked up on this and are proposing extensions to XHTML, within their WAI-ARIA Roadmap and WAI-ARIA Roles proposal, that allow for tabindex on other elements.

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<title>Tabindex Test</title>
</head>

<body>
<span id="cb1" class="triStateCheckbox" tabindex="0"></span>
<span id="cb2" class="triStateCheckbox" tabindex="0"></span>
</body>
</html>

The problem today is that even though adding tabindex to a div within your document improves accessibility, it causes markup validators to flag the attribute as invalid because it is not currently part of the XHTML DTD they are using. With the element selector, developers can take advantage of the tabindex attribute and keep their documents validating by leaving off the tabindex attribute:

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<title>Tabindex Test</title>
<script type="text/javascript" src="triStateCheckbox.js"></script>
</head>

<body>
<span id="cb1" class="triStateCheckbox"></span>
<span id="cb2" class="triStateCheckbox"></span>
</body>
</html>

And attaching it unobtrusively like this:

Spry.Utils.addLoadListener(function()
{
	Spry.$$(".toggleButton").setAttribute("tabindex", "0");
});

This same concept can also be applied to Spry regions. The following markup fails to validate using the W3C Validator because it considers all of the attributes namespaced with the spry prefix as invalid even though there is a namespace declaration:

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:spry="http://ns.adobe.com/spry">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<title>Unobtrusive JavaScript Sample</title>
<script type="text/javascript" src="../../../includes/xpath.js"></script>
<script type="text/javascript" src="../../../includes/SpryData.js"></script>
<script type="text/javascript">
var dsEmployees = new Spry.Data.XMLDataSet("../../../data/employees-01.xml", "/employees/employee");
</script>
<link href="css/sample.css" rel="stylesheet" type="text/css" />
</head>
<body>
<div spry:region="dsEmployees">
	<table id="hoverExample" border="1">
		<tr>
			<th spry:sort="lastname">Last Name</th>
			<th spry:sort="firstname">First Name</th>
		</tr>
		<tr spry:repeat="dsEmployees">
			<td>{lastname}</td>
			<td>{firstname}</td>
		</tr>
	</table>
</div>
</body>
</html>

(Click here to view the sample above.)

Using the element selector, we can remove the namespaced spry attributes from our markup:

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:spry="http://ns.adobe.com/spry">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<title>Unobtrusive JavaScript Sample</title>
<script type="text/javascript" src="../../../includes/xpath.js"></script>
<script type="text/javascript" src="../../../includes/SpryData.js"></script>
<script type="text/javascript" src="../../../includes/SpryDOMUtils.js"></script>
<script type="text/javascript" src="ujs-07.js"></script>
<link href="css/sample.css" rel="stylesheet" type="text/css" />
</head>
<body>
<div>
	<table id="hoverExample" border="1">
		<tr>
			<th>Last Name</th>
			<th>First Name</th>
		</tr>
		<tr>
			<td>{lastname}</td>
			<td>{firstname}</td>
		</tr>
	</table>
</div>
</body>
</html>

(Click here to view the sample above.)

and move them into an external file (ujs-07.js) which unobtrusively attaches them:

var dsEmployees = new Spry.Data.XMLDataSet("../../../data/employees-01.xml", "/employees/employee");

// Since this JavaScript file can load before the browser has even read in and created the actual
// DOM elements we want to attach attributes to, we need to add a load listener that will set the
// attributes on the appropriate elements after the onload event fires.

Spry.Utils.addLoadListener(function()
{
	// Attach the spry namespaced attributes unobtrusively.

	Spry.$$("div").setAttribute("spry:region", "dsEmployees");
	Spry.$$("#hoverExample th:nth-child(1)").setAttribute("spry:sort", "lastname");
	Spry.$$("#hoverExample th:nth-child(2)").setAttribute("spry:sort", "firstname");
	Spry.$$("#hoverExample tr:nth-child(2)").setAttribute("spry:repeat", "dsEmployees");

	// Tell Spry to process regions within the document.

	Spry.Data.initRegions();
});

(Click here to view the sample above.)

Running the resulting markup through the W3C validator now passes with a "This Page Is Valid XHTML 1.0 Transitional!" result.


Copyright © 2007. Adobe Systems Incorporated.
All rights reserved