59 | (function (root, factory) {├── .npmignore ├── .gitignore ├── docs ├── public │ ├── fonts │ │ ├── aller-bold.eot │ │ ├── aller-bold.ttf │ │ ├── aller-bold.woff │ │ ├── aller-light.eot │ │ ├── aller-light.ttf │ │ ├── aller-light.woff │ │ ├── roboto-black.eot │ │ ├── roboto-black.ttf │ │ ├── novecento-bold.eot │ │ ├── novecento-bold.ttf │ │ ├── novecento-bold.woff │ │ └── roboto-black.woff │ └── stylesheets │ │ └── normalize.css ├── docco.css └── component.html ├── .travis.yml ├── .editorconfig ├── examples ├── react-and-react-router │ ├── src │ │ ├── htdocs │ │ │ └── index.html │ │ └── javascripts │ │ │ └── global.js │ ├── Brocfile.js │ └── package.json ├── react-router │ ├── public │ │ └── index.html │ ├── src │ │ ├── store.js │ │ ├── models │ │ │ └── person.js │ │ └── main.js │ ├── package.json │ └── gulpfile.js ├── blog │ ├── public │ │ ├── boot.js │ │ ├── collections │ │ │ └── blog.js │ │ ├── index.html │ │ └── components │ │ │ ├── blog.jsx │ │ │ └── blog.js │ ├── package.json │ ├── Gruntfile.js │ ├── README.md │ └── server.js ├── typewriter │ ├── index.html │ └── typewriter.js └── nested │ ├── index.html │ └── nested.jsx ├── .jshintrc ├── test ├── helpers │ └── polyfills.js └── specs │ ├── on-off.js │ └── mixin.js ├── bower.json ├── LICENSE-MIT ├── package.json ├── Gruntfile.js ├── dist ├── backbone-react-component-min.js └── backbone-react-component.js ├── README.md └── lib └── component.js /.npmignore: -------------------------------------------------------------------------------- 1 | bower_components 2 | examples 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | bower_components 2 | node_modules 3 | 4 | .idea 5 | -------------------------------------------------------------------------------- /docs/public/fonts/aller-bold.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/magalhas/backbone-react-component/HEAD/docs/public/fonts/aller-bold.eot -------------------------------------------------------------------------------- /docs/public/fonts/aller-bold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/magalhas/backbone-react-component/HEAD/docs/public/fonts/aller-bold.ttf -------------------------------------------------------------------------------- /docs/public/fonts/aller-bold.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/magalhas/backbone-react-component/HEAD/docs/public/fonts/aller-bold.woff -------------------------------------------------------------------------------- /docs/public/fonts/aller-light.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/magalhas/backbone-react-component/HEAD/docs/public/fonts/aller-light.eot -------------------------------------------------------------------------------- /docs/public/fonts/aller-light.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/magalhas/backbone-react-component/HEAD/docs/public/fonts/aller-light.ttf -------------------------------------------------------------------------------- /docs/public/fonts/aller-light.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/magalhas/backbone-react-component/HEAD/docs/public/fonts/aller-light.woff -------------------------------------------------------------------------------- /docs/public/fonts/roboto-black.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/magalhas/backbone-react-component/HEAD/docs/public/fonts/roboto-black.eot -------------------------------------------------------------------------------- /docs/public/fonts/roboto-black.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/magalhas/backbone-react-component/HEAD/docs/public/fonts/roboto-black.ttf -------------------------------------------------------------------------------- /docs/public/fonts/novecento-bold.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/magalhas/backbone-react-component/HEAD/docs/public/fonts/novecento-bold.eot -------------------------------------------------------------------------------- /docs/public/fonts/novecento-bold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/magalhas/backbone-react-component/HEAD/docs/public/fonts/novecento-bold.ttf -------------------------------------------------------------------------------- /docs/public/fonts/novecento-bold.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/magalhas/backbone-react-component/HEAD/docs/public/fonts/novecento-bold.woff -------------------------------------------------------------------------------- /docs/public/fonts/roboto-black.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/magalhas/backbone-react-component/HEAD/docs/public/fonts/roboto-black.woff -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - 0.10 4 | notifications: 5 | email: true 6 | before_install: 7 | - npm install -g grunt-cli bower 8 | - bower install -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # editorconfig.org 2 | root = true 3 | 4 | [*] 5 | indent_style = space 6 | indent_size = 2 7 | end_of_line = lf 8 | charset = utf-8 9 | trim_trailing_whitespace = true 10 | insert_final_newline = true 11 | -------------------------------------------------------------------------------- /examples/react-and-react-router/src/htdocs/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 |
4 | 5 |{paragraph.content}
; 9 | }, 10 | render: function () { 11 | return ( 12 |{this.props.model.get('description')}
74 |{post.content}
43 |
411 |
--------------------------------------------------------------------------------
/lib/component.js:
--------------------------------------------------------------------------------
1 | // Backbone React Component
2 | // ========================
3 | //
4 | // Backbone.React.Component v0.10.0
5 | //
6 | // (c) 2014, 2015 "Magalhas" José Magalhães
411 |
--------------------------------------------------------------------------------
/docs/component.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Backbone.React.Component v0.10.0
38 |
39 | (c) 2014, 2015 "Magalhas" José Magalhães <magalhas@gmail.com>
40 | Backbone.React.Component can be freely distributed under the MIT license.
41 | Backbone.React.Component is a mixin that glues Backbone
42 | models and collections into React components.
When the component is mounted, a wrapper starts listening to models and 44 | collections changes to automatically set your component state and achieve UI 45 | binding through reactive updates.
46 |Basic Usage
47 |var MyComponent = React.createClass({
48 | mixins: [Backbone.React.Component.mixin],
49 | render: function () {
50 | return <div>{this.state.model.foo}</div>;
51 | }
52 | });
53 | var model = new Backbone.Model({foo: 'bar'});
54 | ReactDOM.render(<MyComponent model={model} />, document.body);
55 |
56 |
59 | (function (root, factory) {Universal module definition
71 | 72 | if (typeof define === 'function' && define.amd) {
75 | define(['react', 'react-dom', 'backbone', 'underscore'], factory);
76 | } else if (typeof module !== 'undefined' && module.exports) {
77 | module.exports = factory(require('react'), require('react-dom'), require('backbone'), require('underscore'));
78 | } else {
79 | factory(root.React, root.ReactDOM, root.Backbone, root._);
80 | }
81 | }(this, function (React, ReactDOM, Backbone, _) {
82 | 'use strict';
83 | if (!Backbone.React) {
84 | Backbone.React = {};
85 | }
86 | if (!Backbone.React.Component) {
87 | Backbone.React.Component = {};
88 | }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
115 | hasParentBackboneMixin is required all of the others are optional.
childContextTypes: {
120 | hasParentBackboneMixin: React.PropTypes.bool.isRequired,
121 | parentModel: React.PropTypes.any,
122 | parentCollection: React.PropTypes.any
123 | },Types of the context received from the parent component. All of them are 135 | optional.
136 | 137 | contextTypes: {
140 | hasParentBackboneMixin: React.PropTypes.bool,
141 | parentModel: React.PropTypes.any,
142 | parentCollection: React.PropTypes.any
143 | },Passes data to our child components.
155 | 156 | getChildContext: function () {
159 | return {
160 | hasParentBackboneMixin: true,
161 | parentModel: this.getModel(),
162 | parentCollection: this.getCollection()
163 | };
164 | },Sets this.el and this.$el when the component mounts.
componentDidMount: function () {
180 | this.setElement(ReactDOM.findDOMNode(this));
181 | },Sets this.el and this.$el when the component updates.
componentDidUpdate: function () {
197 | this.setElement(ReactDOM.findDOMNode(this));
198 | },When the component gets the initial state, instance a Wrapper to take
210 | care of models and collections binding with this.state.
getInitialState: function () {
215 | var initialState = {};
216 |
217 | if (!this.wrapper) {
218 | this.wrapper = new Wrapper(this, initialState);
219 | }
220 |
221 | return initialState;
222 | },When the component mounts, instance a Wrapper to take care
234 | of models and collections binding with this.state.
componentWillMount: function () {
239 | if (!this.wrapper) {
240 | this.wrapper = new Wrapper(this);
241 | }
242 | },When the component unmounts, dispose listeners and delete
254 | this.wrapper reference.
componentWillUnmount: function () {
259 | if (this.wrapper) {
260 | this.wrapper.stopListening();
261 | delete this.wrapper;
262 | }
263 | },In order to allow passing nested models and collections as reference we
275 | filter nextProps.model and nextProps.collection.
componentWillReceiveProps: function (nextProps) {
280 | var model = nextProps.model;
281 | var collection = nextProps.collection;
282 |
283 | if (this.wrapper.model && model) {
284 | if (this.wrapper.model !== model) {
285 | this.wrapper.stopListening();
286 | this.wrapper = new Wrapper(this, void 0, nextProps);
287 | }
288 | } else if (model) {
289 | this.wrapper = new Wrapper(this, void 0, nextProps);
290 | }
291 |
292 | if (this.wrapper.collection && collection) {
293 | if (this.wrapper.collection !== collection) {
294 | this.wrapper.stopListening();
295 | this.wrapper = new Wrapper(this, void 0, nextProps);
296 | }
297 | } else if (collection) {
298 | this.wrapper = new Wrapper(this, void 0, nextProps);
299 | }
300 | },Shortcut to @$el.find if jQuery ins present, else if fallbacks to DOM
312 | native querySelector. Inspired by Backbone.View.
$: function () {
317 | var els;
318 |
319 | if (this.$el) {
320 | els = this.$el.find.apply(this.$el, arguments);
321 | } else {
322 | var el = ReactDOM.findDOMNode(this);
323 | els = el.querySelector.apply(el, arguments);
324 | }
325 |
326 | return els;
327 | },Grabs the collection from @wrapper.collection or @context.parentCollection
getCollection: function () {
343 | return this.wrapper.collection || this.context.parentCollection;
344 | },Grabs the model from @wrapper.model or @context.parentModel
getModel: function () {
360 | return this.wrapper.model || this.context.parentModel;
361 | },Sets a DOM element to render/mount this component on this.el and this.$el.
373 | 374 | setElement: function (el) {
377 | if (el && Backbone.$ && el instanceof Backbone.$) {
378 | if (el.length > 1) {
379 | throw new Error('You can only assign one element to a component');
380 | }
381 | this.el = el[0];
382 | this.$el = el;
383 | } else if (el) {
384 | this.el = el;
385 | if (Backbone.$) {
386 | this.$el = Backbone.$(el);
387 | }
388 | }
389 | return this;
390 | }
391 | };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)
418 | 419 | this.state = {};1:1 relation with the component
this.component = component;Use nextProps or component.props and grab model and collection
448 | from there
var props = nextProps || component.props || {};
453 | var model, collection;
454 |
455 | if (component.overrideModel && typeof component.overrideModel === 'function'){Define overrideModel() method on your React class to programatically supply a model object
467 | Will override this.props.model
model = component.overrideModel();
472 | } else {
473 | model = props.model;
474 | }
475 |
476 | if (component.overrideCollection && typeof component.overrideCollection === 'function'){Define overrideCollection() method on your React class to programatically supply a collection object
488 | Will override this.props.collection
collection = component.overrideCollection();
493 | } else {
494 | collection = props.collection;
495 | }
496 |
497 | this.setModels(model, initialState);
498 | this.setCollections(collection, initialState);
499 | }Mixing Backbone.Events into Wrapper.prototype
_.extend(Wrapper.prototype, Backbone.Events, {Sets this.state when a model/collection request results in error. It delegates
526 | 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
542 | 543 | if (!options.silent) {
546 | this.component.setState({
547 | isRequesting: false,
548 | hasError: true,
549 | error: res
550 | });
551 | }
552 | },
553 | onInvalid: function (model, res, options) {
554 | if (!options.silent) {
555 | this.component.setState({
556 | isInvalid: true
557 | });
558 | }
559 | },Sets this.state when a model/collection request starts. It delegates to
571 | this.setState. It listens to Backbone.Model#request and
572 | Backbone.Collection#request.
onRequest: function (modelOrCollection, xhr, options) {Set state only if there’s no silent option
if (!options.silent) {
592 | this.component.setState({
593 | isRequesting: true,
594 | hasError: false,
595 | isInvalid: false
596 | });
597 | }
598 | },Sets this.state when a model/collection syncs. It delegates to this.setState.
610 | 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) {
630 | this.component.setState({isRequesting: false});
631 | }
632 | },Check if models is a Backbone.Model or an hashmap of them, sets them
644 | to the component state and binds to update on any future changes
setModels: function (models, initialState, isDeferred) {
649 | var isValid = typeof models !== 'undefined';
650 |
651 | if (isValid) {
652 | if (!models.attributes) {
653 | if (typeof models === 'object') {
654 | var _values = _.values(models);
655 | isValid = _values.length > 0 && _values[0].attributes;
656 | } else {
657 | isValid = false;
658 | }
659 | }
660 | }
661 |
662 | if (isValid) {
663 | this.model = models;Set model(s) attributes on initialState for the first render
this.setStateBackbone(models, void 0, initialState, isDeferred);
679 | this.startModelListeners(models);
680 | }
681 | },Check if collections is a Backbone.Model or an hashmap of them,
693 | sets them to the component state and binds to update on any future changes
setCollections: function (collections, initialState, isDeferred) {
698 | if (typeof collections !== 'undefined' && (collections.models ||
699 | typeof collections === 'object' && _.values(collections)[0].models)) {The collection(s) bound to this component
711 | 712 | this.collection = collections;Set collection(s) models on initialState for the first render
this.setStateBackbone(collections, void 0, initialState, isDeferred);
730 | this.startCollectionListeners(collections);
731 | }
732 | },Used internally to set this.collection or this.model on this.state. Delegates to
744 | this.setState. It listens to Backbone.Collection events such as update,
745 | change, sort, reset and to Backbone.Model change.
setStateBackbone: function (modelOrCollection, key, target, isDeferred) {
750 | if (!(modelOrCollection.models || modelOrCollection.attributes)) {
751 | for (key in modelOrCollection)
752 | this.setStateBackbone(modelOrCollection[key], key, target);
753 | return;
754 | }
755 | this.setState.apply(this, arguments);
756 | },Get the attributes for the collection or model as array or hash
768 | 769 | getAttributes: function (modelOrCollection){
772 | var attrs = [];if a collection, get the attributes of each, otherwise return modelOrCollection
784 | 785 | if (modelOrCollection instanceof Backbone.Collection) {
788 | for (var i = 0; i < modelOrCollection.models.length; i++) {
789 | attrs.push(_.clone(modelOrCollection.models[i].attributes));
790 | }
791 | return attrs;
792 | } else {
793 | return _.clone(modelOrCollection.attributes);
794 | }
795 | },Sets a model, collection or object into state by delegating to this.component.setState.
setState: function (modelOrCollection, key, target, isDeferred) {
811 | var state = {};
812 | var newState = this.getAttributes(modelOrCollection);
813 |
814 | if (key) {
815 | state[key] = newState;
816 | } else if (modelOrCollection.models) {
817 | state.collection = newState;
818 | } else {
819 | state.model = newState;
820 | }
821 |
822 | if (target) {
823 | _.extend(target, state);
824 | } else if (isDeferred) {
825 | this.nextState = _.extend(this.nextState || {}, state);
826 | _.defer(_.bind(function () {
827 | if (this.nextState) {
828 | this.component.setState(this.nextState);
829 | this.nextState = null;
830 | }
831 | }, this));
832 | } else {
833 | this.component.setState(state);
834 | }
835 | },Binds the component to any collection changes.
847 | 848 | startCollectionListeners: function (collection, key) {
851 | if (!collection) collection = this.collection;
852 | if (collection) {
853 | if (collection.models)
854 | this
855 | .listenTo(collection, 'update change sort reset',
856 | _.partial(this.setStateBackbone, collection, key, void 0, true))
857 | .listenTo(collection, 'error', this.onError)
858 | .listenTo(collection, 'request', this.onRequest)
859 | .listenTo(collection, 'sync', this.onSync);
860 | else if (typeof collection === 'object')
861 | for (key in collection)
862 | if (collection.hasOwnProperty(key))
863 | this.startCollectionListeners(collection[key], key);
864 | }
865 | },Binds the component to any model changes.
877 | 878 | startModelListeners: function (model, key) {
881 | if (!model) model = this.model;
882 | if (model) {
883 | if (model.attributes)
884 | this
885 | .listenTo(model, 'change',
886 | _.partial(this.setStateBackbone, model, key, void 0, true))
887 | .listenTo(model, 'error', this.onError)
888 | .listenTo(model, 'request', this.onRequest)
889 | .listenTo(model, 'sync', this.onSync)
890 | .listenTo(model, 'invalid', this.onInvalid);
891 | else if (typeof model === 'object')
892 | for (key in model)
893 | this.startModelListeners(model[key], key);
894 | }
895 | }
896 | });Facade method to bypass the mixin usage. For use cases such as ES6
908 | classes or else. It binds any Backbone.Model and Backbone.Collection
909 | instance found inside backboneInstances.models and
910 | backboneInstances.collections (single instances or objects of them)
mixin.on = function (component, backboneInstances) {
915 | var wrapper;
916 |
917 | if (!component.wrapper) {
918 | wrapper = new Wrapper(component);
919 | } else {
920 | wrapper = component.wrapper;
921 | }
922 |
923 | if (backboneInstances.models) {
924 | wrapper.setModels(backboneInstances.models);
925 | }
926 | if (backboneInstances.collections) {
927 | wrapper.setCollections(backboneInstances.collections);
928 | }
929 | component.wrapper = wrapper;
930 | };Shortcut method to bind a model or multiple models
942 | 943 | mixin.onModel = function (component, models) {
946 | mixin.on(component, {models: models});
947 | };Shortcut method to bind a collection or multiple collections
959 | 960 | mixin.onCollection = function (component, collections) {
963 | mixin.on(component, {collections: collections});
964 | };Facade method to dispose of a component.wrapper
mixin.off = function (component, modelOrCollection) {
980 | if (arguments.length === 2) {
981 | if (component.wrapper) {
982 | component.wrapper.stopListening(modelOrCollection);TODO Remove modelOrCollection from component.state?
}
998 | } else {
999 | mixin.componentWillUnmount.call(component);
1000 | }
1001 | };Expose Backbone.React.Component.mixin.
return mixin;
1017 | }));