前言
在我的个人博客前台的项目中,自己封装了一些组件。为了能让自己更加熟悉开发组件的流程和思想,决定打算记录下来,同时也可以分享给大家。
分析组件的结构
一个 Modal 组件大概分为三个部分,分别是 header、body 和 footer。其中 header 和footer 是固定在上面和下面的。body 中的部分是 children。结构图如下:
设计组件的 Props
从上图来看 Modal 组件大概有 title、visible、children、onOk、onCancel 等属性。title 可以控制组件的标题,visble 控制组件的显示与隐藏,children 放入组件的 body 部分,onOk 是处理确认按钮的回调函数,onCancel 是隐藏组件的回调函数。代码如下:
interface IModalProps {
visible: boolean;
title: string;
okText: string;
cancelText: string;
onCancel: () => void;
onOk: () => void;
footer: boolean;
}
okText 、cancelText是可以控制确认和取消按钮的文案。footer 可以控制组件是否显示底部的部分。
使用 createPortal
将组件渲染到其他结点中 - createPortal 是 ReactDOM 的一个方法 - createPortal 可以将子节点渲染到 DOM 节点中的方式,该节点存在于 DOM 组件的层次结构之外。 - 因此 createPortal 适合脱离文档流的组件,特别是 position: absolute 与 position: fixed的组件。比如模态框,通知,警告等。 我们做的 Modal 组件正是需要这样的特性,再也不用担心组件会被其他组件的样式影响啦~
interface IProps { ... }
const Modal: FC<IProps> = ({ visible, children, title, okText, cancelText, onCancel, onOk, footer = true, }) => {
const modalEleRef = useRef<HTMLElement | null>(null);
useEffect(() => {
const node = document.getElementById("W-Modal");
if (node) {
modalEleRef.current = node;
} else {
modalEleRef.current = document.createElement("div");
modalEleRef.current.setAttribute("id", "W-Modal");
document.body.appendChild(modalEleRef.current);
}
}, []);
if (!modalEleRef.current) return null;
return createPortal(
visible &&
<div className="fixed w-full h-full top-0 backdrop-blur-sm z-10 bg-black/30" onClick={(e) => { onCancel?.(); }} >
<div onClick={(e) => e.stopPropagation()} className={`z-50 m-auto h-full md:w-600 md:h-auto md:mt-32 bg-box-dark md:rounded-xl md:border md:border-gray-400/40 p-5 md:hover:border-blue-600 md:hover:shadow-xl md:hover:shadow-blue-600/10 transition-all duration-300`} >
{/* header */}
<div className="flex justify-between mb-3">
<h3 className=" text-xl font-bold text-blue-600"> {title || "标题"} </h3>
<CloseOutlined onClick={onCancel} className="text-lg font-extrabold hover:text-blue-600 transition-all duration-300 cursor-pointer" />
</div>
{/* body */}
<div className="mt-2 mb-3">
{children}
</div>
{/* footer */}
{footer &&
(<div className="mt-2 flex justify-end">
<div className="mr-4 text-base cursor-pointer px-3 py-1 rounded-md hover:bg-gray-800/30 hover:text-red-400 transition-all duration-300" onClick={onCancel} >
{cancelText || "取消"}
</div>
<div className="text-base cursor-pointer px-3 py-1 rounded-md hover:bg-gray-800/30 hover:text-blue-600 transition-all duration-300" onClick={onOk} >
{okText || "确定"}
</div>
</div>
)}
</div>
</div>,
modalEleRef.current );
};
组件就这样完成啦,非常简单!样式使用的是 Tailwind CSS。一开始使用这个样式库我觉得有点鸡肋,后面用着用着还挺香的😋需要注意的是在组件挂载完时,需要去判断是否有存放 Modal 组件的结点,没有的话则去创建一个结点,如果有则去获取该结点。
添加动画
虽然组件的功能已经完成,但是组件缺少动画效果,在交互上会显得很生硬。所以接下来我们给组件添加动画效果。将采用 react-transition-group 这个动画库去实现。 下面是 Fade 动画组件代码:
interface IProps {
visible?: boolean;
duration?: number;
style?: CSSProperties;
onEntered?: () => void;
onExited?: () => void;
children: React.ReactElement;
}
const Fade: FC<IProps> = ({
visible,
duration,
children,
onEntered,
onExited,
style
}) => (
<div className="component-transition-fade">
<CSSTransition
in={visible}
timeout={duration ?? 300}
classNames="fade"
mountOnEnter
unmountOnExit
onEntered={onEntered}
onExited={onExited}
>
{React.cloneElement(children, { style: { ...style } })}{' '}
</CSSTransition>{' '}
</div>
);
样式代码:
:root {
--duration-base: 0.3s;
--timing-function-enter: ease-out;
--timing-function-leave: ease-in;
}
@keyframes fade-in {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
@keyframes fade-out {
from {
opacity: 1;
}
to {
opacity: 0;
}
}
.component-transition-fade {
width: 100%;
height: 100%;
}
.component-transition-fade .fade-enter-active{
animation: var(--duration-base) fade-in both var(--timing-function-enter);
}
.component-transition-fade .fade-exit-active{
animation: var(--duration-base) fade-out both var(--timing-function-leave);
}
现在只需要给组件嵌套在该 Fade 动画组件里就好了:
const Modal: FC<IProps> = ({...}) => {
...
return createPortal(
<Fade visible={visible}>
...原来组件的代码
</Fade>,
modalEleRef.current
)
}
最终效果如下图:
引用
动画库官方文档地址:reactcommunity.org/react-trans…