├── .npmignore ├── CNAME ├── src ├── coreActions.js ├── constants.js ├── connect │ ├── utils │ │ ├── verifyPlainObject.js │ │ ├── PropTypes.js │ │ ├── shallowEqual.js │ │ └── Subscription.js │ ├── mapSendToProps.js │ ├── mapStateToProps.js │ ├── mergeProps.js │ ├── wrapMapToProps.js │ ├── selectorFactory.js │ └── connect.js ├── compose.js ├── fxs.js ├── tanok.js ├── helpers.js ├── components │ ├── root.js │ └── subcomponent.js ├── core.js ├── component.js ├── tanokInReact.js ├── tanokDispatcher.js ├── createStore.js └── streamWrapper.js ├── book.json ├── .travis.yml ├── examples ├── counter │ ├── src │ │ ├── model.js │ │ ├── index.js │ │ ├── dispatcher.js │ │ └── view.js │ ├── .gitignore │ ├── README.md │ ├── config │ │ ├── polyfills.js │ │ ├── paths.js │ │ ├── env.js │ │ ├── webpackDevServer.config.js │ │ └── webpack.config.dev.js │ ├── package.json │ ├── public │ │ └── index.html │ └── scripts │ │ └── start.js ├── async_counter │ ├── src │ │ ├── model.js │ │ ├── index.js │ │ ├── view.js │ │ └── dispatcher.js │ ├── .gitignore │ ├── README.md │ ├── config │ │ ├── polyfills.js │ │ ├── paths.js │ │ ├── env.js │ │ └── webpackDevServer.config.js │ ├── package.json │ ├── public │ │ └── index.html │ └── scripts │ │ └── start.js ├── subcomponent │ ├── src │ │ ├── counter │ │ │ ├── model.js │ │ │ ├── view.js │ │ │ └── dispatcher.js │ │ ├── model.js │ │ ├── index.js │ │ ├── view.js │ │ └── dispatcher.js │ ├── .gitignore │ ├── README.md │ ├── config │ │ ├── polyfills.js │ │ ├── paths.js │ │ ├── env.js │ │ └── webpackDevServer.config.js │ ├── package.json │ ├── public │ │ └── index.html │ └── scripts │ │ └── start.js ├── search_example │ ├── src │ │ ├── model.js │ │ ├── actions.js │ │ ├── index.js │ │ ├── view.js │ │ └── dispatcher.js │ ├── .gitignore │ ├── README.md │ ├── config │ │ ├── polyfills.js │ │ ├── paths.js │ │ ├── env.js │ │ └── webpackDevServer.config.js │ ├── package.json │ ├── public │ │ └── index.html │ └── scripts │ │ └── start.js ├── subcomponent_with_metadata │ ├── src │ │ ├── counter │ │ │ ├── model.js │ │ │ ├── view.js │ │ │ └── dispatcher.js │ │ ├── model.js │ │ ├── index.js │ │ ├── view.js │ │ └── dispatcher.js │ ├── .gitignore │ ├── README.md │ ├── config │ │ ├── polyfills.js │ │ ├── paths.js │ │ ├── env.js │ │ └── webpackDevServer.config.js │ ├── package.json │ ├── public │ │ └── index.html │ └── scripts │ │ └── start.js └── middlewares │ └── middlewares.js ├── .babelrc ├── CONTRIBUTING.md ├── .gitignore ├── test ├── helpers.js ├── dom.js ├── helpers.test.js ├── dispatcher.test.js ├── compose.test.js ├── components │ └── subcomponent.test.js ├── decorators.test.js ├── fxs.test.js ├── hierarchy.test.js ├── tanokInReact.test.js └── core.test.js ├── SUMMARY.md ├── LICENSE ├── docs ├── AsyncStuff.md ├── Tanok.md └── Basics.md ├── README.md ├── rollup.js ├── package.json ├── CHANGELOG.md └── CODE_OF_CONDUCT.md /.npmignore: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /CNAME: -------------------------------------------------------------------------------- 1 | tanok.js.org -------------------------------------------------------------------------------- /src/coreActions.js: -------------------------------------------------------------------------------- 1 | export const INIT = 'init'; 2 | -------------------------------------------------------------------------------- /book.json: -------------------------------------------------------------------------------- 1 | { 2 | "gitbook": "^3.2.3", 3 | "title": "Tanok" 4 | } -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "6.2.2" 4 | after_success: npm run coverage 5 | -------------------------------------------------------------------------------- /examples/counter/src/model.js: -------------------------------------------------------------------------------- 1 | export function initModel() { 2 | return { 3 | count: 0, 4 | } 5 | }; 6 | -------------------------------------------------------------------------------- /examples/async_counter/src/model.js: -------------------------------------------------------------------------------- 1 | export function initModel() { 2 | return { 3 | count: 0, 4 | synced: false, 5 | } 6 | }; 7 | -------------------------------------------------------------------------------- /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ "es2015", "stage-0", "react" ], 3 | "plugins": [ "transform-decorators-legacy", "transform-object-rest-spread" ] 4 | } -------------------------------------------------------------------------------- /src/constants.js: -------------------------------------------------------------------------------- 1 | export const streamKey = 'stream'; 2 | export const storeKey = 'store'; 3 | export const subscriptionKey = `${storeKey}Subscription`; 4 | -------------------------------------------------------------------------------- /examples/subcomponent/src/counter/model.js: -------------------------------------------------------------------------------- 1 | export function counterInit(id) { 2 | return { 3 | id, 4 | count: 0, 5 | synced: false, 6 | } 7 | }; 8 | -------------------------------------------------------------------------------- /examples/search_example/src/model.js: -------------------------------------------------------------------------------- 1 | export class SearchModel { 2 | constructor() { 3 | this.searchTerm = ''; 4 | this.repos = []; 5 | } 6 | } -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | Contributing is easy - create an issue or pull request. We will discuss it and maybe we'll do something about it. 2 | 3 | Remember to be nice and friendly. 4 | -------------------------------------------------------------------------------- /examples/subcomponent_with_metadata/src/counter/model.js: -------------------------------------------------------------------------------- 1 | export function counterInit(id) { 2 | return { 3 | id, 4 | count: 0, 5 | synced: false, 6 | } 7 | }; 8 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | lib/ 3 | .DS_Store 4 | .idea 5 | .vscode 6 | examples/main.bundle.js 7 | coverage/* 8 | .nyc_output/ 9 | dom/ 10 | .coveralls.yml 11 | _book 12 | -------------------------------------------------------------------------------- /test/helpers.js: -------------------------------------------------------------------------------- 1 | import Enzyme, { mount, render, shallow } from 'enzyme'; 2 | import Adapter from 'enzyme-adapter-react-15'; 3 | 4 | Enzyme.configure({ adapter: new Adapter() }); 5 | -------------------------------------------------------------------------------- /examples/subcomponent/src/model.js: -------------------------------------------------------------------------------- 1 | import { counterInit } from './counter/model'; 2 | 3 | export function initModel() { 4 | return { 5 | top: counterInit('top'), 6 | bottom: counterInit('bottom'), 7 | } 8 | } -------------------------------------------------------------------------------- /test/dom.js: -------------------------------------------------------------------------------- 1 | var jsdom = require('jsdom').JSDOM; 2 | var exposedProperties = ['window', 'navigator', 'document']; 3 | 4 | const dom = new jsdom(''); 5 | 6 | global.window = dom.window; 7 | global.document = dom.window.document; -------------------------------------------------------------------------------- /examples/search_example/src/actions.js: -------------------------------------------------------------------------------- 1 | export const INIT = 'init'; 2 | export const SEARCH = 'search'; 3 | export const SEARCH_OK = 'searchOk'; 4 | export const CANCEL_SEARCH = 'cancelSearch'; 5 | export const INPUT_TERM = 'inputTerm'; 6 | -------------------------------------------------------------------------------- /examples/subcomponent_with_metadata/src/model.js: -------------------------------------------------------------------------------- 1 | import { counterInit } from './counter/model'; 2 | 3 | export function initModel() { 4 | return { 5 | counters: Array.from({length: 10}).map((_, ind) => counterInit(ind)), 6 | }; 7 | } -------------------------------------------------------------------------------- /examples/counter/src/index.js: -------------------------------------------------------------------------------- 1 | import { tanok } from 'tanok'; 2 | 3 | import { CounterDispatcher } from "./dispatcher"; 4 | import { initModel } from "./model"; 5 | import { Counter } from "./view"; 6 | 7 | 8 | tanok(initModel(), new CounterDispatcher(), Counter, { 9 | container: document.getElementById('root'), 10 | }); 11 | -------------------------------------------------------------------------------- /examples/async_counter/src/index.js: -------------------------------------------------------------------------------- 1 | import { tanok } from 'tanok'; 2 | 3 | import { CounterDispatcher } from "./dispatcher"; 4 | import { initModel } from "./model"; 5 | import { Counter } from "./view"; 6 | 7 | 8 | tanok(initModel(), new CounterDispatcher(), Counter, { 9 | container: document.getElementById('root'), 10 | }); 11 | -------------------------------------------------------------------------------- /examples/subcomponent/src/index.js: -------------------------------------------------------------------------------- 1 | import { tanok } from 'tanok'; 2 | 3 | import { DashboardDispatcher } from "./dispatcher"; 4 | import { initModel } from "./model"; 5 | import { TwoCounters } from "./view"; 6 | 7 | 8 | tanok(initModel(), new DashboardDispatcher(), TwoCounters, { 9 | container: document.getElementById('root'), 10 | }); 11 | -------------------------------------------------------------------------------- /examples/search_example/src/index.js: -------------------------------------------------------------------------------- 1 | import { tanok } from 'tanok'; 2 | 3 | import { SearchDispatcher } from './dispatcher'; 4 | import { SearchModel } from './model'; 5 | import { SearchComponent } from './view'; 6 | 7 | tanok(new SearchModel(), new SearchDispatcher(), SearchComponent, { 8 | container: document.getElementById('root'), 9 | }); 10 | -------------------------------------------------------------------------------- /examples/subcomponent_with_metadata/src/index.js: -------------------------------------------------------------------------------- 1 | import { tanok } from 'tanok'; 2 | 3 | import { DashboardDispatcher } from "./dispatcher"; 4 | import { initModel } from "./model"; 5 | import { TwoCounters } from "./view"; 6 | 7 | 8 | tanok(initModel(), new DashboardDispatcher(), TwoCounters, { 9 | container: document.getElementById('root'), 10 | }); 11 | -------------------------------------------------------------------------------- /src/connect/utils/verifyPlainObject.js: -------------------------------------------------------------------------------- 1 | import isPlainObject from 'lodash/isPlainObject' 2 | import warning from './warning' 3 | 4 | export default function verifyPlainObject(value, displayName, methodName) { 5 | if (!isPlainObject(value)) { 6 | warning( 7 | `${methodName}() in ${displayName} must return a plain object. Instead received ${value}.` 8 | ) 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /examples/counter/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | 6 | # testing 7 | /coverage 8 | 9 | # production 10 | /build 11 | 12 | # misc 13 | .DS_Store 14 | .env.local 15 | .env.development.local 16 | .env.test.local 17 | .env.production.local 18 | 19 | npm-debug.log* 20 | yarn-debug.log* 21 | yarn-error.log* 22 | -------------------------------------------------------------------------------- /examples/subcomponent/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | 6 | # testing 7 | /coverage 8 | 9 | # production 10 | /build 11 | 12 | # misc 13 | .DS_Store 14 | .env.local 15 | .env.development.local 16 | .env.test.local 17 | .env.production.local 18 | 19 | npm-debug.log* 20 | yarn-debug.log* 21 | yarn-error.log* 22 | -------------------------------------------------------------------------------- /examples/async_counter/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | 6 | # testing 7 | /coverage 8 | 9 | # production 10 | /build 11 | 12 | # misc 13 | .DS_Store 14 | .env.local 15 | .env.development.local 16 | .env.test.local 17 | .env.production.local 18 | 19 | npm-debug.log* 20 | yarn-debug.log* 21 | yarn-error.log* 22 | -------------------------------------------------------------------------------- /examples/search_example/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | 6 | # testing 7 | /coverage 8 | 9 | # production 10 | /build 11 | 12 | # misc 13 | .DS_Store 14 | .env.local 15 | .env.development.local 16 | .env.test.local 17 | .env.production.local 18 | 19 | npm-debug.log* 20 | yarn-debug.log* 21 | yarn-error.log* 22 | -------------------------------------------------------------------------------- /examples/subcomponent_with_metadata/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | 6 | # testing 7 | /coverage 8 | 9 | # production 10 | /build 11 | 12 | # misc 13 | .DS_Store 14 | .env.local 15 | .env.development.local 16 | .env.test.local 17 | .env.production.local 18 | 19 | npm-debug.log* 20 | yarn-debug.log* 21 | yarn-error.log* 22 | -------------------------------------------------------------------------------- /examples/counter/README.md: -------------------------------------------------------------------------------- 1 | # Basic Counter Example 2 | 3 | This project was bootstrapped with [Create React App](https://github.com/facebookincubator/create-react-app). 4 | 5 | To run example: 6 | 7 | * `yarn start` or `npm start` 8 | 9 | Runs the app in the development mode.
10 | Open [http://localhost:3000](http://localhost:3000) to view it in the browser. 11 | 12 | The page will reload if you make edits.
13 | You will also see any lint errors in the console. 14 | -------------------------------------------------------------------------------- /src/connect/utils/PropTypes.js: -------------------------------------------------------------------------------- 1 | import PropTypes from 'prop-types' 2 | 3 | export const subscriptionShape = PropTypes.shape({ 4 | trySubscribe: PropTypes.func.isRequired, 5 | tryUnsubscribe: PropTypes.func.isRequired, 6 | notifyNestedSubs: PropTypes.func.isRequired, 7 | isSubscribed: PropTypes.func.isRequired, 8 | }) 9 | 10 | export const storeShape = PropTypes.shape({ 11 | subscribe: PropTypes.func.isRequired, 12 | getState: PropTypes.func.isRequired 13 | }) 14 | -------------------------------------------------------------------------------- /examples/counter/src/dispatcher.js: -------------------------------------------------------------------------------- 1 | import { on, TanokDispatcher } from 'tanok'; 2 | 3 | export class CounterDispatcher extends TanokDispatcher { 4 | @on('init') 5 | init(payload, state) { 6 | state.count = 10; 7 | return [state]; 8 | } 9 | 10 | @on('inc') 11 | inc(payload, state) { 12 | state.count += 1; 13 | return [state]; 14 | } 15 | 16 | @on('dec') 17 | dec(payload, state) { 18 | state.count -= 1; 19 | return [state]; 20 | } 21 | 22 | } -------------------------------------------------------------------------------- /examples/subcomponent/README.md: -------------------------------------------------------------------------------- 1 | # Subcomponent Example 2 | 3 | This project was bootstrapped with [Create React App](https://github.com/facebookincubator/create-react-app). 4 | 5 | To run example: 6 | 7 | * `yarn start` or `npm start` 8 | 9 | Runs the app in the development mode.
10 | Open [http://localhost:3000](http://localhost:3000) to view it in the browser. 11 | 12 | The page will reload if you make edits.
13 | You will also see any lint errors in the console. 14 | -------------------------------------------------------------------------------- /examples/async_counter/README.md: -------------------------------------------------------------------------------- 1 | # Counter with Effects Example 2 | 3 | This project was bootstrapped with [Create React App](https://github.com/facebookincubator/create-react-app). 4 | 5 | To run example: 6 | 7 | * `yarn start` or `npm start` 8 | 9 | Runs the app in the development mode.
10 | Open [http://localhost:3000](http://localhost:3000) to view it in the browser. 11 | 12 | The page will reload if you make edits.
13 | You will also see any lint errors in the console. 14 | -------------------------------------------------------------------------------- /examples/subcomponent/src/view.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { tanokComponent } from 'tanok'; 3 | 4 | import { Counter } from './counter/view'; 5 | 6 | @tanokComponent 7 | export class TwoCounters extends React.Component { 8 | render() { 9 | return ( 10 |
11 | 12 | 13 |
14 | ); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /examples/subcomponent_with_metadata/README.md: -------------------------------------------------------------------------------- 1 | # Subcomponent with metadata Example 2 | 3 | This project was bootstrapped with [Create React App](https://github.com/facebookincubator/create-react-app). 4 | 5 | To run example: 6 | 7 | * `yarn start` or `npm start` 8 | 9 | Runs the app in the development mode.
10 | Open [http://localhost:3000](http://localhost:3000) to view it in the browser. 11 | 12 | The page will reload if you make edits.
13 | You will also see any lint errors in the console. 14 | -------------------------------------------------------------------------------- /src/compose.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @param {...Function} funcs The functions to compose. 3 | * @returns {Function} A function obtained by composing the argument functions 4 | * from right to left. For example, compose(f, g, h) is identical to doing 5 | * (...args) => f(g(h(...args))). 6 | */ 7 | 8 | export default function compose(...funcs) { 9 | if (funcs.length === 0) { 10 | return arg => arg 11 | } 12 | 13 | if (funcs.length === 1) { 14 | return funcs[0] 15 | } 16 | 17 | return funcs.reduce((a, b) => (...args) => a(b(...args))) 18 | } -------------------------------------------------------------------------------- /examples/search_example/README.md: -------------------------------------------------------------------------------- 1 | # Search Example 2 | 3 | In that example we show how you can use `Rx` and `tanok` to cancel fetch result. 4 | 5 | This project was bootstrapped with [Create React App](https://github.com/facebookincubator/create-react-app). 6 | 7 | To run example: 8 | 9 | * `yarn start` or `npm start` 10 | 11 | Runs the app in the development mode.
12 | Open [http://localhost:3000](http://localhost:3000) to view it in the browser. 13 | 14 | The page will reload if you make edits.
15 | You will also see any lint errors in the console. 16 | -------------------------------------------------------------------------------- /src/connect/mapSendToProps.js: -------------------------------------------------------------------------------- 1 | import { wrapMapToPropsConstant, wrapMapToPropsFunc } from './wrapMapToProps' 2 | 3 | export function whenMapSendToPropsIsFunction(mapSendToProps) { 4 | return (typeof mapSendToProps === 'function') 5 | ? wrapMapToPropsFunc(mapSendToProps) 6 | : undefined 7 | } 8 | 9 | export function whenMapSendToPropsIsMissing(mapSendToProps) { 10 | return (!mapSendToProps) 11 | ? wrapMapToPropsConstant(send => ({ send })) 12 | : undefined 13 | } 14 | 15 | 16 | export default [ 17 | whenMapSendToPropsIsFunction, 18 | whenMapSendToPropsIsMissing, 19 | ] 20 | -------------------------------------------------------------------------------- /src/connect/mapStateToProps.js: -------------------------------------------------------------------------------- 1 | import { wrapMapToPropsConstant, wrapMapToPropsFunc } from './wrapMapToProps' 2 | 3 | export function whenMapStateToPropsIsFunction(mapStateToProps) { 4 | return (typeof mapStateToProps === 'function') 5 | ? wrapMapToPropsFunc(mapStateToProps, 'mapStateToProps') 6 | : undefined 7 | } 8 | 9 | export function whenMapStateToPropsIsMissing(mapStateToProps) { 10 | return (!mapStateToProps) 11 | ? wrapMapToPropsConstant(() => ({})) 12 | : undefined 13 | } 14 | 15 | export default [ 16 | whenMapStateToPropsIsFunction, 17 | whenMapStateToPropsIsMissing 18 | ] 19 | -------------------------------------------------------------------------------- /examples/subcomponent_with_metadata/src/view.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { tanokComponent } from 'tanok'; 3 | 4 | import { Counter } from './counter/view'; 5 | 6 | @tanokComponent 7 | export class TwoCounters extends React.Component { 8 | render() { 9 | return ( 10 |
11 | {this.props.counters.map((counter) => 12 | 17 | )} 18 |
19 | ); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/fxs.js: -------------------------------------------------------------------------------- 1 | import Rx from '@evo/rx'; 2 | 3 | export function rethrowFx(action, payload) { 4 | return function (stream) { 5 | stream.send(action, payload) 6 | } 7 | } 8 | 9 | export function subcomponentFx(subName, dispatchSub) { 10 | return function (stream) { 11 | stream.subStream(subName, dispatchSub); 12 | } 13 | } 14 | 15 | export function childFx(effect, streamName, metadata = null) { 16 | return (streamWrapper) => { 17 | const substream = streamWrapper.subWithMeta(streamName, metadata); 18 | return effect 19 | ? effect(substream) 20 | : Rx.helpers.noop; 21 | }; 22 | } 23 | -------------------------------------------------------------------------------- /examples/subcomponent_with_metadata/src/dispatcher.js: -------------------------------------------------------------------------------- 1 | import { on, TanokDispatcher, subcomponentFx, childFx } from 'tanok'; 2 | 3 | import { CounterDispatcher } from './counter/dispatcher'; 4 | 5 | export class DashboardDispatcher extends TanokDispatcher { 6 | @on('init') 7 | init(payload, state) { 8 | return [state, 9 | subcomponentFx('counter', new CounterDispatcher()), 10 | ] 11 | } 12 | 13 | @on('counter') 14 | top(payload, state, { metadata }) { 15 | const [newState, ...effects] = payload(state.counters[metadata]); 16 | state.top = newState; 17 | return [state, ...effects.map((e) => childFx(e, 'counter', metadata))] 18 | } 19 | } -------------------------------------------------------------------------------- /src/tanok.js: -------------------------------------------------------------------------------- 1 | export * from './component'; 2 | export * from './core'; 3 | export * from './constants'; 4 | export * from './fxs'; 5 | export * from './helpers'; 6 | export * from './tanokDispatcher'; 7 | export * from './tanokInReact'; 8 | export * from './connect/connect'; 9 | export * from './createStore'; 10 | export * from './components/root'; 11 | export * from './components/subcomponent'; 12 | export { StreamWrapper } from './streamWrapper'; 13 | 14 | import { childFx } from './fxs'; 15 | 16 | export function effectWrapper(effect, streamName, metadata = null) { 17 | console.error('`effectWrapper` is deprecated. Use `childFx` instead'); 18 | return childFx(effect, streamName, metadata); 19 | } 20 | -------------------------------------------------------------------------------- /examples/counter/config/polyfills.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | if (typeof Promise === 'undefined') { 4 | // Rejection tracking prevents a common issue where React gets into an 5 | // inconsistent state due to an error, but it gets swallowed by a Promise, 6 | // and the user has no idea what causes React's erratic future behavior. 7 | require('promise/lib/rejection-tracking').enable(); 8 | window.Promise = require('promise/lib/es6-extensions.js'); 9 | } 10 | 11 | // fetch() polyfill for making API calls. 12 | require('whatwg-fetch'); 13 | 14 | // Object.assign() is commonly used with React. 15 | // It will use the native implementation if it's present and isn't buggy. 16 | Object.assign = require('object-assign'); 17 | -------------------------------------------------------------------------------- /SUMMARY.md: -------------------------------------------------------------------------------- 1 | # Summary 2 | 3 | * [Introduction](README.md) 4 | 5 | ## Getting Started 6 | 7 | * [Basic](/docs/Basics.md) 8 | * [Async Stuff](/docs/AsyncStuff.md) 9 | 10 | ## API Reference 11 | 12 | * [Tanok](/docs/Tanok.md) 13 | 14 | ## Examples 15 | 16 | * [Basic Counter](https://github.com/brabadu/tanok/tree/master/examples/counter) 17 | * [Counter With Effects](https://github.com/brabadu/tanok/tree/master/examples/async_counter) 18 | * [Subcomponent](https://github.com/brabadu/tanok/tree/master/examples/subcomponent) 19 | * [Subcomponent With Metadata](https://github.com/brabadu/tanok/tree/master/examples/subcomponent_with_metadata) 20 | * [Search Example](https://github.com/brabadu/tanok/tree/master/examples/search_example) -------------------------------------------------------------------------------- /examples/async_counter/config/polyfills.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | if (typeof Promise === 'undefined') { 4 | // Rejection tracking prevents a common issue where React gets into an 5 | // inconsistent state due to an error, but it gets swallowed by a Promise, 6 | // and the user has no idea what causes React's erratic future behavior. 7 | require('promise/lib/rejection-tracking').enable(); 8 | window.Promise = require('promise/lib/es6-extensions.js'); 9 | } 10 | 11 | // fetch() polyfill for making API calls. 12 | require('whatwg-fetch'); 13 | 14 | // Object.assign() is commonly used with React. 15 | // It will use the native implementation if it's present and isn't buggy. 16 | Object.assign = require('object-assign'); 17 | -------------------------------------------------------------------------------- /examples/search_example/config/polyfills.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | if (typeof Promise === 'undefined') { 4 | // Rejection tracking prevents a common issue where React gets into an 5 | // inconsistent state due to an error, but it gets swallowed by a Promise, 6 | // and the user has no idea what causes React's erratic future behavior. 7 | require('promise/lib/rejection-tracking').enable(); 8 | window.Promise = require('promise/lib/es6-extensions.js'); 9 | } 10 | 11 | // fetch() polyfill for making API calls. 12 | require('whatwg-fetch'); 13 | 14 | // Object.assign() is commonly used with React. 15 | // It will use the native implementation if it's present and isn't buggy. 16 | Object.assign = require('object-assign'); 17 | -------------------------------------------------------------------------------- /examples/subcomponent/config/polyfills.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | if (typeof Promise === 'undefined') { 4 | // Rejection tracking prevents a common issue where React gets into an 5 | // inconsistent state due to an error, but it gets swallowed by a Promise, 6 | // and the user has no idea what causes React's erratic future behavior. 7 | require('promise/lib/rejection-tracking').enable(); 8 | window.Promise = require('promise/lib/es6-extensions.js'); 9 | } 10 | 11 | // fetch() polyfill for making API calls. 12 | require('whatwg-fetch'); 13 | 14 | // Object.assign() is commonly used with React. 15 | // It will use the native implementation if it's present and isn't buggy. 16 | Object.assign = require('object-assign'); 17 | -------------------------------------------------------------------------------- /test/helpers.test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import Rx from '@evo/rx'; 4 | 5 | import { actionIs } from '../src/helpers'; 6 | 7 | describe('tanokHelpers', () => { 8 | describe('actionIs', () => { 9 | it('passes correct action', (done) => { 10 | const stream = Rx.Observable.of({ action: 't' }); 11 | 12 | actionIs('t').call(stream) 13 | .subscribe(() => { done() }) 14 | }); 15 | 16 | it('filters incorrect action', (done) => { 17 | const stream = Rx.Observable.of({ action: 'f' }); 18 | 19 | actionIs('t').call(stream) 20 | .subscribe( 21 | () => { throw new Error }, 22 | (e) => { throw e }, 23 | () => { done() } 24 | ) 25 | }) 26 | }) 27 | }); -------------------------------------------------------------------------------- /examples/counter/src/view.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { tanokComponent } from 'tanok'; 3 | 4 | @tanokComponent 5 | export class Counter extends React.Component { 6 | constructor(props) { 7 | super(props); 8 | this.onPlusClick = this.onPlusClick.bind(this); 9 | this.onMinusClick = this.onMinusClick.bind(this); 10 | } 11 | 12 | onPlusClick() { 13 | this.send('inc') 14 | } 15 | 16 | onMinusClick() { 17 | this.send('dec') 18 | } 19 | 20 | render() { 21 | return ( 22 |
23 | 24 | {this.props.count} 25 | 26 |
27 | ); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /examples/subcomponent_with_metadata/config/polyfills.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | if (typeof Promise === 'undefined') { 4 | // Rejection tracking prevents a common issue where React gets into an 5 | // inconsistent state due to an error, but it gets swallowed by a Promise, 6 | // and the user has no idea what causes React's erratic future behavior. 7 | require('promise/lib/rejection-tracking').enable(); 8 | window.Promise = require('promise/lib/es6-extensions.js'); 9 | } 10 | 11 | // fetch() polyfill for making API calls. 12 | require('whatwg-fetch'); 13 | 14 | // Object.assign() is commonly used with React. 15 | // It will use the native implementation if it's present and isn't buggy. 16 | Object.assign = require('object-assign'); 17 | -------------------------------------------------------------------------------- /src/connect/utils/shallowEqual.js: -------------------------------------------------------------------------------- 1 | const hasOwn = Object.prototype.hasOwnProperty 2 | 3 | function is(x, y) { 4 | if (x === y) { 5 | return x !== 0 || y !== 0 || 1 / x === 1 / y 6 | } else { 7 | return x !== x && y !== y 8 | } 9 | } 10 | 11 | export default function shallowEqual(objA, objB) { 12 | if (is(objA, objB)) return true 13 | 14 | if (typeof objA !== 'object' || objA === null || 15 | typeof objB !== 'object' || objB === null) { 16 | return false 17 | } 18 | 19 | const keysA = Object.keys(objA) 20 | const keysB = Object.keys(objB) 21 | 22 | if (keysA.length !== keysB.length) return false 23 | 24 | for (let i = 0; i < keysA.length; i++) { 25 | if (!hasOwn.call(objB, keysA[i]) || 26 | !is(objA[keysA[i]], objB[keysA[i]])) { 27 | return false 28 | } 29 | } 30 | 31 | return true 32 | } 33 | -------------------------------------------------------------------------------- /examples/subcomponent/src/dispatcher.js: -------------------------------------------------------------------------------- 1 | import { on, TanokDispatcher, subcomponentFx, childFx } from 'tanok'; 2 | 3 | import { CounterDispatcher } from './counter/dispatcher'; 4 | 5 | export class DashboardDispatcher extends TanokDispatcher { 6 | @on('init') 7 | init(payload, state) { 8 | return [state, 9 | subcomponentFx('top', new CounterDispatcher()), 10 | subcomponentFx('bottom', new CounterDispatcher()), 11 | ] 12 | } 13 | 14 | @on('top') 15 | top(payload, state) { 16 | const [newState, ...effects] = payload(state.top); 17 | state.top = newState; 18 | return [state, ...effects.map((e) => childFx(e, 'top'))] 19 | } 20 | 21 | @on('bottom') 22 | bottom(payload, state) { 23 | const [newState, ...effects] = payload(state.bottom); 24 | state.bottom = newState; 25 | return [state, ...effects.map((e) => childFx(e, 'bottom'))] 26 | } 27 | } -------------------------------------------------------------------------------- /src/helpers.js: -------------------------------------------------------------------------------- 1 | export function actionIs(actionName) { 2 | return function () { 3 | return this.filter(({ action, streamName }) => action === actionName); 4 | }; 5 | } 6 | 7 | export function parentIs(awaitedName) { 8 | console.error('This function is deprecated, use `nameIs` instead') 9 | return function () { 10 | return this.filter(({streamName}) => streamName === awaitedName); 11 | }; 12 | } 13 | 14 | export function nameIs(awaitedName) { 15 | return function () { 16 | return this.filter(({streamName}) => streamName === awaitedName); 17 | }; 18 | } 19 | 20 | export function filter(cond) { 21 | return function () { 22 | return this.filter(cond); 23 | }; 24 | } 25 | 26 | export function debounce(time) { 27 | return function () { 28 | return this.debounce(time); 29 | }; 30 | } 31 | 32 | export function throttle(time) { 33 | return function () { 34 | return this.throttle(time); 35 | }; 36 | } 37 | -------------------------------------------------------------------------------- /examples/async_counter/src/view.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { tanokComponent } from 'tanok'; 3 | 4 | @tanokComponent 5 | export class Counter extends React.Component { 6 | constructor(props) { 7 | super(props); 8 | this.onPlusClick = this.onPlusClick.bind(this); 9 | this.onMinusClick = this.onMinusClick.bind(this); 10 | this.onEffectsClick = this.onEffectsClick.bind(this); 11 | } 12 | onPlusClick() { 13 | this.send('inc') 14 | } 15 | onMinusClick() { 16 | this.send('dec') 17 | } 18 | onEffectsClick() { 19 | this.send('effectKinds') 20 | } 21 | render() { 22 | return ( 23 |
24 | 25 | {this.props.count} 26 | 27 | 28 |
29 | ) 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /examples/subcomponent/src/counter/view.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { tanokComponent } from 'tanok'; 3 | 4 | @tanokComponent 5 | export class Counter extends React.Component { 6 | constructor(props) { 7 | super(props); 8 | this.onPlusClick = this.onPlusClick.bind(this); 9 | this.onMinusClick = this.onMinusClick.bind(this); 10 | this.onEffectsClick = this.onEffectsClick.bind(this); 11 | } 12 | onPlusClick() { 13 | this.send('inc') 14 | } 15 | onMinusClick() { 16 | this.send('dec') 17 | } 18 | onEffectsClick() { 19 | this.send('effectKinds') 20 | } 21 | render() { 22 | return ( 23 |
24 | 25 | {this.props.count} 26 | 27 | 28 |
29 | ) 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /examples/subcomponent_with_metadata/src/counter/view.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { tanokComponent } from 'tanok'; 3 | 4 | @tanokComponent 5 | export class Counter extends React.Component { 6 | constructor(props) { 7 | super(props); 8 | this.onPlusClick = this.onPlusClick.bind(this); 9 | this.onMinusClick = this.onMinusClick.bind(this); 10 | this.onEffectsClick = this.onEffectsClick.bind(this); 11 | } 12 | onPlusClick() { 13 | this.send('inc') 14 | } 15 | onMinusClick() { 16 | this.send('dec') 17 | } 18 | onEffectsClick() { 19 | this.send('effectKinds') 20 | } 21 | render() { 22 | return ( 23 |
24 | 25 | {this.props.count} 26 | 27 | 28 |
29 | ) 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/components/root.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | 4 | import { storeKey, streamKey, subscriptionKey } from '../constants'; 5 | import { storeShape, subscriptionShape } from '../connect/utils/PropTypes'; 6 | 7 | export class Root extends React.Component { 8 | constructor(props, context) { 9 | super(props, context); 10 | this[storeKey] = props.store; 11 | this[streamKey] = props.tanokStream; 12 | } 13 | 14 | getChildContext() { 15 | return { 16 | [storeKey]: this[storeKey], 17 | [streamKey]: this[streamKey], 18 | [subscriptionKey]: null, 19 | } 20 | } 21 | 22 | render() { 23 | return React.Children.only(this.props.children); 24 | } 25 | } 26 | 27 | Root.propTypes = { 28 | store: storeShape.isRequired, 29 | children: PropTypes.element.isRequired, 30 | }; 31 | Root.childContextTypes = { 32 | [storeKey]: storeShape.isRequired, 33 | [streamKey]: PropTypes.any.isRequired, 34 | [subscriptionKey]: subscriptionShape, 35 | }; 36 | -------------------------------------------------------------------------------- /examples/search_example/src/view.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { tanokComponent } from 'tanok'; 3 | import * as action from './actions'; 4 | 5 | @tanokComponent 6 | export class SearchComponent extends React.Component { 7 | render() { 8 | return ( 9 |
10 | { 13 | this.send(action.INPUT_TERM, { term: e.target.value }) 14 | }} 15 | /> 16 | 26 |
27 | ) 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Boryslav Larin 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 | -------------------------------------------------------------------------------- /src/core.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | 4 | import { Root } from './components/root'; 5 | import { createStore } from './createStore'; 6 | 7 | export function tanok(initialState, update, view, options) { 8 | let container = options.container; 9 | if (!container) { 10 | container = document.createElement('div'); 11 | document.body.appendChild(container); 12 | } 13 | const [tanokStream, store] = createStore(initialState, update, options); 14 | 15 | let component; 16 | const render = () => { 17 | const createdView = React.createElement( 18 | view, { 19 | tanokStream, 20 | ...store.getState(), 21 | }); 22 | component = ReactDOM.render( 23 | 24 | {createdView} 25 | , 26 | container 27 | ); 28 | }; 29 | render(); 30 | const sub = store.subscribe(render); 31 | 32 | return { 33 | component, 34 | tanokStream, 35 | store, 36 | shutdown: () => { 37 | sub(); 38 | store.shutdown(); 39 | container && ReactDOM.unmountComponentAtNode(container); 40 | }, 41 | }; 42 | } 43 | -------------------------------------------------------------------------------- /src/component.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | 4 | import { StreamWrapper } from './streamWrapper.js'; 5 | 6 | /** 7 | * Decorator used with class-based React components. 8 | * It provides all the required props and helpers for tanok internals. 9 | * 10 | * Usage example: 11 | * 12 | * @tanokComponent 13 | * class MyComponent extends React.Component { 14 | * ... your component methods. 15 | * } 16 | * 17 | * */ 18 | export function tanokComponent(target) { 19 | target.propTypes = target.propTypes || {}; 20 | target.propTypes.tanokStream = PropTypes.instanceOf(StreamWrapper); 21 | 22 | target.displayName = `TanokComponent(${target.displayName || target.name})`; 23 | 24 | target.prototype.send = function send(action, payload, metadata = null) { 25 | if (metadata !== null) { 26 | console.error('Hey! You no longer can pass metadata `.send()`, use `.sub()`'); 27 | } 28 | 29 | const stream = this.props.tanokStream; 30 | stream.send(action, payload); 31 | }; 32 | 33 | target.prototype.sub = function sub(name, metadata = null) { 34 | const stream = this.props.tanokStream; 35 | return stream && stream.subWithMeta(name, metadata); 36 | }; 37 | 38 | return target; 39 | } 40 | -------------------------------------------------------------------------------- /src/connect/mergeProps.js: -------------------------------------------------------------------------------- 1 | import shallowEqual from './utils/shallowEqual'; 2 | 3 | export function defaultMergeProps(stateProps, sendProps, ownProps) { 4 | return { ...ownProps, ...stateProps, ...sendProps } 5 | } 6 | 7 | export function wrapMergePropsFunc(mergeProps) { 8 | return function initMergePropsProxy() { 9 | let hasRunOnce = false 10 | let mergedProps 11 | const areMergedPropsEqual = shallowEqual; 12 | return function mergePropsProxy(stateProps, sendProps, ownProps) { 13 | const nextMergedProps = mergeProps(stateProps, sendProps, ownProps) 14 | if (hasRunOnce) { 15 | if (!areMergedPropsEqual(nextMergedProps, mergedProps)) 16 | mergedProps = nextMergedProps 17 | } else { 18 | hasRunOnce = true 19 | mergedProps = nextMergedProps 20 | } 21 | 22 | return mergedProps 23 | } 24 | } 25 | } 26 | 27 | export function whenMergePropsIsFunction(mergeProps) { 28 | return (typeof mergeProps === 'function') 29 | ? wrapMergePropsFunc(mergeProps) 30 | : undefined 31 | } 32 | 33 | export function whenMergePropsIsOmitted(mergeProps) { 34 | return (!mergeProps) 35 | ? () => defaultMergeProps 36 | : undefined 37 | } 38 | 39 | export default [ 40 | whenMergePropsIsFunction, 41 | whenMergePropsIsOmitted 42 | ] 43 | -------------------------------------------------------------------------------- /src/components/subcomponent.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | 4 | import { streamKey, storeKey } from '../constants'; 5 | 6 | export class Subcomponent extends React.Component { 7 | constructor(props, context) { 8 | super(props, context); 9 | 10 | const stream = context[streamKey]; 11 | const store = context[storeKey]; 12 | const {name, metadata, selector} = props; 13 | 14 | this[storeKey] = { 15 | ...store, 16 | getState: () => { 17 | return selector(store.getState()); 18 | }, 19 | }; 20 | 21 | this[streamKey] = stream && stream.subWithMeta(name, metadata); 22 | } 23 | 24 | getChildContext() { 25 | return { 26 | [streamKey]: this[streamKey], 27 | [storeKey]: this[storeKey], 28 | } 29 | } 30 | 31 | render() { 32 | return React.Children.only(this.props.children); 33 | } 34 | } 35 | 36 | Subcomponent.propTypes = { 37 | name: PropTypes.any.isRequired, 38 | metadata: PropTypes.any, 39 | selector: PropTypes.func.isRequired, 40 | }; 41 | Subcomponent.childContextTypes = { 42 | [streamKey]: PropTypes.any.isRequired, 43 | [storeKey]: PropTypes.any.isRequired, 44 | }; 45 | Subcomponent.contextTypes = { 46 | [streamKey]: PropTypes.any.isRequired, 47 | [storeKey]: PropTypes.any.isRequired, 48 | }; 49 | -------------------------------------------------------------------------------- /test/dispatcher.test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import React from 'react'; 4 | import expect from 'expect'; 5 | 6 | import { on, TanokDispatcher } from '../src/tanok.js'; 7 | 8 | 9 | describe('TanokDispatcher', () => { 10 | 11 | class TestDispatcher extends TanokDispatcher { 12 | @on('init') 13 | init(_, state) { 14 | return [1]; 15 | } 16 | } 17 | class TestDispatcher2 extends TestDispatcher { 18 | @on('init') 19 | init(_, state) { 20 | return [2]; 21 | } 22 | } 23 | 24 | it('collect return all possible actions', function (done) { 25 | const dispatcher = new TestDispatcher; 26 | const actions = dispatcher.collect(); 27 | expect(actions).toHaveLength(1); 28 | expect(actions[0][0][0]).toEqual('init'); 29 | done(); 30 | }); 31 | 32 | it('iterator works well', function (done) { 33 | const dispatcher = new TestDispatcher; 34 | const iteratedDispatcher = [...dispatcher]; 35 | expect(iteratedDispatcher.length).toEqual(1); 36 | expect(iteratedDispatcher[0][0][0]).toEqual('init'); 37 | done(); 38 | }); 39 | 40 | it('inheritance work well', function (done) { 41 | const dispatcher1 = new TestDispatcher; 42 | const dispatcher2 = new TestDispatcher2; 43 | expect(dispatcher1.init()).toEqual([1]); 44 | expect(dispatcher2.init()).toEqual([2]); 45 | done(); 46 | }); 47 | }); 48 | -------------------------------------------------------------------------------- /test/compose.test.js: -------------------------------------------------------------------------------- 1 | import expect from 'expect'; 2 | import compose from '../src/compose'; 3 | 4 | describe('compose', () => { 5 | it('composes from right to left', () => { 6 | const double = x => x * 2 7 | const square = x => x * x 8 | expect(compose(square)(5)).toBe(25) 9 | expect(compose(square, double)(5)).toBe(100) 10 | expect(compose(double, square, double)(5)).toBe(200) 11 | }) 12 | 13 | it('composes functions from right to left', () => { 14 | const a = next => x => next(x + 'a') 15 | const b = next => x => next(x + 'b') 16 | const c = next => x => next(x + 'c') 17 | const final = x => x 18 | 19 | expect(compose(a, b, c)(final)('')).toBe('abc') 20 | expect(compose(b, c, a)(final)('')).toBe('bca') 21 | expect(compose(c, a, b)(final)('')).toBe('cab') 22 | }) 23 | 24 | it('can be seeded with multiple arguments', () => { 25 | const square = x => x * x 26 | const add = (x, y) => x + y 27 | expect(compose(square, add)(1, 2)).toBe(9) 28 | }) 29 | 30 | it('returns the first given argument if given no functions', () => { 31 | expect(compose()(1, 2)).toBe(1) 32 | expect(compose()(3)).toBe(3) 33 | expect(compose()()).toBe(undefined) 34 | }) 35 | 36 | it('returns the first function if given only one', () => { 37 | const fn = () => {} 38 | 39 | expect(compose(fn)).toBe(fn) 40 | }) 41 | }) -------------------------------------------------------------------------------- /examples/counter/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "counter", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "babel-core": "6.26.0", 7 | "babel-eslint": "7.2.3", 8 | "babel-loader": "7.1.2", 9 | "babel-plugin-transform-decorators-legacy": "^1.3.4", 10 | "babel-preset-react-app": "^3.1.0", 11 | "babel-runtime": "6.26.0", 12 | "case-sensitive-paths-webpack-plugin": "2.1.1", 13 | "dotenv": "4.0.0", 14 | "eslint": "4.10.0", 15 | "eslint-config-react-app": "^2.0.1", 16 | "eslint-loader": "1.9.0", 17 | "eslint-plugin-flowtype": "2.39.1", 18 | "eslint-plugin-import": "2.8.0", 19 | "eslint-plugin-jsx-a11y": "5.1.1", 20 | "eslint-plugin-react": "7.4.0", 21 | "extract-text-webpack-plugin": "3.0.2", 22 | "file-loader": "1.1.5", 23 | "html-webpack-plugin": "2.29.0", 24 | "object-assign": "4.1.1", 25 | "promise": "8.0.1", 26 | "react": "^16.0.0", 27 | "react-dev-utils": "^4.2.1", 28 | "react-dom": "^16.0.0", 29 | "rx": "^4.0.6", 30 | "tanok": "^1.3.0", 31 | "webpack": "3.8.1", 32 | "webpack-dev-server": "2.9.3", 33 | "whatwg-fetch": "2.0.3" 34 | }, 35 | "scripts": { 36 | "start": "node scripts/start.js" 37 | }, 38 | "babel": { 39 | "presets": [ 40 | "react-app" 41 | ] 42 | }, 43 | "eslintConfig": { 44 | "extends": "react-app" 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /examples/subcomponent/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "counter", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "babel-core": "6.26.0", 7 | "babel-eslint": "7.2.3", 8 | "babel-loader": "7.1.2", 9 | "babel-plugin-transform-decorators-legacy": "^1.3.4", 10 | "babel-preset-react-app": "^3.1.0", 11 | "babel-runtime": "6.26.0", 12 | "case-sensitive-paths-webpack-plugin": "2.1.1", 13 | "dotenv": "4.0.0", 14 | "eslint": "4.10.0", 15 | "eslint-config-react-app": "^2.0.1", 16 | "eslint-loader": "1.9.0", 17 | "eslint-plugin-flowtype": "2.39.1", 18 | "eslint-plugin-import": "2.8.0", 19 | "eslint-plugin-jsx-a11y": "5.1.1", 20 | "eslint-plugin-react": "7.4.0", 21 | "extract-text-webpack-plugin": "3.0.2", 22 | "file-loader": "1.1.5", 23 | "html-webpack-plugin": "2.29.0", 24 | "object-assign": "4.1.1", 25 | "promise": "8.0.1", 26 | "react": "^16.0.0", 27 | "react-dev-utils": "^4.2.1", 28 | "react-dom": "^16.0.0", 29 | "rx": "^4.0.6", 30 | "tanok": "^1.3.0", 31 | "webpack": "3.8.1", 32 | "webpack-dev-server": "2.9.3", 33 | "whatwg-fetch": "2.0.3" 34 | }, 35 | "scripts": { 36 | "start": "node scripts/start.js" 37 | }, 38 | "babel": { 39 | "presets": [ 40 | "react-app" 41 | ] 42 | }, 43 | "eslintConfig": { 44 | "extends": "react-app" 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /examples/async_counter/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "counter", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "babel-core": "6.26.0", 7 | "babel-eslint": "7.2.3", 8 | "babel-loader": "7.1.2", 9 | "babel-plugin-transform-decorators-legacy": "^1.3.4", 10 | "babel-preset-react-app": "^3.1.0", 11 | "babel-runtime": "6.26.0", 12 | "case-sensitive-paths-webpack-plugin": "2.1.1", 13 | "dotenv": "4.0.0", 14 | "eslint": "4.10.0", 15 | "eslint-config-react-app": "^2.0.1", 16 | "eslint-loader": "1.9.0", 17 | "eslint-plugin-flowtype": "2.39.1", 18 | "eslint-plugin-import": "2.8.0", 19 | "eslint-plugin-jsx-a11y": "5.1.1", 20 | "eslint-plugin-react": "7.4.0", 21 | "extract-text-webpack-plugin": "3.0.2", 22 | "file-loader": "1.1.5", 23 | "html-webpack-plugin": "2.29.0", 24 | "object-assign": "4.1.1", 25 | "promise": "8.0.1", 26 | "react": "^16.0.0", 27 | "react-dev-utils": "^4.2.1", 28 | "react-dom": "^16.0.0", 29 | "rx": "^4.0.6", 30 | "tanok": "^1.3.0", 31 | "webpack": "3.8.1", 32 | "webpack-dev-server": "2.9.3", 33 | "whatwg-fetch": "2.0.3" 34 | }, 35 | "scripts": { 36 | "start": "node scripts/start.js" 37 | }, 38 | "babel": { 39 | "presets": [ 40 | "react-app" 41 | ] 42 | }, 43 | "eslintConfig": { 44 | "extends": "react-app" 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /examples/search_example/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "counter", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "babel-core": "6.26.0", 7 | "babel-eslint": "7.2.3", 8 | "babel-loader": "7.1.2", 9 | "babel-plugin-transform-decorators-legacy": "^1.3.4", 10 | "babel-preset-react-app": "^3.1.0", 11 | "babel-runtime": "6.26.0", 12 | "case-sensitive-paths-webpack-plugin": "2.1.1", 13 | "dotenv": "4.0.0", 14 | "eslint": "4.10.0", 15 | "eslint-config-react-app": "^2.0.1", 16 | "eslint-loader": "1.9.0", 17 | "eslint-plugin-flowtype": "2.39.1", 18 | "eslint-plugin-import": "2.8.0", 19 | "eslint-plugin-jsx-a11y": "5.1.1", 20 | "eslint-plugin-react": "7.4.0", 21 | "extract-text-webpack-plugin": "3.0.2", 22 | "file-loader": "1.1.5", 23 | "html-webpack-plugin": "2.29.0", 24 | "object-assign": "4.1.1", 25 | "promise": "8.0.1", 26 | "react": "^16.0.0", 27 | "react-dev-utils": "^4.2.1", 28 | "react-dom": "^16.0.0", 29 | "rx": "^4.0.6", 30 | "tanok": "^1.3.0", 31 | "webpack": "3.8.1", 32 | "webpack-dev-server": "2.9.3", 33 | "whatwg-fetch": "2.0.3" 34 | }, 35 | "scripts": { 36 | "start": "node scripts/start.js" 37 | }, 38 | "babel": { 39 | "presets": [ 40 | "react-app" 41 | ] 42 | }, 43 | "eslintConfig": { 44 | "extends": "react-app" 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /examples/subcomponent_with_metadata/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "counter", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "babel-core": "6.26.0", 7 | "babel-eslint": "7.2.3", 8 | "babel-loader": "7.1.2", 9 | "babel-plugin-transform-decorators-legacy": "^1.3.4", 10 | "babel-preset-react-app": "^3.1.0", 11 | "babel-runtime": "6.26.0", 12 | "case-sensitive-paths-webpack-plugin": "2.1.1", 13 | "dotenv": "4.0.0", 14 | "eslint": "4.10.0", 15 | "eslint-config-react-app": "^2.0.1", 16 | "eslint-loader": "1.9.0", 17 | "eslint-plugin-flowtype": "2.39.1", 18 | "eslint-plugin-import": "2.8.0", 19 | "eslint-plugin-jsx-a11y": "5.1.1", 20 | "eslint-plugin-react": "7.4.0", 21 | "extract-text-webpack-plugin": "3.0.2", 22 | "file-loader": "1.1.5", 23 | "html-webpack-plugin": "2.29.0", 24 | "object-assign": "4.1.1", 25 | "promise": "8.0.1", 26 | "react": "^16.0.0", 27 | "react-dev-utils": "^4.2.1", 28 | "react-dom": "^16.0.0", 29 | "rx": "^4.0.6", 30 | "tanok": "^1.3.0", 31 | "webpack": "3.8.1", 32 | "webpack-dev-server": "2.9.3", 33 | "whatwg-fetch": "2.0.3" 34 | }, 35 | "scripts": { 36 | "start": "node scripts/start.js" 37 | }, 38 | "babel": { 39 | "presets": [ 40 | "react-app" 41 | ] 42 | }, 43 | "eslintConfig": { 44 | "extends": "react-app" 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /examples/search_example/src/dispatcher.js: -------------------------------------------------------------------------------- 1 | import Rx from '@evo/rx'; 2 | import { on, TanokDispatcher, rethrowFx } from 'tanok'; 3 | import * as action from './actions'; 4 | 5 | function searchRepos(searchTerm) { 6 | return (stream) => 7 | Rx.Observable.fromPromise( 8 | fetch(`https://api.github.com/search/repositories?q=${searchTerm || 'tanok'}`) 9 | ) 10 | .flatMap((r) => r.json()) 11 | .do(() => console.log('pre', searchTerm)) 12 | .takeUntil(stream.stream.filter(({action: dispatchedAction}) => dispatchedAction === action.CANCEL_SEARCH)) 13 | .do(() => console.log('post', searchTerm)) 14 | .do(({ items }) => stream.send(action.SEARCH_OK, { items })) 15 | } 16 | 17 | function cancelSearch(stream) { 18 | stream.send(action.CANCEL_SEARCH); 19 | } 20 | 21 | export class SearchDispatcher extends TanokDispatcher { 22 | @on(action.INIT) 23 | init(_, state) { 24 | return [state, rethrowFx(action.SEARCH)] 25 | } 26 | 27 | @on(action.INPUT_TERM) 28 | inputTerm({ term }, state) { 29 | state.searchTerm = term; 30 | return [state, rethrowFx(action.SEARCH)] 31 | } 32 | 33 | @on(action.SEARCH) 34 | search(_, state) { 35 | return [state, cancelSearch, searchRepos(state.searchTerm)] 36 | } 37 | 38 | @on(action.SEARCH_OK) 39 | searchOk({ items }, state) { 40 | state.repos = items; 41 | return [state] 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/tanokInReact.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import { createStore } from './createStore'; 4 | import { Root } from "./components/root"; 5 | 6 | 7 | const identity = (x) => x; 8 | 9 | export class TanokInReact extends React.Component { 10 | constructor(props) { 11 | super(props); 12 | const { 13 | initialState, update, view, 14 | middlewares = [], 15 | onNewState, 16 | outerEventStream, 17 | stateSerializer, 18 | } = props; 19 | 20 | const [tanokStream, store] = createStore(initialState, update, { 21 | middlewares, 22 | outerEventStream, 23 | }); 24 | if (onNewState) { 25 | this.storeSubOnNewState = store.subscribe(onNewState); 26 | } 27 | this.view = view; 28 | this.tanokStream = tanokStream; 29 | this.store = store; 30 | this.stateSerializer = stateSerializer || identity; 31 | } 32 | 33 | componentDidMount() { 34 | this.storeSubOnStoreUpd = this.store.subscribe(() => this.forceUpdate()); 35 | } 36 | 37 | componentWillUnmount() { 38 | this.storeSubOnNewState && this.storeSubOnNewState(); 39 | this.storeSubOnStoreUpd(); 40 | this.store.shutdown(); 41 | } 42 | 43 | render() { 44 | return ( 45 | 46 | 49 | 50 | ) 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/tanokDispatcher.js: -------------------------------------------------------------------------------- 1 | export const TanokDispatcher = function() {}; 2 | 3 | TanokDispatcher.prototype.collect = function () { 4 | return Object.keys(this.events).map( 5 | (handlerFuncName) => { 6 | const pair = this.events[handlerFuncName]; 7 | return [pair[0], pair[1].bind(this)]; 8 | } 9 | ); 10 | } 11 | 12 | TanokDispatcher.prototype[Symbol.iterator] = function(){ 13 | function makeIterator(array){ 14 | var nextIndex = 0; 15 | 16 | return { 17 | next: function(){ 18 | return nextIndex < array.length ? 19 | {value: array[nextIndex++], done: false} : 20 | {done: true}; 21 | } 22 | }; 23 | } 24 | 25 | return makeIterator(this.collect()); 26 | } 27 | 28 | /** 29 | * Decorator used with TanokDispatcher 30 | * 31 | * Usage example: 32 | * 33 | * class HelloWorldDispatcher extends TanokDispatcher { 34 | * 35 | * @on('helloEvent') 36 | * helloWorld (eventPayload, state) { 37 | * state.word = eventPayload.word; 38 | * return [state, helloWorldEffect]; 39 | * } 40 | * } 41 | * 42 | * @param predicate - action title or multiple values like @on('actionTitle', debounce(500)) 43 | * @returns {Function} 44 | */ 45 | export function on(...predicate) { 46 | return (target, property) => { 47 | target.events = target.events || {}; 48 | const handlerFunc = target[property]; 49 | target.events[property] = [predicate, handlerFunc]; 50 | }; 51 | } 52 | -------------------------------------------------------------------------------- /docs/AsyncStuff.md: -------------------------------------------------------------------------------- 1 | # Async stuff 2 | 3 | Now how to make asynchronous calls, like ajax requests, setTimeouts and others? 4 | 5 | State mutators return an array with state as it's first item. This state goes 6 | right into React component `setState`, which triggers component rerendering. 7 | Other members of array may be functions (*effects*), which are called sequentially after `setState`. 8 | 9 | They get stream as a parameter, so they can make ajax call and do `stream.send` 10 | back to the dispatcher. This is how effect might look like. 11 | 12 | ```js 13 | function syncEffect(cnt) { 14 | return function (stream) { 15 | fetch('http://www.mocky.io/v2/577824a4120000ca28aac904', { 16 | method: 'POST', 17 | body: cnt, 18 | }) 19 | .then((r) => r.json()) 20 | .then((json) => stream.send('syncSuccess', json)) 21 | } 22 | } 23 | ``` 24 | 25 | To run effect we return new state with effect function. 26 | 27 | ```js 28 | export class CounterDispatcher extends TanokDispatcher { 29 | @on('inc') 30 | inc(payload, state) { 31 | state.count += 1; 32 | state.synced = false; 33 | 34 | return [state, syncEffect(state.count)]; 35 | } 36 | 37 | @on('syncSuccess') 38 | syncSuccess(payload, state) { 39 | state.synced = true; 40 | return [state]; 41 | } 42 | 43 | ... 44 | } 45 | ``` 46 | 47 | Effects usually have to change state somehow, so they get stream as parameter. 48 | So they can call `stream.send(ACTION, payload)` to update state and trigger another rerendering. 49 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # tanok 💃 2 | 3 | [![Build Status](https://travis-ci.org/brabadu/tanok.svg?branch=master)](https://travis-ci.org/brabadu/tanok) 4 | [![Coverage Status](https://coveralls.io/repos/brabadu/tanok/badge.svg?branch=master)](https://coveralls.io/r/brabadu/tanok?branch=master) 5 | [![npm downloads](https://img.shields.io/npm/dm/tanok.svg?style=flat-square)](https://www.npmjs.com/package/tanok) 6 | 7 | State management for React using Rx.js and Elm Architecture inspiration. 8 | 9 | You could start with [Elm Architecture Tutorial](https://github.com/evancz/elm-architecture-tutorial/), there's more info 10 | 11 | Elm Architecture gives you a way to build complex UI with everything we'd like 12 | to have these days. Unidirectional data flow, separation of concerns, 13 | usable child components and fast HTML rendering. 14 | 15 | **tanok** let's you do the same with JavaScript, React and Rx.js. 16 | 17 | > **tanok** is also slavic circle dance 18 | 19 | # Installation 20 | 21 | To install the stable version: 22 | 23 | `npm install --save tanok` 24 | 25 | ## How to start 26 | 27 | Go on with [Getting Started](/docs/Basics.md) section 28 | 29 | # Authors 30 | 31 | Great people of Evo Company: 32 | 33 | * [Anton Verinov](http://github.com/zemlanin) 34 | * [Boryslav Larin](http://github.com/brabadu) 35 | * [Dmitriy Sadkovoy](http://github.com/sadkovoy) 36 | * [Dmitriy Zhuribeda](https://github.com/DZhuribeda) 37 | * [Valeriy Morkovyn](http://github.com/Lex0ne) 38 | 39 | With thoughtful tests and wise advices from many others. 40 | -------------------------------------------------------------------------------- /rollup.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | const rollup = require('rollup') 3 | const babel = require('rollup-plugin-babel') 4 | const commonjs = require('rollup-plugin-commonjs') 5 | const nodeResolve = require('rollup-plugin-node-resolve') 6 | 7 | const peerDependencies = Object.keys(require('./package.json').peerDependencies) 8 | 9 | const entries = [ 10 | { 11 | entry: 'src/tanok.js', 12 | moduleName: 'tanok', 13 | }, 14 | { 15 | entry: 'src/streamWrapper.js', 16 | moduleName: 'tanokStreamWrapper', 17 | }, 18 | ] 19 | const plugins = [ 20 | babel({ 21 | "presets": [ "es2015-rollup", "stage-0", "react" ], 22 | "plugins": [ "transform-decorators-legacy", 23 | ["transform-es2015-classes", {loose: true}], 24 | "transform-object-rest-spread" 25 | ], 26 | "babelrc": false 27 | }), 28 | nodeResolve({ 29 | customResolveOptions: { 30 | moduleDirectory: 'node_modules' 31 | }}), 32 | commonjs(), 33 | ] 34 | 35 | entries.forEach((entry) => { 36 | rollup.rollup({ 37 | input: entry.entry, 38 | plugins: plugins, 39 | external: peerDependencies 40 | }).then( 41 | (bundle) => bundle.write({ 42 | file: entry.entry.replace('src', 'lib'), 43 | format: 'umd', 44 | globals: { 45 | react: 'React', 46 | 'react-dom': 'ReactDOM', 47 | }, 48 | name: entry.moduleName 49 | }), 50 | (error) => { 51 | console.error(error) 52 | } 53 | ) 54 | }); 55 | -------------------------------------------------------------------------------- /examples/subcomponent_with_metadata/src/counter/dispatcher.js: -------------------------------------------------------------------------------- 1 | import { on, TanokDispatcher } from 'tanok'; 2 | import Rx from '@evo/rx'; 3 | 4 | function syncEffect(cnt) { 5 | return function (stream) { 6 | fetch('http://www.mocky.io/v2/577824a4120000ca28aac904', { 7 | method: 'POST', 8 | body: cnt, 9 | }) 10 | .then((r) => r.json()) 11 | .then((json) => stream.send('syncSuccess', json)) 12 | } 13 | } 14 | 15 | 16 | export class CounterDispatcher extends TanokDispatcher { 17 | @on('inc') 18 | inc(payload, state) { 19 | state.count += 1; 20 | state.synced = false; 21 | 22 | return [state, syncEffect(state.count)]; 23 | } 24 | 25 | @on('dec') 26 | dec(payload, state) { 27 | state.count -= 1; 28 | state.synced = false; 29 | 30 | return [state, syncEffect(state.count)]; 31 | } 32 | 33 | @on('syncSuccess') 34 | syncSuccess(payload, state) { 35 | state.synced = true; 36 | return [state]; 37 | } 38 | 39 | @on('effectKinds') 40 | promise(payload, state) { 41 | function promiseFx(stream){ 42 | return new Promise((resolve, reject) => { 43 | stream.send('done', 'Promise done'); 44 | return resolve(1); 45 | }) 46 | } 47 | 48 | function observableFx(stream) { 49 | return Rx.Observable.create((obs) => { 50 | stream.send('done', 'Observable done'); 51 | obs.onNext(1); 52 | obs.onCompleted(); 53 | }) 54 | } 55 | 56 | return [state, promiseFx, observableFx]; 57 | } 58 | 59 | @on('done') 60 | done(payload, state) { 61 | console.log(state.id, payload); 62 | return [state]; 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /docs/Tanok.md: -------------------------------------------------------------------------------- 1 | ## function tanok(initialState, update, view, options) 2 | 3 | Main entry point and the easiest way to create tanok app. 4 | Examples: 5 | ```js 6 | import {tanok} from 'tanok'; 7 | ... 8 | tanok( 9 | {counter: 0}, 10 | new CounterDispatcher, 11 | CounterView, { 12 | container: document.getElementById('root'), 13 | } 14 | ); 15 | ``` 16 | 17 | ### Parameters: 18 | * **`initialState`** - initial state of app. May be any type, usually it's object or instance of some class, that incapsulates all data to start your app. 19 | * **`update`** - instance of `TanokDispatcher`, which describes actions of your app. Same as reducers in Redux. 20 | * **`view`** - Root React component 21 | * **`options`** - object, additional configuration for your app: 22 | * **`container`** - HTMLElement, root node of your application. If not provided, new "div" will be created appended to `document.body`. 23 | * **`outerEventStream`** - `Rx.Observable`, that can pass actions into app from outside world 24 | * **`stateSerializer`** - `Function`, that is called on every action. It's return is then passed as props to View component, if not specified state is passed as is. 25 | * **`middlewares`** - List of middlewares, which are called on action. May intercept actions for analytics, debugging and other purposes 26 | 27 | Returns object with keys: 28 | * **`streamWrapper`** - `StreamWrapper` instance, that was created to pass all actions in app. May be used to send actions inside app 29 | * **`shutdown`** - `Function`, that closes all `streamWrapper` and all other side-effects that was created by app, unmounts View from DOM. 30 | * **`component`** - React object, that is returned by `ReactDOM.render`. -------------------------------------------------------------------------------- /examples/async_counter/src/dispatcher.js: -------------------------------------------------------------------------------- 1 | import { on, TanokDispatcher } from 'tanok'; 2 | import Rx from '@evo/rx'; 3 | 4 | function syncEffect(cnt) { 5 | return function (stream) { 6 | fetch('http://www.mocky.io/v2/577824a4120000ca28aac904', { 7 | method: 'POST', 8 | body: cnt, 9 | }) 10 | .then((r) => r.json()) 11 | .then((json) => stream.send('syncSuccess', json)) 12 | } 13 | } 14 | 15 | 16 | export class CounterDispatcher extends TanokDispatcher { 17 | @on('init') 18 | init(payload, state) { 19 | state.count = 10; 20 | return [state]; 21 | } 22 | 23 | @on('inc') 24 | inc(payload, state) { 25 | state.count += 1; 26 | state.synced = false; 27 | 28 | return [state, syncEffect(state.count)]; 29 | } 30 | 31 | @on('dec') 32 | dec(payload, state) { 33 | state.count -= 1; 34 | state.synced = false; 35 | 36 | return [state, syncEffect(state.count)]; 37 | } 38 | 39 | @on('syncSuccess') 40 | syncSuccess(payload, state) { 41 | state.synced = true; 42 | return [state]; 43 | } 44 | 45 | @on('effectKinds') 46 | promise(payload, state) { 47 | function promiseFx(stream){ 48 | return new Promise((resolve, reject) => { 49 | stream.send('done', 'Promise done') 50 | return resolve(1); 51 | }) 52 | } 53 | 54 | function observableFx(stream) { 55 | return Rx.Observable.create((obs) => { 56 | stream.send('done', 'Observable done') 57 | obs.onNext(1); 58 | obs.onCompleted(); 59 | }) 60 | } 61 | 62 | return [state, promiseFx, observableFx]; 63 | } 64 | 65 | @on('done') 66 | done(payload, state) { 67 | console.log(payload); 68 | return [state]; 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /examples/subcomponent/src/counter/dispatcher.js: -------------------------------------------------------------------------------- 1 | import { on, TanokDispatcher } from 'tanok'; 2 | import Rx from '@evo/rx'; 3 | 4 | function syncEffect(cnt) { 5 | return function (stream) { 6 | fetch('http://www.mocky.io/v2/577824a4120000ca28aac904', { 7 | method: 'POST', 8 | body: cnt, 9 | }) 10 | .then((r) => r.json()) 11 | .then((json) => stream.send('syncSuccess', json)) 12 | } 13 | } 14 | 15 | 16 | export class CounterDispatcher extends TanokDispatcher { 17 | @on('init') 18 | init(payload, state) { 19 | state.count = 10; 20 | return [state]; 21 | } 22 | 23 | @on('inc') 24 | inc(payload, state) { 25 | state.count += 1; 26 | state.synced = false; 27 | 28 | return [state, syncEffect(state.count)]; 29 | } 30 | 31 | @on('dec') 32 | dec(payload, state) { 33 | state.count -= 1; 34 | state.synced = false; 35 | 36 | return [state, syncEffect(state.count)]; 37 | } 38 | 39 | @on('syncSuccess') 40 | syncSuccess(payload, state) { 41 | state.synced = true; 42 | return [state]; 43 | } 44 | 45 | @on('effectKinds') 46 | promise(payload, state) { 47 | function promiseFx(stream){ 48 | return new Promise((resolve, reject) => { 49 | stream.send('done', 'Promise done') 50 | return resolve(1); 51 | }) 52 | } 53 | 54 | function observableFx(stream) { 55 | return Rx.Observable.create((obs) => { 56 | stream.send('done', 'Observable done') 57 | obs.onNext(1); 58 | obs.onCompleted(); 59 | }) 60 | } 61 | 62 | return [state, promiseFx, observableFx]; 63 | } 64 | 65 | @on('done') 66 | done(payload, state) { 67 | console.log(state.id, payload); 68 | return [state]; 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /examples/counter/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 11 | 12 | 13 | 22 | React App 23 | 24 | 25 | 28 |
29 | 39 | 40 | 41 | -------------------------------------------------------------------------------- /examples/async_counter/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 11 | 12 | 13 | 22 | React App 23 | 24 | 25 | 28 |
29 | 39 | 40 | 41 | -------------------------------------------------------------------------------- /examples/search_example/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 11 | 12 | 13 | 22 | React App 23 | 24 | 25 | 28 |
29 | 39 | 40 | 41 | -------------------------------------------------------------------------------- /examples/subcomponent/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 11 | 12 | 13 | 22 | React App 23 | 24 | 25 | 28 |
29 | 39 | 40 | 41 | -------------------------------------------------------------------------------- /examples/subcomponent_with_metadata/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 11 | 12 | 13 | 22 | React App 23 | 24 | 25 | 28 |
29 | 39 | 40 | 41 | -------------------------------------------------------------------------------- /examples/middlewares/middlewares.js: -------------------------------------------------------------------------------- 1 | export function loggingMiddleware(index) { 2 | return (stream) => (next) => (state) => { 3 | console.log(`Before ${index}`); 4 | console.log('State: ', state); 5 | const actionResult = next(state); 6 | console.log(`After ${index}`); 7 | console.log('State: ', actionResult.state); 8 | console.log('Effects: ', actionResult.effects); 9 | console.log('Params: ', actionResult.params); 10 | return actionResult; 11 | } 12 | } 13 | 14 | export function tracingMiddleware() { 15 | return (stream) => (next) => (state) => { 16 | console.log(`Before`); 17 | const actionResult = next(state); 18 | 19 | const tracingId = actionResult.params.tracingId || Math.random() * 1000; 20 | console.log(`Tracing ${tracingId}`); 21 | const cloneStream = Object.assign( 22 | Object.create(Object.getPrototypeOf(stream)), stream); 23 | cloneStream.getStreamOriginalPayload = cloneStream.getStreamPayload; 24 | cloneStream.getStreamPayload = (action, payload) => { 25 | return { 26 | ...cloneStream.getStreamOriginalPayload(action, payload), 27 | tracingId, 28 | } 29 | }; 30 | 31 | console.log(`After`); 32 | actionResult.effects = actionResult.effects.map( 33 | (effect) => (innerStream) => { 34 | const shutdownActionsBefore = cloneStream.shutdownActions; 35 | const result = effect(cloneStream); 36 | const shutdownActionsAfter = cloneStream.shutdownActions; 37 | const diff = shutdownActionsAfter.filter( 38 | (i) => shutdownActionsBefore.indexOf(i) < 0 39 | ); 40 | stream.shutdownActions.push.apply(stream.shutdownActions, diff); 41 | return result; 42 | } 43 | ); 44 | return actionResult; 45 | } 46 | } -------------------------------------------------------------------------------- /src/connect/utils/Subscription.js: -------------------------------------------------------------------------------- 1 | const CLEARED = null 2 | const nullListeners = { notify() {} } 3 | 4 | function createListenerCollection() { 5 | let current = [] 6 | let next = [] 7 | 8 | return { 9 | clear() { 10 | next = CLEARED 11 | current = CLEARED 12 | }, 13 | 14 | notify() { 15 | const listeners = current = next 16 | for (let i = 0; i < listeners.length; i++) { 17 | listeners[i]() 18 | } 19 | }, 20 | 21 | get() { 22 | return next 23 | }, 24 | 25 | subscribe(listener) { 26 | let isSubscribed = true 27 | if (next === current) next = current.slice() 28 | next.push(listener) 29 | 30 | return function unsubscribe() { 31 | if (!isSubscribed || current === CLEARED) return 32 | isSubscribed = false 33 | 34 | if (next === current) next = current.slice() 35 | next.splice(next.indexOf(listener), 1) 36 | } 37 | } 38 | } 39 | } 40 | 41 | export default class Subscription { 42 | constructor(store, parentSub, onStateChange) { 43 | this.store = store 44 | this.parentSub = parentSub 45 | this.onStateChange = onStateChange 46 | this.unsubscribe = null 47 | this.listeners = nullListeners 48 | } 49 | 50 | addNestedSub(listener) { 51 | this.trySubscribe() 52 | return this.listeners.subscribe(listener) 53 | } 54 | 55 | notifyNestedSubs() { 56 | this.listeners.notify() 57 | } 58 | 59 | isSubscribed() { 60 | return Boolean(this.unsubscribe) 61 | } 62 | 63 | trySubscribe() { 64 | if (!this.unsubscribe) { 65 | this.unsubscribe = this.parentSub 66 | ? this.parentSub.addNestedSub(this.onStateChange) 67 | : this.store.subscribe(this.onStateChange) 68 | 69 | this.listeners = createListenerCollection() 70 | } 71 | } 72 | 73 | tryUnsubscribe() { 74 | if (this.unsubscribe) { 75 | this.unsubscribe() 76 | this.unsubscribe = null 77 | this.listeners.clear() 78 | this.listeners = nullListeners 79 | } 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /examples/counter/config/paths.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const path = require('path'); 4 | const fs = require('fs'); 5 | const url = require('url'); 6 | 7 | // Make sure any symlinks in the project folder are resolved: 8 | // https://github.com/facebookincubator/create-react-app/issues/637 9 | const appDirectory = fs.realpathSync(process.cwd()); 10 | const resolveApp = relativePath => path.resolve(appDirectory, relativePath); 11 | 12 | const envPublicUrl = process.env.PUBLIC_URL; 13 | 14 | function ensureSlash(path, needsSlash) { 15 | const hasSlash = path.endsWith('/'); 16 | if (hasSlash && !needsSlash) { 17 | return path.substr(path, path.length - 1); 18 | } else if (!hasSlash && needsSlash) { 19 | return `${path}/`; 20 | } else { 21 | return path; 22 | } 23 | } 24 | 25 | const getPublicUrl = appPackageJson => 26 | envPublicUrl || require(appPackageJson).homepage; 27 | 28 | // We use `PUBLIC_URL` environment variable or "homepage" field to infer 29 | // "public path" at which the app is served. 30 | // Webpack needs to know it to put the right