Skip to content

Commit c63c6ca

Browse files
committed
feat: add FormKit repeater component for dynamic list management
1 parent 2dbe95e commit c63c6ca

7 files changed

Lines changed: 114 additions & 20 deletions

File tree

playground/app/app.vue

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -165,6 +165,12 @@ const items = ref<NavigationMenuItem[]>([
165165
description: 'Examples of using slots to customize FormKit components with Nuxt UI.',
166166
to: '/form/slot',
167167
},
168+
{
169+
label: 'Repeater',
170+
icon: 'i-lucide-list-plus',
171+
description: 'Dynamic list management with add, remove, copy, and reorder capabilities.',
172+
to: '/form/repeater',
173+
},
168174
],
169175
},
170176
{
Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
<script setup lang='ts'>
2+
const { addElement, addList, addListGroup } = useFormKitSchema()
3+
const { addInsertButton, addGroupButtons, addListGroupFunctions } = useFormKitRepeater()
4+
5+
const defaultData = { name: 'Fighter', attacks: [{ name: 'Sword', damage: '2D20' }, { name: 'Dagger', damage: '2D6' }] }
6+
function createDefaultValue(): object {
7+
return { name: 'Bow', damage: '1D6+4' }
8+
}
9+
addListGroupFunctions(defaultData, createDefaultValue())
10+
11+
const data = ref(defaultData)
12+
13+
async function submitHandler() {
14+
console.log('Form Submitted ...', 'Form submitted successfully')
15+
}
16+
17+
const schema = [
18+
{
19+
$formkit: 'nuxtUIInput',
20+
label: 'Name',
21+
name: 'name',
22+
class: 'w-32',
23+
},
24+
25+
addElement('div', [''], { class: 'mt-4' }),
26+
addList('attacks', [
27+
addElement('div', ['Attacks'], { class: 'text-xl' }),
28+
addInsertButton(),
29+
addListGroup([
30+
addElement('div', [
31+
{
32+
$formkit: 'nuxtUIInput',
33+
label: 'Name',
34+
name: 'name',
35+
},
36+
{
37+
$formkit: 'nuxtUIInput',
38+
label: 'Damage',
39+
name: 'damage',
40+
},
41+
addGroupButtons('flex gap-2', '', 'Actions'),
42+
], { class: 'flex gap-2' }),
43+
]),
44+
], true, 'true'),
45+
]
46+
</script>
47+
48+
<template>
49+
<UContainer>
50+
<div class="mb-8">
51+
<h1 class="text-4xl font-bold mb-4">
52+
FormKit Repeater
53+
</h1>
54+
<p class="text-lg text-muted-foreground mb-2">
55+
This example demonstrates using FormKit's repeater functionality to manage dynamic lists of items with add, remove, copy, and reorder capabilities.
56+
</p>
57+
<p class="text-muted-foreground">
58+
Create dynamic form sections where users can add multiple items, copy existing ones, and reorder them using intuitive controls powered by Nuxt UI components.
59+
</p>
60+
</div>
61+
62+
<USeparator class="my-8" />
63+
64+
<div class="space-y-12">
65+
<section>
66+
<h2 class="text-2xl font-semibold mb-4">
67+
Dynamic Attack List
68+
</h2>
69+
<p class="text-muted-foreground mb-6">
70+
Manage a character's attacks with dynamic add, remove, copy, and reorder actions. Each attack can be customized with name and damage values.
71+
</p>
72+
<FUDataEdit
73+
v-if="data"
74+
:data="data"
75+
:schema="schema"
76+
debug-data
77+
@data-saved="submitHandler"
78+
/>
79+
</section>
80+
</div>
81+
</UContainer>
82+
</template>
83+
84+
<style lang='scss' scoped>
85+
86+
</style>

playground/app/pages/form/slot.vue

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,7 @@ function getChip(value: string) {
7676
allowing you to render dynamic content that responds to the current selection.
7777
</p>
7878
<FUDataEdit
79+
v-if="data"
7980
:data="data"
8081
:debug-data="true"
8182
@data-saved="submitHandler"

playground/app/pages/form/without-schema.vue

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ const options = ['Apple', 'Banana', 'Cherry', 'Date', 'Elderberry', 'Fig']
3535
Use FormKit components directly in your template with validation and data binding.
3636
</p>
3737
<FUDataEdit
38+
v-if="data"
3839
:data="data"
3940
:debug-data="true"
4041
@data-saved="submitHandler"

src/runtime/composables/useFormKitRepeater.ts

Lines changed: 16 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,12 @@ import type { FormKitNode } from '@formkit/core'
22
import { useFormKitSchema } from './useFormKitSchema'
33

44
export function useFormKitRepeater() {
5-
const { addElement, addComponent, addElementsInOuterDiv } = useFormKitSchema()
5+
const { addComponent, addElement, addElementsInOuterDiv } = useFormKitSchema()
66

7-
function addInsertButton(label: string = 'Add', innerClass: string = '', outerClass: string = '', buttonClass: string = 'p-button-sm', iconClass: string = 'pi pi-plus') {
8-
return addElementsInOuterDiv([
9-
addComponent('Button', { onClick: '$addNode($node.parent)', label, class: buttonClass, icon: iconClass }, '$node.parent.value.length == 0'),
10-
], innerClass, outerClass)
7+
function addInsertButton(label: string = 'Add Item', icon: string = 'i-lucide-plus', styleClass: string = '', render: string = '$node.children.length == 0') {
8+
return addElement('div', [
9+
addComponent('UButton', { onClick: '$addNode($node)', label, icon }),
10+
], { class: styleClass }, render)
1111
}
1212

1313
function addListGroupFunctions(data: Record<string, unknown>, addNodeDefaultObject: object = {}) {
@@ -39,24 +39,23 @@ export function useFormKitRepeater() {
3939
}
4040
data.copyNode = (parentNode: FormKitNode, index: number) => (): void => {
4141
const obj: unknown = (parentNode.value as unknown[])[index]
42-
const newArray: unknown[] = [...(parentNode.value as unknown[]), { ...(obj as object) }]
43-
parentNode.input(newArray, false)
42+
const array: unknown[] = [...(parentNode.value as unknown[])]
43+
array.splice(index + 1, 0, { ...(obj as object) })
44+
parentNode.input(array, false)
4445
}
4546
}
4647

47-
function addGroupButtons(innerClass: string = '', outerClass: string = 'col-4', label: string = 'Actions', help: string = '', render: string = 'true') {
48-
const addButtonComponent = (onClick: string = '', label: string = '', icon: string = '', severity: string = '', render: string = 'true', styleClass: string = 'p-button-sm'): object => {
49-
return addComponent('Button', { onClick, label, icon, class: styleClass, severity }, render)
48+
function addGroupButtons(innerClass: string = '', outerClass: string = '', label: string = '', help: string = '', render: string = 'true') {
49+
const addButtonComponent = (onClick: string = '', icon: string = '', color: string = '', render: string = 'true'): object => {
50+
return addComponent('UButton', { onClick, icon, color }, render)
5051
}
5152

5253
return addElementsInOuterDiv([
53-
addButtonComponent('$removeNode($node.parent, $index)', '', 'pi pi-times', 'danger'),
54-
addButtonComponent('$copyNode($node.parent, $index)', '', 'pi pi-plus'),
55-
addButtonComponent('$moveNodeUp($node.parent, $index)', '', 'pi pi-arrow-up', 'secondary', '$index != 0'),
56-
addElement('span', [], { class: 'p-space' }, '$index == 0'),
57-
addButtonComponent('$moveNodeDown($node.parent, $index)', '', 'pi pi-arrow-down', 'secondary', '$index < $node.parent.value.length -1'),
58-
addElement('span', [], { class: 'p-space' }, '$index == $node.parent.value.length -1'),
59-
], `p-action-buttons ${innerClass}`, outerClass, label, help, render)
54+
addButtonComponent('$removeNode($node.parent, $index)', 'i-lucide-x', 'error'),
55+
addButtonComponent('$copyNode($node.parent, $index)', 'i-lucide-copy', 'secondary'),
56+
addButtonComponent('$moveNodeUp($node.parent, $index)', 'i-lucide-arrow-up', 'primary', '$index != 0'),
57+
addButtonComponent('$moveNodeDown($node.parent, $index)', 'i-lucide-arrow-down', 'primary', '$index < $node.parent.value.length -1'),
58+
], innerClass, outerClass, label, help, render)
6059
}
6160

6261
return { addInsertButton, addGroupButtons, addListGroupFunctions }

src/runtime/composables/useFormKitSchema.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
export function useFormKitSchema() {
2-
const addComponent = (component: string = 'Button', props: object = {}, render: string | boolean = true, formKitAttrs: object = {}): object => {
2+
const addComponent = (component: string = 'UButton', props: object = {}, render: string | boolean = true, formKitAttrs: object = {}): object => {
33
return {
44
$cmp: component,
55
if: render.toString(),

src/runtime/plugin.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { defineNuxtPlugin } from '#app'
2-
import { consola } from 'consola'
2+
import { UButton } from '#components'
33

44
export default defineNuxtPlugin((_nuxtApp) => {
5-
consola.info('Plugin injected by @sfxcode/formkit-nuxt-ui!')
5+
// add Button for repeater
6+
_nuxtApp.vueApp.component('UButton', UButton)
67
})

0 commit comments

Comments
 (0)