深入解读JavaScript

如何优雅的使用ant.design的Modal组件?

2021-10-28  本文已影响0人  悟C

一个数据列表少不了数据的增、删、改,一般我们都是通过Modal+Form实现表单数据的获取,在一个复杂的数据列表中我们需要好多个Modal+Form的配合,这时候我们需要维护好多个Modal的visible状态,例如下面的场景:

const List = () => {
  const [visible1, setVisible1] = useState(false);
  const [visible2, setVisible2] = useState(false);
  const [visible3, setVisible3] = useState(false);
  const [visible4, setVisible4] = useState(false);
  const [visible5, setVisible5] = useState(false);
  const [visible6, setVisible6] = useState(false);
  return(
    <div>
      <Modal visible={visible1}><Form1/></Modal>
      <Modal visible={visible2}><Form2/></Modal>
      <Modal visible={visible3}><Form3/></Modal>
      <Modal visible={visible4}><Form4/></Modal>
      <Modal visible={visible5}><Form5/></Modal>
      <Modal visible={visible6}><Form6/></Modal>
    <div/>
  )
}

姑且不算其他代码就Modal和state我们就写了将近20行代码,那如何减少Modal和state的维护呢?

封装useModal实现Modal优雅的使用:

首先,我们看一下预期最简单的使用方式,我们只需要调用modal.open方法时,替换children就可达到Modal重复使用。

export default () => {
  const [modal, ModalDOM] = useModal();

  const handleClick = () => {
    modal.open({
      title: '收集数据',
      children: <Form1/>,
      onOk: (values: any) => { // values为收集到的表单d
        modal.close();
      }
    });
  }

  return (
    <>
      <button onClick={handleClick}>自定义useModal</button>
      {ModalDOM}
    </>
  )
}

上面我们写的预期效果,我们知道useModal返回了一个钩子和一个Modal组件,通过调用钩子的open方法和close方法来控制Modal组件打开和关闭,下面我们通过预期,逆向实现主体函数:

interface modalRefType {
  open: () => void;
  close: () => void;
  injectChildren: (child: React.ReactElement) => void;
  injectModalProps: (props: ModalProps) => void;
}

export default () => {
  const modalRef = useRef<modalRefType>();
  const handle = useMemo(() => {
    return {
      open: ({ children, ...rest }) => {
        modalRef.current.injectChildren(children);  // 注入子组件
        modalRef.current.injectModalProps(rest);    // 注入Modal的参数
        modalRef.current.open();
      },
      close: () => {
        modalRef.current.close();
      }
    };
  }, []);

  return [handle, <MyModal ref={modalRef} />] as const;
}

主体函数思路还是比较清晰,通过handle+useRef控制MyModal组件暴露出来的injectChildren、injectModalProps、open、close方法,下面我们再根据暴露出来的这些方法,逆向编程实现MyModal组件:

const MyModal = memo(forwardRef((prop: any, ref) => {
  const [form] = Form.useForm();
  const [modalChildren, setModalChildren] = useState<React.ReactElement>(null);
  const [modalProps, setModalProps] = useState<ModalProps>({
    visible: false,
  });

  // ant.design 4.0 Form的onFinish触发回调
  const onFinish = useCallback((values: any) => {
    modalProps.onOk?.(values);
  }, [form, modalProps]);

  // 关闭当前Modal
  const onClose = useCallback(() => {
    setModalProps((source) => ({
      ...source,
      visible: false,
    }));
  }, [form]);

  // 关闭当前Modal
  const onOpen = useCallback(() => {
    setModalProps((source) => ({
      ...source,
      visible: true,
    }));
  }, [form]);


  useImperativeHandle(ref, () => ({
    // 注入Modal的子组件
    injectChildren: (element) => {
      setModalChildren(element);
    },
    // 注入Modal参数
    injectModalProps: (props) => {
      console.log(props)
      setModalProps((source) => {
        return {
          ...source,
          ...props,
        }
      });
    },
    // 打开Modal
    open: () => {
      onOpen();
    },
    // 关闭Modal
    close: () => {
      onClose();
    }
  }), []);

  // 这里的Modal是ant.design中的Modal
  return (
    <Modal
      {...modalProps}
      onCancel={onClose}
      onOk={() => form.submit()}. 
    >
      {
        modalChildren
        ?
        React.cloneElement(modalChildren, {
          onFinish,
          form,
          onClose,
        })
        : null
      }
    </Modal>
  )
}));

