From 0ca08653868d9eb3ae970db8c8977e7c1110e513 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=91=A8=E6=98=9F=E6=98=9F=E5=90=8C=E5=AD=A6?= Date: Mon, 28 Aug 2023 17:17:55 +0800 Subject: [PATCH] feat: add new hook useModal --- config/hooks.ts | 1 + packages/hooks/src/index.ts | 6 + .../src/useModal/__tests__/index.test.tsx | 67 ++++++++ packages/hooks/src/useModal/demo/MyModal.tsx | 40 +++++ packages/hooks/src/useModal/demo/demo.tsx | 11 ++ packages/hooks/src/useModal/demo/demo1.tsx | 36 +++++ packages/hooks/src/useModal/index.en-US.md | 63 ++++++++ packages/hooks/src/useModal/index.tsx | 153 ++++++++++++++++++ packages/hooks/src/useModal/index.zh-CN.md | 63 ++++++++ 9 files changed, 440 insertions(+) create mode 100644 packages/hooks/src/useModal/__tests__/index.test.tsx create mode 100644 packages/hooks/src/useModal/demo/MyModal.tsx create mode 100644 packages/hooks/src/useModal/demo/demo.tsx create mode 100644 packages/hooks/src/useModal/demo/demo1.tsx create mode 100644 packages/hooks/src/useModal/index.en-US.md create mode 100644 packages/hooks/src/useModal/index.tsx create mode 100644 packages/hooks/src/useModal/index.zh-CN.md diff --git a/config/hooks.ts b/config/hooks.ts index fa74274cf2..11cc60768e 100644 --- a/config/hooks.ts +++ b/config/hooks.ts @@ -100,6 +100,7 @@ export const menus = [ 'useScroll', 'useSize', 'useFocusWithin', + 'useModal', ], }, { diff --git a/packages/hooks/src/index.ts b/packages/hooks/src/index.ts index 58cefd9c79..e5f8f40221 100644 --- a/packages/hooks/src/index.ts +++ b/packages/hooks/src/index.ts @@ -75,6 +75,8 @@ import useVirtualList from './useVirtualList'; import useWebSocket from './useWebSocket'; import useWhyDidYouUpdate from './useWhyDidYouUpdate'; import useMutationObserver from './useMutationObserver'; +import { useModal, ModalProvider } from './useModal'; +import type { ModalProps, ModalResult } from './useModal'; export { useRequest, @@ -156,4 +158,8 @@ export { useRafTimeout, useResetState, useMutationObserver, + useModal, + ModalProvider, }; + +export type { ModalProps, ModalResult }; diff --git a/packages/hooks/src/useModal/__tests__/index.test.tsx b/packages/hooks/src/useModal/__tests__/index.test.tsx new file mode 100644 index 0000000000..6f7d5dbf1d --- /dev/null +++ b/packages/hooks/src/useModal/__tests__/index.test.tsx @@ -0,0 +1,67 @@ +import React from 'react'; +import { render, fireEvent } from '@testing-library/react'; +import { useModal, ModalProvider } from '../index'; // 请根据实际路径导入组件 + +// 模拟一个用于测试的组件 +const TestComponent = () => { + const modal = useModal(TestModalComponent); + + return ( +
+ +
+ ); +}; + +const TestModalComponent = ({ visible, hide, destroy }) => { + return ( +
+ + +
+ ); +}; + +describe('useModal and ModalProvider', () => { + it('should show and hide modal', () => { + const { getByText, getByTestId } = render( + + + , + ); + + const showModalButton = getByText('Show Modal'); + fireEvent.click(showModalButton); + + const modal = getByTestId('modal'); + expect(modal).toBeVisible(); + + const hideButton = getByTestId('hide-button'); + fireEvent.click(hideButton); + + expect(modal).not.toBeVisible(); + }); + + it('should destroy modal', () => { + const { getByText, getByTestId, queryByTestId } = render( + + + , + ); + + const showModalButton = getByText('Show Modal'); + fireEvent.click(showModalButton); + + const modal = getByTestId('modal'); + expect(modal).toBeVisible(); + + const destroyButton = getByTestId('destroy-button'); + fireEvent.click(destroyButton); + + expect(queryByTestId('modal')).toBeNull(); + }); +}); diff --git a/packages/hooks/src/useModal/demo/MyModal.tsx b/packages/hooks/src/useModal/demo/MyModal.tsx new file mode 100644 index 0000000000..c5394f8399 --- /dev/null +++ b/packages/hooks/src/useModal/demo/MyModal.tsx @@ -0,0 +1,40 @@ +import React, { useEffect } from 'react'; +import type { ModalProps } from 'ahooks'; +import { Modal, message } from 'antd'; + +interface IData { + title?: string; + desc?: string; +} + +interface IProps { + onOk: () => void; + onCancel?: () => void; +} + +export default ({ visible, hide, destroy, data = {}, props }: ModalProps) => { + const { title = '新建', desc = 'Hello World!' } = data; + const { onOk, onCancel } = props; + + useEffect(() => { + message.info('useEffect:' + title); + }, [title]); + + return ( + { + onOk?.(); + hide(); + }} + open={visible} + onCancel={() => { + onCancel?.(); + hide(); + }} + // afterClose={() => destroy()} + > + {desc} + + ); +}; diff --git a/packages/hooks/src/useModal/demo/demo.tsx b/packages/hooks/src/useModal/demo/demo.tsx new file mode 100644 index 0000000000..4c626c291e --- /dev/null +++ b/packages/hooks/src/useModal/demo/demo.tsx @@ -0,0 +1,11 @@ +import { ModalProvider } from 'ahooks'; +import React from 'react'; +import Demo1 from './demo1'; + +export default () => { + return ( + + + + ); +}; diff --git a/packages/hooks/src/useModal/demo/demo1.tsx b/packages/hooks/src/useModal/demo/demo1.tsx new file mode 100644 index 0000000000..ffa8190c74 --- /dev/null +++ b/packages/hooks/src/useModal/demo/demo1.tsx @@ -0,0 +1,36 @@ +import React from 'react'; +import { useModal } from 'ahooks'; +import { Button, Space, message } from 'antd'; +import MyModal from './MyModal'; + +export default () => { + const { show, hide, destroy } = useModal(MyModal, { + onOk: () => { + message.success('ok'); + }, + onCancel: () => { + message.error('cancel'); + }, + }); + + return ( + <> + {/* */} {/* 无需再手动注册组件 */} + + + + + + + ); +}; diff --git a/packages/hooks/src/useModal/index.en-US.md b/packages/hooks/src/useModal/index.en-US.md new file mode 100644 index 0000000000..5f108cae02 --- /dev/null +++ b/packages/hooks/src/useModal/index.en-US.md @@ -0,0 +1,63 @@ +--- +nav: + path: /hooks +--- + +# useModal + +Easily use modal/drawer and other modal components in React: + +- No need to manage internal state. +- Not tied to UI components. +- Internally uses Context to maintain context and does not lose global configuration. + +## Code Example + +### Basic Usage + + + +You need to wrap it with `ModalProvider` before using `useModal`. + +```ts +import { ModalProvider } from "ahooks"; + +ReactDOM.createRoot(document.getElementById("root")!).render( + + + + + +); +``` + +## API + +```ts +import { useModal } from 'ahooks'; +import type { ModalProps , ModalResult } from 'ahooks'; + +const Result:ModalResult = useModal((Props:ModalProps)=>{},props) +``` + +## Parameters + +### Props + +| Property | Description | Type | Default | +| -------- | -------------------------------------- | -------------------------------------- | ----------- | +| visible | Whether to show | `boolean` | `false` | +| hide | Hide | `() => void` | - | +| destroy | Destroy | `() => void` | - | +| data | Data passed in when Modal is opened | `T \| Record \| undefined` | - | +| props | Props passed in when registering Modal | `K` | `undefined` | + +> The difference between data and props is that data is passed in each time Modal is opened, and props is passed in when Modal is registered, and props will not change. + +### Result + +| Property | Description | Type | Default | +| -------- | ----------- | ------------------------------------------ | ------- | +| show | Show | `(data?: T \| Record) => void` | - | +| hide | Hide | `() => void` | - | +| destroy | Destroy | `() => void` | - | diff --git a/packages/hooks/src/useModal/index.tsx b/packages/hooks/src/useModal/index.tsx new file mode 100644 index 0000000000..fa49b60b7e --- /dev/null +++ b/packages/hooks/src/useModal/index.tsx @@ -0,0 +1,153 @@ +/* + * @LastEditTime: 2023-08-28 11:37:34 + * @Description: + * @Date: 2023-08-25 17:44:55 + * @Author: @周星星同学 + */ +import React from 'react'; +import type { FC } from 'react'; +import { createContext, useContext, useState, useMemo, useCallback } from 'react'; + +function getRandomKey(length: number) { + const allChars = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'; + let result = ''; + + while (result.length < length) { + const randomIndex = Math.floor(Math.random() * allChars.length); + result += allChars.charAt(randomIndex); + } + + return result; +} + +export interface ModalResult { + show: (v?: T) => void; + hide: () => void; + destroy: () => void; +} + +// +type IData = Record; +type IProps = undefined; + +export interface ModalProps { + visible: boolean; + hide: () => void; + destroy: () => void; + data?: T; + props: K; +} + +type IComponent = FC>; + +/** + * @description: 用于创建一个上下文 + * @param {*} + * @return {*} + */ +const Context = createContext(undefined); + +/** + * @description: 用于创建一个模态框 + * @param {FC} modal + * @return {*} + */ +export function useModal( + component: IComponent, + props?: K, +): ModalResult { + const context = useContext(Context); + const key = useMemo(() => getRandomKey(6), []); + + const show = useCallback( + (v?: T) => { + context?.show(key, component, v, props); + }, + [component, key, context, props], + ); + + const hide = useCallback(() => { + context?.hide(key); + }, [key, context]); + + const destroy = useCallback(() => { + context?.destroy(key); + }, [key, context]); + + return { + show, + hide, + destroy, + }; +} + +/** + * @description: 用于创建一个模态框 + * @param {FC} modal + * @return {*} + */ +export const ModalProvider: FC = ({ children }) => { + const [modals, setModals] = useState< + Record< + string, + { component: IComponent; data?: IData; props?: IProps; visible: boolean } + > + >({}); + + const show = ( + key: string, + component: IComponent, + data?: IData, + props?: IProps, + ) => { + setModals((prev) => { + return { + ...prev, + [key]: { component, data, props, visible: true }, + }; + }); + }; + + const hide = (key: string) => { + const modal = modals[key]; + if (!modal) return; + setModals((prev) => { + return { + ...prev, + [key]: { ...prev[key], visible: false }, + }; + }); + }; + + const destroy = (key: string) => { + setModals((prev) => { + const { [key]: _, ...rest } = prev; + return rest; + }); + }; + + return ( + + {children} + {Object.keys(modals).map((key) => { + const { component: Component, data, props, visible } = modals[key]; + return ( + hide(key)} + destroy={() => destroy(key)} + data={data} + props={props} + /> + ); + })} + + ); +}; diff --git a/packages/hooks/src/useModal/index.zh-CN.md b/packages/hooks/src/useModal/index.zh-CN.md new file mode 100644 index 0000000000..43d01e50a2 --- /dev/null +++ b/packages/hooks/src/useModal/index.zh-CN.md @@ -0,0 +1,63 @@ +--- +nav: + path: /hooks +--- + +# useModal + +在 react 中很方便的使用 modal / drawer 等模态组件: + +- 不需要维护内部状态。 +- 不绑定 UI 组件。 +- 内部使用 Context 维护上下文,不会丢失全局配置。 + +## 代码演示 + +### 基础用法 + + + +需要先用`ModalProvider`包裹才可使用 `useModal`。 + +```ts +import { ModalProvider } from "ahooks"; + +ReactDOM.createRoot(document.getElementById("root")!).render( + + + + + +); +``` + +## API + +```ts +import { useModal } from 'ahooks'; +import type { ModalProps , ModalResult } from 'ahooks'; + +const Result:ModalResult = useModal((Props:ModalProps)=>{},props) +``` + +## 参数 + +### Props + +| 参数 | 说明 | 类型 | 默认值 | +| ------- | ------------------------- | -------------------------------------- | ----------- | +| visible | 是否显示 | `boolean` | `false` | +| hide | 隐藏 | `() => void` | - | +| destroy | 销毁 | `() => void` | - | +| data | Modal 打开时传入的 data | `T \| Record \| undefined` | - | +| props | 注册 Modal 时传入的 props | `K` | `undefined` | + +> data 与 props 的区别在于,data 是每次打开 Modal 时传入的,props 是注册 Modal 时传入的,props 不会变化。 + +### Result + +| 参数 | 说明 | 类型 | 默认值 | +| ------- | ---- | ------------------------------------------ | ------ | +| show | 显示 | `(data?: T \| Record) => void` | - | +| hide | 隐藏 | `() => void` | - | +| destroy | 销毁 | `() => void` | - |