├── .babelrc
├── .eslintignore
├── .eslintrc
├── .gitignore
├── .npmignore
├── .travis.yml
├── CHANGELOG.md
├── CODE_OF_CONDUCT.md
├── LICENSE.md
├── README.md
├── examples
└── todomvc
│ ├── README.md
│ ├── containers
│ └── TodoApp
│ │ ├── components
│ │ └── TodoList
│ │ │ ├── components
│ │ │ ├── Footer.jsx
│ │ │ ├── Header.jsx
│ │ │ ├── MarkAll.jsx
│ │ │ └── Todo.jsx
│ │ │ └── index.jsx
│ │ └── index.jsx
│ ├── index.html
│ ├── index.js
│ ├── legacy
│ ├── app_view.jsx
│ ├── router.js
│ ├── todo.js
│ └── todos.js
│ ├── package.json
│ ├── server.js
│ └── webpack.config.js
├── package.json
├── src
├── action-fabric.js
├── collection-tools.js
├── ear-fabric.js
├── index.js
├── model-add-batcher.js
├── reducer-fabric.js
└── reducer-tools.js
├── test
├── .eslintrc
├── action-fabric-test.js
├── collection-tools-test.js
├── reducer-fabric-test.js
└── reducer-tools-test.js
└── webpack.config.js
/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "stage": 0,
3 | "loose": "all"
4 | }
5 |
--------------------------------------------------------------------------------
/.eslintignore:
--------------------------------------------------------------------------------
1 | lib
2 | **/node_modules
3 | **/webpack.config.js
4 | examples/**/server.js
5 | examples/**/node_modules
6 |
--------------------------------------------------------------------------------
/.eslintrc:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "eslint-config-airbnb",
3 | "env": {
4 | "browser": true,
5 | "mocha": true,
6 | "node": true
7 | },
8 | "rules": {
9 | "func-names": 0,
10 | "id-length": 0,
11 | "react/jsx-uses-react": 2,
12 | "react/jsx-uses-vars": 2,
13 | "react/react-in-jsx-scope": 2
14 | },
15 | "plugins": [
16 | "react"
17 | ]
18 | }
19 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | *.log
3 | .DS_Store
4 | dist
5 | lib
6 | coverage
7 |
--------------------------------------------------------------------------------
/.npmignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | *.log
3 | src
4 | test
5 | examples
6 | coverage
7 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: node_js
2 | node_js:
3 | - "4.1.2"
4 | script:
5 | - npm run ci
6 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # Change log
2 |
3 | This project adheres to [Semantic Versioning](http://semver.org/).
4 | Every release, along with the migration instructions, is documented on the [Github Releases page](https://github.com/redbooth/backbone-redux/releases).
5 |
--------------------------------------------------------------------------------
/CODE_OF_CONDUCT.md:
--------------------------------------------------------------------------------
1 | # Contributor Code of Conduct
2 |
3 | As contributors and maintainers of this project, we pledge to respect all people who contribute through reporting issues, posting feature requests, updating documentation, submitting pull requests or patches, and other activities.
4 |
5 | We are committed to making participation in this project a harassment-free experience for everyone, regardless of level of experience, gender, gender identity and expression, sexual orientation, disability, personal appearance, body size, race, age, or religion.
6 |
7 | Examples of unacceptable behavior by participants include the use of sexual language or imagery, derogatory comments or personal attacks, trolling, public or private harassment, insults, or other unprofessional conduct.
8 |
9 | Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct. Project maintainers who do not follow the Code of Conduct may be removed from the project team.
10 |
11 | Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by opening an issue or contacting one or more of the project maintainers.
12 |
13 | This Code of Conduct is adapted from the [Contributor Covenant](http://contributor-covenant.org), version 1.0.0, available at [http://contributor-covenant.org/version/1/0/0/](http://contributor-covenant.org/version/1/0/0/)
14 |
15 |
--------------------------------------------------------------------------------
/LICENSE.md:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2015 Ilya Zayats
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | backbone-redux
2 | ===============
3 |
4 | The easy way to keep your backbone collections and redux store in sync.
5 |
6 | [](https://www.npmjs.com/package/backbone-redux)
7 | [](https://www.npmjs.com/package/backbone-redux)
8 | [](https://travis-ci.org/redbooth/backbone-redux)
9 |
10 | ```
11 | npm install backbone-redux --save
12 | ```
13 |
14 | Creates reducers and listeners for your backbone collections and fires action
15 | creators on every collection change.
16 |
17 | **Documentation is a work-in-progress**. Feedback is welcome and encouraged.
18 |
19 | * [Why?](#why)
20 | * [How to use](#how-to-use)
21 | * [Auto way](#auto-way)
22 | * [Manual artesanal way](#manual-artesanal-way)
23 | * [Documentation](#documentation)
24 | * [Configuration options](#configuration)
25 | * [collectionMap](#collection-map)
26 | * [indexesMap](#indexes-map)
27 | * [serializer](#serializer)
28 | * [API reference](#api-reference)
29 | * [syncCollections](#sync-collections)
30 | * [buildReducers](#build-reducers)
31 | * [buildEars](#build-ears)
32 | * [actionFabric](#action-fabric)
33 | * [reducerFabric](#reducer-fabric)
34 | * [Examples](#examples)
35 |
36 |
37 | ### Why?
38 |
39 | * You can start migrating your apps from backbone to react+redux in no time.
40 | * No need to worry about migrated/legacy parts of your app being out of sync,
41 | because both are using the single source of truth.
42 | * No boilerplate.
43 | * You can hide all new concepts like `reducers`, `stores`, `action creators`,
44 | `actions` and `purity` from other developers in your team to avoid brain
45 | overloading.
46 | * You have REST-adapter to your server out-of-the-box. Most React projects end
47 | up implementing an ad hoc, bug-ridden implementation of Backbone.Collection
48 | not only once, but for each store.
49 | * You have separation between server-data and UI-data. The later is flat, so
50 | working with it is a pleasure in React.
51 |
52 | ### How to use?
53 | #### Auto way
54 |
55 |
56 | ```javascript
57 | import { createStore, compose } from 'redux';
58 | import { devTools } from 'redux-devtools';
59 | import { syncCollections } from 'backbone-redux';
60 |
61 | // Create your redux-store, include all middlewares you want.
62 | const finalCreateStore = compose(devTools())(createStore);
63 | const store = finalCreateStore(() => {}); // Store with an empty object as a reducer
64 |
65 | // Now just call auto-syncer from backbone-redux
66 | // Assuming you have Todos Backbone collection globally available
67 | syncCollections({todos: Todos}, store);
68 | ```
69 |
70 | What will happen?
71 |
72 | * `syncCollections` will create a reducer under the hood especially for your
73 | collection.
74 | * `action creator` will be constructed with 4 possible actions: `add`, `merge`,
75 | `remove`, and `reset`.
76 | * Special `ear` object will be set up to listen to all collection events and
77 | trigger right actions depending on the event type.
78 | * Reducer will be registered in the store under `todos` key.
79 | * All previous reducers in your store will be replaced.
80 |
81 | You are done. Now any change to `Todos` collection will be reflected in the
82 | redux store.
83 |
84 | Models will be serialized before saving into the redux-tree: a result of
85 | calling `toJSON` on the model + field called `__optimistic_id` which is equal
86 | to model's `cid`;
87 |
88 | Resulting tree will look like this:
89 |
90 | ```javascript
91 | {
92 | todos: {
93 | entities: [{id: 1, ...}, {id: 2, ...}],
94 | by_id: {
95 | 1: {id: 1, ...},
96 | 2: {id: 2, ...}
97 | }
98 | }
99 | }
100 | ```
101 |
102 | `entities` array is just an array of serialized models. `by_id` — default index
103 | which is created for you. It simplifies object retrieval, i.e.:
104 | `store.getState().todos.by_id[2]`
105 |
106 | So, what is happening when you change `Todos`?
107 |
108 | ```
109 | something (your legacy/new UI or anything really) changes Todos
110 | -> Todos collection emits an event
111 | -> ear catches it
112 | -> ActionCreator emits an action
113 | -> Reducer creates a new state based on this action
114 | -> New State is stored and listeners are notified
115 | -> React doing its magic
116 | ```
117 |
118 | #### Manual Artesanal Way
119 |
120 | Sometimes defaults that are provided by `syncCollections` are not enough.
121 |
122 | Reasons could vary:
123 | * your collection could not be globally available
124 | * you need some custom rules when adding/removing/resetting collection
125 | * your collection have any dependency that should be processed too
126 | * etc
127 |
128 | In all these cases you can't use `syncCollections`, but you can create your own
129 | ears to mimic `syncCollections` behavior.
130 |
131 | Any `ear` should look something like this:
132 |
133 | ```javascript
134 | import { bindActionCreators } from 'redux';
135 |
136 | export default function(collection, rawActions, dispatch) {
137 | // binding action creators to the dispatch function
138 | const actions = bindActionCreators(rawActions, dispatch);
139 |
140 | actions.add(collection.models); // initial sync
141 |
142 | // adding listeners
143 | collection.on('add', actions.add);
144 | collection.on('change', actions.merge);
145 | collection.on('remove', actions.remove);
146 | collection.on('reset', ({models}) => actions.reset(models));
147 | }
148 | ```
149 |
150 | As you can see, `ear` requires 3 attributes. `collection` and `dispatch`(this
151 | is just `store.dispatch`) you normally should already have, but how we can
152 | generate `rawActions`? You can use `actionFabric` that `backbone-redux`
153 | provides:
154 |
155 | ```javascript
156 | import {actionFabric} from 'backbone-redux';
157 |
158 | // create some constants that will be used as action types
159 | const constants = {
160 | ADD: 'ADD_MY_MODEL',
161 | REMOVE: 'REMOVE_MY_MODEL',
162 | MERGE: 'MERGE_MY_MODEL',
163 | RESET: 'RESET_MY_MODEL'
164 | };
165 |
166 | // you need some serializer to prepare models to be stored in the store.
167 | // This is the default one that is used in backbone-redux,
168 | // but you can create totally your own, just don't forget about __optimistic_id
169 | const defaultSerializer = model => ({...model.toJSON(), __optimistic_id: model.cid});
170 |
171 | export default actionFabric(constants, defaultSerializer);
172 | ```
173 |
174 | Don't forget that `actionFabric` is just an object with a couple of methods,
175 | you can extend it as you want.
176 |
177 | Time to generate a reducer:
178 |
179 | ```javascript
180 | import {reducerFabric} from 'backbone-redux';
181 |
182 | // the same constants, this is important
183 | const constants = {
184 | ADD: 'ADD_MY_MODEL',
185 | REMOVE: 'REMOVE_MY_MODEL',
186 | MERGE: 'MERGE_MY_MODEL',
187 | RESET: 'RESET_MY_MODEL'
188 | };
189 |
190 | // any indexes that you want to be created for you
191 | const index_map = {
192 | fields: {
193 | by_id: 'id'
194 | },
195 | relations: {
196 | by_channel_id: 'channel_id'
197 | }
198 | };
199 |
200 | export default reducerFabric(constants, index_map);
201 | ```
202 |
203 |
204 | And now we are ready to combine everything together:
205 |
206 | ```javascript
207 | import { syncCollections } from 'backbone-redux';
208 | import store from './redux-store';
209 | import customReducer from './reducer';
210 | import customEar from './ear';
211 | import customActions from './actions';
212 |
213 | export default function() {
214 | // start with syncing normal collections
215 | const collectionsMap = {
216 | collection_that_does_not_need_customization: someCollection
217 | };
218 |
219 | // we need to pass our prepared reducers into the store
220 | // if you don't use syncCollections at all, you just need
221 | // to create store normally with these reducers via
222 | // combineReducers from redux
223 | const extraReducers = {
224 | custom_collection: customReducer
225 | };
226 |
227 | syncCollections(collectionsMap, store, extraReducers);
228 |
229 | // now let's call the ear
230 | customEar(customCollection, customActions, store.dispatch);
231 | }
232 | ```
233 |
234 | Done, you have your custom ear placed and working.
235 |
236 | ## Documentation
237 |
238 | ### Configuration options
239 |
240 | #### collectionMap
241 |
242 | A collection map is a plain object passed to `backbone-redux` functions to set
243 | up reducers for you.
244 |
245 | If you don't need a custom serializer you can use:
246 |
247 | ```javascript
248 | // keys are reducer names, and values are backbone collections
249 | const collectionMap = {
250 | reducer_name: collection
251 | }
252 | ```
253 |
254 | If you want, you can add change configuration by specifying `serializer` and `indexes_map` keys.
255 |
256 | ```javascript
257 | // keys are reducer names, and values are objects defining collection and serializer
258 | const collectionMap = {
259 | reducer_name: {
260 | collection: collection,
261 | serializer: serializer,
262 | indexes_map: indexes_map
263 | }
264 | }
265 | ```
266 |
267 | #### indexesMap
268 |
269 | With `indexesMap` you can specify the way your entities are indexed in the tree.
270 |
271 | `fields` lets you access a *single* entity by a field (for example `id`, `email`, etc).
272 |
273 | `relation` groups entities by a field value (for example `parent_id`).
274 |
275 | Example:
276 |
277 | I have a `people` collection of models with 4 fields: `name`,
278 | `id`, `token`, and `org_id`. And I want to have indexes for all fields except
279 | `name`.
280 |
281 | ```javascript
282 | const jane = new Backbone.Model({id: 1, name: 'Jane', org_id: 1, token: '001'});
283 | const mark = new Backbone.Model({id: 2, name: 'Mark', org_id: 2, token: '002'});
284 | const sophy = new Backbone.Model({id: 3, name: 'Sophy', org_id: 1, token: '003'});
285 | const people = new Backbone.Collection([jane, mark, sophy]);
286 |
287 | const indexesMap = {
288 | fields: {
289 | by_id: 'id',
290 | by_token: 'token'
291 | },
292 | relations: {
293 | by_org_id: 'org_id'
294 | }
295 | };
296 |
297 | syncCollections({
298 | people: {
299 | collection: people,
300 | indexes_map: indexesMap
301 | }
302 | }, store);
303 |
304 | /**
305 | store.getState().people =>
306 |
307 | {
308 | entities: [
309 | {id: 1, name: 'Jane', org_id: 1, token: '001', __optimistic_id: 'c01'},
310 | {id: 2, name: 'Mark', org_id: 2, token: '002', __optimistic_id: 'c02'},
311 | {id: 3, name: 'Sophy', org_id: 1, token: '003', __optimistic_id: 'c03'}
312 | ],
313 | by_id: {
314 | 1: {id: 1, name: 'Jane', org_id: 1, token: '001', __optimistic_id: 'c01'},
315 | 2: {id: 2, name: 'Mark', org_id: 2, token: '002', __optimistic_id: 'c02'},
316 | 3: {id: 3, name: 'Sophy', org_id: 1, token: '003', __optimistic_id: 'c03'}
317 | },
318 | by_token: {
319 | '001': {id: 1, name: 'Jane', org_id: 1, token: '001', __optimistic_id: 'c01'},
320 | '002': {id: 2, name: 'Mark', org_id: 2, token: '002', __optimistic_id: 'c02'},
321 | '003': {id: 3, name: 'Sophy', org_id: 1, token: '003', __optimistic_id: 'c03'}
322 | },
323 | by_org_id: {
324 | 1: [
325 | {id: 1, name: 'Jane', org_id: 1, token: '001', __optimistic_id: 'c01'},
326 | {id: 3, name: 'Sophy', org_id: 1, token: '003', __optimistic_id: 'c03'}
327 | ],
328 | 2: [
329 | {id: 2, name: 'Mark', org_id: 2, token: '002', __optimistic_id: 'c02'}
330 | ]
331 | }
332 | }
333 | */
334 | ```
335 |
336 | And to remove indexes at all, just pass an empty object as `indexes_map` for `syncCollections`.
337 |
338 | #### serializer
339 |
340 | By default models are stored in the tree by calling `model.toJSON` and adding
341 | an extra `__optimistic_id` which is the `model.cid`. You can serialize extra stuff by defining your own serializer function
342 |
343 |
344 | ##### Arguments
345 |
346 | `model` *(Backbone.Model)*: Model to be serialized.
347 |
348 | ##### Returns
349 |
350 | `serialized_model` *(Object)*: Plain object serialization of the model.
351 |
352 |
353 | ### API Reference
354 |
355 | #### syncCollections(collectionMap, store, [extraReducers])
356 |
357 | Builds reducers and setups listeners in collections that dispatch actions to
358 | the store. **syncCollections** will replace existing reducers in your store, but
359 | you can still provide more reducers using the optional **extraReducers**
360 | argument.
361 |
362 | ##### Arguments
363 | `collectionMap` *(CollectionMap)*: See [collectionMap](#collection-map).
364 |
365 | `store` *(Store)*: A Redux store.
366 |
367 | [`extraReducers`] *(Object)*: Optionally specify additional reducers in an
368 | object whose values are reducer functions. These reducers will be merged and combined
369 | together with the ones defined in the collectionMap.
370 |
371 | ---
372 |
373 | #### buildReducers(collectionsMap)
374 |
375 | Creates reducers based on a
376 | [collectionMap](#collection-map),
377 | basically calling [reducerFabric](#reducer-fabric) on each defined reducer.
378 |
379 | ##### Arguments
380 | `collectionMap` *(CollectionMap)*: See [collectionMap](#collection-map).
381 |
382 | ##### Returns
383 | `reducers` *(Object)*: An object whose keys are the collection names defined in
384 | the input collectionMap, and values are generated reducer functions.
385 |
386 | ---
387 |
388 | #### buildEars(collectionsMap, store)
389 |
390 | Creates the basic action creators using [actionFabric](#action-fabric), and binds them to the
391 | appropriate Backbone.Collection events.
392 |
393 | When a collection event happens, the equivalent action will be dispatched.
394 |
395 | ##### Arguments
396 |
397 | `collectionMap` *(CollectionMap)*: See [collectionMap](#collection-map).
398 |
399 | `store` *(Store)*: A Redux store.
400 |
401 | ##### Arguments
402 | `collectionMap` *(CollectionMap)*: See [collectionMap](#collection-map).
403 |
404 | ---
405 |
406 | #### actionFabric(actionTypesMap, serializer)
407 |
408 | Returns an object of action creators functions. This functions can be hooked to
409 | Backbone collections events `add`, `remove`, `change`, and `reset`.
410 |
411 | The actions returned by this functions contain an `entities` field with the
412 | serialized models.
413 |
414 | ##### Arguments
415 |
416 | `actionTypesMap` *(Object)*: Object to map from Backbone collection event to
417 | action constant type. Keys must be `ADD`, `REMOVE`, `MERGE` ( for the change
418 | events ) and `RESET`.
419 |
420 | `serializer` *(Function)*: Model serializer function.
421 |
422 | ##### Returns
423 |
424 | `actionCreators` *(Object)*: Returns an object whose keys are `add`, `remove`,
425 | `merge` and `reset`, and values are action creator functions.
426 |
427 | ---
428 |
429 | #### reducerFabric(actionTypesMap, [indexesMap])
430 |
431 | `actionTypesMap` *(Object)*: Object to map from Backbone collection event to
432 | action constant type. Keys must be `ADD`, `REMOVE`, `MERGE` ( for the change
433 | events ) and `RESET`.
434 |
435 | [`indexesMap`] *(Object)*: Optionally define indices passing an [indexesMap](#indexes-map).
436 |
437 | ---
438 |
439 |
440 | ### Examples
441 |
442 | * [TodoMVC](https://github.com/redbooth/backbone-redux/tree/master/examples/todomvc)
443 |
444 | ### Licence
445 | MIT
446 |
--------------------------------------------------------------------------------
/examples/todomvc/README.md:
--------------------------------------------------------------------------------
1 | ### TodoMVC Example
2 |
3 | To see it in action:
4 | 1. clone the repo
5 | 2. `npm install`
6 | 3. `npm start`
7 | 4. open `localhost:3000`
8 |
9 | You will see classic todomvc app that uses Backbone collection for data handling, Backbone Router for URIs and React + Redux for UI.
10 |
11 | All data synchronisation between backbone collection and redux tree is happenning because of `backbone-redux`: see `index.js` as a starting point.
12 |
--------------------------------------------------------------------------------
/examples/todomvc/containers/TodoApp/components/TodoList/components/Footer.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | export default class Footer extends React.Component {
4 | render() {
5 | const active = this.props.todos.filter(todo => !todo.completed).length;
6 | const completed = this.props.todos.filter(todo => todo.completed).length;
7 |
8 | return (
9 |
30 | );
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/examples/todomvc/containers/TodoApp/components/TodoList/components/Header.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import ReactDOM from 'react-dom';
3 |
4 | const ENTER_KEY = 13;
5 |
6 | export default class Header extends React.Component {
7 | componentDidMount(){
8 | ReactDOM.findDOMNode(this.refs.input).focus();
9 | }
10 |
11 | onPossibleTask({keyCode}) {
12 | if (keyCode != ENTER_KEY) {
13 | return;
14 | }
15 |
16 | const input = ReactDOM.findDOMNode(this.refs.input);
17 | const input_val = input.value.trim();
18 |
19 | if (input_val) {
20 | this.props.onEnterPressed(input_val);
21 | input.value = '';
22 | }
23 | }
24 |
25 | render() {
26 | return (
27 |
28 |