├── .eslintrc ├── .gitignore ├── .npmignore ├── LICENSE.txt ├── README.md ├── examples └── counter │ ├── .babelrc │ ├── actions │ ├── counter.js │ ├── notification.js │ └── snackbar.js │ ├── components │ └── Counter.js │ ├── containers │ ├── App.js │ ├── Notifications.js │ ├── Root.js │ └── Snackbar.js │ ├── demo.gif │ ├── dist │ └── bundle.js │ ├── events │ └── notifyEvents.js │ ├── index.html │ ├── index.js │ ├── package.json │ ├── reducers │ ├── counter.js │ ├── index.js │ ├── notification.js │ └── snackbar.js │ ├── server.js │ ├── store │ └── configureStore.js │ ├── test │ ├── actions │ │ └── counter.spec.js │ ├── components │ │ └── Counter.spec.js │ ├── containers │ │ └── App.spec.js │ ├── reducers │ │ └── counter.spec.js │ └── setup.js │ ├── theme.scss │ └── webpack.config.js ├── package.json └── src └── index.js /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "ecmaFeatures": { 3 | "jsx": true, 4 | "modules": true 5 | }, 6 | "env": { 7 | "browser": true, 8 | "node": true 9 | }, 10 | "parser": "babel-eslint", 11 | "rules": { 12 | "quotes": [2, "single"], 13 | "strict": [2, "never"], 14 | "react/jsx-uses-react": 2, 15 | "react/jsx-uses-vars": 2, 16 | "react/react-in-jsx-scope": 2 17 | }, 18 | "plugins": [ 19 | "react" 20 | ] 21 | } 22 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | .idea/ 3 | node_modules 4 | npm-debug.log 5 | lib 6 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | /.eslint* 2 | /.gitignore 3 | /.npmignore 4 | /.travis.yml 5 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Mihail Diordiev 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, 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, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Redux Notify 2 | Redux middleware to notify when an action from the list is dispatched. 3 | 4 | ![Demo](examples/counter/demo.gif) 5 | 6 | ## Usage 7 | Specify actions to catch, and which actions to trigger: 8 | ```js 9 | const notifyEvents = [ 10 | { 11 | catch: [INCREMENT_COUNTER, DECREMENT_COUNTER], 12 | dispatch: sendNotification 13 | } 14 | ]; 15 | ``` 16 | Then just include it as a redux middleware: 17 | ```js 18 | import notify from 'redux-notify'; 19 | const store = applyMiddleware(notify(notifyEvents))(createStore)(reducer); 20 | ``` 21 | 22 | ## API 23 | #### `notify(notifyEvents, [config])` 24 | - arguments 25 | - **notifyEvents** an *array* of *objects*: 26 | - **catch** - an *array* of action *objects*, which trigger dispatching of actions specified in the *dispatch* parameter. 27 | - **dispatch** - an *array* of action creators (*functions*) or just an action creator (*function*) to be dispatched after the actions in the *catch* parameter. 28 | - **config** *object* (optional) 29 | - **noReverse** *boolean* - prevents triggering the action back, when having cyclic dispatching. 30 | 31 | ## Examples 32 | - [counter example](https://github.com/zalmoxisus/redux-notify/tree/master/examples/counter) - notifications examples with [react-notification-system](https://github.com/igorprado/react-notification-system) and [react-toolbox](https://github.com/react-toolbox/react-toolbox) snackbar. 33 | - [browser-redux](https://github.com/zalmoxisus/browser-redux) - a chrome extension example, which uses `redux-notify` for messaging between content scripts and background page. 34 | -------------------------------------------------------------------------------- /examples/counter/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "stage": 0, 3 | "env": { 4 | "development": { 5 | "plugins": [ 6 | "react-transform" 7 | ], 8 | "extra": { 9 | "react-transform": { 10 | "transforms": [{ 11 | "transform": "react-transform-hmr", 12 | "imports": ["react"], 13 | "locals": ["module"] 14 | }] 15 | } 16 | } 17 | } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /examples/counter/actions/counter.js: -------------------------------------------------------------------------------- 1 | export const INCREMENT_COUNTER = 'INCREMENT_COUNTER'; 2 | export const DECREMENT_COUNTER = 'DECREMENT_COUNTER'; 3 | 4 | export function increment() { 5 | return { 6 | type: INCREMENT_COUNTER 7 | }; 8 | } 9 | 10 | export function decrement() { 11 | return { 12 | type: DECREMENT_COUNTER 13 | }; 14 | } 15 | 16 | export function incrementIfOdd() { 17 | return (dispatch, getState) => { 18 | const { counter } = getState(); 19 | 20 | if (counter % 2 === 0) { 21 | return; 22 | } 23 | 24 | dispatch(increment()); 25 | }; 26 | } 27 | 28 | export function incrementAsync(delay = 1000) { 29 | return dispatch => { 30 | setTimeout(() => { 31 | dispatch(increment()); 32 | }, delay); 33 | }; 34 | } 35 | -------------------------------------------------------------------------------- /examples/counter/actions/notification.js: -------------------------------------------------------------------------------- 1 | export const NOTIFY_TOP = 'NOTIFY_TOP'; 2 | 3 | export function notifyTop(caller) { 4 | // return { type: NOTIFY_TOP }; 5 | return (dispatch, getState) => { 6 | const { counter } = getState(); 7 | dispatch({ type: NOTIFY_TOP, caller: caller }); 8 | }; 9 | } 10 | 11 | export function delayedNotifyTop(caller, delay = 1000) { 12 | return (dispatch, getState) => { 13 | return new Promise((resolve, reject) => { 14 | setTimeout(() => { 15 | dispatch({ type: NOTIFY_TOP, caller: caller }); 16 | resolve(); 17 | }, delay) 18 | }); 19 | }; 20 | } 21 | -------------------------------------------------------------------------------- /examples/counter/actions/snackbar.js: -------------------------------------------------------------------------------- 1 | export const NOTIFY_SNACKBAR = 'NOTIFY_SNACKBAR'; 2 | 3 | export function notifySnackbar() { 4 | return (dispatch, getState) => { 5 | const { counter } = getState(); 6 | dispatch({ type: NOTIFY_SNACKBAR, counter }); 7 | }; 8 | } 9 | -------------------------------------------------------------------------------- /examples/counter/components/Counter.js: -------------------------------------------------------------------------------- 1 | import React, { Component, PropTypes } from 'react'; 2 | 3 | class Counter extends Component { 4 | render() { 5 | const { increment, incrementIfOdd, incrementAsync, decrement, counter } = this.props; 6 | return ( 7 |

