ModalForm / DrawerForm
form-dialog把带校验的表单装进弹窗或抽屉,管好提交与关闭
用法
弹窗表单
trigger 触发打开,提交成功(onFinish resolve)自动关闭。
<ModalForm
title="新增员工"
trigger={<Button>新增</Button>}
onFinish={async (values) => {
await api.create(values);
}}
>
<Field label="姓名">
<Input placeholder="必填" />
</Field>
</ModalForm>关闭守门
点遮罩默认不关;改动过的表单按 Esc 或点关闭键会先确认一次,干净表单直接关。传 dismissible / confirmOnClose 可各自关掉。
<ModalForm title="新增学校" form={form} onFinish={save}>
{/* 点遮罩不关;改过再按 Esc 会问「放弃未提交的内容?」 */}
</ModalForm>
// 恢复原语行为
<ModalForm dismissible confirmOnClose={false} … />抽屉表单
DrawerForm 复用同一编排,从右侧贴边滑出,适合字段较多的编辑场景。
<DrawerForm
title="编辑员工"
trigger={<Button variant="outline">编辑</Button>}
onFinish={(values) => api.update(values)}
>
<Field label="姓名">
<Input />
</Field>
<Field label="邮箱">
<Input />
</Field>
</DrawerForm>抽屉贴边方向
DrawerForm 通过 side 控制贴边方向(left / right)。
<DrawerForm title="筛选" side="left" trigger={<Button variant="outline">左侧抽屉</Button>}>
<Field label="关键词">
<Input />
</Field>
</DrawerForm>自定义按钮文案
submitText / cancelText 覆盖默认的提交 / 取消文案。
<ModalForm
title="导出报表"
submitText="立即导出"
cancelText="再想想"
trigger={<Button>导出</Button>}
>
<Field label="文件名">
<Input placeholder="report.xlsx" />
</Field>
</ModalForm>何时用
列表页点「新增/编辑」弹出表单时用:ModalForm 居中弹窗、DrawerForm 贴边抽屉,二者 API 一致(抽屉多一个 side)。它把 Dialog/Drawer + 提交按钮 footer + 校验编排好了。页面内常驻表单用 ProForm;裸表单容器用 Form;多步向导用 StepsForm。
导入
import { ModalForm, DrawerForm } from "@hulianui/ui"Props
公共(ModalForm = FormDialogBaseProps;DrawerForm 在此基础上加 side):
| 名称 | 类型 | 默认 | 说明 |
|---|---|---|---|
| title * | string | - | 标题(a11y label) |
| open | boolean | - | 受控开关 |
| defaultOpen | boolean | - | 非受控初始开关 |
| form | FormInstance | - | useForm 实例:提供则提交前自动 validate(),校验不过保持打开 |
| submitText | string | locale.modalForm.submit | 提交按钮文案 |
| cancelText | string | locale.modalForm.cancel | 取消按钮文案 |
| className | string | - | 容器类名(控宽度等) |
| side | DrawerSide | "right" | 仅 DrawerForm:抽屉贴边方向 |
| draggable | boolean | false | 仅 ModalForm:允许按住标题拖动对话框(透传 DialogContent.draggable) |
| dismissible | boolean | false | 点遮罩是否关闭。与 `Dialog` / `Drawer` 原语相反:编排件知道自己装着一张表单,填到一半被随手点没的代价太大(#343)。传 true 恢复原语行为 |
| confirmOnClose | boolean | true | 表单改动过时,关闭前先确认一次。判据是 form.isDirty() 与 hasExternalChanges() 取或,两个都没传就不生效;干净表单直接关;提交成功后的关闭也不问。异步回填的编辑表单要用 setFieldsValue(v, { markPristine: true }) 把回填那一刻钉成基线,否则什么都没改也会弹确认(见 Form) |
| hasExternalChanges | () => boolean | - | 补一条 form 管不着的脏判定,与 form.isDirty() 取或,不是覆盖(#351):任一侧为真就先确认。给弹窗里自持 state、不走 form.register 的复合控件用(区划级联、权限勾选组、标签编辑器)。只在真要关的那一刻求值;不传 form 时也能单独用这一侧 |
| discardTitle | ReactNode | locale modalForm.discardTitle | 放弃确认的标题 |
| discardDescription | ReactNode | locale modalForm.discardDescription | 放弃确认的说明 |
Events
| 事件 | 类型 | 说明 |
|---|---|---|
| onOpenChange | (open: boolean) => void | 开关变化回调 |
| onFinish | (values: FormValues) => void | boolean | Promise<void | boolean> | 提交回调;返回 Promise → 按钮 loading;resolve(非 false) 自动关闭;reject 或返回 false 保持打开 |
Slots
| 插槽 | 类型 | 说明 |
|---|---|---|
| trigger | ReactElement | 触发元素(非受控打开用);受控时可省 |
| children | ReactNode | 表单字段 |
禁忌 / 坑
- 关/不关由
onFinish返回值决定:resolve 非 false → 自动关;想留在原地(如自处理错误)reject 或 return false。别在 onFinish 里手动调 onOpenChange(false) 跟自动关闭打架。 - 传了
form才会提交前自动validate()并在校验不过时保持打开;不传 form 则不校验、直接把 values 交给 onFinish。 - 提交成功后想清空字段需自己调
form.resetFields(),组件不会替你重置。
关掉这张表单不是无代价的(#343)
ModalForm / DrawerForm 与裸 Dialog / Drawer 的默认值刻意不同:点遮罩默认不关。原语是通用容器,随手点外面关掉很合理;编排件装的一定是表单,填到第 8 个字段时鼠标落在窗外一点就全部清空,这个代价与那点便利完全不成比例。
退路仍在,只是加了一道确认:Esc 与右上角关闭键在 form.isDirty() 为真时先问一句「放弃未提交的内容?」,确认才关。三种情况不会打扰你:没传 form(编排件无从判断脏净 —— 除非用下面的 hasExternalChanges 把判据接进来)、表单一字未改、以及提交成功后的那次关闭。
// 默认:点遮罩不关,改过就问一句
<ModalForm title="新增学校" form={form} onFinish={save}>…</ModalForm>
// 恢复原语行为
<ModalForm dismissible confirmOnClose={false} …>…</ModalForm>
// 自己判断要不要关:details.reason 区分点遮罩 / Esc / 关闭键
<ModalForm onOpenChange={(open, details) => {
if (!open && details?.reason === "outside-press") details.cancel();
}} …>…</ModalForm>确认框由编排件自己渲染(AlertDialog),不要求你挂 `ModalProvider` —— 命令式 modal.confirm 在没挂 Provider 的应用里什么都不显示,而关闭动作此时已被拦下,那会变成「窗关不掉又没有提示」的死局。
form 管不着的字段也算改过(#351)
上面那道守门的判据只有 form.isDirty(),也就是只覆盖走 form.register 的字段。真实后台表单里相当一部分控件是自持 state 的 —— 区划三级级联、权限勾选组、标签编辑器、学科/年级联动下拉,它们本来就不是「一个输入框一个值」,不进 form.values。于是只选了区划、勾了六条权限就按 Esc,编排件会判成干净表单,不问就关,填的全没。
hasExternalChanges 把这一侧接回来,与 `form.isDirty()` 取或:
const [region, setRegion] = useState<string[]>([]);
const [codes, setCodes] = useState<string[]>([]);
const snapshot = useRef(""); // 打开/回填那一刻的基线,由你自己钉
<ModalForm
title="编辑学校"
form={form}
hasExternalChanges={() => snapshot.current !== JSON.stringify({ region, codes })}
onFinish={save}
>…</ModalForm>用回调不用布尔,是为了不逼着每次渲染都算一遍快照比对 —— 这个答案只在「真要关」的那一刻有人看。要传布尔写 () => flag 即可。
与 `markPristine` 的边界:这一侧的状态全在你手上。form.markPristine() / setFieldsValue(v, { markPristine: true }) 只钉 form 自己的基线,碰不到你的 useState,编排件也不存快照、不会替你重置。所以异步回填的编辑表单要在调 markPristine 的同一处把自己的快照一并刷新(上例里就是给 snapshot.current 重新赋值),否则回填会被这一侧算成「改过」,什么都没动也弹确认。