FormPopup
FormPopup基于 VantPopup组件补齐createForm、FormProvider、默认按钮区和提交态处理,让移动端也能用接近FormDialog/FormDrawer的心智打开一整个表单。
基础表单弹层
vue
<script setup lang="tsx">
import { FormItem, FormPopup, Input } from '@silver-formily/vant'
import { Field } from '@silver-formily/vue'
import { Button as VanButton } from 'vant'
import { defineComponent, ref } from 'vue'
import { showDemoResult } from '../shared'
const latestResult = ref('FormPopup 默认会返回 form.values。')
const BasicContent = defineComponent({
name: 'FormPopupBasicContent',
setup() {
return () => (
<div class="form-popup-demo-fields">
<Field
name="name"
title="联系人"
decorator={[FormItem]}
component={[Input, { placeholder: '请输入联系人姓名' }]}
/>
<Field
name="phone"
title="联系电话"
decorator={[FormItem]}
component={[Input, { type: 'tel', placeholder: '请输入联系电话' }]}
/>
</div>
)
},
})
async function handleOpen() {
const result = await FormPopup<{ name: string, phone: string }>(
{
title: '编辑联系人',
okText: '保存',
},
BasicContent,
).open({
values: {
name: '张三',
phone: '13800000000',
},
})
latestResult.value = JSON.stringify(result, null, 2)
await showDemoResult(result, 'FormPopup 默认返回 form.values')
}
</script>
<template>
<div class="demo-block">
<VanButton type="primary" block @click="handleOpen">
打开基础表单弹层
</VanButton>
<pre class="demo-result">{{ latestResult }}</pre>
</div>
</template>
<style scoped>
.demo-block {
display: grid;
gap: 12px;
}
.demo-result {
margin: 0;
overflow: auto;
border-radius: 12px;
padding: 12px;
background: var(--van-background-2);
color: var(--van-text-color-2);
font-size: 12px;
line-height: 1.6;
white-space: pre-wrap;
}
:global(.form-popup-demo-fields) {
display: grid;
}
</style>作用域插槽与 middleware
vue
<script setup lang="tsx">
import type { FormPopupSlotProps } from '@silver-formily/vant'
import { toJS } from '@formily/reactive'
import { FormItem, FormPopup, Input } from '@silver-formily/vant'
import { Field } from '@silver-formily/vue'
import { Button as VanButton } from 'vant'
import { ref } from 'vue'
import { showDemoResult } from '../shared'
const latestResult = ref('这里同时演示 header / footer 作用域插槽,以及直接通过 saveDraft() 触发动态 middleware。')
interface PublishValues {
title: string
summary: string
}
const publishContent = {
header: () => (
<div class="form-popup-demo-header">
<div class="form-popup-demo-header__title">自定义头部</div>
<div class="form-popup-demo-header__desc">
你可以保留 FormPopup 的表单生命周期,同时按业务需求接管头部和底部按钮。
</div>
</div>
),
default: () => (
<div class="form-popup-demo-fields">
<Field
name="title"
title="方案标题"
validator={[{ required: true, message: '请输入方案标题' }]}
decorator={[FormItem]}
component={[Input, { placeholder: '例如:华东巡检改版' }]}
/>
<Field
name="summary"
title="摘要"
decorator={[FormItem, { labelAlign: 'top' }]}
component={[Input.TextArea, {
rows: 3,
maxlength: 60,
showWordLimit: true,
placeholder: '简要说明这次发布要点',
}]}
/>
</div>
),
footer: ({ form, reject, resolve, saveDraft }: FormPopupSlotProps<PublishValues>) => (
<div class="form-popup-demo-actions">
<VanButton block plain disabled={form.submitting} onClick={() => reject()}>
取消
</VanButton>
<VanButton block loading={form.submitting} onClick={() => saveDraft()}>
保存草稿
</VanButton>
<VanButton block type="primary" loading={form.submitting} onClick={() => resolve()}>
发布
</VanButton>
</div>
),
}
async function handleOpen() {
const result = await FormPopup<PublishValues, ['save-draft']>(
{
title: '发布配置',
closeOnClickOverlay: false,
},
publishContent,
['save-draft'] as const,
)
.forConfirm(async (form) => {
await new Promise(resolve => setTimeout(resolve, 400))
return {
action: 'publish',
values: toJS(form.values),
}
})
.forSaveDraft((form) => {
return {
action: 'draft',
values: toJS(form.values),
}
})
.open({
values: {
title: '华东巡检改版',
summary: '先同步到预发布环境,确认券后价和库存展示。',
},
})
latestResult.value = JSON.stringify(result, null, 2)
await showDemoResult(result, 'FormPopup saveDraft 返回值')
}
</script>
<template>
<div class="demo-block">
<VanButton block @click="handleOpen">
打开 slots + middleware 示例
</VanButton>
<pre class="demo-result">{{ latestResult }}</pre>
</div>
</template>
<style scoped>
.demo-block {
display: grid;
gap: 12px;
}
.demo-result {
margin: 0;
overflow: auto;
border-radius: 12px;
padding: 12px;
background: var(--van-background-2);
color: var(--van-text-color-2);
font-size: 12px;
line-height: 1.6;
white-space: pre-wrap;
}
:global(.form-popup-demo-header) {
display: grid;
gap: 6px;
padding: 2px 2px 4px;
}
:global(.form-popup-demo-header__title) {
font-size: 16px;
font-weight: 600;
color: var(--van-text-color);
}
:global(.form-popup-demo-header__desc) {
font-size: 13px;
line-height: 1.5;
color: var(--van-text-color-2);
}
:global(.form-popup-demo-fields) {
display: grid;
}
:global(.form-popup-demo-actions) {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 12px;
}
</style>Picker 风格头部
vue
<script setup lang="tsx">
import type { FormPopupSlotProps } from '@silver-formily/vant'
import { FormItem, FormPopup, Input } from '@silver-formily/vant'
import { Field } from '@silver-formily/vue'
import { Button as VanButton } from 'vant'
import { ref } from 'vue'
import { showDemoResult } from '../shared'
interface AddressValues {
receiver: string
phone: string
}
const latestResult = ref('这里演示一个仿 Picker 工具栏风格的 FormPopup:取消和确定都放在头部,底部不再显示 footer。')
const pickerLikeContent = {
header: ({ form, reject, resolve }: FormPopupSlotProps<AddressValues>) => (
<div class="van-picker__toolbar">
<button
class="van-picker__cancel van-haptics-feedback"
disabled={form.submitting}
type="button"
onClick={() => reject()}
>
取消
</button>
<div class="van-picker__title van-ellipsis">编辑收货人</div>
<button
class="van-picker__confirm van-haptics-feedback"
disabled={form.submitting}
type="button"
onClick={() => resolve()}
>
确定
</button>
</div>
),
default: () => (
<div class="form-popup-demo-fields">
<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' }]}
/>
</div>
),
footer: () => null,
}
async function handleOpen() {
const result = await FormPopup<AddressValues>(
{
class: 'form-popup-picker-like',
closeOnClickOverlay: false,
},
pickerLikeContent,
).open({
values: {
receiver: '张三',
phone: '13800000000',
},
})
latestResult.value = JSON.stringify(result, null, 2)
await showDemoResult(result, 'FormPopup Picker 风格头部')
}
</script>
<template>
<div class="demo-block">
<VanButton block type="primary" @click="handleOpen">
打开 Picker 风格 FormPopup
</VanButton>
<pre class="demo-result">{{ latestResult }}</pre>
</div>
</template>
<style scoped>
.demo-block {
display: grid;
gap: 12px;
}
.demo-result {
margin: 0;
overflow: auto;
border-radius: 12px;
padding: 12px;
background: var(--van-background-2);
color: var(--van-text-color-2);
font-size: 12px;
line-height: 1.6;
white-space: pre-wrap;
}
:global(.form-popup-demo-fields) {
display: grid;
}
:global(.form-popup-picker-like .silver-formily-vant-form-popup__header) {
padding: 0;
}
:global(.form-popup-picker-like .silver-formily-vant-form-popup__body) {
padding-top: 12px;
}
:global(.form-popup-picker-like .silver-formily-vant-form-popup__footer) {
display: none;
}
:global(.form-popup-picker-like .van-picker__cancel:disabled),
:global(.form-popup-picker-like .van-picker__confirm:disabled) {
opacity: 0.45;
}
</style>API
FormPopup
ts
const popup = FormPopup<TValues, ['save-draft']>(
popupProps,
{
header: ({ form, resolve, reject }) => VNode,
default: ({ form, resolve, reject }) => VNode,
footer: ({ form, resolve, reject, saveDraft }) => VNode,
},
['save-draft'] as const,
)
const result = await popup
.forOpen(formProps => formProps)
.forConfirm(form => form.values)
.forCancel(form => form)
.forSaveDraft(form => ({ type: 'draft', values: form.values }))
.open({
values: initialValues,
})使用约定
FormPopup打开时会基于open()参数创建新的form实例- 默认确认结果是
toJS(form.values);如果forConfirm或动态 middleware 返回非undefined,则以 middleware 返回值为准 - 传入
dynamicMiddlewareNames后,会在作用域插槽参数上额外注入同名 camelCase 方法,例如saveDraft() - 默认底部会渲染“取消 / 确定”按钮,并让确定按钮跟随
form.submitting自动进入 loading - 默认
closeOnClickOverlay是false,避免移动端误触导致表单内容丢失
构造参数
| 参数名 | 类型 | 说明 |
|---|---|---|
popupProps | FormPopupProps | string | 传字符串时会自动映射成 { title } |
content | Component | slots | 表单内容;支持 header / default / footer 作用域插槽 |
dynamicMiddlewareNames | readonly string[] | 动态链路名称,例如 ['save-draft'] as const |
FormPopupProps 补充属性
除了官方 Popup 属性外,FormPopup 还固定支持以下壳层配置:
| 属性名 | 类型 | 说明 | 默认值 |
|---|---|---|---|
title | string | 默认头部标题 | - |
cancelText | string | 默认取消按钮文案 | '取消' |
okText | string | 默认确认按钮文案 | '确定' |
cancelButtonProps | Partial<ButtonProps> | 默认取消按钮属性透传 | - |
okButtonProps | Partial<ButtonProps> | 默认确认按钮属性透传 | - |
作用域插槽参数
form:Form<TValues>,当前表单实例resolve:(type?: string) => void,提交当前表单并进入确认链路reject:() => void,取消并进入 cancel 链路saveDraft等:() => void,传入dynamicMiddlewareNames后自动注入的动态动作
实例方法
open:(props?: IFormProps<TValues>) => Promise<any>,打开弹层并创建表单close:() => void,从外部关闭弹层,行为等同于取消forOpen:(middleware) => popup,打开前调整createForm参数forConfirm:(middleware) => popup,改写默认确认返回值forCancel:(middleware) => popup,处理取消链路副作用forXxx:(middleware) => popup,处理saveDraft()等动态动作命中的链路
默认 Popup 配置
| 属性名 | 默认值 |
|---|---|
position | 'bottom' |
round | true |
overlay | true |
lockScroll | true |
lazyRender | true |
closeOnPopstate | true |
closeOnClickOverlay | false |
safeAreaInsetBottom | true |