前言
本文基于 nextUI 和 tailwind CSS 实现 API 调用式 Modal
组件的封装。
组件的设计与实现
首先,介绍一下 Modal
组件的目录结构:
创建上下文对象
如果想了解 Context 更多内容,可以阅读 React Context 官方文档。
根据自身需求,创建 ModalContext
上下文对象:
// Context.tsx
import { createContext, useContext } from 'react'
import type { ButtonProps, ModalProps as _ModalProps } from '@nextui-org/react'
export type ModalFooterParams = {
ConfirmButton: (props: ButtonProps) => React.ReactNode
CancelButton: (props: ButtonProps) => React.ReactNode
onClose: () => void
}
export type ModalFooterRenderer = (params: ModalFooterParams) => React.ReactNode
export type ModalType = 'default' | 'primary' | 'success' | 'warning' | 'danger'
export type ModalProps = {
/** 模态框类型 */
type?: ModalType
/** 模态框标题 */
title?: React.ReactNode
/** 模态框内容 */
content?: React.ReactNode
/** 自定义底部内容 */
footer?: ModalFooterRenderer | React.ReactNode
/** 关闭回调 */
onClose?: () => void
/** 确认按钮文本 */
confirmButtonText?: string
/** 确认按钮属性 */
confirmButtonProps?: ButtonProps
/** 确认按钮图标 */
confirmButtonIcon?: React.ReactNode
/** 确认回调 */
onConfirm?: () => void
/** 确认前的校验回调 */
beforeConfirm?: () => boolean
/** 确认按钮加载状态 */
isConfirmLoading?: boolean
/** 取消按钮文本 */
cancelButtonText?: string
/** 取消按钮属性 */
cancelButtonProps?: ButtonProps
/** 取消按钮图标 */
cancelButtonIcon?: React.ReactNode
/** 取消回调 */
onCancel?: () => void
} & Partial<Omit<_ModalProps, 'title' | 'content'>>
export const ModalContext = createContext<ModalProps>({} as ModalProps)
export const useModalContext = () => {
return useContext(ModalContext)
}
实现 ModalFooter 组件
实现一个 COLOR_MAP
对象,以定义按钮类型及其对应的主题色。
接下来,将 ConfirmButton
、CancelButton
和 ModalFooter
组件的事件系统进行统一集成,通过事件代理机制确保 onConfirm
和 onCancel
自定义事件的正常触发。
// Footer.tsx
import { useState } from 'react'
import {
Button,
ButtonProps,
ModalFooter as _ModalFooter,
useModalContext as _useModalContext,
} from '@nextui-org/react'
import { useModalContext } from './Context'
export const COLOR_MAP = {
default: 'bg-black dark:text-black dark:bg-white',
primary: 'bg-violet-500/80',
success: 'bg-green-600/80',
warning: 'bg-amber-500/80',
danger: 'bg-red-500/80',
}
export function ConfirmButton(props: ButtonProps) {
const { onClose: _onClose } = _useModalContext()
const {
type = 'default',
confirmButtonText = '确认',
confirmButtonProps,
confirmButtonIcon,
onClose,
onConfirm,
beforeConfirm,
isConfirmLoading,
} = useModalContext()
const [isLoading, setIsLoading] = useState(false)
const onClick = async () => {
if (confirmButtonProps?.type === 'submit') {
return
}
setIsLoading(!!isConfirmLoading)
try {
const isConfirm = await beforeConfirm?.()
if (isConfirm !== false) {
await onConfirm?.()
onClose?.()
_onClose()
}
} finally {
setIsLoading(false)
}
}
return (
<Button
color={type}
className={`w-max py-2 px-8 rounded-lg flex items-center gap-2 text-white border-slate-400 ${COLOR_MAP[type]}`}
startContent={confirmButtonIcon}
isLoading={isLoading}
onClick={onClick}
{...props}
{...confirmButtonProps}>
{confirmButtonText}
</Button>
)
}
export function CancelButton(props: ButtonProps) {
const { onClose: _onClose } = _useModalContext()
const {
cancelButtonText = '取消',
cancelButtonProps,
cancelButtonIcon,
onCancel,
onClose,
} = useModalContext()
const onClick = () => {
onCancel?.()
onClose?.()
_onClose()
}
return (
<Button
className='rounded-lg bg-transparent border border-gray-400/70 text-gray-400/70 dark:text-white dark:border-white hover:bg-default-100'
startContent={cancelButtonIcon}
onClick={onClick}
{...props}
{...cancelButtonProps}>
{cancelButtonText}
</Button>
)
}
export function ModalFooter() {
const { onClose } = _useModalContext()
const { footer } = useModalContext()
const defaultFooter = (
<div className='flex items-center gap-2'>
<CancelButton />
<ConfirmButton />
</div>
)
if (typeof footer === 'function') {
return <_ModalFooter>{footer({ ConfirmButton, CancelButton, onClose })}</_ModalFooter>
}
return footer !== null ? (
<_ModalFooter>{footer || defaultFooter}</_ModalFooter>
) : null
}
实现 Modal 组件
Modal
组件通过 ModalContext
集中管理属性和状态。另外,此处对 Modal
组件的关闭逻辑进行了整合,保证 onCancel
和 onClose
回调函数的依次触发。
Modal
组件的渲染逻辑如下:
// index.tsx
import {
Modal as _Modal,
ModalBody,
ModalHeader,
ModalContent,
ModalFooter as _ModalFooter,
useModalContext as _useModalContext,
} from '@nextui-org/react'
import { ModalContext, type ModalProps } from './Context'
import { ModalFooter } from './Footer'
import { withType } from './modal'
export function Modal(props: ModalProps) {
const {
title,
content,
footer,
onClose: _onClose,
children,
confirmButtonText,
confirmButtonProps,
onConfirm,
beforeConfirm,
isConfirmLoading,
cancelButtonText,
cancelButtonProps,
onCancel,
...restProps
} = props
function onClose() {
onCancel?.()
_onClose?.()
}
return (
<ModalContext.Provider value={props}>
<_Modal disableAnimation className='m-auto' onClose={onClose} {...restProps}>
<ModalContent>
<ModalHeader>{title}</ModalHeader>
<ModalBody className='py-0 text-sm'>
{content || children}
</ModalBody>
<ModalFooter />
</ModalContent>
</_Modal>
</ModalContext.Provider>
)
}
Modal.default = withType('default')
Modal.primary = withType('primary')
Modal.success = withType('success')
Modal.warning = withType('warning')
Modal.danger = withType('danger')
上述代码中,通过向 withType
函数传入 type
参数,即可创建对应类型的 Modal
。
接下来,对 Modal
组件的创建、销毁和渲染不同类型 Modal
的方法进行封装:
// modal.tsx
import { isValidElement } from 'react'
import { createRoot } from 'react-dom/client'
import { Modal } from './index'
import type { ModalProps, ModalType } from './Context'
function modal(config: ModalProps) {
const currentConfig = {
...config,
isOpen: true,
onClose,
}
const container = document.createDocumentFragment()
const root = createRoot(container)
function render(config: ModalProps) {
root.render(<Modal {...config} />)
}
function onClose() {
render({
...currentConfig,
isOpen: false,
})
setTimeout(function () {
root.unmount()
}, 300)
}
render(currentConfig)
return {
onClose,
}
}
export function withType(type: ModalType) {
function _withType(
content: React.ReactNode | ModalProps,
config?: Omit<ModalProps, 'content'>
): Promise<void> {
// 判断传入的 content 是否为 ReactNode 或 string
// 若是,直接视作 content 处理,否则视为 config 对象处理
const _config = isValidElement(content) || typeof content === 'string'
? { ...config, content }
: content as ModalProps
return new Promise((resolve, reject) => {
const onConfirm = async () => {
await _config.onConfirm?.()
resolve()
}
const onCancel = async () => {
await _config.onCancel?.()
reject()
}
modal({
..._config,
onConfirm,
onCancel,
type,
})
})
}
return _withType
}
组件的使用方式
传入文本内容
定义 handleClick
函数,随后在函数内部调用 Modal
组件,传入文本内容:
import { type ModalType } from '@/components/Modal/Context'
import { COLOR_MAP } from '@/components/Modal/Footer'
import { Modal } from '@/components/Modal'
import { Button } from '@nextui-org/react'
export default function Home() {
const colorList: ModalType[] = ['default', 'primary', 'success', 'warning', 'danger']
const handleClick = (type: ModalType) => {
Modal[type](`${type} 操作成功!`)
}
return (
<div className='py-12 flex gap-2 items-center justify-center'>
{colorList.map((color) => (
<Button
key={color}
color={color}
className={`rounded-md text-white ${COLOR_MAP[color]}`}
onClick={() => handleClick(color)}
>
{color} Click
</Button>
))}
</div>
)
}
const handleClick = (type: ModalType) => {
Modal[type](`${type} 操作成功!`)
}
传入配置对象
对 handleClick
函数中 Modal
组件的调用形式进行修改,传入一个配置对象:
const handleClick = (type: ModalType) => {
Modal[type]({
title: '提示',
content: `${type} 操作成功!`,
confirmButtonText: '收到',
cancelButtonText: '不听',
})
}
传入文本内容和配置对象
对 handleClick
函数中 Modal
组件的调用形式进行修改,同时传入文本内容和配置对象:
const handleClick = (type: ModalType) => {
Modal[type](`${type} 操作成功!`, {
title: '提示',
confirmButtonText: '我知道了',
cancelButtonText: '取消',
})
}
最后
欢迎大家提出不足之处或其他建议,一起进步。