├── .babelrc ├── .codeclimate.yml ├── .editorconfig ├── .eslintignore ├── .eslintrc ├── .gitignore ├── .travis.yml ├── CHANGELOG.md ├── LICENSE ├── README.md ├── docs ├── api.md └── design-guideline.md ├── examples └── gh-pages │ ├── Client.webpackConfig.js │ ├── Server.webpackConfig.js │ ├── WebWorker.webpackConfig.js │ ├── package.json │ ├── src │ ├── ReactRoot.js │ ├── SimpleComponent.Componentize.js │ ├── SimpleComponent.ReduxComponentMixin.js │ ├── SimpleComponent.createDispatch.js │ ├── client.js │ ├── randomPromise.js │ └── server.js │ └── views │ └── index.html.js ├── lib ├── __tests__ │ └── setup.js ├── components │ ├── ComponentizeCreator.js │ ├── ReduxComponentMixin.js │ ├── __tests__ │ │ ├── Componentize.spec.js │ │ ├── ReduxComponentMixin.spec.js │ │ └── createDispatch.spec.js │ └── createDispatch.js └── index.js ├── package.json └── src ├── __tests__ └── setup.js ├── components ├── ComponentizeCreator.js ├── ReduxComponentMixin.js ├── __tests__ │ ├── Componentize.spec.js │ ├── ReduxComponentMixin.spec.js │ └── createDispatch.spec.js └── createDispatch.js └── index.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "test": { 4 | "plugins": [ 5 | "typecheck" 6 | ] 7 | } 8 | }, 9 | "plugins": [ 10 | "transform-flow-comments" 11 | ], 12 | "presets": [ 13 | "es2015", 14 | "stage-0", 15 | "react" 16 | ] 17 | } 18 | -------------------------------------------------------------------------------- /.codeclimate.yml: -------------------------------------------------------------------------------- 1 | # This is a sample .codeclimate.yml configured for Engine analysis on Code 2 | # Climate Platform. For an overview of the Code Climate Platform, see here: 3 | # http://docs.codeclimate.com/article/300-the-codeclimate-platform 4 | 5 | # Under the engines key, you can configure which engines will analyze your repo. 6 | # Each key is an engine name. For each value, you need to specify enabled: true 7 | # to enable the engine as well as any other engines-specific configuration. 8 | 9 | # For more details, see here: 10 | # http://docs.codeclimate.com/article/289-configuring-your-repository-via-codeclimate-yml#platform 11 | 12 | # For a list of all available engines, see here: 13 | # http://docs.codeclimate.com/article/296-engines-available-engines 14 | 15 | engines: 16 | # to turn on an engine, add it here and set enabled to `true` 17 | # to turn off an engine, set enabled to `false` or remove it 18 | eslint: 19 | enabled: true 20 | 21 | # Engines can analyze files and report issues on them, but you can separately 22 | # decide which files will receive ratings based on those issues. This is 23 | # specified by path patterns under the ratings key. 24 | 25 | # For more details see here: 26 | # http://docs.codeclimate.com/article/289-configuring-your-repository-via-codeclimate-yml#platform 27 | 28 | ratings: 29 | paths: 30 | - src/** 31 | 32 | # You can globally exclude files from being analyzed by any engine using the 33 | # exclude_paths key. 34 | 35 | exclude_paths: 36 | - lib/** 37 | - public/** 38 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig is awesome: http://EditorConfig.org 2 | 3 | # top-most EditorConfig file 4 | root = true 5 | 6 | # Unix-style newlines with a newline ending every file 7 | [*] 8 | end_of_line = lf 9 | insert_final_newline = true 10 | 11 | # Matches multiple files with brace expansion notation 12 | # Set default charset 13 | [*.{js,py}] 14 | charset = utf-8 15 | 16 | # 4 space indentation 17 | [*.py] 18 | indent_style = space 19 | indent_size = 4 20 | 21 | # Tab indentation (no size specified) 22 | [Makefile] 23 | indent_style = tab 24 | 25 | # Indentation override for all JS under lib directory 26 | [*.js] 27 | indent_style = space 28 | indent_size = 2 29 | 30 | # Matches the exact files either package.json or .travis.yml 31 | [{package.json,.travis.yml}] 32 | indent_style = space 33 | indent_size = 2 34 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | **/node_modules/** 2 | lib/** 3 | public/** 4 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "parser": "babel-eslint", 3 | "extends": "airbnb", 4 | "env": { 5 | "browser": true, 6 | "mocha": true, 7 | "node": true, 8 | }, 9 | "ecmaFeatures": { 10 | "arrowFunctions": true, 11 | "classes": true, 12 | "modules": true, 13 | "jsx": true, 14 | }, 15 | "plugins": [ 16 | "react" 17 | ], 18 | "rules": { 19 | // Possible Errors 20 | "comma-dangle": [2, "always-multiline"], 21 | // Best Practices 22 | "consistent-return": [0], 23 | "no-else-return": [0], 24 | "yoda": [2, "never", {"exceptRange": true}], 25 | // Variables 26 | "no-use-before-define": [2, "nofunc"], 27 | "no-unused-vars": [2, {"args": "none"}], 28 | // Stylistic Issues 29 | "camelcase": [0], 30 | "no-underscore-dangle": [0], 31 | "quotes": [2, "backtick"], 32 | // React extensions 33 | "react/sort-comp": [1], 34 | "react/prop-types": [1], 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Created by http://www.gitignore.io 2 | 3 | ### Editor 4 | *.swp 5 | 6 | ### Node ### 7 | # Logs 8 | logs 9 | *.log 10 | 11 | # Runtime data 12 | pids 13 | *.pid 14 | *.seed 15 | 16 | # OSX 17 | .DS_Store 18 | 19 | # Directory for instrumented libs generated by jscoverage/JSCover 20 | lib-cov 21 | 22 | # Coverage directory used by tools like istanbul 23 | coverage 24 | 25 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 26 | .grunt 27 | 28 | # Compiled binary addons (http://nodejs.org/api/addons.html) 29 | build/Release 30 | 31 | # Dependency directory 32 | # Commenting this out is preferred by some people, see 33 | # https://npmjs.org/doc/faq.html#Should-I-check-my-node_modules-folder-into-git 34 | node_modules 35 | 36 | # Users Environment Variables 37 | .lock-wscript 38 | 39 | 40 | ### Project ### 41 | 42 | .module-cache 43 | public/assets 44 | public/index.html 45 | public/*.gz 46 | tmp/* 47 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - '4' 4 | script: 5 | - npm run test:cov 6 | after_script: 7 | - if [[ `node --version` == *v4* ]]; then cat ./coverage/lcov.info | ./node_modules/.bin/codeclimate-test-reporter; 8 | fi 9 | env: 10 | global: 11 | secure: hoegzX+4E81skdHJMxnSJxW8rLy5M5mmGm6pTjfr1qxvcMJz2NFuGTbHQ1ukxKPUpa6JHnL86pLpJ3WKWLGWjvcaBJ8zFdPS5XYNhfCgT+EsGPZ1pAHNX/9oCqdaZeDeQKq3KCog4uAfR36mvHWfgM57EL3TN5ecq/5E9Q4Z41p4nrQsf1SLjeNw5y/jfD86HaBiHaqa2qWiZVoB25jsdv/WeoLDUnAK5TjuDUmH1te3EIDJ+wYIO2jNY/TAP9ijY0TMswK/wb1r0IrQHsARLbrP5qV541pGf8N9t+9bfdLaQngmu/jA2WKVJ6Fijb93RVXLks9n9wPyyzBa9tLtbE4ct1+pZQNjk97RVDP1LXFE6kP6Ex1AKC6mhYa3LAgqFgxeREerfsnqoVZaSQNqJ74vqd5ODsKvddQLozDIz+Ak+uCBQYv533RWRwCUhco8XP+uiZMnPBsc9vwVQkp7m+9Z6r3lDFSiHu4t10T4TVY1XZMgT69rWOYNzayAI54g/BNWHFQF/aJqVrxPJPsijLAftUcui3X2BRg5TYcVKSqO5BFXJO0doy+RT/ClW/IQgDdBayKJxlieJPUd5zcuX6+lAvvxdezEbiE24CVBc6JB0jZms3MxAAWQDMPKHgjGIZs2dcPSosrwFNUv3dh62/ncSpPMG+TobKnsxp/4isc= 12 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | 2 | # [0.3.0](https://github.com/tomchentw/redux-component/compare/v0.2.0...v0.3.0) (2016-01-25) 3 | 4 | 5 | ### Features 6 | 7 | * **lib:** compile from src using babel ([0177a43](https://github.com/tomchentw/redux-component/commit/0177a43)) 8 | 9 | 10 | ### BREAKING CHANGES 11 | 12 | * lib: Remove commonjs (module.exports) support 13 | 14 | 15 | 16 | 17 | # [0.2.0](https://github.com/tomchentw/redux-component/compare/v0.1.0...v0.2.0) (2015-11-21) 18 | 19 | 20 | ### Features 21 | 22 | * **createDispatch:** expose dispatch in Component's constructor ([a114666](https://github.com/tomchentw/redux-component/commit/a114666)) 23 | * **ReduxComponentMixin:** createDispatch alias for React.createClass ([d710669](https://github.com/tomchentw/redux-component/commit/d710669)) 24 | 25 | 26 | 27 | 28 | ## 0.1.0 (2015-09-27) 29 | 30 | 31 | #### Bug Fixes 32 | 33 | * **Componentize:** lifecycle action should accept current props as _1 ([c4f0d8b4](https://github.com/tomchentw/redux-component/commit/c4f0d8b4)) 34 | 35 | 36 | #### Features 37 | 38 | * **Componentize:** 39 | * clean up in componentWillUnmount ([c16c0adf](https://github.com/tomchentw/redux-component/commit/c16c0adf)) 40 | * action will trigger a setState call ([a66e948b](https://github.com/tomchentw/redux-component/commit/a66e948b)) 41 | * make sure render works as expected ([1f2227df](https://github.com/tomchentw/redux-component/commit/1f2227df)) 42 | * add mapDispatchToActions to _4 ([58a0f1b0](https://github.com/tomchentw/redux-component/commit/58a0f1b0)) 43 | * pass in lifecycle arguments to action creators ([5983d537](https://github.com/tomchentw/redux-component/commit/5983d537)) 44 | * invoke functions return by mapDispatchToLifecycle ([27200f7f](https://github.com/tomchentw/redux-component/commit/27200f7f)) 45 | * add mapDispatchToLifecycle to args ([125709cf](https://github.com/tomchentw/redux-component/commit/125709cf)) 46 | * add createStore and reducer to Componentize args ([69078224](https://github.com/tomchentw/redux-component/commit/69078224)) 47 | * should returns ReduxComponent ([d59c5708](https://github.com/tomchentw/redux-component/commit/d59c5708)) 48 | * add createComponent ([b8695ee0](https://github.com/tomchentw/redux-component/commit/b8695ee0)) 49 | * make sure it exists ([bef6a877](https://github.com/tomchentw/redux-component/commit/bef6a877)) 50 | 51 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Tom Chen 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so, 10 | 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, FITNESS 17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # redux-component 2 | > Create stateful React Component using goodness from redux 3 | 4 | [![Version][npm-image]][npm-url] [![Travis CI][travis-image]][travis-url] [![Quality][codeclimate-image]][codeclimate-url] [![Coverage][codeclimate-coverage-image]][codeclimate-coverage-url] [![Dependencies][gemnasium-image]][gemnasium-url] [![Gitter][gitter-image]][gitter-url] 5 | 6 | 7 | ## Quick start: Say Hi 8 | 9 | The form component that reads name and email from the user and submit the form to the API server. 10 | 11 | ```js 12 | import { Componentize } from "redux-component"; 13 | 14 | const createComponent = Componentize(/* ... */); 15 | 16 | const Component = createComponent(function SayHi (props, state, actions) { 17 | return ( 18 |
actions.submitForm(props, e)}> 19 | {renderUsername(state.usernameLoading, props.userId, state.username)} 20 | {renderError(state.error)} 21 | actions.textChanged(`name`, e)} /> 22 | actions.textChanged(`email`, e)} /> 23 | 24 |
25 | ); 26 | }); 27 | ``` 28 | 29 | This basically covers everything for creating a stateful React component. 30 | 31 | 32 | ## Documentation 33 | 34 | See the full list of [API](docs/api.md#api). 35 | 36 | 37 | ## Usage 38 | 39 | `redux-component` requires __React 0.13 or later.__ 40 | 41 | ```sh 42 | npm install --save redux-component 43 | ``` 44 | 45 | All functions are available on the top-level export. 46 | 47 | ```js 48 | import { Componentize } from "redux-component"; 49 | ``` 50 | 51 | 52 | ## Initiative 53 | 54 | React 0.14 introduces [stateless function components](https://facebook.github.io/react/blog/2015/09/10/react-v0.14-rc1.html#stateless-function-components). However, what if I want to use __pure functions__ to create stateful React components? 55 | 56 | __That's what `redux-component` does.__ 57 | 58 | > Manage a component's local state using a local redux store. 59 | 60 | A *isolated* redux store is created for each React component instance. It has __nothing__ to do with your global flux architecture. There are several goodness for this approach: 61 | 62 | * Express component state transition in a single `reducer` function 63 | * Event callbacks in redux actions are clean and easy to reason about 64 | * You build *pure functions* all the way: `render`, `action`s and `reducer` 65 | * No more `this.setState()` touched in your code 66 | * Easy to test React component implements 67 | 68 | See the complete example in the [examples/gh-pages](https://github.com/tomchentw/redux-component/tree/master/examples/gh-pages/src) folder and demo [hosted on GitHub](https://tomchentw.github.io/redux-component/). 69 | 70 | 71 | ## Contributing 72 | 73 | [![devDependency Status][david-dm-image]][david-dm-url] 74 | 75 | 1. Fork it 76 | 2. Create your feature branch (`git checkout -b my-new-feature`) 77 | 3. Commit your changes (`git commit -am 'Add some feature'`) 78 | 4. Push to the branch (`git push origin my-new-feature`) 79 | 5. Create new Pull Request 80 | 81 | 82 | [npm-image]: https://img.shields.io/npm/v/redux-component.svg?style=flat-square 83 | [npm-url]: https://www.npmjs.org/package/redux-component 84 | 85 | [travis-image]: https://img.shields.io/travis/tomchentw/redux-component.svg?style=flat-square 86 | [travis-url]: https://travis-ci.org/tomchentw/redux-component 87 | [codeclimate-image]: https://img.shields.io/codeclimate/github/tomchentw/redux-component.svg?style=flat-square 88 | [codeclimate-url]: https://codeclimate.com/github/tomchentw/redux-component 89 | [codeclimate-coverage-image]: https://img.shields.io/codeclimate/coverage/github/tomchentw/redux-component.svg?style=flat-square 90 | [codeclimate-coverage-url]: https://codeclimate.com/github/tomchentw/redux-component 91 | [gemnasium-image]: https://img.shields.io/gemnasium/tomchentw/redux-component.svg?style=flat-square 92 | [gemnasium-url]: https://gemnasium.com/tomchentw/redux-component 93 | [gitter-image]: https://badges.gitter.im/Join%20Chat.svg 94 | [gitter-url]: https://gitter.im/tomchentw/redux-component?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge 95 | [david-dm-image]: https://img.shields.io/david/dev/tomchentw/redux-component.svg?style=flat-square 96 | [david-dm-url]: https://david-dm.org/tomchentw/redux-component#info=devDependencies 97 | -------------------------------------------------------------------------------- /docs/api.md: -------------------------------------------------------------------------------- 1 | ## API 2 | 3 | ### `Componentize(createStore, reducer, mapDispatchToLifecycle, mapDispatchToActions)` 4 | 5 | Componentize redux store and redux actions to a React component. 6 | 7 | It returns a `createComponent` function and you'll have to invoke it with a `render` function to get the React component. Notice all 4 arguments are always required. 8 | 9 | #### Arguments 10 | 11 | * `createStore(reducer): reduxStore` \(*Function*): The createStore function from stock `redux` package, or a funciton returned by `applyMiddleware(...middlewares)(createStore)`. It will be invoked with `reducer` function inside the constructor of the React component. 12 | 13 | * `reducer(state, action): nextState` \(*Function*): The `reducer` function in redux. Notice the state here refers to components' `this.state` and the return value (`nextState`) will be passed in to its `this.setState`. 14 | 15 | * `mapDispatchToLifecycle(dispatch): lifecycleActions` \(*object*): An object with the same function names, but bound to a Redux store, will be used in the corresponding React component lifecycle callbacks. (Tip: you may use the [`bindActionCreators()`](http://gaearon.github.io/redux/docs/api/bindActionCreators.html) helper from Redux.) You may not omit it. However, you're free to pass in any [`noop`](https://lodash.com/docs#noop) functions. 16 | 17 | * `mapDispatchToActions(dispatch): eventActions` \(*object*): An object with the same function names, but bound to a Redux store, will be passed in as third argument of the `render` function. Typically it will be used as event handler during JSX creation. (Tip: you may use the [`bindActionCreators()`](http://gaearon.github.io/redux/docs/api/bindActionCreators.html) helper from Redux.) You may not omit it. However, you're free to pass in a functions that returns an empty object. 18 | 19 | #### Returns 20 | 21 | A React component class that manage the state by a local redux store with redux action creators as event handlers. 22 | 23 | #### Remarks 24 | 25 | * It needs to be invoked __two times__. The first time with its arguments described above, and a second time, with the `pure` render function: `Componentize(createStore, reducer, mapDispatchToLifecycle, mapDispatchToActions)(render)`. 26 | 27 | #### Examples 28 | 29 | ##### Minimal setup 30 | 31 | ```js 32 | export default Componentize( 33 | createStore, () => ({}), _.noop, _.noop 34 | )(function render (props, state, actions) { 35 | // Notice the actions argument will be undefined (return value of invoking _.noop) 36 | return (
); 37 | }); 38 | ``` 39 | 40 | ##### Using lifecycle actions 41 | 42 | ```js 43 | export default Componentize(createStore, (state, action) => { 44 | return action; 45 | }, dispatch => { 46 | return bindActionCreators({ 47 | componentDidMount (props) { 48 | return { 49 | type: `MOUNTED_ON_DOM`, // User defined string 50 | windowKeyLength: Object.keys(window).length, 51 | }; 52 | }, 53 | }, dispatch); 54 | }, _.noop)(function render (props, state, actions) { 55 | return ( 56 |
57 | window containing how many keys: {state.windowKeyLength} 58 |
59 | ); 60 | }); 61 | ``` 62 | 63 | ##### Using event actions 64 | 65 | ```js 66 | export default Componentize(createStore, (state, action) => { 67 | return action; 68 | }, _.noop, dispatch => { 69 | return bindActionCreators({ 70 | handleClick (extraKey, event) { 71 | return { 72 | type: `HANDLE_CLICK`, // User defined string 73 | fromWhich: extraKey, 74 | metaKey: event.metaKey, 75 | }; 76 | }, 77 | }, dispatch); 78 | })(function render (props, state, actions) { 79 | return ( 80 |
81 | Last clicked with: {state.metaKey} from {state.fromWhich} 82 | 86 | 90 |
91 | ); 92 | }); 93 | ``` 94 | 95 | ##### Custom createStore from applyMiddleware 96 | 97 | Check out the [SimpleComponent.Componentize](https://github.com/tomchentw/redux-component/blob/master/examples/gh-pages/src/SimpleComponent.Componentize.js) module under [examples/gh-pages](https://github.com/tomchentw/redux-component/tree/master/examples/gh-pages). 98 | -------------------------------------------------------------------------------- /docs/design-guideline.md: -------------------------------------------------------------------------------- 1 | ## Design Guideline 2 | 3 | React stateful component creator using redux. 4 | 5 | ### Inspiration 6 | 7 | * [react-redux](https://github.com/rackt/react-redux/) 8 | * [cycle-react](https://github.com/pH200/cycle-react) 9 | * [ducks-modular-redux](https://github.com/erikras/ducks-modular-redux) 10 | 11 | ### Use Cases 12 | 13 | #### Stateful component with local state 14 | 15 | Sometimes, you don't want every state in your application to go into global redux store. They could be just local state exists in the component via `setState` call. `redux-component` provides you a clean and testable interface to write the stateful component in the first place. 16 | 17 | #### Migration to redux 18 | 19 | You already have a existing global flux architecture in the application. For the time being, you may want to migrate the coe to use `redux`. Then `redux-component` lets you start small steps: migrating the component's local state first, get familiar with the `redux` APIs & ecosystems, then refactor your global flux architecture at the end. 20 | 21 | #### Start local and push to global when necessary 22 | 23 | Build statefull components with `redux-component` and apply `ducks-modular-redux` approach in it. When a global state is needed, simply pull out necessary action creators and reducer to your global redux architecture. After that, `connect` it to the component and you've done with it. 24 | -------------------------------------------------------------------------------- /examples/gh-pages/Client.webpackConfig.js: -------------------------------------------------------------------------------- 1 | import { 2 | resolve as resolvePath, 3 | } from "path"; 4 | 5 | import { 6 | default as webpack, 7 | } from "webpack"; 8 | 9 | import { 10 | default as ExtractTextPlugin, 11 | } from "extract-text-webpack-plugin"; 12 | 13 | let FILENAME_FORMAT; 14 | let BABEL_PLUGINS; 15 | let PRODUCTION_PLUGINS; 16 | 17 | if (process.env.NODE_ENV === `production`) { 18 | FILENAME_FORMAT = `[name]-[chunkhash].js`; 19 | BABEL_PLUGINS = []; 20 | PRODUCTION_PLUGINS = [ 21 | // Same effect as webpack -p 22 | new webpack.optimize.UglifyJsPlugin(), 23 | new webpack.optimize.OccurenceOrderPlugin(), 24 | ]; 25 | } else { 26 | // When HMR is enabled, chunkhash cannot be used. 27 | FILENAME_FORMAT = `[name].js`; 28 | BABEL_PLUGINS = [ 29 | [ 30 | `react-transform`, 31 | { 32 | transforms: [ 33 | { 34 | transform: `react-transform-hmr`, 35 | imports: [`react`], 36 | locals: [`module`], 37 | }, { 38 | transform: `react-transform-catch-errors`, 39 | imports: [`react`, `redbox-react`], 40 | }, 41 | ], 42 | }, 43 | ], 44 | ]; 45 | PRODUCTION_PLUGINS = []; 46 | } 47 | 48 | export default { 49 | devServer: { 50 | port: 8080, 51 | host: `localhost`, 52 | contentBase: resolvePath(__dirname, `../../public`), 53 | publicPath: `/assets/`, 54 | hot: true, 55 | stats: { colors: true }, 56 | }, 57 | output: { 58 | path: resolvePath(__dirname, `../../public/assets`), 59 | pathinfo: process.env.NODE_ENV !== `production`, 60 | publicPath: `assets/`, 61 | filename: FILENAME_FORMAT, 62 | }, 63 | module: { 64 | loaders: [ 65 | { 66 | test: /\.jpg$/, 67 | loader: `file`, 68 | }, 69 | { 70 | test: /\.scss$/, 71 | loader: ExtractTextPlugin.extract(`style`, `css!sass`, { 72 | publicPath: ``, 73 | }), 74 | }, 75 | { 76 | test: /\.js(x?)$/, 77 | exclude: /node_modules/, 78 | loader: `babel`, 79 | query: { 80 | plugins: BABEL_PLUGINS, 81 | }, 82 | }, 83 | ], 84 | }, 85 | plugins: [ 86 | new webpack.EnvironmentPlugin(`NODE_ENV`), 87 | new ExtractTextPlugin(`[name]-[chunkhash].css`, { 88 | disable: process.env.NODE_ENV !== `production`, 89 | }), 90 | ...PRODUCTION_PLUGINS, 91 | ], 92 | }; 93 | -------------------------------------------------------------------------------- /examples/gh-pages/Server.webpackConfig.js: -------------------------------------------------------------------------------- 1 | import { 2 | resolve as resolvePath, 3 | } from "path"; 4 | 5 | import { 6 | default as webpack, 7 | } from "webpack"; 8 | 9 | let PRODUCTION_PLUGINS; 10 | 11 | if (process.env.NODE_ENV === `production`) { 12 | PRODUCTION_PLUGINS = [ 13 | // Same effect as webpack -p 14 | new webpack.optimize.UglifyJsPlugin(), 15 | new webpack.optimize.OccurenceOrderPlugin(), 16 | ]; 17 | } else { 18 | PRODUCTION_PLUGINS = []; 19 | } 20 | 21 | const externals = Object.keys( 22 | require(`./package.json`).dependencies 23 | ).map(key => new RegExp(`^${ key }`)); 24 | 25 | export default { 26 | output: { 27 | path: resolvePath(__dirname, `../../public/assets`), 28 | pathinfo: process.env.NODE_ENV !== `production`, 29 | filename: `[name].js`, 30 | libraryTarget: `commonjs2`, 31 | }, 32 | target: `node`, 33 | externals, 34 | module: { 35 | loaders: [ 36 | { 37 | test: /\.scss$/, 38 | loader: `null`, 39 | }, 40 | { 41 | test: /\.jpg$/, 42 | loader: `null`, 43 | }, 44 | { 45 | test: /\.js(x?)$/, 46 | exclude: /node_modules/, 47 | loader: `babel`, 48 | }, 49 | ], 50 | }, 51 | plugins: [ 52 | new webpack.EnvironmentPlugin(`NODE_ENV`), 53 | ...PRODUCTION_PLUGINS, 54 | ], 55 | }; 56 | -------------------------------------------------------------------------------- /examples/gh-pages/WebWorker.webpackConfig.js: -------------------------------------------------------------------------------- 1 | import { 2 | resolve as resolvePath, 3 | } from "path"; 4 | 5 | import { 6 | default as webpack, 7 | } from "webpack"; 8 | 9 | let FILENAME_FORMAT; 10 | let PRODUCTION_PLUGINS; 11 | 12 | if (process.env.NODE_ENV === `production`) { 13 | FILENAME_FORMAT = `[name]-[chunkhash].js`; 14 | PRODUCTION_PLUGINS = [ 15 | // Same effect as webpack -p 16 | new webpack.optimize.UglifyJsPlugin(), 17 | new webpack.optimize.OccurenceOrderPlugin(), 18 | ]; 19 | } else { 20 | // When HMR is enabled, chunkhash cannot be used. 21 | FILENAME_FORMAT = `[name].js`; 22 | PRODUCTION_PLUGINS = []; 23 | } 24 | 25 | export default { 26 | output: { 27 | path: resolvePath(__dirname, `../../public/assets`), 28 | pathinfo: process.env.NODE_ENV !== `production`, 29 | publicPath: `assets/`, 30 | filename: FILENAME_FORMAT, 31 | }, 32 | target: `webworker`, 33 | module: { 34 | loaders: [ 35 | { 36 | test: /\.scss$/, 37 | loader: `null`, 38 | }, 39 | { 40 | test: /\.js(x?)$/, 41 | exclude: /node_modules/, 42 | loader: `babel`, 43 | }, 44 | ], 45 | }, 46 | plugins: [ 47 | new webpack.EnvironmentPlugin(`NODE_ENV`), 48 | ...PRODUCTION_PLUGINS, 49 | ], 50 | }; 51 | -------------------------------------------------------------------------------- /examples/gh-pages/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "gh-pages", 3 | "version": "1.0.0", 4 | "scripts": { 5 | "clean": "rimraf ../../public/index.html ../../public/assets", 6 | "prebuild": "npm run clean", 7 | "build": "cross-env NODE_ENV=production reacthtmlpack buildToDir ../../public './views/**/*.html.js'", 8 | "predev": "npm run clean", 9 | "dev": "cross-env NODE_ENV=development reacthtmlpack buildToDir ../../public './views/**/*.html.js'", 10 | "prestart": "npm run clean", 11 | "start": "cross-env NODE_ENV=development reacthtmlpack devServer ./Client.webpackConfig.js ../../public './views/**/*.html.js'" 12 | }, 13 | "devDependencies": { 14 | "babel-core": "^6.4.5", 15 | "babel-loader": "^6.2.1", 16 | "babel-plugin-react-transform": "^2.0.0", 17 | "cross-env": "^1.0.7", 18 | "css-loader": "^0.23.1", 19 | "extract-text-webpack-plugin": "^1.0.1", 20 | "file-loader": "^0.8.5", 21 | "node-sass": "^3.4.2", 22 | "null-loader": "^0.1.1", 23 | "react-transform-catch-errors": "^1.0.1", 24 | "react-transform-hmr": "^1.0.1", 25 | "reacthtmlpack": "^1.2.1", 26 | "redbox-react": "^1.2.0", 27 | "rimraf": "^2.5.1", 28 | "sass-loader": "^3.1.2", 29 | "style-loader": "^0.13.0", 30 | "webpack": "^1.12.12", 31 | "webpack-dev-server": "^1.14.1" 32 | }, 33 | "dependencies": { 34 | "animate.css": "^3.4.0", 35 | "bootstrap-sass": "^3.3.6", 36 | "classnames": "^2.2.3", 37 | "fbjs": "^0.6.1", 38 | "node-libs-browser": "^1.0.0", 39 | "prismjs": "git+https://github.com/PrismJS/prism.git#master", 40 | "raf": "^3.1.0", 41 | "react": "^0.14.6", 42 | "react-addons-pure-render-mixin": "^0.14.6", 43 | "react-dom": "^0.14.6", 44 | "react-github-fork-ribbon": "^0.4.2", 45 | "react-prism": "^3.1.0", 46 | "react-pure-render": "^1.0.2", 47 | "react-toastr": "^2.3.1", 48 | "redux": "^3.0.6", 49 | "redux-component": "^0.2.0", 50 | "redux-thunk": "^1.0.3", 51 | "toastr": "^2.1.2" 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /examples/gh-pages/src/ReactRoot.js: -------------------------------------------------------------------------------- 1 | import { 2 | default as React, 3 | Component, 4 | } from "react"; 5 | 6 | import { 7 | Component as SimpleComponent_Componentize, 8 | } from "./SimpleComponent.Componentize"; 9 | 10 | import { 11 | Component as SimpleComponent_createDispatch, 12 | } from "./SimpleComponent.createDispatch"; 13 | 14 | import { 15 | Component as SimpleComponent_ReduxComponentMixin, 16 | } from "./SimpleComponent.ReduxComponentMixin"; 17 | 18 | export default class ReactRoot extends Component { 19 | render() { 20 | return ( 21 |
22 | 23 | 24 | 25 |
26 | ); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /examples/gh-pages/src/SimpleComponent.Componentize.js: -------------------------------------------------------------------------------- 1 | import { 2 | default as React, 3 | } from "react"; 4 | 5 | import { 6 | createStore, 7 | applyMiddleware, 8 | bindActionCreators, 9 | } from "redux"; 10 | 11 | import { 12 | default as thunkMiddleware, 13 | } from "redux-thunk"; 14 | 15 | import { 16 | Componentize, 17 | } from "redux-component"; 18 | 19 | import { 20 | default as randomPromise, 21 | } from "./randomPromise"; 22 | 23 | const TEXT_CHANGED = `SimpleComponent/TEXT_CHANGED`; 24 | 25 | const SUBMIT_FORM_REQUEST = `SimpleComponent/SUBMIT_FORM_REQUEST`; 26 | const SUBMIT_FORM_SUCCESS = `SimpleComponent/SUBMIT_FORM_SUCCESS`; 27 | const SUBMIT_FORM_FAILURE = `SimpleComponent/SUBMIT_FORM_FAILURE`; 28 | 29 | export function textChanged(formKey, event) { 30 | return { 31 | type: TEXT_CHANGED, 32 | formKey, 33 | value: event.target.value, 34 | }; 35 | } 36 | 37 | export function submitForm(props, event) { 38 | return (dispatch, getState) => { 39 | event.preventDefault(); 40 | event.stopPropagation(); 41 | 42 | dispatch(submitFormRequest()); 43 | 44 | const { formValues } = getState(); 45 | 46 | props.globlaReduxActionSubmitForm(formValues) 47 | .then(() => { 48 | dispatch(submitFormSuccess()); 49 | }) 50 | .catch((error) => { 51 | dispatch(submitFormFailure(error)); 52 | }); 53 | }; 54 | } 55 | 56 | export function submitFormRequest() { 57 | return { 58 | type: SUBMIT_FORM_REQUEST, 59 | }; 60 | } 61 | 62 | export function submitFormSuccess() { 63 | return { 64 | type: SUBMIT_FORM_SUCCESS, 65 | }; 66 | } 67 | 68 | export function submitFormFailure(error) { 69 | return { 70 | type: SUBMIT_FORM_FAILURE, 71 | error, 72 | }; 73 | } 74 | 75 | // Lifecycle --- BEGIN 76 | export function componentDidMount(props) { 77 | return (dispatch, getState) => { 78 | dispatch(loadUsernameRequest()); 79 | 80 | props.globlaReduxActionGetUsername(props.userId) 81 | .then((username) => { 82 | dispatch(loadUsernameSuccess(username)); 83 | }) 84 | .catch((error) => { 85 | dispatch(loadUsernameFailure(error)); 86 | }); 87 | }; 88 | } 89 | 90 | const LOAD_USERNAME_REQUEST = `SimpleComponent/LOAD_USERNAME_REQUEST`; 91 | const LOAD_USERNAME_SUCCESS = `SimpleComponent/LOAD_USERNAME_SUCCESS`; 92 | const LOAD_USERNAME_FAILURE = `SimpleComponent/LOAD_USERNAME_FAILURE`; 93 | 94 | export function loadUsernameRequest() { 95 | return { 96 | type: LOAD_USERNAME_REQUEST, 97 | }; 98 | } 99 | 100 | export function loadUsernameSuccess(username) { 101 | return { 102 | type: LOAD_USERNAME_SUCCESS, 103 | username, 104 | }; 105 | } 106 | 107 | export function loadUsernameFailure(error) { 108 | return { 109 | type: LOAD_USERNAME_FAILURE, 110 | error, 111 | }; 112 | } 113 | 114 | // Lifecycle --- END 115 | 116 | 117 | const initialState = { 118 | formValues: { 119 | name: `Tom Chen`, 120 | email: `developer@tomchentw.com`, 121 | }, 122 | error: null, 123 | usernameLoading: false, 124 | username: null, 125 | }; 126 | 127 | export function reducer(state = initialState, action) { 128 | switch (action.type) { 129 | case TEXT_CHANGED: 130 | return { 131 | ...state, 132 | formValues: { 133 | ...state.formValues, 134 | [action.formKey]: action.value, 135 | }, 136 | }; 137 | case SUBMIT_FORM_SUCCESS: 138 | return { 139 | ...state, 140 | error: null, 141 | }; 142 | case SUBMIT_FORM_FAILURE: 143 | return { 144 | ...state, 145 | error: action.error, 146 | }; 147 | case LOAD_USERNAME_REQUEST: 148 | return { 149 | ...state, 150 | usernameLoading: true, 151 | }; 152 | case LOAD_USERNAME_SUCCESS: 153 | return { 154 | ...state, 155 | error: null, 156 | usernameLoading: false, 157 | username: action.username, 158 | }; 159 | case LOAD_USERNAME_FAILURE: 160 | return { 161 | ...state, 162 | error: action.error, 163 | usernameLoading: false, 164 | }; 165 | default: 166 | return state; 167 | } 168 | } 169 | 170 | const createStoreWithMiddleware = applyMiddleware( 171 | thunkMiddleware, // lets us dispatch() functions 172 | )(createStore); 173 | 174 | // only visible inside Componentize. 175 | function mapDispatchToLifecycle(dispatch) { 176 | return bindActionCreators({ 177 | componentDidMount, 178 | }, dispatch); 179 | } 180 | 181 | function mapDispatchToActions(dispatch) { 182 | return bindActionCreators({ 183 | textChanged, 184 | submitForm, 185 | }, dispatch); 186 | } 187 | 188 | /* eslint-disable new-cap */ 189 | const createComponent = Componentize( 190 | createStoreWithMiddleware, reducer, mapDispatchToLifecycle, mapDispatchToActions 191 | ); 192 | /* eslint-enable new-cap */ 193 | 194 | function renderUsername(usernameLoading, userId, username) { 195 | if (usernameLoading) { 196 | return ( 197 | 198 | ); 199 | } else { 200 | return ( 201 | 202 | ); 203 | } 204 | } 205 | 206 | function renderError(error) { 207 | if (error) { 208 | return ( 209 |

{error.message}

210 | ); 211 | } else { 212 | return null; 213 | } 214 | } 215 | 216 | function SimpleComponent(props, state, actions) { 217 | /* eslint-disable react/jsx-no-bind */ 218 | return ( 219 |
actions.submitForm(props, e)}> 220 | {renderUsername(state.usernameLoading, props.userId, state.username)} 221 | {renderError(state.error)} 222 | actions.textChanged(`name`, e)} 226 | /> 227 | actions.textChanged(`email`, e)} 231 | /> 232 | 233 |
234 | ); 235 | /* eslint-enable react/jsx-no-bind */ 236 | } 237 | 238 | export const Component = createComponent(SimpleComponent); 239 | 240 | Component.defaultProps = { 241 | userId: 1, 242 | globlaReduxActionGetUsername: randomPromise, 243 | globlaReduxActionSubmitForm: randomPromise, 244 | }; 245 | -------------------------------------------------------------------------------- /examples/gh-pages/src/SimpleComponent.ReduxComponentMixin.js: -------------------------------------------------------------------------------- 1 | import { 2 | default as React, 3 | PropTypes, 4 | } from "react"; 5 | 6 | import { 7 | ReduxComponentMixin, 8 | } from "redux-component"; 9 | 10 | import { 11 | default as randomPromise, 12 | } from "./randomPromise"; 13 | 14 | // Hey let's just borrow from ES2015 class version 15 | import { 16 | TEXT_CHANGED, 17 | SUBMIT_FORM_REQUEST, 18 | SUBMIT_FORM_SUCCESS, 19 | SUBMIT_FORM_FAILURE, 20 | LOAD_USERNAME_REQUEST, 21 | LOAD_USERNAME_SUCCESS, 22 | LOAD_USERNAME_FAILURE, 23 | 24 | reducer, 25 | } from "./SimpleComponent.createDispatch"; 26 | 27 | /* eslint-disable react/prefer-es6-class */ 28 | // Here we define React Component using React.createClass & mixins 29 | export const Component = React.createClass({ 30 | 31 | propTypes: { 32 | userId: PropTypes.any.isRequired, 33 | globlaReduxActionGetUsername: PropTypes.any.isRequired, 34 | globlaReduxActionSubmitForm: PropTypes.any.isRequired, 35 | }, 36 | 37 | mixins: [ 38 | /* eslint-disable new-cap */ 39 | ReduxComponentMixin(reducer), 40 | /* eslint-enable new-cap */ 41 | ], 42 | 43 | getDefaultProps() { 44 | return { 45 | userId: 1, 46 | globlaReduxActionGetUsername: randomPromise, 47 | globlaReduxActionSubmitForm: randomPromise, 48 | }; 49 | }, 50 | 51 | componentDidMount() { 52 | this.dispatch({ 53 | type: LOAD_USERNAME_REQUEST, 54 | }); 55 | 56 | this.props.globlaReduxActionGetUsername(this.props.userId) 57 | .then((username) => { 58 | this.dispatch({ 59 | type: LOAD_USERNAME_SUCCESS, 60 | username, 61 | }); 62 | }) 63 | .catch((error) => { 64 | this.dispatch({ 65 | type: LOAD_USERNAME_FAILURE, 66 | error, 67 | }); 68 | }); 69 | }, 70 | 71 | handleSubmitForm(event) { 72 | event.preventDefault(); 73 | event.stopPropagation(); 74 | 75 | this.dispatch({ 76 | type: SUBMIT_FORM_REQUEST, 77 | }); 78 | 79 | const { formValues } = this.state; 80 | 81 | this.props.globlaReduxActionSubmitForm(formValues) 82 | .then(() => { 83 | this.dispatch({ 84 | type: SUBMIT_FORM_SUCCESS, 85 | }); 86 | }) 87 | .catch((error) => { 88 | this.dispatch({ 89 | type: SUBMIT_FORM_FAILURE, 90 | error, 91 | }); 92 | }); 93 | }, 94 | 95 | renderUsername() { 96 | if (this.state.usernameLoading) { 97 | return ( 98 | 99 | ); 100 | } else { 101 | return ( 102 | 103 | ); 104 | } 105 | }, 106 | 107 | renderError() { 108 | if (this.state.error) { 109 | return ( 110 |

{this.state.error.message}

111 | ); 112 | } else { 113 | return null; 114 | } 115 | }, 116 | 117 | render() { 118 | /* eslint-disable react/jsx-no-bind */ 119 | return ( 120 |
121 | {this.renderUsername()} 122 | {this.renderError()} 123 | this.dispatch({ 124 | type: TEXT_CHANGED, 125 | formKey: `name`, 126 | value: event.target.value, 127 | })} 128 | /> 129 | this.dispatch({ 130 | type: TEXT_CHANGED, 131 | formKey: `email`, 132 | value: event.target.value, 133 | })} 134 | /> 135 | 136 |
137 | ); 138 | /* eslint-enable react/jsx-no-bind */ 139 | }, 140 | }); 141 | -------------------------------------------------------------------------------- /examples/gh-pages/src/SimpleComponent.createDispatch.js: -------------------------------------------------------------------------------- 1 | import { 2 | default as React, 3 | PropTypes, 4 | } from "react"; 5 | 6 | import { 7 | createDispatch, 8 | } from "redux-component"; 9 | 10 | import { 11 | default as randomPromise, 12 | } from "./randomPromise"; 13 | 14 | export const TEXT_CHANGED = `TEXT_CHANGED`; 15 | 16 | export const SUBMIT_FORM_REQUEST = `SUBMIT_FORM_REQUEST`; 17 | export const SUBMIT_FORM_SUCCESS = `SUBMIT_FORM_SUCCESS`; 18 | export const SUBMIT_FORM_FAILURE = `SUBMIT_FORM_FAILURE`; 19 | 20 | export const LOAD_USERNAME_REQUEST = `LOAD_USERNAME_REQUEST`; 21 | export const LOAD_USERNAME_SUCCESS = `LOAD_USERNAME_SUCCESS`; 22 | export const LOAD_USERNAME_FAILURE = `LOAD_USERNAME_FAILURE`; 23 | 24 | const initialState = { 25 | formValues: { 26 | name: `Tom Chen`, 27 | email: `developer@tomchentw.com`, 28 | }, 29 | error: null, 30 | usernameLoading: false, 31 | username: null, 32 | }; 33 | 34 | export function reducer(state = initialState, action) { 35 | switch (action.type) { 36 | case TEXT_CHANGED: 37 | return { 38 | ...state, 39 | formValues: { 40 | ...state.formValues, 41 | [action.formKey]: action.value, 42 | }, 43 | }; 44 | case SUBMIT_FORM_SUCCESS: 45 | return { 46 | ...state, 47 | error: null, 48 | }; 49 | case SUBMIT_FORM_FAILURE: 50 | return { 51 | ...state, 52 | error: action.error, 53 | }; 54 | case LOAD_USERNAME_REQUEST: 55 | return { 56 | ...state, 57 | usernameLoading: true, 58 | }; 59 | case LOAD_USERNAME_SUCCESS: 60 | return { 61 | ...state, 62 | error: null, 63 | usernameLoading: false, 64 | username: action.username, 65 | }; 66 | case LOAD_USERNAME_FAILURE: 67 | return { 68 | ...state, 69 | error: action.error, 70 | usernameLoading: false, 71 | }; 72 | default: 73 | return state; 74 | } 75 | } 76 | 77 | export class Component extends React.Component { 78 | 79 | static propTypes = { 80 | userId: PropTypes.any.isRequired, 81 | globlaReduxActionGetUsername: PropTypes.any.isRequired, 82 | globlaReduxActionSubmitForm: PropTypes.any.isRequired, 83 | }; 84 | 85 | static defaultProps = { 86 | userId: 1, 87 | globlaReduxActionGetUsername: randomPromise, 88 | globlaReduxActionSubmitForm: randomPromise, 89 | }; 90 | 91 | componentDidMount() { 92 | this.dispatch({ 93 | type: LOAD_USERNAME_REQUEST, 94 | }); 95 | 96 | this.props.globlaReduxActionGetUsername(this.props.userId) 97 | .then((username) => { 98 | this.dispatch({ 99 | type: LOAD_USERNAME_SUCCESS, 100 | username, 101 | }); 102 | }) 103 | .catch((error) => { 104 | this.dispatch({ 105 | type: LOAD_USERNAME_FAILURE, 106 | error, 107 | }); 108 | }); 109 | } 110 | 111 | dispatch = createDispatch(this, reducer); 112 | 113 | handleSubmitForm(event) { 114 | event.preventDefault(); 115 | event.stopPropagation(); 116 | 117 | this.dispatch({ 118 | type: SUBMIT_FORM_REQUEST, 119 | }); 120 | 121 | const { formValues } = this.state; 122 | 123 | this.props.globlaReduxActionSubmitForm(formValues) 124 | .then(() => { 125 | this.dispatch({ 126 | type: SUBMIT_FORM_SUCCESS, 127 | }); 128 | }) 129 | .catch((error) => { 130 | this.dispatch({ 131 | type: SUBMIT_FORM_FAILURE, 132 | error, 133 | }); 134 | }); 135 | } 136 | 137 | renderUsername() { 138 | if (this.state.usernameLoading) { 139 | return ( 140 | 141 | ); 142 | } else { 143 | return ( 144 | 145 | ); 146 | } 147 | } 148 | 149 | renderError() { 150 | if (this.state.error) { 151 | return ( 152 |

{this.state.error.message}

153 | ); 154 | } else { 155 | return null; 156 | } 157 | } 158 | 159 | render() { 160 | /* eslint-disable react/jsx-no-bind */ 161 | return ( 162 |
163 | {this.renderUsername()} 164 | {this.renderError()} 165 | this.dispatch({ 166 | type: TEXT_CHANGED, 167 | formKey: `name`, 168 | value: event.target.value, 169 | })} 170 | /> 171 | this.dispatch({ 172 | type: TEXT_CHANGED, 173 | formKey: `email`, 174 | value: event.target.value, 175 | })} 176 | /> 177 | 178 |
179 | ); 180 | /* eslint-enable react/jsx-no-bind */ 181 | } 182 | } 183 | -------------------------------------------------------------------------------- /examples/gh-pages/src/client.js: -------------------------------------------------------------------------------- 1 | import { 2 | default as React, 3 | } from "react"; 4 | 5 | import { 6 | default as ReactDOM, 7 | } from "react-dom"; 8 | 9 | import { 10 | default as ReactRoot, 11 | } from "./ReactRoot"; 12 | 13 | ReactDOM.render(( 14 | 15 | ), 16 | document.getElementById(`react-container`) 17 | ); 18 | -------------------------------------------------------------------------------- /examples/gh-pages/src/randomPromise.js: -------------------------------------------------------------------------------- 1 | 2 | export default function randomPromise() { 3 | return new Promise((resolve, reject) => { 4 | setTimeout(() => { 5 | if (Math.random() > 0.2) { 6 | resolve(`tomchentw`); 7 | } else { 8 | reject(new Error(`Hey you've got some random error from your API response...`)); 9 | } 10 | }, 750 + Math.random() * 500); 11 | }); 12 | } 13 | -------------------------------------------------------------------------------- /examples/gh-pages/src/server.js: -------------------------------------------------------------------------------- 1 | import { default as ReactRoot } from "./ReactRoot"; 2 | 3 | export default ReactRoot; 4 | -------------------------------------------------------------------------------- /examples/gh-pages/views/index.html.js: -------------------------------------------------------------------------------- 1 | import { 2 | default as React, 3 | } from "react"; 4 | 5 | import { 6 | WebpackScriptEntry, 7 | WebpackStyleEntry, 8 | ReactRenderToStringEntry, 9 | } from "reacthtmlpack/lib/entry"; 10 | 11 | export default ( 12 | 13 | 14 | redux-component | tomchentw 15 | 16 | 17 | 22 | 27 | 28 | 29 | 36 | 41 | 42 | 43 | ); 44 | -------------------------------------------------------------------------------- /lib/__tests__/setup.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | var _jsdom = require("jsdom"); 4 | 5 | global.document = (0, _jsdom.jsdom)(""); 6 | global.window = document.defaultView; 7 | global.navigator = global.window.navigator; -------------------------------------------------------------------------------- /lib/components/ComponentizeCreator.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | var _extends = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; }; 4 | 5 | var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }(); 6 | 7 | Object.defineProperty(exports, "__esModule", { 8 | value: true 9 | }); 10 | exports.default = createComponentize; 11 | 12 | var _createDispatch = require("./createDispatch"); 13 | 14 | function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } 15 | 16 | function _possibleConstructorReturn(self, call) { if (!self) { throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); } return call && (typeof call === "object" || typeof call === "function") ? call : self; } 17 | 18 | function _inherits(subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function, not " + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass; } 19 | 20 | function createComponentize(React) { 21 | var Component = React.Component; 22 | 23 | var NullLifecycleActions = { 24 | componentWillMount: function componentWillMount() {}, 25 | componentDidMount: function componentDidMount() {}, 26 | componentWillReceiveProps: function componentWillReceiveProps() {}, 27 | componentWillUpdate: function componentWillUpdate() {}, 28 | componentDidUpdate: function componentDidUpdate() {}, 29 | componentWillUnmount: function componentWillUnmount() {} 30 | }; 31 | 32 | return function Componentize(createStore, reducer, mapDispatchToLifecycle, mapDispatchToActions) { 33 | // 34 | return function createComponent(_render) { 35 | // 36 | return function (_Component) { 37 | _inherits(ReduxComponent, _Component); 38 | 39 | function ReduxComponent() { 40 | var _Object$getPrototypeO; 41 | 42 | _classCallCheck(this, ReduxComponent); 43 | 44 | for (var _len = arguments.length, args = Array(_len), _key = 0; _key < _len; _key++) { 45 | args[_key] = arguments[_key]; 46 | } 47 | 48 | var _this = _possibleConstructorReturn(this, (_Object$getPrototypeO = Object.getPrototypeOf(ReduxComponent)).call.apply(_Object$getPrototypeO, [this].concat(args))); 49 | 50 | var dispatch = (0, _createDispatch.createDispatchWithStore)(_this, createStore(reducer)); 51 | 52 | _this.lifecycleActions = _extends({}, NullLifecycleActions, mapDispatchToLifecycle(dispatch)); 53 | 54 | _this.eventActions = mapDispatchToActions(dispatch); 55 | return _this; 56 | } 57 | 58 | _createClass(ReduxComponent, [{ 59 | key: "componentWillMount", 60 | value: function componentWillMount() { 61 | this.lifecycleActions.componentWillMount(this.props); 62 | } 63 | }, { 64 | key: "componentDidMount", 65 | value: function componentDidMount() { 66 | this.lifecycleActions.componentDidMount(this.props); 67 | } 68 | }, { 69 | key: "componentWillReceiveProps", 70 | value: function componentWillReceiveProps(nextProps /*: Object*/) { 71 | this.lifecycleActions.componentWillReceiveProps(this.props, nextProps); 72 | } 73 | }, { 74 | key: "componentWillUpdate", 75 | value: function componentWillUpdate(nextProps /*: Object*/, nextState /*: Object*/) { 76 | this.lifecycleActions.componentWillUpdate(this.props, nextProps); 77 | } 78 | }, { 79 | key: "componentDidUpdate", 80 | value: function componentDidUpdate(prevProps /*: Object*/, prevState /*: Object*/) { 81 | this.lifecycleActions.componentDidUpdate(this.props, prevProps); 82 | } 83 | }, { 84 | key: "componentWillUnmount", 85 | value: function componentWillUnmount() { 86 | this.eventActions = null; 87 | 88 | this.lifecycleActions.componentWillUnmount(this.props); 89 | this.lifecycleActions = null; 90 | } 91 | }, { 92 | key: "render", 93 | value: function render() { 94 | return _render(this.props, this.state, this.eventActions); 95 | } 96 | }]); 97 | 98 | return ReduxComponent; 99 | }(Component); 100 | }; 101 | }; 102 | } -------------------------------------------------------------------------------- /lib/components/ReduxComponentMixin.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | Object.defineProperty(exports, "__esModule", { 4 | value: true 5 | }); 6 | exports.default = ReduxComponentMixin; 7 | 8 | var _redux = require("redux"); 9 | 10 | function ReduxComponentMixin(reducer) { 11 | return { 12 | getInitialState: function getInitialState() { 13 | var _this = this; 14 | 15 | this.store = (0, _redux.createStore)(reducer); 16 | this.unsubscribeFromStore = this.store.subscribe(function () { 17 | _this.setState(_this.store.getState()); 18 | }); 19 | this.dispatch = this.store.dispatch; 20 | return this.store.getState(); 21 | }, 22 | componentWillUnmount: function componentWillUnmount() { 23 | this.unsubscribeFromStore(); 24 | } 25 | }; 26 | } -------------------------------------------------------------------------------- /lib/components/__tests__/Componentize.spec.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | var _extends = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; }; /* eslint-disable prefer-arrow-callback */ 4 | /* eslint-disable new-cap */ 5 | 6 | var _expect = require("expect"); 7 | 8 | var _expect2 = _interopRequireDefault(_expect); 9 | 10 | var _react = require("react"); 11 | 12 | var _react2 = _interopRequireDefault(_react); 13 | 14 | var _reactDom = require("react-dom"); 15 | 16 | var _reactDom2 = _interopRequireDefault(_reactDom); 17 | 18 | var _reactAddonsTestUtils = require("react-addons-test-utils"); 19 | 20 | var _reactAddonsTestUtils2 = _interopRequireDefault(_reactAddonsTestUtils); 21 | 22 | var _redux = require("redux"); 23 | 24 | var _index = require("../../index"); 25 | 26 | function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } 27 | 28 | function noop() {} 29 | 30 | describe("React", function describeReact() { 31 | describe("Componentize", function describeComponentize() { 32 | it("should exist", function it() { 33 | (0, _expect2.default)(_index.Componentize).toExist(); 34 | }); 35 | 36 | context("when called", function contextWhenCalled() { 37 | it("returns a function", function it() { 38 | (0, _expect2.default)((0, _index.Componentize)()).toBeA("function"); 39 | }); 40 | 41 | context("when called with \"render\" function", function contextWhenCalledWithRender() { 42 | it("returns ReduxComponent, a React.Component class", function it() { 43 | var ReduxComponent = (0, _index.Componentize)()(); 44 | 45 | (0, _expect2.default)(ReduxComponent.prototype).toBeA(_react.Component); 46 | (0, _expect2.default)(ReduxComponent.prototype.render).toBeA("function"); 47 | }); 48 | }); 49 | }); 50 | 51 | describe("(_1, _2, mapDispatchToLifecycle)", function describeMapDispatchToLifecycle() { 52 | it("should contain React.Component lifecycle functions", function it() { 53 | var ReduxComponent = (0, _index.Componentize)(_redux.createStore, function () { 54 | return {}; 55 | }, noop, noop)(); 56 | var comp = new ReduxComponent(); 57 | 58 | (0, _expect2.default)(comp.componentWillMount).toBeA("function"); 59 | (0, _expect2.default)(comp.componentDidMount).toBeA("function"); 60 | (0, _expect2.default)(comp.componentWillReceiveProps).toBeA("function"); 61 | (0, _expect2.default)(comp.componentWillUpdate).toBeA("function"); 62 | (0, _expect2.default)(comp.componentDidUpdate).toBeA("function"); 63 | (0, _expect2.default)(comp.componentWillUnmount).toBeA("function"); 64 | }); 65 | 66 | it("should invoke action inside React.Component lifecycle functions", function it() { 67 | var lifecycleCallbacks = { 68 | componentWillMount: function componentWillMount() {}, 69 | componentDidMount: function componentDidMount() {}, 70 | componentWillReceiveProps: function componentWillReceiveProps() {}, 71 | componentWillUpdate: function componentWillUpdate() {}, 72 | componentDidUpdate: function componentDidUpdate() {}, 73 | componentWillUnmount: function componentWillUnmount() {} 74 | }; 75 | 76 | var spies = Object.keys(lifecycleCallbacks).reduce(function (acc, key) { 77 | /* eslint-disable no-param-reassign */ 78 | acc[key] = _expect2.default.spyOn(lifecycleCallbacks, key); 79 | /* eslint-enable no-param-reassign */ 80 | return acc; 81 | }, {}); 82 | 83 | var mapDispatchToLifecycle = function mapDispatchToLifecycle() { 84 | return lifecycleCallbacks; 85 | }; 86 | 87 | var ReduxComponent = (0, _index.Componentize)(_redux.createStore, function () { 88 | return {}; 89 | }, mapDispatchToLifecycle, noop)(); 90 | var comp = new ReduxComponent(); 91 | 92 | Object.keys(spies).forEach(function (key) { 93 | return (0, _expect2.default)(spies[key]).toNotHaveBeenCalled(); 94 | }); 95 | 96 | Object.keys(lifecycleCallbacks).forEach(function (key) { 97 | return comp[key]({}, {}); 98 | }); 99 | 100 | Object.keys(spies).forEach(function (key) { 101 | return (0, _expect2.default)(spies[key]).toHaveBeenCalled(); 102 | }); 103 | }); 104 | 105 | /* eslint-disable max-len */ 106 | it("should invoke actions with correct arguments in certain Component lifecycle functions", function it() { 107 | /* eslint-enable max-len */ 108 | var lifecycleCallbacks = { 109 | componentWillMount: function componentWillMount(props) {}, 110 | componentDidMount: function componentDidMount(props) {}, 111 | componentWillReceiveProps: function componentWillReceiveProps(props, nextProps) {}, 112 | componentWillUpdate: function componentWillUpdate(props, nextProps) {}, 113 | componentDidUpdate: function componentDidUpdate(props, prevProps) {}, 114 | componentWillUnmount: function componentWillUnmount(props) {} 115 | }; 116 | 117 | var spies = Object.keys(lifecycleCallbacks).reduce(function (acc, key) { 118 | /* eslint-disable no-param-reassign */ 119 | acc[key] = _expect2.default.spyOn(lifecycleCallbacks, key); 120 | /* eslint-enable no-param-reassign */ 121 | return acc; 122 | }, {}); 123 | 124 | var mapDispatchToLifecycle = function mapDispatchToLifecycle() { 125 | return lifecycleCallbacks; 126 | }; 127 | 128 | var ReduxComponent = (0, _index.Componentize)(_redux.createStore, function () { 129 | return {}; 130 | }, mapDispatchToLifecycle, noop)(); 131 | var comp = new ReduxComponent({ 132 | name: "Tom Chen" 133 | }); 134 | 135 | Object.keys(spies).forEach(function (key) { 136 | return (0, _expect2.default)(spies[key]).toNotHaveBeenCalled(); 137 | }); 138 | 139 | comp.componentWillMount(); 140 | 141 | (0, _expect2.default)(spies.componentWillMount).toHaveBeenCalledWith({ 142 | name: "Tom Chen" 143 | }); 144 | 145 | comp.componentDidMount(); 146 | 147 | (0, _expect2.default)(spies.componentDidMount).toHaveBeenCalledWith({ 148 | name: "Tom Chen" 149 | }); 150 | 151 | comp.componentWillReceiveProps({ 152 | email: "developer@tomchentw.com" 153 | }); 154 | 155 | (0, _expect2.default)(spies.componentWillReceiveProps).toHaveBeenCalledWith({ 156 | name: "Tom Chen" 157 | }, { 158 | email: "developer@tomchentw.com" 159 | }); 160 | 161 | comp.componentWillUpdate({ 162 | email: "developer@tomchentw.com" 163 | }, {}); 164 | 165 | (0, _expect2.default)(spies.componentWillUpdate).toHaveBeenCalledWith({ 166 | name: "Tom Chen" 167 | }, { 168 | email: "developer@tomchentw.com" 169 | }); 170 | 171 | comp.componentDidUpdate({ 172 | age: 0 173 | }, {}); 174 | 175 | (0, _expect2.default)(spies.componentDidUpdate).toHaveBeenCalledWith({ 176 | name: "Tom Chen" 177 | }, { 178 | age: 0 179 | }); 180 | 181 | comp.componentWillUnmount(); 182 | 183 | (0, _expect2.default)(spies.componentWillUnmount).toHaveBeenCalledWith({ 184 | name: "Tom Chen" 185 | }); 186 | }); 187 | }); 188 | 189 | /* eslint-disable max-len */ 190 | describe("(_1, _2, _3, mapDispatchToActions) with render function", function describeMapDispatchToActions() { 191 | /* eslint-enable max-len */ 192 | context("(_1, _2, _3, mapDispatchToActions)", function contextMapDispatchToActions() { 193 | it("should pass actions as third arguments of render", function it(done) { 194 | var mapDispatchToActions = function mapDispatchToActions(dispatch) { 195 | return { 196 | customAction: function customAction() { 197 | dispatch({ 198 | type: "CUSTOM_ACTION" 199 | }); 200 | } 201 | }; 202 | }; 203 | 204 | var customActionTriggered = false; 205 | 206 | var render = function render(props, state, actions) { 207 | (0, _expect2.default)(actions.customAction).toBeA("function"); 208 | if (!customActionTriggered) { 209 | // Emulate some event handler triggerd this action. 210 | setTimeout(function () { 211 | actions.customAction(); 212 | done(); 213 | }); 214 | customActionTriggered = true; 215 | } 216 | return _react2.default.createElement("div", null); 217 | }; 218 | 219 | var ReduxComponent = (0, _index.Componentize)(_redux.createStore, function () { 220 | return {}; 221 | }, noop, mapDispatchToActions)(render); 222 | 223 | _reactAddonsTestUtils2.default.renderIntoDocument(_react2.default.createElement(ReduxComponent, null)); 224 | }); 225 | }); 226 | 227 | context("render", function contextRender() { 228 | it("should pass props and null state", function it(done) { 229 | var render = function render(props, state, actions) { 230 | (0, _expect2.default)(props).toBeA("object"); 231 | (0, _expect2.default)(props).toEqual({ 232 | name: "Tom Chen" 233 | }); 234 | 235 | (0, _expect2.default)(state).toEqual(null); 236 | done(); 237 | return _react2.default.createElement("div", null); 238 | }; 239 | 240 | var ReduxComponent = (0, _index.Componentize)(_redux.createStore, function () { 241 | return undefined; 242 | }, noop, noop)(render); 243 | 244 | _reactAddonsTestUtils2.default.renderIntoDocument(_react2.default.createElement(ReduxComponent, { 245 | name: "Tom Chen" 246 | })); 247 | }); 248 | 249 | it("should pass props and initial state from reducer", function it(done) { 250 | var render = function render(props, state, actions) { 251 | (0, _expect2.default)(props).toBeA("object"); 252 | (0, _expect2.default)(props).toEqual({ 253 | name: "Tom Chen" 254 | }); 255 | 256 | (0, _expect2.default)(state).toBeA("object"); 257 | (0, _expect2.default)(state).toEqual({ 258 | age: 0 259 | }); 260 | done(); 261 | return _react2.default.createElement("div", null); 262 | }; 263 | 264 | var initialState = { 265 | age: 0 266 | }; 267 | 268 | var ReduxComponent = (0, _index.Componentize)(_redux.createStore, function () { 269 | return initialState; 270 | }, noop, noop)(render); 271 | 272 | _reactAddonsTestUtils2.default.renderIntoDocument(_react2.default.createElement(ReduxComponent, { 273 | name: "Tom Chen" 274 | })); 275 | }); 276 | }); 277 | 278 | context("dispatch an action", function contextDispathAnAction() { 279 | it("should update the state and pass in to render", function it(done) { 280 | var mapDispatchToActions = function mapDispatchToActions(dispatch) { 281 | return { 282 | getOlder: function getOlder() { 283 | dispatch({ 284 | type: "GET_OLDER", 285 | age: 1 286 | }); 287 | } 288 | }; 289 | }; 290 | 291 | var initialRender = true; 292 | 293 | var render = function render(props, state, actions) { 294 | (0, _expect2.default)(props).toBeA("object"); 295 | (0, _expect2.default)(props).toEqual({ 296 | name: "Tom Chen" 297 | }); 298 | 299 | if (initialRender) { 300 | (0, _expect2.default)(state).toBeA("object"); 301 | (0, _expect2.default)(state).toEqual({ 302 | age: 0 303 | }); 304 | // Emulate some event handler triggerd this action. 305 | setTimeout(actions.getOlder); 306 | 307 | initialRender = false; 308 | } else { 309 | (0, _expect2.default)(state).toBeA("object"); 310 | (0, _expect2.default)(state).toEqual({ 311 | age: 1 312 | }); 313 | done(); 314 | } 315 | return _react2.default.createElement("div", null); 316 | }; 317 | 318 | var initialState = { 319 | age: 0 320 | }; 321 | 322 | var reducer = function reducer() { 323 | var state = arguments.length <= 0 || arguments[0] === undefined ? initialState : arguments[0]; 324 | var action = arguments[1]; 325 | 326 | if (action.type === "GET_OLDER") { 327 | return _extends({}, state, { 328 | age: action.age 329 | }); 330 | } 331 | return state; 332 | }; 333 | 334 | var ReduxComponent = (0, _index.Componentize)(_redux.createStore, reducer, noop, mapDispatchToActions)(render); 335 | 336 | _reactAddonsTestUtils2.default.renderIntoDocument(_react2.default.createElement(ReduxComponent, { 337 | name: "Tom Chen" 338 | })); 339 | }); 340 | }); 341 | 342 | it("will clean up Component after unmount", function it() { 343 | var ReduxComponent = (0, _index.Componentize)(_redux.createStore, function () { 344 | return {}; 345 | }, noop, noop)(function () { 346 | return _react2.default.createElement("div", null); 347 | }); 348 | 349 | var div = document.createElement("div"); 350 | 351 | var comp = _reactDom2.default.render(_react2.default.createElement(ReduxComponent, { name: "Tom Chen" }), div); 352 | 353 | _reactDom2.default.unmountComponentAtNode(div); 354 | 355 | (0, _expect2.default)(comp.unsubscribeFromStore).toNotExist(); 356 | (0, _expect2.default)(comp.eventActions).toNotExist(); 357 | (0, _expect2.default)(comp.lifecycleActions).toNotExist(); 358 | (0, _expect2.default)(comp.store).toNotExist(); 359 | }); 360 | }); 361 | }); 362 | }); -------------------------------------------------------------------------------- /lib/components/__tests__/ReduxComponentMixin.spec.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | var _extends = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; }; /* eslint-disable prefer-arrow-callback */ 4 | /* eslint-disable new-cap */ 5 | 6 | var _expect = require("expect"); 7 | 8 | var _expect2 = _interopRequireDefault(_expect); 9 | 10 | var _react = require("react"); 11 | 12 | var _react2 = _interopRequireDefault(_react); 13 | 14 | var _reactAddonsTestUtils = require("react-addons-test-utils"); 15 | 16 | var _reactAddonsTestUtils2 = _interopRequireDefault(_reactAddonsTestUtils); 17 | 18 | var _index = require("../../index"); 19 | 20 | function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } 21 | 22 | describe("redux-component", function describeReduxComponent() { 23 | describe("ReduxComponentMixin", function describeReduxComponentMixin() { 24 | it("should exist", function it() { 25 | (0, _expect2.default)(_index.ReduxComponentMixin).toExist(); 26 | }); 27 | 28 | it("should have signature of (reducer)", function it() { 29 | (0, _expect2.default)(_index.ReduxComponentMixin.length).toEqual(1); 30 | }); 31 | 32 | it("returns a mixin object", function it() { 33 | var mixin = (0, _index.ReduxComponentMixin)(function () { 34 | return {}; 35 | }); 36 | 37 | (0, _expect2.default)(mixin.getInitialState).toBeA("function", "and have getInitialState function"); 38 | (0, _expect2.default)(mixin.componentWillUnmount).toBeA("function", "and have componentWillUnmount function"); 39 | }); 40 | 41 | describe("mixed into React.createClass", function describeMixedIntoReactCreateClass() { 42 | var mockedComp = undefined; 43 | 44 | beforeEach(function beforeEachDescribe() { 45 | var mockedReducer = function mockedReducer() { 46 | var state = arguments.length <= 0 || arguments[0] === undefined ? { value: "INITIAL_STATE" } : arguments[0]; 47 | var action = arguments[1]; 48 | return _extends({}, state, action); 49 | }; 50 | 51 | /* eslint-disable react/prefer-es6-class */ 52 | var MockedComponent = _react2.default.createClass({ 53 | displayName: "MockedComponent", 54 | 55 | mixins: [(0, _index.ReduxComponentMixin)(mockedReducer)], 56 | render: function render() { 57 | return _react2.default.createElement("div", null); 58 | } 59 | }); 60 | /* eslint-enable react/prefer-es6-class */ 61 | 62 | mockedComp = _reactAddonsTestUtils2.default.renderIntoDocument(_react2.default.createElement(MockedComponent, null)); 63 | }); 64 | 65 | it("should have initial state from reducer", function it() { 66 | (0, _expect2.default)(mockedComp.state.value).toEqual("INITIAL_STATE"); 67 | }); 68 | 69 | it("should change the component's state by dispatching an action", function it(done) { 70 | (0, _expect2.default)(mockedComp.state.value).toNotEqual("ANOTHER_VALUE"); 71 | 72 | mockedComp.dispatch({ 73 | type: "CHANGE_STATE", 74 | value: "ANOTHER_VALUE" 75 | }); 76 | 77 | setTimeout(function () { 78 | (0, _expect2.default)(mockedComp.state.type).toEqual("CHANGE_STATE"); 79 | (0, _expect2.default)(mockedComp.state.value).toEqual("ANOTHER_VALUE"); 80 | done(); 81 | }); 82 | }); 83 | }); 84 | }); 85 | }); -------------------------------------------------------------------------------- /lib/components/__tests__/createDispatch.spec.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }(); 4 | 5 | var _extends = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; }; /* eslint-disable prefer-arrow-callback */ 6 | 7 | var _expect = require("expect"); 8 | 9 | var _expect2 = _interopRequireDefault(_expect); 10 | 11 | var _react = require("react"); 12 | 13 | var _react2 = _interopRequireDefault(_react); 14 | 15 | var _reactAddonsTestUtils = require("react-addons-test-utils"); 16 | 17 | var _reactAddonsTestUtils2 = _interopRequireDefault(_reactAddonsTestUtils); 18 | 19 | var _index = require("../../index"); 20 | 21 | function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } 22 | 23 | function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } 24 | 25 | function _possibleConstructorReturn(self, call) { if (!self) { throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); } return call && (typeof call === "object" || typeof call === "function") ? call : self; } 26 | 27 | function _inherits(subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function, not " + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass; } 28 | 29 | describe("redux-component", function describeReduxComponent() { 30 | describe("createDispatch", function describeCreateDispatch() { 31 | it("should exist", function it() { 32 | (0, _expect2.default)(_index.createDispatch).toExist(); 33 | }); 34 | 35 | it("should have signature of (component, reducer)", function it() { 36 | (0, _expect2.default)(_index.createDispatch.length).toEqual(2); 37 | }); 38 | 39 | describe("returns function dispatch", function describeReturnsFunctionDispatch() { 40 | var mockedComp = undefined; 41 | 42 | beforeEach(function beforeEachDescribe() { 43 | var mockedReducer = function mockedReducer() { 44 | var state = arguments.length <= 0 || arguments[0] === undefined ? { value: "INITIAL_STATE" } : arguments[0]; 45 | var action = arguments[1]; 46 | return _extends({}, state, action); 47 | }; 48 | 49 | var MockedComponent = function (_Component) { 50 | _inherits(MockedComponent, _Component); 51 | 52 | function MockedComponent() { 53 | var _Object$getPrototypeO; 54 | 55 | _classCallCheck(this, MockedComponent); 56 | 57 | for (var _len = arguments.length, args = Array(_len), _key = 0; _key < _len; _key++) { 58 | args[_key] = arguments[_key]; 59 | } 60 | 61 | var _this = _possibleConstructorReturn(this, (_Object$getPrototypeO = Object.getPrototypeOf(MockedComponent)).call.apply(_Object$getPrototypeO, [this].concat(args))); 62 | 63 | _this.dispatch = (0, _index.createDispatch)(_this, mockedReducer); 64 | return _this; 65 | } 66 | 67 | _createClass(MockedComponent, [{ 68 | key: "render", 69 | value: function render() { 70 | return _react2.default.createElement("div", null); 71 | } 72 | }]); 73 | 74 | return MockedComponent; 75 | }(_react.Component); 76 | 77 | mockedComp = _reactAddonsTestUtils2.default.renderIntoDocument(_react2.default.createElement(MockedComponent, null)); 78 | }); 79 | 80 | it("should have initial state from reducer", function it() { 81 | (0, _expect2.default)(mockedComp.state.value).toEqual("INITIAL_STATE"); 82 | }); 83 | 84 | it("should change the component's state by dispatching an action", function it(done) { 85 | (0, _expect2.default)(mockedComp.state.value).toNotEqual("ANOTHER_VALUE"); 86 | 87 | mockedComp.dispatch({ 88 | type: "CHANGE_STATE", 89 | value: "ANOTHER_VALUE" 90 | }); 91 | 92 | setTimeout(function () { 93 | (0, _expect2.default)(mockedComp.state.type).toEqual("CHANGE_STATE"); 94 | (0, _expect2.default)(mockedComp.state.value).toEqual("ANOTHER_VALUE"); 95 | done(); 96 | }); 97 | }); 98 | }); 99 | }); 100 | }); -------------------------------------------------------------------------------- /lib/components/createDispatch.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | Object.defineProperty(exports, "__esModule", { 4 | value: true 5 | }); 6 | exports.createDispatchWithStore = createDispatchWithStore; 7 | exports.default = createDispatch; 8 | 9 | var _redux = require("redux"); 10 | 11 | function noop() {} 12 | 13 | function createDispatchWithStore(component, store) { 14 | /* eslint-disable no-param-reassign */ 15 | component.state = store.getState(); 16 | 17 | var unsubscribeFromStore = store.subscribe(function () { 18 | component.setState(store.getState()); 19 | }); 20 | 21 | var oldComponentWillUnmount = component.componentWillUnmount || noop; 22 | 23 | component.componentWillUnmount = function () { 24 | unsubscribeFromStore(); 25 | oldComponentWillUnmount.call(component); 26 | }; 27 | 28 | return store.dispatch; 29 | /* eslint-enable no-param-reassign */ 30 | } 31 | 32 | function createDispatch(component, reducer) { 33 | return createDispatchWithStore(component, (0, _redux.createStore)(reducer)); 34 | } -------------------------------------------------------------------------------- /lib/index.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | Object.defineProperty(exports, "__esModule", { 4 | value: true 5 | }); 6 | exports.ReduxComponentMixin = exports.createDispatch = exports.Componentize = undefined; 7 | 8 | var _react = require("react"); 9 | 10 | var _react2 = _interopRequireDefault(_react); 11 | 12 | var _ComponentizeCreator = require("./components/ComponentizeCreator"); 13 | 14 | var _ComponentizeCreator2 = _interopRequireDefault(_ComponentizeCreator); 15 | 16 | var _createDispatch = require("./components/createDispatch"); 17 | 18 | var _createDispatch2 = _interopRequireDefault(_createDispatch); 19 | 20 | var _ReduxComponentMixin = require("./components/ReduxComponentMixin"); 21 | 22 | var _ReduxComponentMixin2 = _interopRequireDefault(_ReduxComponentMixin); 23 | 24 | function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } 25 | 26 | /* eslint-disable new-cap */ 27 | 28 | var Componentize = exports.Componentize = (0, _ComponentizeCreator2.default)(_react2.default); 29 | 30 | exports.createDispatch = _createDispatch2.default; 31 | exports.ReduxComponentMixin = _ReduxComponentMixin2.default; -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "redux-component", 3 | "version": "0.3.0", 4 | "description": "Create stateful React Component using goodness from redux", 5 | "main": "lib/index.js", 6 | "files": [ 7 | "lib/", 8 | "src/", 9 | "CHANGELOG.md" 10 | ], 11 | "scripts": { 12 | "clean": "rimraf lib", 13 | "prebuild": "npm run lint && npm run clean", 14 | "build": "cross-env NODE_ENV=production babel src --out-dir lib", 15 | "lint": "cross-env NODE_ENV=test eslint .", 16 | "pretest:cov": "npm run lint", 17 | "pretest": "npm run lint", 18 | "test:cov": "cross-env NODE_ENV=test babel-node ./node_modules/.bin/isparta cover --report lcov _mocha -- $npm_package_config_mocha", 19 | "test:watch": "npm test -- --watch", 20 | "test": "cross-env NODE_ENV=test mocha $npm_package_config_mocha" 21 | }, 22 | "config": { 23 | "mocha": "--compilers js:babel-register ./src/**/__tests__/*.spec.js --require ./src/__tests__/setup.js" 24 | }, 25 | "repository": { 26 | "type": "git", 27 | "url": "https://github.com/tomchentw/redux-component" 28 | }, 29 | "keywords": [ 30 | "react", 31 | "react-component", 32 | "flux", 33 | "redux", 34 | "component" 35 | ], 36 | "author": { 37 | "name": "tomchentw", 38 | "email": "developer@tomchentw.com", 39 | "url": "https://github.com/tomchentw" 40 | }, 41 | "license": "MIT", 42 | "bugs": { 43 | "url": "https://github.com/tomchentw/redux-component/issues" 44 | }, 45 | "homepage": "https://tomchentw.github.io/redux-component/", 46 | "devDependencies": { 47 | "babel-cli": "^6.4.5", 48 | "babel-core": "^6.4.5", 49 | "babel-eslint": "^5.0.0-beta6", 50 | "babel-plugin-transform-flow-comments": "^6.4.0", 51 | "babel-plugin-typecheck": "^3.6.1", 52 | "babel-preset-es2015": "^6.3.13", 53 | "babel-preset-react": "^6.3.13", 54 | "babel-preset-stage-0": "^6.3.13", 55 | "babel-register": "^6.4.3", 56 | "codeclimate-test-reporter": "^0.3.0", 57 | "cross-env": "^1.0.7", 58 | "eslint": "^1.10.3", 59 | "eslint-config-airbnb": "^4.0.0", 60 | "eslint-plugin-react": "^3.16.1", 61 | "expect": "^1.13.4", 62 | "isparta": "^4.0.0", 63 | "istanbul": "^0.4.2", 64 | "jsdom": "^8.0.0", 65 | "mocha": "^2.3.4", 66 | "react": "^0.14.6", 67 | "react-addons-test-utils": "^0.14.6", 68 | "react-dom": "^0.14.6", 69 | "redux": "^3.0.6", 70 | "rimraf": "^2.5.1", 71 | "tomchentw-npm-dev": "^3.2.0" 72 | }, 73 | "dependencies": { 74 | "invariant": "^2.2.0" 75 | }, 76 | "peerDependencies": { 77 | "redux": "^3.0.0" 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /src/__tests__/setup.js: -------------------------------------------------------------------------------- 1 | import { 2 | jsdom, 3 | } from "jsdom"; 4 | 5 | global.document = jsdom(``); 6 | global.window = document.defaultView; 7 | global.navigator = global.window.navigator; 8 | -------------------------------------------------------------------------------- /src/components/ComponentizeCreator.js: -------------------------------------------------------------------------------- 1 | import { 2 | createDispatchWithStore, 3 | } from "./createDispatch"; 4 | 5 | export default function createComponentize(React) { 6 | const { 7 | Component, 8 | } = React; 9 | 10 | const NullLifecycleActions = { 11 | componentWillMount() {}, 12 | componentDidMount() {}, 13 | componentWillReceiveProps() {}, 14 | componentWillUpdate() {}, 15 | componentDidUpdate() {}, 16 | componentWillUnmount() {}, 17 | }; 18 | 19 | return function Componentize(createStore, reducer, mapDispatchToLifecycle, mapDispatchToActions) { 20 | // 21 | return function createComponent(render) { 22 | // 23 | return class ReduxComponent extends Component { 24 | constructor(...args) { 25 | super(...args); 26 | const dispatch = createDispatchWithStore(this, createStore(reducer)); 27 | 28 | this.lifecycleActions = { 29 | ...NullLifecycleActions, 30 | // TODO: componentWillReceiveProps 31 | ...mapDispatchToLifecycle(dispatch), 32 | }; 33 | 34 | this.eventActions = mapDispatchToActions(dispatch); 35 | } 36 | 37 | componentWillMount() { 38 | this.lifecycleActions.componentWillMount(this.props); 39 | } 40 | 41 | componentDidMount() { 42 | this.lifecycleActions.componentDidMount(this.props); 43 | } 44 | 45 | componentWillReceiveProps(nextProps: Object) { 46 | this.lifecycleActions.componentWillReceiveProps(this.props, nextProps); 47 | } 48 | 49 | componentWillUpdate(nextProps: Object, nextState: Object) { 50 | this.lifecycleActions.componentWillUpdate(this.props, nextProps); 51 | } 52 | 53 | componentDidUpdate(prevProps: Object, prevState: Object) { 54 | this.lifecycleActions.componentDidUpdate(this.props, prevProps); 55 | } 56 | 57 | componentWillUnmount() { 58 | this.eventActions = null; 59 | 60 | this.lifecycleActions.componentWillUnmount(this.props); 61 | this.lifecycleActions = null; 62 | } 63 | 64 | render() { 65 | return render(this.props, this.state, this.eventActions); 66 | } 67 | }; 68 | }; 69 | }; 70 | } 71 | -------------------------------------------------------------------------------- /src/components/ReduxComponentMixin.js: -------------------------------------------------------------------------------- 1 | import { 2 | createStore, 3 | } from "redux"; 4 | 5 | export default function ReduxComponentMixin(reducer) { 6 | return { 7 | getInitialState() { 8 | this.store = createStore(reducer); 9 | this.unsubscribeFromStore = this.store.subscribe(() => { 10 | this.setState(this.store.getState()); 11 | }); 12 | this.dispatch = this.store.dispatch; 13 | return this.store.getState(); 14 | }, 15 | 16 | componentWillUnmount() { 17 | this.unsubscribeFromStore(); 18 | }, 19 | }; 20 | } 21 | -------------------------------------------------------------------------------- /src/components/__tests__/Componentize.spec.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable prefer-arrow-callback */ 2 | /* eslint-disable new-cap */ 3 | 4 | import { 5 | default as expect, 6 | } from "expect"; 7 | 8 | import { 9 | default as React, 10 | Component, 11 | } from "react"; 12 | 13 | import { 14 | default as ReactDOM, 15 | } from "react-dom"; 16 | 17 | import { 18 | default as TestUtils, 19 | } from "react-addons-test-utils"; 20 | 21 | import { 22 | createStore, 23 | } from "redux"; 24 | 25 | import { 26 | Componentize, 27 | } from "../../index"; 28 | 29 | function noop() { 30 | } 31 | 32 | describe(`React`, function describeReact() { 33 | describe(`Componentize`, function describeComponentize() { 34 | it(`should exist`, function it() { 35 | expect(Componentize).toExist(); 36 | }); 37 | 38 | context(`when called`, function contextWhenCalled() { 39 | it(`returns a function`, function it() { 40 | expect(Componentize()).toBeA(`function`); 41 | }); 42 | 43 | context(`when called with "render" function`, function contextWhenCalledWithRender() { 44 | it(`returns ReduxComponent, a React.Component class`, function it() { 45 | const ReduxComponent = Componentize()(); 46 | 47 | expect(ReduxComponent.prototype).toBeA(Component); 48 | expect(ReduxComponent.prototype.render).toBeA(`function`); 49 | }); 50 | }); 51 | }); 52 | 53 | describe(`(_1, _2, mapDispatchToLifecycle)`, function describeMapDispatchToLifecycle() { 54 | it(`should contain React.Component lifecycle functions`, function it() { 55 | const ReduxComponent = Componentize(createStore, () => ({}), noop, noop)(); 56 | const comp = new ReduxComponent(); 57 | 58 | expect(comp.componentWillMount).toBeA(`function`); 59 | expect(comp.componentDidMount).toBeA(`function`); 60 | expect(comp.componentWillReceiveProps).toBeA(`function`); 61 | expect(comp.componentWillUpdate).toBeA(`function`); 62 | expect(comp.componentDidUpdate).toBeA(`function`); 63 | expect(comp.componentWillUnmount).toBeA(`function`); 64 | }); 65 | 66 | it(`should invoke action inside React.Component lifecycle functions`, function it() { 67 | const lifecycleCallbacks = { 68 | componentWillMount() {}, 69 | componentDidMount() {}, 70 | componentWillReceiveProps() {}, 71 | componentWillUpdate() {}, 72 | componentDidUpdate() {}, 73 | componentWillUnmount() {}, 74 | }; 75 | 76 | const spies = Object.keys(lifecycleCallbacks).reduce((acc, key) => { 77 | /* eslint-disable no-param-reassign */ 78 | acc[key] = expect.spyOn(lifecycleCallbacks, key); 79 | /* eslint-enable no-param-reassign */ 80 | return acc; 81 | }, {}); 82 | 83 | const mapDispatchToLifecycle = () => lifecycleCallbacks; 84 | 85 | const ReduxComponent = Componentize( 86 | createStore, () => ({}), mapDispatchToLifecycle, noop 87 | )(); 88 | const comp = new ReduxComponent(); 89 | 90 | Object.keys(spies).forEach(key => 91 | expect(spies[key]).toNotHaveBeenCalled() 92 | ); 93 | 94 | Object.keys(lifecycleCallbacks).forEach(key => 95 | comp[key]({}, {}) 96 | ); 97 | 98 | Object.keys(spies).forEach(key => 99 | expect(spies[key]).toHaveBeenCalled() 100 | ); 101 | }); 102 | 103 | /* eslint-disable max-len */ 104 | it(`should invoke actions with correct arguments in certain Component lifecycle functions`, function it() { 105 | /* eslint-enable max-len */ 106 | const lifecycleCallbacks = { 107 | componentWillMount(props) {}, 108 | componentDidMount(props) {}, 109 | componentWillReceiveProps(props, nextProps) {}, 110 | componentWillUpdate(props, nextProps) {}, 111 | componentDidUpdate(props, prevProps) {}, 112 | componentWillUnmount(props) {}, 113 | }; 114 | 115 | const spies = Object.keys(lifecycleCallbacks).reduce((acc, key) => { 116 | /* eslint-disable no-param-reassign */ 117 | acc[key] = expect.spyOn(lifecycleCallbacks, key); 118 | /* eslint-enable no-param-reassign */ 119 | return acc; 120 | }, {}); 121 | 122 | const mapDispatchToLifecycle = () => lifecycleCallbacks; 123 | 124 | const ReduxComponent = Componentize( 125 | createStore, () => ({}), mapDispatchToLifecycle, noop 126 | )(); 127 | const comp = new ReduxComponent({ 128 | name: `Tom Chen`, 129 | }); 130 | 131 | Object.keys(spies).forEach(key => 132 | expect(spies[key]).toNotHaveBeenCalled() 133 | ); 134 | 135 | comp.componentWillMount(); 136 | 137 | expect(spies.componentWillMount).toHaveBeenCalledWith({ 138 | name: `Tom Chen`, 139 | }); 140 | 141 | comp.componentDidMount(); 142 | 143 | expect(spies.componentDidMount).toHaveBeenCalledWith({ 144 | name: `Tom Chen`, 145 | }); 146 | 147 | comp.componentWillReceiveProps({ 148 | email: `developer@tomchentw.com`, 149 | }); 150 | 151 | expect(spies.componentWillReceiveProps).toHaveBeenCalledWith({ 152 | name: `Tom Chen`, 153 | }, { 154 | email: `developer@tomchentw.com`, 155 | }); 156 | 157 | comp.componentWillUpdate({ 158 | email: `developer@tomchentw.com`, 159 | }, {}); 160 | 161 | expect(spies.componentWillUpdate).toHaveBeenCalledWith({ 162 | name: `Tom Chen`, 163 | }, { 164 | email: `developer@tomchentw.com`, 165 | }); 166 | 167 | comp.componentDidUpdate({ 168 | age: 0, 169 | }, {}); 170 | 171 | expect(spies.componentDidUpdate).toHaveBeenCalledWith({ 172 | name: `Tom Chen`, 173 | }, { 174 | age: 0, 175 | }); 176 | 177 | comp.componentWillUnmount(); 178 | 179 | expect(spies.componentWillUnmount).toHaveBeenCalledWith({ 180 | name: `Tom Chen`, 181 | }); 182 | }); 183 | }); 184 | 185 | /* eslint-disable max-len */ 186 | describe(`(_1, _2, _3, mapDispatchToActions) with render function`, function describeMapDispatchToActions() { 187 | /* eslint-enable max-len */ 188 | context(`(_1, _2, _3, mapDispatchToActions)`, function contextMapDispatchToActions() { 189 | it(`should pass actions as third arguments of render`, function it(done) { 190 | const mapDispatchToActions = dispatch => ({ 191 | customAction() { 192 | dispatch({ 193 | type: `CUSTOM_ACTION`, 194 | }); 195 | }, 196 | }); 197 | 198 | let customActionTriggered = false; 199 | 200 | const render = (props, state, actions) => { 201 | expect(actions.customAction).toBeA(`function`); 202 | if (!customActionTriggered) { 203 | // Emulate some event handler triggerd this action. 204 | setTimeout(() => { 205 | actions.customAction(); 206 | done(); 207 | }); 208 | customActionTriggered = true; 209 | } 210 | return
; 211 | }; 212 | 213 | const ReduxComponent = Componentize( 214 | createStore, () => ({}), noop, mapDispatchToActions 215 | )(render); 216 | 217 | TestUtils.renderIntoDocument(); 218 | }); 219 | }); 220 | 221 | context(`render`, function contextRender() { 222 | it(`should pass props and null state`, function it(done) { 223 | const render = (props, state, actions) => { 224 | expect(props).toBeA(`object`); 225 | expect(props).toEqual({ 226 | name: `Tom Chen`, 227 | }); 228 | 229 | expect(state).toEqual(null); 230 | done(); 231 | return
; 232 | }; 233 | 234 | const ReduxComponent = Componentize(createStore, () => undefined, noop, noop)(render); 235 | 236 | TestUtils.renderIntoDocument( 237 | 240 | ); 241 | }); 242 | 243 | it(`should pass props and initial state from reducer`, function it(done) { 244 | const render = (props, state, actions) => { 245 | expect(props).toBeA(`object`); 246 | expect(props).toEqual({ 247 | name: `Tom Chen`, 248 | }); 249 | 250 | expect(state).toBeA(`object`); 251 | expect(state).toEqual({ 252 | age: 0, 253 | }); 254 | done(); 255 | return
; 256 | }; 257 | 258 | const initialState = { 259 | age: 0, 260 | }; 261 | 262 | const ReduxComponent = Componentize(createStore, () => initialState, noop, noop)(render); 263 | 264 | TestUtils.renderIntoDocument( 265 | 268 | ); 269 | }); 270 | }); 271 | 272 | context(`dispatch an action`, function contextDispathAnAction() { 273 | it(`should update the state and pass in to render`, function it(done) { 274 | const mapDispatchToActions = dispatch => ({ 275 | getOlder() { 276 | dispatch({ 277 | type: `GET_OLDER`, 278 | age: 1, 279 | }); 280 | }, 281 | }); 282 | 283 | let initialRender = true; 284 | 285 | const render = (props, state, actions) => { 286 | expect(props).toBeA(`object`); 287 | expect(props).toEqual({ 288 | name: `Tom Chen`, 289 | }); 290 | 291 | if (initialRender) { 292 | expect(state).toBeA(`object`); 293 | expect(state).toEqual({ 294 | age: 0, 295 | }); 296 | // Emulate some event handler triggerd this action. 297 | setTimeout(actions.getOlder); 298 | 299 | initialRender = false; 300 | } else { 301 | expect(state).toBeA(`object`); 302 | expect(state).toEqual({ 303 | age: 1, 304 | }); 305 | done(); 306 | } 307 | return ( 308 |
309 | ); 310 | }; 311 | 312 | const initialState = { 313 | age: 0, 314 | }; 315 | 316 | const reducer = (state = initialState, action) => { 317 | if (action.type === `GET_OLDER`) { 318 | return { 319 | ...state, 320 | age: action.age, 321 | }; 322 | } 323 | return state; 324 | }; 325 | 326 | const ReduxComponent = Componentize( 327 | createStore, reducer, noop, mapDispatchToActions 328 | )(render); 329 | 330 | TestUtils.renderIntoDocument( 331 | 334 | ); 335 | }); 336 | }); 337 | 338 | it(`will clean up Component after unmount`, function it() { 339 | const ReduxComponent = Componentize(createStore, () => ({}), noop, noop)(() => (
)); 340 | 341 | const div = document.createElement(`div`); 342 | 343 | const comp = ReactDOM.render( 344 | 345 | , div); 346 | 347 | ReactDOM.unmountComponentAtNode(div); 348 | 349 | expect(comp.unsubscribeFromStore).toNotExist(); 350 | expect(comp.eventActions).toNotExist(); 351 | expect(comp.lifecycleActions).toNotExist(); 352 | expect(comp.store).toNotExist(); 353 | }); 354 | }); 355 | }); 356 | }); 357 | -------------------------------------------------------------------------------- /src/components/__tests__/ReduxComponentMixin.spec.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable prefer-arrow-callback */ 2 | /* eslint-disable new-cap */ 3 | 4 | import { 5 | default as expect, 6 | } from "expect"; 7 | 8 | import { 9 | default as React, 10 | } from "react"; 11 | 12 | import { 13 | default as TestUtils, 14 | } from "react-addons-test-utils"; 15 | 16 | import { 17 | ReduxComponentMixin, 18 | } from "../../index"; 19 | 20 | describe(`redux-component`, function describeReduxComponent() { 21 | describe(`ReduxComponentMixin`, function describeReduxComponentMixin() { 22 | it(`should exist`, function it() { 23 | expect(ReduxComponentMixin).toExist(); 24 | }); 25 | 26 | it(`should have signature of (reducer)`, function it() { 27 | expect(ReduxComponentMixin.length).toEqual(1); 28 | }); 29 | 30 | it(`returns a mixin object`, function it() { 31 | const mixin = ReduxComponentMixin(() => ({})); 32 | 33 | expect(mixin.getInitialState).toBeA(`function`, `and have getInitialState function`); 34 | expect(mixin.componentWillUnmount).toBeA( 35 | `function`, `and have componentWillUnmount function` 36 | ); 37 | }); 38 | 39 | describe(`mixed into React.createClass`, function describeMixedIntoReactCreateClass() { 40 | let mockedComp; 41 | 42 | beforeEach(function beforeEachDescribe() { 43 | const mockedReducer = (state = { value: `INITIAL_STATE` }, action) => ( 44 | { ...state, ...action } 45 | ); 46 | 47 | /* eslint-disable react/prefer-es6-class */ 48 | const MockedComponent = React.createClass({ 49 | mixins: [ReduxComponentMixin(mockedReducer)], 50 | render() { return
; }, 51 | }); 52 | /* eslint-enable react/prefer-es6-class */ 53 | 54 | mockedComp = TestUtils.renderIntoDocument(); 55 | }); 56 | 57 | it(`should have initial state from reducer`, function it() { 58 | expect(mockedComp.state.value).toEqual(`INITIAL_STATE`); 59 | }); 60 | 61 | it(`should change the component's state by dispatching an action`, function it(done) { 62 | expect(mockedComp.state.value).toNotEqual(`ANOTHER_VALUE`); 63 | 64 | mockedComp.dispatch({ 65 | type: `CHANGE_STATE`, 66 | value: `ANOTHER_VALUE`, 67 | }); 68 | 69 | setTimeout(() => { 70 | expect(mockedComp.state.type).toEqual(`CHANGE_STATE`); 71 | expect(mockedComp.state.value).toEqual(`ANOTHER_VALUE`); 72 | done(); 73 | }); 74 | }); 75 | }); 76 | }); 77 | }); 78 | -------------------------------------------------------------------------------- /src/components/__tests__/createDispatch.spec.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable prefer-arrow-callback */ 2 | 3 | import { 4 | default as expect, 5 | } from "expect"; 6 | 7 | import { 8 | default as React, 9 | Component, 10 | } from "react"; 11 | 12 | import { 13 | default as TestUtils, 14 | } from "react-addons-test-utils"; 15 | 16 | import { 17 | createDispatch, 18 | } from "../../index"; 19 | 20 | describe(`redux-component`, function describeReduxComponent() { 21 | describe(`createDispatch`, function describeCreateDispatch() { 22 | it(`should exist`, function it() { 23 | expect(createDispatch).toExist(); 24 | }); 25 | 26 | it(`should have signature of (component, reducer)`, function it() { 27 | expect(createDispatch.length).toEqual(2); 28 | }); 29 | 30 | describe(`returns function dispatch`, function describeReturnsFunctionDispatch() { 31 | let mockedComp; 32 | 33 | beforeEach(function beforeEachDescribe() { 34 | const mockedReducer = (state = { value: `INITIAL_STATE` }, action) => ( 35 | { ...state, ...action } 36 | ); 37 | 38 | class MockedComponent extends Component { 39 | constructor(...args) { 40 | super(...args); 41 | this.dispatch = createDispatch(this, mockedReducer); 42 | } 43 | 44 | render() { return
; } 45 | } 46 | 47 | mockedComp = TestUtils.renderIntoDocument(); 48 | }); 49 | 50 | it(`should have initial state from reducer`, function it() { 51 | expect(mockedComp.state.value).toEqual(`INITIAL_STATE`); 52 | }); 53 | 54 | it(`should change the component's state by dispatching an action`, function it(done) { 55 | expect(mockedComp.state.value).toNotEqual(`ANOTHER_VALUE`); 56 | 57 | mockedComp.dispatch({ 58 | type: `CHANGE_STATE`, 59 | value: `ANOTHER_VALUE`, 60 | }); 61 | 62 | setTimeout(() => { 63 | expect(mockedComp.state.type).toEqual(`CHANGE_STATE`); 64 | expect(mockedComp.state.value).toEqual(`ANOTHER_VALUE`); 65 | done(); 66 | }); 67 | }); 68 | }); 69 | }); 70 | }); 71 | -------------------------------------------------------------------------------- /src/components/createDispatch.js: -------------------------------------------------------------------------------- 1 | import { 2 | createStore, 3 | } from "redux"; 4 | 5 | function noop() { 6 | } 7 | 8 | export function createDispatchWithStore(component, store) { 9 | /* eslint-disable no-param-reassign */ 10 | component.state = store.getState(); 11 | 12 | const unsubscribeFromStore = store.subscribe(() => { 13 | component.setState(store.getState()); 14 | }); 15 | 16 | const oldComponentWillUnmount = component.componentWillUnmount || noop; 17 | 18 | component.componentWillUnmount = () => { 19 | unsubscribeFromStore(); 20 | oldComponentWillUnmount.call(component); 21 | }; 22 | 23 | return store.dispatch; 24 | /* eslint-enable no-param-reassign */ 25 | } 26 | 27 | export default function createDispatch(component, reducer) { 28 | return createDispatchWithStore(component, createStore(reducer)); 29 | } 30 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable new-cap */ 2 | 3 | import { 4 | default as React, 5 | } from "react"; 6 | 7 | import { 8 | default as ComponentizeCreator, 9 | } from "./components/ComponentizeCreator"; 10 | 11 | import { 12 | default as createDispatch, 13 | } from "./components/createDispatch"; 14 | 15 | import { 16 | default as ReduxComponentMixin, 17 | } from "./components/ReduxComponentMixin"; 18 | 19 | export const Componentize = ComponentizeCreator(React); 20 | 21 | export { createDispatch }; 22 | 23 | export { ReduxComponentMixin }; 24 | --------------------------------------------------------------------------------