Download Patches Future

Spry:DougPatch v2

This document details a list of patches that I have made to Spry 1.3 (made by the fine folks at Adobe), to fix some bugs, increase functionality, and overall, make Spry even better.

This version was a hefty change to add JSON support to the framework, by far probably the most important change.

Download

You can get the latest version of Spry with all of my patches included here. I provide this as a service to those who would like to incorporate my changes. For documentation on Spry itself, please see Adobe's website.

Questions

Please post any questions you have about these patches to the Spry forum so that others can benefit, or send me a private message there. My contact ID is dougsm.

Patches Explained

This wil give you a list of all the changes I have made to Spry, why, how I made the change, and any additional documentation you might need for the feature. These are all unified patches. I'd suggest you just grab the actual full file. It'll be easier.

JSON Data Set

I made three changes to get JSON support into the framework. First, I changed all the classes that were XMLDataSet to RemoteDataSet. This forms the basis for all data that is fetched via XMLHttpRequest. I then created a new XMLDataSet class that sublcasses RemoteDataSet. This class implements various methods that have to be written to handle processing of the datafile it gets back. It should do everything that it previously did. And finally, I created a JSONDataSet class that processes that data as a JSON file.

I even managed to create a very basic XPath-like mechanism for picking data from the JSON file. When you create a Spry.Data.JSONDataSet, the second parameter should be a string specified as array indexes, [index]. As many index values as you want can be specified, and they can be either numbers (for Arrays) or strings (for properties). So you could specify a path like ["foo"][2]. Since each value inside of a [] will be evaluated before the JSON data will be looked at, you need to make sure that strings return strings, so make sure to quote them, unless you wish them to be treated as functions. This string should work with references to other datasets in it. So something like [2][{ds::lookup}][3] should work as expected.

Additionally, since I could not determine how everyone would want to try to find data in a JSON data object, if you pass a function as the second value in the constructor, or via the new setDataSetPath() method that exists on RemoteDataSet, then that function will be called with the JSON document and should return an array of objects that can be processed as a dataset.

And finally, if you don't give it a path or a function, then the document itself will be treated as the data for the dateset.

Since the changes for this are extensive, they are not documented here.

This functionality has only been lighted tested. Use at your own risk.

Undocumented Feature: spry:selected

This isn't a patch I made, but is a new feature that I found while going through the code, but didn't find documented anywhere: spry:selected. Used with the spry:select and spry:selectgroup it allows you to specify which item in the group is selected when the region regenerates. I needed this for a Spry-built tabbing system that has some display issues before, but now works perfectly.

Prototype 1.5_rc0

The initial version of Spry 1.2 did not work with Prototype 1.5_rc0 (found in the script.aculo.us latest 1.6.1 version here).

The 1.3 version of Spry fixed the issues with Prototype.

Extend spry:state

I found that the new spry:state feature to be a really nice addition to the toolkit. I was able to replace a bunch of code that I had implemented to basically do the same thing but this is much cleaner than what i had. However, I immediately saw that spry:state could be much more powerful with a little change to the code.

In my particular case, I had three states: Loading, Ready, and Empty. Empty being the same as Ready except for it shows up when the dataset has no data in it. I could see other states being useful, depending on how I get data. Perhaps I want to show a popup if I get less than 10 rows, and a select browser if I get more than 10. I know I could use spry:if, but spry:state just seems a little more elegant. So I set about making a way to have arbitray 'ready' states.

Spry.Data.DataSet delegate: I created an accessor methods for a delegate object that is attached to a dataset. This object should have one function on it, onReadyState(dataSet, regionId), which is a function that takes its first parameter a that has loaded its data is is now 'ready', and the second is the regionId of the region that is being updated. The function should return the name of a spry:state that should be displayed. If no value is returned, then 'ready' will be used.

Note: I didn't make delegates for onErrorState or onLoadingState as I didn't really feel that those were states that really needed to be able to be setup in different ways. Also it looked like that code depended on 'error' and 'loading' to always be that.

Note: I also thought about putting the delegate on the region itself, as that is the item that is actually changing, and you could attach different delegate to different regions for the same dataset based on what region needed to do what. I'm not really positive that that might not be a better way to go. But it is the loading of the dataset that actually determines the states.

@@ -1302,6 +1302,8 @@
 	this.lastSortOrder = "";
 
 	this.loadIntervalID = 0;
