(function (root, factory) {Backbone.React.Component v0.8.1
(c) 2014 "Magalhas" José Magalhães <magalhas@gmail.com>
Backbone.React.Component can be freely distributed under the MIT license.
Backbone.React.Component is a mixin that glues Backbone
models and collections into React components.
When the component is mounted, a wrapper starts listening to models and collections changes to automatically set your component state and achieve UI binding through reactive updates.
Basic Usage
var MyComponent = React.createClass({
mixins: [Backbone.React.Component.mixin],
render: function () {
return <div>{this.state.foo}</div>;
}
});
var model = new Backbone.Model({foo: 'bar'});
React.render(<MyComponent model={model} />, document.body);
(function (root, factory) {Universal module definition
if (typeof define === 'function' && define.amd) {
define(['react', 'backbone', 'underscore'], factory);
} else if (typeof module !== 'undefined' && module.exports) {
module.exports = factory(require('react'), require('backbone'), require('underscore'));
} else {
factory(root.React, root.Backbone, root._);
}
}(this, function (React, Backbone, _) {
'use strict';
if (!Backbone.React) {
Backbone.React = {};
}
if (!Backbone.React.Component) {
Backbone.React.Component = {};
}Mixin used in all component instances. Exported through Backbone.React.Component.mixin.
var mixin = Backbone.React.Component.mixin = {Types of the context passed to child components. Only
hasParentBackboneMixin is required all of the others are optional.
childContextTypes: {
hasParentBackboneMixin: React.PropTypes.bool.isRequired,
parentModel: React.PropTypes.any,
parentCollection: React.PropTypes.any
},Types of the context received from the parent component. All of them are optional.
contextTypes: {
hasParentBackboneMixin: React.PropTypes.bool,
parentModel: React.PropTypes.any,
parentCollection: React.PropTypes.any
},Passes data to our child components.
getChildContext: function () {
return {
hasParentBackboneMixin: true,
parentModel: this.getModel(),
parentCollection: this.getCollection()
};
},Sets this.el and this.$el when the component mounts.
componentDidMount: function () {
this.setElement(React.findDOMNode(this));
},Sets this.el and this.$el when the component updates.
componentDidUpdate: function () {
this.setElement(React.findDOMNode(this));
},When the component gets the initial state, instance a Wrapper to take
care of models and collections binding with this.state.
getInitialState: function () {
var initialState = {};
if (!this.wrapper) {
this.wrapper = new Wrapper(this, initialState);
}
return initialState;
},When the component mounts, instance a Wrapper to take care
of models and collections binding with this.state.
componentWillMount: function () {
if (!this.wrapper) {
this.wrapper = new Wrapper(this);
}
},When the component unmounts, dispose listeners and delete
this.wrapper reference.
componentWillUnmount: function () {
if (this.wrapper) {
this.wrapper.stopListening();
delete this.wrapper;
}
},In order to allow passing nested models and collections as reference we
filter nextProps.model and nextProps.collection.
componentWillReceiveProps: function (nextProps) {
var model = nextProps.model;
var collection = nextProps.collection;
if (this.wrapper.model && model) {
if (this.wrapper.model !== model) {
this.wrapper.stopListening();
this.wrapper = new Wrapper(this, void 0, nextProps);
}
} else if (model) {
this.wrapper = new Wrapper(this, void 0, nextProps);
}
if (this.wrapper.collection && collection) {
if (this.wrapper.collection !== collection) {
this.wrapper.stopListening();
this.wrapper = new Wrapper(this, void 0, nextProps);
}
} else if (collection) {
this.wrapper = new Wrapper(this, void 0, nextProps);
}
},Shortcut to @$el.find if jQuery ins present, else if fallbacks to DOM
native querySelector. Inspired by Backbone.View.
$: function () {
var els;
if (this.$el) {
els = this.$el.find.apply(this.$el, arguments);
} else {
var el = React.findDOMNode(this);
els = el.querySelector.apply(el, arguments);
}
return els;
},Grabs the collection from @wrapper.collection or @context.parentCollection
getCollection: function () {
return this.wrapper.collection || this.context.parentCollection;
},Grabs the model from @wrapper.model or @context.parentModel
getModel: function () {
return this.wrapper.model || this.context.parentModel;
},Sets a DOM element to render/mount this component on this.el and this.$el.
setElement: function (el) {
if (el && Backbone.$ && el instanceof Backbone.$) {
if (el.length > 1) {
throw new Error('You can only assign one element to a component');
}
this.el = el[0];
this.$el = el;
} else if (el) {
this.el = el;
if (Backbone.$) {
this.$el = Backbone.$(el);
}
}
return this;
}
};Binds models and collections to a React.Component. It mixes Backbone.Events.
function Wrapper (component, initialState, nextProps) {Object to store wrapper state (not the component state)
this.state = {};1:1 relation with the component
this.component = component;Use nextProps or component.props and grab model and collection
from there
var props = nextProps || component.props || {};
var model, collection;
if (component.overrideModel && typeof component.overrideModel === 'function'){Define overrideModel() method on your React class to programatically supply a model object
Will override this.props.model
model = component.overrideModel();
} else {
model = props.model;
}
if (component.overrideCollection && typeof component.overrideCollection === 'function'){Define overrideCollection() method on your React class to programatically supply a collection object
Will override this.props.collection
collection = component.overrideCollection();
} else {
collection = props.collection;
}
this.setModels(model, initialState);
this.setCollections(collection, initialState);
}Mixing Backbone.Events into Wrapper.prototype
_.extend(Wrapper.prototype, Backbone.Events, {Sets this.state when a model/collection request results in error. It delegates
to this.setState. It listens to Backbone.Model#error and Backbone.Collection#error.
onError: function (modelOrCollection, res, options) {Set state only if there’s no silent option
if (!options.silent) {
this.component.setState({
isRequesting: false,
hasError: true,
error: res
});
}
},
onInvalid: function (model, res, options) {
if (!options.silent) {
this.component.setState({
isInvalid: true
});
}
},Sets this.state when a model/collection request starts. It delegates to
this.setState. It listens to Backbone.Model#request and
Backbone.Collection#request.
onRequest: function (modelOrCollection, xhr, options) {Set state only if there’s no silent option
if (!options.silent) {
this.component.setState({
isRequesting: true,
hasError: false,
isInvalid: false
});
}
},Sets this.state when a model/collection syncs. It delegates to this.setState.
It listens to Backbone.Model#sync and Backbone.Collection#sync
onSync: function (modelOrCollection, res, options) {Calls setState only if there’s no silent option
if (!options.silent) {
this.component.setState({isRequesting: false});
}
},Check if models is a Backbone.Model or an hashmap of them, sets them
to the component state and binds to update on any future changes
setModels: function (models, initialState, isDeferred) {
if (typeof models !== 'undefined' && (models.attributes ||
typeof models === 'object' && _.values(models)[0].attributes)) {The model(s) bound to this component
this.model = models;Set model(s) attributes on initialState for the first render
this.setStateBackbone(models, void 0, initialState, isDeferred);
this.startModelListeners(models);
}
},Check if collections is a Backbone.Model or an hashmap of them,
sets them to the component state and binds to update on any future changes
setCollections: function (collections, initialState, isDeferred) {
if (typeof collections !== 'undefined' && (collections.models ||
typeof collections === 'object' && _.values(collections)[0].models)) {The collection(s) bound to this component
this.collection = collections;Set collection(s) models on initialState for the first render
this.setStateBackbone(collections, void 0, initialState, isDeferred);
this.startCollectionListeners(collections);
}
},Used internally to set this.collection or this.model on this.state. Delegates to
this.setState. It listens to Backbone.Collection events such as add, remove,
change, sort, reset and to Backbone.Model change.
setStateBackbone: function (modelOrCollection, key, target, isDeferred) {
if (!(modelOrCollection.models || modelOrCollection.attributes)) {
for (key in modelOrCollection)
this.setStateBackbone(modelOrCollection[key], key, target);
return;
}
this.setState.apply(this, arguments);
},Sets a model, collection or object into state by delegating to this.component.setState.
setState: function (modelOrCollection, key, target, isDeferred) {
var state = {};
var newState = modelOrCollection.toJSON ? modelOrCollection.toJSON() : modelOrCollection;
if (key) {
state[key] = newState;
} else if (modelOrCollection.models) {
state.collection = newState;
} else {
state.model = newState;
}
if (target) {
_.extend(target, state);
} else if (isDeferred) {
this.nextState = _.extend(this.nextState || {}, state);
_.defer(_.bind(function () {
if (this.nextState) {
this.component.setState(this.nextState);
this.nextState = null;
}
}, this));
} else {
this.component.setState(state);
}
},Binds the component to any collection changes.
startCollectionListeners: function (collection, key) {
if (!collection) collection = this.collection;
if (collection) {
if (collection.models)
this
.listenTo(collection, 'add remove change sort reset',
_.partial(this.setStateBackbone, collection, key, void 0, true))
.listenTo(collection, 'error', this.onError)
.listenTo(collection, 'request', this.onRequest)
.listenTo(collection, 'sync', this.onSync);
else if (typeof collection === 'object')
for (key in collection)
if (collection.hasOwnProperty(key))
this.startCollectionListeners(collection[key], key);
}
},Binds the component to any model changes.
startModelListeners: function (model, key) {
if (!model) model = this.model;
if (model) {
if (model.attributes)
this
.listenTo(model, 'change',
_.partial(this.setStateBackbone, model, key, void 0, true))
.listenTo(model, 'error', this.onError)
.listenTo(model, 'request', this.onRequest)
.listenTo(model, 'sync', this.onSync)
.listenTo(model, 'invalid', this.onInvalid);
else if (typeof model === 'object')
for (key in model)
this.startModelListeners(model[key], key);
}
}
});Expose Backbone.React.Component.mixin.
return mixin;
}));