createPopup
createPopup是一个轻量的 VantPopup工厂函数。
createPopup 函数是当前封装库内所有弹出式选择器的统一抽象,和 FormPopup 不同,他返回的是一个值,而不是一个表单,没有 formily 相关的逻辑。但是使用的心智(函数入参的设计)还是靠近 FormPopup 的。
这里选择将这个函数暴露出来是因为自定义封装的业务组件也有很高的概率需要使用弹出层,在封装自定义组件时只要 emit 了 confirm 或者 finish 事件(因为 Vant 的组件有这两种)就能很方便的将弹出式组件接入表单。用户可以不用再关心显示隐藏的值绑定以及 modelValue 的值绑定,只需要关心这个函数返回的 Promise 的 resolve 的值即可。若用户取消、点击遮罩关闭或 popup 主动关闭,则统一 reject(new Error('cancel'))。
open 不只支持传静态 props 对象,也支持传 reactive、ref、computed 或 getter。弹层打开期间,如果外部 props source 发生变化,createPopup 会自动把最新 props 同步给当前内容组件;同时,当前会话里通过 update:modelValue 产生的临时值会被保留,不会被后续的外部 source 更新直接冲掉。
注意
open方法的入参。有些props或者事件是内部保留的,无法修改或覆盖:show、onUpdate:show、onConfirm、onFinish、onCancel、onUpdate:modelValue
最小自定义组件
下面的示例会复用同一个内容组件,分别演示 closeOnClickOverlay、style.height 和 lockScroll 这些 Popup 壳层参数的传入方式。
<script setup lang="tsx">
import type { PropType } from 'vue'
import { createPopup } from '@silver-formily/vant'
import { Button as VanButton } from 'vant'
import { defineComponent } from 'vue'
import { showDemoResult } from '../shared'
const ConfirmValueCard = defineComponent({
name: 'FunctionalPopupBasicDemoContent',
props: {
modelValue: {
type: Number as PropType<number>,
default: 3,
},
},
emits: ['confirm', 'cancel', 'update:modelValue'],
setup(props, { emit }) {
return () => (
<div class="functional-popup-demo-card">
<div class="functional-popup-demo-card__title">
当前值:
{props.modelValue}
</div>
<div class="functional-popup-demo-card__desc">
这里会在组件内部不断更新 modelValue,但外部最终只会拿到 confirm 返回的 payload。
</div>
<div class="functional-popup-demo-card__actions">
<VanButton
block
type="primary"
onClick={() => emit('update:modelValue', Number(props.modelValue ?? 0) + 1)}
>
内部 +1
</VanButton>
<VanButton
block
type="success"
onClick={() => emit('confirm', { value: props.modelValue })}
>
确认并返回当前值
</VanButton>
<VanButton
block
plain
type="default"
onClick={() => emit('cancel')}
>
取消
</VanButton>
</div>
</div>
)
},
})
const defaultPopup = createPopup(
{
closeOnClickOverlay: false,
},
ConfirmValueCard,
)
const customHeightPopup = createPopup(
{
style: {
height: '60vh',
},
},
ConfirmValueCard,
)
const unlockScrollPopup = createPopup(
{
lockScroll: false,
},
ConfirmValueCard,
)
async function handleOpenDefault() {
try {
const result = await defaultPopup.open({
modelValue: 3,
})
await showDemoResult(result, '默认示例:禁止点击遮罩关闭')
}
catch {
}
}
async function handleOpenCustomHeight() {
try {
const result = await customHeightPopup.open({
modelValue: 5,
})
await showDemoResult(result, '自定义高度:style.height = 60vh')
}
catch {
}
}
async function handleOpenUnlockScroll() {
try {
const result = await unlockScrollPopup.open({
modelValue: 8,
})
await showDemoResult(result, '允许背景滚动:lockScroll = false')
}
catch {
}
}
</script>
<template>
<div class="demo-block">
<div class="demo-block__desc">
同一个内容组件可以配不同的 `popupProps`,这里分别演示 `closeOnClickOverlay`、`style.height` 和 `lockScroll`。
</div>
<VanButton block type="primary" @click="handleOpenDefault">
禁止点击遮罩关闭
</VanButton>
<VanButton block type="success" @click="handleOpenCustomHeight">
自定义弹层高度
</VanButton>
<VanButton block plain type="warning" @click="handleOpenUnlockScroll">
允许背景滚动
</VanButton>
</div>
</template>
<style scoped>
.demo-block {
display: grid;
gap: 12px;
}
.demo-block__desc {
color: var(--van-text-color-2);
font-size: 13px;
line-height: 1.6;
}
:global(.functional-popup-demo-card) {
display: grid;
gap: 12px;
padding: 16px;
min-height: 220px;
box-sizing: border-box;
}
:global(.functional-popup-demo-card__title) {
color: var(--van-text-color);
font-size: 16px;
font-weight: 600;
}
:global(.functional-popup-demo-card__desc) {
color: var(--van-text-color-2);
font-size: 13px;
line-height: 1.6;
}
:global(.functional-popup-demo-card__actions) {
display: grid;
gap: 8px;
}
</style>包装官方 Picker
<script setup lang="ts">
import { createPopup } from '@silver-formily/vant'
import { Button as VanButton, Picker as VanPicker } from 'vant'
import { h } from 'vue'
import { showDemoResult } from '../shared'
const cityColumns = [
{ text: '杭州', value: 'hz' },
{ text: '宁波', value: 'nb' },
{ text: '苏州', value: 'sz' },
]
const pickerPopup = createPopup(
{},
VanPicker,
{
title: () => h('span', { class: 'functional-popup-picker-title' }, '自定义标题插槽'),
},
)
async function handleOpen() {
try {
const result = await pickerPopup.open({
columns: cityColumns,
modelValue: ['hz'],
title: '选择城市',
})
await showDemoResult(result, 'VanPicker confirm payload')
}
catch {
}
}
</script>
<template>
<div class="demo-block">
<VanButton block type="primary" @click="handleOpen">
打开官方 Picker
</VanButton>
</div>
</template>
<style scoped>
.demo-block {
display: grid;
}
</style>包装官方 DatePicker
<script setup lang="ts">
import { createPopup } from '@silver-formily/vant'
import { Button as VanButton, DatePicker as VanDatePicker } from 'vant'
import { h } from 'vue'
import { showDemoResult } from '../shared'
const minDate = new Date(2025, 0, 1)
const maxDate = new Date(2027, 11, 31)
const datePickerPopup = createPopup(
{},
VanDatePicker,
{
title: () => h('span', '自定义日期标题'),
},
)
async function handleOpen() {
try {
const result = await datePickerPopup.open({
minDate,
maxDate,
modelValue: ['2026', '03', '20'],
title: '选择日期',
})
await showDemoResult(result, 'VanDatePicker confirm payload')
}
catch {
}
}
</script>
<template>
<div class="demo-block">
<VanButton block type="primary" @click="handleOpen">
打开官方 DatePicker
</VanButton>
</div>
</template>
<style scoped>
.demo-block {
display: grid;
}
</style>弹出式分步表单
如果内容组件本身就是一个自定义表单,createPopup 也不限制内部结构。下面的例子把 FormStep 放进弹层内容里,由 createPopup 负责壳层开关,由 FormStep 负责步骤切换和最终 confirm 返回值。
<script setup lang="tsx">
import type { PropType } from 'vue'
import { createForm } from '@silver-formily/core'
import { createPopup, Form, FormButtonGroup, FormItem, FormStep, Input, Submit } from '@silver-formily/vant'
import { createSchemaField, FormConsumer } from '@silver-formily/vue'
import { Button as VanButton } from 'vant'
import { defineComponent } from 'vue'
import { showDemoResult } from '../shared'
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: {
FormItem,
FormStep,
Input,
},
})
const stepSchema = {
type: 'object',
properties: {
stepper: {
'type': 'void',
'x-component': 'FormStep',
'x-component-props': {
formStep: '{{formStep}}',
activeColor: '#1989fa',
},
'properties': {
contact: {
'type': 'void',
'x-component': 'FormStep.StepPane',
'x-component-props': {
title: '联系信息',
},
'properties': {
name: {
'type': 'string',
'title': '姓名',
'required': true,
'x-decorator': 'FormItem',
'x-component': 'Input',
'x-component-props': {
placeholder: '请输入联系人姓名',
},
},
mobile: {
'type': 'string',
'title': '手机号',
'required': true,
'x-decorator': 'FormItem',
'x-component': 'Input',
'x-component-props': {
type: 'tel',
placeholder: '请输入手机号',
},
},
},
},
delivery: {
'type': 'void',
'x-component': 'FormStep.StepPane',
'x-component-props': {
title: '配送信息',
},
'properties': {
address: {
'type': 'string',
'title': '收货地址',
'required': true,
'x-decorator': 'FormItem',
'x-component': 'Input.TextArea',
'x-component-props': {
rows: 3,
placeholder: '请输入街道、楼栋、门牌号',
},
},
deliveryTime: {
'type': 'string',
'title': '送达时间',
'x-decorator': 'FormItem',
'x-component': 'Input',
'x-component-props': {
placeholder: '例如工作日晚上 7 点后',
},
},
},
},
confirm: {
'type': 'void',
'x-component': 'FormStep.StepPane',
'x-component-props': {
title: '确认提交',
},
'properties': {
remark: {
'type': 'string',
'title': '备注',
'x-decorator': 'FormItem',
'x-component': 'Input.TextArea',
'x-component-props': {
rows: 2,
placeholder: '可选,补充门禁、楼层或电话备注',
},
},
},
},
},
},
},
}
const StepFormPopupContent = defineComponent({
name: 'CreatePopupFormStepDemoContent',
props: {
values: {
type: Object as PropType<StepFormValues>,
default: () => defaultValues,
},
},
emits: ['confirm', 'cancel'],
setup(props, { emit }) {
const form = createForm({
values: {
...defaultValues,
...props.values,
},
})
const formStep = FormStep.createFormStep()
async function handleSubmit(values: typeof form.values) {
emit('confirm', values)
}
return () => (
<div class="step-form-popup-shell">
<Form form={form} class="step-form-popup-layout">
<div class="step-form-popup-header">
<div class="step-form-popup-card">
<div class="step-form-popup-card__title">弹出式分步表单</div>
<div class="step-form-popup-card__desc">
createPopup 只负责弹层开关,步骤切换和最终提交仍然由 FormStep 管理。
</div>
</div>
</div>
<div class="step-form-popup-main">
<SchemaField schema={stepSchema} scope={{ formStep }} />
</div>
<FormConsumer>
{() => (
<div class="step-form-popup-actions">
<FormButtonGroup layout="horizontal">
{formStep.allowBack
? (
<VanButton plain type="default" onClick={() => formStep.back()}>
上一步
</VanButton>
)
: (
<VanButton plain type="default" onClick={() => emit('cancel')}>
取消
</VanButton>
)}
{formStep.allowNext
? (
<VanButton type="primary" onClick={() => formStep.next()}>
下一步
</VanButton>
)
: (
<Submit onSubmit={handleSubmit}>
确认提交
</Submit>
)}
</FormButtonGroup>
</div>
)}
</FormConsumer>
</Form>
</div>
)
},
})
const stepFormPopup = createPopup<typeof StepFormPopupContent, StepFormValues>(
{
closeOnClickOverlay: false,
style: {
minHeight: '72vh',
},
},
StepFormPopupContent,
)
async function handleOpen() {
try {
const result = await stepFormPopup.open({
values: defaultValues,
})
await showDemoResult(result, 'createPopup + FormStep 提交结果')
}
catch {
}
}
</script>
<template>
<div class="demo-block">
<div class="demo-block__desc">
通过自定义内容组件把 FormStep 放进 createPopup,适合资料补录、地址编辑这类需要分步完成的移动端弹层流程。
</div>
<VanButton block type="primary" @click="handleOpen">
打开弹出式分步表单
</VanButton>
</div>
</template>
<style scoped>
.demo-block {
display: grid;
gap: 12px;
}
.demo-block__desc {
color: var(--van-text-color-2);
font-size: 13px;
line-height: 1.6;
}
:global(.step-form-popup-card) {
padding: 12px;
border-radius: 12px;
background: rgba(25, 137, 250, 0.08);
}
:global(.step-form-popup-shell) {
min-height: 72vh;
}
:global(.step-form-popup-layout) {
display: flex;
flex-direction: column;
min-height: 72vh;
box-sizing: border-box;
}
:global(.step-form-popup-header) {
padding: 16px 12px 0;
}
:global(.step-form-popup-main) {
flex: 1;
min-height: 0;
padding-top: 12px;
}
:global(.step-form-popup-actions) {
margin-top: auto;
padding: 16px 12px calc(12px + env(safe-area-inset-bottom));
background: linear-gradient(180deg, rgba(255, 255, 255, 0) 0%, var(--van-background-2) 100%);
}
:global(.step-form-popup-actions .van-formily-form-button-group) {
margin: 0;
}
:global(.step-form-popup-actions .van-formily-form-button-group--horizontal) {
margin-top: 0;
}
:global(.step-form-popup-actions .van-button) {
min-height: 44px;
}
:global(.step-form-popup-card__title) {
color: var(--van-text-color);
font-size: 15px;
font-weight: 600;
}
:global(.step-form-popup-card__desc) {
margin-top: 6px;
color: #1989fa;
font-size: 12px;
line-height: 1.6;
}
</style>API
createPopup
const popup = createPopup<TComponent, TResult>(
popupProps,
component,
slots,
)
const result = await popup.open(componentProps)构造参数
| 参数名 | 类型 | 说明 |
|---|---|---|
popupProps | Partial<PopupProps> | Popup 壳层配置 |
component | Component | 要包进 popup 的 Vue 组件 |
slots | Record<string, Slot> | 透传给组件的插槽对象 |
返回值
popup.open(componentProps?) => Promise<TResult>:打开弹层,componentProps支持静态对象、reactive、ref、computed或 getterpopup.close(reason?):主动关闭当前弹层,默认以Error('cancel')作为 reject 原因
Props Source
open 的入参支持以下几种形式:
- 静态对象:适合一次性打开、不需要会话内同步外部状态的场景
reactive/ref/computed/ getter:适合弹层打开后,options、loading、title等仍可能继续变化的场景
如果内容组件会在会话中通过 emit('update:modelValue', value) 更新临时值,createPopup 会把这部分值视为“会话态”,优先保留当前用户操作结果,再叠加外部最新 props source。
默认 Popup 配置
| 属性名 | 默认值 |
|---|---|
position | 'bottom' |
round | true |
overlay | true |
lockScroll | true |
lazyRender | true |
closeOnPopstate | true |
closeOnClickOverlay | true |
safeAreaInsetBottom | true |
与 FormPopup 的区别
createPopup:只是一层简单的函数式封装,监听confirm(payload)或finish(payload)事件的载荷。一般用在表单项的封装上。FormPopup:会创建form、提供默认按钮区,并支持 middleware 与动态动作链路。