// Backbone React Component // ======================== // // Backbone.React.Component v0.6.2 // // (c) 2014 "Magalhas" José Magalhães // Backbone.React.Component can be freely distributed under the MIT license. // // // `Backbone.React.Component` is a React.Component wrapper that serves // as a bridge between the React and Backbone worlds. Besides some extra members // that may be set by extending/instancing a component, it works pretty much the // same way that [React](http://facebook.github.io/react/) components do. // // When mounted (if using mixin mode) or created (wrapper mode) it starts listening // to models and collections changes to automatically set your component props and // achieve UI binding through reactive updates. // // // Basic usage // // var MyComponent = Backbone.React.Component.extend({ // render: function () { // return
{this.props.foo}
; // } // }); // var model = new Backbone.Model({foo: 'bar'}); // var myComponent = ; // myComponent.mount(); // // Mixin usage // // var MyComponent = React.createClass({ // mixins: [Backbone.React.Component.mixin], // render: function () { // return
{this.props.foo}
; // } // }); // var model = new Backbone.Model({foo: 'bar'}); // React.renderComponent(, document.body); 'use strict'; (function (root, factory) { // Universal module definition if (typeof define === 'function' && define.amd) define(['react', 'backbone', 'underscore'], factory); else if (typeof module !== 'undefined' && module.exports) { var React = require('react'); var Backbone = require('backbone'); var _ = require('underscore'); module.exports = factory(React, Backbone, _); } else factory(root.React, root.Backbone, root._); }(this, function (React, Backbone, _) { // Here lies the public API available on each component that runs through the wrapper // instead of the mixin approach. if (!Backbone.React) Backbone.React = {}; Backbone.React.Component = { // Wraps `React.Component` into `Backbone.React.Component` and extends to a new // class. extend: function (Clazz) { function Factory (props, children) { return (new Wrapper(Component, props, children)).virtualComponent; } // Allow deep extending Factory.extend = function () { return Backbone.React.Component.extend(_.extend({}, Clazz, arguments[0])); }; // Apply mixin if (Clazz.mixins) { if (Clazz.mixins[0] !== mixin) Clazz.mixins.splice(0, 0, mixin); } else Clazz.mixins = [mixin]; // Create the react component later used by `Factory` var Component = React.createClass(_.extend(Factory.prototype, Clazz)); return Factory; }, // Renders/mounts the component. It delegates to `React.renderComponent`. mount: function (el, onRender) { if (!(el || this.el)) throw new Error('No element to mount on'); else if (!el) el = this.el; this.wrapper.component = React.renderComponent(this, el, onRender); return this; }, // Stops all listeners and unmounts this component from the DOM. remove: function () { if (this.wrapper.component && this.wrapper.component.isMounted()) this.unmount(); this.wrapper.stopListening(); this.stopListening(); }, // Intended to be used on the server side (Node.js), renders your component to a string // ready to be used on the client side by delegating to `React.renderComponentToString`. toHTML: function () { // Since we're only able to call `renderComponentToString` once, lets clone this component // and use it insteaad. var clone = this.clone(_.extend({}, this.props, { collection: this.wrapper.collection, model: this.wrapper.model })); // Return the html representation of the component. return React.renderComponentToString(clone); }, // Unmount the component from the DOM. unmount: function () { var parentNode = this.el.parentNode; if (!React.unmountComponentAtNode(parentNode)) { // Throw an error if there's any unmounting the component. throw new Error('There was an error unmounting the component'); } delete this.wrapper.component; this.setElement(parentNode); } }; // Mixin used in all component instances. Exported through `Backbone.React.Component.mixin`. var mixin = Backbone.React.Component.mixin = { // Sets this.el and this.$el when the component mounts. componentDidMount: function () { this.setElement(this.getDOMNode()); }, // Sets this.el and this.$el when the component updates. componentDidUpdate: function () { this.setElement(this.getDOMNode()); }, // Checks if the component is running in mixin or wrapper mode when it mounts. // If it's a mixin set a flag for later use and instance a Wrapper to take care // of models and collections binding. componentWillMount: function () { if (!this.wrapper) { this.isBackboneMixin = true; this.wrapper = new Wrapper(this, this.props); } }, // If running in mixin mode disposes of the wrapper that was created when the // component mounted. componentWillUnmount: function () { if (this.wrapper && this.isBackboneMixin) { this.wrapper.stopListening(); delete this.wrapper; } }, // Shortcut to this.$el.find. Inspired by Backbone.View. $: function () { if (this.$el) return this.$el.find.apply(this.$el, arguments); }, // Crawls to the owner of the component searching for a collection. getCollection: function () { var owner = this; var lookup = owner.wrapper; while (!lookup.collection) { owner = owner._owner; if (!owner) throw new Error('Collection not found'); lookup = owner.wrapper; } return lookup.collection; }, // Crawls to the owner of the component searching for a model. getModel: function () { var owner = this; var lookup = owner.wrapper; while (!lookup.model) { owner = owner._owner; if (!owner) throw new Error('Model not found'); lookup = owner.wrapper; } return lookup.model; }, // Crawls `this._owner` recursively until it finds the owner of this // component. In case of being a parent component (no owners) it returns itself. getOwner: function () { var owner = this; while (owner._owner) owner = owner._owner; return owner; }, // 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, props) { props = props || {}; var el, model = props.model, collection = props.collection; // Check if props.el is a DOM element or a jQuery object if (_.isElement(props.el) || Backbone.$ && props.el instanceof Backbone.$) { el = props.el; delete props.el; } // Check if props.model is a Backbone.Model or an hashmap of them if (typeof model !== 'undefined' && (model.attributes || typeof model === 'object' && _.values(model)[0].attributes)) { delete props.model; // The model(s) bound to this component this.model = model; // Set model(s) attributes on props for the first render this.setPropsBackbone(model, void 0, props); } // Check if props.collection is a Backbone.Collection or an hashmap of them if (typeof collection !== 'undefined' && (collection.models || typeof collection === 'object' && _.values(collection)[0].models)) { delete props.collection; // The collection(s) bound to this component this.collection = collection; // Set collection(s) models on props for the first render this.setPropsBackbone(collection, void 0, props); } var component; if (Component.prototype) { // Instance the component mixing Backbone.Events, our public API and some special // properties. component = this.virtualComponent = _.defaults(Component.apply(this, _.rest(arguments)).__realComponentInstance, Backbone.Events, _.omit(Backbone.React.Component, 'mixin'), { // Clones the component wrapper and returns the component. clone: function (props, children) { return (new Wrapper(Component, props, children)).virtualComponent; }, // Assign a component unique id, this is handy sometimes as a DOM attribute cid: _.uniqueId(), // One to one relationship between the wrapper and the component wrapper: this }); // Set element if (el) mixin.setElement.call(component, el); } else { component = Component; this.component = component; } // Start listeners if this is a root node if (!component._owner) { this.startModelListeners(); this.startCollectionListeners(); } } // Mixing `Backbone.Events` into `Wrapper.prototype` _.extend(Wrapper.prototype, Backbone.Events, { // Sets this.props when a model/collection request results in error. It delegates // to `this.setProps`. It listens to `Backbone.Model#error` and `Backbone.Collection#error`. onError: function (modelOrCollection, res, options) { // Set props only if there's no silent option if (!options.silent) this.setProps({ isRequesting: false, hasError: true, error: res }); }, // Sets this.props when a model/collection request starts. It delegates to // `this.setProps`. It listens to `Backbone.Model#request` and `Backbone.Collection#request`. onRequest: function (modelOrCollection, xhr, options) { // Set props only if there's no silent option if (!options.silent) this.setProps({ isRequesting: true, hasError: false }); }, // Sets this.props when a model/collection syncs. It delegates to `this.setProps`. // It listens to `Backbone.Model#sync` and `Backbone.Collection#sync` onSync: function (modelOrCollection, res, options) { // Set props only if there's no silent option if (!options.silent) this.setProps({isRequesting: false}); }, // Used internally to set this.collection or this.model on this.props. Delegates to // `this.setProps`. It listens to `Backbone.Collection#add`, `Backbone.Collection#remove`, // `Backbone.Collection#change` and `Backbone.Model#change`. setPropsBackbone: function (modelOrCollection, key, target) { if (!(modelOrCollection.models || modelOrCollection.attributes)) { for (key in modelOrCollection) this.setPropsBackbone(modelOrCollection[key], key, target); return; } this.setProps.apply(this, arguments); }, // Sets a model, collection or object into props by delegating to `React.Component#setProps`. setProps: function (modelOrCollection, key, target) { // If the component isn't rendered/mounted set target because you can't set props // on an unmounted (virtual) component. if (!target && !(this.component && this.component.isMounted())) target = this.virtualComponent.props; var props = {}; var newProps = modelOrCollection.toJSON ? modelOrCollection.toJSON() : modelOrCollection; if (key) props[key] = newProps; else if (modelOrCollection instanceof Backbone.Collection) props.collection = newProps; else props = newProps; if (target) _.extend(target, props); else { this.nextProps = _.extend(this.nextProps || {}, props); _.defer(_.bind(function () { if (this.nextProps) { this.component.setProps(this.nextProps); delete this.nextProps; } }, this)); } }, // 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.setPropsBackbone, collection, key, void 0)) .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.setPropsBackbone, model, key, void 0)) .listenTo(model, 'error', this.onError) .listenTo(model, 'request', this.onRequest) .listenTo(model, 'sync', this.onSync); else if (typeof model === 'object') for (key in model) this.startModelListeners(model[key], key); } } }); // Expose `Backbone.React.Component`. return Backbone.React.Component; })); // Fork me on GitHub