Nested XML Data Sample

This page demonstrates the 2 methods now available to developers for accessing nested XML data:

The examples on this page will be working with an XML schema that looks something like this:

You can see the actual XML data file used here.

This schema demonstrates a couple of nested data scenarios that previous versions of Spry (Pre-Release 1.4 and earlier) had a hard time dealing with, specifically the use of sub-structures like <batters> in this example:

<items>
	<item id="0001" type="donut">
		<name>Cake</name>
		<ppu>0.55</ppu>
		<batters>
			<batter id="1001">Regular</batter>
			<batter id="1002">Chocolate</batter>
			<batter id="1003">Blueberry</batter>
		</batters>
		<topping id="5001">None</topping>
		<topping id="5002">Glazed</topping>
		<topping id="5005">Sugar</topping>
		<topping id="5006">Sprinkles</topping>
		<topping id="5003">Chocolate</topping>
		<topping id="5004">Maple</topping>
	</item>

	...

</items>

when the <item> node (XPath: /items/item) was selected by the Data Set. Repeating nodes that were directly underneath the selected node, like <topping> were also a problem:

<items>
	<item id="0001" type="donut">
		<name>Cake</name>
		<ppu>0.55</ppu>
		<batters>
			<batter id="1001">Regular</batter>
			<batter id="1002">Chocolate</batter>
			<batter id="1003">Blueberry</batter>
		</batters>
		<topping id="5001">None</topping>
		<topping id="5002">Glazed</topping>
		<topping id="5005">Sugar</topping>
		<topping id="5006">Sprinkles</topping>
		<topping id="5003">Chocolate</topping>
		<topping id="5004">Maple</topping>
	</item>

	...

</items>

Controlling the Flattening Process

By default, when you select a node via XPath, the XML data set will follow this algorithm for flattening the selected node into a row:

All other child elements are ignored. The main reason they are ignored is because it's too hard to intelligently flatten nested repeating data into something predictable, especially when many XML schemas allow for variations in the format. For example, some schemas allow zero, one, or more child elements with the same name.

For Spry Pre-Release 1.5, we have added the ability for the user to give the data set hints on how to flatten the data. The following examples illustrate how to do this, and what the results look like.


To start us off, lets imagine that we want to generate a table of all items we have. We will start off by creating a data set like we normally would, and then define some markup with spry attributes and data references in it to describe how we would like to generate our table:

var dsItems1 = new Spry.Data.XMLDataSet("../../data/donuts.xml", "/items/item");

...

<div spry:region="dsItems1">
	<table class="dataTable">
		<tr>
			<th>ID</th>
			<th>Name</th>
			<th>Price Per Unit</th>
			<th>Batter</th>
			<th>Topping</th>
		</tr>
		<tr spry:repeat="dsItems1">
			<td>{@id}</td>
			<td>{name}</td>
			<td>{ppu}</td>
			<td>{batters/batter}
			<td>{topping}
		</tr>
	</table>
</div>

We end up with the following table when viewed in a browser:

ID Name Price Per Unit Batter Topping
{@id} {name} {ppu} {batters/batter} {topping}

Not exactly what we wanted, but its a start. This is exactly the output you would get with Spry Pre-Release 1.4 and earlier. You get one row in the data set for each node that was matched by the data set's XPath with a column schema that looked something like this:

ds_RowID @id @type name ppu topping topping/@id

Notice that in the "Topping" column we actually get a value, that's because if a selected node contains any child elements that repeats, like <topping>, and that child element has no child elements, the value of the last instance ends up being the value stored in that column. Notice also, that we have "undefined" values in our "Batter" column. That's because our flattening algorithm failed to flatten the <batters> sub structure because our flattening algorithm doesn't handle child elements with element children.


Lets try to fix the <batters> problem by giving the data set a hint that it needs to include the <batters> nested structure in its flattening process. You do this by passing a "subPaths" constructor option. The value of the "subPaths" constructor option can be an XPath string that is relative to the data set's main XPath, which describes the path down to the structure you want to include in the flattening process. In this specific case, our sub-structure contains repeating <batter> nodes, so we want to specify a relative XPath of "batters/batter":

