├── .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 [![Build Status](https://travis-ci.org/fayster/react-toastify-redux.svg?branch=develop)](https://travis-ci.org/fayster/react-toastify-redux) [![npm version](https://badge.fury.io/js/react-toastify-redux.svg)](https://badge.fury.io/js/react-toastify-redux) [![npm](https://img.shields.io/npm/dm/react-toastify-redux.svg)](https://github.com/fayster/react-toastify-redux) [![Coverage Status](https://coveralls.io/repos/github/fayster/react-toastify-redux/badge.svg?branch=develop)](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 | }; --------------------------------------------------------------------------------