Spry Data Set and Dynamic Region Overview

What is the Spry framework?

The Spry framework is a client-side JavaScript library that consists of a set of JavaScript, CSS, and image files that provide support for XML Data Sets, Dynamic Regions, Widgets, and Transition Effects.

Browser Diagram

Figure - The Spry framework runs on the client-side in a browser.

Although these files reside on a server, they are meant to be loaded and run inside of a browser. Users of the framework will include/link to these files and make use of the various components of the framework in their HTML documents to add a richer experience for their users.

The Spry framework is geared towards Designers, so every element of the framework should adhere to the following guidelines:


XML Data Sets

An XML data set is a JavaScript Object that provides an API for loading and managing data from an XML document. As the data is loaded, it is flattened out into a tabular format of rows and columns.

As you read about the features of the XML data set, please keep in mind that this is a preview release, so this document simply describes what is currently implemented. The API for the data set is still being worked out and may change.

Creating an XML Data Set

To create an XML data set, you must first include 2 JavaScript files in your HTML document:

<!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=iso-8859-1" />
<title>Dynamic Region Example</title>

<script type="text/javascript" src="../../includes/xpath.js"></script>
<script type="text/javascript" src="../../includes/SpryData.js"></script>
</head>
<body>
</body>
</html>

Source Listing - To use XML data sets and dynamic regions, you must first include xpath.js and SpryData.js in your HTML document.

"xpath.js" is Google's JavaScript implementation of the XPath 1.0 standard. You can get more information about it by visiting their open source google-ajaxslt project page. "SpryData.js" contains the code that defines XML data sets and dynamic regions.

XML data sets are created programmatically with the JavaScript "new" keyword. The constructor for the XML data set requires 2 arguments:

<!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=iso-8859-1" />
<title>Dynamic Region Example</title>
<script type="text/javascript" src="../../includes/xpath.js"></script>
<script type="text/javascript" src="../../includes/SpryData.js"></script>
<script type="text/javascript">
var dsPhotos = new Spry.Data.XMLDataSet("/photos.php?galleryid=2000", "/gallery/photos/photo");
</script>

</head>
<body>
</body>
</html>

Source Listing - Example of creating an XML Data Set in JavaScript. The first argument to the constructor is the url. The second argument is the XPath to the data.

The URL argument can be a relative or absolute URL to an XML file, or to a web service/application that returns XML data. Users need to be aware that the loading of XML data is subject to the browser's security model which requires that any data you load must come from the domain from which the HTML page, currently running in the browser, came from. Web developers typically get around this limitation by having a server-side script that can be called from the browser that can load URLs from other domains.

By default, the data set uses the HTTP GET method when retrieving the XML data. The data set can also retrieve XML data via the HTTP POST method, by specifying additional constructor options.

<!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=iso-8859-1" />
<title>Dynamic Region Example</title>

<script type="text/javascript" src="../../includes/xpath.js"></script>
<script type="text/javascript" src="../../includes/SpryData.js"></script>
<script type="text/javascript">

var dsPhotos = new Spry.Data.XMLDataSet("/photos.php", "/gallery/photos/photo", { method: "POST", postData: "galleryid=2000&offset=20&limit=10", headers: { "Content-Type": "application/x-www-form-urlencoded; charset=UTF-8" } });
</script>
</head>
<body>
</body>

</html>

Source Listing - Example of how to fetch XML data via the HTTP POST method.

If the POST method is used, but no content type is specified, the default content type will be set to "application/x-www-form-urlencoded; charset=UTF-8".

Here's a quick list of the HTTP related constructor options that can be specified:

XMLDataSet Constructor HTTP Options
method
The HTTP method to use when fetching the XML data. Must be the string "GET" or "POST".
postData
Can be a string containing url encode form args or any value supported by the XMLHttpRequest object. If a "Content-Type" header is not specified in conjunction with the postData, the "application/x-www-form-urlencoded; charset=UTF-8" content type will be used.
username
The server username to use when accessing the XML data.
password
The password to use in conjunction with username whn accessing the XML data.
headers
This is an object or associative array which uses the HTTP request field name as its property/key to store values.

It is important to note that the data set contains no data after you've created it with the "new" keyword. You must first call the data set's loadData() method to fire off a request to load the XML data. This loading is asynchronous, so the data may still not be available if you try to access it right after calling loadData.

The data set uses the XMLHTTPRequest object to asynchronously load the URL. When the XML data arrives, it is actually in 2 formats, a text format, and a DOM tree format.

<gallery id="12345">
	<photographer id="4532">John Doe</photographer>
	<email>john@doe.com</email>
	<photos id="2000">

		<photo path="sun.jpg" width="16" height="16" />
		<photo path="tree.jpg" width="16" height="16" />
		<photo path="surf.jpg" width="16" height="16" />
	</photos>
</gallery>
          

Source Listing - Example of XML in text format.

XML as Text

Figure - Example of XML as a DOM tree.

The data set uses the XPath argument that was passed to its constructor to navigate the XML DOM tree to find the set of nodes that represent the data you are interested in.


<gallery id="12345">
	<photographer id="4532">John Doe</photographer>
	<email>john@doe.com</email>
	<photos id="2000">
		<photo path="sun.jpg" width="16" height="16" />

		<photo path="tree.jpg" width="16" height="16" />
		<photo path="surf.jpg" width="16" height="16" />
	</photos>
</gallery>
          

Source Listing -Data selected with the XPath "/gallery/photos/photo".

XPath /gallery/photos/photo

Figure - Data selected with the XPath "/gallery/photos/photo".

The data set then flattens the set of nodes into a tabular format:

@path @width @height
sun.jpg 16 16
tree.jpg 16 16
surf.jpg 16 16

Figure - Flattened data selected with XPath "/gallery/photos/photo".

The column names of the flattened table are derived from the tag name of the selected node or its child nodes, and their attributes. A column will be created for each attribute on the selected node. If the selected node has a single child that is a text or cdata node, that child is considered the value of that node. A column will be created for that text or cdata node and its name will be the tag name for the node. If the selected node has child elements, a column will be created for the value, if it has one, of each child element, and each of its attributes.

This description is a bit confusing, so lets run through a few more examples to illustrate the flattening process and the different column names that can be generated.

<gallery id="12345">
	<photographer id="4532">John Doe</photographer>
	<email>john@doe.com</email>

	<photos id="2000">
		<photo path="sun.jpg" width="16" height="16" />
		<photo path="tree.jpg" width="16" height="16" />
		<photo path="surf.jpg" width="16" height="16" />
	</photos>
</gallery>
          

Source Listing - Selected nodes for XPath "/gallery/photographer".

XPath /gallery/photographer

Figure - Selected nodes for XPath "/gallery/photographer".

An XPath of "/gallery/photographer" would result in the following table:

photographer @id
John Doe 16

Figure - Flattened data for XPath "/gallery/photographer".

In this particular case, only one node is selected, so we only get one row in our data set. The value of the <photographer> node is the text "John Doe", so a column named "photographer" is created to store that value. "id" is an attribute of the <photographer> node so its value is placed in the "@id" column. All attribute names are preceded by an "@" character.

An XPath of "/gallery":

<gallery id="12345">
	<photographer id="4532">John Doe</photographer>
	<email>john@doe.com</email>

	<photos id="2000">
		<photo path="sun.jpg" width="16" height="16" />
		<photo path="tree.jpg" width="16" height="16" />
		<photo path="surf.jpg" width="16" height="16" />
	</photos>