8 | Clicked: {counter} times 9 | {' '} 10 | 11 | {' '} 12 | 13 | {' '} 14 | 15 | {' '} 16 | 17 |

18 | ); 19 | } 20 | } 21 | 22 | Counter.propTypes = { 23 | increment: PropTypes.func.isRequired, 24 | incrementIfOdd: PropTypes.func.isRequired, 25 | incrementAsync: PropTypes.func.isRequired, 26 | decrement: PropTypes.func.isRequired, 27 | counter: PropTypes.number.isRequired 28 | }; 29 | 30 | export default Counter; 31 | -------------------------------------------------------------------------------- /examples/counter/containers/App.js: -------------------------------------------------------------------------------- 1 | import { bindActionCreators } from 'redux'; 2 | import { connect } from 'react-redux'; 3 | import Counter from '../components/Counter'; 4 | import * as CounterActions from '../actions/counter'; 5 | 6 | function mapStateToProps(state) { 7 | return { 8 | counter: state.counter 9 | }; 10 | } 11 | 12 | function mapDispatchToProps(dispatch) { 13 | return bindActionCreators(CounterActions, dispatch); 14 | } 15 | 16 | export default connect(mapStateToProps, mapDispatchToProps)(Counter); 17 | -------------------------------------------------------------------------------- /examples/counter/containers/Notifications.js: -------------------------------------------------------------------------------- 1 | import React, { Component, PropTypes } from 'react'; 2 | import ReactNotificationSystem from 'react-notification-system'; 3 | import { bindActionCreators } from 'redux'; 4 | import { connect } from 'react-redux'; 5 | 6 | class Notifications extends Component { 7 | static propTypes = { 8 | notification: PropTypes.object.isRequired 9 | }; 10 | 11 | componentWillUpdate(nextProps) { 12 | this.refs.notifications.addNotification(nextProps.notification); 13 | } 14 | 15 | render() { 16 | return ; 17 | } 18 | } 19 | 20 | function mapStateToProps(state) { 21 | return { 22 | notification: state.notification 23 | }; 24 | } 25 | 26 | export default connect(mapStateToProps)(Notifications); 27 | -------------------------------------------------------------------------------- /examples/counter/containers/Root.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { bindActionCreators } from 'redux'; 3 | import { connect } from 'react-redux'; 4 | import App from './App'; 5 | import Notifications from './Notifications'; 6 | import Snackbar from './Snackbar'; 7 | 8 | export default class Root extends Component { 9 | render() { 10 | return
11 | 12 | 13 | 14 |
; 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /examples/counter/containers/Snackbar.js: -------------------------------------------------------------------------------- 1 | import React, { Component, PropTypes } from 'react'; 2 | import { connect } from 'react-redux'; 3 | import Snackbar from 'react-toolbox/lib/snackbar'; 4 | 5 | class Notification extends React.Component { 6 | static propTypes = { 7 | label: PropTypes.string.isRequired 8 | }; 9 | 10 | componentWillUpdate() { 11 | this.refs.snackbar.show(); 12 | } 13 | 14 | render () { 15 | return ( 16 | 23 | ); 24 | } 25 | } 26 | 27 | function mapStateToProps(state) { 28 | return { 29 | label: state.snackbar.label 30 | }; 31 | } 32 | 33 | export default connect(mapStateToProps)(Notification); 34 | -------------------------------------------------------------------------------- /examples/counter/demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zalmoxisus/redux-notify/012b23a07e05bc9b7e1f3c671079adbf424e5380/examples/counter/demo.gif -------------------------------------------------------------------------------- /examples/counter/events/notifyEvents.js: -------------------------------------------------------------------------------- 1 | import { INCREMENT_COUNTER, DECREMENT_COUNTER } from '../actions/counter'; 2 | import { notifyTop, delayedNotifyTop } from '../actions/notification'; 3 | import { notifySnackbar } from '../actions/snackbar'; 4 | 5 | const events = [ 6 | { 7 | catch: [INCREMENT_COUNTER, DECREMENT_COUNTER], 8 | dispatch: [delayedNotifyTop, notifySnackbar] 9 | } 10 | ]; 11 | 12 | export default events; 13 | -------------------------------------------------------------------------------- /examples/counter/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Redux counter example 5 | 6 | 7 | 8 |
9 |
10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /examples/counter/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { render } from 'react-dom'; 3 | import { Provider } from 'react-redux'; 4 | import Root from './containers/Root'; 5 | import configureStore from './store/configureStore'; 6 | 7 | const store = configureStore(); 8 | 9 | render( 10 | 11 | 12 | , 13 | document.getElementById('root') 14 | ); 15 | -------------------------------------------------------------------------------- /examples/counter/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "redux-counter-example", 3 | "version": "0.0.0", 4 | "description": "Redux counter example", 5 | "scripts": { 6 | "start": "node server.js", 7 | "test": "NODE_ENV=test mocha --recursive --compilers js:babel-core/register --require ./test/setup.js", 8 | "test:watch": "npm test -- --watch" 9 | }, 10 | "repository": { 11 | "type": "git", 12 | "url": "https://github.com/rackt/redux.git" 13 | }, 14 | "license": "MIT", 15 | "bugs": { 16 | "url": "https://github.com/rackt/redux/issues" 17 | }, 18 | "homepage": "http://rackt.github.io/redux", 19 | "dependencies": { 20 | "react": "^0.14.0", 21 | "react-dom": "^0.14.0", 22 | "react-notification-system": "^0.2.5", 23 | "react-redux": "^4.0.0", 24 | "react-toolbox": "^0.11.5", 25 | "redux": "^3.0.0", 26 | "redux-notify": "file:../../", 27 | "redux-thunk": "^0.1.0" 28 | }, 29 | "devDependencies": { 30 | "autoprefixer": "^6.1.0", 31 | "babel-core": "^5.6.18", 32 | "babel-loader": "^5.1.4", 33 | "babel-plugin-react-transform": "^1.1.0", 34 | "css-loader": "^0.23.0", 35 | "expect": "^1.6.0", 36 | "express": "^4.13.3", 37 | "extract-text-webpack-plugin": "^0.9.1", 38 | "jsdom": "^5.6.1", 39 | "mocha": "^2.2.5", 40 | "node-libs-browser": "^0.5.2", 41 | "node-sass": "~3.4.2", 42 | "normalize.css": "^3.0.3", 43 | "postcss": "^5.0.12", 44 | "postcss-loader": "^0.8.0", 45 | "react-addons-test-utils": "^0.14.0", 46 | "react-transform-hmr": "^1.0.0", 47 | "redux-immutable-state-invariant": "^1.1.1", 48 | "sass-loader": "^3.1.1", 49 | "style-loader": "^0.13.0", 50 | "toolbox-loader": "0.0.3", 51 | "webpack": "^1.9.11", 52 | "webpack-dev-middleware": "^1.2.0", 53 | "webpack-hot-middleware": "^2.2.0" 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /examples/counter/reducers/counter.js: -------------------------------------------------------------------------------- 1 | import { INCREMENT_COUNTER, DECREMENT_COUNTER } from '../actions/counter'; 2 | 3 | export default function counter(state = 0, action) { 4 | switch (action.type) { 5 | case INCREMENT_COUNTER: 6 | return state + 1; 7 | case DECREMENT_COUNTER: 8 | return state - 1; 9 | default: 10 | return state; 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /examples/counter/reducers/index.js: -------------------------------------------------------------------------------- 1 | import { combineReducers } from 'redux'; 2 | import counter from './counter'; 3 | import notification from './notification'; 4 | import snackbar from './snackbar'; 5 | 6 | const rootReducer = combineReducers({ 7 | counter, 8 | notification, 9 | snackbar 10 | }); 11 | 12 | export default rootReducer; 13 | -------------------------------------------------------------------------------- /examples/counter/reducers/notification.js: -------------------------------------------------------------------------------- 1 | import { INCREMENT_COUNTER } from '../actions/counter'; 2 | import { NOTIFY_TOP } from '../actions/notification'; 3 | 4 | export default function notification(state = { message: '' }, action) { 5 | switch (action.type) { 6 | case NOTIFY_TOP: 7 | return { 8 | ...state, 9 | message: 'Dispatched ' + action.caller.type, 10 | level: (action.caller.type === INCREMENT_COUNTER ? 'success' : 'info') 11 | }; 12 | default: 13 | return state; 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /examples/counter/reducers/snackbar.js: -------------------------------------------------------------------------------- 1 | import { NOTIFY_SNACKBAR } from '../actions/snackbar'; 2 | 3 | export default function notification(state = { label: '' }, action) { 4 | switch (action.type) { 5 | case NOTIFY_SNACKBAR: 6 | return { 7 | ...state, 8 | label: 'Changed counter to ' + action.counter 9 | }; 10 | default: 11 | return state; 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /examples/counter/server.js: -------------------------------------------------------------------------------- 1 | var webpack = require('webpack'); 2 | var webpackDevMiddleware = require('webpack-dev-middleware'); 3 | var webpackHotMiddleware = require('webpack-hot-middleware'); 4 | var config = require('./webpack.config'); 5 | 6 | var app = new require('express')(); 7 | var port = 4001; 8 | 9 | var compiler = webpack(config); 10 | app.use(webpackDevMiddleware(compiler, { noInfo: true, publicPath: config.output.publicPath })); 11 | app.use(webpackHotMiddleware(compiler)); 12 | 13 | app.get("/", function(req, res) { 14 | res.sendFile(__dirname + '/index.html'); 15 | }); 16 | 17 | app.listen(port, function(error) { 18 | if (error) { 19 | console.error(error); 20 | } else { 21 | console.info("==> 🌎 Listening on port %s. Open up http://localhost:%s/ in your browser.", port, port); 22 | } 23 | }); 24 | -------------------------------------------------------------------------------- /examples/counter/store/configureStore.js: -------------------------------------------------------------------------------- 1 | import { createStore, applyMiddleware, compose } from 'redux'; 2 | import thunk from 'redux-thunk'; 3 | import notify from 'redux-notify'; 4 | import invariant from 'redux-immutable-state-invariant'; 5 | import reducer from '../reducers'; 6 | import notifyEvents from '../events/notifyEvents'; 7 | 8 | const middleware = [ 9 | notify(notifyEvents), 10 | thunk, 11 | invariant() 12 | ]; 13 | 14 | export default function configureStore(initialState) { 15 | const finalCreateStore = compose( 16 | applyMiddleware(...middleware), 17 | window.devToolsExtension ? window.devToolsExtension() : f => f 18 | )(createStore); 19 | 20 | const store = finalCreateStore(reducer, initialState); 21 | 22 | if (module.hot) { 23 | // Enable Webpack hot module replacement for reducers 24 | module.hot.accept('../reducers', () => { 25 | const nextReducer = require('../reducers'); 26 | store.replaceReducer(nextReducer); 27 | }); 28 | } 29 | 30 | return store; 31 | } 32 | -------------------------------------------------------------------------------- /examples/counter/test/actions/counter.spec.js: -------------------------------------------------------------------------------- 1 | import expect from 'expect'; 2 | import { applyMiddleware } from 'redux'; 3 | import thunk from 'redux-thunk'; 4 | import * as actions from '../../actions/counter'; 5 | 6 | const middlewares = [thunk]; 7 | 8 | /* 9 | * Creates a mock of Redux store with middleware. 10 | */ 11 | function mockStore(getState, expectedActions, onLastAction) { 12 | if (!Array.isArray(expectedActions)) { 13 | throw new Error('expectedActions should be an array of expected actions.'); 14 | } 15 | if (typeof onLastAction !== 'undefined' && typeof onLastAction !== 'function') { 16 | throw new Error('onLastAction should either be undefined or function.'); 17 | } 18 | 19 | function mockStoreWithoutMiddleware() { 20 | return { 21 | getState() { 22 | return typeof getState === 'function' ? 23 | getState() : 24 | getState; 25 | }, 26 | 27 | dispatch(action) { 28 | const expectedAction = expectedActions.shift(); 29 | expect(action).toEqual(expectedAction); 30 | if (onLastAction && !expectedActions.length) { 31 | onLastAction(); 32 | } 33 | return action; 34 | } 35 | }; 36 | } 37 | 38 | const mockStoreWithMiddleware = applyMiddleware( 39 | ...middlewares 40 | )(mockStoreWithoutMiddleware); 41 | 42 | return mockStoreWithMiddleware(); 43 | } 44 | 45 | describe('actions', () => { 46 | it('increment should create increment action', () => { 47 | expect(actions.increment()).toEqual({ type: actions.INCREMENT_COUNTER }); 48 | }); 49 | 50 | it('decrement should create decrement action', () => { 51 | expect(actions.decrement()).toEqual({ type: actions.DECREMENT_COUNTER }); 52 | }); 53 | 54 | it('incrementIfOdd should create increment action', (done) => { 55 | const expectedActions = [ 56 | { type: actions.INCREMENT_COUNTER } 57 | ]; 58 | const store = mockStore({ counter: 1 }, expectedActions, done); 59 | store.dispatch(actions.incrementIfOdd()); 60 | }); 61 | 62 | it('incrementIfOdd shouldnt create increment action if counter is even', (done) => { 63 | const expectedActions = []; 64 | const store = mockStore({ counter: 2 }, expectedActions); 65 | store.dispatch(actions.incrementIfOdd()); 66 | done(); 67 | }); 68 | 69 | it('incrementAsync should create increment action', (done) => { 70 | const expectedActions = [ 71 | { type: actions.INCREMENT_COUNTER } 72 | ]; 73 | const store = mockStore({ counter: 0 }, expectedActions, done); 74 | store.dispatch(actions.incrementAsync(100)); 75 | }); 76 | }); 77 | -------------------------------------------------------------------------------- /examples/counter/test/components/Counter.spec.js: -------------------------------------------------------------------------------- 1 | import expect from 'expect'; 2 | import React from 'react'; 3 | import TestUtils from 'react-addons-test-utils'; 4 | import Counter from '../../components/Counter'; 5 | 6 | function setup() { 7 | const actions = { 8 | increment: expect.createSpy(), 9 | incrementIfOdd: expect.createSpy(), 10 | incrementAsync: expect.createSpy(), 11 | decrement: expect.createSpy() 12 | }; 13 | const component = TestUtils.renderIntoDocument(); 14 | return { 15 | component: component, 16 | actions: actions, 17 | buttons: TestUtils.scryRenderedDOMComponentsWithTag(component, 'button'), 18 | p: TestUtils.findRenderedDOMComponentWithTag(component, 'p') 19 | }; 20 | } 21 | 22 | describe('Counter component', () => { 23 | it('should display count', () => { 24 | const { p } = setup(); 25 | expect(p.textContent).toMatch(/^Clicked: 1 times/); 26 | }); 27 | 28 | it('first button should call increment', () => { 29 | const { buttons, actions } = setup(); 30 | TestUtils.Simulate.click(buttons[0]); 31 | expect(actions.increment).toHaveBeenCalled(); 32 | }); 33 | 34 | it('second button should call decrement', () => { 35 | const { buttons, actions } = setup(); 36 | TestUtils.Simulate.click(buttons[1]); 37 | expect(actions.decrement).toHaveBeenCalled(); 38 | }); 39 | 40 | it('third button should call incrementIfOdd', () => { 41 | const { buttons, actions } = setup(); 42 | TestUtils.Simulate.click(buttons[2]); 43 | expect(actions.incrementIfOdd).toHaveBeenCalled(); 44 | }); 45 | 46 | it('fourth button should call incrementAsync', () => { 47 | const { buttons, actions } = setup(); 48 | TestUtils.Simulate.click(buttons[3]); 49 | expect(actions.incrementAsync).toHaveBeenCalled(); 50 | }); 51 | }); 52 | -------------------------------------------------------------------------------- /examples/counter/test/containers/App.spec.js: -------------------------------------------------------------------------------- 1 | import expect from 'expect'; 2 | import React from 'react'; 3 | import TestUtils from 'react-addons-test-utils'; 4 | import { Provider } from 'react-redux'; 5 | import App from '../../containers/App'; 6 | import configureStore from '../../store/configureStore'; 7 | 8 | function setup(initialState) { 9 | const store = configureStore(initialState); 10 | const app = TestUtils.renderIntoDocument( 11 | 12 | 13 | 14 | ); 15 | return { 16 | app: app, 17 | buttons: TestUtils.scryRenderedDOMComponentsWithTag(app, 'button'), 18 | p: TestUtils.findRenderedDOMComponentWithTag(app, 'p') 19 | }; 20 | } 21 | 22 | describe('containers', () => { 23 | describe('App', () => { 24 | it('should display initial count', () => { 25 | const { p } = setup(); 26 | expect(p.textContent).toMatch(/^Clicked: 0 times/); 27 | }); 28 | 29 | it('should display updated count after increment button click', () => { 30 | const { buttons, p } = setup(); 31 | TestUtils.Simulate.click(buttons[0]); 32 | expect(p.textContent).toMatch(/^Clicked: 1 times/); 33 | }); 34 | 35 | it('should display updated count after decrement button click', () => { 36 | const { buttons, p } = setup(); 37 | TestUtils.Simulate.click(buttons[1]); 38 | expect(p.textContent).toMatch(/^Clicked: -1 times/); 39 | }); 40 | 41 | it('shouldnt change if even and if odd button clicked', () => { 42 | const { buttons, p } = setup(); 43 | TestUtils.Simulate.click(buttons[2]); 44 | expect(p.textContent).toMatch(/^Clicked: 0 times/); 45 | }); 46 | 47 | it('should change if odd and if odd button clicked', () => { 48 | const { buttons, p } = setup({ counter: 1 }); 49 | TestUtils.Simulate.click(buttons[2]); 50 | expect(p.textContent).toMatch(/^Clicked: 2 times/); 51 | }); 52 | }); 53 | }); 54 | -------------------------------------------------------------------------------- /examples/counter/test/reducers/counter.spec.js: -------------------------------------------------------------------------------- 1 | import expect from 'expect'; 2 | import counter from '../../reducers/counter'; 3 | import { INCREMENT_COUNTER, DECREMENT_COUNTER } from '../../actions/counter'; 4 | 5 | describe('reducers', () => { 6 | describe('counter', () => { 7 | it('should handle initial state', () => { 8 | expect(counter(undefined, {})).toBe(0); 9 | }); 10 | 11 | it('should handle INCREMENT_COUNTER', () => { 12 | expect(counter(1, { type: INCREMENT_COUNTER })).toBe(2); 13 | }); 14 | 15 | it('should handle DECREMENT_COUNTER', () => { 16 | expect(counter(1, { type: DECREMENT_COUNTER })).toBe(0); 17 | }); 18 | 19 | it('should handle unknown action type', () => { 20 | expect(counter(1, { type: 'unknown' })).toBe(1); 21 | }); 22 | }); 23 | }); 24 | -------------------------------------------------------------------------------- /examples/counter/test/setup.js: -------------------------------------------------------------------------------- 1 | import { jsdom } from 'jsdom'; 2 | 3 | global.document = jsdom(''); 4 | global.window = document.defaultView; 5 | global.navigator = global.window.navigator; 6 | -------------------------------------------------------------------------------- /examples/counter/theme.scss: -------------------------------------------------------------------------------- 1 | $color-primary: $palette-blue-500 !default; 2 | $color-primary-dark: $palette-blue-700 !default; -------------------------------------------------------------------------------- /examples/counter/webpack.config.js: -------------------------------------------------------------------------------- 1 | var path = require('path'); 2 | var webpack = require('webpack'); 3 | const autoprefixer = require('autoprefixer'); 4 | var ExtractTextPlugin = require('extract-text-webpack-plugin'); 5 | 6 | module.exports = { 7 | context: __dirname, 8 | devtool: 'cheap-module-eval-source-map', 9 | entry: [ 10 | 'webpack-hot-middleware/client', 11 | './index' 12 | ], 13 | output: { 14 | path: path.join(__dirname, 'dist'), 15 | filename: 'bundle.js', 16 | publicPath: '/static/' 17 | }, 18 | plugins: [ 19 | new ExtractTextPlugin('react-toolbox.css', { allChunks: true }), 20 | new webpack.optimize.OccurenceOrderPlugin(), 21 | new webpack.HotModuleReplacementPlugin(), 22 | new webpack.NoErrorsPlugin() 23 | ], 24 | resolve: { 25 | extensions: ['', '.jsx', '.scss', '.js', '.json'], 26 | modulesDirectories: [ 27 | 'node_modules', 28 | path.resolve(__dirname, './node_modules') 29 | ] 30 | }, 31 | module: { 32 | loaders: [{ 33 | test: /(\.js|\.jsx)$/, 34 | loaders: ['babel'], 35 | exclude: /node_modules/, 36 | include: __dirname 37 | }, { 38 | test: /(\.scss|\.css)$/, 39 | loader: ExtractTextPlugin.extract('style', 'css?sourceMap&modules&importLoaders=1&localIdentName=[name]__[local]___[hash:base64:5]!postcss!sass?sourceMap!toolbox') 40 | }] 41 | }, 42 | postcss: [autoprefixer] 43 | }; 44 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "redux-notify", 3 | "version": "0.2.0", 4 | "description": "Notify when specified redux actions are dispatched.", 5 | "main": "lib/index.js", 6 | "scripts": { 7 | "clean": "rimraf lib dist coverage", 8 | "lint": "eslint src test examples", 9 | "build": "babel src --out-dir lib", 10 | "preversion": "npm run clean && npm run check", 11 | "version": "npm run build", 12 | "postversion": "git push && git push --tags && npm run clean", 13 | "prepublish": "npm run clean && npm run build", 14 | "test": "eslint ." 15 | }, 16 | "repository": { 17 | "type": "git", 18 | "url": "git+https://github.com/zalmoxisus/redux-notify.git" 19 | }, 20 | "homepage": "https://github.com/zalmoxisus/redux-notify", 21 | "keywords": [ 22 | "redux", 23 | "notify", 24 | "messaging", 25 | "postMessage", 26 | "events" 27 | ], 28 | "devDependencies": { 29 | "babel": "^5.8.23", 30 | "babel-core": "^5.8.25", 31 | "babel-loader": "^5.3.2", 32 | "isparta": "^3.0.3", 33 | "rimraf": "^2.3.4" 34 | }, 35 | "peerDependencies": { 36 | "redux": ">1.0.0" 37 | }, 38 | "author": "Mihail Diordiev ", 39 | "license": "MIT" 40 | } 41 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | function isPromise(obj) { 2 | return !!obj 3 | && (typeof obj === 'object' || typeof obj === 'function') 4 | && typeof obj.then === 'function' 5 | && typeof obj.catch === 'function'; 6 | } 7 | 8 | const notify = (events, config) => ({ dispatch }) => next => action => { 9 | let promiseChain; 10 | 11 | events.forEach( event => { 12 | if (event.catch.indexOf(action.type) !== -1) { 13 | if (config && config.noReverse) { 14 | if (action.notified) return; 15 | else action = {...action, notified: true}; 16 | } 17 | if (event.dispatch instanceof Function) { 18 | setTimeout(() => { dispatch(event.dispatch(action)) }, 0); 19 | } 20 | else if (event.dispatch instanceof Array) { 21 | event.dispatch.forEach( fn => { 22 | setTimeout(() => { 23 | if (isPromise(promiseChain)) { 24 | promiseChain = promiseChain 25 | .then(() => dispatch(fn(action))) 26 | .catch(e => {throw new Error(e)}); 27 | } else { 28 | const res = dispatch(fn(action)); 29 | if (isPromise(res)) { 30 | promiseChain = res; 31 | } 32 | } 33 | }, 0); 34 | }); 35 | } 36 | else throw new Error('Expected dispatch value to be a function or an array of functions.'); 37 | } 38 | }); 39 | return next(action); 40 | }; 41 | 42 | export default notify; 43 | --------------------------------------------------------------------------------