FormDrawer
Drawer form, mainly used when a form is opened by a simple event trigger.
Note
This component has been refactored and no longer passes context by id. Please pay attention to the function signature changes. Similar capabilities are now implemented with Vue's JSX slot syntax.
Tip
When using function components, you can quickly access form through destructuring. See the template example for details.
Markup Schema Example
<script setup lang="tsx">
import { FormDrawer, 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,
},
})
// Drawer form component
const DrawerForm = {
props: ['form'],
data() {
const schema = {
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',
},
},
}
return {
schema,
}
},
render() {
return (
<FormLayout labelCol={6} wrapperCol={10}>
<SchemaField schema={this.schema} />
</FormLayout>
)
},
}
function handleOpen() {
FormDrawer('Drawer Form', DrawerForm)
.forOpen((props, next) => {
setTimeout(() => {
next()
}, 1000)
})
.open({
initialValues: {
aaa: '123',
},
})
.then(console.log)
}
</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 { FormDrawer, 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 drawerSchema: 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 DrawerForm() {
return (
<FormLayout labelCol={6} wrapperCol={10}>
<SchemaField schema={drawerSchema} />
</FormLayout>
)
}
function handleOpen() {
FormDrawer('Drawer Form', DrawerForm)
.open({
initialValues: {
aaa: '123',
},
})
.then((values) => {
console.log('values', values)
})
.catch((error) => {
console.error(error)
})
}
</script>
<template>
<ElButton @click="handleOpen">
Open Form
</ElButton>
</template>Template Example
<script setup lang="tsx">
import { FormDrawer, FormItem, FormLayout, Input } from '@silver-formily/element-plus'
import { Field } from '@silver-formily/vue'
import { ElButton } from 'element-plus'
function handleOpen() {
FormDrawer('Drawer 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>
)
})
.forConfirm(async (form, next) => {
setTimeout(() => {
console.log('form', form)
next()
}, 1000)
})
.open({
initialValues: {
aaa: '123',
},
})
.then((values) => {
console.log('values', values)
})
.catch((error) => {
console.log(error)
})
}
</script>
<template>
<ElButton @click="handleOpen">
Open Form
</ElButton>
</template>Template Slot Example
<script setup lang="tsx">
import { FormDrawer, FormItem, FormLayout, Input } from '@silver-formily/element-plus'
import { Field } from '@silver-formily/vue'
import { ElButton } from 'element-plus'
function handleOpen() {
FormDrawer('Drawer 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
FormDrawer now supports generics for both form value types and dynamic middleware names. The two most common usage 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
}
FormDrawer<UserFormValues>('Edit User', ({ form }) => {
form.values.name
form.values.age
return <UserForm />
})
FormDrawer<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
FormDrawer also listens for the Enter key inside inputs and calls resolve by default. If the drawer contains custom shortcuts or nested popup layers, you can disable that behavior with enterSubmit: false.
FormDrawer also closes automatically when the browser URL changes, including Back, Forward, and application-side pushState / replaceState. If you want the drawer to stay open across route changes, explicitly set closeOnUrlChange: false.
<script setup lang="tsx">
import { FormDrawer, 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="email"
required
title="Email"
decorator={[FormItem]}
component={[Input, { placeholder: 'Press Enter after typing' }]}
/>
</FormLayout>
)
}
function openDrawer({ title, enterSubmit }: { title: string, enterSubmit?: boolean }) {
FormDrawer({ title, enterSubmit }, renderForm)
.forConfirm((form, next) => {
console.log('submit', form.values)
next()
})
.open()
.catch(console.warn)
}
function handleDefault() {
openDrawer({ title: 'Submit on Enter enabled by default' })
}
function handleDisabled() {
openDrawer({ 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>API
FormDrawer Function Arguments
| Parameter | Description | Type |
|---|---|---|
title or formDrawerProps | Drawer title or Drawer props | string FormDrawerProps |
formDrawerSlots | Drawer content, supporting components, VNodes, and slot-style authoring | Component VNode[] () => VNode[] FormDrawerSlots |
dynamicMiddlewareNames | List of dynamic middleware names. They are converted to camelCase on usage. | string[] except cancel, confirm, open |
Note
formDrawerProps has reserved fields. Passing modelValue or onUpdate:modelValue has no effect because they are already used internally by FormDrawer.
Complete function type declaration:
interface FormDrawer {
<TValues extends object = any, DynamicMiddlewareNames extends readonly string[] = []>(
title: IFormDrawerProps | string,
content?: Component | FormDrawerSlotContent<TValues, DynamicMiddlewareNames[number]>,
dynamicMiddlewareNames?: DynamicMiddlewareNames
): IFormDrawer<TValues, DynamicMiddlewareNames[number]>
}title
The first argument. When a string is passed, it is displayed as the drawer title. You can also pass IFormDrawerProps for customization. Prefer middleware such as forOpen, forConfirm, and forCancel to control the drawer 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 drawer closes automatically on URL change | boolean | true |
For the rest, see https://element-plus.org/en-US/component/drawer.html
content
The second argument. In addition to components and VNodes, it can also accept Vue's JSX slot syntax to customize header and footer.
| Slot | Description | Type |
|---|---|---|
default | Main drawer content. Supports components, VNodes, and scoped-slot style content. Injects form, resolve, and reject. | FormDrawerSlotProps |
header | Header slot. Scoped content can call resolve or reject to close the drawer. resolve can receive names from dynamicMiddlewareNames. | FormDrawerSlotProps |
footer | Footer slot. Scoped content can call resolve or reject to close the drawer. resolve can receive names from dynamicMiddlewareNames. | FormDrawerSlotProps |
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 drawer, pass 'saveDraft' in dynamicMiddlewareNames, then bind resolve('saveDraft') to a button inside footer.
After that, you can add 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
IFormDrawer Return Value
The return value is a Promise object, so you can await it to simplify flow control. You need to call open to display the drawer. Chain calls can be used to handle different lifecycle events. Dynamic middleware actions are also supported through dynamicMiddlewareNames.
| Method | Description | Type |
|---|---|---|
open | Open drawer | (IFormProps) => Promise<IFormProps.values> |
forOpen | Drawer open hook | (IMiddleware<IFormProps>) => IFormDrawer |
forConfirm | Confirm hook | (IMiddleware<Form>) => IFormDrawer |
forCancel | Cancel hook | (IMiddleware<Form>) => IFormDrawer |
for${Dynamic} | Custom hook | (IMiddleware<Form>) => IFormDrawer |
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
Drawers closed without resolve are now thrown as errors. That means in async/await flows, any logic after await FormDrawer(...) only runs after a successful form submission.
Type Declarations
IFormDrawerProps
export type IFormDrawerProps = Partial<DrawerProps> & {
cancelText?: string
cancelButtonProps?: ButtonProps
okText?: string
okButtonProps?: ButtonProps
loadingText?: string
enterSubmit?: boolean
}FormDrawerSlots
export interface FormDrawerResolve {
(type?: string): void
}
interface FormDrawerBaseSlotProps<T extends object = any> {
resolve: FormDrawerResolve
reject: () => void
form: Form<T>
}
export type FormDrawerSlotProps<T extends object = any> = FormDrawerBaseSlotProps<T> & Record<string, any>
export interface FormDrawerSlots<T extends object = any, _DynamicMiddlewareName extends string = never> {
header?: (props: FormDrawerSlotProps<T>) => VNode | VNode[]
default?: (props: FormDrawerSlotProps<T>) => VNode | VNode[]
footer?: (props: FormDrawerSlotProps<T>) => VNode | VNode[]
}IFormDrawer
type ReservedFormDrawerMiddlewareName = 'open' | 'confirm' | 'cancel'
type ReservedFormDrawerMiddlewareMethodName = `for${Capitalize<ReservedFormDrawerMiddlewareName>}`
type NormalizeFormDrawerDynamicMiddlewareName<T extends string> = string extends T
? string
: T extends `${infer Head}-${infer Tail}`
? `${Lowercase<Head>}${Capitalize<NormalizeFormDrawerDynamicMiddlewareName<Tail>>}`
: T extends `${infer Head}_${infer Tail}`
? `${Lowercase<Head>}${Capitalize<NormalizeFormDrawerDynamicMiddlewareName<Tail>>}`
: T extends `${infer Head} ${infer Tail}`
? `${Lowercase<Head>}${Capitalize<NormalizeFormDrawerDynamicMiddlewareName<Tail>>}`
: T
type FormDrawerDynamicMiddlewareMethodName<T extends string> = `for${Capitalize<NormalizeFormDrawerDynamicMiddlewareName<T>>}`
type FormDrawerDynamicMiddlewareMethods<T extends object, DynamicMiddlewareName extends string> = {
[K in FormDrawerDynamicMiddlewareMethodName<DynamicMiddlewareName> as K extends ReservedFormDrawerMiddlewareMethodName ? never : K]: (middleware: IMiddleware<Form<T>>) => IFormDrawer<T, DynamicMiddlewareName>
}
interface IFormDrawerBase<T extends object = any, DynamicMiddlewareName extends string = never> {
forOpen: (middleware: IMiddleware<IFormProps<T>>) => IFormDrawer<T, DynamicMiddlewareName>
forConfirm: (middleware: IMiddleware<Form<T>>) => IFormDrawer<T, DynamicMiddlewareName>
forCancel: (middleware: IMiddleware<Form<T>>) => IFormDrawer<T, DynamicMiddlewareName>
open: (props?: IFormProps<T>) => Promise<any>
close: () => void
}
export type IFormDrawer<T extends object = any, DynamicMiddlewareName extends string = never>
= IFormDrawerBase<T, DynamicMiddlewareName> & FormDrawerDynamicMiddlewareMethods<T, DynamicMiddlewareName>