跳转到内容

更多例子

自定义弹窗

openify 支持任意类型可打开的组件,只要它能实现OpenParams中的open,onClose,onUnmount即可

查看代码
tsx
import { Button, Form, Input, Modal, message } from "antd";
import { type OpenParams, Slot, openify } from "openify";
import React, { useEffect, useState } from "react";
import { slotId, wait } from "./utils";

type CustomModalProps = {
    open?: boolean;
    id: string;
    onOk?: (id: string) => void;
    onCancel?: () => void;
    afterClose?: () => void;
};

const CustomModal = ({
    open,
    id,
    onOk,
    onCancel,
    afterClose,
}: CustomModalProps) => {
    const [form] = Form.useForm();
    const [loading, setLoading] = useState(false);
    // biome-ignore lint/correctness/useExhaustiveDependencies(form.setFieldValue): <explanation>
    // biome-ignore lint/correctness/useExhaustiveDependencies(id): <explanation>
    useEffect(() => {
        if (open) {
            form.setFieldValue("id", id);
        }
    }, [open]);
    return (
        <Modal
            title="自定义弹窗"
            open={open}
            confirmLoading={loading}
            onOk={async () => {
                const nextId = form.getFieldValue("id");
                setLoading(true);
                await wait(1000);
                setLoading(false);
                onOk?.(nextId);
            }}
            onCancel={onCancel}
            afterClose={afterClose}
        >
            <Form form={form}>
                <Form.Item label="ID" name="id">
                    <Input />
                </Form.Item>
            </Form>
        </Modal>
    );
};

type OpenableCustomModalParams = OpenParams<string | null> &
    Omit<CustomModalProps, "open" | "onOk" | "onCancel" | "afterClose">;

const openableCustomModal = openify<OpenableCustomModalParams>(
    ({ open, onClose, onUnmount, ...rest }) => (
        <CustomModal
            {...rest}
            open={open}
            onOk={(taskId) => onClose(taskId)}
            onCancel={() => onClose(null)}
            afterClose={onUnmount}
        />
    ),
);

export default () => {
    const [form] = Form.useForm();
    return (
        <>
            <Form form={form}>
                <Form.Item label="ID" name="id">
                    <Input />
                </Form.Item>
            </Form>
            <Button
                onClick={async () => {
                    const id = form.getFieldValue("id");
                    const nextId = await Slot.getById(slotId).open(
                        openableCustomModal,
                        { id },
                    );
                    if (nextId !== null) {
                        message.success(`生成新ID: ${nextId}`);
                        form.setFieldValue("id", nextId);
                    } else {
                        message.warning("弹窗取消");
                    }
                }}
            >
                自定义弹窗
            </Button>
        </>
    );
};

错误捕获

弹窗异常崩溃时,不会影响主应用,并且会reject对应错误至open生成的Promise

查看代码
tsx
import { Button, Modal, message } from "antd";
import { type OpenParams, Slot, openify } from "openify";
import React, { useState } from "react";
import { slotId } from "./utils";

type CatchErrorProps = {
    open?: boolean;
    onClose?: () => void;
    afterClose?: () => void;
};

const CatchError = ({ open, onClose, afterClose }: CatchErrorProps) => {
    const [hasError, setHasError] = useState(false);
    if (hasError) {
        throw new Error("模拟错误");
    }
    return (
        <Modal
            title="错误捕获"
            open={open}
            onOk={onClose}
            onCancel={onClose}
            afterClose={afterClose}
        >
            <Button onClick={() => setHasError(true)}>触发异常</Button>
        </Modal>
    );
};

type OpenableCatchErrorParams = OpenParams<string | null> &
    Omit<CatchErrorProps, "open" | "onOk" | "onCancel" | "afterClose">;

const openableCatchError = openify<OpenableCatchErrorParams>(
    ({ open, onClose, onUnmount, ...rest }) => (
        <CatchError
            {...rest}
            open={open}
            onClose={onClose}
            afterClose={onUnmount}
        />
    ),
);

export default () => {
    return (
        <Button
            onClick={async () => {
                try {
                    await Slot.getById(slotId).open(openableCatchError);
                    message.success("弹窗正确关闭");
                } catch (e) {
                    console.error(e);
                    message.error("弹窗异常退出");
                }
            }}
        >
            异常弹窗
        </Button>
    );
};

获取Context

Slot放在合适的位置,就能拿到对应位置的Context

查看代码
tsx
import { Button, Modal, Space } from "antd";
import { type OpenParams, Slot, openify } from "openify";
import React, { createContext, useContext } from "react";
import { slotId } from "./utils";

const MyContext = createContext("默认Context");

type GetContextProps = {
    open?: boolean;
    onClose?: () => void;
    afterClose?: () => void;
};

const GetContext = ({ open, onClose, afterClose }: GetContextProps) => {
    const contextValue = useContext(MyContext);
    return (
        <Modal
            title="获取Context"
            open={open}
            onOk={onClose}
            onCancel={onClose}
            afterClose={afterClose}
        >
            {`当前Context值: ${contextValue}`}
        </Modal>
    );
};

type OpenableGetContextParams = OpenParams<string | null> &
    Omit<GetContextProps, "open" | "onOk" | "onCancel" | "afterClose">;

