Browse Source

HUE-225. Make Partial Refresh work when the response has more or fewer items in it than the previous response

Aaron Newton 15 years ago
parent
commit
0fe7e113ed

+ 14 - 14
apps/filebrowser/src/filebrowser/static/js/Source/Native/Number.Files.js

@@ -27,7 +27,7 @@ Number.implement({
 	parsePermissions: function() {
 		var perms = {};
 		$H({
-		  user_read: 0400,
+			user_read: 0400,
 			user_write: 0200,
 			user_execute: 0100,
 			group_read: 0040,
@@ -48,17 +48,17 @@ Number.implement({
 		if (this < 1099511627776) return (this/1073741824).round(1) + ' GB';
 		if (this < (1099511627776*1024)) return (this/1099511627776).round(1) + ' TB';
 		return (this/(1099511627776*1024)).round(1) + ' PB'; //hey, it could happen
-  },
-  
-  //From: http://snipplr.com/view/7345/format-number-with-commas-every-3-decimal-places/
-  convertWithCommas: function(){
-    var pieces = this.toString().split('.');
-    var p1 = pieces[0];
-    var p2 = pieces.length > 1 ? '.' + pieces[1] : '';
-    var regex = /(\d+)(\d{3})/;
-    while (regex.test(p1)) {
-  		p1 = p1.replace(regex, '$1' + ',' + '$2');
-  	}
-  	return p1 + p2;
-  }
+	},
+	
+	//From: http://snipplr.com/view/7345/format-number-with-commas-every-3-decimal-places/
+	convertWithCommas: function(){
+		var pieces = this.toString().split('.');
+		var p1 = pieces[0];
+		var p2 = pieces.length > 1 ? '.' + pieces[1] : '';
+		var regex = /(\d+)(\d{3})/;
+		while (regex.test(p1)) {
+			p1 = p1.replace(regex, '$1' + ',' + '$2');
+		}
+		return p1 + p2;
+	}
 });

+ 12 - 0
apps/jframegallery/src/jframegallery/templates/ajax-target.html

@@ -39,6 +39,18 @@ limitations under the License.
 				<div style="padding: 10px; background: #99f;">The response gets appended after this box</div>
 			</div>
 			<hr/>
+			<a href="/jframegallery/partial_refresh.mako?sleep=1" data-ajax-after=".ajax_after"><em>Also</em> adds the response after the blue box below.</a>
+			<div style="border: 1px solid #000; padding: 10px; margin: 10px 0px;">
+				<div style="padding: 10px; background: #99f;" class="ajax_after">The response gets appended after this box</div>
+			</div>
+			<hr/>
+			<a href="/jframegallery/partial_refresh.mako?sleep=1" data-ajax-before=".ajax_before">Add the response <em>before</em> the blue box below.</a>
+			<div style="border: 1px solid #000; padding: 10px; margin: 10px 0px;">
+				<div style="padding: 10px; background: #99f;" class="ajax_before">The response gets prepended <em>before</em> this box</div>
+			</div>
+			<hr/>
+
+
 			<a href="/jframegallery/partial_refresh.mako?sleep=1" data-ajax-target=".ajax_filter" data-ajax-filter="textarea">This link loads the same url as the others, but only the textarea is inserted into the box below</a>
 			<div style="border: 1px solid #000; padding: 10px; margin: 10px 0px;" class="ajax_filter">
 				This box is replaced with a textarea when you click the 

+ 22 - 17
apps/jframegallery/src/jframegallery/templates/html-table.treeview.ajax.mako

@@ -34,9 +34,11 @@
     .table-depth-10>td:first-child { padding-left: 160px; }
     .table-depth-11>td:first-child { padding-left: 175px; }
     </style>
+    <meta http-equiv="refresh" content="5" />
   </head>
   <body>
-    <div class="jframe_padded"> 
+    <div class="jframe_padded partial_refresh"> 
+      <input value="you can put some text in here to verify that the whole view doesn't refresh" style="width: 500px;"/>
       <p><a href="${request_path}?show_all=true">Show everything</a> || <a href="${request_path}">back to top</a></p>
       <table data-filters="HtmlTable" class="selectable treeView" style="border: 1px solid #999; width: 98%">
 
@@ -49,7 +51,9 @@
           if node.children:
             folder = "table-folder"
         %>
-        <tr class="${folder} table-depth-${depth} ${expanded} pstree-${node.pid}">
+        <tr class="${folder} table-depth-${depth} ${expanded} pstree-${node.pid}"
+          data-dblclick-delegate="{'dblclick_loads':'.sub'}"
+          data-partial-line-id="pstree-line-${node.pid}">
           <td style="max-width:400px">
             % if path in open_paths:
               <a href="${remove(path)}" class="ccs-hidden">collapse</a>
@@ -58,36 +62,37 @@
             % endif
 
             % if node.children:
-              <a href="${request_path}?subtree=${node.pid}&depth=${int(depth)+1}&sleep=1" class="expand"
+              <a href="${request_path}?subtree=${node.pid}&depth=${int(depth)+1}" class="expand"
                 data-spinner-target=".pstree-${node.pid}"
                 data-livepath-toggle="${urllib.urlencode([('paths', node.path)])}"
-                data-ajax-replace=".pstree-${node.pid}-target" data-ajax-filter="tbody tr">subset</a>
+                data-ajax-after=".pstree-${node.pid}" data-ajax-filter="tbody tr">subset</a>
+              <a href="${request_path}?subtree=${node.pid}" class="sub ccs-hidden">browse</a>
             % endif
-      
-            <div style="overflow:hidden; white-space:nowrap;">${node.command}</div></td>
+
+            <div style="overflow:hidden; white-space:nowrap;" data-filters="FitText">${node.command}</div></td>
           <td>${node.pid}</td>
           <td>${node.user}</td>
-          <td>${node.cputime}</td>
+          <td data-partial-id="pstree-cputime-${node.pid}">${node.cputime}</td>
         </tr>
         % if path in open_paths or show_all:
           % for child in node.children:
             ${create_row(child, depth+1, path+"/"+str(child.pid))}
           % endfor
-        % elif not show_all and len(node.children) > 0:
-          <tr style="display:none" class="html-table-tree-ignore pstree-${node.pid}-target"><td colspan="4"></td></tr>
         % endif
       </%def>
 
       <thead>
-      <th>command</th>
-      <th>pid</th>
-      <th>user</th>
-      <th>cputime</th>
+        <tr>
+          <th>command</th>
+          <th>pid</th>
+          <th>user</th>
+          <th>cputime</th>
+        </tr>
       </thead>
-      <tbody>
-      % for top in tops:
-       ${create_row(top, depth, "/" + str(top.pid))}
-      % endfor
+      <tbody data-partial-container-id="pstree-body">
+        % for top in tops:
+         ${create_row(top, depth, "/" + str(top.pid))}
+        % endfor
       </tbody>
       </table>
     </div>

+ 50 - 1
apps/jframegallery/src/jframegallery/templates/partial_refresh.mako

@@ -16,11 +16,14 @@
 <%!
 from datetime import datetime
 %>
+<%
+count = int(get_var('count', 0))
+%>
 <!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01//EN">
 <html>
   <head>
     <title>Partial Refresh</title>
-    <meta http-equiv="refresh" content="2;/jframegallery/partial_refresh.mako?sleep=1" />
+    <meta http-equiv="refresh" content="2;/jframegallery/partial_refresh.mako?sleep=1&count=${count + 1}" />
   </head>
   <body>
     <div class="jframe_padded partial_refresh">
@@ -29,6 +32,52 @@ from datetime import datetime
       <br/>
       <textarea type="text">you can change this</textarea>
       <p>you can interact with the input above while the blocks with the time stamps update. Note that the button is rendered each time and your input changes aren't.</p>
+      <hr/>
+      <p>
+        The table below will continue to grow until there are 10 rows; the time in each row will update with each refresh.
+      </p>
+      <table class="HtmlTable">
+        <thead>
+          <tr>
+            <th>count</th>
+            <th>current time</th>
+          </tr>
+        </thead>
+        <tbody data-partial-container-id="partials-tbody">
+          <% index = 0 %>
+          % while index < count and index < 10:
+            <tr data-partial-line-id="tr-${index}">
+              <td data-partial-id="index-${index}">${index}</td>
+              <td data-partial-id="time-${index}">${datetime.now().strftime("%Y-%m-%d %H:%M:%S")}</td>
+            </tr>
+            <% index = index + 1 %>
+          % endwhile
+        </tbody>
+      </table>
+      <hr/>
+      <p>
+        The table below will shrink from 10 rows down to zero; the time in each row will update on each refresh.
+      </p>
+      <table class="HtmlTable">
+        <thead>
+          <tr>
+            <th>count</th>
+            <th>current time</th>
+          </tr>
+        </thead>
+        <tbody data-partial-container-id="partials-tbody-down">
+          <%
+            count = 10 - count
+            index = 0 %>
+          % while index < count:
+            <tr data-partial-line-id="tr-down-${index}">
+              <td data-partial-id="index-down-${index}">${index}</td>
+              <td data-partial-id="time-down-${index}">${datetime.now().strftime("%Y-%m-%d %H:%M:%S")}</td>
+            </tr>
+            <% index = index + 1 %>
+          % endwhile
+        </tbody>
+      </table>
     </div>
   </body>
 </html>

+ 35 - 16
desktop/core/static/js/Source/CCS/CCS.JFrame.js

@@ -121,7 +121,7 @@ CCS.JFrame = new Class({
 
 	// path: initial page to load
 	initialize: function(path, options){
-                this.parent(options);
+		this.parent(options);
 		new ART.Keyboard(this, this.keyboardOptions);
 		this.addLinkers(this.options.linkers);
 		this.addFilters(this.options.filters);
@@ -265,7 +265,11 @@ CCS.JFrame = new Class({
 	 *   options: see renderContent's options
 	 */
 	load: function(options){
-		this.fireEvent('request', [options.requestPath, options.userData]);
+		options = $merge({
+			//by default, requests reload the entire jframe
+			fullFrameLoad: true
+		}, options);
+		this.fireEvent('request', [options.requestPath, options.userData, options]);
 		var req = new Request();
 		this._setRequestOptions(req, 
 			$merge(options, {
@@ -295,10 +299,10 @@ CCS.JFrame = new Class({
 		);
 	},
 
-        getBehaviorState: function() {
-                if(!this.options.behaviorState) this.options.behaviorState = {};
-                return this.options.behaviorState;
-        },
+	getBehaviorState: function() {
+		if(!this.options.behaviorState) this.options.behaviorState = {};
+		return this.options.behaviorState;
+	},
 
 	/*
 	options:
@@ -715,10 +719,15 @@ CCS.JFrame = new Class({
 		if (request._jframeConfigured) return;
 		request._jframeConfigured = true;
 		request.setOptions($merge({
+			//determine if this request should be appearent to the user
 			useSpinner: this.options.spinnerCondition.apply(this, [options]),
+			//where to put the spinner
 			spinnerTarget: this.options.spinnerTarget || this.element,
+			//any options specific to spinner
 			spinnerOptions: { fxOptions: {duration: 200} },
+			//when there's an exception, invoke an error handler
 			onFailure: this.error.bind(this),
+			//do not eval scripts in the response; in theory this should never been overridden
 			evalScripts: false,
 			onRequest: function(){
 				/*
@@ -733,10 +742,18 @@ CCS.JFrame = new Class({
 				this._request = request;
 			}.bind(this),
 			onSuccess: function(requestTxt){
-				this._requestSuccessHandler(request, requestTxt, options);
+				//if there's a method called requestChecker defined in the options, run our response through it
+				//if it returns false, then throw out the response.
+				if (!options.requestChecker || options.requestChecker(requestTxt, request, options)) {
+					this._requestSuccessHandler(request, requestTxt, options);
+				}
+				//we're done with this request
 				this._request = null;
 			}.bind(this),
 			onCcsErrorPopup: function(alert){
+				//when the request shows a popup error because there's been an exception of some sort
+				//attach some logic to that popup so that when the user closes the alert the app window,
+				//if it's never been displayed and is still hidden, is destroyed.
 				alert.addEvent('destroy', function(){
 					if (!this.loadedOnce) {
 						var win = this.getWindow();
@@ -745,12 +762,14 @@ CCS.JFrame = new Class({
 				}.bind(this));
 			}.bind(this)
 		}, options));
+		//whenever the request completes, destroy it's spinner
 		request.addEvent('complete', function(){
 			if (this.spinner) {
 				this.spinner.destroy();
 				this.spinner = null;
 			}
 		}.bind(request));
+		//custom header for Hue
 		request.setHeader('X-Hue-JFrame', 'true');
 	},
 
@@ -760,7 +779,7 @@ CCS.JFrame = new Class({
 
 	_requestSuccessHandler: function(request, html, options) {
 		var error, blankWindowWithError, previousPath;
-                previousPath = this.currentPath;
+		previousPath = this.currentPath;
 		if (this._checkForEmptyErrorState(request, html)) {
 			error = true;
 			if (!this.loadedOnce) blankWindowWithError = true;
@@ -784,14 +803,14 @@ CCS.JFrame = new Class({
 			});
 		}
 		if (redirected) this.fireEvent('redirectAfterRender', [this.currentPath, responsePath]);
-                var loadOptions = $merge({
-                        content: html,
-                        responsePath: responsePath || request.options.url,
-                        error: error,
-                        blankWindowWithError: blankWindowWithError,
-                        previousPath: previousPath
-                }, options || {}); 
-                this.behavior.fireEvent('load', loadOptions); 
+		var loadOptions = $merge({
+			content: html,
+			responsePath: responsePath || request.options.url,
+			error: error,
+			blankWindowWithError: blankWindowWithError,
+			previousPath: previousPath
+		}, options || {}); 
+		this.behavior.fireEvent('load', loadOptions); 
 	},
 
 	/*

+ 89 - 40
desktop/core/static/js/Source/JFrameFilters/CCS.JFrame.AutoRefresh.js

@@ -34,52 +34,101 @@ CCS.JFrame.addGlobalFilters({
 		//it auto refreshes the input. if span.sec_to_autorefresh is present, fill its
 		//contents with the number of seconds until a refresh.
 		var ignoreAutoRefresh = content && content.options && content.options.ignoreAutoRefresh;
-		if (!ignoreAutoRefresh && content && content.meta) {
-			var sec, url;
-			content.meta.each(function(meta) {
-				var parts = meta.get('content').split(';');
-				if(meta.get('http-equiv') == "refresh") {
-					sec = parts[0].toInt();
-					if (parts[1]) url = unescape(parts[1].replace(urlRE, ''));
-				}
-			}, this);
-			if (!sec) return;
-			var end = new Date().increment('second', sec);
-			var until, diff, span;
-			var update = function() {
-				span = $(this).getElement('span.sec_to_autorefresh');
-				diff = ((end - new Date()) / Date.units['second']());
-				until = diff.toInt();
-				if (span) span.set('html', until);
-			}.bind(this);
-			update();
+		if (!ignoreAutoRefresh && content && content.meta) setupAutoRefresh.call(this, content);
+	}
 
-			var timer = (function(){
-				if (diff < 1) {
-					if (span) span.set('html', 0);
-					if (url && url != unescape(this.currentPath)) this.load({ requestPath: url, autorefreshed: true });
-					else this.refresh({ autorefreshed: true });
-					$clear(timer);
-				} else {
-					update();
-				}
-			}).periodical(250, this);
+});
 
-			refreshTable.set(this, timer);
+//given the content object from the filter handler, set up an auto refresher if the meta tag
+//for it is present
+var setupAutoRefresh = function(content) {
+	var sec, url;
+	//get the meta tags from the content and check them for a refresh tag
+	content.meta.each(function(meta) {
+		var parts = meta.get('content').split(';');
+		if(meta.get('http-equiv') == "refresh") {
+			sec = parts[0].toInt();
+			if (parts[1]) url = unescape(parts[1].replace(urlRE, ''));
+		}
+	}, this);
+	//if there's no refresh variable, exit
+	if (!sec) return;
+	//determin the timestamp for when we should refresh
+	var end = new Date().increment('second', sec);
+	var until, diff, span;
+	//this method updates the DOM when the counter decrements
+	var update = function() {
+		span = $(this).getElement('span.sec_to_autorefresh');
+		diff = ((end - new Date()) / Date.units['second']());
+		until = diff.toInt();
+		if (span) span.set('html', until);
+	}.bind(this);
+	update();
 
-			var clearer = function(){
-				$clear(timer);
-				this.removeEvent('request', clearer);
-			};
-			this.addEvent('request', clearer);
-			this.markForCleanup(function(){
-				$clear(timer);
+	//this method compares to urls to see if they match
+	var compareURI = function(one, two) {
+		return new URI(one).toString() == new URI(two).toString();
+	};
+
+	//this method checks a request to see if it's been spoiled
+	//with the ajax linkers and the live url, it's possible that
+	//the user clicked a link that updated the url for this frame
+	//while a request was running
+	var requestChecker = function(text, request, options) {
+		var valid = compareURI(options.url, url ? url : this.currentPath);
+		if (!valid) load();
+		return valid;
+	}.bind(this);
+
+	//this method refreshs the frame either with the url from the meta
+	//tag or from the current path
+	var load = function(){
+		if (url && url != unescape(this.currentPath)) {
+			this.load({
+				requestPath: url,
+				autorefreshed: true,
+				requestChecker: requestChecker
 			});
+		} else {
+			this.refresh({ autorefreshed: true, requestChecker: requestChecker });
 		}
-	}
+	}.bind(this);
 
-});
+	//this timer goes off ever .25 seconds and checks to see if we've reached our
+	//target refresh time
+	var timer = (function(){
+		if (diff < 1) {
+			if (span) span.set('html', 0);
+			load();
+			$clear(timer);
+		} else {
+			update();
+		}
+	}).periodical(250, this);
+
+	//this stores the timer in a universal table object.
+	refreshTable.set(this, timer);
+
+	//when a new request is generated (i.e. the user clicks a link, or refresh, or autorefresh)
+	//check to see if the options for the request have the fullFrameLoad flag set to true; if not
+	//restart our counter. For example, if a user does an AJAX request to update a dom (as we do
+	//in the AjaxLoad jframe linker), that's still a request, but it's not a request for the entire
+	//page so has no refresh handling of its own
+	var clearer = function(requestPath, userData, options){
+		if (!options.fullFrameLoad) {
+			setupAutoRefresh.call(this, content);
+		} else {
+			$clear(timer);
+			this.removeEvent('request', clearer);
+		}
+	};
+
+	this.addEvent('request', clearer);
+	this.markForCleanup(function(){
+		$clear(timer);
+	});
+};
 
 var refreshTable = new Table();
 
-})();
+})();

+ 6 - 2
desktop/core/static/js/Source/JFrameLinkers/CCS.JFrame.AjaxLoad.js

@@ -42,7 +42,7 @@ script: CCS.JFrame.AjaxLoad.js
 
 	var linkers = {};
 
-	['append', 'replace', 'target'].each(function(action){
+	['append', 'replace', 'target', 'after', 'before'].each(function(action){
 
 		linkers['[data-ajax-' + action + ']'] = function(event, link){
 			var target = $(this).getElement(link.get('data', 'ajax-' + action));
@@ -61,9 +61,10 @@ script: CCS.JFrame.AjaxLoad.js
 				requestPath: link.get('href'),
 				spinnerTarget: target,
 				target: requestTarget,
-				ignorePartialRefresh: true,
+				onlyProcessPartials: true,
 				ignoreAutoRefresh: true,
 				suppressLoadComplete: true,
+				fullFrameLoad: false,
 				retainPath: true,
 				callback: function(data){
 					switch(action){
@@ -75,9 +76,12 @@ script: CCS.JFrame.AjaxLoad.js
 							target.destroy();
 							break;
 						case 'append':
+						case 'after':
 							//see note above in 'replace' case as to why we use reverse here
 							data.elements.reverse().injectAfter(target);
 							break;
+						case 'before':
+							data.elements.reverse().injectBefore(target);
 						//do nothing for update, as Request.HTML already does it for you
 					}
 					var state = {

+ 302 - 82
desktop/core/static/js/Source/JFrameRenderers/CCS.JFrame.PartialRefresh.js

@@ -28,20 +28,78 @@ script: CCS.JFrame.PartialRefresh.js
 
 	CCS.JFrame.addGlobalRenderers({
 
+		/*
+			Partials are comprised of instructions in HTML responses that mark areas of the document that should be
+			compaired against previous responses. These areas's raw HTML (the HTML returned from the server, not the
+			current DOM state, which may have classes or style attributes or even new DOM elements in place) is compared
+			to the previous response. When there's a mis-match in the response, only those elements that have changed
+			are run through the JFrame filters to configure them for display and then replace their expired counterparts.
+
+			Each area that may have elements that should be checked is identified with the class "partial_refresh" on some
+			container; often a div wrapping the entire response.
+
+			Each individual item that might be refreshed must be marked with a data-partial-id property that is unique
+			to the JFrame (i.e. multiple app windows can have id overlap).
+
+			If you need to support the loading of additional content after the fact, such as with loading additional Ajax
+			elements within the frame or if the response may return more or fewer items than the previous response, then
+			you must add some additional instrumentation to your HTML. Specifically, you *must* define a container where
+			new elements are injected. This is typically the immediate parent of the partials, like so:
+
+			Example w/ just partials and container
+			<div data-partial-container-id="div1">
+				<p data-partial-id="p1">foo</p>
+				<p data-partial-id="p2">bar</p>
+			</div>
+
+			In some cases, such as with tables, you may wish to add "lines" between the partials and the container. A good
+			example here is a table which has partials for individual td elements, but new tr elements may come and go.
+
+			Example w/ partials, container, and lines
+			<tbody data-partial-container-id="tbody1">
+				<tr data-partial-line-id="tr1">
+					<td data-partial-id="id1"></td>
+					<td data-partial-id="id2"></td>
+				</tr>
+			</tbody>
+
+			When the above markup is used, new partials may appear (the tds) and the parent element with the data-partial-line-id
+			will be used for injection instead. Note that this id, and the container id, must also be unique to the frame.
+
+			Both lines and partials will be injected in their relative position; i.e. if in the above example our response returned
+			a new tr line afer tr1, the new row would be found in the response, the reponse's DOM tree would be inspected and
+			the previous row would be found there. It's id is "tr1". The *live* DOM tree - the one the user sees - would be searched
+			for an element with that line-id. The new line would be injected after it.
+
+			When using ajax to load in new content (such as with the AjaxLoad linker) you'll likely load new content in ouside
+			the scope of this refresh handler. I.e. you'll handle the response and inject the new items yourself. In this case
+			partial refresh needs only to store the original request state for future checksums. To accomplish this, set the
+			options of the request to have the onlyProcessPartials flag set to true. This will run the ajax response through
+			this renderer, find the new partial elements, and store their initial state for checking later.
+
+		*/
+
 		partialRefresh: function(content){
-			var options = content.options;
+			var options = content.options || {};
+			//if the request options include the ignorePartialRequest flag, exit
+			if (options.ignorePartialRefresh) return;
 			//when we load content via ajax, we don't want the response being parsed for partials
-			if (options && options.ignorePartialRefresh) return;
 			var jState = getJState(this);
 			//get the partial containers; containers that have elements in them to be partially refreshed
-			var partialContainers = content.elements.filter('.partial_refresh');
-			var setPrevPath = function(){
-				jState.prevPath = options ? options.responsePath : null;
-			};
+			var partialContainers = new Element('div').adopt(content.elements).getElements('.partial_refresh');
+
+			//for requests that want to handle the element injection themselves (ajaxload, for example)
+			//they can instruct this filter to process the partials in the response and then exit
+			if (options.onlyProcessPartials) {
+				processPartials(content, jState);
+				return;
+			}
+			//store the previous path to be the current one
+			jState.prevPath = options ? options.responsePath : null;
+
 			if (!partialContainers.length) {
 				if (enableLog) dbug.log('no partials to refresh, exiting');
 				//no partial containers, reset and fall through to other renderers
-				setPrevPath();
 				jState.partials = null;
 				this.enableSpinnerUsage();
 				return;
@@ -50,76 +108,64 @@ script: CCS.JFrame.PartialRefresh.js
 			var partials = getPartials(new Element('div').adopt(partialContainers), true);
 			//if the options aren't defined or if we didn't auto refresh, reset and
 			//return (fall through to other renderers)
-			if (!options || !options.autorefreshed || 
-					//or if the last time we loaded we stored partials but the url has changed
-					//(i.e. a new page is loaded, this one also having partials)
-					(jState.prevPath != options.responsePath && !options.forcePartial)) {
+			if (!options || !(options.autorefreshed || options.forcePartial)) {
 				//...then store the state and render as usual (fall through to other renderers)
 				if (enableLog) dbug.log('not auto refreshed (%s), or new path (%s != %s) and not forced (%s), existing partial refresh after setup', !options.autorefreshed, jState.prevPath, options.responsePath, options.forcePartial);
-				setPrevPath();
 				jState.partials = partials;
 				this.disableSpinnerUsage();
 				return;
 			}
-			
+
 			if (new URI(options.requestPath).toString() != new URI(options.responsePath).toString()) {
-				if (enableLog) dbug.warn('detected partial refresh on a possible redirect (the request path != the response path), continuing with partial refresh');
+				if (enableLog) dbug.warn('detected partial refresh on a possible redirect (the request path (%s) != the response path (%s)), continuing with partial refresh', new URI(options.requestPath).toString(), new URI(options.responsePath).toString());
 			}
-			setPrevPath();
 
 			//don't show the spinner for partial refreshes
 			this.disableSpinnerUsage();
 
-			//check that the old partials are present in the new ones
-			//and that all the new ones are present in the old ones
-			//if not, store the new state and fall through to other renderers
-			var checked = checkPartials(jState.partials, partials);
-			if (checked.spoiled) {
-				if (enableLog) dbug.log('checked partials spoiled, setup and return');
-				//TODO tell the user that their state has spoiled and then update
-				jState.partials = partials;
-				//fall through to other renderers
-				return;
-			}
-			
 			/*******************************
 			FORM HERE ON OUT
 			this filter will handle the response; we return true and other renderers are excluded
+			UNLESS there is a partial returned that we cannot find the proper place to put it
+			(i.e. it has no partial-container).
 			*******************************/
-			
+			if (enableLog) dbug.log('proceeding with partial refresh');
 			//store the path as the current one
 			this.currentPath = options.responsePath || this.currentPath;
-			
-			//if there's something to update...
-			if (checked.update.length > 0) {
-				//apply the JFrame filters to the response
-				//but only to the elements require updating
-				//this requires some foresight on how you apply the partials
-				//if your partial contains a filter and that filter expects
-				//the rest of the response to be around it, it's not going to be
-				//only the children of each section marked as a partial can be relied upon
-				//to be in this DOM structure when we apply the filters.
-				var pElements = new Elements(checked.update.map(function(id){ return partials[id]; }));
-				var target = new Element('div').adopt(pElements);
-				content.elements = pElements;
-				this.applyFilters(target, content);
-				//get the partials from the target now that they've been set up with the filters
-				partials = getPartials(target);
-				//update the items that require it
-				if (enableLog) dbug.log('updating partial refresh items (%s)', checked.update.length);
-				checked.update.each(function(id){
-					//replace the element
-					partials[id].replaces(jState.partials[id]);
-					//destroy the old one from memory (garbage collection)
-					jState.partials[id].destroy();
-					//update the pointer to the new one
-					jState.partials[id] = partials[id];
-				}, this);
-			} else {
+
+			var checkedPartials = checkPartials(partials, jState);
+
+			//render the content
+			if (!checkedPartials.renderedPartials.length) {
+				if (enableLog) dbug.log('no partials for render; exiting quietly');
 				//if we aren't updating anything, that's cool, but still call the autorefresh filter
 				//to ensure that the frame keeps refreshing
 				this.applyFilter('autorefresh', new Element('div'), content);
+				//if there is no new content, return true (so no other renderers are called)
+				return true;
 			}
+
+			//apply all the jframe magic to our filtered content
+			if (enableLog) dbug.log('applying filters');
+			//filters expect to be handed an object (content) that has various properties of the
+			//response; the meta tags, the script tags, and elements that were in the body, etc.
+			//because we want to run the elements we're updating through the filters, we overwrite
+			//the elements that were passed in to this renderer with only the elements we want
+			//updated and then apply the filters.
+			content.elements = checkedPartials.renderedPartials;
+			this.applyFilters(checkedPartials.target, content);
+
+			//now loop through the partials again and inject them into the DOM structure from the response
+			//replacing the original partial with the cloned one
+			restorePartials(checkedPartials.partialClones, partials);
+
+			var success = injectPartials(partials, jState, checkedPartials.renderedIds, this.behavior, $(this));
+			if (!success) return;
+
+			cleanRemainingParitals(partials, jState, this.behavior);
+
+			//we've updated the display, so tell filters that are waiting that they may need to update their display, too
+			this.behavior.fireEvent('show');
 			//prevent other renderers from handling the response
 			return true;
 		}
@@ -151,40 +197,214 @@ script: CCS.JFrame.PartialRefresh.js
 			//if instructed to, store the original state of the response before it was altered by any filter
 			if (store) partial.store('partialRefresh:unaltered', partial.innerHTML);
 		});
-		return partials;
+		return $H(partials);
+	};
+
+	var processPartials = function(content, jState){
+		var toProcess = getPartials(new Element('div').adopt(content.elements), true);
+		//if there are partials already, merge the result
+		if (jState.partials) {
+			toProcess.each(function(partial, id){
+				jState.partials[id] = partial;
+			});
+		//else there were no partials in the previous response; so assign these.
+		} else {
+			jState.partials = toProcess;
+		}
 	};
 
-	//checks two collections of partials to see if they are equal
-	//that each group has the same number of partials
-	//that the ids match up and, in addition, that the content 
-	//returned has updated or not
-	var checkPartials = function(partials1, partials2) {
-		var keys = $H(partials1).getKeys();
-		keys.combine($H(partials2).getKeys());
-		
-		var result = {
-			update: [],
-			spoiled: false
+	var checkPartials = function(partials, jState){
+		var data = {
+			renderedIds: {},
+			renderedPartials: new Elements(),
+			partialClones: {},
+			target: new Element('div')
 		};
-	
-		keys.every(function(key) {
-			//get the partial from both sets
-			var p1 = partials1[key],
-					p2 = partials2[key];
-			//if they are present
-			if (p1 && p2) {
-				//and their *original* html doesn't match, then mark it for update
-				if (p1.retrieve('partialRefresh:unaltered') != p2.retrieve('partialRefresh:unaltered')) result.update.push(key);
-				//continue looping
-				return true;
+		//loop through the partials and figure out which ones need updating so that we can 
+		//run only those through the filters
+		partials.each(function(partial, id) {
+			if (enableLog) dbug.log('considering %s for update', id);
+			//get the corresponding element in the dom
+			var before = jState.partials[id];
+			//if there isn't one, or thier raw html don't match, we'll update it, so we must render it
+			if (!before || !compare(before, partial)) {
+				if (enableLog) dbug.log('preparing %s for update', id);
+				//we must preserve the DOM structure to be able to find partial containers and partial lines
+				//so clone the partial for rendering
+				var clone = partial.clone(true, true);
+				data.target.adopt(clone);
+				//we need an array of these elements for when we call applyFilters below, which expects
+				//content.elements to be an array of the elements returned in the response
+				data.renderedPartials.push(clone);
+				data.renderedIds[id] = true;
+				//we also need a key/value map of all the clones for quick lookups when we put them back
+				//into the response DOM
+				data.partialClones[id] = clone;
+			}
+		});
+		return data;
+	};
+
+	//given a partial, attempts to find the line it is in
+	//example: for a td that is a partial, it may have the tr as its line
+	var getPartialLine = function(partial){
+		return partial.getParent('[data-partial-line-id]');
+	};
+	//given a partial, attempts to find the container it is in
+	//for example, for a td that is a partial, it may have the tr as its line and the table as its container
+	var getPartialContainers = function(partial, container){
+		var containers = {
+			container: partial.getParent('[data-partial-container-id]')
+		};
+		if (containers.container) {
+			containers.DOMcontainer = container.getElement('[data-partial-container-id=' + 
+			  containers.container.get('data', 'partial-container-id') + ']');
+		}
+		return containers;
+	};
+
+	//given two partials, compares their raw HTML before they were parsed by filters
+	var compare = function(before, after){
+		return before.retrieve('partialRefresh:unaltered') == after.retrieve('partialRefresh:unaltered');
+	};
+
+	//this method destroys a partial given its partial id
+	var destroyPartial = function(id, jState, behavior){
+		//get the element
+		var element = jState.partials[id];
+		//clean up its behaviors
+		behavior.cleanup(element);
+		//destroy the element
+		element.destroy();
+		//delete it from the jState
+		delete jState.partials[id];
+	};
+
+	//this method takes a group of cloned partials (that have been passed through filters)
+	//and puts them back into the DOM from which they came, replacing the elements they
+	//were cloned from. This allows us to retain the DOM structure of the response, while
+	//only running through the filters the elements that need it.
+	var restorePartials = function(clones, partials){
+		for (id in clones) {
+			if (enableLog) dbug.log('replacing target with clone: ', id);
+			var clone = clones[id],
+			    partial = partials[id];
+			//because we're replacing, we need to copy over thier original HTML state for the checksum
+			clone.store('partialRefresh:unaltered', partial.retrieve('partialRefresh:unaltered'));
+			clone.replaces(partial);
+			//and then update the pointer as the clone is now the rendered partial
+			partials[id] = clone;
+		}
+	};
+
+	var injectPartials = function(partials, jState, rendered, behavior, container) {
+		var insertedPartials = {},
+		    prevId;
+		if (enableLog) dbug.log('iterating over partials for injection');
+		//iterate over all the partials to inject them into the live DOM
+		return partials.every(function(partial, id){
+			if (enableLog) dbug.log('considering %s for injection', id);
+			//if it's in a line that's been injected, skip it
+			//if it was passed through the renderers, it means that it needs an update or insertion
+			if (rendered[id] && !insertedPartials[id]) {
+				//get the corresponding partial in the DOM
+				var before = jState.partials[id];
+				//if there's a corresponding partial already in the DOM, replace it
+				if (before) {
+					if (enableLog) dbug.log('performing update for %s', id);
+					partial.replaces(before);
+					destroyPartial(id, jState, behavior);
+				} else {
+					//else it's not in the DOM
+					//look to see if this partial is in a line item (for example, the tr for a td that is a partial)
+					var line = getPartialLine(partial);
+					//if there is no line, inject it into the DOM in the container
+					var success = line ? injectPartialLine(partial, line, container, insertedPartials)
+					    : injectPartial(partial, jState.partials[prevId], container);
+					if (!success) return false;
+				}
+			}
+			if (rendered[id]) jState.partials[id] = partial;
+			prevId = id;
+			return true;
+		}, this);
+	};
+
+	//given a line, destroy it
+	var destroyLine = function(line, behavior){
+		if (enableLog) dbug.log('destroying line:', line);
+		behavior.cleanup(line);
+		line.destroy();
+	};
+
+	var cleanRemainingParitals = function(partials, jState, behavior){
+		var linesToDestroy = {};
+		//for any partials that were in the DOM but not in the response, remove them
+		jState.partials.each(function(partial, id){
+			//if the partial is in the DOM but not the response
+			if (!partials[id]) {
+				//get its line; assume that we have to remove that, too
+				var line = getPartialLine(partial);
+				if (enableLog) dbug.log('destroying %s', id, line);
+				//destroy the partial
+				destroyPartial(id, jState, behavior);
+				linesToDestroy[line.get('data', 'partial-line-id')] = line;
+			}
+		});
+		for (id in linesToDestroy) {
+			destroyLine(linesToDestroy[id], behavior);
+		}
+	};
+
+	var injectPartial = function(partial, previousPartial, container){
+		if (prevId) {
+			if (enableLog) dbug.log('injecting line for %s after previous item (%s)', id, prevId);
+			//if this isn't the first one, inject it after the previous id
+			partial.inject(jState.partials[prevId], 'after');
+		} else {
+			//find the container and inject it as the first item there
+			var containers = getPartialContainers(partial, container);
+			if (containers.DOMcontainer) {
+				if (enableLog) dbug.log('injecting %s into top of container (%o)', id, containers.DOMcontainer);
+				partial.inject(containers.DOMcontainer, 'top');
+			} else {
+				//else, we don't know where to inject it
+				dbug.warn('Could not inject partial (%o); no container or previous item found.', partial);
+				return false;
+			}
+		}
+		return true;
+	};
+
+	var injectPartialLine = function(partial, line, container, insertedPartials){
+		if (enableLog) dbug.log('preparing line for injection');
+		//there is a line, so we inject it instead of the partial.
+		//get the previous line (from the response)
+		var prevLine = line.getPrevious('[data-partial-line-id]'),
+		    prevLineInDOM;
+		//now find it's counterpart in the live DOM
+		if (prevLine) prevLineInDOM = container.getElement('[data-partial-line-id=' + prevLine.get('data', 'partial-line-id') + ']');
+		//if it's there, inject this line after it
+		if (prevLineInDOM) {
+			if (enableLog) dbug.log('injecting line (%o) after previous line (%o)', line, prevLine);
+			line.inject(prevLineInDOM, 'after');
+		} else {
+			//else this is the first line, so inject it at the top of the container
+			var lineContainers = getPartialContainers(partial, container);
+			if (lineContainers.DOMcontainer) {
+				if (enableLog) dbug.log('injecting line (%o) into top of container (%o)', line, lineContainers.DOMcontainer);
+				line.inject(lineContainers.DOMcontainer, 'top');
 			} else {
-				//mismatch, the batch is spoiled, we reload the whole view
-				result.spoiled = true;
-				//break looping
+				//else, we don't know where to inject it
+				dbug.warn('Could not inject partial (%o) in line (%o); no container or previous item found.', partial, line);
 				return false;
 			}
+		}
+		//store the fact that we just injected all the partials in this line
+		line.getElements('[data-partial-id]').each(function(partial){
+			insertedPartials[partial.get('data', 'partial-id')] = true;
 		});
-		return result;
+		return true;
 	};
 
 })();