Do more than what the default Backbone Model/Collection request
event does for you. The primary benefits are
- Provide robust lifecycle events (
before-send
,after-send
,success
,error
,complete
) - Emit type specific XHR events to allow for focused binding
- Give ability to see if a model currently has any pending XHR activity
- Provide a global event bus to bind to all Model/Collection XHR activity
- Allow requests to be intercepted to, for example, return cached content
- Add ability to intercept and override response data before it is returned to the Model/Collection
- Provide event forwarding capabilities so other objects can simulate XHR activity to match another Model/Collection
- Make all event names and additional attributes overrideable to meet the needs of your particular project
- Give external entities a way to observe ajax activity on your Collections and Models
Browser:
<script src=".../underscore[-min].js"></script>
<script src=".../backbone[-min].js"></script>
<script src=".../backbone-xhr-events[-min].js"></script>
CommonJS
require('backbone-xhr-events');
AMD
require(
['backbone-xhr-events'], function() {
// don't do do anything with it... it's initialized now
});
Listen to all XHR activity
Backbone.xhrEvents.on('xhr', function(context) {
context.on('success', function() {
// context.model has been updated
});
});
Override fetch XHR payload or cache it
Backbone.xhrEvents.on('xhr:read', function(context) {
context.on('after-send', function(data, status, xhr, responseType) {
// responseType will either be 'success' or 'error'
if (responseType === 'success') {
// wrap the response as a "response" attribute (just to show how to modify it)
context.data = { response: data };
// cache the response
_cacheFetchResponse(JSON.stringify(data), context.xhrSettings.url);
}
});
});
Intercept a fetch request and return a cached payload
Backbone.xhrEvents.on('xhr:read', function(context) {
// use the "before-send" event (rather than just doing it here)
// so we have access to the XHR settings to get the URL
context.on('before-send', function(xhr, settings) {
var cachedResult = _getFetchCache(settings.url);
if (cachedResult) {
context.preventDefault().success(JSON.parse(cachedResult), 'success');
}
});
});
Make a successful XHR look like a failure
model.on('xhr', function(context) {
context.on('after-send', function(data, status, xhr, responseType) {
if (responseType === 'success') {
// provide the parameters that you would have wanted coming back directly from the $.ajax callback
context.preventDefault().error(xhr, 'error', 'Not Found');
}
});
});
Alter the payload asynchronously after the initial response is returned
Backbone.xhrEvents.on('xhr', function(context) {
context.on('after-send', function(data, status, xhr) {
var handler = context.preventDefault();
yourOwnGetDataAsyncMethod(function(data) {
handler.success(data, status, xhr);
});
});
});
Add simulated 1 second latency
Backbone.xhrEvents.on('xhr', function(context) {
context.on('after-send', function(p1, p2, p3, responseType) {
var handler = context.preventDefault();
setTimeout(function() {
var successOrErrorMethod = handler[responseType];
successOrErrorMethod(p1, p2, p3);
}, 1000);
});
});
Set a default connection timeout on all XHR activity
Backbone.xhrEvents.on('xhr', function(context) {
context.options.timeout = 3000;
});
Determine fetch status of a model
model.fetch();
!!model.xhrActivity === true;
// model fetch complete now
!!model.xhrActivity === false;
// if the model fetch succeeded
model.hasBeenFetched === true;
model.hadFetchError === false;
// if the model fetch has failed...
model.hadFetchError === true;
model.hasBeenFetched === false;
Forward xhr events to another model (source model will continue to emit xhr events as well)
// forward all events
Backbone.forwardXHREvents(sourceModel, receiverModel);
// and stop the fowarding
Backbone.stopXHRForwarding(sourceModel, receiverModel);
// forward events for a specific Backbone.sync method
Backbone.forwardXHREvents(sourceModel, receiverModel, 'read');
// and stop the fowarding
Backbone.stopXHRForwarding(sourceModel, receiverModel, 'read');
// forward events *only while the callback function is executed*
Backbone.forwardXHREvents(sourceModel, receiverModel, function() {
// any XHR activity that sourceModel executes will be emitted by
// receiverModel as well
});
Prevent duplicate concurrent submission of any XHR request
Backbone.xhrEvents.on('xhr', function(context) {
var model = context.model,
method = context.method;
context.on('before-send', function(xhr, settings) {
// see if any current XHR activity matches this request
var match = _.find(model.xhrActivity, function(_context) {
return _context.xhrSettings.url === settings.url
&& method === _context.method
&& _.isEqual(_context.xhrSettings.data, settings.data);
});
if (match) {
var handler = context.preventDefault();
// when the pending request comes back, simulate the same activity on this request
match.on('success', handler.success);
match.on('error', handler.error);
}
});
});
Prevent the error callback if the request is aborted
Backbone.xhrEvents.on('xhr', function(context) {
context.on('abort', function() {
context.preventDefault().complete('abort');
});
});
// create, update, patch, delete, read
- fetch: read method (
xhr:read
) - destroy: delete method (
xhr:destroy
) - save (new): create method (
xhr:create
) - save (existing): update (
xhr:update
) - save (existing with options {patch: true}): patch (
xhr:patch
)
Custom Event
this.fetch({event: foo}) -> "xhr:foo"
Almost all event names and model/global attributes can be overridden to suit your needs.
- Backbone.xhrCompleteEventName: the event triggered on a model/collection when all XHR activity has completed (default:
xhr:complete
) - Backbone.xhrModelLoadingAttribute: the model attribute which can be used to return an array of all current XHR request events and determind if the model/collection has current XHR activity (default:
xhrActivity
) - Backbone.xhrEventName: the event triggered on models/collection and the global bus to signal an XHR request (default:
xhr
) - Backbone.xhrGlobalAttribute: global event handler attribute name (on Backbone) used to subscribe to all model xhr events (default:
xhrEvents
)
The RequestContext is the object provided as the only parameter with the xhr
events. It is the context providing the ability to bind to the request lifecycle events.
model.on('xhr', function(requestContext) { ... });
see the RequestContext events and RequestContext functions and attributes
The RequestHandler is the response from requestContext.preventDefault(). When preventDefault
is called, the request lifecycle is halted. When this happens, you must either call success, error, or complete.
- xhr: the XMLHTTPRequest
- settings: the actual jquery settings object sent by Backbone.sync
- requestContext: the same object passed to the
xhr
event. It is provided in case the function does not have access to the closure scope.
Triggered after Backbone.sync has been executed and an XHR object has been created (but before execution).
model.on('xhr', function(requestContext) {
requestContext.on('before-send', function(xhr, settings, requestContext) {
...
});
});
- jqueryParams: if success
data, status, xhr
; if errorxhr, status, error
; more details - responseType: if success
success
; if errorerror
; - requestContext: the same object passed to the
xhr
event. It is provided in case the function does not have access to the closure scope.
Triggered after the XMLHttpRequest has completed and before the Backbone.sync callback has been executed. This can be used to override the response data or alter the default behavior of the response.
model.on('xhr', function(requestContext) {
requestContext.on('after-send', function(p1, p2, p3, responseType, requestContext) {
if (responseType === 'success') {
var data = p1, status = p2, xhr = p3;
// we can change the response payload using requestContext.data
requestContext.data = {foo: 'bar'};
} else if (responseType === 'error') {
var xhr = p1, status = p2, error = p3;
// or we can completely change the default behavior and make an error look like a success
requestContext.preventDefault().success({foo: 'bar'}, 'success', xhr);
}
});
});
- data: the response payload
- status: the XMLHttpRequest text status
- xhr: the XMLHttpRequest
- requestContext: the same object passed to the
xhr
event. It is provided in case the function does not have access to the closure scope.
Triggered after the success callback handler has executed. This can be used to perform actions after the model has been updated.
model.on('xhr', function(requestContext) {
requestContext.on('success', function(data, status, xhr, requestContext) {
// the updated mode/collection is requestContext.model
});
});
- xhr: the XMLHttpRequest
- status: the XMLHttpRequest text status
- error: the error thrown
- requestContext: the same object passed to the
xhr
event. It is provided in case the function does not have access to the closure scope.
Triggered after the error callback handler has executed. This can be used to perform actions when an error scenario has occurred.
model.on('xhr', function(requestContext) {
requestContext.on('error', function(xhr, status, error, requestContext) {
...
});
});
- responseType: if success
success
; if errorerror
- requestContext: the same object passed to the
xhr
event. It is provided in case the function does not have access to the closure scope.
model.on('xhr', function(requestContext) {
requestContext.on('complete', function(type, requestContext) {
...
});
});
Executed when the XHR is aborted using RequestContext.abort
model.on('xhr', function(requestContext) {
requestContext.on('abort', function() {
// if we wanted to prevent the error callback when we abort
requestContext.preventDefault().complete('abort');
});
});
- method: the Backbone.sync method (by default,
read
,update
, ordelete
) - requestContext: the RequestContext
This event is triggered on any model or collection when any XHR activity is initiated from that model / collection. It is also triggered on Backbone.xhrEvents
when XHR activity is initiated from any model / collection.
model.on('xhr', function(requestContext, method) {
// the method is also available as requestContext.method
// for any fetch operations
if (method === 'read') {
...
}
});
or, to watch XHR activity from any model or collection
Backbone.xhrEvents.on('xhr', function(requestContext, method) {
var theModel = requestContext.model;
...
});
- method: the Backbone sync method (by default,
read
,update
, ordelete
) - requestContext: the request context
Emitted when only XHR activity matching the method in the event name occurs
model.on('xhr:read', function(requestContext) {
// this is only triggered on model.fetch()
});
or, to watch XHR activity from any model or collection
Backbone.xhrEvents.on('xhr:read', function(method, model, context) {
...
});
- context: the context representing the last XHR activity (see RequestContext)
Triggered on a model or collection (not Backbone.xhrActivity) when any XHR activity has completed and there is no more concurrent XHR activity
The RequestContext is the object provided as the only parameter with the xhr
events. It is the context providing the ability to bind to the request lifecycle events.
model.on('xhr', function(requestContext) { ... });
also refer to the RequestContext events
Prevent the default execution of the request lifecycle. A ResponseHandler is returned and either the success
, error
or complete
method must be called.
The following example demonstrates preventing an XMLHttpRequest from being created and simulating a success response.
model.on('xhr', function(requestContext) {
var responseHandler = requestContext.stopPropogation();
setTimeout(function() {
// this can be used asynchronously
responseHandler.success({foo: 'bar'}, 'success');
}, 100);
});
Abort the XMLHttpRequest and trigger the abort
event.
model.on('xhr', function(requestContext) {
// if we need to abort for some reason
requestContext.abort();
});
The model or collection that initiated the request
var myModel = ...;
myModel.on('xhr', function(requestContext) {
requestContext.model === myModel;
});
The Backbone.sync method. This is either read
, update
, or delete
if the default methods are used.
The options provided to the Backbone.ajax method.
var myModel = ...;
myModel.on('xhr', function(requestContext) {
requestContext.options === {foo: 'bar'};
});
myModel.fetch({foo: 'bar'});
The XMLHttpRequest
model.on('xhr', function(requestContext) {
// this is a valid object on or after the "before-send" event
requestContext.xhr;
});
The XMLHttpRequest settings containing atributes like the request URL.
model.on('xhr', function(requestContext) {
// this is a valid object on or after the "before-send" event
var url = requestContext.xhrSettings.url;
});
The RequestHandler is the response from requestContext.preventDefault(). When preventDefault
is called, the request lifecycle is halted. When this happens, you must either call success, error, or complete.
- data: the response payload
- status: the XMLHttpRequest text status
- xhr: the XMLHttpRequest
Initiate a success response if the preventDefault method was called.
model.on('xhr', function(requestContext) {
var requestHandler = requestContext.preventDefault();
requestHandler.success({foo: 'bar'}, 'success', undefined);
});
- xhr: the XMLHttpRequest
- status: the XMLHttpRequest text status
- error: the error thrown
Initiate an error response if the preventDefault method was called.
model.on('xhr', function(requestContext) {
var requestHandler = requestContext.preventDefault();
requestHandler.error(undefined, 'error', 'Not Found');
});
- type: the completion type; normally
success
orerror
but can technically be anything. This will be the value passed to thecomplete
event triggered from the RequestContext.
Using this method will bypass any success / error handlers bound to the XHR request but any completion handlers will still be executed.
model.on('xhr', function(requestContext) {
var requestHandler = requestContext.preventDefault();
requestHandler.complete('abort');
});
- successCallback: function to be called when the model/collection has been fetched
- errorCallback: function to be called when if model/collection fetch failed
Initiate a fetch if not already fetching or fetched. Once the model/collection has been fetch, execute the appropriate callback.
myModel.whenFetched(function(model) {
// executed when model is fetched (model and myModel are the same)
},
function(model) {
// executed if the model fetch fails
}
);
This is a Model or Collection attribute that can be evaluated as a truthy if there is any current XHR activity.
var isCurrentXhrActivity = !!model.xhrActivity;
This will be true if the Model or Collection has previously been fetched with a sucessful response. If the Model or Collection is cleared, this value will be reset to undefined.
var isTheModelPopulated = model.hasBeenFetched;
This will be true if the Model or Collection encountered an XHR error when performing a fetch. It will reset to false upon a successful fetch operation.
var wasThereAFetchError = model.hadFetchError;
- sourceModel: the originator model of the XHR events
- destModel: the receiver or proxy of the source model XHR events
- method: the optional Backbone.sync method to filter the forwarded events
- autoStopFunc: callback function that, if provided, will stop the forwarding after the function completes execution
Forward XHR events that originate in sourceModel
to destModel
. These events will also be emitted in sourceModel
as well.
This can be useful if you have a composite model containing sub-models and want to aggregate xhr activity to the composite model.
var CompositeModel = Backbone.Model.extend({
initialize: function() {
Backbone.Model.prototype.initialize.apply(this, arguments);
// when model1 or model2 have xhr activity, "this" will expose the same xhr events
this.model1 = new Backbone.Model();
Backbone.forwardXHREvents(this.model1, this);
this.model2 = new Backbone.Model();
Backbone.forwardXHREvents(this.model2, this);
}
});
- sourceModel: the originator model of the XHR events
- destModel: the receiver or proxy of the source model XHR events
- method: optional Backbone.sync method to filter the forwarded events
Stop forwarding XHR events. This must match a previous forwardXHREvents
call.
Backbone.forwardXHREvents(sourceModel, destModel, 'read');