</gallery>
          

Source Listing - Selected nodes for XPath /gallery.

XPath /gallery

Figure - Selected nodes for XPath /gallery.

Would result in the following table and column names:

@id photographer photographer/@id email
12345 John Doe 4532 john@doe.com

Figure - Flattened data selected with XPath "/gallery".

Notice that the column name for attributes of children elements are prefixed with the tag name of the child element. In this particular case, <photographer> is a child element of the selected <gallery> node, so its id attribute is prefixed with "photographer/@". Also notice that nothing was added to the table for the <photos> element even though it is a child of the <gallery> node. That is because we don't flatten any child elements that contain other elements.

With XPath, you can also select attributes of nodes, so if you used an XPath of "gallery/photos/photo/@path":

<gallery id="12345">
	<photographer id="4532">John Doe</photographer>
	<email>john@doe.com</email>

	<photos id="2000">
		<photo path="sun.jpg" width="16" height="16" />
		<photo path="tree.jpg" width="16" height="16" />
		<photo path="surf.jpg" width="16" height="16" />

	</photos>
</gallery>
          

Source Listing -Selected nodes for XPath "gallery/photos/photo/@path".

XPath /gallery/photos/photo

Figure - Selected nodes for XPath "gallery/photos/photo/@path".

you would end up with a table like this:

@path
sun.jpg
tree.jpg
surf.jpg

Figure - Flattened data selected with XPath "/gallery/photos/photo/@path".

Caching

By default, any XML data loaded by a data set is cached on the client by Spry. If a data set attempts to load the XML data for a given URL that is already in the cache, a reference to the cached data is returned to the data set. If multiple data sets attempt to load the same URL at the same time, all load requests are coalesced into a single HTTP request to save bandwidth.

Use of data caching and of coalescing requests can greatly improve performance, especially when multiple data sets refer to the same XML data, but there may be times when you need to load the data directly off the server. For example, a given URL may return different data each time you access it. You can force the data set to load the XML data directly from the server, by-passing the cache, by setting the "useCache" XMLDataSet constructor option to false.

Var dsPhotos = new  Spry.Data.XMLDataSet("/photos.php?galleryid=2000", "/gallery/photos/photo", { useCache:  false })

Source Listing - The data in the data set is stored as an array of row objects with properties that are the columns.

Getting at the Data

As mentioned above, after the XML data is loaded it is flattened into a tabular format. Inside the data set, the data is actually stored as an array of objects (rows) with properties (columns) and values.

[
	{ "@path": "sun.jpg",  "@width": 16, "@height": 16 },
	{ "@path": "tree.jpg", "@width": 16, "@height": 16 },
	{ "@path": "surf.jpg", "@width": 16, "@height": 16 }
]
          
@path @width @height
sun.jpg 16 16
tree.jpg 16 16
surf.jpg 16 16

Source Listing - The data in the data set is stored as an array of row objects with properties that are the columns.

You can get all of the rows in the data set by calling getData(). As mentioned above, the data is loaded asynchronously, so you can only access the data after it has been loaded. You may need to register an observer to be notified when the data is ready. See the Observer Notifications section below for more details.

var dsPhotos = new Spry.Data.XMLDataSet("/photos.php?galleryid=2000", "/gallery/photos/photo");

...

var rows = dsPhotos.getData(); // Get all rows.
var path = rows[0]["@path"];   // Get the data in the "@path" column of the first row.

Source Listing - Use getData() to fetch all the rows in the Data Set. You can get the value of a specific column by indexing into the row with the column name.

Current Row

Each data set maintains the notion of a "current row". By default, the current row is set to the first row in the data set. The current row can be changed programmatically by calling the setCurrentRowNumber() method and passing the row number of the row you want to make the current row. Note that the index of the first row is always zero, so if a data set contains 10 rows, the index of the last row is 9.

var dsPhotos = new Spry.Data.XMLDataSet("/photos.php?galleryid=2000", "/gallery/photos/photo");

...

dsPhotos.setCurrentRowNumber(2); // Make the 3rd row in the data set the current row.

Source Listing - Use setCurrentRowByNumber() or setCurrentRow() to change the current row.

Each row in the data set is also assigned a unique ID which allows someone to refer to a specific row in the data set even after the order of the rows in the data set change. You can get the ID of a row by accessing its "ds_RowID" property. You can also change the "current row" by calling setCurrentRow() and passing the row ID of the row you want to make the current row.

var dsPhotos = new Spry.Data.XMLDataSet("/photos.php?galleryid=2000", "/gallery/photos/photo");

...

var id = dsPhotos.getData()[2]["ds_RowID"]; // Get the ID of the 3rd row.

...

dsPhotos.setCurrentRow(id); // Make the 3rd row the current row by using its ID.

Source Listing - You can get the ID of a row by accessing its "ds_RowID" property. You can call setCurrentRow with a rowID to set the data set's "current row".

Sorting

You can sort the rows of a data set using the values in a given column by calling the sort() method on the data set.


var dsPhotos = new Spry.Data.XMLDataSet("/photos.php?galleryid=2000", "/gallery/photos/photo");

...

dsPhotos.sort("@path"); // Sort the rows of the data set using the "@path" column.

Source Listing - Calling the sort() method of a data set will sort the rows in ascending order, using data in the column specified.

The sort() method takes two arguments. The first argument can be the name of the column, or an array of column names to use when sorting. If the first argument is an array, the first element of the array serves as the primary sort column, each column after that is used for secondary, tertiary, etc sorting. The second argument is the sort order to use, and must be one of the following strings: "ascending", "descending" or "toggle". If the second argument is not specified, the sort order defaults to "ascending". Specifying "toggle" as the sort order causes the data sorted in "ascending" order to be sorted in "descending" order, and vice versa. If the column has never been sorted before, and the sort order used is "toggle", the data will initially be sorted in "ascending" order.

var dsPhotos = new Spry.Data.XMLDataSet("/photos.php?galleryid=2000", "/gallery/photos/photo");

...

dsPhotos.sort("@path", "toggle"); // Toggle the Sort order of the rows of the data set using the "@path" column.

Source Listing - Calling the sort() method with a "toggle" sort order will switch between "ascending" and "descending" sorts..

If you'd like to have the data in the data set sorted automatically whenever data is loaded, you can specify the column to sort by and the sort order to use as an option to the data set constructor.

var dsPhotos = new Spry.Data.XMLDataSet("/photos.php?galleryid=2000", "/gallery/photos/photo", { sortOnLoad: "@path", sortOrderOnLoad: "descending" });

Source Listing - Use the "sortOnLoad" option to the data set constructor to specify a column to sort by. In this example the data set will automatically sort in descending order using the data in the "@path" column.

By default all values in the data set are treated as text. This can be problematic when sorting numeric or date values. You can specify the data type for a given column by calling the setColumnType() method.

var dsPhotos = new Spry.Data.XMLDataSet("/photos.php?galleryid=2000", "/gallery/photos/photo");
dsPhotos.setColumnType("@width", "number");
dsPhotos.setColumnType("@height", "number");

...

dsPhotos.sort("@width"); // Sort the rows of the data set using the "@width" column.

Source Listing - Use the setColumnType() method to specify the data type for a given column.

Current supported data types are "number", "date", and "string".

Distinct

You can remove duplicate rows in a data set by using the distinct() method on the data set.

