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 |
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 |
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 |
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 |
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 |
--------------------------------------------------------------------------------