基于Typescript和Hooks写法实现一个Modal弹窗组件
捋清楚实现需求和实现思路
需求(可根据自己喜好定制)
- 弹窗的标题,主体,底部都可以自定义替换
- 遮罩层可以设置是否点击可关闭
- 点击确定或取消按钮执行回调
定义组件Props
interface ModalProps {
visible: boolean; // 组件是否可见
footer?: null | ReactElement[]; // 底部按钮,不传则显示默认按钮,亦可传入元素数组替换
maskClosable?: boolean; // 遮罩层是否可点击
okText?: string; // 确定按钮文本
onOkClick?: React.MouseEventHandler; // 点击确定按钮的回调函数
cancelText?: string; // 取消按钮文本
onCancelClick?: React.MouseEventHandler; // 点击取消按钮的回调函数
title?: string; // 弹窗标题
}
主体内容利用props.children
展示,类型上可以自己定义多一个childre?: ReactNode
,也可以利用PropsWithChildren
这个类型把ModalProps
当泛型传入,最终组件的基本定义如下
const Modal: FC<PropsWithChildren<ModalProps>> = (props) => {
const {children, visible, footer, maskClosable, okText, onOkClick, cancelText, onCancelClick, title} = props;
// ... 省略
return (<></>);
}
继续完善内容,做这个效果可以利用一个非常方便的Api
介绍一下ReactDOM.createPortal
函数第一个参数需要传入节点内容,第二个参数传入当成容器的元素,一般选在挂到document.body
上,编写如下,此时引用并展示这个组件,元素就会挂到body
上而不是当前引入的元素节点树上
const Demo = ()=>{
return ReactDOM.createPortal(<div>123</div>,document.body);
}
组件实现完整代码如下,Icon
和Button
是我写好的组件,自行替换即可
import React, {FC, MouseEventHandler, PropsWithChildren, ReactElement, useRef} from 'react';
import ReactDOM from 'react-dom';
import './modal.less';
import Icon from '../icon/icon';
import Button from '../button/button';
function classes(...names: (string | undefined)[]) {
return names.filter(Boolean).join(" ");
}
interface ModalProps {
visible: boolean;
footer?: null | ReactElement[];
maskClosable?: boolean;
okText?: string;
onOkClick?: React.MouseEventHandler;
cancelText?: string;
onCancelClick?: React.MouseEventHandler;
title?: string;
}
const Modal: FC<PropsWithChildren<ModalProps>> = (props) => {
const {children, visible, footer, maskClosable, okText, onOkClick, cancelText, onCancelClick, title} = props
const ele = useRef<HTMLDivElement>(null);
// props传入maskClosable为true,则点击遮罩后调用传入的取消事件(一般这个事件要控制visible的变化要关闭弹窗)
const onMaskClickHandler: MouseEventHandler = (e) => {
maskClosable && onCancelClickHandler(e);
};
const onCancelClickHandler: MouseEventHandler = (e) => {
if (onCancelClick) {
ele.current!.className = ele.current!.className += ` lm-modal-fade-out`
setTimeout(() => {
onCancelClick(e);
}, 200)
}
}
const onOkClickHandler: MouseEventHandler = (e) => {
ele.current!.className = ele.current!.className += ` lm-modal-fade-out`
setTimeout(() => {
onOkClick && onOkClick(e);
},)
}
const instance = visible && (<>
<div className={classes('lm-modal-mask')} onClick={onMaskClickHandler}/>
<div className={'lm-modal'} ref={ele}>
<header className={classes('lm-modal-header')}>
<div>{title ? title : "Modal"}</div>
<div className={'lm-modal-close'} onClick={onCancelClickHandler}>
<Icon name={'close'} className={'lm-modal-close-icon'}/>
</div>
</header>
<main className={classes('lm-modal-content')}>{children}</main>
{footer !== null
&& (<footer className={classes('lm-modal-footer')}>
{footer
? footer.map((eleItem, index) => React.cloneElement(eleItem, {key: index}))
: <div>
<Button onClick={onCancelClickHandler}>{cancelText ? cancelText : '取消'}</Button>
<Button style={{marginLeft: 8}} onClick={onOkClickHandler}
type={'primary'}>{okText ? okText : '确定'}</Button>
</div>}
</footer>)}
</div>
</>);
return ReactDOM.createPortal(instance, document.body);
};
export default Modal;
Modal.less
样式代码如下,不用less
的自行改成其他语言
@border-color: #f1eff0;
.lm-modal {
z-index: 2;
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
border-radius: 8px;
min-width: 400px;
min-height: 200px;
background: white;
display: flex;
flex-direction: column;
animation: fade 500ms;
&-fade-out {
animation: fade-out 300ms;
}
&-mask {
z-index: 1;
background: fade(black, 35%);
width: 100vw;
height: 100vh;
position: fixed;
left: 0;
top: 0;
}
&-header {
display: flex;
position: relative;
height: 48px;
padding: 8px 16px;
border-bottom: 1px solid @border-color;
line-height: 48px;
align-items: center;
}
&-close {
height: 48px;
width: 48px;
position: absolute;
right: 0;
top: 0;
display: flex;
justify-content: center;
align-items: center;
&:hover {
.lm-modal-close-icon {
color: #de3131;
height: 1.2em;
width: 1.2em;
}
cursor: pointer;
}
}
&-content {
flex: 1;
padding: 16px;
}
&-footer {
border-top: 1px solid @border-color;
height: 48px;
display: flex;
align-items: center;
justify-content: flex-end;
padding-right: 16px;
}
}
@keyframes fade {
0% {
opacity: 0;
transform: scale(0) translate(-50%, -50%);
}
100% {
opacity: 1;
transform: scale(100%) translate(-50%, -50%);
}
}
@keyframes fade-out {
0% {
opacity: 1;
transform: scale(100%) translate(-50%, -50%);
}
100% {
background: white;
transform: scale(0) translate(-50%, -50%);
opacity: 0;
}
}
在React
组件中使用Modal
组件,主要在于一个布尔值来控制弹窗是否显示,在点击确定和关闭事件中把布尔值设置为false
const App = () => {
const [visible, setVisible] = useState(false);
return (
<>
<Modal visible={visible} maskClosable onCancelClick={() => setVisible(false)} onOkClick={() => setVisible(false)}>
<div>123</div>
</Modal>
<Button onClick={() => setVisible(true)}>Click!</Button>
</>
);
};
最后的演示效果
实现挺简单的,主要是搞清楚思路以及细节点即可,有些api
没有演示,按照类型自行测试即可
完:)