var dsItems2 = new Spry.Data.XMLDataSet("../../data/donuts.xml", "/items/item", { subPaths: "batters/batter" });

Using the same HTML markup as the previous example, this yields:

ID Name Price Per Unit Batter Topping
{@id} {name} {ppu} {batters/batter} {topping}

The first thing you will notice is that there are more items in the table. Because our subPath selected another set of repeating nodes, the results of flattening this subPath were merged/joined back with our original set of rows. The schema for our data set rows now looks something like this:

ds_RowID @id @type name ppu batters/batter batters/batter/@id topping topping/@id

 


Now lets try to fix the <topping> problem by giving the data set a hint that it needs to include the <topping> repeated nodes in its flattening process. The "subPaths" constructor option can also take an array of sub path strings. This allows us to pass both the "batters/batter" and "topping" sub paths to our data set constructor:

var dsItems3 = new Spry.Data.XMLDataSet("../../data/donuts.xml", "/items/item", { subPaths: [ "batters/batter", "topping" ] });

Using the same HTML markup as the previous example, this yields:

ID Name Price Per Unit Batter Topping
{@id} {name} {ppu} {batters/batter} {topping}

This yields even more rows in our table. What happened here is that the data set flattened the set of nodes that matched its initial XPath of "/items/item". It then flattened the sub path for "batters/batter" and joined the results of that with its original set of rows. It then flattened the "topping" sub path and merged those results with the previously calculated set of rows. In effect, what you get is a set of permutations for every sub path specified in the "subPaths" constructor option.

The schema for the rows in our data set is the same as in our previous example, the exception being that now each row has the correct/expected topping value and id in it:

ds_RowID @id @type name ppu batters/batter batters/batter/@id topping topping/@id

 


This example uses the exact same data set as the previous example, but it illustrates how you access the attributes of the nodes selected by the sub paths. In this example, we will combine the values of the id attributes from the <item>, <batter>, and <topping> nodes to produce a unique SKU number. We'll also add the ability to sort on the columns:

<div class="liveSample" spry:region="dsItems3">
	<table class="dataTable">
		<tr>
			<th spry:sort="@id batters/batter/@id topping/@id">SKU</th>
			<th spry:sort="@id">ID</th>
			<th spry:sort="name">Name</th>
			<th spry:sort="ppu">Price Per Unit</th>
			<th spry:sort="batters/batter">Batter</th>
			<th spry:sort="topping">Topping</th>
		</tr>
		<tr spry:repeat="dsItems3">
			<td>{@id}-{batters/batter/@id}-{topping/@id}</td>
			<td>{@id}</td>
			<td>{name}</td>
			<td>{ppu}</td>
			<td>{batters/batter} </td>
			<td>{topping} </td>
		</tr>
	</table>
</div>

This markup yields the following table:

SKU ID Name Price Per Unit Batter Topping
{@id}-{batters/batter/@id}-{topping/@id} {@id} {name} {ppu} {batters/batter} {topping}

Using Nested Data Sets

Aside from controlling the flattening process, the Spry Pre-Release 1.5 introduces the concept of a "Nested Data Set". Nested data sets derive their data from another data set, considered its parent/master. The data inside the nested data set is determined by the current row of the parent data set. Any time the current row of the parent data set changes, the data inside the nested data set is updated automatically.

Exactly how the nested data set extracts its data from the parent data set depends on the actual implementation of both the master and nested data sets. For the Spry.Data.NestedXMLDataSet, its parent data set *must* be a Spry.Data.XMLDataSet, or another Spry.Data.NestedXMLDataSet. The actual implementation of the NestedXMLDataSet extracts its data directly from the same in-memory copy of the XML DOM document used by its parent. This means that for any XML document used by a parent data set and one or more nested XML data sets, the document is only loaded off the wire once. It should be explicitly stated that if new XML data must be loaded from the server, that the actual loading be done by the parent Spry.Data.XMLDataSet. NestedXMLDataSets do not have any ability to load data off the wire, from a server.


In this example, we set up a parent data set that has 2 nested data sets that load data from different parts of the XML underneath the nodes selected by the parent's XPath. Notice in the sample code below that we now include an extra JS file called SpryNestedXMLDataSet.js:

<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" src="../../includes/SpryNestedXMLDataSet.js"></script>