var dsPhotos = new Spry.Data.XMLDataSet("/photos.php?galleryid=2000", "/gallery/photos/photo");

...


dsPhotos.distinct(); // Remove all duplicate rows.

Source Listing - Use the distinct() method to remove duplicate rows.

If you'd like to run the distinct() method automatically whenever data is loaded into a data set, use the "distinctOnLoad" option to the constructor.

var dsPhotos = new Spry.Data.XMLDataSet("/photos.php?galleryid=2000", "/gallery/photos/photo", { distinctOnLoad: true });

Source Listing - Use the distinct() method to remove duplicate rows.

Users should be aware that the distinct() method is destructive, so it discards any non-distinct rows. The only way to get the data back is to re-load the XML data.

Filtering

Data sets support both destructive and non-destructive filtering.

Before using either method of filtering, you must supply a filter function that takes a data set, row object and rowNumber. This function will be invoked by the data sets filtering methods for each row in the data set. Your function must return either the row object passed to your function, or a new row object, meant to replace the row passed into your function. If your function wants to filter out the row, it should return a null value.

The data set's destructive filter method is filterData(). This method will actually replace or discard the rows of the data set. The only way to get the original data back is to reload the XML data of the data set.

var dsPhotos = new Spry.Data.XMLDataSet("/photos.php?galleryid=2000", "/gallery/photos/photo");

...

// Filter out all rows that don't have a path that begins
// with the letter 's'.
var myFilterFunc = function(dataSet, row, rowNumber)
{
	if (row["@path"].search(/^s/) != -1)
		return row;                     // Return the row to keep it in the data set.
	return null;                        // Return null to remove the row from the data set.
}


dsPhotos.filterData(myFilterFunc); // Filter the rows in the data set.

Source Listing - Use the destructive filterData() method to permanently discard rows in a data set.

It is important to note that your filter function will remain active, even when loading XML data from another URL, until you call filterData() with a null.

dsPhotos.filterData(null); // Turn off destructive filtering.

Source Listing - Call filterData() with a null argument to uninstall your filter function.

The data set's non-destructive filter method is filter(). Unlike filterData(), filter() creates a new array of rows which reference the original data, so as long as your filter function does not modify the row object passed into it, you will be able to get the original data back by simply calling filter() and passing a null argument.

var dsPhotos = new Spry.Data.XMLDataSet("/photos.php?galleryid=2000", "/gallery/photos/photo");

...

// Filter out all rows that don't have a path that begins
// with the letter 's'.
var myFilterFunc = function(dataSet, row, rowNumber)
{
	if (row["@path"].search(/^s/) != -1)
		return row;                     // Return the row to keep it in the data set.
	return null;                        // Return null to remove the row from the data set.
}


dsPhotos.filter(myFilterFunc); // Filter the rows in the data set.

Source Listing - Use the non-destructive filter() method to filter the rows in a data set.

dsPhotos.filterData(null); // Turn off non-destructive filtering.

Source Listing - Call filterData() with a null argument to uninstall your filter function.

Automatic Data Refresh

Data sets have the ability to re-load their data at a specified interval expressed in milliseconds. This can be handy when the data at a given URL changes periodically.

You can tell a data set to load at a given interval by passing the optional loadInterval option when calling the XMLDataSet constructor.

// Load the data every 10 seconds. Turn off the cache to  make sure we get it directly from the server.
var dsPhotos = new  Spry.Data.XMLDataSet("/photos.php?galleryid=2000", "/gallery/photos/photo", { useCache:  false, loadInterval: 10000 });

Source Listing - You can specify the load interval in the constructor call for the XML data set.

You can also turn on this interval loading programatically with the startLoadInterval() method, and stop it with the stopLoadInterval() method.

dsPhotos.startLoadInterval(10000); // Start loading data every 10 seconds.

...

dsPhotos.stopLoadInterval(); // Turn off interval loading.

Source Listing - You can programatically start and stop interval loading with startLoadInterval() and stopLoadInterval().

Observer Notifications

The XML data set supports an observer mechanism that allows an object or callback function to recieve event notifications.

XMLDataSet Notifications
onPreLoad
The data set is about to fire off a request for data. If the data set depends on other data sets, this event notification will not fire until they have all loaded successfully.
onPostLoad
The request for data was successful. The data is ready for consumption.
onLoadError
An error occurred while requesting the data.
onDataChanged
The data in the data set has been modified.
onPreSort
The data in the data set is about to be sorted.
onPostSort
The data in the data set has been sorted.
onCurrentRowChanged
The data set's notion of the current row has changed.

Objects as Observers

To receive notifications, an object must define a method for each notification it is interested in receiving, and then register itself as an observer on the data set.

var dsPhotos = new Spry.Data.XMLDataSet("/photos.php?galleryid=2000", "/gallery/photos/photo");

...

