From 1f8feb5efcb77eb83036baaa70205c864420af3e Mon Sep 17 00:00:00 2001 From: Mateusz Uzdowski Date: Thu, 22 Aug 2013 14:12:31 +1200 Subject: [PATCH] API Provide a thin alternative to loadPanel/submitForm. This is needed in some situations when we only want to update a small single component, sometimes even using a different controller to the one implied in the URL. An example here is reloading dynamically the subsite dropdown without reloading the entire page, updating a filter sidebar or suchlike. --- admin/javascript/LeftAndMain.js | 84 +++++++++++++++++++++++++-- docs/en/reference/cms-architecture.md | 41 +++++++++++++ 2 files changed, 121 insertions(+), 4 deletions(-) diff --git a/admin/javascript/LeftAndMain.js b/admin/javascript/LeftAndMain.js index c513df1af..b349f1ba5 100644 --- a/admin/javascript/LeftAndMain.js +++ b/admin/javascript/LeftAndMain.js @@ -115,7 +115,15 @@ jQuery.noConflict(); */ $('.cms-container').entwine({ - CurrentXHR: null, + /** + * Tracks current panel request. + */ + StateChangeXHR: null, + + /** + * Tracks current fragment-only parallel PJAX requests. + */ + FragmentXHR: {}, StateChangeCount: 0, @@ -419,7 +427,7 @@ jQuery.noConflict(); */ handleStateChange: function() { // Don't allow parallel loading to avoid edge cases - if(this.getCurrentXHR()) this.getCurrentXHR().abort(); + if(this.getStateChangeXHR()) this.getStateChangeXHR().abort(); var self = this, h = window.History, state = h.getState(), fragments = state.data.pjax || 'Content', headers = {}, @@ -455,7 +463,7 @@ jQuery.noConflict(); headers: headers, url: state.url, complete: function() { - self.setCurrentXHR(null); + self.setStateChangeXHR(null); // Remove loading indication from old content els (regardless of which are replaced) contentEls.removeClass('loading'); }, @@ -465,7 +473,75 @@ jQuery.noConflict(); } }); - this.setCurrentXHR(xhr); + this.setStateChangeXHR(xhr); + }, + + /** + * ALternative to loadPanel/submitForm. + * + * Triggers a parallel-fetch of a PJAX fragment, which is a separate request to the + * state change requests. There could be any amount of these fetches going on in the background, + * and they don't register as a HTML5 history states. + * + * This is meant for updating a PJAX areas that are not complete panel/form reloads. These you'd + * normally do via submitForm or loadPanel which have a lot of automation built in. + * + * On receiving successful response, the framework will update the element tagged with appropriate + * data-pjax-fragment attribute (e.g. data-pjax-fragment=""). Make sure this element + * is available. + * + * Example usage: + * $('.cms-container').loadFragment('admin/foobar/', 'FragmentName'); + * + * @param url string Relative or absolute url of the controller. + * @param pjaxFragments string PJAX fragment(s), comma separated. + */ + loadFragment: function(url, pjaxFragments) { + + var self = this, + xhr, + headers = {}, + baseUrl = $('base').attr('href'), + fragmentXHR = this.getFragmentXHR(); + + // Make sure only one XHR for a specific fragment is currently in progress. + if( + typeof fragmentXHR[pjaxFragments]!=='undefined' && + fragmentXHR[pjaxFragments]!==null + ) { + fragmentXHR[pjaxFragments].abort(); + fragmentXHR[pjaxFragments] = null; + } + + url = $.path.isAbsoluteUrl(url) ? url : $.path.makeUrlAbsolute(url, baseUrl); + headers['X-Pjax'] = pjaxFragments; + + xhr = $.ajax({ + headers: headers, + url: url, + success: function(data, status, xhr) { + var elements = self.handleAjaxResponse(data, status, xhr, null); + + // We are fully done now, make it possible for others to hook in here. + self.trigger('afterloadfragment', { data: data, status: status, xhr: xhr, elements: elements }); + }, + error: function(xhr, status, error) { + self.trigger('loadfragmenterror', { xhr: xhr, status: status, error: error }); + }, + complete: function() { + // Reset the current XHR in tracking object. + var fragmentXHR = self.getFragmentXHR(); + if( + typeof fragmentXHR[pjaxFragments]!=='undefined' && + fragmentXHR[pjaxFragments]!==null + ) { + fragmentXHR[pjaxFragments] = null; + } + } + }); + + // Store the fragment request so we can abort later, should we get a duplicate request. + fragmentXHR[pjaxFragments] = xhr; }, /** diff --git a/docs/en/reference/cms-architecture.md b/docs/en/reference/cms-architecture.md index 4697cd5fd..7a2248330 100644 --- a/docs/en/reference/cms-architecture.md +++ b/docs/en/reference/cms-architecture.md @@ -287,6 +287,47 @@ tracked in the browser history, use the `pjax` attribute on the state data. $('.cms-container').loadPanel('admin/pages', null, {pjax: 'Content'}); +## Loading lightweight PJAX fragments + +Normal navigation between URLs in the admin section of the Framework occurs through `loadPanel` and `submitForm`. +These calls make sure the HTML5 history is updated correctly and back and forward buttons work. They also take +care of some automation, for example restoring currently selected tabs. + +However there are situations when you would like to only update a small area in the CMS, and when this operation should +not trigger a browser's history pushState. A good example here is reloading a dropdown that relies on backend session +information that could have been updated as part of action elsewhere, updates to sidebar status, or other areas +unrelated to the main flow. + +In this case you can use the `loadFragment` call supplied by `LeftAndMain.js`. You can trigger as many of these in +parallel as you want. This will not disturb the main navigation. + + $('.cms-container').loadFragment('admin/foobar/', 'Fragment1'); + $('.cms-container').loadFragment('admin/foobar/', 'Fragment2'); + $('.cms-container').loadFragment('admin/foobar/', 'Fragment3'); + +The ongoing requests are tracked by the PJAX fragment name (Fragment1, 2, and 3 above) - resubmission will +result in the prior request for this fragment to be aborted. Other parallel requests will continue undisturbed. + +You can also load multiple fragments in one request, as long as they are to the same controller (i.e. URL): + + $('.cms-container').loadFragment('admin/foobar/', 'Fragment2,Fragment3'); + +This counts as a separate request type from the perspective of the request tracking, so will not abort the singular +`Fragment2` nor `Fragment3`. + +Upon the receipt of the response, the fragment will be injected into DOM where a matching `data-pjax-fragment` attribute +has been found on an element (this element will get completely replaced). Afterwards a `afterloadfragment` event +will be triggered. In case of a request error a `loadfragmenterror` will be raised and DOM will not be touched. + +You can hook up a response handler that obtains all the details of the XHR request like this: + + 'from .cms-container': { + onafterloadfragment: function(e, data) { + // Say 'success'! + alert(data.status); + } + } + ## Ajax Redirects Sometimes, a server response represents a new URL state, e.g. when submitting an "add record" form,