diff --git a/.babelrc b/.babelrc new file mode 100644 index 0000000..d56c25d --- /dev/null +++ b/.babelrc @@ -0,0 +1,11 @@ +{ + "plugins": [ + "transform-es2015-modules-commonjs", + "transform-es2015-arrow-functions", + "transform-es2015-parameters", + "transform-es2015-block-scoping", + "transform-es2015-computed-properties", + "transform-object-rest-spread", + "transform-es2015-shorthand-properties" + ] +} diff --git a/.eslintrc b/.eslintrc new file mode 100644 index 0000000..949fb3c --- /dev/null +++ b/.eslintrc @@ -0,0 +1,4 @@ +{ + "extends": "airbnb", + "parser": "babel-eslint" +} diff --git a/.gitignore b/.gitignore index e920c16..656f5ad 100644 --- a/.gitignore +++ b/.gitignore @@ -31,3 +31,5 @@ node_modules # Optional REPL history .node_repl_history + +build diff --git a/.npmignore b/.npmignore new file mode 100644 index 0000000..afb55f7 --- /dev/null +++ b/.npmignore @@ -0,0 +1,7 @@ +node_modules +lib +test +logs +*.log +.babelrc +.eslintrc diff --git a/README.md b/README.md index 9552c71..89be827 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,56 @@ # redux-duck -Helper function to create Redux modules easy using `ducks-modular-redux` proposal +Helper function to create Redux modules using the `ducks-modular-redux` proposal. + +## Installation +```bash +npm i -S redux-duck +``` + +## API +### Create duck +```javascript +import { createDuck } from 'redux-duck'; + +const myDuck = createDuck('duck-name', 'application-name'); +``` +* `createDuck` receive 2 arguments, the second argument is optional. +* The first argument is the duck name. +* The second, and optional, argument is the application or module name. + +### Define action types +```javascript +const ACTION_TYPE = myDuck.defineType('ACTION_TYPE'); +``` +* `defineType` receive just one argument. +* The argument is the name of the action. +* The result should be an string like `application-name/duck-name/ACTION_TYPE` or `duck-name/ACTION_TYPE` if the application or module name was not defined. + +### Create action creators +```javascript +const actionType = myDuck.createAction(ACTION_TYPE); +``` +* `createAction` receive just one argument. +* This argument should be the defined action type string. +* It should return a function who will receive the action payload and return a valid (FSA compilant) action object. +* The action creator will receive an optional argument with the action payload. + +### Create reducer +```javascript +const initialState = { + list: Immutable.List(), + data: Immutable.Map(), +}; + +const reducer = myDuck.createReducer({ + [ACTION_TYPE]: (state, action) => ({ + ...state, + list: state.list.push(action.payload.id), + data: state.map.set(action.payload.id+'', action.payload), + }), +}, initialState); +``` +* `createReducer` receive two arguments, both required. +* The first argument is an object with the possible action cases. +* The second argument is the reducer initial state. +* The first argument should use the previously defined *action types* as keys. +* Each key in the first argument object should be a function who will receive the current state and the dispatched action as arguments and return the updated state. diff --git a/lib/index.js b/lib/index.js new file mode 100644 index 0000000..23fb706 --- /dev/null +++ b/lib/index.js @@ -0,0 +1,36 @@ +export function createDuck(name, app) { + function defineType(type) { + if (app) { + return `${app}/${name}/${type}`; + } + return `${name}/${type}`; + } + + function createReducer(cases, defaultState = {}) { + return function reducer(state = defaultState, action = {}) { + if (state === undefined) return defaultState; + for (const caseName in cases) { + if (action.type === caseName) return cases[caseName](state, action); + } + return state; + }; + } + + function createAction(type) { + return function actionCreator(payload) { + const action = { + type, + }; + + if (payload) action.payload = payload; + + return action; + }; + } + + return { + defineType, + createReducer, + createAction, + }; +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..b931619 --- /dev/null +++ b/package.json @@ -0,0 +1,52 @@ +{ + "name": "redux-duck", + "version": "1.0.0", + "description": "Helper function to create Redux modules using the ducks-modular-redux proposal.", + "main": "build/index.js", + "directories": { + "test": "test" + }, + "scripts": { + "lint": "eslint lib/index.js", + "prebuild": "npm run lint", + "build": "babel lib --out-dir build", + "pretest": "npm run build", + "test": "babel-node test/index.js | tap-spec", + "prepublish": "npm run test" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/sergiodxa/redux-duck.git" + }, + "keywords": [ + "redux", + "duck", + "module", + "helper" + ], + "author": "Sergio Daniel Xalambrí (http://sergio.xalambri.com.ar/)", + "license": "MIT", + "bugs": { + "url": "https://github.com/sergiodxa/redux-duck/issues" + }, + "homepage": "http://sergio.xalambri.com.ar/redux-duck/", + "devDependencies": { + "babel": "6.5.2", + "babel-cli": "6.7.5", + "babel-core": "6.7.6", + "babel-eslint": "6.0.3", + "babel-plugin-transform-es2015-arrow-functions": "6.5.2", + "babel-plugin-transform-es2015-block-scoping": "6.7.1", + "babel-plugin-transform-es2015-computed-properties": "6.6.5", + "babel-plugin-transform-es2015-modules-commonjs": "6.7.4", + "babel-plugin-transform-es2015-parameters": "6.7.0", + "babel-plugin-transform-es2015-shorthand-properties": "6.5.0", + "babel-plugin-transform-object-rest-spread": "6.6.5", + "eslint": "2.8.0", + "eslint-config-airbnb": "7.0.0", + "eslint-plugin-jsx-a11y": "0.6.2", + "eslint-plugin-react": "4.3.0", + "tap-spec": "4.1.1", + "tape": "4.5.1" + } +} diff --git a/test/index.js b/test/index.js new file mode 100644 index 0000000..ef09d7e --- /dev/null +++ b/test/index.js @@ -0,0 +1,140 @@ +import test from 'tape'; +import { createDuck } from '../build/index.js'; + +test('create duck', t => { + t.plan(6); + + const duck = createDuck('test', 'redux-duck'); + + t.ok(duck.defineType, 'it should had a key `defineType`'); + t.equals( + typeof duck.defineType, + 'function', + 'it should be a function' + ); + + t.ok(duck.createReducer, 'it should had a key `createReducer`'); + t.equals( + typeof duck.createReducer, + 'function', + 'it should be a function' + ); + + t.ok(duck.createAction, 'it should had a key `createAction`'); + t.equals( + typeof duck.createAction, + 'function', + 'it should be a function' + ); +}); + +test('define type', t => { + t.plan(2); + + const duck1 = createDuck('test1', 'redux-duck'); + const duck2 = createDuck('test2', 'redux-duck'); + + const type1 = duck1.defineType('TYPE'); + const type2 = duck2.defineType('TYPE'); + + t.equals( + type1, + 'redux-duck/test1/TYPE', + 'it should be the expected action type string' + ); + + t.equals( + type2, + 'redux-duck/test2/TYPE', + 'it should be the expected action type string' + ); +}); + +test('create action creator', t => { + t.plan(3); + + const duck = createDuck('test', 'redux-duck'); + + const type = duck.defineType('TYPE'); + + const createType = duck.createAction(type); + + const testData = { + id: 123, + message: 'hello world', + }; + + const action = createType(testData); + + const emptyActon = createType(); + + t.equals( + typeof createType, + 'function', + 'it should create a valid function' + ); + + t.deepEquals( + action, + { + type, + payload: testData, + }, + 'it should create a valid action object' + ); + + t.deepEquals( + emptyActon, + { + type, + }, + 'it should be able to create an action without payload' + ); +}); + +test('create reducer', t => { + t.plan(3); + + const duck = createDuck('test', 'redux-duck'); + + const type = duck.defineType('TYPE'); + + const reducer = duck.createReducer({ + [type]: (state, action) => ({ + ...state, + [action.payload.id]: action.payload, + }), + }, {}); + + const testData = { + id: 123, + message: 'hello world', + }; + + const testAction = { + type, + payload: testData, + }; + + const state = reducer({}, testAction); + + t.equals( + typeof reducer, + 'function', + 'the reducer should be a function' + ); + + t.deepEquals( + reducer(), + {}, + 'the reducer should be able to return the default state' + ); + + t.deepEquals( + state, + { + [testData.id]: testData, + }, + 'the reducer should work with the defined cases' + ); +});