+	
+	this.delegate = null;
 };
 
 Spry.Data.DataSet.prototype = new Spry.Utils.Notifier();
@@ -1724,6 +1726,17 @@
 	this.loadIntervalID = null;
 };
 
+Spry.Data.DataSet.prototype.getDelegate = function()
+{
+	return this.delegate;
+}
+
+Spry.Data.DataSet.prototype.setDelegate = function(obj)
+{
+	this.delegate = obj;
+}
+
+
 Spry.Data.DataSet.nextDataSetID = 0;
 
 //////////////////////////////////////////////////////////////////////
@@ -2474,14 +2490,23 @@
 
 	if (!allDataSetsReady)
 	{
-		Spry.Data.Region.notifyObservers("onLoadingData", this.name);
+		Spry.Data.Region.notifyObservers("onLoadingData", this.name, this.regionNode);
 
 		// Just return, this method will get called again automatically
 		// as each data set load completes!
 		return;
 	}
 
-	this.setState("ready");
+	var status = null;
+	
+	if (ds.getDelegate() && ds.getDelegate().onReadyState) {
+		status = ds.getDelegate().onReadyState(ds, this.name);
+	}
+	
+	if (status)
+		this.setState(status)
+	else
+		this.setState("ready");
 };
 
 Spry.Data.Region.prototype.clearContent = function()

RegionNode Passing On onXXXUpdate Events

I was setting up a region observer for an onPostUpdate, where I needed to attach an event listener to the elements in the generated code. I realize now that I could have attached a listener to the node itself, and I didn't necessarily need to do it on the generated elements, but whatever, I went down this path. Now all the region observers will receive as their last argument, a object with a regionID specifying the region that changed. Well, I could get the actual node easily enough with this, but why bother when Spry already knows it? So in the patch below, I just make Spry pass the regionNode value to the Spry.Data.Region.notifyObservers method, and have that method add regionNode as an additional parameter that it sends to the observers. Provides a quick shortcut to getting the actual node that source was generated into.

I made an additional update to this after I posted the patch to the forum, to also pass what spry:state state was getting set. This allowed me to know whether or not the region was being regenerated for an error, loading or ready state, and then take the appropriate steps. This parameter is regionState.

@@ -2268,11 +2284,11 @@
 		n.removeObserver(observer);
 };
 
-Spry.Data.Region.notifyObservers = function(methodName, regionID)
+Spry.Data.Region.notifyObservers = function(methodName, regionID, regionNode, regionState)
 {
 	var n = Spry.Data.Region.notifiers[regionID];
 	if (n)
-		n.notifyObservers(methodName, { regionID: regionID });
+		n.notifyObservers(methodName, { regionID: regionID, regionNode: regionNode, regionState: regionState });
 };
 
 Spry.Data.Region.RS_Error = 0x01;
@@ -2295,7 +2311,7 @@
 	if (this.states[stateName])
 	{
 		if (!suppressNotfications)
-			Spry.Data.Region.notifyObservers("onPreUpdate", this.name);
+			Spry.Data.Region.notifyObservers("onPreUpdate", this.name, this.regionNode, stateName);
 	
 		// Make the region transform the xml data. The result is
 		// a string that we need to parse and insert into the document.
@@ -2316,7 +2332,7 @@
 			this.attachBehaviors();
 	
 		if (!suppressNotfications)
-			Spry.Data.Region.notifyObservers("onPostUpdate", this.name);
+			Spry.Data.Region.notifyObservers("onPostUpdate", this.name, this.regionNode, stateName);
 	}
 };
 
@@ -2371,7 +2387,7 @@
 {
 	if (this.currentState != "error")
 		this.setState("error");
-	Spry.Data.Region.notifyObservers("onError", this.name);
+	Spry.Data.Region.notifyObservers("onError", this.name, this.regionNode);
 };
 
 Spry.Data.Region.prototype.onCurrentRowChanged = function(dataSet, data)
@@ -2474,14 +2490,23 @@
 
 	if (!allDataSetsReady)
 	{
-		Spry.Data.Region.notifyObservers("onLoadingData", this.name);
+		Spry.Data.Region.notifyObservers("onLoadingData", this.name, this.regionNode);
 
 		// Just return, this method will get called again automatically
 		// as each data set load completes!
 		return;
 	}
 
