├── .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 | 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 | --------------------------------------------------------------------------------