├── .eslintignore ├── .gitignore ├── .browserslistrc ├── .travis.yml ├── tests ├── fixtures │ ├── types.js │ ├── container.js │ ├── actions.js │ └── reducer.js ├── helper.js ├── types.js ├── reset.js ├── container.jsx ├── reducer.js └── actions.js ├── .eslintrc ├── appveyor.yml ├── gulpfile.js ├── .babelrc ├── .editorconfig ├── .nycrc ├── docs ├── types.md ├── install.md ├── container.md ├── use.md ├── actions.md ├── reducer.md └── options.md ├── package.json ├── README.md └── src └── index.js /.eslintignore: -------------------------------------------------------------------------------- 1 | lib 2 | coverage 3 | node_modules 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | lib 3 | !lib/.gitkeep 4 | node_modules 5 | coverage 6 | .nyc_output 7 | -------------------------------------------------------------------------------- /.browserslistrc: -------------------------------------------------------------------------------- 1 | # Browsers that we support 2 | 3 | > 1% 4 | Last 2 versions 5 | IE 10 # sorry 6 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "8" 4 | - "9" 5 | - "10" 6 | script: 7 | - npm test 8 | - npm run report 9 | -------------------------------------------------------------------------------- /tests/fixtures/types.js: -------------------------------------------------------------------------------- 1 | export const POST_TITLE = '@@post/TITLE'; 2 | export const POST_BODY = '@@post/BODY'; 3 | export const POST_SUBMIT = '@@post/SUBMIT'; 4 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "parser": "babel-eslint", 3 | "extends": "airbnb", 4 | "env": { 5 | "browser": true, 6 | "node": true, 7 | "mocha": true 8 | }, 9 | "rules": { 10 | "react/destructuring-assignment": 0 11 | } 12 | } 13 | 14 | -------------------------------------------------------------------------------- /appveyor.yml: -------------------------------------------------------------------------------- 1 | environment: 2 | matrix: 3 | - nodejs_version: "8" 4 | install: 5 | - ps: Install-Product node $env:nodejs_version 6 | - npm install 7 | test_script: 8 | - node --version 9 | - npm --version 10 | - npm test 11 | build: off 12 | -------------------------------------------------------------------------------- /gulpfile.js: -------------------------------------------------------------------------------- 1 | const gulp = require('gulp'); 2 | const babel = require('gulp-babel'); 3 | const cleanDest = require('gulp-clean-dest'); 4 | 5 | gulp.task('default', () => gulp.src(['./src/index.js']) 6 | .pipe(cleanDest('lib')) 7 | .pipe(babel()) 8 | .pipe(gulp.dest('lib'))); 9 | -------------------------------------------------------------------------------- /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | "@babel/preset-env", 4 | "@babel/preset-react" 5 | ], 6 | "plugins": [ 7 | "@babel/plugin-proposal-class-properties", 8 | "@babel/plugin-proposal-object-rest-spread", 9 | "@babel/plugin-syntax-dynamic-import" 10 | ] 11 | } 12 | -------------------------------------------------------------------------------- /tests/fixtures/container.js: -------------------------------------------------------------------------------- 1 | import { connect } from 'react-redux'; 2 | import * as actions from './actions'; 3 | 4 | export const POST = '@@post/POST'; 5 | 6 | const mapStateToProps = state => state[POST]; 7 | const mapDispatchToProps = { ...actions }; 8 | 9 | export default connect(mapStateToProps, mapDispatchToProps); 10 | -------------------------------------------------------------------------------- /.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 | # Set default charse 12 | charset = utf-8 13 | 14 | # 2 space indentation 15 | indent_style = space 16 | indent_size = 2 17 | -------------------------------------------------------------------------------- /tests/fixtures/actions.js: -------------------------------------------------------------------------------- 1 | import { 2 | POST_TITLE, 3 | POST_BODY, 4 | POST_SUBMIT, 5 | } from './types'; 6 | 7 | export const titleAction = title => ({ 8 | type: POST_TITLE, 9 | title, 10 | }); 11 | 12 | export const bodyAction = body => ({ 13 | type: POST_BODY, 14 | body, 15 | }); 16 | 17 | export const submitAction = () => ({ 18 | type: POST_SUBMIT, 19 | }); 20 | -------------------------------------------------------------------------------- /tests/fixtures/reducer.js: -------------------------------------------------------------------------------- 1 | import { POST_TITLE, POST_BODY } from './types'; 2 | 3 | export const defaultState = { 4 | title: '', 5 | body: '', 6 | }; 7 | 8 | export default (state = defaultState, action) => { 9 | switch (action.type) { 10 | case POST_TITLE: 11 | case POST_BODY: 12 | return { ...state, ...action }; 13 | default: 14 | return state; 15 | } 16 | }; 17 | -------------------------------------------------------------------------------- /.nycrc: -------------------------------------------------------------------------------- 1 | { 2 | "extension": [".js",".jsx"], 3 | "require": ["./tests/helper.js"], 4 | "exclude": [ 5 | "node_modules", 6 | "lib", 7 | "coverage", 8 | "tests", 9 | "gulpfile.js" 10 | ], 11 | "check-coverage": true, 12 | "per-file": false, 13 | "statements": 100, 14 | "branches": 98, 15 | "functions": 100, 16 | "lines": 100, 17 | "reporter": [ 18 | "lcov", 19 | "text", 20 | "text-summary", 21 | "html" 22 | ], 23 | "all": true 24 | } 25 | -------------------------------------------------------------------------------- /tests/helper.js: -------------------------------------------------------------------------------- 1 | require('@babel/register'); 2 | 3 | const { configure } = require('enzyme'); 4 | const Adapter = require('enzyme-adapter-react-16'); 5 | 6 | configure({ adapter: new Adapter() }); 7 | 8 | const { JSDOM } = require('jsdom'); 9 | 10 | const exposedProperties = ['window', 'navigator', 'document']; 11 | 12 | const { window } = new JSDOM('', { url: 'http://localhost' }); 13 | global.document = window.document; 14 | global.window = window; 15 | Object.keys(document.defaultView).forEach((property) => { 16 | if (typeof global[property] === 'undefined') { 17 | exposedProperties.push(property); 18 | global[property] = document.defaultView[property]; 19 | } 20 | }); 21 | 22 | global.navigator = { 23 | userAgent: 'node.js', 24 | }; 25 | 26 | global.localStorage = { 27 | setItem() {}, 28 | }; 29 | 30 | global.documentRef = document; 31 | -------------------------------------------------------------------------------- /tests/types.js: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai'; 2 | import RL from '../src'; 3 | import * as types from './fixtures/types'; 4 | 5 | const rl = new RL('post'); 6 | 7 | rl.addAction('title', { title: '' }); 8 | rl.addAction('body', { body: '' }); 9 | rl.addAction('submit'); 10 | 11 | const { types: rlTypes } = rl.flush(); 12 | 13 | describe('Testing types', () => { 14 | describe('Redux', () => { 15 | it('should test POST_TITLE', () => { 16 | expect(types.POST_TITLE).to.be.equal('@@post/TITLE'); 17 | }); 18 | 19 | it('should test POST_BODY', () => { 20 | expect(types.POST_BODY).to.be.equal('@@post/BODY'); 21 | }); 22 | 23 | it('should test POST_SUBMIT', () => { 24 | expect(types.POST_SUBMIT).to.be.equal('@@post/SUBMIT'); 25 | }); 26 | }); 27 | 28 | describe('Redux Lazy', () => { 29 | it('should test POST_TITLE', () => { 30 | expect(rlTypes.POST_TITLE).to.be.equal('@@post/TITLE'); 31 | }); 32 | 33 | it('should test POST_BODY', () => { 34 | expect(rlTypes.POST_BODY).to.be.equal('@@post/BODY'); 35 | }); 36 | 37 | it('should test POST_SUBMIT', () => { 38 | expect(rlTypes.POST_SUBMIT).to.be.equal('@@post/SUBMIT'); 39 | }); 40 | 41 | it('should test type with camelCase to underscore', () => { 42 | const newRl = new RL('newPost'); 43 | newRl.addAction('newTitle', { title: '' }); 44 | const { types: newRlTypes } = newRl.flush(); 45 | 46 | expect(newRlTypes.NEW_POST_NEW_TITLE).to.be.equal('@@newPost/NEW_TITLE'); 47 | }); 48 | }); 49 | }); 50 | -------------------------------------------------------------------------------- /docs/types.md: -------------------------------------------------------------------------------- 1 | # Types 2 | 3 | ## If you use redux you need to create [types](https://redux.js.org/basics/actions): 4 | 5 | ```javascript 6 | export const POST_TITLE = '@@post/TITLE'; 7 | export const POST_BODY = '@@post/BODY'; 8 | export const POST_SUBMIT = '@@post/SUBMIT'; 9 | ``` 10 | 11 | ## Redux Lazy 12 | 13 | With Redux Lazy you don't need to make it manually. 14 | Create **rl.js** file inside of your module and put code like this: 15 | 16 | ```javascript 17 | import RL from 'redux-lazy'; 18 | 19 | const rl = new RL('post'); 20 | 21 | rl.addAction('title', { title: '' }); 22 | rl.addAction('body', { body: '' }); 23 | rl.addAction('submit'); 24 | 25 | export default rl; 26 | ``` 27 | How to add actions see [docs](https://github.com/evheniy/redux-lazy/blob/master/docs/actions.md). 28 | 29 | Then import **rl** where you need it: 30 | 31 | ```javascript 32 | import rl from './rl'; 33 | 34 | const { types } = rl; 35 | ``` 36 | 37 | ## Testing 38 | 39 | ```javascript 40 | import { expect } from 'chai'; 41 | import rl from '../src/rl'; 42 | 43 | const { types } = rl.flush(); 44 | 45 | describe('Testing types', () => { 46 | it('should test POST_TITLE', () => { 47 | expect(types.POST_TITLE).to.be.equal('@@post/TITLE'); 48 | }); 49 | 50 | it('should test POST_BODY', () => { 51 | expect(types.POST_BODY).to.be.equal('@@post/BODY'); 52 | }); 53 | 54 | it('should test POST_SUBMIT', () => { 55 | expect(types.POST_SUBMIT).to.be.equal('@@post/SUBMIT'); 56 | }); 57 | }); 58 | 59 | ``` 60 | 61 | ## Documentation 62 | 63 | * [Install](https://github.com/evheniy/redux-lazy/blob/master/docs/install.md) 64 | * [How to use](https://github.com/evheniy/redux-lazy/blob/master/docs/use.md) 65 | * [Actions](https://github.com/evheniy/redux-lazy/blob/master/docs/actions.md) 66 | * [Reducer](https://github.com/evheniy/redux-lazy/blob/master/docs/reducer.md) 67 | * [Container](https://github.com/evheniy/redux-lazy/blob/master/docs/container.md) 68 | * [Options](https://github.com/evheniy/redux-lazy/blob/master/docs/options.md) 69 | 70 | 71 | [More examples](https://github.com/evheniy/redux-lazy/blob/master/tests/types.js) 72 | -------------------------------------------------------------------------------- /docs/install.md: -------------------------------------------------------------------------------- 1 | # Install 2 | 3 | ## How to install 4 | 5 | npm i -S redux-lazy 6 | 7 | Or 8 | 9 | yarn add redux-lazy 10 | 11 | ## How to use 12 | 13 | To work with Redux Lazy you need to make only 3 steps: 14 | 15 | ### 1. Import redux-lazy 16 | 17 | ```javascript 18 | import RL from 'redux-lazy'; 19 | ``` 20 | 21 | ### 2. Create a instance 22 | 23 | ```javascript 24 | const rl = new RL('post'); 25 | ``` 26 | 27 | ### 3. Add actions 28 | 29 | ```javascript 30 | rl.addAction('title', { title: '' }); 31 | rl.addAction('body', { body: '' }); 32 | ``` 33 | After that you can flush all code for working with redux: 34 | 35 | ```javascript 36 | const { 37 | nameSpace, 38 | types, 39 | actions, 40 | defaultState, 41 | reducer, 42 | mapStateToProps, 43 | mapDispatchToProps, 44 | Container, 45 | } = rl.flush(); 46 | ``` 47 | 48 | You can export each type and action: 49 | 50 | ```javascript 51 | const { 52 | nameSpace, 53 | types, 54 | actions, 55 | ... 56 | } = rl.flush(); 57 | 58 | const { titleAction, bodyAction } = actions; 59 | const { POST_TITLE, POST_BODY } = types; 60 | 61 | export default rl; 62 | 63 | export { 64 | nameSpace, 65 | titleAction, 66 | bodyAction, 67 | POST_TITLE, 68 | POST_BODY, 69 | }; 70 | ``` 71 | This will help you to avoid magic in code. 72 | 73 | ## Default state 74 | 75 | When you add action to Redux Lazy you can set payload. 76 | Payload from all actions creates default state. 77 | 78 | If you need to customize it you can set default state: 79 | 80 | ```javascript 81 | const defaultState = { title: 'title', body: 'body' }; 82 | const rl = new RL('post', defaultState); 83 | ``` 84 | 85 | ## Documentation 86 | 87 | * [How to use](https://github.com/evheniy/redux-lazy/blob/master/docs/use.md) 88 | * [Types](https://github.com/evheniy/redux-lazy/blob/master/docs/types.md) 89 | * [Actions](https://github.com/evheniy/redux-lazy/blob/master/docs/actions.md) 90 | * [Reducer](https://github.com/evheniy/redux-lazy/blob/master/docs/reducer.md) 91 | * [Container](https://github.com/evheniy/redux-lazy/blob/master/docs/container.md) 92 | * [Options](https://github.com/evheniy/redux-lazy/blob/master/docs/options.md) 93 | 94 | [More examples](https://github.com/evheniy/redux-lazy/blob/master/tests/) 95 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "redux-lazy", 3 | "version": "0.6.1", 4 | "description": "redux-lazy", 5 | "main": "lib/index.js", 6 | "files": [ 7 | "lib" 8 | ], 9 | "scripts": { 10 | "test": "npm-run-all test:*", 11 | "test:security": "nsp check", 12 | "test:lint": "eslint . --ext .js,.jsx", 13 | "test:coverage": "cross-env NODE_ENV=test nyc mocha tests/*.{js,jsx}", 14 | "report": "cat ./coverage/lcov.info | coveralls", 15 | "build": "cross-env NODE_ENV=production gulp", 16 | "precommit": "npm t", 17 | "prepush": "npm t", 18 | "release": "npm run build && npm publish" 19 | }, 20 | "repository": { 21 | "type": "git", 22 | "url": "git+https://github.com/evheniy/redux-lazy.git" 23 | }, 24 | "keywords": [ 25 | "redux", 26 | "react", 27 | "lazy", 28 | "action", 29 | "creator", 30 | "types", 31 | "reduce", 32 | "container" 33 | ], 34 | "author": "Evheniy Bystrov", 35 | "license": "MIT", 36 | "bugs": { 37 | "url": "https://github.com/evheniy/redux-lazy/issues" 38 | }, 39 | "homepage": "https://github.com/evheniy/redux-lazy#readme", 40 | "peerDependencies": { 41 | "react-redux": "^5.0.7", 42 | "redux": "^4.0.0" 43 | }, 44 | "devDependencies": { 45 | "@babel/core": "^7.1.2", 46 | "@babel/plugin-proposal-class-properties": "^7.1.0", 47 | "@babel/plugin-proposal-object-rest-spread": "^7.0.0", 48 | "@babel/plugin-syntax-dynamic-import": "^7.0.0", 49 | "@babel/preset-env": "^7.1.0", 50 | "@babel/preset-react": "^7.0.0", 51 | "@babel/register": "^7.0.0", 52 | "babel-eslint": "^10.0.1", 53 | "chai": "^4.2.0", 54 | "coveralls": "^3.0.2", 55 | "enzyme": "^3.7.0", 56 | "enzyme-adapter-react-16": "^1.6.0", 57 | "eslint": "^5.6.1", 58 | "eslint-config-airbnb": "^17.1.0", 59 | "eslint-plugin-import": "^2.14.0", 60 | "eslint-plugin-jsx-a11y": "^6.1.2", 61 | "eslint-plugin-react": "^7.11.1", 62 | "gulp": "^4.0.0", 63 | "gulp-babel": "^8.0.0", 64 | "gulp-clean-dest": "^0.2.0", 65 | "gulp-react": "^3.1.0", 66 | "husky": "^1.1.1", 67 | "jsdom": "^12.2.0", 68 | "mocha": "^5.2.0", 69 | "npm-run-all": "^4.1.3", 70 | "nsp": "^3.2.1", 71 | "nyc": "^13.1.0", 72 | "react": "^16.5.2", 73 | "react-dom": "^16.5.2", 74 | "react-redux": "^5.0.7", 75 | "redux": "^4.0.0", 76 | "redux-mock-store": "^1.5.3", 77 | "sinon": "^6.3.5" 78 | }, 79 | "dependencies": { 80 | "cross-env": "^5.2.0" 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /docs/container.md: -------------------------------------------------------------------------------- 1 | # Container 2 | 3 | To get state from store and put it to React components you should use **[react-redux](https://github.com/reduxjs/react-redux)** library. 4 | It has **[connect](https://github.com/reduxjs/react-redux/blob/master/docs/api.md#connectmapstatetoprops-mapdispatchtoprops-mergeprops-options)** - high order component (HOC) to get part of store and put it to component. 5 | 6 | ## Redux 7 | 8 | ```javascript 9 | import { connect } from 'react-redux'; 10 | import * as actions from './actions'; 11 | 12 | export const POST = '@@post/POST'; 13 | 14 | const mapStateToProps = state => state[POST]; 15 | const mapDispatchToProps = { ...actions }; 16 | 17 | export default connect(mapStateToProps, mapDispatchToProps); 18 | ``` 19 | 20 | Here we have 3 main points: 21 | 22 | * We need to create constant (POST) - it's useful for working with **[combineReducers](https://redux.js.org/basics/reducers#splitting-reducers)** and making selectors for **[mapStateToProps](https://github.com/reduxjs/react-redux/blob/master/docs/api.md#connectmapstatetoprops-mapdispatchtoprops-mergeprops-options)**. 23 | * We need to create selector and put it to **[mapStateToProps](https://github.com/reduxjs/react-redux/blob/master/docs/api.md#connectmapstatetoprops-mapdispatchtoprops-mergeprops-options)**. 24 | * We need to create **[mapDispatchToProps](https://github.com/reduxjs/react-redux/blob/master/docs/api.md#connectmapstatetoprops-mapdispatchtoprops-mergeprops-options)**. 25 | 26 | After that we can use **connect** to wrap React component and put part of state using **selectors**. 27 | 28 | ## Redux Lazy 29 | 30 | With Redux Lazy you don't need to make it manually. 31 | Create **rl.js** file inside of your module and put code like this: 32 | 33 | ```javascript 34 | import RL from 'redux-lazy'; 35 | 36 | const rl = new RL('post'); 37 | 38 | rl.addAction('title', { title: '' }); 39 | rl.addAction('body', { body: '' }); 40 | rl.addAction('submit'); 41 | 42 | export default rl; 43 | ``` 44 | 45 | Then import **rl** where you need it: 46 | 47 | ```javascript 48 | import rl from './rl'; 49 | 50 | const { 51 | nameSpace, 52 | Container, 53 | mapStateToProps, 54 | mapDispatchToProps, 55 | } = rl; 56 | ``` 57 | 58 | We can use **nameSpace** for selectors. 59 | 60 | 61 | ## Documentation 62 | 63 | * [Install](https://github.com/evheniy/redux-lazy/blob/master/docs/install.md) 64 | * [How to use](https://github.com/evheniy/redux-lazy/blob/master/docs/use.md) 65 | * [Types](https://github.com/evheniy/redux-lazy/blob/master/docs/types.md) 66 | * [Actions](https://github.com/evheniy/redux-lazy/blob/master/docs/actions.md) 67 | * [Reducer](https://github.com/evheniy/redux-lazy/blob/master/docs/reducer.md) 68 | * [Options](https://github.com/evheniy/redux-lazy/blob/master/docs/options.md) 69 | 70 | [More examples](https://github.com/evheniy/redux-lazy/blob/master/tests/container.jsx) 71 | -------------------------------------------------------------------------------- /docs/use.md: -------------------------------------------------------------------------------- 1 | # How to use 2 | 3 | ## Form 4 | 5 | If you need to submit form: 6 | 7 | ### Redux 8 | 9 | Action: 10 | ```javascript 11 | export const MODULE_SUBMIT = '@@module/SUBMIT'; 12 | 13 | export const submitAction = () => ({ 14 | type: MODULE_SUBMIT, 15 | }); 16 | ``` 17 | React: 18 | ```html 19 |
72 | ); 73 | 74 | export default Container(FormComponent); 75 | 76 | export { reducer }; 77 | ``` 78 | 79 | Just add reducer to redux and this example should work. 80 | 81 | ## Articles 82 | 83 | **React — redux for lazy developers:** 84 | * [Part 1](https://hackernoon.com/react-redux-for-lazy-developers-b551f16a456f) 85 | * [Part 2](https://hackernoon.com/react-redux-for-lazy-developers-part-2-d0c60123592f) 86 | * [Part 3](https://medium.com/@evheniybystrov/react-redux-for-lazy-developers-part-3-319b639a22c3) 87 | 88 | **[React/Redux development on steroids](https://medium.com/@evheniybystrov/react-redux-development-on-steroids-95dfed7e7a85)** 89 | 90 | ## Documentation 91 | 92 | * [Install](https://github.com/evheniy/redux-lazy/blob/master/docs/install.md) 93 | * [How to use](https://github.com/evheniy/redux-lazy/blob/master/docs/use.md) 94 | * [Types](https://github.com/evheniy/redux-lazy/blob/master/docs/types.md) 95 | * [Actions](https://github.com/evheniy/redux-lazy/blob/master/docs/actions.md) 96 | * [Reducer](https://github.com/evheniy/redux-lazy/blob/master/docs/reducer.md) 97 | * [Container](https://github.com/evheniy/redux-lazy/blob/master/docs/container.md) 98 | * [Options](https://github.com/evheniy/redux-lazy/blob/master/docs/options.md) 99 | -------------------------------------------------------------------------------- /tests/reducer.js: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai'; 2 | import RL from '../src'; 3 | import * as types from './fixtures/types'; 4 | import reducer from './fixtures/reducer'; 5 | 6 | const rl = new RL('post'); 7 | 8 | rl.addAction('title', { title: '' }); 9 | rl.addAction('body', { body: '' }); 10 | rl.addAction('submit'); 11 | 12 | const { 13 | types: rlTypes, 14 | reducer: rlReducer, 15 | defaultState: rlDefaultState, 16 | } = rl.flush(); 17 | 18 | describe('Testing reducer', () => { 19 | const defaultState = { 20 | title: '', 21 | body: '', 22 | }; 23 | 24 | describe('Redux', () => { 25 | it('should test default state', () => { 26 | const state = reducer(undefined, { type: 'test' }); 27 | 28 | expect(state).to.be.eql(defaultState); 29 | }); 30 | 31 | it('should test POST_TITLE action', () => { 32 | const title = 'title'; 33 | 34 | const state = reducer(undefined, { type: types.POST_TITLE, title }); 35 | 36 | expect(state.title).to.be.equal(title); 37 | }); 38 | 39 | it('should test POST_BODY action', () => { 40 | const body = 'body'; 41 | 42 | const state = reducer(undefined, { type: types.POST_BODY, body }); 43 | 44 | expect(state.body).to.be.equal(body); 45 | }); 46 | 47 | it('should test default state with value', () => { 48 | const state = reducer({ test: true }, { type: 'test' }); 49 | 50 | expect(state.test).to.be.equal(true); 51 | }); 52 | }); 53 | 54 | describe('Redux Lazy', () => { 55 | it('should test RL default state', () => { 56 | expect(rlDefaultState).to.be.eql(defaultState); 57 | }); 58 | 59 | it('should test RL new default state', () => { 60 | const newDefaultState = { title: 'title', body: 'body' }; 61 | const newRl = new RL('post', newDefaultState); 62 | const { defaultState: newRlDefaultState } = newRl.flush(); 63 | 64 | expect(newRlDefaultState).to.be.not.eql(defaultState); 65 | expect(newRlDefaultState).to.be.eql(newDefaultState); 66 | }); 67 | 68 | it('should test default state', () => { 69 | const state = rlReducer(undefined, { type: 'test' }); 70 | 71 | expect(state).to.be.eql(defaultState); 72 | }); 73 | 74 | it('should test POST_TITLE action', () => { 75 | const title = 'title'; 76 | 77 | const state = rlReducer(undefined, { type: rlTypes.POST_TITLE, payload: { title } }); 78 | 79 | expect(state.title).to.be.equal(title); 80 | }); 81 | 82 | it('should test POST_BODY action', () => { 83 | const body = 'body'; 84 | 85 | const state = rlReducer(undefined, { type: rlTypes.POST_BODY, payload: { body } }); 86 | 87 | expect(state.body).to.be.equal(body); 88 | }); 89 | 90 | it('should test default state with value', () => { 91 | const state = rlReducer({ test: true }, { type: 'test' }); 92 | 93 | expect(state.test).to.be.equal(true); 94 | }); 95 | 96 | it('should test POST_TITLE action with asParams option', () => { 97 | const title = 'title'; 98 | 99 | const newRl = new RL('post'); 100 | 101 | newRl.addAction('title', { title: '' }, { asParams: 'title' }); 102 | 103 | const { 104 | types: newRlTypes, 105 | reducer: newRlReducer, 106 | } = newRl.flush(); 107 | 108 | const state = newRlReducer(undefined, { type: newRlTypes.POST_TITLE, title }); 109 | 110 | expect(state.title).to.be.equal(title); 111 | }); 112 | 113 | it('should test defaultState', () => { 114 | const prevState = {}; 115 | 116 | const newRl = new RL('post', prevState); 117 | 118 | const { reducer: newRlReducer } = newRl.flush(); 119 | 120 | const state = newRlReducer(undefined, { type: 'test' }); 121 | 122 | expect(JSON.stringify(state)).to.be.equal(JSON.stringify(prevState)); 123 | }); 124 | 125 | it('should test defaultState', () => { 126 | const prevState = {}; 127 | 128 | const newRl = new RL('post', prevState); 129 | 130 | const { reducer: newRlReducer } = newRl.flush(); 131 | 132 | const state = newRlReducer(prevState, { type: 'test' }); 133 | 134 | expect(JSON.stringify(state)).to.be.equal(JSON.stringify(prevState)); 135 | }); 136 | 137 | it('should test wrong type', () => { 138 | const prevState = {}; 139 | 140 | const newRl = new RL('post'); 141 | 142 | const { reducer: newRlReducer } = newRl.flush(); 143 | 144 | const state = newRlReducer(prevState, { type: 'test' }); 145 | 146 | expect(state).to.be.equal(prevState); 147 | }); 148 | 149 | it('should test wrong type', () => { 150 | const prevState = {}; 151 | 152 | const newRl = new RL('post'); 153 | 154 | const { reducer: newRlReducer } = newRl.flush(); 155 | 156 | const state = newRlReducer(undefined, { type: 'test' }); 157 | 158 | expect(JSON.stringify(state)).to.be.equal(JSON.stringify(prevState)); 159 | }); 160 | 161 | it('should test empty type', () => { 162 | const newRl = new RL('post'); 163 | 164 | newRl.addParamAction('title'); 165 | newRl.addEventAction('event'); 166 | 167 | const { 168 | actions: { 169 | titleAction, 170 | eventAction, 171 | }, 172 | reducer: newRlReducer, 173 | } = newRl.flush(); 174 | 175 | const state = newRlReducer( 176 | newRlReducer(undefined, titleAction('test')), 177 | eventAction(), 178 | ); 179 | 180 | expect(state).to.be.eql({ title: 'test' }); 181 | }); 182 | }); 183 | }); 184 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | const { connect } = require('react-redux'); 2 | 3 | const toUnderscore = text => text.replace(/([A-Z])/g, $1 => `_${$1.toLowerCase()}`); 4 | 5 | const toConst = text => toUnderscore(text).toUpperCase(); 6 | 7 | class RL { 8 | constructor(ns, defaultState = null) { 9 | this.ns = ns.trim(); 10 | this.defaultState = defaultState; 11 | this.actions = []; 12 | } 13 | 14 | addAction(name, payload = {}, options = {}) { 15 | if (name === 'type') { 16 | throw new Error('Action name should not be "type"! This can create a conflict with redux action creators.'); 17 | } 18 | 19 | if (payload.type !== undefined) { 20 | throw new Error('You should not make "type" fields! This can create a conflict with redux action creators.'); 21 | } 22 | 23 | this.actions.push({ name, payload, options }); 24 | } 25 | 26 | addFormAction(name) { 27 | this.addAction(name, {}, { isForm: true }); 28 | } 29 | 30 | addFormElementAction(name, defaultValue = null) { 31 | this.addAction( 32 | name, 33 | { [name]: defaultValue }, 34 | { isFormElement: true, asParams: name }, 35 | ); 36 | } 37 | 38 | addEventAction(name) { 39 | this.addAction(name, {}, { isEvent: true }); 40 | } 41 | 42 | addParamAction(name, defaultValue = null) { 43 | this.addAction( 44 | name, 45 | { [name]: defaultValue }, 46 | { asParams: name }, 47 | ); 48 | } 49 | 50 | addParamsAction(name, payload = {}) { 51 | this.addAction( 52 | name, 53 | payload, 54 | { asParams: Object.keys(payload) }, 55 | ); 56 | } 57 | 58 | addResetAction(name = 'reset', exactly = false) { 59 | this.addAction(name, {}, { isReset: true, exactly }); 60 | } 61 | 62 | getNSKey() { 63 | return `@@${this.ns}`; 64 | } 65 | 66 | flush() { 67 | const nameSpace = `${this.getNSKey()}/${toConst(this.ns)}`; 68 | 69 | const types = {}; 70 | const actions = {}; 71 | const defaultState = {}; 72 | 73 | this.actions.forEach((action) => { 74 | // types 75 | const typeKey = `${toConst(this.ns)}_${toConst(action.name)}`; 76 | const type = `${this.getNSKey()}/${toConst(action.name)}`; 77 | types[typeKey] = type; 78 | 79 | // actions 80 | const actionKey = `${action.name}Action`; 81 | 82 | // reset 83 | if (action.options.isReset) { 84 | const returnState = action.options.exactly ? this.defaultState : defaultState; 85 | actions[actionKey] = () => ({ type, ...returnState }); 86 | } 87 | 88 | // event 89 | if (action.options.isEvent) { 90 | actions[actionKey] = () => ({ type }); 91 | } 92 | 93 | // submit form 94 | if (action.options.isForm) { 95 | actions[actionKey] = (event) => { 96 | if (event && event.preventDefault) { 97 | event.preventDefault(); 98 | } 99 | 100 | return { type }; 101 | }; 102 | } 103 | 104 | // set data from input event (event.target.value) 105 | if (action.options.isFormElement) { 106 | actions[actionKey] = (event) => { 107 | const payload = { ...action.payload }; 108 | if (event && event.target && event.target.value !== undefined) { 109 | const { target: { value } } = event; 110 | Object.keys(action.payload).forEach((key) => { 111 | payload[key] = value; 112 | }); 113 | } 114 | 115 | return { type, payload }; 116 | }; 117 | } 118 | 119 | // map action({ data }) to action(data) 120 | if (action.options.asParams) { 121 | let params = action.options.asParams; 122 | if (!Array.isArray(params)) { 123 | params = [params]; 124 | } 125 | 126 | actions[actionKey] = (...args) => { 127 | const payload = {}; 128 | params.forEach((param, number) => { 129 | const data = ( 130 | action.options.isFormElement 131 | && args[number] 132 | && args[number].target 133 | && args[number].target.value !== undefined 134 | ) 135 | ? args[number].target.value 136 | : args[number]; 137 | payload[param] = data !== undefined ? data : action.payload[param]; 138 | }); 139 | 140 | return { ...payload, type }; 141 | }; 142 | } 143 | 144 | if (!Object.keys(action.options).length) { 145 | actions[actionKey] = (payload = action.payload) => { 146 | const response = { type, payload }; 147 | 148 | if (!Object.keys(payload).length) { 149 | delete response.payload; 150 | } 151 | 152 | return response; 153 | }; 154 | } 155 | 156 | // default state 157 | Object.assign(defaultState, action.payload); 158 | }); 159 | 160 | // full default state 161 | Object.assign(defaultState, this.defaultState); 162 | 163 | const reducer = (state, action) => { 164 | const newState = state || defaultState; 165 | 166 | if (Object.values(types).includes(action.type)) { 167 | const returnState = { 168 | ...newState, 169 | ...action, 170 | ...action.payload, 171 | }; 172 | 173 | delete returnState.type; 174 | 175 | return returnState; 176 | } 177 | 178 | return newState; 179 | }; 180 | 181 | const mapStateToProps = state => state[nameSpace]; 182 | const mapDispatchToProps = { ...actions }; 183 | 184 | const Container = connect(mapStateToProps, mapDispatchToProps); 185 | 186 | return { 187 | nameSpace, 188 | types, 189 | actions, 190 | defaultState, 191 | reducer, 192 | mapStateToProps, 193 | mapDispatchToProps, 194 | Container, 195 | }; 196 | } 197 | } 198 | 199 | module.exports = RL; 200 | -------------------------------------------------------------------------------- /docs/options.md: -------------------------------------------------------------------------------- 1 | # Options 2 | 3 | When you add action you can set the 3rd parameter - options 4 | 5 | ```javascript 6 | rl.addAction('title', payload, options); 7 | ``` 8 | ## isEvent 9 | 10 | If you are using **[redux-observable](https://redux-observable.js.org/)** you need to send events to epics. 11 | Each event action should have only the type without any other fields. 12 | 13 | ```javascript 14 | rl.addAction('event', {}, { isEvent: true }); 15 | ``` 16 | The same result you can get using addEventAction: 17 | 18 | ```javascript 19 | rl.addEventAction('event'); 20 | ``` 21 | 22 | ## isForm 23 | 24 | When you need action to submit form you need to run event.preventDefault. 25 | To make this you can wrap function: 26 | 27 | ```javascript 28 | rl.addAction('submit'); 29 | ``` 30 | 31 | And put as props: 32 | 33 | ```html 34 |