我们通过React.cloneElement克隆了子组件(就是我们的业务代码Form),并且注入form和onFinish实现对表单的控制,通过onOk方法的挟持,实现在点击Modal的ok按钮时获取表单数据。

以上代码有一个细节,我们在改变状态的时候都是使用函数的方式setModalProps((sourcce) => ({...source, ...})),这主要是为了获取最新的状态进行。

到目前为止,我们已经实现了useModal基础版本,它只能让我们创建表单,而无法编辑表单和脱离表单去使用。

进阶实现useModal

为了脱离Form使用和表单的编辑,我们添加了type、initialValues参数:

....  
// 修改使用方式
  const handleClick = () => {
    modal.open({
      title: '收集数据',
      type: 'form',   // 类型为form时需要Form触发onFinish才触发onOk
      initialValues: { name: '原值' },  // 表单默认值
      children: <Form1/>,
      onOk: (values: any) => {
        console.log('收集到的表单', values);
        modal.close();
      }
    });
  }
  ....

升级后useModal完整的代码:

import React, { useRef, useMemo, memo, forwardRef, useCallback, useState, useImperativeHandle } from 'react';
import { Modal, Form } from 'antd';
import type { ModalProps } from 'antd';
import "antd/dist/antd.css";

const MyModal = memo(forwardRef((prop: any, ref) => {
  const [form] = Form.useForm();
  const [modalChildren, setModalChildren] = useState<React.ReactElement>(null);
  const [modalProps, setModalProps] = useState<ModalProps>({
    visible: false,
  });
  const typeRef = useRef<string>();

  // ant.design 4.0 Form的onFinish触发回调
  const onFinish = useCallback((values: any) => {
    modalProps.onOk?.(values);
  }, [form, modalProps]);

  // 关闭当前Modal
  const onClose = useCallback(() => {
    setModalProps((source) => ({
      ...source,
      visible: false,
    }));
  }, [form]);

  // 关闭当前Modal
  const onOpen = useCallback(() => {
    setModalProps((source) => ({
      ...source,
      visible: true,
    }));
  }, [form]);


  useImperativeHandle(ref, () => ({
    // 注入Modal的子组件
    injectChildren: (element) => {
      setModalChildren(element);
    },
    // 注入Modal参数
    injectModalProps: (props) => {
      console.log(props)
      setModalProps((source) => {
        return {
          ...source,
          ...props,
        }
      });
    },
    // 打开Modal
    open: () => {
      onOpen();
    },
    // 关闭Modal
    close: () => {
      onClose();
    },
    // 设置表单数据
    setFieldsValue: (values: any) => {
      form.setFieldsValue?.(values);
    },
    setType: (type: string) => {
      typeRef.current = type;
    }
  }), []);

  const handleOk = useCallback(() => {
    if (typeRef.current === 'form') {
      form.submit();
    } else {
      modalProps.onOk?.(null);
    }
  }, [form, modalProps]);

  // 这里的Modal是ant.design中的Modal
  return (
    <Modal
      {...modalProps}
      onCancel={onClose}
      onOk={handleOk}
    >
      {
        modalChildren
        ?
        React.cloneElement(modalChildren, {
          onFinish,
          form,
          onClose,
        })
        : null
      }
    </Modal>
  )
}));

interface modalRefType {
  open: () => void;
  close: () => void;
  injectChildren: (child: React.ReactElement) => void;
  injectModalProps: (props: ModalProps) => void;
  setFieldsValue: (values: any) => void;
  setType: (type: string) => void;
}

interface openArgType extends ModalProps {
  children?: React.ReactElement,
  type?: 'form' | 'default',
  initialValues?: {
    [key: string]: any;
  },
}

export default () => {
  const modalRef = useRef<modalRefType>();
  const handle = useMemo(() => {
    return {
      open: ({ children, type, initialValues, ...rest }: openArgType) => {
        modalRef.current.setType(type);
        modalRef.current.injectChildren(children);  // 注入子组件
        modalRef.current.injectModalProps(rest);    // 注入Modal的参数
        modalRef.current.open();

        if (initialValues && type === 'form') {
          modalRef.current.setFieldsValue?.(initialValues);
        }
      },
      close: () => {
        modalRef.current.close();
      }
    };
  }, []);

  return [handle, <MyModal ref={modalRef} />] as const;
}

完结:以上我们通过了逆向的编程方式,实现了我们需求的useModal钩子,用这样的一个hook使用Modal是不是优雅多了。

上一篇下一篇

猜你喜欢

热点阅读