Skip to content

Commit

Permalink
feat(ToastContainer): rewrite
Browse files Browse the repository at this point in the history
  • Loading branch information
tomchentw committed Oct 28, 2017
1 parent 9469454 commit a3cd015
Show file tree
Hide file tree
Showing 6 changed files with 478 additions and 0 deletions.
194 changes: 194 additions & 0 deletions src/components/ToastContainer.jsx
@@ -0,0 +1,194 @@
// @flow
import _ from "lodash"
import React from "react"

import ToastMessageAnimated from "./ToastMessage/ToastMessageAnimated"
import type { IconClassNames } from "./ToastMessage/ToastMessage"

type Props = {
toastType?: IconClassNames,
id?: string,
toastMessageFactory?: func,
/**
* Prevent identical toast messages from displaying.
*/
preventDuplicates?: boolean,
/**
* Display new toast messages at the top or bottom of the queue.
*/
newestOnTop?: boolean,
onClick?: func,
}

/**
* A React container for displaying a list of toast messages.
* It mimics the APIs with the vanilla [toastr.js](https://github.com/CodeSeven/toastr)
* by retaining a [ref][react-ref] to publish a new **toast**.
*
* To display HTML, simply pass JSX instead of strings for title and message.
*
* ```javascript
* this.container.success(
* <strong>I am a strong title</strong>,
* <em>I am an emphasized message</em>
* });
* ```
*
* If you're using Redux for managing states, you might consider using the
* underlying component directly. See [ToastMessageAnimated](#toastmessage) below
*/
export class ToastContainer extends React.PureComponent<Props> {
static defaultProps = {
toastType: {
error: "error",
info: "info",
success: "success",
warning: "warning",
},
id: "toast-container",
toastMessageFactory: React.createFactory(ToastMessageAnimated),
preventDuplicates: true,
newestOnTop: true,
onClick: _.noop,
}

state = {
toastList: [],
}

toastMessageRefs = {}

/**
*
* @param {any} message
* @param {any} title
* @param {any} optionsOverride
* @public
*/
error(message, title, optionsOverride) {
this.handleNotify(
this.props.toastType.error,
message,
title,
optionsOverride
)
}

/**
*
* @param {any} message
* @param {any} title
* @param {any} optionsOverride
* @public
*/
info(message, title, optionsOverride) {
this.handleNotify(
this.props.toastType.info,
message,
title,
optionsOverride
)
}

/**
*
* @param {any} message
* @param {any} title
* @param {any} optionsOverride
* @public
*/
success(message, title, optionsOverride) {
this.handleNotify(
this.props.toastType.success,
message,
title,
optionsOverride
)
}

/**
*
* @param {any} message
* @param {any} title
* @param {any} optionsOverride
* @public
*/
warning(message, title, optionsOverride) {
this.handleNotify(
this.props.toastType.warning,
message,
title,
optionsOverride
)
}

/**
*
* @public
*/
clear() {
_.forEach(this.toastMessageRefs, ref => {
if (_.isObject(ref)) {
ref.handleHide()
}
})
}

handleNotify(type, message, title, optionsOverride = {}) {
if (
this.props.preventDuplicates &&
_.includes(this.state.toastList, { message })
) {
return
}
const key = _.uniqueId("toast_")
const nextToast = {
...optionsOverride,
key,
type,
title,
message,
ref: ref => {
this.toastMessageRefs[key] = ref
},
onClick: event => {
if (_.isFunction(optionsOverride.handleOnClick)) {
optionsOverride.handleOnClick()
}
this.handleOnToastClick(event)
},
onRemove: _.bind(this.handleOnToastRemove, this, key),
}
this.setState(state => ({
toastList: this.props.newestOnTop
? [nextToast, ...state.toastList]
: [...state.toastList, nextToast],
}))
}

handleOnToastClick = event => {
this.props.onClick(event)
if (event.defaultPrevented) {
return
}
event.preventDefault()
event.stopPropagation()
}

handleOnToastRemove = key => {
this.setState(state => ({
toastList: _.reject(state.toastList, { key }),
}))
}

render() {
const restProps = _.omit(this.props, _.keys(ToastContainer.defaultProps))
return (
<div {...restProps} id={this.props.id} aria-live="polite" role="alert">
{this.state.toastList.map(it => this.props.toastMessageFactory(it))}
</div>
)
}
}

export default ToastContainer
60 changes: 60 additions & 0 deletions src/components/ToastContainer.md
@@ -0,0 +1,60 @@
### Usage

```jsx static
import { ToastContainer } from "react-toastr";

```

### DEMO App


```jsx
require("./demo.css");
let container;

<div className="container">
<ToastContainer
ref={ref => container = ref}
className="toast-top-right"
/>
<h1>
React-Toastr
<small>React.js toastr component</small>
</h1>
<div className="btn-container">
<button
className="primary"
onClick={() =>
container.success(`hi! Now is ${new Date()}`, `///title\\\\\\`, {
closeButton: true,
})
}
>
Hello
</button>
<button className="primary" onClick={() => container.clear()}>
CLEAR
</button>
</div>
<div className="github-button-container">
<iframe
title="Hello"
src="https://ghbtns.com/github-btn.html?user=tomchentw&amp;repo=react-toastr&amp;type=watch&amp;count=true"
allowTransparency="true"
frameBorder="0"
scrolling="0"
width="90"
height="20"
/>
<iframe
title="CLEAR"
src="https://ghbtns.com/github-btn.html?user=tomchentw&amp;repo=react-toastr&amp;type=fork&amp;count=true"
allowTransparency="true"
frameBorder="0"
scrolling="0"
width="90"
height="20"
/>
</div>
</div>
```
40 changes: 40 additions & 0 deletions src/components/__tests__/ToastContainer.test.js
@@ -0,0 +1,40 @@
import React from "react"
import ReactDOM from "react-dom"
import ReactTestRenderer from "react-test-renderer"

describe(`ToastContainer module`, () => {
const { ToastContainer } = require("../ToastContainer")

describe(`renders a toast message`, () => {
it(`exists in the container`, () => {
const renderer = ReactTestRenderer.create(<ToastContainer />)
expect(renderer).toMatchSnapshot()

renderer.getInstance().success(`yeah,`, `cool`)
expect(renderer).toMatchSnapshot()
})

it(`should be closed by clicking on it`, done => {
const renderer = ReactTestRenderer.create(<ToastContainer />)
renderer.getInstance().success(`yeah,`, `cool`, { hideAnimation: null })
expect(renderer).toMatchSnapshot()

renderer.toJSON().children[0].props.onClick({ defaultPrevented: true })

setTimeout(() => {
expect(renderer).toMatchSnapshot()
done()
}, 100)
})
})

describe(`when component function is triggered multiple times`, () => {
it(`renders a list of toast messages`, () => {
const renderer = ReactTestRenderer.create(<ToastContainer />)
renderer.getInstance().success(`yeah`, `cool`)
renderer.getInstance().error(`blabla`, `foobar`)

expect(renderer).toMatchSnapshot()
})
})
})

0 comments on commit a3cd015

Please sign in to comment.