FormDialog
Dialog-based form component, mainly used when a form is opened from a simple event trigger.
Note
This component has been refactored and no longer passes context by id. Pay close attention to the function signature changes. Similar capabilities are now implemented with Vue's JSX slot syntax.
Tip
When using function components, you can access form conveniently through destructuring. See the template example for details.
Markup Schema Example
<script setup lang="tsx">
import { FormDialog, FormItem, FormLayout, Input } from '@silver-formily/element-plus'
import { createSchemaField } from '@silver-formily/vue'
import { ElButton } from 'element-plus'
const { SchemaField, SchemaStringField } = createSchemaField({
components: {
FormItem,
Input,
},
})
// Dialog form component
const DialogForm = {
props: ['form'],
render() {
return (
<FormLayout labelCol={6} wrapperCol={10}>
<SchemaField>
<SchemaStringField
name="aaa"
required
title="Input 1"
x-decorator="FormItem"
x-component="Input"
/>
<SchemaStringField
name="bbb"
required
title="Input 2"
x-decorator="FormItem"
x-component="Input"
/>
<SchemaStringField
name="ccc"
required
title="Input 3"
x-decorator="FormItem"
x-component="Input"
/>
<SchemaStringField
name="ddd"
required
title="Input 4"
x-decorator="FormItem"
x-component="Input"
/>
</SchemaField>
</FormLayout>
)
},
}
function handleOpen() {
FormDialog('Dialog Form', DialogForm)
.forOpen((payload, next) => {
setTimeout(() => {
next({
initialValues: {
aaa: '123',
},
})
}, 1000)
})
.forConfirm((payload, next) => {
setTimeout(() => {
console.log(payload)
next(payload)
}, 1000)
})
.forCancel((payload, next) => {
setTimeout(() => {
console.log(payload)
next(payload)
}, 1000)
})
.open()
.then(console.log)
.catch(console.error)
}
</script>
<template>
<ElButton @click="handleOpen">
Open Form
</ElButton>
</template>JSON Schema Example
<script setup lang="tsx">
import type { ISchema } from '@silver-formily/json-schema'
import { FormDialog, FormItem, FormLayout, Input } from '@silver-formily/element-plus'
import { createSchemaField } from '@silver-formily/vue'
import { ElButton } from 'element-plus'
const { SchemaField } = createSchemaField({
components: {
FormItem,
Input,
},
})
const dialogSchema: ISchema = {
type: 'object',
properties: {
aaa: {
'type': 'string',
'title': 'Input 1',
'required': true,
'x-decorator': 'FormItem',
'x-component': 'Input',
},
bbb: {
'type': 'string',
'title': 'Input 2',
'required': true,
'x-decorator': 'FormItem',
'x-component': 'Input',
},
ccc: {
'type': 'string',
'title': 'Input 3',
'required': true,
'x-decorator': 'FormItem',
'x-component': 'Input',
},
ddd: {
'type': 'string',
'title': 'Input 4',
'required': true,
'x-decorator': 'FormItem',
'x-component': 'Input',
},
},
}
function DialogForm() {
return (
<FormLayout labelCol={6} wrapperCol={10}>
<SchemaField schema={dialogSchema} />
</FormLayout>
)
}
function handleOpen() {
FormDialog('Dialog Form', DialogForm)
.forOpen((payload, next) => {
setTimeout(() => {
next({
initialValues: {
aaa: '123',
},
})
}, 1000)
})
.forConfirm((payload, next) => {
setTimeout(() => {
console.log(payload)
next(payload)
}, 1000)
})
.forCancel((payload, next) => {
setTimeout(() => {
console.log(payload)
next(payload)
}, 1000)
})
.open()
.then(console.log)
.catch(console.error)
}
</script>
<template>
<ElButton @click="handleOpen">
Open Form
</ElButton>
</template>Template Example
<script setup lang="tsx">
import { FormDialog, FormItem, FormLayout, Input } from '@silver-formily/element-plus'
import { Field } from '@silver-formily/vue'
import { ElButton } from 'element-plus'
function handleOpen() {
FormDialog('Dialog Form', ({ form }) => {
console.log('form', form)
return (
<FormLayout labelCol={6} wrapperCol={10}>
<Field
name="aaa"
required
title="Input 1"
decorator={[FormItem]}
component={[Input]}
/>
<Field
name="bbb"
required
title="Input 2"
decorator={[FormItem]}
component={[Input]}
/>
<Field
name="ccc"
required
title="Input 3"
decorator={[FormItem]}
component={[Input]}
/>
<Field
name="ddd"
required
title="Input 4"
decorator={[FormItem]}
component={[Input]}
/>
</FormLayout>
)
})
.forOpen((payload, next) => {
setTimeout(() => {
next({
initialValues: {
aaa: '123',
},
})
}, 1000)
})
.forConfirm((payload, next) => {
setTimeout(() => {
console.log(payload)
next(payload)
}, 1000)
})
.forCancel((payload, next) => {
setTimeout(() => {
console.log(payload)
next(payload)
}, 1000)
})
.open()
.then(console.log)
.catch(console.error)
}
</script>
<template>
<ElButton @click="handleOpen">
Open Form
</ElButton>
</template>Template Slot Example
<script setup lang="tsx">
import { FormDialog, FormItem, FormLayout, Input } from '@silver-formily/element-plus'
import { Field } from '@silver-formily/vue'
import { ElButton } from 'element-plus'
function handleOpen() {
FormDialog('Dialog Form', {
header: ({ reject }) => (
<div>
<ElButton onClick={() => reject()}>Close</ElButton>
<span>This is the title</span>
</div>
),
default: () => (
<FormLayout labelCol={6} wrapperCol={10}>
<Field
name="aaa"
required
title="Input 1"
decorator={[FormItem]}
component={[Input]}
/>
<Field
name="bbb"
required
title="Input 2"
decorator={[FormItem]}
component={[Input]}
/>
<Field
name="ccc"
required
title="Input 3"
decorator={[FormItem]}
component={[Input]}
/>
<Field
name="ddd"
required
title="Input 4"
decorator={[FormItem]}
component={[Input]}
/>
</FormLayout>
),
footer: ({ form, resolve, reject }) => {
return [
<ElButton
onClick={() => reject()}
>
Cancel
</ElButton>,
<ElButton loading={form.submitting} onClick={() => resolve('extra')}>extra</ElButton>,
<ElButton loading={form.submitting} onClick={() => resolve('saveDraft')}>Save Draft</ElButton>,
<ElButton
type="primary"
loading={form.submitting}
onClick={() => resolve()}
>
Confirm
</ElButton>,
]
},
}, ['extra', 'saveDraft'])
.forOpen((payload, next) => {
next({
initialValues: {
aaa: '123',
},
})
})
.forConfirm((payload, next) => {
setTimeout(() => {
next(payload)
}, 1000)
})
.forExtra((payload, next) => {
setTimeout(() => {
console.log('extra')
next(payload)
}, 1000)
})
.forSaveDraft((payload, next) => {
setTimeout(() => {
console.log('saveDraft')
next(payload)
}, 1000)
})
.forCancel((payload, next) => {
setTimeout(() => {
next(payload)
}, 1000)
})
.open()
.then(console.log)
.catch(console.error)
}
</script>
<template>
<ElButton @click="handleOpen">
Open Form
</ElButton>
</template>Using Generics
FormDialog now supports generics for both form value types and dynamic middleware names. The two most common patterns are:
- Declare only the form value type so
form.values,open({ values }), andforConfirmall get precise typing. - Declare dynamic middleware names as well so methods such as
forSaveDraftget type hints, and pair them withresolve('saveDraft')to trigger the corresponding logic.
type UserFormValues = {
name: string
age: number
}
FormDialog<UserFormValues>('Edit User', ({ form }) => {
form.values.name
form.values.age
return <UserForm />
})
FormDialog<UserFormValues, ['save-draft']>(
'Edit User',
{
footer: ({ resolve, reject, form }) => {
form.values.name
resolve('saveDraft')
resolve()
reject()
return []
},
},
['save-draft'] as const,
)
.forSaveDraft((form) => {
return form.values
})Tip
If you pass dynamicMiddlewareNames, prefer a readonly literal such as ['save-draft'] as const so return methods like forSaveDraft can be inferred correctly.
Enter-to-Submit Configuration
By default, FormDialog listens for the Enter key inside active inputs and triggers resolve. If you need to disable that behavior, pass enterSubmit: false.
FormDialog also closes automatically when the browser URL changes, including Back, Forward, and application-side pushState / replaceState. If you want the dialog to stay open across route changes, set closeOnUrlChange: false explicitly.
<script setup lang="tsx">
import { FormDialog, FormItem, FormLayout, Input } from '@silver-formily/element-plus'
import { Field } from '@silver-formily/vue'
import { ElButton, ElSpace } from 'element-plus'
function renderForm() {
return (
<FormLayout labelCol={6} wrapperCol={12} layout="vertical">
<Field
name="user"
required
title="Username"
decorator={[FormItem]}
component={[Input, { placeholder: 'Press Enter to try' }]}
/>
</FormLayout>
)
}
function openDialog({ title, enterSubmit }: { title: string, enterSubmit?: boolean }) {
FormDialog({ title, enterSubmit }, renderForm)
.forConfirm((form, next) => {
console.log('submit', form.values)
next()
})
.open()
.catch(console.warn)
}
function handleDefault() {
openDialog({ title: 'Submit on Enter enabled by default' })
}
function handleDisabled() {
openDialog({ title: 'Disable Enter to submit', enterSubmit: false })
}
</script>
<template>
<ElSpace>
<ElButton @click="handleDefault">
Default Enter Submit
</ElButton>
<ElButton @click="handleDisabled">
Disable Enter to submit
</ElButton>
</ElSpace>
</template>Nested Popup Example
You can call FormDialog again from inside an existing FormDialog to open a nested dialog. The component automatically ensures that only the top-most instance responds to shortcuts and submission.
<script setup lang="tsx">
import { FormDialog, FormItem, FormLayout, Input } from '@silver-formily/element-plus'
import { Field } from '@silver-formily/vue'
import { ElButton } from 'element-plus'
function renderParentForm() {
return (
<FormLayout labelCol={6} wrapperCol={12} layout="vertical">
<Field
name="company"
title="Company Name"
decorator={[FormItem]}
component={[Input, { placeholder: 'Enter company name' }]}
/>
</FormLayout>
)
}
function renderChildForm() {
return (
<FormLayout labelCol={6} wrapperCol={12} layout="vertical">
<Field
name="contact"
title="Contact"
decorator={[FormItem]}
component={[Input, { placeholder: 'Enter contact name' }]}
/>
</FormLayout>
)
}
function openChildDialog() {
FormDialog('Second-level Dialog', renderChildForm)
.forConfirm((form, next) => {
console.log('child submit', form.values)
next()
})
.open()
.catch(console.warn)
}
function handleOpen() {
FormDialog('First-level Dialog', () => (
<div>
{renderParentForm()}
<ElButton class="mt-2" type="primary" onClick={openChildDialog}>
Open the second-level dialog
</ElButton>
</div>
))
.forConfirm((form, next) => {
console.log('parent submit', form.values)
next()
})
.open()
.catch(console.warn)
}
</script>
<template>
<ElButton @click="handleOpen">
Open a form with a nested dialog
</ElButton>
</template>API
FormDialog Function Arguments
| Parameter | Description | Type |
|---|---|---|
title or formDialogProps | Dialog title or Dialog props | string FormDialogProps |
formDialogSlots | Dialog content, supporting components, VNodes, and slot-style authoring | Component VNode[] () => VNode[] FormDialogSlots |
dynamicMiddlewareNames | List of dynamic middleware names. They are normalized to camelCase when used. | string[] except cancel, confirm, open |
Note
formDialogProps has reserved fields. Passing modelValue or onUpdate:modelValue has no effect because they are already used internally by FormDialog.
Complete function type declaration:
interface FormDialog {
<TValues extends object = any, DynamicMiddlewareNames extends readonly string[] = []>(
title: IFormDialogProps | string,
content?: Component | FormDialogSlotContent<TValues, DynamicMiddlewareNames[number]>,
dynamicMiddlewareNames?: DynamicMiddlewareNames
): IFormDialog<TValues, DynamicMiddlewareNames[number]>
}title
The first argument. When a string is passed, it is displayed as the dialog title. You can also pass IFormDialogProps for customization. Prefer middleware such as forOpen, forConfirm, and forCancel when you need to control the dialog lifecycle.
| Parameter | Description | Type | Default |
|---|---|---|---|
cancelText | Cancel button text | string | Cancel |
cancelButtonProps | Props for the cancel button | ButtonProps | - |
okText | Confirm button text | string | Confirm |
okButtonProps | Props for the confirm button | ButtonProps | - |
loadingText | Loading text | string | loading |
enterSubmit | Whether pressing Enter in an input immediately triggers resolve | boolean | true |
closeOnUrlChange | Whether the dialog closes automatically on URL change | boolean | true |
For the rest, see https://element-plus.org/en-US/component/dialog.html
content
The second argument. In addition to components and VNodes, it can also accept Vue's JSX slot syntax for customizing header and footer.
| Slot | Description | Type |
|---|---|---|
default | Main dialog content. Supports components, VNodes, and scoped-slot style content. Injects form, resolve, and reject. | FormDialogSlotProps |
header | Header slot. Scoped content can call resolve or reject to close the dialog. resolve can receive names from dynamicMiddlewareNames. | FormDialogSlotProps |
footer | Footer slot. Scoped content can call resolve or reject to close the dialog. resolve can receive names from dynamicMiddlewareNames. | FormDialogSlotProps |
dynamicMiddlewareNames
The third argument. It is a string array used to trigger custom actions from buttons defined in the header or footer.
For example, if you want to add a save-draft action to the dialog, pass 'saveDraft' in dynamicMiddlewareNames, then bind resolve('saveDraft') to a button inside footer.
After that, you can attach business logic with forSaveDraft, just like forConfirm. See the demo for a complete example.
Tip
Strings passed through dynamicMiddlewareNames are converted to camelCase. For example, 'save-draft' becomes 'saveDraft'.
Tip
When used together with generics, literal values in dynamicMiddlewareNames affect the method-level type hints on the return value, such as:
forSaveDraftforPublishNow
IFormDialog Return Value
The return value is a Promise-like object, so you can await it to simplify flow control. You still need to call open to display the dialog. Chain calls can be used to handle different lifecycle events, and dynamic middleware actions are also supported through dynamicMiddlewareNames.
| Method | Description | Type |
|---|---|---|
open | Open dialog | (IFormProps) => Promise<IFormProps.values> |
forOpen | Dialog open hook | (IMiddleware<IFormProps>) => IFormDialog |
forConfirm | Confirm hook | (IMiddleware<Form>) => IFormDialog |
forCancel | Cancel hook | (IMiddleware<Form>) => IFormDialog |
for${Dynamic} | Custom hook | (IMiddleware<Form>) => IFormDialog |
Tip
In custom hooks, Dynamic corresponds to the values passed into dynamicMiddlewareNames. The related action is triggered by calling resolve inside scoped slots. When methods are generated, names from dynamicMiddlewareNames are converted to PascalCase, so ['save-draft'] becomes forSaveDraft.
Tip
Dialogs that close without calling resolve are now surfaced as errors. In async/await flows, that means any logic after await FormDialog(...) only runs after a successful form submission.
Type Declarations
IFormDialogProps
export type IFormDialogProps = Partial<DialogProps> & {
cancelText?: string
cancelButtonProps?: ButtonProps
okText?: string
okButtonProps?: ButtonProps
loadingText?: string
enterSubmit?: boolean
}FormDialogSlots
export interface FormDialogResolve {
(type?: string): void
}
interface FormDialogBaseSlotProps<T extends object = any> {
resolve: FormDialogResolve
reject: () => void
form: Form<T>
}
export type FormDialogSlotProps<T extends object = any> = FormDialogBaseSlotProps<T> & Record<string, any>
export interface FormDialogSlots<T extends object = any, _DynamicMiddlewareName extends string = never> {
header?: (props: FormDialogSlotProps<T>) => VNode | VNode[]
default?: (props: FormDialogSlotProps<T>) => VNode | VNode[]
footer?: (props: FormDialogSlotProps<T>) => VNode | VNode[]
}IFormDialog
type ReservedFormDialogMiddlewareName = 'open' | 'confirm' | 'cancel'
type ReservedFormDialogMiddlewareMethodName = `for${Capitalize<ReservedFormDialogMiddlewareName>}`
type NormalizeFormDialogDynamicMiddlewareName<T extends string> = string extends T
? string
: T extends `${infer Head}-${infer Tail}`
? `${Lowercase<Head>}${Capitalize<NormalizeFormDialogDynamicMiddlewareName<Tail>>}`
: T extends `${infer Head}_${infer Tail}`
? `${Lowercase<Head>}${Capitalize<NormalizeFormDialogDynamicMiddlewareName<Tail>>}`
: T extends `${infer Head} ${infer Tail}`
? `${Lowercase<Head>}${Capitalize<NormalizeFormDialogDynamicMiddlewareName<Tail>>}`
: T
type FormDialogDynamicMiddlewareMethodName<T extends string> = `for${Capitalize<NormalizeFormDialogDynamicMiddlewareName<T>>}`
type FormDialogDynamicMiddlewareMethods<T extends object, DynamicMiddlewareName extends string> = {
[K in FormDialogDynamicMiddlewareMethodName<DynamicMiddlewareName> as K extends ReservedFormDialogMiddlewareMethodName ? never : K]: (middleware: IMiddleware<Form<T>>) => IFormDialog<T, DynamicMiddlewareName>
}
interface IFormDialogBase<T extends object = any, DynamicMiddlewareName extends string = never> {
forOpen: (middleware: IMiddleware<IFormProps<T>>) => IFormDialog<T, DynamicMiddlewareName>
forConfirm: (middleware: IMiddleware<Form<T>>) => IFormDialog<T, DynamicMiddlewareName>
forCancel: (middleware: IMiddleware<Form<T>>) => IFormDialog<T, DynamicMiddlewareName>
open: (props?: IFormProps<T>) => Promise<any>
close: () => void
}
export type IFormDialog<T extends object = any, DynamicMiddlewareName extends string = never>
= IFormDialogBase<T, DynamicMiddlewareName> & FormDialogDynamicMiddlewareMethods<T, DynamicMiddlewareName>