const openableGetContext = openify<OpenableGetContextParams>(
    ({ open, onClose, onUnmount, ...rest }) => (
        <GetContext
            {...rest}
            open={open}
            onClose={onClose}
            afterClose={onUnmount}
        />
    ),
);

export default () => {
    return (
        <MyContext.Provider value="自定义Context">
            <Space size={16}>
                <Button
                    onClick={async () => {
                        await Slot.getById(slotId).open(openableGetContext);
                    }}
                >
                    默认Context
                </Button>
                <Button
                    onClick={async () => {
                        await Slot.getById("in-context").open(
                            openableGetContext,
                        );
                    }}
                >
                    自定义Context
                </Button>
            </Space>
            <Slot id="in-context" />
        </MyContext.Provider>
    );
};

外部关闭

open返回的是一个带cancelPromise,可以手动调用cancel关闭弹窗,传入参数同onClose

查看代码
tsx
import { Button, Modal, message } from "antd";
import { type OpenParams, Slot, openify } from "openify";
import React from "react";
import { slotId } from "./utils";

type CloseOutsideProps = {
    open?: boolean;
    onClose?: (reaseon: string) => void;
    afterClose?: () => void;
};

const CloseOutside = ({ open, onClose, afterClose }: CloseOutsideProps) => {
    return (
        <Modal
            title="外部关闭"
            open={open}
            onOk={() => onClose?.("内部确定")}
            onCancel={() => onClose?.("内部取消")}
            afterClose={afterClose}
        />
    );
};

type OpenableCloseOutsideParams = OpenParams<string> &
    Omit<CloseOutsideProps, "open" | "onClose" | "afterClose">;

const openableCloseOutside = openify<OpenableCloseOutsideParams>(
    ({ open, onClose, onUnmount, ...rest }) => (
        <CloseOutside
            {...rest}
            open={open}
            onClose={onClose}
            afterClose={onUnmount}
        />
    ),
);

export default () => {
    return (
        <Button
            onClick={async () => {
                const res = Slot.getById(slotId).open(openableCloseOutside);
                setTimeout(() => {
                    res.cancel("外部关闭");
                }, 5000);
                const reason = await res;
                message.info(`关闭原因: ${reason}`);
            }}
        >
            打开弹窗
        </Button>
    );
};

渲染隔离

Slot外部组件和内部组件之间的渲染是独立的,自身更新不会带来另一方的非必要渲染

注意

ContextStore之类的更新依然会生效

查看代码
tsx
import { Button, Modal, Space, message } from "antd";
import { type OpenParams, Slot, openify } from "openify";
import React, { useEffect, useRef, useState } from "react";
import { slotId } from "./utils";

type RenderSelfProps = {
    open?: boolean;
    onClose?: () => void;
    afterClose?: () => void;
};

const RenderSelf = ({ open, onClose, afterClose }: RenderSelfProps) => {
    const [flag, update] = useState({});
    const initRef = useRef(false);
    // biome-ignore lint/correctness/useExhaustiveDependencies(flag): <explanation>
    useEffect(() => {
        if (initRef.current) {
            message.info("弹窗渲染");
        } else {
            initRef.current = true;
        }
    }, [flag]);
    const [timeId, setTimeId] = useState<number | null>(null);
    useEffect(() => {
        return () => {
            if (timeId) {
                clearTimeout(timeId);
            }
        };
    }, [timeId]);
    return (
        <Modal
            title="渲染隔离"
            open={open}
            onOk={onClose}
            onCancel={onClose}
            afterClose={afterClose}
        >
            <Button
                onClick={() => {
                    if (timeId === null) {
                        setTimeId(
                            window.setInterval(() => {
                                update({});
                            }, 1000),
                        );
                    } else {
                        setTimeId(null);
                    }
                }}
            >
                {timeId === null ? "开始刷新" : "停止刷新"}
            </Button>
        </Modal>
    );
};

type OpenableRenderSelfParams = OpenParams<void> &
    Omit<RenderSelfProps, "open" | "onClose" | "afterClose">;

const openableRenderSelf = openify<OpenableRenderSelfParams>(
    ({ open, onClose, onUnmount, ...rest }) => (
        <RenderSelf
            {...rest}
            open={open}
            onClose={onClose}
            afterClose={onUnmount}
        />
    ),
);

export default () => {
    const [flag, update] = useState({});
    const initRef = useRef(false);
    // biome-ignore lint/correctness/useExhaustiveDependencies(flag): <explanation>
    useEffect(() => {
        if (initRef.current) {
            message.info("App渲染");
        } else {
            initRef.current = true;
        }
    }, [flag]);
    const [timeId, setTimeId] = useState<number | null>(null);
    useEffect(() => {
        return () => {
            if (timeId) {
                clearTimeout(timeId);
            }
        };
    }, [timeId]);
    return (
        <Space size={12}>
            <Button
                onClick={() => {
                    if (timeId === null) {
                        setTimeId(
                            window.setInterval(() => {
                                update({});
                            }, 1000),
                        );
                    } else {
                        setTimeId(null);
                    }
                }}
            >
                {timeId === null ? "开始刷新" : "停止刷新"}
            </Button>
            <Button
                onClick={async () => {
                    await Slot.getById(slotId).open(openableRenderSelf);
                }}
            >
                打开弹窗
            </Button>
        </Space>
    );
};