├── .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 |
17 | {this.props.repos
18 | ? this.props.repos.map((repo, index) => (
19 | -
20 | {repo.name} {repo.stargazers_count} stars
21 |
22 | ))
23 | : 'Failed to load anything'
24 | }
25 |
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 | [](https://travis-ci.org/brabadu/tanok)
4 | [](https://coveralls.io/r/brabadu/tanok?branch=master)
5 | [](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