├── .editorconfig ├── .gitattributes ├── .gitignore ├── .npmignore ├── .prettierignore ├── .prettierrc ├── .travis.yml ├── .vscode ├── settings.json └── tasks.json ├── LICENSE ├── README.md ├── jest.config.js ├── package.json ├── src ├── InlineNotification.tsx ├── index.ts └── notifications.redux.ts ├── style.css ├── tests └── placeholder.spec.ts ├── tsconfig.build.json ├── tsconfig.json ├── tslint.json └── yarn.lock /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig helps developers define and maintain consistent 2 | # coding styles between different editors and IDEs 3 | # editorconfig.org 4 | 5 | root = true 6 | 7 | 8 | [*] 9 | 10 | # Change these settings to your own preference 11 | indent_style = space 12 | indent_size = 4 13 | 14 | # We recommend you to keep these unchanged 15 | end_of_line = lf 16 | charset = utf-8 17 | trim_trailing_whitespace = true 18 | insert_final_newline = true 19 | 20 | [*.md] 21 | trim_trailing_whitespace = false 22 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | build/ 2 | gh-pages/ 3 | dist-modules/ 4 | node_modules/ 5 | 6 | /dist/ 7 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JakeGinnivan/react-redux-notifications/718880a0be13eb692751ea3ac7dfe82e44a52e54/.npmignore -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | package.json 2 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "semi": false, 3 | "singleQuote": true, 4 | "tabWidth": 4, 5 | "trailingComma": "all" 6 | } 7 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - lts/* 4 | cache: yarn 5 | notifications: 6 | email: false 7 | script: yarn verify 8 | env: 9 | global: 10 | - secure: e3y8xJ0Npx1q1pArGTrkfkGBJiN66xhhP2XvKz9gKrq7nKOdou9FOv3QaR6tCHfqcrKEmTpFMmyKGCHDPNU3Eqkn5IUMtck06hxYpzlun3K3JvmWhrMSrMixnTv7LrfnAnLmO/Mw7wMtGkEJM+1etkMG+aNN83AtBC/m4NOtLaIdfg8Jvxn37M47xNfBJmPIk0/RAIWphIczjZQDE5+H7lvYIj9R219wfKvwAWqWvWaAGvJ5+cD1C2XO53YMsPcFnNtJCHP2ftQPyF4e6VifJgoV31GQ/dWIi98phjd5KyhJqQVpHiUQM8W0nl9b/ctNWASo8RJDPAfX/IOg7aAmoN5hEuSnw/EocFJWatDUINClOGkAaoVOKMFA0+rIDiy9+4EnpQ9jgmp0Ybc7NK1KDY13nAq8hTmVgFUbkLZxhRQri2ndoPH6nbkXdID9lFA2ZC0354O39dCyA3AUsr7/0ik3xbjmXvc265cWTIJQeRHS/ZeUrTVrEwG4yNTN2UdjSxnxipMy0Ylxupc7cW3/WrRaW00IPT+vW3xLerdGyoG1yQ/vx6oJRhsdLdiJBQ+3PRGwIe+KM8FheFGxu8s2EEFKGralNURopXp6rhuh/Jy7wmhBKFb4unBpjxnry9SSFN8eUGWgCSSTNP48/x0xJ3CJLGU8UPLK37VdpfuPH94= 11 | - secure: iimxfkyM0AEtr5+roXFBiwCE6bTW9duaufD6lMIwTSkJOCZo5x4Dp/uojyHvY50rPB6xxPeBiWAeLkzxn8J8OIVY7ijB+qyvWJSpSrzDYqHAGwQKxpPHkGFHMlo7Qe4ymrrROkfqmIkQgPomHpqSXsii6mlCv5sadmixOku9S0MivQTzQLiLJhnaAtPcLFizaFdlDlXcHm3JjX0uHkNTdBFtFXX9eHKlx4cKmV9CUJ2OIdy9BhK2UEJf3j+bf7uyP5MXD+K6oTwNT+ESsJ6UU3BB81ZAvrmfdt57dCp0B3DNHS+jGljWwLyE/+MUgQXBJFoEVTsnrrcylboEYxPNtaN99hTZQMHvN6rzazP3RdeSfhNl1cJi2FsLS6SF3Y3RVVA1xYfGyeFgcNfU/V7iNKRHBZr7jdW11gMdeEhWJwj7DcVE23k6giRcxwOFVGyhp/x39QY71EnWnlpvD71E0FDfEOSazlEU+318d1e4i9kYPgj+fqhd+n654yTAdV9XcYqptdQOUC+nEC57/rVP97ZlB6dMKdgKPzmATh2us33MQFo631Wlg4FRH5UzJRldCKn9s3XqBs+wHgDyM84Ia+iQUFRlGCVCqZgwEVL2mqcijrZRxZuz2Dz3AwUjXoju4O1lNVHYHLAuVM9XbO5C7NvF9unUudJEA8P1EiGj6AQ= 12 | jobs: 13 | include: 14 | - stage: release 15 | node_js: lts/* 16 | deploy: 17 | provider: script 18 | skip_cleanup: true 19 | script: 20 | - npx semantic-release 21 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.formatOnSave": true 3 | } 4 | -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | // See https://go.microsoft.com/fwlink/?LinkId=733558 3 | // for the documentation about the tasks.json format 4 | "version": "2.0.0", 5 | "tasks": [ 6 | { 7 | "type": "npm", 8 | "script": "build", 9 | "problemMatcher": [ 10 | "$tsc" 11 | ] 12 | } 13 | ] 14 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Jake Ginnivan 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-redux-notifications 2 | 3 | [![Build Status](https://travis-ci.com/JakeGinnivan/react-redux-notifications.svg?branch=master)](https://travis-ci.com/JakeGinnivan/react-redux-notifications) 4 | 5 | Say you have a form with an API call and when the form submit succeeds you want to show the user a success message, or 6 | in the event of a failure, show them a failure message. 7 | 8 | React-redux-notifications is a redux middleware powered notification system which makes this super easy in a decoupled way. 9 | 10 | ## Setup 11 | 12 | ```js 13 | // Reducer setup 14 | import { reducer as notifications } from 'react-redux-notifications' 15 | 16 | const todoApp = combineReducers({ 17 | notifications, 18 | ...restOfMiddleware, 19 | }) 20 | 21 | // Middleware setup 22 | import { middleware as NotificationMiddleware } from 'react-redux-notifications' 23 | 24 | let store = createStore( 25 | myAppReducers, 26 | compose( 27 | applyMiddleware(ReduxThunk, NotificationMiddleware), 28 | window.devToolsExtension ? window.devToolsExtension() : _ => _, 29 | ), 30 | ) 31 | 32 | // To use a different key to middleware 33 | import { createMiddleware } from 'react-redux-notifications' 34 | import { reducer as myCustomKey } from 'react-redux-notifications' 35 | 36 | const todoApp = combineReducers({ 37 | myCustomKey, 38 | ...restOfMiddleware, 39 | }) 40 | 41 | const NotificationMiddleware = createMiddleware('myCustomKey') 42 | 43 | let store = createStore( 44 | myAppReducers, 45 | compose( 46 | applyMiddleware(ReduxThunk, NotificationMiddleware), 47 | window.devToolsExtension ? window.devToolsExtension() : _ => _, 48 | ), 49 | ) 50 | 51 | 55 | ``` 56 | 57 | ## Example usage 58 | 59 | ```js 60 | 64 | 65 | 69 | ``` 70 | 71 | ## API 72 | 73 | ### InlineNotification component 74 | 75 | - `triggeredBy` - either string or array of strings defining which redux event(s) to listen for 76 | - `defaultMessage` - The fallback message to render. `notificationMessage` on the triggering event type will take precident 77 | - `hideAfter` - Hide notification after specified number of ms 78 | - `showDismiss` - Show the default dismiss button 79 | - `renderDismiss` - Override the rendering of the dismiss button (this has no effect when renderNotification is specified, as dismiss is part of the notification) 80 | - `renderNotification` - Override rendering each notification `function(notification) { return
{notification.message}
}` 81 | - `renderContainer` - Override the container render `function(notifications) { return
{notifications}
}` 82 | - `notifications` are the rendered components, you need to specify renderContainer and renderNotification separately 83 | - `reduxKey` - If not using the default `notifications` key for redux, your key can be specified here 84 | 85 | #### notification 86 | 87 | The notification object which is passed to `renderNotification` 88 | 89 | ```js 90 | { 91 | key: '' 92 | message: '' 93 | trigger: '' 94 | } 95 | ``` 96 | 97 | ## License 98 | 99 | _react-redux-notifications_ is available under MIT. See LICENSE for more details. 100 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | preset: 'ts-jest', 3 | testEnvironment: 'node' 4 | } 5 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-redux-notifications", 3 | "description": "Redux state driven notification components for React", 4 | "user": "JakeGinnivan", 5 | "author": "Jake Ginnivan", 6 | "version": "0.3.2", 7 | "scripts": { 8 | "prepack": "yarn verify && yarn build", 9 | "build": "yarn build:es2015 && yarn build:esm && yarn build:cjs", 10 | "build:es2015": "tsc --module es2015 --target es2015 --lib dom,es2015 --outDir dist/es2015 -p tsconfig.build.json", 11 | "build:esm": "tsc --module es2015 --target es5 --outDir dist/esm -p tsconfig.build.json", 12 | "build:cjs": "tsc --module commonjs --target es5 --outDir dist/cjs -p tsconfig.build.json", 13 | "lint": "yarn tslint --project tsconfig.build.json", 14 | "test": "jest", 15 | "verify": "yarn tsc -p tsconfig.json && yarn test && yarn lint", 16 | "semantic-release": "semantic-release" 17 | }, 18 | "main": "dist/cjs/index.js", 19 | "module": "dist/esm/index.js", 20 | "es2015": "dist/es2015/index.js", 21 | "devDependencies": { 22 | "@types/jest": "^23.3.10", 23 | "@types/prop-types": "^15.5.8", 24 | "@types/react": "^16.8.2", 25 | "@types/react-redux": "^7.0.1", 26 | "jest": "^24.1.0", 27 | "prettier": "^1.16.4", 28 | "react": "^16.8.1", 29 | "react-addons-css-transition-group": "^15.6.2", 30 | "react-addons-test-utils": "^15.6.2", 31 | "react-dom": "^16.8.1", 32 | "react-redux": "^4.4.5", 33 | "redux": "^3.5.0", 34 | "semantic-release": "^15.13.3", 35 | "ts-jest": "^23.10.5", 36 | "tslint": "^5.12.1", 37 | "tslint-config-prettier": "^1.18.0", 38 | "tslint-eslint-rules": "^5.4.0", 39 | "typescript": "^3.3.3" 40 | }, 41 | "peerDependencies": { 42 | "prop-types": "^15.6.2", 43 | "react": ">= 15.0.0 < 17.0.0", 44 | "react-redux": ">= 4.0.0 < 7.0.0", 45 | "redux": ">= 3.0.0 < 5.0.0" 46 | }, 47 | "dependencies": { 48 | "redux-thunk": "^2.0.1" 49 | }, 50 | "repository": { 51 | "type": "git", 52 | "url": "https://github.com/JakeGinnivan/ReduxNotifications.git" 53 | }, 54 | "homepage": "https://github.com/JakeGinnivan/ReduxNotifications", 55 | "bugs": { 56 | "url": "https://github.com/JakeGinnivan/ReduxNotifications/issues" 57 | }, 58 | "keywords": [ 59 | "react", 60 | "redux", 61 | "notifications" 62 | ], 63 | "license": "MIT", 64 | "pre-push": [ 65 | "test", 66 | "test:lint" 67 | ] 68 | } 69 | -------------------------------------------------------------------------------- /src/InlineNotification.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import PropTypes from 'prop-types' 3 | import { connect, HandleThunkActionCreator } from 'react-redux' 4 | import { 5 | listen, 6 | unlisten, 7 | hide, 8 | Notification, 9 | NotificationsState, 10 | } from './notifications.redux' 11 | 12 | interface MappedStateProps { 13 | subscribers: NotificationsState['subscribers'] 14 | } 15 | export interface OwnProps { 16 | defaultMessage?: string 17 | hideAfter?: number 18 | triggeredBy: string | string[] 19 | renderNotification?: ( 20 | notification: Notification, 21 | dismissNotification: () => void, 22 | ) => React.ReactElement 23 | renderContainer?: ( 24 | notifications: Array>, 25 | ) => React.ReactElement 26 | renderDismiss?: (dismiss: () => void) => React.ReactElement | null 27 | showDismiss?: boolean 28 | reduxKey?: string 29 | } 30 | interface DispatchProps { 31 | listen: HandleThunkActionCreator 32 | unlisten: HandleThunkActionCreator 33 | hide: HandleThunkActionCreator 34 | } 35 | 36 | type Props = DispatchProps & OwnProps & MappedStateProps 37 | 38 | class InlineNotification extends React.Component { 39 | static idSeed = 1 40 | static propTypes = { 41 | triggeredBy: PropTypes.oneOfType([ 42 | PropTypes.string.isRequired, 43 | PropTypes.arrayOf(PropTypes.string.isRequired), 44 | ]).isRequired, 45 | defaultMessage: PropTypes.string, 46 | hideAfter: PropTypes.number, 47 | renderNotification: PropTypes.func, 48 | renderContainer: PropTypes.func, 49 | renderDismiss: PropTypes.func, 50 | showDismiss: PropTypes.bool, 51 | } 52 | 53 | componentId = `inline-${InlineNotification.idSeed++}` 54 | 55 | componentDidMount() { 56 | this.dispatchListen(this.props) 57 | } 58 | 59 | componentWillUnmount() { 60 | this.props.unlisten(this.componentId) 61 | } 62 | 63 | componentDidUpdate(prevProps: Props) { 64 | if ( 65 | this.props.triggeredBy !== prevProps.triggeredBy || 66 | this.props.hideAfter !== prevProps.hideAfter || 67 | this.props.defaultMessage !== prevProps.defaultMessage 68 | ) { 69 | this.componentWillUnmount() 70 | this.dispatchListen(prevProps) 71 | } 72 | } 73 | 74 | dispatchListen(props: Props) { 75 | this.props.listen({ 76 | componentId: this.componentId, 77 | triggeredBy: Array.isArray(props.triggeredBy) 78 | ? props.triggeredBy 79 | : [props.triggeredBy], 80 | hideAfter: props.hideAfter || undefined, 81 | defaultMessage: props.defaultMessage || undefined, 82 | }) 83 | } 84 | 85 | dismiss = (notification: Notification) => { 86 | this.props.hide(this.componentId, notification.key) 87 | } 88 | 89 | renderNotification( 90 | notification: Notification, 91 | dismiss: React.ReactElement | null, 92 | ): React.ReactElement { 93 | return ( 94 |
95 | {notification.message} 96 | {dismiss} 97 |
98 | ) 99 | } 100 | 101 | renderContainer(notifications: Array>) { 102 | return
{notifications}
103 | } 104 | 105 | render() { 106 | const renderContainer = 107 | this.props.renderContainer || this.renderContainer 108 | const componentState = this.props.subscribers[this.componentId] 109 | 110 | return renderContainer( 111 | componentState 112 | ? componentState.notifications.map(n => { 113 | const dismiss = () => this.dismiss(n) 114 | if (this.props.renderNotification) { 115 | return this.props.renderNotification(n, dismiss) 116 | } 117 | 118 | const dismissEl = this.props.renderDismiss ? ( 119 | this.props.renderDismiss(dismiss) 120 | ) : this.props.showDismiss ? ( 121 | 127 | ) : null 128 | return this.renderNotification(n, dismissEl) 129 | }) 130 | : [], 131 | ) 132 | } 133 | } 134 | 135 | export default connect< 136 | MappedStateProps, 137 | DispatchProps, 138 | OwnProps, 139 | { [key: string]: NotificationsState } 140 | >( 141 | (state, ownProps) => { 142 | const reduxKey = ownProps.reduxKey || 'notifications' 143 | const notificationState = state[reduxKey] 144 | if (!notificationState) { 145 | throw new Error(`Missing ${reduxKey} key in redux state`) 146 | } 147 | return { 148 | subscribers: notificationState.subscribers, 149 | } 150 | }, 151 | { 152 | listen, 153 | hide, 154 | unlisten, 155 | }, 156 | )(InlineNotification) 157 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import InlineNotification from './InlineNotification.js' 2 | import reducer, { 3 | middleware, 4 | NotificationsState, 5 | NotificationActions, 6 | Notification, 7 | } from './notifications.redux' 8 | 9 | export { 10 | reducer, 11 | middleware, 12 | InlineNotification, 13 | NotificationsState, 14 | NotificationActions, 15 | Notification, 16 | } 17 | -------------------------------------------------------------------------------- /src/notifications.redux.ts: -------------------------------------------------------------------------------- 1 | import redux, { AnyAction } from 'redux' 2 | 3 | export interface ComponentState { 4 | options: ListenOptions 5 | notifications: Notification[] 6 | } 7 | export interface NotificationsState { 8 | subscribers: { 9 | [componentId: string]: ComponentState 10 | } 11 | } 12 | 13 | export interface Notification { 14 | key: string 15 | message: string 16 | trigger: AnyAction 17 | } 18 | 19 | export interface ListeningTo { 20 | hideAfter?: number | null 21 | defaultMessage?: string | null 22 | showDismiss: boolean 23 | } 24 | 25 | export const LISTEN_TO = 'notifications/listenTo' 26 | export const STOP_LISTEN = 'notifications/stopListen' 27 | export const SHOW_NOTIFICATION = 'notification/showNotification' 28 | export const HIDE_NOTIFICATION = 'notification/hideNotification' 29 | 30 | export interface ListenOptions { 31 | componentId: string 32 | triggeredBy: string[] 33 | hideAfter: number | undefined 34 | defaultMessage: string | undefined 35 | } 36 | 37 | export function listen(options: ListenOptions): ListenToAction { 38 | return { options, type: LISTEN_TO } 39 | } 40 | 41 | export function unlisten(componentId: string): StopListenAction { 42 | return { 43 | type: STOP_LISTEN, 44 | componentId, 45 | } 46 | } 47 | 48 | export function hide(componentId: string, key: string): HideNotificationAction { 49 | return { 50 | type: HIDE_NOTIFICATION, 51 | componentId, 52 | notificationKey: key, 53 | } 54 | } 55 | 56 | let keySeed = 0 57 | 58 | export function createMiddleware(key: string): redux.Middleware { 59 | return store => next => action => { 60 | const result = next(action) 61 | const state: NotificationsState | undefined = (store.getState() as any)[ 62 | key 63 | ] 64 | 65 | if (!state) { 66 | throw new Error( 67 | `No state key ${key}, ensure notifications middleware is registered`, 68 | ) 69 | } 70 | 71 | const notificationKey = `notification_${keySeed++}` 72 | const notificationMessage = 73 | action && (action as any).notificationMessage 74 | 75 | for (const componentId in state.subscribers) { 76 | if (state.subscribers.hasOwnProperty(componentId)) { 77 | const subscriber = state.subscribers[componentId] 78 | 79 | if ( 80 | subscriber.options.triggeredBy.indexOf(action.type) !== -1 81 | ) { 82 | const show: ShowNotificationAction = { 83 | type: SHOW_NOTIFICATION, 84 | componentId, 85 | notification: { 86 | key: notificationKey, 87 | message: 88 | notificationMessage || 89 | subscriber.options.defaultMessage, 90 | trigger: action, 91 | }, 92 | } 93 | store.dispatch(show) 94 | 95 | if (subscriber.options.hideAfter) { 96 | setTimeout(() => { 97 | const hideAction: HideNotificationAction = { 98 | type: HIDE_NOTIFICATION, 99 | notificationKey, 100 | componentId, 101 | } 102 | store.dispatch(hideAction) 103 | }, subscriber.options.hideAfter) 104 | } 105 | } 106 | } 107 | } 108 | 109 | return result 110 | } 111 | } 112 | 113 | export const middleware: redux.Middleware = createMiddleware('notifications') 114 | 115 | export interface ListenToAction { 116 | type: typeof LISTEN_TO 117 | options: ListenOptions 118 | } 119 | export interface StopListenAction { 120 | type: typeof STOP_LISTEN 121 | componentId: string 122 | } 123 | export interface ShowNotificationAction { 124 | type: typeof SHOW_NOTIFICATION 125 | componentId: string 126 | notification: Notification 127 | } 128 | export interface HideNotificationAction { 129 | type: typeof HIDE_NOTIFICATION 130 | componentId?: string 131 | notificationKey: string 132 | } 133 | export type NotificationActions = 134 | | ListenToAction 135 | | StopListenAction 136 | | ShowNotificationAction 137 | | HideNotificationAction 138 | 139 | const defaultState: NotificationsState = { subscribers: {} } 140 | 141 | // tslint:disable-next-line:no-shadowed-variable 142 | const reducer: redux.Reducer = function reducer( 143 | state = defaultState, 144 | action: NotificationActions | AnyAction, 145 | ): NotificationsState { 146 | const narrowedActions = action as NotificationActions 147 | switch (narrowedActions.type) { 148 | case LISTEN_TO: { 149 | return { 150 | ...state, 151 | subscribers: { 152 | ...state.subscribers, 153 | [narrowedActions.options.componentId]: { 154 | options: narrowedActions.options, 155 | notifications: [], 156 | }, 157 | }, 158 | } 159 | } 160 | 161 | case STOP_LISTEN: { 162 | const newListeningTo: NotificationsState = { 163 | ...state, 164 | subscribers: { ...state.subscribers }, 165 | } 166 | delete newListeningTo.subscribers[narrowedActions.componentId] 167 | 168 | return newListeningTo 169 | } 170 | 171 | case SHOW_NOTIFICATION: { 172 | return { 173 | ...state, 174 | subscribers: { 175 | ...state.subscribers, 176 | [narrowedActions.componentId]: { 177 | ...state.subscribers[narrowedActions.componentId], 178 | options: 179 | state.subscribers[narrowedActions.componentId] 180 | .options, 181 | notifications: [ 182 | ...state.subscribers[narrowedActions.componentId] 183 | .notifications, 184 | narrowedActions.notification, 185 | ], 186 | }, 187 | }, 188 | } 189 | } 190 | case HIDE_NOTIFICATION: { 191 | const newState: NotificationsState = { 192 | ...state, 193 | subscribers: { 194 | ...state.subscribers, 195 | }, 196 | } 197 | 198 | if (narrowedActions.componentId) { 199 | const existing = 200 | newState.subscribers[narrowedActions.componentId] 201 | newState.subscribers[narrowedActions.componentId] = { 202 | options: existing.options, 203 | notifications: existing.notifications.filter( 204 | val => val.key !== narrowedActions.notificationKey, 205 | ), 206 | } 207 | } else { 208 | for (const subscriberId in newState.subscribers) { 209 | if (newState.subscribers.hasOwnProperty(subscriberId)) { 210 | const subscriber = newState.subscribers[subscriberId] 211 | 212 | const filtered = subscriber.notifications.filter( 213 | notification => 214 | notification.key !== 215 | narrowedActions.notificationKey, 216 | ) 217 | if ( 218 | filtered.length !== subscriber.notifications.length 219 | ) { 220 | newState.subscribers[subscriberId] = { 221 | options: 222 | newState.subscribers[subscriberId].options, 223 | notifications: filtered, 224 | } 225 | } 226 | } 227 | } 228 | } 229 | 230 | return newState 231 | } 232 | } 233 | 234 | return state 235 | } 236 | 237 | export default reducer 238 | -------------------------------------------------------------------------------- /style.css: -------------------------------------------------------------------------------- 1 | /* TODO: insert default styles of your component here for easy access */ 2 | 3 | .notification { 4 | border-radius: 0.25rem; 5 | padding: 0.9375rem; 6 | border: #000 1px solid; 7 | background: lightgreen; 8 | } 9 | 10 | .notification_dismiss { 11 | float: right; 12 | position: relative; 13 | font-size: 1.3125rem; 14 | font-weight: 700; 15 | line-height: 1; 16 | color: #000; 17 | text-shadow: 0 1px 0 #fff; 18 | opacity: 0.2; 19 | cursor: pointer; 20 | background: unset; 21 | } 22 | 23 | .notification_dismiss:hover { 24 | opacity: 0.5; 25 | } 26 | -------------------------------------------------------------------------------- /tests/placeholder.spec.ts: -------------------------------------------------------------------------------- 1 | it('placeholder', () => { 2 | // TODO: test something now 3 | expect(true).toBe(true) 4 | }) 5 | -------------------------------------------------------------------------------- /tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "include": ["src/index.ts"], 3 | "extends": "./tsconfig.json", 4 | "compilerOptions": { 5 | "importHelpers": true /* Import emit helpers from 'tslib'. */, 6 | "noEmit": false /* Do not emit outputs. */, 7 | "declaration": true /* Generates corresponding '.d.ts' file. */, 8 | "sourceMap": true /* Generates corresponding '.map' file. */, 9 | 10 | /* Additional Checks */ 11 | "noUnusedLocals": true /* Report errors on unused locals. */, 12 | "noUnusedParameters": true /* Report errors on unused parameters. */, 13 | "noImplicitReturns": true /* Report error when not all code paths in function return a value. */, 14 | "noFallthroughCasesInSwitch": true /* Report errors for fallthrough cases in switch statement. */ 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "exclude": ["node_modules", "dist", "src/type-safety-fixtures"], 3 | "compilerOptions": { 4 | "target": "es5" /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017','ES2018' or 'ESNEXT'. */, 5 | "module": "commonjs" /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */, 6 | "lib": [ 7 | "dom", 8 | "es5" 9 | ] /* Specify library files to be included in the compilation. */, 10 | "noEmit": true /* Do not emit outputs. */, 11 | "strict": true /* Enable all strict type-checking options. */, 12 | "moduleResolution": "node" /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */, 13 | "esModuleInterop": true /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */, 14 | "jsx": "react" 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["tslint:latest", "tslint-eslint-rules", "tslint-config-prettier"], 3 | "rules": { 4 | "ordered-imports": false, 5 | "member-ordering": false, 6 | "object-literal-sort-keys": false, 7 | "member-access": false, 8 | "interface-name": false 9 | } 10 | } 11 | --------------------------------------------------------------------------------