Skip to content

Commit 00146a9

Browse files
committed
array of objects
1 parent 46afdfb commit 00146a9

File tree

4 files changed

+153
-75
lines changed

4 files changed

+153
-75
lines changed

src/app/src/components/form/FormPanelSection.vue

Lines changed: 19 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -19,44 +19,29 @@ const childrenCount = computed(() => {
1919
</script>
2020

2121
<template>
22-
<UCollapsible
22+
<Collapsible
2323
v-if="formItem.children"
24-
class="w-full group/collapsible"
24+
:label="formItem.title"
2525
:default-open="true"
26+
class="w-full mt-3"
2627
>
27-
<div class="flex items-center gap-2 w-full mt-3">
28-
<div class="flex items-center justify-center size-4 rounded bg-gray-100 dark:bg-gray-800 transition-colors duration-200 group-hover/collapsible:bg-gray-200 dark:group-hover/collapsible:bg-gray-700">
29-
<UIcon
30-
name="i-lucide-chevron-right"
31-
class="size-2.5 text-gray-500 dark:text-gray-400 transition-transform duration-200 group-data-[state=open]/collapsible:rotate-90"
32-
/>
33-
</div>
34-
<div class="flex gap-2 items-center">
35-
<span class="text-xs font-semibold text-gray-900 dark:text-gray-100 tracking-tight">
36-
{{ formItem.title }}
37-
</span>
38-
<UBadge
39-
v-if="childrenCount"
40-
variant="soft"
41-
size="xs"
42-
class="text-muted"
43-
>
44-
{{ childrenCount }} propert{{ childrenCount === 1 ? 'y' : 'ies' }}
45-
</UBadge>
46-
</div>
47-
</div>
48-
49-
<template #content>
50-
<div class="mt-1 ml-3 pl-3 border-l border-gray-200 dark:border-gray-700/50 space-y-0.5">
51-
<FormPanelSection
52-
v-for="childKey in Object.keys(formItem.children)"
53-
:key="formItem.children[childKey].id"
54-
v-model="form"
55-
:form-item="formItem.children[childKey]"
56-
/>
57-
</div>
28+
<template #badge>
29+
<UBadge
30+
v-if="childrenCount"
31+
variant="subtle"
32+
size="xs"
33+
>
34+
{{ childrenCount }} propert{{ childrenCount === 1 ? 'y' : 'ies' }}
35+
</UBadge>
5836
</template>
59-
</UCollapsible>
37+
38+
<FormPanelSection
39+
v-for="childKey in Object.keys(formItem.children)"
40+
:key="formItem.children[childKey].id"
41+
v-model="form"
42+
:form-item="formItem.children[childKey]"
43+
/>
44+
</Collapsible>
6045

6146
<FormPanelInput
6247
v-else

src/app/src/components/form/input/FormInputArray.vue

Lines changed: 18 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -67,61 +67,38 @@ function saveStringEditing() {
6767
function updateObjectItem(index: number, value: Record<string, unknown>) {
6868
model.value = model.value.map((item, i) => i === index ? value : item)
6969
}
70-
71-
function toggleExpand(index: number) {
72-
activeIndex.value = activeIndex.value === index ? null : index
73-
}
7470
</script>
7571

7672
<template>
77-
<div class="space-y-2">
73+
<div>
7874
<!-- Array of Objects -->
7975
<template v-if="itemsType === 'object'">
80-
<div
76+
<Collapsible
8177
v-for="item in items"
8278
:key="item.index"
83-
class="group/item rounded-lg border border-default overflow-hidden"
79+
:open="activeIndex === item.index"
80+
:label="item.label"
81+
class="group/item"
82+
@update:open="(open: boolean) => activeIndex = open ? item.index : null"
8483
>
85-
<button
86-
type="button"
87-
class="flex items-center justify-between w-full px-3 py-2 text-left bg-elevated hover:bg-accented transition-colors"
88-
@click="toggleExpand(item.index)"
89-
>
90-
<div class="flex items-center gap-2">
91-
<div class="flex items-center justify-center size-4 rounded bg-muted transition-colors">
92-
<UIcon
93-
name="i-lucide-chevron-right"
94-
class="size-2.5 text-muted transition-transform duration-200"
95-
:class="{ 'rotate-90': activeIndex === item.index }"
96-
/>
97-
</div>
98-
<span class="text-xs font-medium text-highlighted tracking-tight">
99-
{{ item.label }}
100-
</span>
101-
</div>
102-
84+
<template #actions>
10385
<UButton
10486
variant="ghost"
10587
color="neutral"
106-
size="xs"
107-
icon="i-lucide-trash-2"
88+
size="2xs"
89+
icon="i-lucide-trash"
10890
class="opacity-0 group-hover/item:opacity-100 transition-opacity"
10991
aria-label="Delete item"
11092
@click.stop="deleteItem(item.index)"
11193
/>
112-
</button>
113-
114-
<div
115-
v-if="activeIndex === item.index"
116-
class="px-3 py-3 border-t border-default bg-default"
117-
>
118-
<FormInputObject
119-
:model-value="item.value"
120-
:children="formItem.children"
121-
@update:model-value="(v: Record<string, unknown>) => updateObjectItem(item.index, v)"
122-
/>
123-
</div>
124-
</div>
94+
</template>
95+
96+
<FormInputObject
97+
:model-value="item.value"
98+
:children="formItem.children"
99+
@update:model-value="updateObjectItem(item.index, $event)"
100+
/>
101+
</Collapsible>
125102
</template>
126103

127104
<!-- Array of Strings -->
@@ -132,7 +109,7 @@ function toggleExpand(index: number) {
132109
:key="item.label"
133110
variant="subtle"
134111
color="neutral"
135-
size="md"
112+
size="sm"
136113
class="group/badge flex items-center gap-3 px-2 py-1 min-w-0"
137114
>
138115
<UInput
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
<script setup lang="ts">
2+
import { titleCase } from 'scule'
3+
import type { FormTree } from '../../../types'
4+
import type { PropType } from 'vue'
5+
import { computed } from 'vue'
6+
7+
const props = defineProps({
8+
children: {
9+
type: Object as PropType<FormTree>,
10+
default: null,
11+
},
12+
})
13+
14+
const model = defineModel({
15+
type: Object as PropType<Record<string, unknown>>,
16+
default: (): Record<string, unknown> => ({}),
17+
})
18+
19+
const entries = computed(() => {
20+
if (!props.children) return []
21+
22+
return Object.entries(props.children).map(([key, child]) => ({
23+
key,
24+
label: titleCase(child.title || key),
25+
value: model.value?.[key] ?? child.default ?? '',
26+
type: child.type === 'number' ? 'number' : 'text',
27+
placeholder: child.type === 'number' ? '0' : `Enter ${(child.title || key).toLowerCase()}...`,
28+
}))
29+
})
30+
31+
function updateValue(key: string, value: string | number) {
32+
model.value = { ...model.value, [key]: value }
33+
}
34+
</script>
35+
36+
<template>
37+
<div class="space-y-2">
38+
<template v-if="entries.length">
39+
<UFormField
40+
v-for="entry in entries"
41+
:key="entry.key"
42+
:name="entry.key"
43+
:label="entry.label"
44+
:ui="{
45+
label: 'text-xs font-medium tracking-tight',
46+
}"
47+
>
48+
<UInput
49+
:model-value="entry.value"
50+
:placeholder="entry.placeholder"
51+
:type="entry.type"
52+
class="w-full"
53+
@update:model-value="updateValue(entry.key, $event)"
54+
/>
55+
</UFormField>
56+
</template>
57+
58+
<div
59+
v-else
60+
class="flex flex-col items-center justify-center py-6 rounded-md border border-dashed border-muted"
61+
>
62+
<UIcon
63+
name="i-lucide-box"
64+
class="size-5 text-muted mb-2"
65+
/>
66+
<p class="text-xs text-muted">
67+
No properties defined
68+
</p>
69+
</div>
70+
</div>
71+
</template>
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
<script setup lang="ts">
2+
defineProps({
3+
label: {
4+
type: String,
5+
required: true,
6+
},
7+
defaultOpen: {
8+
type: Boolean,
9+
default: false,
10+
},
11+
})
12+
13+
const open = defineModel('open', { type: Boolean, default: undefined })
14+
</script>
15+
16+
<template>
17+
<UCollapsible
18+
v-model:open="open"
19+
:default-open="defaultOpen"
20+
class="group/collapsible"
21+
>
22+
<div class="flex items-center justify-between gap-2 w-full">
23+
<div class="flex items-center gap-2">
24+
<div class="flex items-center justify-center size-4 rounded bg-gray-100 dark:bg-gray-800 transition-colors duration-200 group-hover/collapsible:bg-gray-200 dark:group-hover/collapsible:bg-gray-700">
25+
<UIcon
26+
name="i-lucide-chevron-right"
27+
class="size-2.5 text-gray-500 dark:text-gray-400 transition-transform duration-200 group-data-[state=open]/collapsible:rotate-90"
28+
/>
29+
</div>
30+
<span class="text-xs font-medium text-highlighted tracking-tight">
31+
{{ label }}
32+
</span>
33+
<slot name="badge" />
34+
</div>
35+
36+
<slot name="actions" />
37+
</div>
38+
39+
<template #content>
40+
<div class="mt-1 ml-2 pl-3 border-l border-gray-200 dark:border-gray-700/50">
41+
<slot />
42+
</div>
43+
</template>
44+
</UCollapsible>
45+
</template>

0 commit comments

Comments
 (0)