-	this.setState("ready");
+	var status = null;
+	
+	if (ds.getDelegate() && ds.getDelegate().onReadyState) {
+		status = ds.getDelegate().onReadyState(ds, this.name);
+	}
+	
+	if (status)
+		this.setState(status)
+	else
+		this.setState("ready");
 };
 
 Spry.Data.Region.prototype.clearContent = function()

XMLDataSet selectOnLoad Option

This is a pretty trivial patch. I need my application to NOT select the first row when a dataset is loaded. What I'm doing requires the user to actually select the specific row themselves, and there was no point in triggering the numerous detail fetches that would happen if the toolkit did it for me.

To make use of this one, just add selectOnLoad:false to the list of options you send when you set up your dataset.

@@ -1282,7 +1282,7 @@
 
 	this.name = "";
 	this.internalID = Spry.Data.DataSet.nextDataSetID++;
-	this.curRowID = 0;
+	this.curRowID = -1;
 	this.data = null;
 	this.unfilteredData = null;
 	this.dataHash = null;
@@ -1748,6 +1761,7 @@
 	this.dataSetsForDataRefStrings = new Array;
 	this.hasDataRefStrings = false;
 	this.useCache = true;
+	this.selectOnLoad = true;
 
 	// Create a loadURL request object to store any load options
 	// the caller specified. We'll fill in the URL at the last minute
@@ -1925,7 +1939,7 @@
 	this.dataWasLoaded = false;
 	this.unfilteredData = null;
 	this.dataHash = null;
-	this.curRowID = 0;
+	this.curRowID = -1;
 
 	// At this point the url should've been processed if it contained any
 	// data references. Set the url of the requestInfo structure and pass it
@@ -2018,7 +2032,9 @@
 		this.filter(this.filterFunc, true);
 
 	// The default "current" row is the first row of the data set.
-	if (this.data && this.data.length > 0)
+	if (!this.selectOnLoad)
+		this.curRowID = -1;
+	else if (this.data && this.data.length > 0)
 		this.curRowID = this.data[0]['ds_RowID'];
 	else
 		this.curRowID = 0;

Psuedo XML Parameter ds_TotalRowCount

This is another trivial one. I wanted a way to know what the total number of rows in my unfiltered data were. This would be useful for doing 'Selected 5 out of 20' type filter statuses, and would be very handy if you want to implement a multipage selection system and use the filter function to achieve it.

@@ -2910,6 +2935,8 @@
 				outputStr += (dsContext.getRowIndex() + 1);
 			else if (token.data == "ds_RowCount")
 				outputStr += dsContext.getNumRows();
+			else if (token.data == "ds_TotalRowCount")
+				outputStr += dsContext.getTotalNumRows();
 			else if (token.data == "ds_CurrentRowNumber")
 				outputStr += ds.getRowNumber(ds.getCurrentRow());
 			else if (token.data == "ds_CurrentRowID")
@@ -3128,6 +3155,11 @@
 				resultStr += dsContext.getNumRows();
 				row = null;
 			}
+			else if (fieldName == "ds_TotalRowCount")
+			{
+				resultStr += dsContext.getTotalNumRows();
+				row = null;
+			}
 			else if (fieldName == "ds_CurrentRowNumber")
 			{
 				var ds = dsContext.getDataSet();
@@ -3245,6 +3277,10 @@
 		var rows = m_dataSet.getData();
 		return rows ? rows.length : 0;
 	};
+	this.getTotalNumRows = function() {
+		var rows = m_dataSet.getUnfilteredData();
+		return rows ? rows.length : 0;
+	};
 	this.getCurrentRow = function()
 	{
 		if (m_curRowIndexArray.length < 2 || getInternalRowIndex() < 0)

Future Enhancements

These are future enhancements that i could see making, but don't really have any need for at the time. They exist only as a reminder of various ideas that I might like Spry to implement.

Notices

Copywrites remain with Adobe for all this code. I am not associaed with Adobe in anyway (other than a purchaser and user of their products), and this code is not sanctioned as 'official' code for Spry 1.2 and I make no claims to its warranty or fitness for any specific purpose. It worked on my machine, and that was only with IE 6 and FireFox 1.5 on Windows. Your mileage may vary. If it eats your app, well I warned you. Questions about Spry should be made on the official Spry forum which is the only place I will support it. And even then, I can only do so much. Get Adobe to hire me and maybe I can do more. Just kidding. :-)

Prototype and Scriptaculous have their own copyrights, I make no claim to any association with those groups.