├── .babelrc
├── .gitignore
├── .npmignore
├── .travis.yml
├── LICENSE
├── README.md
├── package-lock.json
├── package.json
├── src
├── __tests__
│ ├── actions.ts
│ ├── container.tsx
│ ├── reducer.ts
│ └── utils.ts
├── actions.ts
├── container.tsx
├── definitions.d.ts
├── index.ts
├── reducer.ts
├── types.ts
└── utils
│ ├── compare.ts
│ └── uniqueId.ts
├── tsconfig.json
└── webpack.config.js
/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "presets": [
3 | "env",
4 | "react-app"
5 | ]
6 | }
7 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .idea
2 | dist
3 | lib
4 | node_modules
5 | npm-debug.log
6 | coverage/
--------------------------------------------------------------------------------
/.npmignore:
--------------------------------------------------------------------------------
1 | .idea/
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: node_js
2 | node_js:
3 | - "8"
4 | script:
5 | - npm run test:coverage
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2018 Roman
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.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # React Toastify Redux [](https://travis-ci.org/fayster/react-toastify-redux) [](https://badge.fury.io/js/react-toastify-redux) [](https://github.com/fayster/react-toastify-redux) [](https://coveralls.io/github/fayster/react-toastify-redux?branch=master)
2 |
3 |
4 |
5 | Wraps [react-toastify](https://github.com/fkhadra/react-toastify) into a component and exposes actions and reducer.
6 |
7 | ## Installation
8 | ```
9 | $ npm install --save react-toastify-redux
10 | $ yarn add react-toastify-redux
11 | ```
12 |
13 | ## Usage
14 | Import the reducer and pass it to your combineReducers:
15 | ```javascript
16 | import {combineReducers} from 'redux';
17 | import {toastsReducer as toasts} from 'react-toastify-redux';
18 |
19 | export default combineReducers({
20 | // ...other reducers
21 | toasts
22 | });
23 | ```
24 |
25 | Include the toast controller in main view:
26 | ```javascript
27 | import {ToastController} from 'react-toastify-redux';
28 |
29 | export default () => {
30 | return (
31 |
32 | ...
33 |
34 |
35 | );
36 | };
37 | ```
38 |
39 | ### Actions
40 | Use actions for create, update and remove toasts:
41 |
42 | ```javascript
43 | import {dismiss, update, error, message, warning, success, info} from 'react-toastify-redux';
44 |
45 | dispatch(dismiss(id));
46 | dispatch(dismiss()); // dismiss all toasts
47 | dispatch(update(id, options));
48 | dispatch(message('Default message'));
49 | dispatch(success('Success message'));
50 | dispatch(error('Error message'));
51 | dispatch(warning('Warning message'));
52 | dispatch(info('Info message'));
53 | ```
54 |
55 | ### Customization toast
56 | Create toast component
57 | ```javascript
58 | export default ({ header, message }) => (
59 |
60 |
{header}
61 |
{message}
62 |
63 | );
64 | ```
65 |
66 | Include this component in ToastConroller
67 | ```javascript
68 | import {ToastController} from 'react-toastify-redux';
69 | import CustomToastComponent from 'awesome-folder/custom-toast-component';
70 |
71 | export default () => {
72 | return (
73 |
74 | ...
75 |
76 |
77 | );
78 | };
79 | ```
80 |
81 | ## API
82 |
83 | ### ToastContainer
84 | ToastContainer extends properties from ToastContainer in react-toastify. Also have new properties:
85 |
86 | | Props | Type | Default | Description |
87 | |----------------|-------------------------|---------|--------------------------------------------------|
88 | | toastComponent | ComponentClass or false | - | Element that will be displayed after emit toast |
89 |
90 | ### Dismiss
91 | | Parameter | Type | Required | Description |
92 | |-----------|--------|----------|--------------------------------------------------------------------------|
93 | | toast id | string | ✘ | Id toast for dismiss. If call action without id, then dismiss all toasts |
94 |
95 | ### Update
96 | | Parameter | Type | Required | Description |
97 | |-----------|--------|----------|----------------------|
98 | | toast id | string | ✓ | Id toast for update |
99 | | options | object | ✘ | Options listed below |
100 | * Available options :
101 | * [See: Toast Base Options](#toast-base-option)
102 | * message: toast message for update
103 |
104 | ### Toast Actions (Message, Success, Info, Warning, Error)
105 | | Parameter | Type | Required | Description |
106 | |-----------|--------|----------|----------------------|
107 | | message | string | ✓ | Message for toast |
108 | | options | object | ✘ | Options listed below |
109 | * Available options :
110 | * [See: Toast Base Options](#toast-base-option)
111 | * id: custom id for a toast. By default in generated automatically.
112 |
113 |
114 | ### Toast Base Options
115 | | Parameter | Type | Default | Description |
116 | |------------------------|---------|---------|----------------------|
117 | | renderDefaultComponent | boolean | false | Render default toast component? Use this propery if using custom toast component. |
118 | | title | string | '' | Title for custom toast component
119 | | type | One of: 'info', 'success', 'warning', 'error', 'default' | 'default' | Toast type
120 | | autoClose | number or false | 5000 | Set the delay in ms to close the toast automatically
121 | | hideProgressBar | boolean | false | Hide or show the progress bar
122 | | position | One of: 'top-right', 'top-center', 'top-left', 'bottom-right', 'bottom-center', 'bottom-left' | 'top-right' | Set the default position to use
123 | | pauseOnHover | boolean | true | Pause the timer when the mouse hover the toast
124 | | className | string or object | - | An optional css class to set
125 | | bodyClassName | string or object | - | An optional css class to set for the toast content.
126 | | progressClassName | string or object | - | An optional css class to set for the progress bar.
127 | | draggable | boolean | true | Allow toast to be draggable
128 | | draggablePercent | number | 80 | The percentage of the toast's width it takes for a drag to dismiss a toast
129 |
130 | ## License
131 | Licensed under MIT
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "react-toastify-redux",
3 | "version": "1.0.0-rc.2",
4 | "description": "react-toastify with Redux",
5 | "main": "lib/index.js",
6 | "typings": "src/definitions.d.ts",
7 | "scripts": {
8 | "test": "jest",
9 | "test:coverage": "jest --coverage && cat ./coverage/lcov.info | coveralls",
10 | "prebuild": "npm run test",
11 | "build": "npm run build:umd && npm run build:lib",
12 | "build:umd": "npm run clean:umd && cross-env NODE_ENV=production webpack",
13 | "build:lib": "npm run clean:lib && cross-env NODE_ENV=production tsc",
14 | "postbuild:lib": "cross-env NODE_ENV=production babel lib --out-dir lib && rimraf lib/container.jsx",
15 | "clean:umd": "rimraf dist/*",
16 | "clean:lib": "rimraf lib/*"
17 | },
18 | "repository": {
19 | "type": "git",
20 | "url": "git+https://github.com/fayster/react-toastify-redux.git"
21 | },
22 | "keywords": [
23 | "react",
24 | "redux",
25 | "notification",
26 | "react-toastify"
27 | ],
28 | "author": "fayster ",
29 | "license": "MIT",
30 | "bugs": {
31 | "url": "https://github.com/fayster/react-toastify-redux/issues"
32 | },
33 | "homepage": "https://github.com/fayster/react-toastify-redux#readme",
34 | "devDependencies": {
35 | "@types/jest": "^22.2.3",
36 | "@types/react": "^16.3.11",
37 | "@types/react-redux": "^5.0.16",
38 | "awesome-typescript-loader": "^5.0.0",
39 | "babel-cli": "^6.26.0",
40 | "babel-jest": "^22.4.3",
41 | "babel-loader": "^7.1.2",
42 | "babel-preset-env": "^1.6.0",
43 | "babel-preset-react-app": "^3.0.3",
44 | "coveralls": "^3.0.0",
45 | "cross-env": "^5.1.4",
46 | "enzyme": "^3.3.0",
47 | "enzyme-adapter-react-16": "^1.1.1",
48 | "jest": "^22.4.3",
49 | "react": "^16.3.2",
50 | "react-dom": "^16.3.2",
51 | "react-redux": "^5.0.7",
52 | "react-toastify": "^4.0.0-rc.5",
53 | "redux-mock-store": "^1.5.1",
54 | "rimraf": "^2.6.2",
55 | "ts-jest": "^22.4.4",
56 | "typescript": "^2.8.1",
57 | "webpack": "^4.6.0",
58 | "webpack-cli": "^2.0.14"
59 | },
60 | "peerDependencies": {
61 | "react": ">=15.0.0",
62 | "react-dom": ">=15.0.0",
63 | "react-redux": ">=4.4.9",
64 | "react-toastify": ">=4.0.0-rc.4"
65 | },
66 | "jest": {
67 | "moduleFileExtensions": [
68 | "ts",
69 | "tsx",
70 | "js"
71 | ],
72 | "transform": {
73 | "^.+\\.(ts|tsx)$": "/node_modules/ts-jest/preprocessor.js"
74 | },
75 | "testMatch": [
76 | "**/__tests__/*.(ts|tsx)"
77 | ],
78 | "globals": {
79 | "ts-jest": {
80 | "useBabelrc": true
81 | }
82 | }
83 | }
84 | }
85 |
--------------------------------------------------------------------------------
/src/__tests__/actions.ts:
--------------------------------------------------------------------------------
1 | import * as actions from '../actions';
2 | import * as types from '../types';
3 | import {toast, ToastType} from 'react-toastify';
4 |
5 | describe('actions', () => {
6 | const message = 'Foo bar';
7 |
8 | describe('message', () => {
9 | it('should create an action to add a default toast', () => {
10 | const expectedAction = {
11 | type: types.TOAST_MESSAGE,
12 | payload: {
13 | id: 'toast1',
14 | type: toast.TYPE.DEFAULT,
15 | message,
16 | title: 'foo bar'
17 | }
18 | };
19 | expect(actions.message(message, {title: 'foo bar'})).toEqual(expectedAction);
20 | });
21 | });
22 |
23 | describe('error', () => {
24 | it('should create an action to add a error toast', () => {
25 | const expectedAction = {
26 | type: types.TOAST_MESSAGE,
27 | payload: {
28 | id: 'toast2',
29 | type: toast.TYPE.ERROR,
30 | message
31 | }
32 | };
33 | expect(actions.error(message)).toEqual(expectedAction);
34 | });
35 | });
36 |
37 | describe('success', () => {
38 | it('should create an action to add a success toast', () => {
39 | const expectedAction = {
40 | type: types.TOAST_MESSAGE,
41 | payload: {
42 | id: 'toast3',
43 | type: toast.TYPE.SUCCESS,
44 | message
45 | }
46 | };
47 | expect(actions.success(message)).toEqual(expectedAction);
48 | });
49 | });
50 |
51 | describe('info', () => {
52 | it('should create an action to add a info toast', () => {
53 | const expectedAction = {
54 | type: types.TOAST_MESSAGE,
55 | payload: {
56 | id: 'toast4',
57 | type: toast.TYPE.INFO,
58 | message
59 | }
60 | };
61 | expect(actions.info(message)).toEqual(expectedAction);
62 | });
63 | });
64 |
65 | describe('warning', () => {
66 | it('should create an action to add a warning toast', () => {
67 | const expectedAction = {
68 | type: types.TOAST_MESSAGE,
69 | payload: {
70 | id: 'toast5',
71 | type: toast.TYPE.WARNING,
72 | message
73 | }
74 | };
75 | expect(actions.warning(message)).toEqual(expectedAction);
76 | });
77 | });
78 |
79 | describe('dismiss', () => {
80 | it('should create an action to dismiss a toast', () => {
81 | const expectedAction = {
82 | type: types.TOAST_DISMISS,
83 | payload: {
84 | id: 'toast1'
85 | }
86 | };
87 | expect(actions.dismiss('toast1')).toEqual(expectedAction);
88 | });
89 | });
90 |
91 | describe('update', () => {
92 | it('should create an action to update a toast', () => {
93 | const updateOptions = {
94 | message: 'Hello world',
95 | position: toast.POSITION.BOTTOM_CENTER
96 | };
97 | const expectedAction = {
98 | type: types.TOAST_UPDATE,
99 | payload: {
100 | id: 'toast1',
101 | options: {...updateOptions}
102 | }
103 | };
104 | expect(actions.update('toast1', updateOptions)).toEqual(expectedAction);
105 | });
106 | });
107 |
108 | describe('toastActionCreator', () => {
109 | it('should create an action to add a default toast', () => {
110 | const options = {
111 | title: 'Default message',
112 | message: 'Hello world',
113 | position: toast.POSITION.BOTTOM_CENTER
114 | };
115 | const expectedAction = {
116 | type: types.TOAST_MESSAGE,
117 | payload: {
118 | type: toast.TYPE.DEFAULT,
119 | id: 'toast6',
120 | ...options,
121 | message
122 | }
123 | };
124 | expect(actions.toastActionCreator(toast.TYPE.DEFAULT as ToastType)(message, options)).toEqual(expectedAction);
125 | });
126 | });
127 | });
--------------------------------------------------------------------------------
/src/__tests__/container.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react'
2 | import Enzyme, {mount} from 'enzyme';
3 | import Adapter from 'enzyme-adapter-react-16';
4 | import {ToastContainer} from '../container';
5 | import {toast} from 'react-toastify';
6 | import {Toast} from "../definitions";
7 | import * as types from "../types";
8 | import * as actions from "../actions";
9 | import ConnectedToastContainer from "../container";
10 | import configureStore from 'redux-mock-store';
11 | import {Provider} from 'react-redux';
12 |
13 | jest.useFakeTimers();
14 |
15 | Enzyme.configure({adapter: new Adapter()});
16 |
17 | describe('container', () => {
18 | const toastList = [{
19 | id: 'toast1',
20 | message: 'Foo bar',
21 | type: toast.TYPE.DEFAULT
22 | }] as Toast[];
23 |
24 | const CustomComponent = ({message}) => (
25 | {message}
26 | );
27 |
28 | describe('unconnected component', () => {
29 | const dismiss = jest.fn();
30 |
31 | let wrapper;
32 |
33 | beforeEach(() => {
34 | dismiss.mockClear();
35 | wrapper = mount();
36 | });
37 |
38 | afterEach(() => {
39 | wrapper.unmount();
40 | });
41 |
42 | describe('without custom component', () => {
43 | it('should render self and one toast', () => {
44 | expect(wrapper.find('div.Toastify__toast--default').length).toBe(0);
45 |
46 | jest.runAllTimers();
47 |
48 | wrapper.update();
49 |
50 | expect(wrapper.find('div.Toastify__toast--default').length).toBe(1);
51 | });
52 |
53 | it('should update toasts', () => {
54 | jest.runAllTimers();
55 |
56 | wrapper.setProps({
57 | toastList: [{
58 | id: 'toast1',
59 | message: 'Foo bar',
60 | type: toast.TYPE.WARNING,
61 | }, {
62 | id: 'toast2',
63 | message: 'Hello World',
64 | type: toast.TYPE.INFO
65 | }]
66 | });
67 |
68 | jest.runAllTimers();
69 | wrapper.update();
70 |
71 | expect(wrapper.find('div.Toastify__toast').length).toBe(2);
72 | expect(wrapper.find('div.Toastify__toast').at(0).find('div.Toastify__toast-body').text()).toBe('Foo bar');
73 | expect(wrapper.find('div.Toastify__toast').at(1).find('div.Toastify__toast-body').text()).toBe('Hello World');
74 | });
75 |
76 | it('should dismiss toast', () => {
77 | jest.runAllTimers();
78 |
79 | wrapper.update();
80 |
81 | expect(wrapper.find('div.Toastify__toast').length).toBe(1);
82 | wrapper.find('div.Toastify__toast').at(0).find('button.Toastify__close-button').simulate('click');
83 |
84 | jest.runAllTimers();
85 | wrapper.update();
86 |
87 | expect(wrapper.find('div.Toastify__toast').length).toBe(0);
88 | expect(dismiss.mock.calls.length).toBe(1);
89 | });
90 |
91 | it('should dismiss toast without click', () => {
92 | jest.runAllTimers();
93 |
94 | wrapper.setProps({toastList: []});
95 |
96 | jest.runAllTimers();
97 |
98 | wrapper.update();
99 |
100 | expect(wrapper.find('div.Toastify__toast').length).toBe(0);
101 | expect(dismiss.mock.calls.length).toBe(1);
102 | });
103 | });
104 |
105 | describe('with custom component', () => {
106 | it('should render custom toast', () => {
107 | jest.runAllTimers();
108 |
109 | wrapper.setProps({
110 | toastComponent: CustomComponent,
111 | toastList: [{...toastList[0], type: toast.TYPE.WARNING}]
112 | });
113 |
114 | jest.runAllTimers();
115 |
116 | wrapper.update();
117 |
118 | const toasts = wrapper.find('div.Toastify__toast');
119 | expect(toasts.length).toBe(1);
120 | expect(toasts.find('div.foo-bar').text()).toBe('Foo bar');
121 |
122 | wrapper.setProps({
123 | toastComponent: undefined,
124 | toastList: [{...toastList[0], type: toast.TYPE.WARNING}]
125 | });
126 |
127 | jest.runAllTimers();
128 |
129 | wrapper.update();
130 |
131 | expect(wrapper.find('div.Toastify__toast').at(0).find('div.Toastify__toast-body').text()).toBe('Foo bar');
132 | });
133 |
134 | it('should update toasts', () => {
135 | jest.runAllTimers();
136 |
137 | wrapper.update();
138 |
139 | expect(wrapper.find('div.Toastify__toast').length).toBe(1);
140 |
141 | wrapper.setProps({
142 | toastList: [{
143 | ...toastList[0],
144 | renderDefaultComponent: true,
145 | message: 'Bar foo'
146 | }]
147 | });
148 |
149 | jest.runAllTimers();
150 | wrapper.update();
151 |
152 | expect(wrapper.find('div.Toastify__toast').length).toBe(1);
153 | expect(wrapper.find('div.Toastify__toast').at(0).find('div.Toastify__toast-body').text()).toBe('Bar foo');
154 |
155 | wrapper.setProps({
156 | toastComponent: CustomComponent,
157 | renderDefaultComponent: true,
158 | toastList
159 | });
160 |
161 | jest.runAllTimers();
162 | wrapper.update();
163 |
164 | expect(wrapper.find('div.Toastify__toast').length).toBe(1);
165 | expect(wrapper.find('div.Toastify__toast').at(0).find('div.Toastify__toast-body').text()).toBe('Foo bar');
166 | });
167 | });
168 | });
169 |
170 | describe('unconnect component with init toast component', () => {
171 | const dismiss = jest.fn();
172 |
173 | let wrapper;
174 |
175 | beforeEach(() => {
176 | dismiss.mockClear();
177 | wrapper = mount(
178 | );
183 | });
184 |
185 | afterEach(() => {
186 | wrapper.unmount();
187 | });
188 |
189 | it('should render custom toast', () => {
190 | jest.runAllTimers();
191 | wrapper.update();
192 |
193 | const toasts = wrapper.find('div.Toastify__toast');
194 | expect(toasts.length).toBe(1);
195 | expect(toasts.find('div.foo-bar').text()).toBe('Foo bar');
196 | });
197 | });
198 |
199 | describe('connected component', () => {
200 | const initialState = {
201 | toasts: toastList
202 | };
203 |
204 | const mockStore = configureStore();
205 | let store;
206 | let providerWrapper;
207 |
208 | beforeEach(() => {
209 | store = mockStore(initialState);
210 | providerWrapper = mount(
211 |
212 |
213 |
214 | );
215 | });
216 |
217 | afterEach(() => {
218 | providerWrapper.unmount();
219 | });
220 |
221 | it('should render the connected component', () => {
222 | expect(providerWrapper.find(ConnectedToastContainer).length).toEqual(1);
223 | });
224 |
225 | it('should check prop matches with initialState', () => {
226 | expect(providerWrapper.find(ToastContainer).prop('toastList')).toEqual(initialState.toasts);
227 | });
228 |
229 | it('should call dismiss action', () => {
230 | providerWrapper.find(ToastContainer).prop('dismiss')();
231 |
232 | expect(store.getActions()[0].type).toBe(types.TOAST_DISMISS);
233 | });
234 |
235 | it('should call actions ', () => {
236 | store.dispatch(actions.message('Foo bar'));
237 | store.dispatch(actions.dismiss('toast1'));
238 |
239 | const calledActions = store.getActions();
240 |
241 | expect(calledActions[0].type).toBe(types.TOAST_MESSAGE);
242 | expect(calledActions[1].type).toBe(types.TOAST_DISMISS);
243 | });
244 | });
245 | });
--------------------------------------------------------------------------------
/src/__tests__/reducer.ts:
--------------------------------------------------------------------------------
1 | import * as types from '../types';
2 | import {Toast} from "../definitions";
3 | import reducer from '../reducer';
4 |
5 | const initialState: Toast[] = [];
6 |
7 | describe('reducer', () => {
8 | it('should return the initial state', () => {
9 | expect(reducer(undefined, {
10 | type: 'UNKNOWN_ACTION_TYPE',
11 | payload: null
12 | })).toEqual(initialState);
13 | });
14 |
15 | it('should handle TOAST_MESSAGE', () => {
16 | expect(reducer([], {
17 | type: types.TOAST_MESSAGE,
18 | payload: {
19 | id: 'toast1',
20 | message: 'foo bar'
21 | }
22 | })).toEqual([{
23 | id: 'toast1',
24 | message: 'foo bar',
25 | }]);
26 | });
27 |
28 | it('should handle TOAST_DISMISS', () => {
29 | expect(reducer([], {
30 | type: types.TOAST_DISMISS,
31 | payload: {
32 | id: 'toast1'
33 | }
34 | })).toEqual([]);
35 |
36 | expect(reducer([{
37 | id: 'toast1',
38 | message: 'foo bar'
39 | }], {
40 | type: types.TOAST_DISMISS,
41 | payload: {
42 | id: 'toast2'
43 | }
44 | })).toEqual([{
45 | id: 'toast1',
46 | message: 'foo bar',
47 | }]);
48 |
49 | expect(reducer([{
50 | id: 'toast1',
51 | message: 'foo bar'
52 | }], {
53 | type: types.TOAST_DISMISS,
54 | payload: {}
55 | })).toEqual([]);
56 | });
57 |
58 | it('should handle TOAST_UPDATE', () => {
59 | const state = [{
60 | id: 'toast1',
61 | message: 'foo bar',
62 | }];
63 |
64 | expect(reducer(state, {
65 | type: types.TOAST_UPDATE,
66 | payload: {
67 | id: 'toast1',
68 | options: {
69 | message: 'hello world',
70 | type: 'message'
71 | }
72 | }
73 | })).toEqual([{
74 | id: 'toast1',
75 | message: 'hello world',
76 | type: 'message'
77 | }]);
78 |
79 | expect(reducer(state, {
80 | type: types.TOAST_UPDATE,
81 | payload: {
82 | id: 'toast2',
83 | options: {
84 | message: 'hello world',
85 | type: 'message'
86 | }
87 | }
88 | })).toEqual([{
89 | id: 'toast1',
90 | message: 'foo bar',
91 | }]);
92 | });
93 | });
--------------------------------------------------------------------------------
/src/__tests__/utils.ts:
--------------------------------------------------------------------------------
1 | import compare from '../utils/compare';
2 | import uniqueId from '../utils/uniqueId';
3 |
4 | describe('compare', () => {
5 | const value = {foo: 'bar'};
6 |
7 | it('Should return true, with call equal objects', () => {
8 | const other = {...value};
9 |
10 | expect(compare(value, other)).toBeTruthy();
11 | });
12 |
13 | it('Should return false, with call not equal objects', () => {
14 | const other = {...value, hello: 'world'};
15 |
16 | expect(compare(value, other)).toBeFalsy();
17 | });
18 |
19 | it('Should return false, with call equal nested objects', () => {
20 | const anotherValue = {...value, bar: {foo: 'foobar'}};
21 | const other = {...value, bar: {foo: 'foobar'}};
22 |
23 | expect(compare(anotherValue, other)).toBeFalsy();
24 | });
25 |
26 | it('Should return true, with call equal by link objects', () => {
27 | expect(compare(value, value)).toBeTruthy();
28 | });
29 | });
30 |
31 | describe('uniqueId', () => {
32 | const testPrefix = 'test';
33 |
34 | it('Should return 1 with first call', () => {
35 | expect(uniqueId(testPrefix)).toBe('test1');
36 | });
37 | });
--------------------------------------------------------------------------------
/src/actions.ts:
--------------------------------------------------------------------------------
1 | import * as types from './types';
2 | import {toast, ToastType} from "react-toastify";
3 | import uniqueId from './utils/uniqueId';
4 | import {
5 | ToastAction, DismissActionPayload, Toast, ToastOptions, UpdateActionOptions, UpdateActionPayload
6 | } from './definitions';
7 |
8 | export const toastActionCreator = (type: ToastType) => {
9 | return (message: any, options: ToastOptions = {}): ToastAction => ({
10 | type: types.TOAST_MESSAGE,
11 | payload: {
12 | id: options.id || uniqueId('toast'),
13 | ...options,
14 | message,
15 | type
16 | }
17 | });
18 | };
19 |
20 | export const dismiss = (id?: string): ToastAction => ({
21 | type: types.TOAST_DISMISS,
22 | payload: {id}
23 | });
24 |
25 | export const update = (id: string, options: UpdateActionOptions): ToastAction => ({
26 | type: types.TOAST_UPDATE,
27 | payload: {id, options}
28 | });
29 |
30 | export const error = toastActionCreator(toast.TYPE.ERROR as ToastType);
31 | export const warning = toastActionCreator(toast.TYPE.WARNING as ToastType);
32 | export const info = toastActionCreator(toast.TYPE.INFO as ToastType);
33 | export const message = toastActionCreator(toast.TYPE.DEFAULT as ToastType);
34 | export const success = toastActionCreator(toast.TYPE.SUCCESS as ToastType);
--------------------------------------------------------------------------------
/src/container.tsx:
--------------------------------------------------------------------------------
1 | import React, {ComponentClass, SFC} from 'react';
2 | import {toast, ToastContainer as ReactToastContainer, ToastContainerProps, ToastOptions} from 'react-toastify';
3 | import {connect} from "react-redux";
4 | import compare from './utils/compare';
5 | import {dismiss} from "./actions";
6 | import {Toast, ToastComponentAdditionalProps} from './definitions';
7 |
8 | interface ToastIds {
9 | [storageToastId: string]: number;
10 | }
11 |
12 | type ToasterContainerProps = OwnProps & StateProps & DispatchProps;
13 |
14 | export class ToastContainer extends React.Component {
15 | private _toastIds: ToastIds = {};
16 |
17 | private getCustomComponentProps = (toastItem: Toast): ToastComponentAdditionalProps => {
18 | const {id, message, title=''} = toastItem;
19 |
20 | return {
21 | id,
22 | message,
23 | title
24 | };
25 | };
26 |
27 | private getToastOptions = (toastItem: Toast): ToastOptions => {
28 | const {
29 | id,
30 | message,
31 | title,
32 | renderDefaultComponent,
33 | ...options
34 | } = toastItem;
35 |
36 | return {
37 | onClose: () => this.onCloseHandler(id),
38 | ...options
39 | };
40 | };
41 |
42 | private renderToasts = (nextProps: ToasterContainerProps) => {
43 | nextProps.toastList.forEach((toastItem: Toast) => {
44 | const {renderDefaultComponent = false} = toastItem;
45 |
46 | // new toast
47 | if (!(toastItem.id in this._toastIds)) {
48 | this._toastIds[toastItem.id] = (nextProps.toastComponent && !renderDefaultComponent)
49 | ? toast(
50 | React.createElement(
51 | nextProps.toastComponent,
52 | this.getCustomComponentProps(toastItem)
53 | ),
54 | this.getToastOptions(toastItem)
55 | )
56 | : toast(toastItem.message, this.getToastOptions(toastItem));
57 | }
58 |
59 | // update toast
60 | const foundToast = this.props.toastList.find(toast => toast.id === toastItem.id);
61 | if (foundToast && (!compare(toastItem, foundToast) || nextProps.toastComponent !== this.props.toastComponent)) {
62 | toast.update(this._toastIds[toastItem.id], {
63 | ...this.getToastOptions(toastItem),
64 | render: (nextProps.toastComponent && !renderDefaultComponent)
65 | ? React.createElement(nextProps.toastComponent, this.getCustomComponentProps(toastItem))
66 | : toastItem.message
67 | });
68 | }
69 | });
70 |
71 | // delete toast
72 | this.props.toastList
73 | .filter((toastItem: Toast) => {
74 | const foundItem = nextProps.toastList.find(nextToastItem => nextToastItem.id === toastItem.id);
75 | return !foundItem && toastItem.id in this._toastIds;
76 | })
77 | .forEach((doomedToast) => this.closeToast(doomedToast.id));
78 | };
79 |
80 | private closeToast = (storageToastId: string) => {
81 | /* istanbul ignore next */
82 | const {[storageToastId]: _toastId, ...toastIds} = this._toastIds;
83 | this._toastIds = toastIds;
84 |
85 | toast.dismiss(_toastId);
86 | };
87 |
88 | private onCloseHandler = (storageToastId: string) => {
89 | this.closeToast(storageToastId);
90 | this.props.dismiss(storageToastId);
91 | };
92 |
93 | public componentDidMount() {
94 | this.renderToasts(this.props);
95 | }
96 |
97 | public componentWillUnmount() {
98 | this.props.dismiss();
99 | this._toastIds = {};
100 | }
101 |
102 | public componentWillReceiveProps(nextProps: ToasterContainerProps) {
103 | this.renderToasts(nextProps);
104 | }
105 |
106 | public shouldComponentUpdate(nextProps: ToasterContainerProps) {
107 | return this.props !== nextProps;
108 | }
109 |
110 | public render() {
111 | const {dismiss, toastList, toastComponent, ...rest} = this.props;
112 |
113 | return (
114 |
115 | );
116 | }
117 | }
118 |
119 | export interface OwnProps extends ToastContainerProps {
120 | toastComponent?: SFC | ComponentClass | string;
121 | }
122 |
123 | export interface StateProps {
124 | toastList: Toast[];
125 | }
126 |
127 | const mapStateToProps = (state): StateProps => ({
128 | toastList: state.toasts
129 | });
130 |
131 | export interface DispatchProps {
132 | dismiss(id?: string): void;
133 | }
134 |
135 | const mapDispatchToProps = (dispatch): DispatchProps => ({
136 | dismiss: (id) => dispatch(dismiss(id))
137 | });
138 |
139 | export default connect(mapStateToProps, mapDispatchToProps)(ToastContainer);
140 |
--------------------------------------------------------------------------------
/src/definitions.d.ts:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import {ToastType, ToastContainerProps as ReactToastContainerProps} from "react-toastify";
3 | import {ComponentClass, SFC} from "react";
4 |
5 | export interface ToastBaseOptions {
6 | /**
7 | * Render default or custom toast component
8 | * If use only default component, this property will be ignored
9 | * `Default: false`
10 | */
11 | renderDefaultComponent?: boolean;
12 |
13 | /**
14 | * Title for custom toast component
15 | * `Default: ''`
16 | */
17 | title?: string;
18 |
19 | /**
20 | * Set the toast type.
21 | * `One of: 'info', 'success', 'warning', 'error', 'default'`
22 | */
23 | type?: ToastType;
24 |
25 | /**
26 | * Pause the timer when the mouse hover the toast.
27 | * `Default: true`
28 | */
29 | pauseOnHover?: boolean;
30 |
31 | /**
32 | * Remove the toast when clicked.
33 | * `Default: true`
34 | */
35 | closeOnClick?: boolean;
36 |
37 | /**
38 | * Set the delay in ms to close the toast automatically.
39 | * Use `false` to prevent the toast from closing.
40 | * `Default: 5000`
41 | */
42 | autoClose?: number | false;
43 |
44 | /**
45 | * Set the default position to use.
46 | * `One of: 'top-right', 'top-center', 'top-left', 'bottom-right', 'bottom-center', 'bottom-left'`
47 | * `Default: 'top-right'`
48 | */
49 | position?: string;
50 |
51 | /**
52 | * An optional css class to set for the progress bar.
53 | */
54 | progressClassName?: string | object;
55 |
56 | /**
57 | * An optional css class to set.
58 | */
59 | className?: string | object;
60 |
61 | /**
62 | * An optional css class to set for the toast content.
63 | */
64 | bodyClassName?: string | object;
65 |
66 | /**
67 | * Hide or show the progress bar.
68 | * `Default: false`
69 | */
70 | hideProgressBar?: boolean;
71 |
72 | /**
73 | * Allow toast to be draggable
74 | * `Default: true`
75 | */
76 | draggable?: boolean;
77 |
78 | /**
79 | * The percentage of the toast's width it takes for a drag to dismiss a toast
80 | * `Default: 80`
81 | */
82 | draggablePercent?: number;
83 | }
84 |
85 | /**
86 | * Toast item property
87 | */
88 | export interface Toast extends ToastBaseOptions {
89 | id: string;
90 | message: any;
91 | }
92 |
93 | /**
94 | * Toast options for add toast actions
95 | */
96 | export interface ToastOptions extends ToastBaseOptions {
97 | id?: string;
98 | }
99 |
100 | /**
101 | * Toast Container options
102 | */
103 | export interface ToastContainerProps {
104 | /**
105 | * Custom toast component
106 | * `Default: undefined`
107 | */
108 | toastComponent?: SFC | ComponentClass | string;
109 | }
110 |
111 | /**
112 | * Additional props for custom toast component
113 | */
114 | export interface ToastComponentAdditionalProps {
115 | id: string;
116 | title: string;
117 | message: any;
118 | }
119 |
120 | /**
121 | * Props for custom toast component
122 | */
123 | export interface ToastComponentProps extends ToastComponentAdditionalProps {
124 | /**
125 | * Close toast handler
126 | */
127 | closeToast(): void;
128 | }
129 |
130 | /**
131 | * Toast action type
132 | */
133 | export interface ToastAction {
134 | /**
135 | * Action type
136 | */
137 | type: string;
138 | payload: T;
139 | }
140 |
141 | /**
142 | * Dismiss action payload
143 | */
144 | export interface DismissActionPayload {
145 | /**
146 | * Identificational number for dismiss toast
147 | */
148 | id: string;
149 | }
150 |
151 | /**
152 | * Update action options
153 | */
154 | export interface UpdateActionOptions extends ToastBaseOptions {
155 | message: any;
156 | }
157 |
158 | /**
159 | * Update action payload
160 | */
161 | export interface UpdateActionPayload {
162 | /**
163 | * Identificational number for update toast
164 | */
165 | id: string;
166 | options: UpdateActionOptions;
167 | }
168 |
169 | export const toastsReducer: (toastList: Toast[], action: ToastAction) => Toast[];
170 |
171 | export class ToastContainer extends React.Component {
172 | }
173 |
174 | export const dismiss: (id?: string) => ToastAction;
175 | export const update: (id: string, options: UpdateActionOptions) => ToastAction;
176 | export const error: (message: string, options?: ToastOptions) => ToastAction;
177 | export const warning: (message: string, options?: ToastOptions) => ToastAction;
178 | export const info: (message: string, options?: ToastOptions) => ToastAction;
179 | export const message: (message: string, options?: ToastOptions) => ToastAction;
180 | export const success: (message: string, options?: ToastOptions) => ToastAction;
--------------------------------------------------------------------------------
/src/index.ts:
--------------------------------------------------------------------------------
1 | import {dismiss, update, error, message, warning, success, info} from './actions';
2 | import reducer from './reducer';
3 | import ToastContainer from './container';
4 |
5 | export {
6 | dismiss,
7 | update,
8 | error,
9 | message,
10 | warning,
11 | success,
12 | info,
13 | reducer as toastsReducer,
14 | ToastContainer
15 | };
--------------------------------------------------------------------------------
/src/reducer.ts:
--------------------------------------------------------------------------------
1 | import * as types from './types';
2 | import {Toast, ToastAction, DismissActionPayload, UpdateActionPayload} from './definitions';
3 |
4 | const initialState: Toast[] = [];
5 |
6 | const handlers = {
7 | [types.TOAST_MESSAGE]: (toasts: Toast[], action: ToastAction) => (
8 | toasts.concat(action.payload)
9 | ),
10 | [types.TOAST_DISMISS]: (toasts: Toast[], action: ToastAction) => (
11 | 'id' in action.payload
12 | ? toasts.filter(toast => toast.id !== action.payload.id)
13 | : []
14 | ),
15 | [types.TOAST_UPDATE]: (toasts: Toast[], action: ToastAction) => (
16 | toasts.map(toast => toast.id === action.payload.id
17 | ? {...toast, ...action.payload.options}
18 | : toast
19 | )
20 | )
21 | };
22 |
23 | export default (toasts = initialState, action: ToastAction) => {
24 | return action.type in handlers
25 | ? handlers[action.type](toasts, action)
26 | : toasts;
27 | };
28 |
--------------------------------------------------------------------------------
/src/types.ts:
--------------------------------------------------------------------------------
1 | export const TOAST_DISMISS = 'TOAST_DISMISS';
2 | export const TOAST_MESSAGE = 'TOAST_MESSAGE';
3 | export const TOAST_UPDATE = 'TOAST_UPDATE';
--------------------------------------------------------------------------------
/src/utils/compare.ts:
--------------------------------------------------------------------------------
1 | export default (value, other) => {
2 | if (value === other) {
3 | return true;
4 | }
5 |
6 | if (
7 | value instanceof Object && other instanceof Object &&
8 | Object.keys(value).length === Object.keys(other).length
9 | ) {
10 | return !Object.keys(value).some(keyValue =>
11 | !(keyValue in other && value[keyValue] === other[keyValue])
12 | );
13 | }
14 |
15 | return false;
16 | }
--------------------------------------------------------------------------------
/src/utils/uniqueId.ts:
--------------------------------------------------------------------------------
1 | let idCounter = 0;
2 |
3 | export default (prefix: string) => {
4 | const id = ++idCounter;
5 | return `${prefix}${id}`;
6 | };
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "baseUrl": ".",
4 | "outDir": "lib",
5 | "allowSyntheticDefaultImports": true,
6 | "module": "es6",
7 | "target": "es6",
8 | "jsx": "preserve",
9 | "moduleResolution": "node",
10 | "lib": [
11 | "es2015"
12 | ]
13 | },
14 | "include": [
15 | "src/index.ts",
16 | "externals.d.ts"
17 | ]
18 | }
19 |
--------------------------------------------------------------------------------
/webpack.config.js:
--------------------------------------------------------------------------------
1 | const webpack = require('webpack');
2 | const path = require('path');
3 |
4 | module.exports = {
5 | mode: process.env.NODE_ENV || 'production',
6 | devtool: 'source-map',
7 | entry: './src/index.ts',
8 | output: {
9 | path: __dirname + "/dist",
10 | filename: 'ReactToastifyRedux.js',
11 | libraryTarget: 'umd',
12 | library: 'ReactToastifyRedux'
13 | },
14 | module: {
15 | rules: [
16 | {
17 | test: /\.tsx?$/,
18 | exclude: /node_modules/,
19 | loader: ['babel-loader', 'awesome-typescript-loader']
20 | }
21 | ]
22 | },
23 | resolve: {
24 | modules: ['node_modules'],
25 | extensions: ['.js', '.jsx', '.tsx', '.ts'],
26 | alias: {
27 | react: path.resolve('./node_modules/react'),
28 | 'react-dom': path.resolve('./node_modules/react-dom'),
29 | 'react-toasify': path.resolve('./node_modules/react-toastify'),
30 | 'react-redux': path.resolve('./node_modules/react-redux')
31 | }
32 | },
33 | externals: [
34 | 'react',
35 | 'react-dom',
36 | 'react-toastify',
37 | 'react-redux'
38 | ],
39 | plugins: [
40 | new webpack.DefinePlugin({
41 | 'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV)
42 | })
43 | ]
44 | };
--------------------------------------------------------------------------------