(function (root, factory) {
Backbone.React.Component v0.10.0
(c) 2014, 2015 "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.model.foo}</div>;
}
});
var model = new Backbone.Model({foo: 'bar'});
ReactDOM.render(<MyComponent model={model} />, document.body);
(function (root, factory) {
Universal module definition
if (typeof define === 'function' && define.amd) {
define(['react', 'react-dom', 'backbone', 'underscore'], factory);
} else if (typeof module !== 'undefined' && module.exports) {
module.exports = factory(require('react'), require('react-dom'), require('backbone'), require('underscore'));
} else {
factory(root.React, root.ReactDOM, root.Backbone, root._);
}
}(this, function (React, ReactDOM, 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(ReactDOM.findDOMNode(this));
},
Sets this.el
and this.$el
when the component updates.
componentDidUpdate: function () {
this.setElement(ReactDOM.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 = ReactDOM.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) {
var isValid = typeof models !== 'undefined';
if (isValid) {
if (!models.attributes) {
if (typeof models === 'object') {
var _values = _.values(models);
isValid = _values.length > 0 && _values[0].attributes;
} else {
isValid = false;
}
}
}
if (isValid) {
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 update
,
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);
},
Get the attributes for the collection or model as array or hash
getAttributes: function (modelOrCollection){
var attrs = [];
if a collection, get the attributes of each, otherwise return modelOrCollection
if (modelOrCollection instanceof Backbone.Collection) {
for (var i = 0; i < modelOrCollection.models.length; i++) {
attrs.push(modelOrCollection.models[i].attributes);
}
return attrs;
} else {
return modelOrCollection.attributes
}
},
Sets a model, collection or object into state by delegating to this.component.setState
.
setState: function (modelOrCollection, key, target, isDeferred) {
var state = {};
var newState = this.getAttributes(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, 'update 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);
}
}
});
Facade method to bypass the mixin
usage. For use cases such as ES6
classes or else. It binds any Backbone.Model
and Backbone.Collection
instance found inside backboneInstances.models
and
backboneInstances.collections
(single instances or objects of them)
mixin.on = function (component, backboneInstances) {
var wrapper;
if (!component.wrapper) {
wrapper = new Wrapper(component);
} else {
wrapper = component.wrapper;
}
if (backboneInstances.models) {
wrapper.setModels(backboneInstances.models);
}
if (backboneInstances.collections) {
wrapper.setCollections(backboneInstances.collections);
}
component.wrapper = wrapper;
};
Shortcut method to bind a model or multiple models
mixin.onModel = function (component, models) {
mixin.on(component, {models: models});
};
Shortcut method to bind a collection or multiple collections
mixin.onCollection = function (component, collections) {
mixin.on(component, {collections: collections});
};
Facade method to dispose of a component.wrapper
mixin.off = function (component, modelOrCollection) {
if (arguments.length === 2) {
if (component.wrapper) {
component.wrapper.stopListening(modelOrCollection);
TODO Remove modelOrCollection from component.state
?
}
} else {
mixin.componentWillUnmount.call(component);
}
};
Expose Backbone.React.Component.mixin
.
return mixin;
}));