Toast
可以提供使用者操作的反馈讯息。包含一般资讯、操作成功、操作失败、警告讯息等。预设为在顶部置中显示并自动消失,是一种不打断用户操作的轻量级提示方式。
我们来参考一下 Antd 的 message 元件,这个元件很有意思,我还蛮喜欢的。
他跟我们其他元件需要写 JSX 在画面上不一样,他是直接执行一个 function 来显示 toast,如下:
import { message, Button } from 'antd';
const info = () => {
message.info('This is a normal message');
};
ReactDOM.render(
<Button type="primary" onClick={info}>
Display normal message
</Button>,
mountNode,
);
这个 message.info()
只要被执行一次,toast 就会在画面上跳出来一次。
另外,我也参考了 React-Toastify 这个 npm 套件,这个也是我过去在专案中有接触过的套件,我们来看他的使用方法:
import React from 'react';
import { ToastContainer, toast } from 'react-toastify';
import 'react-toastify/dist/ReactToastify.css';
function App(){
const notify = () => toast("Wow so easy!");
return (
<div>
<button onClick={notify}>Notify!</button>
<ToastContainer />
</div>
);
}
虽然看起来也是还蛮不错的 toast,但是我们观察看看,他要使用的时候会需要比 Antd message 多几个步骤,像是他需要放一个 <ToastContainer />
在画面上,然後需要引入他的 CSS 样式,另外也需要执行他特有的 toast
function 才能显示 toast 讯息。
所以今天我们要来挑战看看是不是能够做出 Antd 这种一拿就能够用的 toast。
我们多按几次来观察一下他的行为,首先,我们发现他跟其他提示元件一样都是把元件画到比较外层的 DOM 结构上,大概是如下的结构:
<html>
<header>...</header>
<body>
<div id="root">...</div>
<div class="ant-message">
<div>...</div>
</div>
</body>
</html>
所以这个就是我们本次的目标,我们要想办法把 toast 画在比较上层的节点上,并且是用呼叫一个 function 的方式来做。只要能够做到这一件事,其他的部分感觉起来就不难了。
然後我想像我未来会这样使用这个 toast,因为提示讯息有分种类,分别有 操作成功
、一般通知讯息
、警告讯息
、错误讯息
。
然後讯息也会有它的内容,甚至有时候我们想要控制这个讯息显示的持续时间。
一个简单的 Toast 应该具备上述几个参数就足够了。再更进阶一点,我们有看到 React-toastify
有很多的参数控制项,我们来偷看一下:
toast('? Wow so easy!', {
position: "bottom-center",
autoClose: 5000,
hideProgressBar: false,
closeOnClick: true,
pauseOnHover: true,
draggable: true,
progress: undefined,
});
以上述的参数来看,甚至他可以决定 Toast 的出现位置,从视窗的左上、右上、左下、右下...等等各种位置来出现,还有是不是能够手动关掉 Toast,因为可能有时候 Toast 遮挡住画面上我们正在浏览的讯息。
因此我们今天的目标,至少先做出一个简单版的 Toast,其他的部分,可以依照自己的需求再修改或添加。
属性 | 说明 | 类型 | 默认值 |
---|---|---|---|
type | 提示讯息种类 | success , info , warn , error |
|
content | 提示讯息内容 | ReactElement , string |
|
duration | 提示讯息展示时间 | 3000ms |
假设我今天要跳出一个讯息,我希望我的介面是长这样:
message.success({ type: 'success', content: '新增成功' });
我们知道,success 是一个 function,这个 function 会帮我们把画面画出来。
在 React 当中,有一个方法可以帮助我们做到这件事,就是 ReactDOM.render()
:
ReactDOM.render(element, container[, callback])
所以简单来说,我们要用 ReactDOM.render()
这个方法,想办法让我们的 Toast 变成这样:
<html>
<header>...</header>
<body>
<div id="root">...</div>
<div class="toast-root">
<div class="toast-container">
<Toast />
<Toast />
<Toast />
<Toast />
<Toast />
...
</div>
</div>
</body>
</html>
以下是我的方法:
export const message = {
success: (props) => {
render(<Toast {...props} type="success" />, getContainer());
},
info: (props) => {
render(<Toast {...props} type="info" />, getContainer());
},
warn: (props) => {
render(<Toast {...props} type="warn" />, getContainer());
},
error: (props) => {
render(<Toast {...props} type="error" />, getContainer());
},
};
在 getContainer()
当中,我要想办法制造出下面这样的结构,并透过 document.body.appendChild(...);
把他塞进 body 下面,之後新增一个在 toast-container
下面的子节点当作 container 回传回来给 ReactDOM.render()
就可以了。
<div class="toast-root">
<div class="toast-container">
</div>
</div>
其中,toast-container 存在的目的,是为了要帮助我们对他的 children ,也就是 Toast 做一些排版上的布局,例如置中...等等。
const rootId = 'toast-root';
const getContainer = () => {
let toastRoot;
let toastContainer;
// 制造出 toastRoot
if (document.getElementById(rootId)) {
toastRoot = document.getElementById(rootId);
} else {
const divDOM = document.createElement('div');
divDOM.id = rootId;
document.body.appendChild(divDOM);
toastRoot = divDOM;
}
// 制造出 toastContainer,并放在 toastRoot 底下
if (toastRoot.firstChild) {
toastContainer = toastRoot.firstChild;
} else {
const divDOM = document.createElement('div');
toastRoot.appendChild(divDOM);
toastContainer = divDOM;
}
// 制造出 container,并放在 toastContainer 底下
const divDOM = document.createElement('div');
toastContainer.appendChild(divDOM);
// 调整 toastRoot 以及 toastContainer 的样式
toastRoot.style = css`
position: absolute;
top: 0px;
left: 0px;
width: 100vw;
`;
toastContainer.style = css`
position: absolute;
top: 0px;
left: 50%;
transform: translateX(-50%);
z-index: 9999;
display: flex;
flex-direction: column;
align-items: center;
`;
// 把 container 回传,成为 ReactDOM.render() 的第二个参数
return divDOM;
};
以上就是我们把 Toast 画在外面的方法了。
再来我们来看 Toast 本体:
const Toast = ({
type,
content,
duration,
}) => {
const toastRef = useRef();
const [isVisible, setIsVisible] = useState(true);
const color = getColor(type);
useEffect(() => {
setTimeout(() => {
setIsVisible(false);
}, duration);
setTimeout(() => {
const currentDOM = toastRef.current;
const parentDOM = currentDOM.parentElement;
parentDOM.parentElement.removeChild(parentDOM);
}, duration + 200);
}, [duration]);
return (
<ToastWrapper
ref={toastRef}
$isVisible={isVisible}
>
<Icon $color={color}>{iconMap[type]}</Icon>
{content}
</ToastWrapper>
);
};
这个本体也就很简单,就是一些样式的排版与呈现,需要特别说明的部分是我们透过 isVisible
这个 state 来控制 Toast 的出现与消失,同时伴随着他的动画效果:
const topIn = keyframes`
0% {
transform: translateY(-50%);
opacity: 0;
}
100% {
transform: translateY(100%);
opacity: 1;
}
`;
const topOut = keyframes`
0% {
transform: translateY(100%);
opacity: 1;
}
100% {
transform: translateY(-50%);
opacity: 0;
}
`;
const topStyle = css`
animation: ${(props) => (props.$isVisible ? topIn : topOut)} 200ms ease-in-out forwards;
`;
const ToastWrapper = styled.div`
/* ...略 */
${topStyle}
`;
当然,如果还有余裕的话,我们就能够来刻各种方向出现与消失的动画,这边先以 top
方向为例。
那在这个 Toast 当中,依照不同类型的提示讯息,我们可以给他不同的 icon:
import SuccessIcon from '@material-ui/icons/Check';
import InfoIcon from '@material-ui/icons/InfoOutlined';
import WarnIcon from '@material-ui/icons/ReportProblemOutlined';
import ErrorIcon from '@material-ui/icons/HighlightOffOutlined';
const iconMap = {
success: <SuccessIcon />,
info: <InfoIcon />,
warn: <WarnIcon />,
error: <ErrorIcon />,
};
而不同的讯息,当然也需要对应的不同颜色:
const getColor = (type) => {
if (type === 'success') {
return '#52c41a';
}
if (type === 'info') {
return '#1890ff';
}
if (type === 'warn') {
return '#faad14';
}
if (type === 'error') {
return '#d9363e';
}
return '#1890ff';
};
这样我们的简单的 Toast 就完整了:
<ToastWrapper
ref={toastRef}
$isVisible={isVisible}
>
<Icon $color={color}>{iconMap[type]}</Icon>
{content}
</ToastWrapper>
最後我们来 Demo 我们的成果:
const ToastDemo = (args) => (
<ButtonGroup>
<Button variant="outlined" onClick={() => message.success({ type: 'success', content: '新增成功' })}>Success</Button>
<Button variant="outlined" onClick={() => message.info({ type: 'info', content: 'Some information' })}>Information</Button>
<Button variant="outlined" onClick={() => message.warn({ type: 'warn', content: '服务器出了一点问题' })}>Warning</Button>
<Button variant="outlined" onClick={() => message.error({ type: 'error', content: '删除失败' })}>Error</Button>
</ButtonGroup>
);
Toast 元件原始码:
Source code
Storybook:
Toast
https://www.npmjs.com/package/antd-like-message
https://github.com/bingqichen/antd-message/blob/master/src/message/index.js
>>: Day26【Web】TCP 安全协定:SSL/TLS
上午: 人工智慧AIoT资料分析应用系统框架设计与实作 初步介绍网站,WWW的历史以及各个浏览器的演...
(ISC)² 道德准则仅适用於 (ISC)² 会员。垃圾邮件发送者的身份不明或匿名,这些垃圾邮件发送...
Youtube连结:https://bit.ly/35x3dih 这次我们将精确定位出,在整个演算...
Springboot 简介 ...
预设 先要有一个开发板,可以接各种sensor。 可以先跟电脑有实体连接,这样就有指定的port可以...