...

<script type="text/javascript">

...

// Setup a parent data set:

var dsItems1 = new Spry.Data.XMLDataSet("../../data/donuts.xml", "/items/item");

// Setup a couple of nested data sets:

var dsBatters = new Spry.Data.NestedXMLDataSet(dsItems1, "batters/batter");
var dsToppings = new Spry.Data.NestedXMLDataSet(dsItems1, "topping");

...

</script>

...

<table>
	<tr>
		<th>Region using dsItems1 (Parent)</th>
		<th>Region using dsBatters (Nested)</th>
		<th>Region using dsToppings (Nested)</th>
	</tr>
	<tr>
		<td spry:region="dsItems1">
			<ul spry:repeatchildren="dsItems1" spry:choose="">
				<li spry:when="{ds_CurrentRowNumber} == {ds_RowNumber}" spry:setrow="dsItems1" spry:select="select" spry:hover="hover" spry:selected="">{name}</li>
				<li spry:default="" spry:setrow="dsItems1" spry:select="select" spry:hover="hover">{name}</li>
			</ul>
		</td>
		<td spry:region="dsBatters">
			<ul spry:repeatchildren="dsBatters">
				<li>{batter}</li>
			</ul>
		</td>
		<td spry:region="dsToppings">
			<ul spry:repeatchildren="dsToppings">
				<li>{topping}</li>
			</ul>
		</td>
	</tr>
</table>

When you click on a row in the parent list below, it updates the current row of the parent data set. Notice how the other 2 lists, which use nested data sets, change? This happens because anytime the parent's row changes, they reload their data to match.

Region using dsItems1 (Parent) Region using dsBatters (Nested) Region using dsToppings (Nested)
  • {name}
  • {name}
  • {batter}
  • {topping}

 


An interesting feature of nested data sets is that if it is used within a looping context that iterates over the rows of its parent data set, any data references from the nested data set that are used *inside* the loop are kept in sync with the current row of the parent being processed. This makes it ideal for writing out data that is grouped by data in the parent data set.

As an example, suppose that we want to write out all of the toppings available for a given item:

<div spry:region="dsItems1 dsToppings">
	<ul>
		<li spry:repeat="dsItems1">{dsItems1::name}
			<ul>
				<li spry:repeat="dsToppings">{dsToppings::topping}</li>
			</ul>
		</li>
	</ul>
</div>

This would yield:

 


You can still use nested data sets in combination with its parent data set to generate a table that looks something like what we saw in the previous section:

<div class="liveSample" spry:region="dsItems1 dsToppings">
		<table class="dataTable">
			<tr>
				<th spry:sort="dsItems1 @id">ID</th>
				<th spry:sort="dsItems1 name">Name</th>
				<th spry:sort="dsItems1 ppu">Price Per Unit</th>
				<th spry:sort="dsToppings topping">Topping</th>
			</tr>
			<tbody spry:repeatchildren="dsItems1">
				<tr spry:repeat="dsToppings">
					<td>{dsItems1::@id}</td>
					<td>{dsItems1::name}</td>
					<td>{dsItems1::ppu}</td>
					<td>{dsToppings::topping} </td>
				</tr>
			</tbody>
		</table>
</div>

Although it looks similar to the tables from the previous "Controlling the Flattening Process" section:

ID Name Price Per Unit Topping
{dsItems1::@id} {dsItems1::name} {dsItems1::ppu} {dsToppings::topping}

there are some important differences. For instance, if you click on the headers for ID, Name, and Price Per Unit, you will notice that the entire table seems to sort based on those values. But, if you click on the Topping header, the toppings seem to sort only within groups. This is because whenever you sort or filter a nested data set, it applies those operations to the set of data associated with each row of the parent data set. Another way to think about this is: inside a nested data set, there are multiple data sets, one for each row of the parent data set. Any time the current row of the parent data set changes, the nested data set simply swaps in the data from the internal data set that is associated with that parent row. When you sort or filter the nested data set, the sorting and filtering functions operate on each individual internal data set inside the nested data set.

So the drawback with this approach, is that you cannot sort across *all* of the nested data, even though the presentation of the data on-screen with a table makes it look like you can.