Skip to content

Commit b2fe439

Browse files
sfxcodeclaude
andcommitted
feat: add useFormKitOverlay composable for promise-based overlay forms
Composes FUDataEdit/FUAutoForm, useFormKitForm, and Nuxt UI's useOverlay into `overlay.edit({ data, schema, title })` / `overlay.auto({ data, ... })`, opening a modal or slideover and resolving to the saved data or null on cancel/Esc/backdrop dismissal - closing the gap between this module's existing pieces and the most common edit-in-a-dialog CRUD pattern. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
1 parent 8a45ddd commit b2fe439

12 files changed

Lines changed: 931 additions & 1 deletion

File tree

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,7 @@ FormKit Nuxt UI bridges the gap between [FormKit](https://formkit.com/)'s powerf
7575
- `useFormKitMultiStep` - Multi-step tab-item mapping and navigation bridging
7676
- `useFormKitForm` - Submit/reset/error-management wrapper for a form's imperative APIs, callable from outside the form
7777
- `useFormKitAutoForm` - Schema inference from data value shapes, Valibot, or Zod schemas (`inferFormSchema`/`inferFormSchemaFromValibot`/`inferFormSchemaFromZod`)
78+
- `useFormKitOverlay` - Promise-based modal/slideover forms: `await overlay.edit({ data, schema, title })` (or `.auto(...)` for a schema-free version), resolving to the saved data or `null` on cancel
7879
- `colorConverter` - Color format conversion utilities
7980
- `durationConverter` - Duration format conversion utilities
8081

docs/.vitepress/config.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,7 @@ export default defineConfig({
111111
{ text: 'FUDataEdit', link: '/components/data-edit' },
112112
{ text: 'FUAutoForm', link: '/components/auto-form' },
113113
{ text: 'Standard Schema Validation', link: '/components/standard-schema' },
114+
{ text: 'useFormKitOverlay', link: '/components/overlay' },
114115
],
115116
},
116117
{

docs/api/utilities.md

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,25 @@ const { submit, reset, isValid, isSubmitted, hasErrors, isLoading } = useFormKit
5454

5555
**Returns:** `submit`, `reset`, `setErrors`, `clearErrors`, `isValid`, `isSubmitted`, `hasErrors`, `isLoading`
5656

57+
### useFormKitOverlay
58+
59+
Opens [`FUDataEdit`](/components/data-edit) or [`FUAutoForm`](/components/auto-form) in a `UModal`/`USlideover` via Nuxt UI's `useOverlay` - see [the full guide](/components/overlay) for details.
60+
61+
```typescript
62+
import { useFormKitOverlay } from '@sfxcode/nuxt-ui-formkit'
63+
64+
const overlay = useFormKitOverlay()
65+
66+
const result = await overlay.edit({ data, schema, title }) // hand-written schema
67+
const inferred = await overlay.auto({ data, title }) // schema inferred via FUAutoForm
68+
```
69+
70+
**Parameters (`edit`):** `data`, `schema`, `title` *(optional)*, `as` *(optional, `'modal' | 'slideover'`, default `'modal'`)*
71+
72+
**Parameters (`auto`):** `data`/`valibotSchema`/`zodSchema`/`overrides` *(optional)*, `title` *(optional)*, `as` *(optional)*
73+
74+
**Returns:** `{ edit, auto }`, each resolving to the saved data or `null` on cancel/dismiss
75+
5776
### useFormKitSchema
5877

5978
Schema builder utilities for creating FormKit schemas programmatically.

docs/components/overlay.md

Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
---
2+
title: useFormKitOverlay Composable
3+
description: Promise-based modal/slideover forms - open FUDataEdit or FUAutoForm in a UModal/USlideover and await the saved data or null on cancel.
4+
---
5+
6+
## Overview
7+
8+
`useFormKitOverlay()` composes `FUDataEdit`/`FUAutoForm`, `useFormKitForm`, and Nuxt UI's `useOverlay` into one call for the most common CRUD pattern: click Edit, fill in a dialog, save or cancel.
9+
10+
```typescript
11+
const result = await overlay.edit({ data, schema, title })
12+
```
13+
14+
`result` is the saved data on Save, or `null` on Cancel, <kbd>Esc</kbd>, or a backdrop click - there's no separate open/close state, mount timing, or FormKit submit plumbing to hand-assemble yourself.
15+
16+
## Basic Usage
17+
18+
```vue
19+
<script setup lang="ts">
20+
const overlay = useFormKitOverlay()
21+
22+
const schema = [
23+
{ $formkit: 'nuxtUIInput', name: 'name', label: 'Name', validation: 'required' },
24+
{ $formkit: 'nuxtUIInput', name: 'email', label: 'Email', inputType: 'email', validation: 'required|email' },
25+
]
26+
27+
const rows = ref([
28+
{ id: 1, name: 'Ada Lovelace', email: 'ada@example.com' },
29+
])
30+
31+
async function editRow(index: number) {
32+
const result = await overlay.edit({
33+
data: rows.value[index],
34+
schema,
35+
title: `Edit ${rows.value[index]?.name}`,
36+
})
37+
38+
if (result)
39+
rows.value[index] = result
40+
}
41+
</script>
42+
```
43+
44+
## Modal vs Slideover
45+
46+
`as: 'modal' | 'slideover'` (default `'modal'`) chooses which Nuxt UI component wraps the form - one composable, not two:
47+
48+
```typescript
49+
await overlay.edit({ data, schema, title, as: 'slideover' })
50+
```
51+
52+
## Cancel and Dismissal Semantics
53+
54+
Every way a user can close the overlay besides Save resolves the promise to `null`, never leaving it pending: the built-in Cancel button, <kbd>Esc</kbd>, and clicking outside the modal/slideover (backdrop click) all resolve the same way.
55+
56+
## Auto-Inferred Schemas
57+
58+
`auto()` opens the same modal/slideover, but infers the form from your data (or a [Valibot/Zod schema](/components/auto-form)) via `FUAutoForm` instead of requiring a hand-written FormKit schema:
59+
60+
```typescript
61+
const result = await overlay.auto({ data: rows.value[index], title: `Edit ${rows.value[index]?.name}` })
62+
```
63+
64+
```vue
65+
<script setup lang="ts">
66+
import * as v from 'valibot'
67+
68+
const overlay = useFormKitOverlay()
69+
70+
const registrationSchema = v.object({
71+
name: v.pipe(v.string(), v.minLength(2)),
72+
email: v.pipe(v.string(), v.email()),
73+
})
74+
75+
async function editRegistration(data: object) {
76+
return overlay.auto({ data, valibotSchema: registrationSchema, title: 'Registration' })
77+
}
78+
</script>
79+
```
80+
81+
`auto()` accepts the same `overrides`/`valibotSchema`/`zodSchema` options [`FUAutoForm`](/components/auto-form) does, plus this composable's own `title`/`as`.
82+
83+
## Options
84+
85+
### edit(options)
86+
87+
| Option | Type | Default | Description |
88+
| --- | --- | --- | --- |
89+
| `data` | `unknown` | *(required)* | Data to pre-fill the form with |
90+
| `schema` | `FormKitSchemaDefinition` | *(required)* | Hand-written FormKit schema |
91+
| `title` | `string` | `undefined` | Modal/slideover header title |
92+
| `as` | `'modal' \| 'slideover'` | `'modal'` | Which Nuxt UI overlay component to render |
93+
94+
### auto(options)
95+
96+
| Option | Type | Default | Description |
97+
| --- | --- | --- | --- |
98+
| `data` | `unknown` | `undefined` | Data to infer the schema from and pre-fill the form with |
99+
| `valibotSchema` | `ValibotLikeSchema` | `undefined` | Infer inputs/validation from a Valibot object schema instead |
100+
| `zodSchema` | `ZodLikeSchema` | `undefined` | Infer inputs/validation from a Zod object schema instead |
101+
| `overrides` | `AutoFormOverrides` | `undefined` | Dot-path keyed map of partial schema nodes or `false` - see [`FUAutoForm`](/components/auto-form) |
102+
| `title` | `string` | `undefined` | Modal/slideover header title |
103+
| `as` | `'modal' \| 'slideover'` | `'modal'` | Which Nuxt UI overlay component to render |
104+
105+
Both methods return `Promise<unknown>`, resolving to the saved data or `null`.
106+
107+
## One Overlay Instance at a Time
108+
109+
`edit()` and `auto()` each reuse a single registered overlay instance across calls, rather than stacking a new one per invocation. Calling either again before the previous call's promise has resolved doesn't open a second, independent overlay - it replaces the first one's data/title in place, and **the first call's promise never resolves** (its resolver is discarded, not rejected). Always `await` an `edit()`/`auto()` call (or otherwise ensure it has resolved) before starting another from the same composable instance.
110+
111+
## Next Steps
112+
113+
<CardGrid>
114+
<Card title="FUDataEdit" icon="i-lucide-file-edit" to="/components/data-edit">
115+
The schema-driven form edit() opens
116+
</Card>
117+
118+
<Card title="FUAutoForm" icon="i-lucide-wand-sparkles" to="/components/auto-form">
119+
Schema-free forms auto() opens
120+
</Card>
121+
122+
<Card title="API Reference" icon="i-lucide-book" to="/api/utilities">
123+
Complete API documentation
124+
</Card>
125+
</CardGrid>

playground/app/app.vue

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -238,6 +238,12 @@ const items = ref<NavigationMenuItem[]>([
238238
description: 'Validate a form against a Zod/Valibot/ArkType schema via FUDataEdit\'s standard-schema prop.',
239239
to: '/form/standard-schema',
240240
},
241+
{
242+
label: 'useFormKitOverlay Composable',
243+
icon: 'i-lucide-panel-right',
244+
description: 'Promise-based modal/slideover forms: await overlay.edit({ data, schema, title }).',
245+
to: '/form/overlay-form',
246+
},
241247
],
242248
},
243249
{
Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
<script setup lang="ts">
2+
// `useFormKitOverlay` is auto-imported (`src/module.ts` registers
3+
// `./runtime/composables` via `addImportsDir`) - no explicit import needed
4+
// in a real consuming app, same as `useFormKitForm`.
5+
const overlay = useFormKitOverlay()
6+
7+
const as = ref<'modal' | 'slideover'>('modal')
8+
9+
interface Person {
10+
id: number
11+
name: string
12+
email: string
13+
role: string
14+
}
15+
16+
const rows = ref<Person[]>([
17+
{ id: 1, name: 'Ada Lovelace', email: 'ada@example.com', role: 'Engineer' },
18+
{ id: 2, name: 'Grace Hopper', email: 'grace@example.com', role: 'Admiral' },
19+
{ id: 3, name: 'Katherine Johnson', email: 'katherine@example.com', role: 'Mathematician' },
20+
])
21+
22+
const schema = [
23+
{ $formkit: 'nuxtUIInput', name: 'name', label: 'Name', validation: 'required' },
24+
{ $formkit: 'nuxtUIInput', name: 'email', label: 'Email', inputType: 'email', validation: 'required|email' },
25+
{ $formkit: 'nuxtUIInput', name: 'role', label: 'Role', validation: 'required' },
26+
]
27+
28+
async function editRow(index: number) {
29+
const result = await overlay.edit({
30+
data: rows.value[index],
31+
schema,
32+
title: `Edit ${rows.value[index]?.name}`,
33+
as: as.value,
34+
})
35+
36+
if (result)
37+
rows.value[index] = result as Person
38+
}
39+
40+
// No hand-written schema at all here - `auto()` infers one from the row's
41+
// own data value shapes, the same `FUAutoForm`-backed path `/form/auto-form`
42+
// demonstrates directly, driven through a modal/slideover instead.
43+
async function autoEditRow(index: number) {
44+
const result = await overlay.auto({
45+
data: rows.value[index],
46+
title: `Edit ${rows.value[index]?.name} (auto-inferred)`,
47+
as: as.value,
48+
})
49+
50+
if (result)
51+
rows.value[index] = result as Person
52+
}
53+
</script>
54+
55+
<template>
56+
<UContainer>
57+
<div class="mb-8">
58+
<h1 class="text-4xl font-bold mb-4">
59+
useFormKitOverlay Composable
60+
</h1>
61+
<p class="text-lg text-muted-foreground mb-2">
62+
<code>const result = await overlay.edit({ data, schema, title })</code> opens a <code>FUDataEdit</code> form in a modal or slideover, resolving with the saved data on Save or <code>null</code> on Cancel/Esc/backdrop dismissal.
63+
</p>
64+
<p class="text-muted-foreground">
65+
No hand-assembled open/close state, mount timing, or FormKit submit plumbing - just click Edit, await the result, and update the row if it isn't <code>null</code>. "Auto-Edit" uses <code>overlay.auto({ data, title })</code> instead, inferring the schema from the row's own data rather than the hand-written one above.
66+
</p>
67+
</div>
68+
69+
<USeparator class="my-8" />
70+
71+
<div class="space-y-6">
72+
<USelect
73+
v-model="as"
74+
:items="['modal', 'slideover']"
75+
class="w-40"
76+
/>
77+
78+
<div class="space-y-2">
79+
<div
80+
v-for="(row, index) in rows"
81+
:key="row.id"
82+
class="flex items-center justify-between gap-4 rounded-lg border border-default p-4"
83+
>
84+
<div>
85+
<p class="font-medium">
86+
{{ row.name }}
87+
</p>
88+
<p class="text-sm text-muted-foreground">
89+
{{ row.email }} · {{ row.role }}
90+
</p>
91+
</div>
92+
<div class="flex gap-2">
93+
<UButton
94+
label="Edit"
95+
icon="i-lucide-pencil"
96+
variant="outline"
97+
@click="editRow(index)"
98+
/>
99+
<UButton
100+
label="Auto-Edit"
101+
icon="i-lucide-wand-sparkles"
102+
variant="outline"
103+
color="neutral"
104+
@click="autoEditRow(index)"
105+
/>
106+
</div>
107+
</div>
108+
</div>
109+
</div>
110+
</UContainer>
111+
</template>

0 commit comments

Comments
 (0)