FormPopup 表单弹层
参考其他的
formily组件库封装的弹出式表单封装,在接口设计上几乎一致。
基础表单弹层
编辑联系人姓名和电话,保存后默认返回 form.values。
import { FormItem, FormLayout, FormPopup, Input } from '@silver-formily/antd-mobile'
import { Field } from '@silver-formily/react'
import { Button } from 'antd-mobile'
interface ContactValues {
name: string
phone: string
}
function BasicContent() {
return (
<FormLayout>
<Field name="name" title="联系人" decorator={[FormItem]} component={[Input, { placeholder: '请输入联系人姓名' }]} />
<Field name="phone" title="联系电话" decorator={[FormItem]} component={[Input, { type: 'tel', placeholder: '请输入联系电话' }]} />
</FormLayout>
)
}
function onSubmit(values: unknown) {
return Prompts.alert(JSON.stringify(values, null, 2))
}
export default function BasicDemo() {
async function handleOpen() {
try {
const result = await FormPopup<ContactValues>({ title: '编辑联系人', okText: '保存' }, BasicContent).open({
values: { name: '张三', phone: '13800000000' },
})
await onSubmit(result)
}
catch (error) {
if (!(error instanceof Error && error.message === 'cancel'))
throw error
}
}
return <Button block color="primary" onClick={handleOpen}>打开基础表单弹层</Button>
}结合 FormStep
FormPopup 创建并提供完整的 Form 实例,FormStep 负责步骤切换。自定义 footer 通过 form.reset() 重置表单,通过 formStep.next() 校验当前步骤并前进,最后调用 resolve() 进入 FormPopup 的校验与确认中间件流程。无需再创建 Form 或嵌套 FormProvider。
每次打开都创建新的步骤控制器,从第一步开始。observer 让按钮跟随步骤和 form.submitting 更新;异步提交期间禁用取消、返回和重置。forConfirm 接收完整的 Form,可读取或修改字段状态和值,本例模拟请求并去除备注两端空白,完成后返回表单值。
未激活步骤保留值并跳过校验,因此应使用“下一步”顺序完成必填步骤;最后确认只校验当前可见字段,不会重新校验全部隐藏步骤。
import type { FormPopupContentProps, IFormStep } from '@silver-formily/antd-mobile'
import { FormItem, FormLayout, FormPopup, FormStep, Input } from '@silver-formily/antd-mobile'
import { createSchemaField, observer } from '@silver-formily/react'
import { Button } from 'antd-mobile'
import './style.css'
interface StepFormValues {
name: string
mobile: string
address: string
deliveryTime: string
remark: string
}
const defaultValues: StepFormValues = {
name: '张三',
mobile: '13800000000',
address: '杭州市西湖区文三路 188 号',
deliveryTime: '工作日晚上 7 点后',
remark: '',
}
const SchemaField = createSchemaField({ components: { FormStep, FormItem, Input } })
const StepFooter = observer(({ form, resolve, reject, formStep }: FormPopupContentProps<StepFormValues> & { formStep: IFormStep }) => (
<div style={{ display: 'grid', gap: 12, width: '100%' }}>
<Button
block
disabled={form.submitting}
onClick={async () => {
await form.reset()
formStep.setCurrent(0)
}}
>
重置表单
</Button>
<div className="form-popup-demo-actions">
<Button disabled={form.submitting} onClick={reject}>取消</Button>
<Button disabled={form.submitting || !formStep.allowBack} onClick={() => formStep.back()}>上一步</Button>
{formStep.allowNext
? <Button color="primary" onClick={() => formStep.next()}>下一步</Button>
: <Button color="primary" loading={form.submitting} onClick={() => resolve()}>确认提交</Button>}
</div>
</div>
))
function onSubmit(values: unknown) {
return Prompts.alert(JSON.stringify(values, null, 2))
}
export default function FormStepDemo() {
async function handleOpen() {
// 每次打开创建独立的步骤控制器,与本次 FormPopup 的 Form 对应。
const formStep = FormStep.createFormStep()
const popup = FormPopup<StepFormValues>({ title: '分步填写配送信息' }, {
default: () => (
<FormLayout>
<SchemaField>
<SchemaField.Void name="steps" x-component="FormStep" x-component-props={{ formStep }}>
<SchemaField.Void name="contact" title="联系信息" x-component="FormStep.StepPane">
<SchemaField.String name="name" title="姓名" required x-decorator="FormItem" x-component="Input" x-component-props={{ placeholder: '请输入联系人姓名' }} />
<SchemaField.String name="mobile" title="手机号" required x-decorator="FormItem" x-component="Input" x-component-props={{ type: 'tel', placeholder: '请输入手机号' }} />
</SchemaField.Void>
<SchemaField.Void name="delivery" title="配送信息" x-component="FormStep.StepPane">
<SchemaField.String name="address" title="收货地址" required x-decorator="FormItem" x-component="Input.TextArea" x-component-props={{ rows: 3, placeholder: '请输入街道、楼栋、门牌号' }} />
<SchemaField.String name="deliveryTime" title="送达时间" x-decorator="FormItem" x-component="Input" x-component-props={{ placeholder: '例如工作日晚上 7 点后' }} />
</SchemaField.Void>
<SchemaField.Void name="confirm" title="确认提交" x-component="FormStep.StepPane">
<SchemaField.String name="remark" title="备注" x-decorator="FormItem" x-component="Input.TextArea" x-component-props={{ rows: 2, placeholder: '可选,补充门禁、楼层或电话备注' }} />
</SchemaField.Void>
</SchemaField.Void>
</SchemaField>
</FormLayout>
),
footer: props => <StepFooter {...props} formStep={formStep} />,
}).forConfirm(async (form) => {
// 可以读取、修改完整 Form 实例;返回 Promise 让提交 Loading 覆盖整个请求。
await new Promise(resolve => setTimeout(resolve, 1000))
form.setValues({ remark: form.values.remark.trim() })
})
try {
const result = await popup.open({ initialValues: { ...defaultValues } })
await onSubmit(result)
}
catch (error) {
if (!(error instanceof Error && error.message === 'cancel'))
throw error
}
}
return <Button block color="primary" onClick={handleOpen}>打开 FormPopup 分步表单</Button>
}自定义头尾与 middleware
通过 header / footer 接管头尾区域。发布时执行异步确认中间件,保存草稿时通过 saveDraft() 触发动态中间件。自定义操作组件使用 observer 跟随 form.submitting 更新加载与禁用状态。
import type { FormPopupContentProps } from '@silver-formily/antd-mobile'
import { FormItem, FormLayout, FormPopup, Input } from '@silver-formily/antd-mobile'
import { Field, observer } from '@silver-formily/react'
import { toJS } from '@silver-formily/reactive'
import { Button } from 'antd-mobile'
import './style.css'
interface PublishValues {
title: string
summary: string
}
const PublishFooter = observer(({ form, reject, resolve, saveDraft }: FormPopupContentProps<PublishValues, 'save-draft'>) => (
<div className="form-popup-demo-actions">
<Button block disabled={form.submitting} onClick={reject}>取消</Button>
<Button block loading={form.submitting} onClick={saveDraft}>保存草稿</Button>
<Button block color="primary" loading={form.submitting} onClick={() => resolve()}>发布</Button>
</div>
))
function onSubmit(values: unknown) {
return Prompts.alert(JSON.stringify(values, null, 2))
}
export default function MiddlewareDemo() {
async function handleOpen() {
try {
const result = await FormPopup<PublishValues, ['save-draft']>({ title: '发布配置', closeOnMaskClick: false }, {
header: () => (
<div className="form-popup-demo-header">
<div className="form-popup-demo-header-title">自定义头部</div>
<div className="form-popup-demo-header-description">保留表单生命周期,按业务需求接管头部和底部按钮。</div>
</div>
),
default: () => (
<FormLayout>
<Field
name="title"
title="方案标题"
validator={[{ required: true, message: '请输入方案标题' }]}
decorator={[FormItem]}
component={[Input, { placeholder: '例如:华东巡检改版' }]}
/>
<Field
name="summary"
title="摘要"
decorator={[FormItem]}
component={[Input.TextArea, { rows: 3, maxLength: 60, showCount: true, placeholder: '简要说明这次发布要点' }]}
/>
</FormLayout>
),
footer: props => <PublishFooter {...props} />,
}, ['save-draft'])
.forConfirm(async (form) => {
await new Promise(resolve => setTimeout(resolve, 400))
return toJS(form.values)
})
.forSaveDraft((form) => {
form.setValues({ summary: `${form.values.summary || ''}(草稿)` })
})
.open({ values: { title: '华东巡检改版', summary: '先同步到预发布环境,确认券后价和库存展示。' } })
await onSubmit(result)
}
catch (error) {
if (!(error instanceof Error && error.message === 'cancel'))
throw error
}
}
return <Button block onClick={handleOpen}>打开自定义头尾 + middleware 示例</Button>
}打开前异步加载
forOpen 支持返回 Promise 的中间件。执行超过 100ms 时自动展示 Loading,并阻止点击背景;完成后关闭 Loading,再展示表单。快速完成时不会闪烁,中间件失败或等待期间调用 close() 也会清理 Loading。
通过 loadingText 自定义提示文案,默认是 'Loading...'。下面模拟 1.5 秒的数据请求,并将结果作为表单初始值。
import { FormItem, FormLayout, FormPopup, Input } from '@silver-formily/antd-mobile'
import { Field } from '@silver-formily/react'
import { Button } from 'antd-mobile'
function onSubmit(values: unknown) {
return Prompts.alert(JSON.stringify(values, null, 2))
}
export default function LoadingDemo() {
async function handleOpen() {
try {
const result = await FormPopup<{ name: string }>({
title: '编辑联系人',
loadingText: '正在加载联系人...',
}, () => (
<FormLayout>
<Field name="name" title="联系人" decorator={[FormItem]} component={[Input]} />
</FormLayout>
))
.forOpen(async (props) => {
// 模拟请求:等待期间自动显示 Loading,完成后再打开表单。
await new Promise(resolve => setTimeout(resolve, 1500))
return { ...props, initialValues: { name: '张三' } }
})
.open()
await onSubmit(result)
}
catch (error) {
if (!(error instanceof Error && error.message === 'cancel'))
throw error
}
}
return <Button block color="primary" onClick={handleOpen}>异步加载后打开表单</Button>
}提交 Loading
点击“保存”并通过校验后,forConfirm 模拟 2 秒的异步提交。等待期间,默认确认按钮跟随 form.submitting 显示 Loading,取消按钮禁用;提交完成后关闭弹层并展示提交结果。清空必填项再保存,可以观察校验失败时弹层保持打开。
请在 forConfirm 中 await 或返回提交请求的 Promise,才能让 Loading 覆盖整个请求过程。默认按钮区已处理提交状态,无需手动设置 loading。
import { FormItem, FormLayout, FormPopup, Input } from '@silver-formily/antd-mobile'
import { Field } from '@silver-formily/react'
import { Button } from 'antd-mobile'
function onSubmit(values: unknown) {
return Prompts.alert(JSON.stringify(values, null, 2))
}
export default function SubmitLoadingDemo() {
async function handleOpen() {
try {
const result = await FormPopup<{ name: string }>({
title: '保存联系人',
okText: '保存',
}, () => (
<FormLayout>
<Field name="name" title="联系人" required decorator={[FormItem]} component={[Input]} />
</FormLayout>
))
.forConfirm(async () => {
// 模拟提交请求:返回 Promise,让 form.submitting 持续到请求完成。
await new Promise(resolve => setTimeout(resolve, 2000))
// 未返回自定义结果时,默认使用 form.values。
})
.open({ initialValues: { name: '张三' } })
await onSubmit(result)
}
catch (error) {
if (!(error instanceof Error && error.message === 'cancel'))
throw error
}
}
return <Button block color="primary" onClick={handleOpen}>异步提交 Loading</Button>
}Picker 风格头部
将取消和确定放在头部,通过 footer: () => null 和局部样式隐藏默认底部区域。必填项校验失败时保持弹层打开。
import type { FormPopupContentProps } from '@silver-formily/antd-mobile'
import { FormItem, FormLayout, FormPopup, Input } from '@silver-formily/antd-mobile'
import { Field, observer } from '@silver-formily/react'
import { Button } from 'antd-mobile'
import './style.css'
interface AddressValues {
receiver: string
phone: string
}
const PickerHeader = observer(({ form, reject, resolve }: FormPopupContentProps<AddressValues>) => (
<div className="form-popup-demo-toolbar">
<Button fill="none" disabled={form.submitting} onClick={reject}>取消</Button>
<div className="form-popup-demo-toolbar-title">编辑收货人</div>
<Button fill="none" color="primary" disabled={form.submitting} onClick={() => resolve()}>确定</Button>
</div>
))
function onSubmit(values: unknown) {
return Prompts.alert(JSON.stringify(values, null, 2))
}
export default function PickerLikeDemo() {
async function handleOpen() {
try {
const result = await FormPopup<AddressValues>({ className: 'form-popup-demo-picker-like', closeOnMaskClick: false }, {
header: props => <PickerHeader {...props} />,
default: () => (
<FormLayout>
<Field
name="receiver"
title="收货人"
validator={[{ required: true, message: '请输入收货人姓名' }]}
decorator={[FormItem]}
component={[Input, { placeholder: '例如:张三' }]}
/>
<Field
name="phone"
title="联系电话"
validator={[{ required: true, message: '请输入联系电话' }]}
decorator={[FormItem]}
component={[Input, { type: 'tel', placeholder: '例如:13800000000' }]}
/>
</FormLayout>
),
footer: () => null,
}).open({ values: { receiver: '张三', phone: '13800000000' } })
await onSubmit(result)
}
catch (error) {
if (!(error instanceof Error && error.message === 'cancel'))
throw error
}
}
return <Button block color="primary" onClick={handleOpen}>打开 Picker 风格 FormPopup</Button>
}API
const popup = FormPopup('编辑', ({ form, resolve, reject }) => (
<Content />
))
const values = await popup.open({ initialValues: { name: 'Silver' } })
popup.close()FormPopup(props | title, content?, dynamicMiddlewareNames?) 返回可复用控制器,创建时不操作 DOM。内容支持 React 元素、组件或渲染函数,也可传入 { header, default, footer } 渲染函数对象;各函数均收到 { form, resolve, reject }。内容已包裹 FormProvider,可直接使用 Field、SchemaField、useForm();自定义区域也可访问表单上下文。
弹层属性
IFormPopupProps 继承 antd-mobile Popup 属性,visible、children、forceRender、destroyOnClose 由封装管理。默认底部弹出,禁止点击遮罩关闭,启用底部安全区。
| 属性 | 说明 |
|---|---|
loadingText | 打开前异步加载提示文案,默认 'Loading...' |
title | 默认头部内容,支持 ReactNode |
okText / cancelText | 按钮文案,默认「确定」/「取消」 |
okButtonProps / cancelButtonProps | antd-mobile Button 属性;点击行为和按钮 type 由封装管理 |
safeArea | 是否显示底部安全区,默认 true |
header 替换标题,footer 替换默认按钮。默认确认按钮在提交时显示 loading,取消按钮在提交时禁用。自定义按钮需自行处理 form.submitting。
控制器与中间件
open(props?):传入 core 的IFormProps,每次创建新表单;活动会话中重复调用返回同一个 Promise。close()/reject():取消,执行取消中间件后以Error('cancel')拒绝 Promise。调用方应捕获取消异常。resolve(type?):校验通过后执行确认中间件;校验失败保持弹层打开,允许修改后重新提交。forOpen(middleware):打开前处理表单参数。forConfirm(middleware):校验后处理 Form,默认返回表单值;中间件可返回其他提交结果。forCancel(middleware):取消时处理 Form。
中间件签名为 (payload, next) => result,支持异步、next() 和 next(nextPayload)。未返回值时使用默认结果。打开、确认或取消中间件抛错会拒绝 open() 的 Promise 并清理弹层。
const popup = FormPopup('编辑', {
default: () => <Content />,
footer: ({ saveDraft }) => (
<Button onClick={saveDraft}>保存草稿</Button>
),
}, ['save-draft'])
.forOpen(props => ({ ...props, validateFirst: true }))
.forSaveDraft(async (form, next) => {
await saveDraft(form.values)
return next(form)
})动态名称规范化为 camelCase(save-draft → saveDraft),生成 forSaveDraft 和内容回调中的 saveDraft()。动态动作同样先校验,完成后返回表单值;保留名称 open、confirm、cancel、form、resolve、reject 不可用。
确认或取消完成 Promise 后播放关闭动画,动画结束卸载 React root。可以立即打开新会话,旧回调不会影响新会话。打开中间件等待期间调用 close() 也会取消,并阻止延迟挂载。提交或取消中间件执行期间忽略重复操作。
每个弹层使用独立 React root,不继承调用位置的其他 Context;业务 Provider 请在内容中显式包裹。