├── .babelrc
├── .eslintrc
├── .gitignore
├── .npmignore
├── CHANGELOG.md
├── README.md
├── docs
└── api.md
├── index.js
├── package.json
├── scripts
├── mocha_runner.js
└── prepublish.sh
└── src
├── components
├── DefaultErrorComponent.js
├── DefaultLoadingComponent.js
└── DummyComponent.js
├── compose.js
├── composeAll.js
├── composers
├── withObservable.js
├── withPromise.js
├── withReduxState.js
└── withTracker.js
├── helpers
├── getContext.js
├── withContext.js
├── withHandlers.js
├── withLifecycle.js
├── withState.js
└── withStateHandlers.js
├── index.js
├── utils
├── getDisplayName.js
├── inheritStatics.js
└── isReactNative.js
└── window_bind.js
/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "presets": [
3 | "es2015",
4 | "stage-0",
5 | "react"
6 | ]
7 | }
8 |
--------------------------------------------------------------------------------
/.eslintrc:
--------------------------------------------------------------------------------
1 | {
2 | "parser": "babel-eslint",
3 | "extends": "airbnb",
4 |
5 | "plugins": [
6 | "babel",
7 | "react"
8 | ],
9 |
10 | "env": {
11 | "es6": true,
12 | "node": true
13 | },
14 |
15 | "globals": {
16 | "window": true,
17 | "navigator": true
18 | },
19 |
20 | "ecmaFeatures": {
21 | "arrowFunctions": true,
22 | "binaryLiterals": true,
23 | "blockBindings": true,
24 | "classes": true,
25 | "defaultParams": true,
26 | "destructuring": true,
27 | "experimentalObjectRestSpread": true,
28 | "forOf": true,
29 | "generators": true,
30 | "globalReturn": true,
31 | "jsx": true,
32 | "modules": true,
33 | "objectLiteralComputedProperties": true,
34 | "objectLiteralDuplicateProperties": true,
35 | "objectLiteralShorthandMethods": true,
36 | "objectLiteralShorthandProperties": true,
37 | "octalLiterals": true,
38 | "regexUFlag": true,
39 | "regexYFlag": true,
40 | "restParams": true,
41 | "spread": true,
42 | "superInFunctions": true,
43 | "templateStrings": true,
44 | "unicodeCodePointEscapes": true
45 | },
46 |
47 | "rules": {
48 | "no-param-reassign": 0
49 | }
50 | }
51 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | *.swp
2 | *~
3 | *.iml
4 | .*.haste_cache.*
5 | .DS_Store
6 | .idea
7 | npm-debug.log
8 | node_modules
9 | lib
10 |
--------------------------------------------------------------------------------
/.npmignore:
--------------------------------------------------------------------------------
1 | *.swp
2 | *~
3 | *.iml
4 | .*.haste_cache.*
5 | .DS_Store
6 | .idea
7 | .babelrc
8 | .eslintrc
9 | npm-debug.log
10 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # ChangeLog
2 |
3 | ## v2.2.0
4 | 05-July-2016
5 |
6 | * Added `withStateHandlers`.
7 | * Renamed `withRedux` to `withReduxState`.
8 |
9 | ## v2.1.0
10 | 01-July-2016
11 |
12 | * Added functional React specs
13 |
14 | ## v2.0.8
15 | 20-June-2016
16 |
17 | * Better container naming.
18 |
19 | ## v2.0.7
20 | 09-June-2016
21 |
22 | * Force `composeWithTracker` to use Tracker from context.
23 |
24 | ## v2.0.6
25 | 08-June-2016
26 |
27 | * Added the ability to pass loading component and error component in `composeWithRedux`. [PR #2](https://github.com/sammkj/react-komposer-plus/pull/2)
28 |
29 | ## v2.0.2
30 | 07-June-2016
31 |
32 | * Added the ability to pass props into loading component. (PR by clayne11, see [here](https://github.com/kadirahq/react-komposer/pull/47))
33 |
34 | ## v2.0.0
35 | 04-June-2016
36 |
37 | * Separated all functions into modules. So now we can import individual function like this:
38 | ```
39 | import composeAll from 'react-komposer-plus/lib/composeAll';
40 | ```
41 | * Added `composeWithRedux`. Deprecated `react-komposer-redux`.
42 |
43 | ## v1.8.0
44 | 09-April-2016
45 |
46 | * Added support to React 15.x.x
47 |
48 | ## v1.7.2
49 | 30-March-2016
50 |
51 | * Added disableMode support for composeAll.
52 |
53 | ## v1.7.1
54 | 30-March-2016
55 |
56 | * Removed react-native from peerDependencies.
57 |
58 | ### v1.7.0
59 | 30-March-2016
60 |
61 | * Removed default loading and error components in ReactNative. User always needs to provide them.
62 | * Earlier we conditionally require react-native and use it. But, it's not going to work with Webpack as it needs RN to be available inside the project.
63 |
64 | ### v1.6.0
65 | 30-March-2016
66 |
67 | * Added a way to disable the functionality of React Komposer. See [more](https://github.com/kadirahq/react-komposer#disable-functionality).
68 |
69 | ### v1.5.0
70 | 30-March-2016
71 |
72 | * Added loading components for ReactNative. See: [PR64](https://github.com/kadirahq/react-komposer/pull/64)
73 |
74 | ### v1.4.1
75 | 16-March-2016
76 |
77 | * Removed browser flag completely where it might give us errors in Meteor.
78 |
79 | ### v1.4.0
80 | 16-March-2016
81 |
82 | * Added support for React Native. See: [PR53](https://github.com/kadirahq/react-komposer/pull/53)
83 |
84 | ### v1.3.3
85 |
86 | * Fixed some issue with Meteor's Tracker integration. See [PR49](https://github.com/kadirahq/react-komposer/pull/49)
87 |
88 | ### v1.3.2
89 |
90 | * Updated `_mounted` internal state when unmounting. See: [PR39](https://github.com/kadirahq/react-komposer/pull/39)
91 |
92 | ### v1.3.1
93 | * Fix a small typo. See: [#28](https://github.com/kadirahq/react-komposer/pull/28)
94 |
95 | ### v1.3.0
96 | * Implemented purity in containers. See: [#19](https://github.com/kadirahq/react-komposer/issues/19).
97 |
98 | ### v1.2.1
99 |
100 | * Stop wrapping the UI component with a div. See: [#15](https://github.com/kadirahq/react-komposer/issues/15)
101 |
102 | ### v1.2.0
103 |
104 | * Added custom loading and error components to all composers. See: [#12](https://github.com/kadirahq/react-komposer/pull/12)
105 |
106 | ### v1.1.0
107 |
108 | * Added `composeAll` utility.
109 | * Allow to pass custom error component and loading component as options. See: [#7](https://github.com/kadirahq/react-komposer/issues/7)
110 | * Allow to return a cleanup function from the tracker composerFunction as well. See: [#8](https://github.com/kadirahq/react-komposer/issues/8)
111 |
112 | ### v1.0.0
113 |
114 | * Initial Release
115 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # React Komposer Plus
2 |
3 | > This is fork of [React Komposer](https://github.com/kadirahq/react-komposer)
4 |
5 | Let's compose React containers and feed data into components. Supports both React and React Native.
6 |
7 | ## Installation
8 |
9 | ```
10 | npm i --save react-komposer-plus
11 | ```
12 |
13 | ## Basic Usage
14 |
15 | ```
16 | import ComponentA from 'path/to/ComponentA';
17 | import { PropTypes } from 'react';
18 | import {
19 | withHandlers,
20 | getContext,
21 | withRedux,
22 | withState,
23 | withLifecycle,
24 | composeAll,
25 | } from 'react-komposer-plus';
26 | import { useDeps } from 'mantra-plus';
27 |
28 | const mapStateToProps = ({ layout }) => ({
29 | windowWidth: layout.windowWidth,
30 | windowHeight: layout.windowHeight,
31 | windowScrollTop: layout.windowScrollTop,
32 | });
33 |
34 | export default composeAll(
35 | withLifecycle({
36 | componentWillMount() {
37 | console.log('component will mount');
38 | },
39 | componentDidMount() {
40 | console.log('component mounted');
41 | },
42 | }),
43 | withHandler({
44 | handleClickCounter: (props, event) => {
45 | props.setState({
46 | counter: props.state + 1,
47 | });
48 | },
49 | }),
50 | withRedux(mapStateToProps),
51 | withState({
52 | counter: 1,
53 | }),
54 | getContext({
55 | parentCtx: PropTypes.object,
56 | }),
57 | useDeps()
58 | )(ComponentA);
59 | ```
60 |
61 | In your functional stateless ComponentA, you can now use props to do your thing! **Please take note that props flow from bottom to top of `composeAll`**.
62 |
63 | ```
64 | export default const ComponentA = ({
65 | state,
66 | setState,
67 | windowWidth,
68 | windowHeight,
69 | windowScrollTop,
70 | handleClickCounter,
71 | }, context) => {
72 | return (
73 |
74 |
current counter value: {state.counter}
75 |
76 |
77 |
Window Information
78 |
79 | - Window Height: {windowHeight}
80 | - Window Width: {windowWidth}
81 | - Window ScrollTop: {windowScrollTop}
82 |
83 |
84 | );
85 | }
86 | ```
87 |
88 | ## Full APIs
89 |
90 | See [here](https://github.com/sammkj/react-komposer-plus/blob/master/docs/api.md)
91 |
--------------------------------------------------------------------------------
/docs/api.md:
--------------------------------------------------------------------------------
1 | # React Komposer Plus APIs
2 |
3 | ## Table of Content
4 |
5 | * **General**
6 | * [`compose`](#compose)
7 | * [`composeAll`](#composeAll)
8 |
9 | * **Component Specs and Lifecycle**
10 | * [`withContext`](#withContext)
11 | * [`getContext`](#getContext)
12 | * [`withHandlers`](#withHandlers)
13 | * [`withState`](#withState)
14 | * [`withStateHandlers`](#withStateHandlers)
15 | * [`withLifecycle`](withLifecycle)
16 |
17 | * **Other integrations**
18 | * [`withRedux`](#withRedux)
19 | * [`withTracker`](#withTracker)
20 | * [`withPromise`](#withPromise)
21 | * [`withObservable`](#withObservable)
22 |
23 | ### `compose`
24 |
25 | ```
26 | import { compose } from 'react-komposer-plus';
27 | import PostList from '../components/PostList';
28 |
29 | const composerFunction = (props, onData) => {
30 | return () => {console.log('Container disposed!');}
31 | };
32 |
33 | // Note the use of composeWithTracker
34 | const Container = compose(composerFunction)(PostList);
35 | ```
36 |
37 | ### `composeAll`
38 |
39 | ```
40 | import {
41 | composeAll,
42 | withRedux,
43 | withState,
44 | } from 'react-komposer-plus';
45 |
46 | const ComposedClock = composeAll(
47 | withRedux(mapStateToProps),
48 | withState({
49 | counter: 1,
50 | counterA: 1,
51 | counterB: 3,
52 | }),
53 | )(Counter);
54 | ```
55 |
56 | ### `withContext`
57 |
58 | ### `getContext`
59 |
60 | ```
61 | import { getContext } from 'react-komposer-plus';
62 | import { PropTypes } from 'react';
63 |
64 | const ComposedClock = getContext({
65 | time: PropTypes.string,
66 | })(Clock);
67 | ```
68 |
69 | ### `withHandlers`
70 |
71 | ```
72 | import { withHandlers } from 'react-komposer-plus';
73 |
74 | const Clock = ({ handleClick }) => (
75 |
76 | );
77 |
78 | const ComposedClock = withHandlers({
79 | handleClick: (props, event) {
80 | console.log(props);
81 | console.log(event);
82 | },
83 | })(Clock);
84 | ```
85 |
86 | ### `withState`
87 |
88 | ```
89 | import { withState } from 'react-komposer-plus';
90 |
91 | const Counter = ({
92 | customStateName,
93 | customStateSetterName,
94 | }) => (
95 | {customStateName.counter}
96 | );
97 |
98 | const initialState = {
99 | counter: 1,
100 | }
101 | const ComposedCounter = withState(initialState, 'customStateName', 'customStateSetterName')(Counter);
102 | ```
103 |
104 | ### `withStateHandlers`
105 |
106 | ```
107 | import {
108 | withState,
109 | withStateHandlers,
110 | composeAll,
111 | } from 'react-komposer-plus';
112 |
113 | const Clock = ({ handleClickCounter }) => (
114 |
115 | );
116 |
117 | const ComposedClock = composeAll(
118 | withStateHandlers({
119 | handleClick: (state, props) {
120 | // do something with state and props
121 |
122 | // return a state object
123 | return {
124 | time: state.time + 60,
125 | };
126 | },
127 | }),
128 | withState({
129 | time: 0,
130 | counter: 1,
131 | })
132 | )(Clock);
133 | ```
134 |
135 | ### `withLifecycle`
136 |
137 | ```
138 | import { withLifecycle } from 'react-komposer-plus';
139 |
140 | const ComposedCounter = withLifecycle({
141 | componentWillMount() {
142 | console.log('component will mount');
143 | },
144 | componentDidMount() {
145 | console.log('component mounted');
146 | },
147 | })(Counter);
148 | ```
149 |
150 | ### `withRedux`
151 |
152 | ```
153 | import { withRedux } from 'react-komposer-plus';
154 |
155 | // clock is from reducer
156 | const mapStateToProps = ({ clock }) => ({
157 | time: clock.time,
158 | })
159 |
160 | const ComposedClock = composeWithRedux(mapStateToProps)(Clock);
161 | ```
162 |
163 | ### `withTracker`
164 |
165 | ```
166 | import { withTracker } from 'react-komposer-plus';
167 | import PostList from '../components/PostList';
168 |
169 | const composerFunction = (props, onData) => {
170 | // tracker related code
171 | return () => {console.log('Container disposed!');}
172 | };
173 |
174 | // Note the use of composeWithTracker
175 | const Container = composeWithTracker(composerFunction)(PostList);
176 | ```
177 |
178 | ### `withPromise`
179 |
180 | ```
181 | import { withPromise } from 'react-komposer-plus'
182 |
183 | // Create a component to display Time
184 | const Time = ({time}) => ({time}
);
185 |
186 | // Assume this get's the time from the Server
187 | const getServerTime = () => {
188 | return new Promise((resolve) => {
189 | const time = new Date().toString();
190 | setTimeout(() => resolve({time}), 2000);
191 | });
192 | };
193 |
194 | // Create the composer function and tell how to fetch data
195 | const composerFunction = (props) => {
196 | return getServerTime();
197 | };
198 |
199 | // Compose the container
200 | const Clock = composeWithPromise(composerFunction)(Time, Loading);
201 | ```
202 |
203 | ### `withObservable`
204 |
205 | ```
206 | import { withObservable } from 'react-komposer-plus'
207 |
208 | // Create a component to display Time
209 | const Time = ({time}) => ({time}
);
210 |
211 | const now = Rx.Observable.interval(1000)
212 | .map(() => ({time: new Date().toString()}));
213 |
214 | // Create the composer function and tell how to fetch data
215 | const composerFunction = (props) => now;
216 |
217 | // Compose the container
218 | const Clock = composeWithObservable(composerFunction)(Time);
219 | ```
220 |
--------------------------------------------------------------------------------
/index.js:
--------------------------------------------------------------------------------
1 | module.exports = require('./lib/index');
2 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "react-komposer-plus",
3 | "version": "2.2.3",
4 | "description": "Compose React containers and feed data into components.",
5 | "main": "index.js",
6 | "react-native": "index.js",
7 | "repository": {
8 | "type": "git",
9 | "url": "https://github.com/sammkj/react-komposer-plus"
10 | },
11 | "license": "MIT",
12 | "options": {
13 | "mocha": "--require scripts/mocha_runner lib/**/__tests__/**/*.js"
14 | },
15 | "scripts": {
16 | "prepublish": ". ./scripts/prepublish.sh",
17 | "lint": "eslint ./lib",
18 | "lintfix": "eslint ./lib --fix",
19 | "testonly": "mocha $npm_package_options_mocha",
20 | "test": "npm run lint && npm run testonly",
21 | "test-watch": "npm run testonly -- --watch"
22 | },
23 | "devDependencies": {
24 | "babel-cli": "^6.9.0",
25 | "babel-core": "^6.9.1",
26 | "babel-eslint": "^4.1.8",
27 | "babel-plugin-transform-runtime": "^6.9.0",
28 | "babel-polyfill": "^6.9.1",
29 | "babel-preset-es2015": "^6.9.0",
30 | "babel-preset-react": "^6.5.0",
31 | "babel-preset-stage-0": "^6.5.0",
32 | "browserify": "12.x.x",
33 | "chai": "3.x.x",
34 | "enzyme": "^2.2.0",
35 | "eslint": "^2.11.1",
36 | "eslint-config-airbnb": "^6.1.0",
37 | "eslint-plugin-babel": "^2.2.0",
38 | "eslint-plugin-react": "^5.1.1",
39 | "exposify": "0.5.x",
40 | "mocha": "2.x.x",
41 | "nodemon": "1.7.x",
42 | "react": "^15.0.0",
43 | "react-addons-test-utils": "^15.0.0",
44 | "react-dom": "^15.0.0",
45 | "rx": "4.x.x",
46 | "uglifyify": "3.x.x"
47 | },
48 | "peerDependencies": {
49 | "react": "^0.14.3 || ^15.0.0"
50 | },
51 | "dependencies": {
52 | "babel-runtime": "6.x.x",
53 | "hoist-non-react-statics": "1.x.x",
54 | "invariant": "2.x.x",
55 | "lodash.omit": "^4.3.0",
56 | "shallowequal": "0.2.x"
57 | }
58 | }
59 |
--------------------------------------------------------------------------------
/scripts/mocha_runner.js:
--------------------------------------------------------------------------------
1 | require('babel-core/register');
2 | require('babel-polyfill');
3 |
4 | process.on('unhandledRejection', function (error) {
5 | console.error('Unhandled Promise Rejection:');
6 | console.error(error && error.stack || error);
7 | });
8 |
--------------------------------------------------------------------------------
/scripts/prepublish.sh:
--------------------------------------------------------------------------------
1 | echo "> Start transpiling ES2015"
2 | echo ""
3 | rm -rf ./lib
4 | babel --plugins "transform-runtime" src --ignore __tests__ --out-dir ./lib
5 | cd lib
6 | browserify --debug --ignore-missing -t [ exposify --expose [ --react React ] ] ./window_bind.js > ./browser.js
7 | cat ./browser.js | uglifyjs -c > ./browser.min.js
8 | echo ""
9 | echo "> Complete transpiling ES2015"
10 |
--------------------------------------------------------------------------------
/src/components/DefaultErrorComponent.js:
--------------------------------------------------------------------------------
1 | import React, { PropTypes } from 'react';
2 |
3 | const propTypes = {
4 | error: PropTypes.object,
5 | };
6 |
7 | const DefaultErrorComponent = ({ error }) => {
8 | const textStyle = {
9 | marginTop: 20,
10 | color: 'red',
11 | };
12 |
13 | const formattedError = `${error.message} \n${error.stack}`;
14 | return (
15 | {formattedError}
16 | );
17 | };
18 |
19 | DefaultErrorComponent.propTypes = propTypes;
20 |
21 | export default DefaultErrorComponent;
22 |
--------------------------------------------------------------------------------
/src/components/DefaultLoadingComponent.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | const DefaultLoadingComponent = () => {
4 | const loadingText = 'Loading...';
5 | return ({loadingText}
);
6 | };
7 |
8 | export default DefaultLoadingComponent;
9 |
--------------------------------------------------------------------------------
/src/components/DummyComponent.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | const DummyComponent = () => (null);
4 |
5 | export default DummyComponent;
6 |
--------------------------------------------------------------------------------
/src/compose.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import invariant from 'invariant';
3 | import shallowEqual from 'shallowequal';
4 | import inheritStatics from './utils/inheritStatics';
5 | import isReactNative from './utils/isReactNative';
6 |
7 | import DefaultLoadingComponent from './components/DefaultLoadingComponent';
8 | import DefaultErrorComponent from './components/DefaultErrorComponent';
9 |
10 | function compose(fn, L1, E1, options = { pure: true, displayName: 'Container' }) {
11 | return (ChildComponent, L2, E2) => {
12 | invariant(
13 | Boolean(ChildComponent),
14 | 'Should provide a child component to build the higher order container.'
15 | );
16 |
17 | if (isReactNative()) {
18 | invariant(
19 | L1 || L2,
20 | 'Should provide a loading component in ReactNative.'
21 | );
22 |
23 | invariant(
24 | E1 || E2,
25 | 'Should provide a error handling component in ReactNative.'
26 | );
27 | }
28 |
29 | const LoadingComponent = L1 || L2 || DefaultLoadingComponent;
30 | const ErrorComponent = E1 || E2 || DefaultErrorComponent;
31 |
32 | // If this is disabled, we simply need to return the DummyComponent
33 | /*
34 | if (disableMode) {
35 | return inheritStatics(DummyComponent, ChildComponent);
36 | }
37 | */
38 |
39 | const Container = class extends React.Component {
40 | constructor(props, context) {
41 | super(props, context);
42 |
43 | this.state = {};
44 |
45 | // XXX: In the server side environment, we need to
46 | // stop the subscription right away. Otherwise, it's a starting
47 | // point to huge subscription leak.
48 | this._subscribe(props);
49 | }
50 |
51 | componentDidMount() {
52 | this._mounted = true;
53 | }
54 |
55 | componentWillReceiveProps(props) {
56 | this._subscribe(props);
57 | }
58 |
59 | componentWillUnmount() {
60 | this._mounted = false;
61 | this._unsubscribe();
62 | }
63 |
64 | shouldComponentUpdate(nextProps, nextState) {
65 | if (!options.pure) {
66 | return true;
67 | }
68 |
69 | return (
70 | !shallowEqual(this.props, nextProps) ||
71 | this.state.error !== nextState.error ||
72 | !shallowEqual(this.state.payload, nextState.payload)
73 | );
74 | }
75 |
76 | render() {
77 | const error = this._getError();
78 | const loading = this._isLoading();
79 |
80 | if (error) {
81 | return ();
82 | }
83 |
84 | if (loading) {
85 | return ();
86 | }
87 |
88 | return ();
89 | }
90 |
91 | _subscribe(props) {
92 | this._unsubscribe();
93 |
94 | this._stop = fn(props, (error, payload) => {
95 | if (error) {
96 | invariant(
97 | error.message && error.stack,
98 | 'Passed error should be an instance of an Error.'
99 | );
100 | }
101 |
102 | const state = { error, payload };
103 |
104 | if (this._mounted) {
105 | this.setState(state);
106 | } else {
107 | this.state = state;
108 | }
109 | });
110 | }
111 |
112 | _unsubscribe() {
113 | if (this._stop) {
114 | this._stop();
115 | }
116 | }
117 |
118 | _getProps() {
119 | const {
120 | payload = {},
121 | } = this.state;
122 |
123 | const props = {
124 | ...this.props,
125 | ...payload,
126 | };
127 |
128 | return props;
129 | }
130 |
131 | _getError() {
132 | const { error } = this.state;
133 |
134 | return error;
135 | }
136 |
137 | _isLoading() {
138 | const { payload } = this.state;
139 | return !Boolean(payload);
140 | }
141 | };
142 |
143 | return inheritStatics(Container, ChildComponent, options.displayName);
144 | };
145 | }
146 |
147 | export default compose;
148 |
--------------------------------------------------------------------------------
/src/composeAll.js:
--------------------------------------------------------------------------------
1 | // utility function to compose multiple composers at once.
2 | function composeAll(...composers) {
3 | return function buildFinalComponent(BaseComponent) {
4 | /*
5 | if (disableMode) {
6 | return DummyComponent;
7 | }
8 | */
9 |
10 | if (BaseComponent === null || BaseComponent === undefined) {
11 | throw new Error('Curry function of composeAll needs an input.');
12 | }
13 |
14 | let finalComponent = BaseComponent;
15 | composers.forEach(composer => {
16 | if (typeof composer !== 'function') {
17 | throw new Error('Composer should be a function.');
18 | }
19 |
20 | finalComponent = composer(finalComponent);
21 |
22 | if (finalComponent === null || finalComponent === undefined) {
23 | throw new Error('Composer function should return a value.');
24 | }
25 | });
26 |
27 | return finalComponent;
28 | };
29 | }
30 |
31 | export default composeAll;
32 |
--------------------------------------------------------------------------------
/src/composers/withObservable.js:
--------------------------------------------------------------------------------
1 | import compose from '../compose';
2 | import invariant from 'invariant';
3 |
4 | function composeWithObservable(fn, L, E, options = { displayName: 'WithObservable' }) {
5 | const onPropsChange = (props, sendData) => {
6 | const observable = fn(props);
7 | invariant(
8 | typeof observable.subscribe === 'function',
9 | 'Should return an observable from the callback of `composeWithObservable`'
10 | );
11 |
12 | sendData();
13 | const onData = data => {
14 | invariant(
15 | typeof data === 'object',
16 | 'Should return a plain object from the promise'
17 | );
18 | const clonedData = { ...data };
19 | sendData(null, clonedData);
20 | };
21 |
22 | const onError = err => {
23 | sendData(err);
24 | };
25 |
26 | const sub = observable.subscribe(onData, onError);
27 | return sub.completed.bind(sub);
28 | };
29 |
30 | return compose(onPropsChange, L, E, options);
31 | }
32 |
33 | export default composeWithObservable;
34 |
--------------------------------------------------------------------------------
/src/composers/withPromise.js:
--------------------------------------------------------------------------------
1 | import compose from '../compose';
2 | import invariant from 'invariant';
3 |
4 | function composeWithPromise(fn, L, E, options = { displayName: 'WithPromise' }) {
5 | const onPropsChange = (props, onData) => {
6 | const promise = fn(props);
7 | invariant(
8 | (typeof promise.then === 'function') &&
9 | (typeof promise.catch === 'function'),
10 | 'Should return a promise from the callback of `composeWithPromise`'
11 | );
12 |
13 | onData();
14 | promise
15 | .then(data => {
16 | invariant(
17 | typeof data === 'object',
18 | 'Should return a plain object from the promise'
19 | );
20 | const clonedData = { ...data };
21 | onData(null, clonedData);
22 | })
23 | .catch(err => {
24 | onData(err);
25 | });
26 | };
27 |
28 | return compose(onPropsChange, L, E, options);
29 | }
30 |
31 | export default composeWithPromise;
32 |
--------------------------------------------------------------------------------
/src/composers/withReduxState.js:
--------------------------------------------------------------------------------
1 | import compose from '../compose';
2 |
3 | function composeReduxBase(fn, props, onData) {
4 | if (!props.context) {
5 | throw new Error('No context passed as prop.');
6 | }
7 |
8 | const context = typeof props.context === 'function' ? props.context() : props.context;
9 | const Store = context.Store || context.store;
10 |
11 | if (!Store) {
12 | throw new Error('No store found in the context');
13 | }
14 |
15 | const processState = () => {
16 | try {
17 | const state = Store.getState();
18 | const data = fn(state, props);
19 | onData(null, data);
20 | } catch (ex) {
21 | onData(ex);
22 | }
23 | };
24 |
25 | processState();
26 | Store.subscribe(processState);
27 | }
28 |
29 | export default function composeWithRedux(fn, L1, E1, options = { displayName: 'WithRedux' }) {
30 | return compose(composeReduxBase.bind(null, fn), L1, E1, options);
31 | }
32 |
--------------------------------------------------------------------------------
/src/composers/withTracker.js:
--------------------------------------------------------------------------------
1 | import compose from '../compose';
2 |
3 | function composeWithTracker(reactiveFn, L, E, options = { displayName: 'WithTracker' }) {
4 | const onPropsChange = (props, onData) => {
5 | if (!props.context) {
6 | throw new Error('No context passed as prop.');
7 | }
8 |
9 | const context = typeof props.context === 'function' ? props.context() : props.context;
10 | const Tracker = context.Tracker || context.tracker || context.Trackr;
11 |
12 | if (!Tracker) {
13 | throw new Error('No Tracker found in the context');
14 | }
15 |
16 | let trackerCleanup;
17 | const handler = Tracker.nonreactive(() => (
18 | Tracker.autorun(() => {
19 | trackerCleanup = reactiveFn(props, onData);
20 | })
21 | ));
22 |
23 | return () => {
24 | if (typeof (trackerCleanup) === 'function') {
25 | trackerCleanup();
26 | }
27 | return handler.stop();
28 | };
29 | };
30 |
31 | return compose(onPropsChange, L, E, options);
32 | }
33 |
34 | export default composeWithTracker;
35 |
--------------------------------------------------------------------------------
/src/helpers/getContext.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import hoistNonReactStatic from 'hoist-non-react-statics';
3 | import getDisplayName from '../utils/getDisplayName';
4 |
5 | function composeGetContext(contextTypes) {
6 | return (ChildComponent) => {
7 | class GetContext extends Component {
8 | render() {
9 | return ();
10 | }
11 | }
12 |
13 | GetContext.contextTypes = contextTypes;
14 | GetContext.displayName = `GetContext(${getDisplayName(ChildComponent)})`;
15 | return hoistNonReactStatic(GetContext, ChildComponent);
16 | };
17 | }
18 |
19 | export default composeGetContext;
20 |
--------------------------------------------------------------------------------
/src/helpers/withContext.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import hoistNonReactStatic from 'hoist-non-react-statics';
3 | import getDisplayName from '../utils/getDisplayName';
4 |
5 | function composeWithContext(childContextTypes, getChildContext) {
6 | return (ChildComponent) => {
7 | class WithContext extends Component {
8 | getChildContext() {
9 | getChildContext(this.props);
10 | }
11 |
12 | render() {
13 | return ();
14 | }
15 | }
16 |
17 | WithContext.childContextTypes = childContextTypes;
18 | WithContext.displayName = `WithContext(${getDisplayName(ChildComponent)})`;
19 |
20 | return hoistNonReactStatic(WithContext, ChildComponent);
21 | };
22 | }
23 |
24 | export default composeWithContext;
25 |
--------------------------------------------------------------------------------
/src/helpers/withHandlers.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import hoistNonReactStatic from 'hoist-non-react-statics';
3 | import getDisplayName from '../utils/getDisplayName';
4 |
5 | const mapHandlers = (handlers, func) => {
6 | const result = {};
7 |
8 | /* eslint-disable no-restricted-syntax */
9 | for (const key in handlers) {
10 | if (handlers.hasOwnProperty(key)) {
11 | result[key] = func(handlers[key], key);
12 | }
13 | }
14 |
15 | return result;
16 | };
17 |
18 | function composeWithHandlers(handlers) {
19 | return (ChildComponent) => {
20 | class WithHandlers extends Component {
21 | componentWillReceiveProps() {
22 | this.cachedHandlers = {};
23 | }
24 |
25 | cachedHandlers = {};
26 |
27 | handlers = mapHandlers(handlers, (createHandler, handlerName) => (...args) => {
28 | const cachedHandler = this.cachedHandlers[handlerName];
29 | if (cachedHandler) {
30 | return cachedHandler(this.props, ...args);
31 | }
32 |
33 | const handler = createHandler;
34 | this.cachedHandlers[handlerName] = handler;
35 |
36 | if (typeof handler !== 'function') {
37 | const message = 'withHandlers(): Expected a function.';
38 | throw new Error(message);
39 | }
40 |
41 | return handler(this.props, ...args);
42 | })
43 |
44 | render() {
45 | return ();
46 | }
47 | }
48 |
49 | WithHandlers.displayName = `WithHandlers(${getDisplayName(ChildComponent)})`;
50 |
51 | return hoistNonReactStatic(WithHandlers, ChildComponent);
52 | };
53 | }
54 |
55 | export default composeWithHandlers;
56 |
--------------------------------------------------------------------------------
/src/helpers/withLifecycle.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import omit from 'lodash.omit';
3 | import hoistNonReactStatic from 'hoist-non-react-statics';
4 | import getDisplayName from '../utils/getDisplayName';
5 |
6 | function withLifecycle(specs) {
7 | return (ChildComponent) => {
8 | const cleanSpecs = omit(specs, ['render']);
9 |
10 | const WithLifecycle = React.createClass({
11 | ...cleanSpecs,
12 | render() {
13 | return ();
14 | },
15 | });
16 |
17 | WithLifecycle.displayName = `WithLifecycle(${getDisplayName(ChildComponent)})`;
18 | return hoistNonReactStatic(WithLifecycle, ChildComponent);
19 | };
20 | }
21 |
22 | export default withLifecycle;
23 |
--------------------------------------------------------------------------------
/src/helpers/withState.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import hoistNonReactStatic from 'hoist-non-react-statics';
3 | import getDisplayName from '../utils/getDisplayName';
4 |
5 | function composeWithState(
6 | initialState,
7 | stateName = 'state',
8 | stateSetterName = 'setState'
9 | ) {
10 | return (ChildComponent) => {
11 | class WithState extends Component {
12 | state = {
13 | ...(typeof initialState === 'function' ? initialState(this.props) : initialState),
14 | };
15 |
16 | setStateValue = (updateFn, callback) => (
17 | this.setState((previousState, currentProps) => ({
18 | ...(typeof updateFn === 'function' ? updateFn(previousState, currentProps) : updateFn),
19 | }), callback)
20 | )
21 |
22 | render() {
23 | const stateProps = {
24 | [stateName]: this.state,
25 | [stateSetterName]: this.setStateValue,
26 |
27 | // this is required for withStateHandlers()
28 | [`__stateSetterNameFor(${stateName})`]: stateSetterName,
29 | };
30 |
31 | return (
32 | );
33 | }
34 | }
35 |
36 | WithState.displayName = `WithState(${getDisplayName(ChildComponent)})`;
37 | return hoistNonReactStatic(WithState, ChildComponent);
38 | };
39 | }
40 |
41 | export default composeWithState;
42 |
--------------------------------------------------------------------------------
/src/helpers/withStateHandlers.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import hoistNonReactStatic from 'hoist-non-react-statics';
3 | import getDisplayName from '../utils/getDisplayName';
4 |
5 | const mapHandlers = (handlers, func) => {
6 | const result = {};
7 |
8 | /* eslint-disable no-restricted-syntax */
9 | for (const key in handlers) {
10 | if (handlers.hasOwnProperty(key)) {
11 | result[key] = func(handlers[key], key);
12 | }
13 | }
14 |
15 | return result;
16 | };
17 |
18 | function composeWithStateHandlers(
19 | handlers,
20 | stateName = 'state',
21 | stateSetterName = 'setState',
22 | ) {
23 | return (ChildComponent) => {
24 | class WithStateHandlers extends Component {
25 | componentWillReceiveProps() {
26 | this.cachedHandlers = {};
27 | }
28 |
29 | cachedHandlers = {};
30 | currentState = this.props[stateName] || {};
31 | updateState = this.props[stateSetterName] ||
32 | this.props[this.props[`__stateSetterNameFor(${stateName})`]] ||
33 | null;
34 |
35 | handlers = mapHandlers(handlers, (createNewState, handlerName) => {
36 | const cachedHandler = this.cachedHandlers[handlerName];
37 | if (cachedHandler) {
38 | return cachedHandler;
39 | }
40 |
41 | const createHandler = () => {
42 | const newState = createNewState.call(null, this.currentState, this.props);
43 | return this.updateState(newState);
44 | };
45 |
46 | const handler = createHandler;
47 | this.cachedHandlers[handlerName] = handler;
48 |
49 | if (typeof handler !== 'function') {
50 | const message = 'withStateHandlers(): Expected a function.';
51 | throw new Error(message);
52 | }
53 |
54 | return handler;
55 | })
56 |
57 | render() {
58 | return ();
59 | }
60 | }
61 |
62 | WithStateHandlers.displayName = `WithStateHandlers(${getDisplayName(ChildComponent)})`;
63 |
64 | return hoistNonReactStatic(WithStateHandlers, ChildComponent);
65 | };
66 | }
67 |
68 | export default composeWithStateHandlers;
69 |
--------------------------------------------------------------------------------
/src/index.js:
--------------------------------------------------------------------------------
1 | import compose from './compose';
2 | import composeAll from './composeAll';
3 |
4 | /* composers */
5 | import withObservable from './composers/withObservable';
6 | import withPromise from './composers/withPromise';
7 | import withReduxState from './composers/withReduxState';
8 | import withTracker from './composers/withTracker';
9 |
10 | /* helpers */
11 | import getContext from './helpers/getContext';
12 | import withContext from './helpers/withContext';
13 | import withHandlers from './helpers/withHandlers';
14 | import withLifecycle from './helpers/withLifecycle';
15 | import withState from './helpers/withState';
16 | import withStateHandlers from './helpers/withStateHandlers';
17 |
18 | export {
19 | compose,
20 | composeAll,
21 | withObservable as composeWithObservable,
22 | withPromise as composeWithPromise,
23 | withReduxState as composeWithRedux,
24 | withTracker as composeWithTracker,
25 | withObservable,
26 | withPromise,
27 | withReduxState,
28 | withReduxState as withRedux, // to be removed
29 | withTracker,
30 | getContext,
31 | withContext,
32 | withHandlers,
33 | withLifecycle,
34 | withState,
35 | withStateHandlers,
36 | };
37 |
--------------------------------------------------------------------------------
/src/utils/getDisplayName.js:
--------------------------------------------------------------------------------
1 | const getDisplayName = Component => (
2 | Component.displayName || Component.name || 'Component'
3 | );
4 |
5 | export default getDisplayName;
6 |
--------------------------------------------------------------------------------
/src/utils/inheritStatics.js:
--------------------------------------------------------------------------------
1 | import hoistStatics from 'hoist-non-react-statics';
2 |
3 | export default function inheritStatics(Container, ChildComponent, displayName = 'Container') {
4 | const childDisplayName = ChildComponent.displayName || ChildComponent.name || 'ChildComponent';
5 |
6 | Container.displayName = `${displayName}(${childDisplayName})`;
7 | return hoistStatics(Container, ChildComponent);
8 | }
9 |
--------------------------------------------------------------------------------
/src/utils/isReactNative.js:
--------------------------------------------------------------------------------
1 | export default function isReactNative() {
2 | if (typeof navigator !== 'undefined' && navigator.product === 'ReactNative') {
3 | return true;
4 | }
5 |
6 | return false;
7 | }
8 |
--------------------------------------------------------------------------------
/src/window_bind.js:
--------------------------------------------------------------------------------
1 | if (typeof window !== 'undefined') {
2 | window.ReactKomposer = require('./index');
3 | }
4 |
--------------------------------------------------------------------------------