├── .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 | 
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 ;
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 |
--------------------------------------------------------------------------------