diff --git a/backbone-reaction.js b/backbone-reaction.js index 7faab8a..6edb475 100644 --- a/backbone-reaction.js +++ b/backbone-reaction.js @@ -25,10 +25,10 @@ /* Container script which includes the following: - https://github.com/jhudson8/react-mixin-manager v0.7.1 - https://github.com/jhudson8/react-events v0.5.0 - https://github.com/jhudson8/backbone-xhr-events v0.6.0 - https://github.com/jhudson8/react-backbone v0.11.0 + https://github.com/jhudson8/react-mixin-manager v0.8.0 + https://github.com/jhudson8/react-events v0.5.2 + https://github.com/jhudson8/backbone-xhr-events v0.8.1 + https://github.com/jhudson8/react-backbone v0.11.2 */ (function(main) { if (typeof define === 'function' && define.amd) { @@ -64,6 +64,22 @@ var SUCCESS = 'success'; var ERROR = 'error'; + var Context = function(method, model, options) { + this.method = method; + this.model = model; + this.options = options; + } + Context.prototype.abort = function() { + if (!this.aborted) { + this.aborted = true; + this.preventDefault = true; + if (this.xhr) { + this.xhr.abort(); + } + } + } + _.extend(Context.prototype, Backbone.Events); + // allow backbone to send xhr events on models var _sync = Backbone.sync; Backbone.sync = function (method, model, options) { @@ -74,20 +90,14 @@ options.url = _.result(model, 'url'); } - var context = initializeXHRLoading(model, model, options, method); - - // options.intercept can be used to override the standard response - // it is assumed that either options.success or options.error will be called - var intercept = context.intercept; - if (intercept) { - if (_.isFunction(intercept)) { - return intercept(options); - } else { - throw new Error("intercept must be function(options)"); - } + var context = initializeXHRLoading(method, model, model, options); + if (context.preventDefault) { + // it is assumed that either context.options.success or context.options.error will be called + return; } - context.xhr = _sync.call(this, method, model, options); - return context.xhr; + var xhr = _sync.call(this, method, model, options); + context.xhr = xhr; + return xhr; }; // provide helper flags to determine model fetched status @@ -102,6 +112,31 @@ }); + // execute the callback directly if the model is fetch + // initiate a fetch with this callback as the success option if not fetched + // or plug into the current fetch if in progress + Backbone.Model.prototype.whenFetched = Backbone.Collection.whenFetched = function(success, error) { + var model = this; + function successWrapper() { + success(model); + } + if (this.hasBeenFetched) { + return success(this); + } + // find current fetch call (if any) + var _fetch = _.find(this[xhrLoadingAttribute], function(req) { + return req.method === 'read'; + }); + if (_fetch) { + _fetch.on('success', successWrapper); + if (error) { + _fetch.on('error', error); + } + } else { + this.fetch({ success: successWrapper, error: error }); + } + } + // forward all or some XHR events from the source object to the dest object Backbone.forwardXhrEvents = function (source, dest, typeOrCallback) { var handler = handleForwardedEvents(!_.isFunction(typeOrCallback) && typeOrCallback); @@ -140,7 +175,7 @@ eventName = type; } // these events will be called because we are using the same options object as the source call - initializeXHRLoading(this, events.model, events.options, events.method); + initializeXHRLoading(events.method, this, events.model, events.options); } _eventForwarders[type] = func; } @@ -151,15 +186,10 @@ // "model" is to trigger events on and "sourceModel" is the model to provide to the success/error callbacks // these are the same unless there is event forwarding in which case the "sourceModel" is the model that actually // triggered the events and "model" is just forwarding those events - function initializeXHRLoading(model, sourceModel, options, method) { + function initializeXHRLoading(method, model, sourceModel, options) { var loads = model[xhrLoadingAttribute] = model[xhrLoadingAttribute] || [], eventName = options && options.event || method, - context = _.extend({}, Backbone.Events); - - context.method = method; - context.options = options; - context.model = sourceModel; - loads.push(context); + context = new Context(method, sourceModel, options); var scopedEventName = xhrEventName + ':' + eventName; model.trigger(xhrEventName, eventName, context); @@ -170,15 +200,35 @@ globalXhrBus.trigger(scopedEventName, model, context); } + // allow for 1 last override + var _beforeSend = options.beforeSend; + options.beforeSend = function(xhr, settings) { + context.xhr = xhr; + context.settings = settings; + + if (_beforeSend) { + var rtn = _beforeSend.call(this, xhr, settings); + if (rtn === false) { + return rtn; + } + } + context.trigger('before-send', xhr, settings, context); + if (context.preventDefault) { + return false; + } + loads.push(context); + }; + + function onComplete(type) { var _type = options[type]; // success: (data, status, xhr); error: (xhr, type, error) options[type] = function (p1, p2, p3) { - if (type === SUCCESS && !context.stop) { + if (type === SUCCESS && !context.preventDefault) { // trigger the "data" event which allows manipulation of the response before any other events or callbacks are fired - context.trigger('data', p1, p2, p3, context); - p1 = context.response || p1; - // if lifecycleEvents.stop is set, it is assumed that the option success or callback will be manually called + context.trigger('after-send', p1, p2, p3, type, context); + p1 = context.data || p1; + // if context.preventDefault is true, it is assumed that the option success or callback will be manually called if (context.preventDefault) { return; } @@ -203,7 +253,7 @@ } // trigger the success/error event - var args = (type === SUCCESS) ? [type, sourceModel, context] : [type, sourceModel, p1, p2, p3, context]; + var args = (type === SUCCESS) ? [type, context] : [type, p1, p2, p3, context]; context.trigger.apply(context, args); // trigger the complete event @@ -218,7 +268,7 @@ } /******************* - * end of backbone-async-event + * end of backbone-xhr-events ********************/ })(); @@ -863,7 +913,7 @@ } function getKey(context) { - return context.props.key || context.props.ref; + return context.key || context.ref || context.props.key || context.props.ref; } function modelEventHandler(identifier, context, eventFormat, callback) { @@ -953,19 +1003,19 @@ React.mixins.add('modelValueAware', function(key) { return { getModelValue: function() { - key = key || getKey(this); + var _key = key || getKey(this); var model = this.getModel(); - if (model && key) { - return model.get(key); + if (model && _key) { + return model.get(_key); } }, setModelValue: function(value, options) { - key = key || getKey(this); + var _key = key || getKey(this); var model = this.getModel(); model = this.getModel(); - if (model && key) { - return model.set(key, value, options); + if (model && _key) { + return model.set(_key, value, options); } } } @@ -1181,8 +1231,8 @@ var key = getKey(this); if (key) { this.modelOn('invalid', function(model, errors) { - errors = this.modelIndexErrors(errors) || {}; - var message = errors[key]; + var _errors = this.modelIndexErrors(errors) || {}; + var message = _errors && _errors[key]; if (message) { setState({invalid: message}, this); }