var myObserver = new Object;
myObserver.onDataChanged = function(dataSet, data)
{
	alert("onDataChanged called!";
};

dsPhotos.addObserver(myObserver);

Source Listing - An object interested in onDataChanged notifications must define an onDataChanged () method and then call addObserver().

The first argument for each notification method is the object that is sending the notification. So for data set observers, this will always be the dataSet object. The second argument will either be undefined, or an object that depends on the type of notification.

Data Passed Into XMLDataSet Notifications
onPreLoad
undefined
onPostLoad
undefined
onLoadError
The Spry.Utils.loadURL.Request object that was used when making the request.
onDataChanged
undefined
onPreSort

Object with the following properties:

oldSortColumns:

An array of columns used during the last sort.

oldSortOrder: The sort order used during the last sort.
newSortColumns: The array of columns about to be used for the sort.
newSortOrder: The sort order about to be used for the sort.
onPostSort

Object with the following properties:

oldSortColumns:

An array of the columns used in the previous sort.

oldSortOrder: The sort order used during the previous sort.
newSortColumns: The array of columns used for the sort.
newSortOrder: The sort order used for the sort.
onCurrentRowChanged

Object with the following properties:

oldRowID:

The ds_RowID of the last current row.

newRowID: The ds_RowID of the current row..

To stop an object from receiving notifications, the object must be removed from the list of observers with a call to removeObserver().

var dsPhotos = new Spry.Data.XMLDataSet("/photos.php?galleryid=2000", "/gallery/photos/photo");

...

dsPhotos.removeObserver(myObserver);

Source Listing - Calling removeObserver() stops an object from being notified.

Functions as Observers

Functions can also be registered as observers. The main difference between object and function observers is that an object is only notified for the notification methods it defines, whereas a function observer will be called for every type of event notification.

var dsPhotos = new Spry.Data.XMLDataSet("/photos.php?galleryid=2000", "/gallery/photos/photo");

...


function myObserverFunc(notificationType, dataSet, data)
{
	if (notificationType == "onDataChanged")
		alert("onDataChanged called!";
	else if (notificationType == "onPostSort")
		alert("onPostSort called!";
};

dsPhotos.addObserver(myObserverFunc);

Source Listing - Functions can also be registered as an observer on a data set.

A function observer is registered with the same call to addObserver.

When the function is called, the first argument to past into it is the notfication type. This is a string that is the name of the notification. The second argument is the notifier, which in this case is the data set, and the third argument is the data for the notification.

To stop a function observer from receiving notifications, it must be removed from the list of observers with a call to removeObserver().

var dsPhotos = new Spry.Data.XMLDataSet("/photos.php?galleryid=2000", "/gallery/photos/photo");

...

dsPhotos.removeObserver(myObserverFunc);

Source Listing - Calling removeObserver() stops an object from being notified.


Dynamic Regions

A dynamic region is a portion of an HTML document that is bound to one or more data sets, and has the ability to regenerate itself as the data in the data sets change.

This automatic regeneration is possible because each dynamic region registers itself as an observer of each of the data sets it is bound to.

Region Observes Data Set

Figure - Each dynamic region on the page registers itself as an observer of the data sets it is bound to.

Anytime the data in any of these data sets is modified (loaded, updated, sorted, filtered, etc), the data sets fire off a notification to all of its observers, triggering each dynamic region that is listening to regenerate itself.

Data Set Notifies Region

Figure - As the data in the data set is changed, each dynamic region is notified, resulting in their regeneration.

Custom attributes are used to denote portions of the page that are dynamic regions and give special processing instructions that are used during the regeneration of region markup. These custom attributes are XML namespaced, to aid with document validation, so all documents that use Spry dynamic regions should include the following xmlns attribute in their HTML tag to declare the Spry namespace.

<!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">
...

</html>

Source Listing - HTML documents that use Spry regions must declare the Spry XML namespace.

Creating a Dynamic Region

Dynamic regions are contained completely within a single element. Adding a "spry:region" attribute with a value that is a space separated list of data set names, tells the framework that all of the content inside that element should be treated as a dynamic region that is bound to the named data sets.

<!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=iso-8859-1" />
<title>Dynamic Region Example</title>
<script type="text/javascript" src="../../includes/xpath.js"></script>
<script type="text/javascript" src="../../includes/SpryData.js"></script>
<script type="text/javascript">
var dsPhotos = new Spry.Data.XMLDataSet("/photos.php?galleryid=2000", "/gallery/photos/photo");

</script>
</head>
<body>
	<ul spry:region="dsPhotos">
		<li>{dsPhotos::path}</li>
	</ul>

</body>
</html>

Source Listing - This ul element is a dynamic region container that is bound to the dsPhotos data set.

Most elements can be a dynamic region container, but there are some exceptions. The following elements cannot be dynamic region containers:

COL, COLGROUP, FRAMESET, HTML, IFRAME, STYLE, TABLE, TBODY, TFOOT, THEAD, TITLE, TR

Although the elements listed above can't be a dynamic region container, they can exist inside a dynamic region container.

Data References

In a dynamic region, data references of the form:

{<data set name>::<data set column>}

are used as place holders to indicate where real data should be inserted when the region is re-generated.

<!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=iso-8859-1" />
<title>Dynamic Region Example</title>
<script type="text/javascript" src="../../includes/xpath.js"></script>
<script type="text/javascript" src="../../includes/SpryData.js"></script>

<script type="text/javascript">
var dsPhotos = new Spry.Data.XMLDataSet("/photos.php?galleryid=2000", "/gallery/photos/photo");
</script>
</head>
<body>
	<ul spry:region="dsPhotos">
		<li>{dsPhotos::@path}</li>

	</ul>
</body>
</html>

Source Listing - {dsPhotos::@path} is a data reference.

If a region is only bound to a single data set you can leave off the data set name in a data reference.

<!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=iso-8859-1" />
<title>Dynamic Region Example</title>

<script type="text/javascript" src="../../includes/xpath.js"></script>
<script type="text/javascript" src="../../includes/SpryData.js"></script>
<script type="text/javascript">

var dsPhotos = new Spry.Data.XMLDataSet("/photos.php?galleryid=2000", "/gallery/photos/photo");
</script>
</head>
<body>
	<ul spry:region="dsPhotos">
		<li>{@path}</li>

	</ul>
</body>
</html>

Source Listing - {@path} can be used as a data reference because the region is only bound to one data set.

As a dynamic region is re-generated, data references are replaced with data from the current row of the data set.

<!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=iso-8859-1" />
<title>Dynamic Region Example</title>

<script type="text/javascript" src="../../includes/xpath.js"></script>
<script type="text/javascript" src="../../includes/SpryData.js"></script>
<script type="text/javascript">

var dsPhotos = new Spry.Data.XMLDataSet("/photos.php?galleryid=2000", "/gallery/photos/photo");
</script>
</head>
<body>
	<ul>
		<li>sun.jpg</li>

	</ul>
</body>
</html>

Source Listing - {@path} was replaced with the data in the path column of the current row of the data set.

Each data set has a set of built-in data references that may be useful during the region re-generation process:

Like data set column names, these built-in data references must be prefixed with the name of the data set if the dynamic region is bound to more than one data set.

Looping Constructs

Dynamic regions currently support 2 looping constructs. One allows you to repeat an element and all of its content for each row in a given data set (spry:repeat), and another that allows you to repeat all of the children of a given element for each row in a given data set (spry:repeatchildren).

To designate an element as something that repeats, simply add an "spry:repeat" attribute to the element with a value that is the name of the data set you want repeat over:

<!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=iso-8859-1" />
<title>Dynamic Region Example</title>

<script type="text/javascript" src="../../includes/xpath.js"></script>
<script type="text/javascript" src="../../includes/SpryData.js"></script>
<script type="text/javascript">

var dsPhotos = new Spry.Data.XMLDataSet("/photos.php?galleryid=2000", "/gallery/photos/photo");
</script>
</head>
<body>
	<div spry:region="dsPhotos">
		<ul>

			<li spry:repeat="dsPhotos">{@path}</li>
		</ul>
	</div>
</body>
</html>

Source Listing - This li will now repeat for every row in the dsPhotos data set.

To repeat just the children of an element, simply use the "spry:repeatchildren" attribute instead:

<!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=iso-8859-1" />
<title>Dynamic Region Example</title>
<script type="text/javascript" src="../../includes/xpath.js"></script>

<script type="text/javascript" src="../../includes/SpryData.js"></script>
<script type="text/javascript">
var dsPhotos = new Spry.Data.XMLDataSet("/photos.php?galleryid=2000", "/gallery/photos/photo");
</script>

</head>
<body>
	<div spry:region="dsPhotos">
		<ul spry:repeatchildren="dsPhotos">
			<li>{@path}</li>

		</ul>
	</div>
</body>
</html>

Source Listing - The children of the ul will repeat for every row in the dsPhotos data set.

The "spry:repeat" and "spry:repeatchildren" examples above are functionally equivalent, but "spry:repeatchildren" becomes more useful when used in conjunction with conditional constructs, covered in the next section. Both examples would result in the following output:

<!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=iso-8859-1" />

<title>Dynamic Region Example</title>
<script type="text/javascript" src="../../includes/xpath.js"></script>
<script type="text/javascript" src="../../includes/SpryData.js"></script>

<script type="text/javascript">
var dsPhotos = new Spry.Data.XMLDataSet("/photos.php?galleryid=2000", "/gallery/photos/photo");
</script>
</head>
<body>
	<div>

		<ul>
			<li>sun.jpg</li>
			<li>tree.jpg</li>
			<li>surf.jpg</li>

		</ul>
	</div>
</body>
</html>

Source Listing - The code after the region is re-generated.

There may be times when you don't want to output the content in a repeat region for every row in the data set. You can limit what gets written out during the loop processing by adding an "spry:test" attribute to the element that has the "spry:repeat" or "spry:repeatchildren" attribute on 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" xmlns:spry="http://ns.adobe.com/spry">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=iso-8859-1" />

<title>Dynamic Region Example</title>
<script type="text/javascript" src="../../includes/xpath.js"></script>
<script type="text/javascript" src="../../includes/SpryData.js"></script>

<script type="text/javascript">
var dsPhotos = new Spry.Data.XMLDataSet("/photos.php?galleryid=2000", "/gallery/photos/photo");
</script>
</head>
<body>
	<div spry:region="dsPhotos">

		<ul>
			<li spry:repeat="dsPhotos" spry:test="'{@path}'.search(/^s/) != -1;">{@path}</li>
		</ul>
	</div>

</body>
</html>

Source Listing - This li will only be output if the first letter of the value of {@path} starts with the character 's'.

The value of this spry:test attribute can be any JavaScript expression that evaluates to zero/false or some non-zero value. If the expression returns a non-zero value, the content will be output. Keep in mind that since we are using XHTML, that any special characters like '&', '<' or '>' that may be used in a JavaScript expression will need to be converted to HTML entities. You can also use data references inside this JavaScript expression and the dynamic region processing engine will plug in the actual values from a data set just before evaluating the spry:test expression.

<!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=iso-8859-1" />
<title>Dynamic Region Example</title>

<script type="text/javascript" src="../../includes/xpath.js"></script>
<script type="text/javascript" src="../../includes/SpryData.js"></script>
<script type="text/javascript">

var dsPhotos = new Spry.Data.XMLDataSet("/photos.php?galleryid=2000", "/gallery/photos/photo");
</script>
</head>
<body>
	<div>
		<ul>

			<li>sun.jpg</li>
			<li>surf.jpg</li>
		</ul>
	</div>
</body>

</html>

Source Listing - Only paths that begin with 's' are written to the final output.

Conditional Constructs

Dynamic regions currently support 2 conditional constructs. The first is "spry:if":

<!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=iso-8859-1" />
<title>Dynamic Region Example</title>

<script type="text/javascript" src="../../includes/xpath.js"></script>
<script type="text/javascript" src="../../includes/SpryData.js"></script>
<script type="text/javascript">

var dsPhotos = new Spry.Data.XMLDataSet("/photos.php?galleryid=2000", "/gallery/photos/photo");
</script>
</head>
<body>
	<div spry:region="dsPhotos">
		<ul class="spry:repeat">

			<li spry:if="'{@path}'.search(/^s/) != -1;">{@path}</li>
		</ul>
	</div>
</body>
</html>

Source Listing - The li is only written out if the value of {@path} begins with the character 's'.

To make an element conditional, add an "spry:if" attribute to the element with a value that is a JavaScript expression that returns zero or non-zero values. A non-zero value returned by the JavaScript expression will result in the element being written to the final output.

If you need an if/else construct, use the "spry:choose" construct:

<!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=iso-8859-1" />
<title>Dynamic Region Example</title>

<script type="text/javascript" src="../../includes/xpath.js"></script>
<script type="text/javascript" src="../../includes/SpryData.js"></script>
<script type="text/javascript">

var dsPhotos = new Spry.Data.XMLDataSet("/photos.php?galleryid=2000", "/gallery/photos/photo");
</script>
</head>
<body>
	<div spry:region="dsPhotos">
		<div spry:choose="spry:choose">

			<div spry:when="'{@path}' == 'surf.gif'">{@path}</div>
			<div spry:when="'{@path}' == 'undefined'">Path was not defined.</div>
			<div spry:default="spry:default">Unexpected value for path!</div>

		</div>
	</div>
</body>
</html>

Source Listing - Example use of the spry:choose construct to color every other div.

The "spry:choose" construct provides functionality equivalent to a case statement, or an if/else if/else construct. To create an "spry:choose" structure add an "spry:choose" attribute to an element. Next, add one or more child elements with "spry:when" attributes on them. The value of an "spry:when" attribute should be a JavaScript expression that returns a zero or non-zero value.. If you want to have a default case, just in case all of the JS expressions for each spry:when attribute returns zero/false, add an element with an "spry:default" attribute. The "spry:default" attribute doesn't require a value, but XHTML states that all attributes must have a value, so we use the convention of setting the value of the attribute equal to its name.

The region processing engine will evaluate the "spry:when" attribute of each node in the order they are listed under their parent element. The "spry:default" element is always evaluated last, and only if no "spry:when" expression returned a non-zero value.

Region States

Spry supports the notion of region states. That is, at any given time, a region is either "loading" data, "ready" to display the data, or in an "error" state because one or more of the data sets it is bound to failed to load its data. A spry:state attribute, with a value of "loading", "error" or "ready", can be placed on elements *inside* a region container to associate it with a specific region state. This can be quite useful for displaying a loading message as the data for a region loads, or notifying the user that the region failed to get its data. As the region changes states, it will automatically re-generate its markup and display any elements with a spry:state attribute that matches the current state.

<div spry:region="dsEmployees">
	<div spry:state="loading">Loading employee data ...</div>
	<div spry:state="error">Failed to load employee data!</div>
	<ul spry:state="ready">
		<li spry:repeat="dsEmployees">{firstname} {lastname}</li>
	</ul>
</div>

Source Listing - Example use of the spry:state attribute to display loading and error messages.

It should be noted that any content that does not have a spry:state attribute on it, or is not a child/descendent of an element that has a spry:state attribute on it, will always be included in the output when the markup is re-generated. Also, children/descendents of an element with a spry:state attribute cannot have spry:state attributes. That is, nesting of elements with spry:state attributes is not supported.

Region Observer Notifications

Spry supports an observer mechanism that allows a developer to register an object or function to recieve a notification whenever the state of a region changes. This mechanism is almost identical to what is used for data sets with the following exceptions:

Objects as Region Observers

To receive notifications, an object must define a method for each notification it is interested in receiving, and then register itself as an observer on the region.

<script>

...

// Create an observer object and define the methods to receive the notifications
// it wants.
myObserver = new Object;
myObserver.onPostUpdate = function(notifier, data)
{
	alert("onPostUpdate called for " + data.regionID);
};

...

// Call addObserver() to register the object as an observer.
Spry.Data.Region.addObserver("employeeListRegion", myObserver);

...

// You can unregister your object so it stops recieving notifications
// with a call to removeObserver().
Spry.Data.Region.removeObserver("employeeListRegion", myObserver);

...

</script>

...

<ul id="employeeListRegion" spry:region="dsEmployees">

...

</ul>

Source Listing - Registering an object as an observer on a dynamic region.

The first argument for each notification method is the object that is sending the notification. So for region observers, this currently is *NOT* the region object. This may change in the future, but in the meantime, the second argument is an object that contains a regionID property that identifies the region that triggered the notification.

To stop an object from receiving notifications, the object must be removed from the list of observers with a call to removeObserver().

Functions as Region Observers

Functions can also be registered as observers. The main difference between object and function observers is that an object is only notified for the notification methods it defines, whereas a function observer will be called for every type of event notification.

<script>

...

function myRegionCallback(notificationState, notifier, data)
{
	if (notificationType == "onPreUpdate")
		alert(regionID + " is starting an update!");
	else if (notificationType == "onPostUpdate")
		alert(regionID + " is done updating!");
}

...

// Call addObserver() to register your function as an observer.
Spry.Data.Region.addObserver("employeeListRegion", MyRegionCallback);

...

// You can unregister your callback function so it stops recieving notifications
// with a call to removeObserver().
Spry.Data.Region.removeObserver("employeeListRegion", MyRegionCallback);

...

</script>

...

<ul id="employeeListRegion" spry:region="dsEmployees">

...

</ul>

Source Listing - Registering a function as an observer on a dynamic region.

A function observer is registered with the same call to addObserver.

When the function is called, the first argument passed into it is the notfication type. This is a string that is the name of the notification. The second argument is the notifier, which in this case is *NOT* the region object, which may change in the future. The third argument is a data object that has a regionID property which tells us what region triggered the notification.

To stop a function observer from receiving notifications, it must be removed from the list of observers with a call to removeObserver().

Here's a table that describes the current set of supported notifications:

Region Notification Types
onLoadingData One or more of the region's bound data sets is loading its data.
onPreUpdate All of the region's bound data sets have loaded successfully. The region is about to re-generate its markup.
onPostUpdate The region has re-generated its markup and has inserted it into the document.
onError An error occurred while loading data.

 


Hiding Data References

In some browsers, loading pages over a slow connection can result in the user briefly seeing the unprocessed regions and data references on the page before the document's onload notification fires.

The Spry dynamic region code automatically removes spry:region and spry:detailregion attributes from region container elements when initially processing them after the document's onload notification fires. The idea behind doing this was so that designers could use CSS to define a rule that hid any content that had a spry:region or spry:detail region attribute on it so that users would not see the unprocessed region. Once Spry programatically removed the attributes, any markup that was generated underneath the region container would magically appear. This worked fine until we switched to using XML namespaced attributes. Unfortunately the CSS in most browsers don't support XML namespaces, but if they did implement the W3C's current recommendation, the following CSS would work:

<style>
@namespace spry url(http://ns.adobe.com/spry);
 	
*[spry|region], *[spry|detailregion]{
	visibility: hidden;
}
</style>

Source Listing - These CSS rules should work in browsers that support XML namespaces within CSS.

Since no browser currently supports XML namespaces in CSS, we've had to resort to "Plan B". The dynamic region processing code was modified so that it automatically removes the class name "SpryHiddenRegion" from any class attribute on a spry:region or spry:detailregion. So now all you have to do is provide a CSS rule for the SpryHiddenRegion class:

<style>
.SpryHiddenRegion {
	visibility: hidden;
}
</style>

...

<div spry:region="dsEmployees" class="SpryHiddenRegion">
...
</div>

Source Listing - CSS rule for hiding regions with a class attribute containing the class SpryHiddenRegion.

An alternate way to hide just the data references, is to leverage the spry:content attribute. You can actually use a spry:content attribute as a substitute for a data reference. Since the actual data reference is an attribute on the element that has the spry:content attribute, it is not visible when the page loads.

<!-- Example of a normal region. -->
<div spry:region="dsEmployees">
	Hello my name is {firstname}.
</div>

<!-- Example of a region using spry:content. -->
<div spry:region="dsEmployees">
	Hello my name is <span spry:content="{firstname}"></span>.
</div>

Source Listing - An example of hiding a data references with a spry:content attribute on an element.


Master/Detail Relationships

When working with data, many applications/web pages use a Master/Detail display pattern. The basic idea is that you have one region of the page, called the master, that drives the display of another region, the detail. Typically, the master region displays an abbreviated form of a set of records, and the detail region displays more information about the current record.

Master/Detail

Figure - The table on the left is a master region, the detail region is to the right of it.

Master/Detail Changed
Figure - As the user selects a different object in the master region, the detail region updates.

The Spry framework makes it really easy to set up master/detail relationships between 2 or more dynamic regions of the page by taking advantage of a few key facts:

As you may have noticed, the data set is the one common thing between all of the facts mentioned above, so the techniques used to establish a master/detail relationship all center around the use of one or more data sets.

Lets walk through a few examples that show how to take advantage of these facts.

Example 1: Use "spry:detailregion" When the Master and Detail Share the Same Data Set

In this example the master and detail regions both rely on data from the same data set. You can see a working version of this example here.

The master region is a span that consists of a single select form widget that displays a list of all employee "usernames".

<!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=iso-8859-1" />
<title>Master/Detail Same Data Set</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("../dynamic_table/samples/data/employees-01.xml", "/employees/employee");
</script>
</head>
<body>
<span spry:region="dsEmployees">
<select spry:repeatchildren="dsEmployees" onchange="dsEmployees.setCurrentRow(this.value)">
	<option spry:if="{ds_RowNumber} == 0" value="{ds_RowID}" selected="selected">{username}</option>

	<option spry:if="{ds_RowNumber} != 0" value="{ds_RowID}">{username}</option>
</select>
</span>
<span spry:detailregion="dsEmployees">{@id} - {firstname} {lastname} - {phone} </span>
</body>
</html>

Source Listing - The master region contains a single select widget.

A select option is generated for each row in the dsEmployees data set, its value is set to the ds_RowID of the row it is associated with, and the display value for the option will be the "username" field of the row. A couple of "spry:if" options are necessary so we can add a "selected" attribute to the first option in the select widget to make sure that it is what shows by default when the widget is first displayed.

<!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=iso-8859-1" />
<title>Master/Detail Same Data Set</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("../dynamic_table/samples/data/employees-01.xml", "/employees/employee");
</script>
</head>
<body>
<span>
<select onchange="dsEmployees.setCurrentRow(this.value)">
	<option value="0" selected="selected">esmith</option>

	<option value="1">njohnson</option>
	<option value="2">swilliams</option>
	<option value="3">jjones</option>

	<option value="4">jbrown</option>
</select>
</span>

<span>123456 - Edward Smith - (415) 333-0235 </span>
</body>

</html>

Source Listing - After the master region is processed/regenerated, it looks something like this.

The detail region is a span that will display the employee id, first name, last name and phone number of the username that is selected in the widget.

<!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=iso-8859-1" />
<title>Master/Detail Same Data Set</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("../dynamic_table/samples/data/employees-01.xml", "/employees/employee");
</script>

</head>
<body>
<span spry:region="dsEmployees">
<select spry:repeatchildren="dsEmployees" onchange="dsEmployees.setCurrentRow(this.value)">
	<option spry:if="{ds_RowNumber} == 0" value="{ds_RowID}" selected="selected">{username}</option>

	<option spry:if="{ds_RowNumber} != 0" value="{ds_RowID}">{username}</option>
</select>
</span>
<span spry:detailregion="dsEmployees">{@id} - {firstname} {lastname} - {phone} </span>

</body>
</html>

Source Listing - The detail region displays more information for the username currently selected in the select widget.

You'll notice that the detail region uses an "spry:detailregion" attribute instead of an "spry:region" attribute to tell the framework that it is a dynamic region. "spry:region" and "spry:detailregion" are identical, except that dynamic regions with an "spry:detailregion" will regenerate themselves when they receive a "CurrentRowChanged" notification from any of the data sets they are bound to.

Master/Detail spry:detailregion

Figure - Regions with an "spry:detailregion" attribute also listen for CurrentRowChanged notifications.

As the user changes the value of the select widget, the onchange handler of the widget is triggered.

<!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=iso-8859-1" />
<title>Master/Detail Same Data Set</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("../dynamic_table/samples/data/employees-01.xml", "/employees/employee");
</script>

</head>
<body>
<span spry:region="dsEmployees">
<select spry:repeatchildren="dsEmployees" onchange="dsEmployees.setCurrentRow(this.value)">
	<option spry:if="{ds_RowNumber} == 0" value="{ds_RowID}" selected="selected">{username}</option>

	<option spry:if="{ds_RowNumber} != 0" value="{ds_RowID}">{username}</option>
</select>
</span>
<span spry:detailregion="dsEmployees">{@id} - {firstname} {lastname} - {phone} </span>

</body>
</html>

Source Listing - As the value of the select widget changes, the current row of the dsEmployees data set is changed.

The onchange handler simply calls the setCurrentRow() method on the dsEmployees data set and gives it the ID of the row in the data set that it is associated with. This results in a CurrentRowChanged notification being fired off to the detail region, causing it to regenerate itself.

Master/Detail spry:detailregion notification

Figure - The master region sets the current row which triggers a CurrentRowChanged notification to the detail region.

Example 2: Master and Detail Use 2 Different Data Sets

In this example we have one data set that will be used to display a list of states, and a second data set that will be used to display the list of cities for a given state. Because the list of cities for a given state can be quite long, the data for the second data set will be fetched "on-the-fly" whenever the current state changes. You can see a working version of example 2 here.

Since the list of states and cities can be quite long, this example will use select widgets to display information. As the user selects a state from the "State" select widget, the "Cities" select widget will automatically update with the list of cities for that state.

We start by creating the 2 data sets we need. One that will provide our list of states (dsStates) and one that will provide the list of cities for a given state (dsCities).

<!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=iso-8859-1" />
<title>States Example</title>
<script type="text/javascript" src="../../includes/xpath.js"></script>
<script type="text/javascript" src="../../includes/SpryData.js"></script>
<script type="text/javascript">
var dsStates = new Spry.Data.XMLDataSet("../data_set_overview/samples/data/states/states.xml", "states/state");
var dsCities = new Spry.Data.XMLDataSet("../data_set_overview/samples/data/states/{dsStates::url}", "state/cities/city");
</script>

</head>
<body>
</body>
</html>

Source Listing - The URL for dsCities depends on data from the dsStates data set.

If you look at the URL for dsCities, you'll notice that it has a data reference "{dsStates::url}" in it. The URL and XPath arguments to the XMLDataSet constructor are allowed to contain data references to other data sets, as long as these data references don't create a circular dependency between the data sets. That is you can't have data set 'B' depend on data set 'A', if 'A' already has a data reference to 'B'. Also, you can't have data set 'B' depend on data set 'A' if 'A' depends on data set 'C' and data set 'C' depends on 'B'.

If a data set has a URL or XPath with data references in it, the data set will register itself as an observer on every data set used by a data reference. This allows the data set to reload its data or reapply its XPath whenever any of the data set dependencies fire off a DataChanged or CurrentRowChanged notification. The data references will be converted to real data just before the data set tries to load the data at the given url or applies the XPath to the resulting XML DOM tree.

It should be noted that in this example, the URL "data_set_overview/samples/data/states/{dsStates::url}" results in a path to a particular sample XML file that contains data for a given state, but had this data come from a real web service or application, we could've easily used an URL that looked something like "/webapp/cities.php?stateid={dsStates::@id}".

Now that we've set up our data sets, lets set up our master and detail regions. The master region will consist of a select form widget that displays the list of states that it gets from the dsStates data set..

<!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=iso-8859-1" />
<title>States Example</title>
<script type="text/javascript" src="../../includes/xpath.js"></script>

<script type="text/javascript" src="../../includes/SpryData.js"></script>
<script type="text/javascript">
var dsStates = new Spry.Data.XMLDataSet("../data_set_overview/samples/data/states/states.xml", "states/state");
var dsCities = new Spry.Data.XMLDataSet("../data_set_overview/samples/data/states/{dsStates::url}", "state/cities/city");

</script>
</head>
<body>
<form name="selectForm">
	State:
	<span spry:region="dsStates" id="stateSelector">

		<select spry:repeatchildren="dsStates" name="stateSelect">
			<option spry:if="{ds_RowNumber} == 0" value="{ds_RowID}" selected="selected">{name}</option>

			<option spry:if="{ds_RowNumber} != 0" value="{ds_RowID}">{name}</option>
		</select>
	</span>
</form>

</body>
</html>

Source Listing - The master region displays a select widget that is the list of states.

The detail region will consist of a select form widget that displays the list of cities for the current state that it gets from the dsCities data set.

<!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=iso-8859-1" />
<title>States Example</title>
<script type="text/javascript" src="../../includes/xpath.js"></script>

<script type="text/javascript" src="../../includes/SpryData.js"></script>
<script type="text/javascript">
var dsStates = new Spry.Data.XMLDataSet("../data_set_overview/samples/data/states/states.xml", "states/state");
var dsCities = new Spry.Data.XMLDataSet("../data_set_overview/samples/data/states/{dsStates::url}", "state/cities/city");

</script>
</head>
<body>
<form name="selectForm">
	State:
	<span spry:region="dsStates" id="stateSelector">

		<select spry:repeatchildren="dsStates" name="stateSelect">
			<option spry:if="{ds_RowNumber} == 0" value="{ds_RowID}" selected="selected">{name}</option>

			<option spry:if="{ds_RowNumber} != 0" value="{ds_RowID}">{name}</option>
		</select>
	</span>
	City:

	<span spry:region="dsCities" id="citySelector">
		<select spry:repeatchildren="dsCities" name="citySelect">
			<option spry:if="{ds_RowNumber} == 0" value="{name}" selected="selected">{name}</option>

			<option spry:if="{ds_RowNumber} != 0" value="{name}">{name}</option>
		</select>
	</span>
</form>

</body>
</html>

Source Listing - The detail region displays a select widget that is the list of cities for a given state.

At this point, we now have the following observer relationships between our data sets and dynamic regions:

Master/Detail Observer Relationships

Figure - Observer relationships between the data sets and dynamic regions.

To get the "City" select widget to update automatically whenever the user selects a new state from the "State" select widget, we need to add an onchange handler to the "State" select widget.

<!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=iso-8859-1" />

<title>States Example</title>
<script type="text/javascript" src="../../includes/xpath.js"></script>
<script type="text/javascript" src="../../includes/SpryData.js"></script>

<script type="text/javascript">
var dsStates = new Spry.Data.XMLDataSet("../data_set_overview/samples/data/states/states.xml", "states/state");
var dsCities = new Spry.Data.XMLDataSet("../data_set_overview/samples/data/states/{dsStates::url}", "state/cities/city");
</script>
</head>
<body>

<form name="selectForm">
	State:
	<span spry:region="dsStates" id="stateSelector">
		<select spry:repeatchildren="dsStates" name="stateSelect" onchange="document.forms[0].citySelect.disabled = true; dsStates.setCurrentRow(this.value);">

			<option spry:if="{ds_RowNumber} == 0" value="{ds_RowID}" selected="selected">{name}</option>
			<option spry:if="{ds_RowNumber} != 0" value="{ds_RowID}">{name}</option>

		</select>
	</span>
	City:
	<span spry:region="dsCities" id="citySelector">
		<select spry:repeatchildren="dsCities" name="citySelect">

			<option spry:if="{ds_RowNumber} == 0" value="{name}" selected="selected">{name}</option>
			<option spry:if="{ds_RowNumber} != 0" value="{name}">{name}</option>

		</select>
	</span>
</form>
</body>
</html>

Source Listing - Add an onchange event handler to the State select widget.

This onchange handler disables the City select widget and then sets the current row on the dsStates data set. This triggers the dsStates data set to fire off a CurrentRowChanged notification, causing the dsCities data set to reload its data. When the dsCities data set finishes loading its data, it fires off a DataChanged notification, causing the detail region to regenerate itself, resulting in the creation of a new "City" select widget that contains all of the cities for the state in the current row of the dsState data set.

Master/Detail URL/XPath data reference auto update

Figure - Setting the current row on data set 'A' sets off a chain of events that causes the detail region to automatically update.


Behavior Attributes

Behavior attributes can be placed on elements within a dynamic region to automatically enable common behaviors that would ordinarily require some manual programming. Currently, only 2 behavior attributes are supported:

spry:hover

The value for the "spry:hover" attribute should be the name of the class to put on the element whenever the mouse enters or exits the element.

<!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=iso-8859-1" />
<title>Behavior Attributes Example</title>
<style>
.myHoverClass {
	background-color: yellow;
}

</style>
<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("../dynamic_table/samples/data/employees-01.xml", "/employees/employee"); </script> </head> <body> <div spry:region="dsEmployees"> <ul> <li spry:repeat="dsEmployees" spry:hover="myHoverClass">{username}</li> </ul> </div> </body> </html>

Source Listing - Whenever the mouse enters an li element, the "myHoverClass" class name will be added to the elements class attribute. It will automatically be removed when the mouse exits the element.

spry:select

The value for the "spry:select" attribute should be the name of the class to put on the element whenever the mouse is clicked over the element, which causes the element to appear selected.

<!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=iso-8859-1" />
<title>Behavior Attributes Example</title>
<style>
.myHoverClass {
	background-color: yellow;
}

.mySelectClass {
	color: white;
	background-color: black;
}
</style>
<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("../dynamic_table/samples/data/employees-01.xml", "/employees/employee");
</script>
</head>
<body>
<div spry:region="dsEmployees">

	<ul>
		<li spry:repeat="dsEmployees" spry:hover="myHoverClass" spry:select="mySelectClass">{username}</li>
	</ul>

</div>
</body>
</html>

Source Listing - Whenever the mouse is clicked over an li element, the "mySelectClass" class name will be added to the elements class attribute.

If an element on the page with an "spry:select" attribute was previously selected, the class name used as the value for its "spry:select" attribute will automatically be removed, in effect unselecting that element.

You can use a "spry:selectgroup" attribute in conjunction with an "spry:select" attribute to allow you to have more than one set of selections on a page. See the RSS Reader example that is distributed with the framework for a working example of 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" xmlns:spry="http://ns.adobe.com/spry">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=iso-8859-1" />
<title>Behavior Attributes Example</title>

<style>
.myHoverClass {
	background-color: yellow;
}
.mySelectClass {
	color: white;
	background-color: black;
}
.myOtherSelectClass {
	color: white;
	background-color: black;
}
</style><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("../dynamic_table/samples/data/employees-01.xml", "/employees/employee"); </script> </head> <body> <div spry:region="dsEmployees"> <ul> <li spry:repeat="dsEmployees" spry:hover="myHoverClass" spry:select="mySelectClass" spry:selectgroup="username">{username}</li> </ul> <ul> <li spry:repeat="dsEmployees" spry:hover="myHoverClass" spry:select="myOtherSelectClass" spry:selectgroup="firstname">{firstname}</li> </ul> </div> </body> </html>

Source Listing - Use an spry:selectgroup attribute to group elements that effect each other.

The value of a "spry:selectgroup" attribute is an arbitrary name. The idea is that any element that uses the same name for its "spry:selectgroup" attribute will automatically become unselected when another element with the same select group name is clicked. Other elements with differing "spry:selectgroup" values are unaffected.


Progressive Enhancement and Data Accessibility

Progressive Enhancement is the approach of marking up a document for the lowest common denominator of browser functionality and then enhancing the presentation and behavior of the page, using modern technologies such as CSS, JavaScript, Flash, Java, SVG, etc. The idea is that pages created with this approach will provide an enhanced experience in modern browsers, but the data is still accessible and the page still functional in the absence of these technologies.

Although all of the Spry examples in this document, up until this point, have dealt with using JavaScript to load XML data and generate regions of the document on the fly, it is possible to use Spry in a progressive enhancement manner described in this article about the Hijax methodology. The Hijax methodology describes the method of using JavaScript to unobtrusively attach event handlers to items on the page like links to catch user events, like clicks, and perform some other action. It also describes the methodology of adding the ability to replace parts of your document with markup fragments delivered asynchronously from the server to avoid having to load an entire page.

As trivial example of using Spry with this methodology, you could start with an HTML page filled with static data and links:

<!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=iso-8859-1" />
<title>Hijax Demo - Notes 1</title>

</head>

<body>
<a href="notes1.html">Note 1</a>
<a href="notes2.html">Note 2</a>
<a href="notes3.html">Note 3</a>

<div>
	<p>This is some <b>static content</b> for note 1.</p>
</div>
</body>
</html>

Source Listing - Traditional page with static content and links.

To progressively enhance this page with Spry, so that you don't have to load an entirely new page when clicking on the links, you would first have to make sure that the page fragments of each page that the links referred to were accessible via XML. Here's one way to externalize the static data:

<?xml version="1.0" encoding="iso-8859-1"?>
<notes>
	<note><![CDATA[<p>This is some <b>dynamic content</b> for note 1.</p>]]></note>

	<note><![CDATA[<p>This is some <b>dynamic content</b> for note 2.</p>]]></note>
	<note><![CDATA[<p>This is some <b>dynamic content</b> for note 3.</p>]]></note>

</notes>

You can then apply Spry to our HTML page the following manner:

<!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=iso-8859-1" />
<title>Hijax Demo - Notes 1</title>
<script language="JavaScript" type="text/javascript" src="includes/xpath.js"></script>

<script language="JavaScript" type="text/javascript" src="includes/SpryData.js"></script>
<script language="JavaScript" type="text/javascript">
<!--
var dsNotes = new Spry.Data.XMLDataSet('data/notes.xml', "/notes/note");
-->

</script>
</head>

<body>
<a href="note1.html" onclick="dsNotes.setCurrentRowNumber(0); return false;">Note 1</a>
<a href="note2.html" onclick="dsNotes.setCurrentRowNumber(1); return false;">Note 2</a>

<a href="note3.html" onclick="dsNotes.setCurrentRowNumber(2); return false;">Note 3</a>
<div spry:detailregion="dsNotes" spry:content="{note}">
	<p>This is some <b>static content</b> for note 1.</p>

</div>
</body>
</html>

Source Listing - Traditional page with Spry added to it.

The Spry code added above looks very familiar, but you'll notice that there is a "spry:content" attribute on the div that is the spry:detailregion. This spry:content attribute tells Spry's dynamic region processing code to replace the static data, currently contained in the region, with the data represented by the data reference in its attribute value if there is ever any data in the data set the region is bound to.

So if this page is loaded in a browser with JavaScript disabled, it gracefully degrades and you get the same functionality as our original page with static content and traditional link navigation. If JavaScript is enabled, our data set will load our XML data and replace the static content in the region, and clicking on the links will cause our region to update with markup from the data set.

One note about the Spry example above. Hijax promotes the use of unobtrusively attaching event handlers to links. The example above intentionally used onclick attributes to quickly illustrate the point of attaching a JS event handler.


Copyright © 2006. Adobe Systems Incorporated.
All rights reserved.