@htlkg/components 0.0.11 → 0.0.13
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{AdminWrapper.vue_vue_type_script_setup_true_lang-B32IylcT.js → AdminWrapper.vue_vue_type_script_setup_true_lang-BhnWQ-b0.js} +26 -29
- package/dist/AdminWrapper.vue_vue_type_script_setup_true_lang-BhnWQ-b0.js.map +1 -0
- package/dist/Alert.vue_vue_type_script_setup_true_lang-DxPCS-Hx.js.map +1 -1
- package/dist/DateRange.vue_vue_type_script_setup_true_lang-BLVg1Hah.js.map +1 -1
- package/dist/ProductBadge.vue_vue_type_script_setup_true_lang-Cmr2f4Cy.js.map +1 -1
- package/dist/components.css +4 -4
- package/dist/composables/index.js +23 -22
- package/dist/data/index.js +10 -10
- package/dist/{filterHelpers-DgRyoYSa.js → filterHelpers-DpHSlTuh.js} +11 -11
- package/dist/filterHelpers-DpHSlTuh.js.map +1 -0
- package/dist/index-QK97OdqQ.js.map +1 -1
- package/dist/index.js +34 -33
- package/dist/navigation/index.js +1 -1
- package/dist/{useAdminPage-GhgXp0x8.js → useAdminPage-AgWRvw6o.js} +150 -26
- package/dist/useAdminPage-AgWRvw6o.js.map +1 -0
- package/package.json +3 -3
- package/src/composables/index.ts +1 -0
- package/src/composables/useJsonForm.test.ts +272 -0
- package/src/composables/useJsonForm.ts +261 -0
- package/src/composables/useModal.test.ts +264 -0
- package/src/composables/useModal.ts +54 -8
- package/src/data/Chart/index.ts +2 -0
- package/src/data/DataList/index.ts +1 -0
- package/src/data/{DataTable.vue → DataTable/DataTable.vue} +2 -2
- package/src/data/DataTable/index.ts +8 -0
- package/src/data/SearchableSelect/index.ts +1 -0
- package/src/data/Table/index.ts +1 -0
- package/src/data/index.ts +5 -15
- package/src/domain/BrandCard/index.ts +1 -0
- package/src/domain/BrandSelector/index.ts +1 -0
- package/src/domain/ProductBadge/index.ts +1 -0
- package/src/domain/UserAvatar/index.ts +1 -0
- package/src/domain/index.ts +4 -4
- package/src/forms/DateRange/index.ts +2 -0
- package/src/forms/JsonSchemaForm/index.ts +1 -0
- package/src/forms/index.ts +2 -3
- package/src/navigation/{AdminWrapper.vue → AdminWrapper/AdminWrapper.vue} +41 -30
- package/src/navigation/AdminWrapper/index.ts +1 -0
- package/src/navigation/Breadcrumbs/index.ts +1 -0
- package/src/navigation/Stepper/index.ts +2 -0
- package/src/navigation/Tabs/index.ts +2 -0
- package/src/navigation/index.ts +4 -6
- package/src/overlays/Alert/index.ts +1 -0
- package/src/overlays/Drawer/index.ts +1 -0
- package/src/overlays/Modal/index.ts +1 -0
- package/src/overlays/Notification/index.ts +1 -0
- package/src/overlays/index.ts +4 -4
- package/src/patterns/DASHBOARD_PAGE.md +642 -0
- package/src/patterns/DETAIL_PAGE.md +446 -0
- package/src/patterns/FORM_PAGE.md +439 -0
- package/src/patterns/LIST_PAGE.md +340 -0
- package/src/patterns/PAGE_PATTERNS.md +110 -0
- package/src/patterns/WIZARD_PAGE.md +733 -0
- package/dist/AdminWrapper.vue_vue_type_script_setup_true_lang-B32IylcT.js.map +0 -1
- package/dist/filterHelpers-DgRyoYSa.js.map +0 -1
- package/dist/useAdminPage-GhgXp0x8.js.map +0 -1
- package/src/data/Table.vue +0 -295
- /package/src/data/{Chart.demo.vue → Chart/Chart.demo.vue} +0 -0
- /package/src/data/{Chart.md → Chart/Chart.md} +0 -0
- /package/src/data/{Chart.vue → Chart/Chart.vue} +0 -0
- /package/src/data/{DataList.md → DataList/DataList.md} +0 -0
- /package/src/data/{DataList.test.ts → DataList/DataList.test.ts} +0 -0
- /package/src/data/{DataList.vue → DataList/DataList.vue} +0 -0
- /package/src/data/{SearchableSelect.md → SearchableSelect/SearchableSelect.md} +0 -0
- /package/src/data/{SearchableSelect.vue → SearchableSelect/SearchableSelect.vue} +0 -0
- /package/src/data/{Table.demo.vue → Table/Table.demo.vue} +0 -0
- /package/src/data/{Table.md → Table/Table.md} +0 -0
- /package/src/data/{Table.property.test.ts → Table/Table.property.test.ts} +0 -0
- /package/src/data/{Table.test.ts → Table/Table.test.ts} +0 -0
- /package/src/data/{Table.unit.test.ts → Table/Table.unit.test.ts} +0 -0
- /package/src/domain/{BrandCard.md → BrandCard/BrandCard.md} +0 -0
- /package/src/domain/{BrandCard.vue → BrandCard/BrandCard.vue} +0 -0
- /package/src/domain/{BrandSelector.md → BrandSelector/BrandSelector.md} +0 -0
- /package/src/domain/{BrandSelector.vue → BrandSelector/BrandSelector.vue} +0 -0
- /package/src/domain/{ProductBadge.md → ProductBadge/ProductBadge.md} +0 -0
- /package/src/domain/{ProductBadge.vue → ProductBadge/ProductBadge.vue} +0 -0
- /package/src/domain/{UserAvatar.md → UserAvatar/UserAvatar.md} +0 -0
- /package/src/domain/{UserAvatar.vue → UserAvatar/UserAvatar.vue} +0 -0
- /package/src/forms/{DateRange.demo.vue → DateRange/DateRange.demo.vue} +0 -0
- /package/src/forms/{DateRange.md → DateRange/DateRange.md} +0 -0
- /package/src/forms/{DateRange.vue → DateRange/DateRange.vue} +0 -0
- /package/src/forms/{JsonSchemaForm.demo.vue → JsonSchemaForm/JsonSchemaForm.demo.vue} +0 -0
- /package/src/forms/{JsonSchemaForm.md → JsonSchemaForm/JsonSchemaForm.md} +0 -0
- /package/src/forms/{JsonSchemaForm.property.test.ts → JsonSchemaForm/JsonSchemaForm.property.test.ts} +0 -0
- /package/src/forms/{JsonSchemaForm.test.ts → JsonSchemaForm/JsonSchemaForm.test.ts} +0 -0
- /package/src/forms/{JsonSchemaForm.unit.test.ts → JsonSchemaForm/JsonSchemaForm.unit.test.ts} +0 -0
- /package/src/forms/{JsonSchemaForm.vue → JsonSchemaForm/JsonSchemaForm.vue} +0 -0
- /package/src/navigation/{Breadcrumbs.demo.vue → Breadcrumbs/Breadcrumbs.demo.vue} +0 -0
- /package/src/navigation/{Breadcrumbs.md → Breadcrumbs/Breadcrumbs.md} +0 -0
- /package/src/navigation/{Breadcrumbs.test.ts → Breadcrumbs/Breadcrumbs.test.ts} +0 -0
- /package/src/navigation/{Breadcrumbs.vue → Breadcrumbs/Breadcrumbs.vue} +0 -0
- /package/src/navigation/{Stepper.demo.vue → Stepper/Stepper.demo.vue} +0 -0
- /package/src/navigation/{Stepper.md → Stepper/Stepper.md} +0 -0
- /package/src/navigation/{Stepper.vue → Stepper/Stepper.vue} +0 -0
- /package/src/navigation/{Tabs.demo.vue → Tabs/Tabs.demo.vue} +0 -0
- /package/src/navigation/{Tabs.md → Tabs/Tabs.md} +0 -0
- /package/src/navigation/{Tabs.test.ts → Tabs/Tabs.test.ts} +0 -0
- /package/src/navigation/{Tabs.vue → Tabs/Tabs.vue} +0 -0
- /package/src/overlays/{Alert.demo.vue → Alert/Alert.demo.vue} +0 -0
- /package/src/overlays/{Alert.md → Alert/Alert.md} +0 -0
- /package/src/overlays/{Alert.test.ts → Alert/Alert.test.ts} +0 -0
- /package/src/overlays/{Alert.vue → Alert/Alert.vue} +0 -0
- /package/src/overlays/{Drawer.md → Drawer/Drawer.md} +0 -0
- /package/src/overlays/{Drawer.test.ts → Drawer/Drawer.test.ts} +0 -0
- /package/src/overlays/{Drawer.vue → Drawer/Drawer.vue} +0 -0
- /package/src/overlays/{Modal.demo.vue → Modal/Modal.demo.vue} +0 -0
- /package/src/overlays/{Modal.md → Modal/Modal.md} +0 -0
- /package/src/overlays/{Modal.test.ts → Modal/Modal.test.ts} +0 -0
- /package/src/overlays/{Modal.vue → Modal/Modal.vue} +0 -0
- /package/src/overlays/{Notification.md → Notification/Notification.md} +0 -0
- /package/src/overlays/{Notification.test.ts → Notification/Notification.test.ts} +0 -0
- /package/src/overlays/{Notification.vue → Notification/Notification.vue} +0 -0
|
@@ -0,0 +1,439 @@
|
|
|
1
|
+
# Form Page Pattern
|
|
2
|
+
|
|
3
|
+
Create or edit entity data using JsonSchemaForm for automatic form generation and validation.
|
|
4
|
+
|
|
5
|
+
## Architecture
|
|
6
|
+
|
|
7
|
+
```
|
|
8
|
+
┌─────────────────────────────────────────────────────────────┐
|
|
9
|
+
│ AdminLayout │
|
|
10
|
+
├─────────────────────────────────────────────────────────────┤
|
|
11
|
+
│ Page Header (title + breadcrumbs) │
|
|
12
|
+
├─────────────────────────────────────────────────────────────┤
|
|
13
|
+
│ Optional: Back Button │
|
|
14
|
+
├─────────────────────────────────────────────────────────────┤
|
|
15
|
+
│ Form Container (lg:w-2/3 xl:w-1/2) │
|
|
16
|
+
│ ├── Section Title │
|
|
17
|
+
│ ├── Input Fields (auto-generated from schema) │
|
|
18
|
+
│ ├── Section Title │
|
|
19
|
+
│ ├── More Fields... │
|
|
20
|
+
│ └── Submit Button │
|
|
21
|
+
├─────────────────────────────────────────────────────────────┤
|
|
22
|
+
│ Optional: Unsaved Changes Warning │
|
|
23
|
+
└─────────────────────────────────────────────────────────────┘
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
## CRM Template Reference
|
|
27
|
+
|
|
28
|
+
Based on: `ui/src/components/Templates/CRM/SettingsGeneral.vue`
|
|
29
|
+
|
|
30
|
+
```vue
|
|
31
|
+
<!-- CRM Template Structure (raw @hotelinking/ui) -->
|
|
32
|
+
<uiWrapper :sidebar :topbar>
|
|
33
|
+
<uiViewHeader :pages="pages" title="Hotel Group Information" />
|
|
34
|
+
|
|
35
|
+
<div class="lg:w-2/3 xl:w-1/2">
|
|
36
|
+
<uiInput label="Group Name" :value="formData.groupName"
|
|
37
|
+
@inputChanged="handleInputChange('groupName', $event)" />
|
|
38
|
+
|
|
39
|
+
<uiTextArea label="Description" :value="formData.description"
|
|
40
|
+
@inputChanged="handleInputChange('description', $event)" />
|
|
41
|
+
|
|
42
|
+
<uiSectionTitle title="Corporate Headquarters"
|
|
43
|
+
description="Main office location." />
|
|
44
|
+
|
|
45
|
+
<div class="grid gap-x-4 md:grid-cols-2">
|
|
46
|
+
<uiInput label="City" :value="formData.city" />
|
|
47
|
+
<uiSelectV2 label="Country" :items="countries" />
|
|
48
|
+
</div>
|
|
49
|
+
|
|
50
|
+
<uiButton class="my-8">Save Changes</uiButton>
|
|
51
|
+
</div>
|
|
52
|
+
</uiWrapper>
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
## Enhanced Pattern (with @htlkg/*)
|
|
56
|
+
|
|
57
|
+
### Astro Page
|
|
58
|
+
|
|
59
|
+
```astro
|
|
60
|
+
---
|
|
61
|
+
// src/pages/[brandId]/admin/config.astro
|
|
62
|
+
import Layout from '@/layouts/Layout.astro';
|
|
63
|
+
import BrandConfigForm from '@/components/admin/BrandConfigForm.vue';
|
|
64
|
+
import { getBrand } from '@/services/brand';
|
|
65
|
+
|
|
66
|
+
export const prerender = false;
|
|
67
|
+
|
|
68
|
+
const { brandId } = Astro.params;
|
|
69
|
+
const brand = await getBrand(Number(brandId));
|
|
70
|
+
|
|
71
|
+
const layoutProps = {
|
|
72
|
+
title: 'Brand Configuration',
|
|
73
|
+
breadcrumbs: [
|
|
74
|
+
{ name: 'Brands', href: '/admin/brands' },
|
|
75
|
+
{ name: brand.name, href: `/${brandId}/admin` },
|
|
76
|
+
{ name: 'Configuration', current: true },
|
|
77
|
+
],
|
|
78
|
+
};
|
|
79
|
+
---
|
|
80
|
+
|
|
81
|
+
<Layout {...layoutProps}>
|
|
82
|
+
<BrandConfigForm client:load initialData={brand} brandId={brandId} />
|
|
83
|
+
</Layout>
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
### Vue Component with JsonSchemaForm
|
|
87
|
+
|
|
88
|
+
```vue
|
|
89
|
+
<!-- src/components/admin/BrandConfigForm.vue -->
|
|
90
|
+
<script setup lang="ts">
|
|
91
|
+
import { ref, computed } from 'vue';
|
|
92
|
+
import { JsonSchemaForm } from '@htlkg/components/forms';
|
|
93
|
+
import { useJsonForm, useConfirmation, useNotifications } from '@htlkg/components/composables';
|
|
94
|
+
import { uiButton, uiSectionTitle } from '@hotelinking/ui';
|
|
95
|
+
|
|
96
|
+
interface Props {
|
|
97
|
+
initialData: Brand;
|
|
98
|
+
brandId: string;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const props = defineProps<Props>();
|
|
102
|
+
const emit = defineEmits<{
|
|
103
|
+
saved: [data: Brand];
|
|
104
|
+
}>();
|
|
105
|
+
|
|
106
|
+
// Form state with useJsonForm
|
|
107
|
+
const { formRef, values, isDirty, validate, reset, setValues } = useJsonForm({
|
|
108
|
+
initialValues: props.initialData,
|
|
109
|
+
confirmOnLeave: true,
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
// Confirmation dialog for reset
|
|
113
|
+
const resetConfirmation = useConfirmation();
|
|
114
|
+
|
|
115
|
+
// Notifications
|
|
116
|
+
const { notify } = useNotifications();
|
|
117
|
+
|
|
118
|
+
// JSON Schema for the form
|
|
119
|
+
const schema = {
|
|
120
|
+
type: 'object',
|
|
121
|
+
title: 'Brand Configuration',
|
|
122
|
+
properties: {
|
|
123
|
+
name: {
|
|
124
|
+
type: 'string',
|
|
125
|
+
title: 'Brand Name',
|
|
126
|
+
minLength: 1,
|
|
127
|
+
},
|
|
128
|
+
description: {
|
|
129
|
+
type: 'string',
|
|
130
|
+
title: 'Description',
|
|
131
|
+
minLength: 100, // Will use textarea
|
|
132
|
+
},
|
|
133
|
+
type: {
|
|
134
|
+
type: 'string',
|
|
135
|
+
title: 'Brand Type',
|
|
136
|
+
enum: ['hotel', 'resort', 'boutique', 'chain'],
|
|
137
|
+
},
|
|
138
|
+
totalProperties: {
|
|
139
|
+
type: 'number',
|
|
140
|
+
title: 'Total Properties',
|
|
141
|
+
minimum: 0,
|
|
142
|
+
},
|
|
143
|
+
totalRooms: {
|
|
144
|
+
type: 'number',
|
|
145
|
+
title: 'Total Rooms',
|
|
146
|
+
minimum: 0,
|
|
147
|
+
},
|
|
148
|
+
// Corporate section
|
|
149
|
+
headquartersAddress: {
|
|
150
|
+
type: 'string',
|
|
151
|
+
title: 'Headquarters Address',
|
|
152
|
+
},
|
|
153
|
+
city: {
|
|
154
|
+
type: 'string',
|
|
155
|
+
title: 'City',
|
|
156
|
+
},
|
|
157
|
+
country: {
|
|
158
|
+
type: 'string',
|
|
159
|
+
title: 'Country',
|
|
160
|
+
enum: ['US', 'UK', 'ES', 'FR', 'DE'],
|
|
161
|
+
},
|
|
162
|
+
corporatePhone: {
|
|
163
|
+
type: 'string',
|
|
164
|
+
title: 'Corporate Phone',
|
|
165
|
+
},
|
|
166
|
+
corporateEmail: {
|
|
167
|
+
type: 'string',
|
|
168
|
+
title: 'Corporate Email',
|
|
169
|
+
format: 'email',
|
|
170
|
+
},
|
|
171
|
+
website: {
|
|
172
|
+
type: 'string',
|
|
173
|
+
title: 'Website',
|
|
174
|
+
format: 'uri',
|
|
175
|
+
},
|
|
176
|
+
// Regional settings
|
|
177
|
+
timezone: {
|
|
178
|
+
type: 'string',
|
|
179
|
+
title: 'Timezone',
|
|
180
|
+
enum: ['UTC', 'EST', 'PST', 'CET'],
|
|
181
|
+
},
|
|
182
|
+
language: {
|
|
183
|
+
type: 'string',
|
|
184
|
+
title: 'Language',
|
|
185
|
+
enum: ['en', 'es', 'fr', 'de'],
|
|
186
|
+
},
|
|
187
|
+
currency: {
|
|
188
|
+
type: 'string',
|
|
189
|
+
title: 'Currency',
|
|
190
|
+
enum: ['USD', 'EUR', 'GBP'],
|
|
191
|
+
},
|
|
192
|
+
},
|
|
193
|
+
required: ['name', 'type'],
|
|
194
|
+
};
|
|
195
|
+
|
|
196
|
+
// UI Schema for customization
|
|
197
|
+
const uiSchema = {
|
|
198
|
+
description: {
|
|
199
|
+
'ui:widget': 'textarea',
|
|
200
|
+
'ui:placeholder': 'Enter brand description...',
|
|
201
|
+
},
|
|
202
|
+
corporatePhone: {
|
|
203
|
+
'ui:placeholder': '+1 (000) 000-0000',
|
|
204
|
+
},
|
|
205
|
+
website: {
|
|
206
|
+
'ui:placeholder': 'https://www.example.com',
|
|
207
|
+
},
|
|
208
|
+
};
|
|
209
|
+
|
|
210
|
+
// Submit handler
|
|
211
|
+
async function handleSubmit(formData: Brand) {
|
|
212
|
+
try {
|
|
213
|
+
const response = await saveBrand(props.brandId, formData);
|
|
214
|
+
notify({ type: 'success', message: 'Configuration saved successfully' });
|
|
215
|
+
setValues(response); // Update initial values after save
|
|
216
|
+
emit('saved', response);
|
|
217
|
+
} catch (error) {
|
|
218
|
+
notify({ type: 'error', message: 'Failed to save configuration' });
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// Reset handler with confirmation
|
|
223
|
+
function handleReset() {
|
|
224
|
+
resetConfirmation.confirm(
|
|
225
|
+
'Are you sure you want to reset all changes?',
|
|
226
|
+
() => {
|
|
227
|
+
reset();
|
|
228
|
+
notify({ type: 'info', message: 'Form reset to original values' });
|
|
229
|
+
}
|
|
230
|
+
);
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
// Loading state
|
|
234
|
+
const isLoading = ref(false);
|
|
235
|
+
</script>
|
|
236
|
+
|
|
237
|
+
<template>
|
|
238
|
+
<div class="lg:w-2/3 xl:w-1/2">
|
|
239
|
+
<!-- Unsaved changes indicator -->
|
|
240
|
+
<div v-if="isDirty" class="mb-4 p-3 bg-yellow-50 border border-yellow-200 rounded-lg">
|
|
241
|
+
<p class="text-sm text-yellow-800">You have unsaved changes</p>
|
|
242
|
+
</div>
|
|
243
|
+
|
|
244
|
+
<!-- Basic Information Section -->
|
|
245
|
+
<uiSectionTitle
|
|
246
|
+
class="mb-4"
|
|
247
|
+
title="Basic Information"
|
|
248
|
+
description="Brand identification and details."
|
|
249
|
+
/>
|
|
250
|
+
|
|
251
|
+
<JsonSchemaForm
|
|
252
|
+
ref="formRef"
|
|
253
|
+
v-model="values"
|
|
254
|
+
:schema="schema"
|
|
255
|
+
:ui-schema="uiSchema"
|
|
256
|
+
:loading="isLoading"
|
|
257
|
+
@submit="handleSubmit"
|
|
258
|
+
>
|
|
259
|
+
<!-- Custom actions slot -->
|
|
260
|
+
<template #actions>
|
|
261
|
+
<div class="flex gap-4 pt-6 border-t">
|
|
262
|
+
<uiButton
|
|
263
|
+
type="button"
|
|
264
|
+
color="gray"
|
|
265
|
+
:disabled="!isDirty"
|
|
266
|
+
@click="handleReset"
|
|
267
|
+
>
|
|
268
|
+
Reset
|
|
269
|
+
</uiButton>
|
|
270
|
+
<uiButton
|
|
271
|
+
type="submit"
|
|
272
|
+
color="primary"
|
|
273
|
+
:loading="isLoading"
|
|
274
|
+
:disabled="!isDirty"
|
|
275
|
+
>
|
|
276
|
+
Save Changes
|
|
277
|
+
</uiButton>
|
|
278
|
+
</div>
|
|
279
|
+
</template>
|
|
280
|
+
</JsonSchemaForm>
|
|
281
|
+
|
|
282
|
+
<!-- Reset Confirmation Modal -->
|
|
283
|
+
<Modal
|
|
284
|
+
:open="resetConfirmation.isOpen.value"
|
|
285
|
+
title="Reset Form"
|
|
286
|
+
@close="resetConfirmation.cancel"
|
|
287
|
+
>
|
|
288
|
+
<p>{{ resetConfirmation.message.value }}</p>
|
|
289
|
+
<template #actions>
|
|
290
|
+
<uiButton color="gray" @click="resetConfirmation.cancel">Cancel</uiButton>
|
|
291
|
+
<uiButton color="red" @click="resetConfirmation.execute">Reset</uiButton>
|
|
292
|
+
</template>
|
|
293
|
+
</Modal>
|
|
294
|
+
</div>
|
|
295
|
+
</template>
|
|
296
|
+
```
|
|
297
|
+
|
|
298
|
+
## Key Components
|
|
299
|
+
|
|
300
|
+
### JsonSchemaForm
|
|
301
|
+
|
|
302
|
+
The primary form component that auto-generates UI from JSON Schema.
|
|
303
|
+
|
|
304
|
+
```typescript
|
|
305
|
+
import { JsonSchemaForm } from '@htlkg/components/forms';
|
|
306
|
+
```
|
|
307
|
+
|
|
308
|
+
**Props:**
|
|
309
|
+
- `schema` - JSON Schema definition
|
|
310
|
+
- `modelValue` - Form data (v-model)
|
|
311
|
+
- `uiSchema` - UI customizations
|
|
312
|
+
- `loading` - Loading state
|
|
313
|
+
|
|
314
|
+
**Events:**
|
|
315
|
+
- `submit` - Form submitted with valid data
|
|
316
|
+
- `validation-error` - Validation failed
|
|
317
|
+
|
|
318
|
+
**Exposed Methods:**
|
|
319
|
+
- `validate()` - Trigger validation
|
|
320
|
+
- `reset()` - Reset form
|
|
321
|
+
|
|
322
|
+
### useJsonForm Composable
|
|
323
|
+
|
|
324
|
+
Wraps JsonSchemaForm with additional features.
|
|
325
|
+
|
|
326
|
+
```typescript
|
|
327
|
+
import { useJsonForm } from '@htlkg/components/composables';
|
|
328
|
+
|
|
329
|
+
const {
|
|
330
|
+
formRef, // Ref to JsonSchemaForm instance
|
|
331
|
+
values, // Form data (reactive)
|
|
332
|
+
isDirty, // Has unsaved changes
|
|
333
|
+
validate, // Trigger validation
|
|
334
|
+
reset, // Reset to initial values
|
|
335
|
+
setValues, // Update initial values (after save)
|
|
336
|
+
} = useJsonForm({
|
|
337
|
+
initialValues: props.data,
|
|
338
|
+
confirmOnLeave: true, // Warn before leaving with unsaved changes
|
|
339
|
+
});
|
|
340
|
+
```
|
|
341
|
+
|
|
342
|
+
### JSON Schema Field Types
|
|
343
|
+
|
|
344
|
+
```typescript
|
|
345
|
+
const schema = {
|
|
346
|
+
properties: {
|
|
347
|
+
// Text input
|
|
348
|
+
name: { type: 'string', title: 'Name', minLength: 1 },
|
|
349
|
+
|
|
350
|
+
// Email input (auto-detected from format)
|
|
351
|
+
email: { type: 'string', title: 'Email', format: 'email' },
|
|
352
|
+
|
|
353
|
+
// URL input
|
|
354
|
+
website: { type: 'string', title: 'Website', format: 'uri' },
|
|
355
|
+
|
|
356
|
+
// Number input
|
|
357
|
+
count: { type: 'number', title: 'Count', minimum: 0, maximum: 100 },
|
|
358
|
+
|
|
359
|
+
// Select dropdown (from enum)
|
|
360
|
+
status: { type: 'string', title: 'Status', enum: ['active', 'inactive'] },
|
|
361
|
+
|
|
362
|
+
// Boolean toggle
|
|
363
|
+
enabled: { type: 'boolean', title: 'Enabled' },
|
|
364
|
+
|
|
365
|
+
// Textarea (auto for minLength > 100)
|
|
366
|
+
description: { type: 'string', title: 'Description', minLength: 100 },
|
|
367
|
+
|
|
368
|
+
// Password (via uiSchema)
|
|
369
|
+
password: { type: 'string', title: 'Password' },
|
|
370
|
+
|
|
371
|
+
// Array of strings
|
|
372
|
+
tags: { type: 'array', title: 'Tags', items: { type: 'string' } },
|
|
373
|
+
},
|
|
374
|
+
required: ['name', 'email'], // Required fields
|
|
375
|
+
};
|
|
376
|
+
```
|
|
377
|
+
|
|
378
|
+
### UI Schema Customizations
|
|
379
|
+
|
|
380
|
+
```typescript
|
|
381
|
+
const uiSchema = {
|
|
382
|
+
password: { 'ui:widget': 'password' },
|
|
383
|
+
description: { 'ui:widget': 'textarea' },
|
|
384
|
+
volume: { 'ui:widget': 'slider' },
|
|
385
|
+
field: {
|
|
386
|
+
'ui:placeholder': 'Enter value...',
|
|
387
|
+
'ui:autocomplete': 'email',
|
|
388
|
+
},
|
|
389
|
+
};
|
|
390
|
+
```
|
|
391
|
+
|
|
392
|
+
## Form Sections
|
|
393
|
+
|
|
394
|
+
For long forms, use section titles to group related fields:
|
|
395
|
+
|
|
396
|
+
```vue
|
|
397
|
+
<template>
|
|
398
|
+
<div class="lg:w-2/3 xl:w-1/2">
|
|
399
|
+
<!-- Section 1 -->
|
|
400
|
+
<uiSectionTitle title="Basic Info" description="..." />
|
|
401
|
+
<!-- Fields -->
|
|
402
|
+
|
|
403
|
+
<!-- Section 2 -->
|
|
404
|
+
<uiSectionTitle title="Contact" description="..." class="mt-8" />
|
|
405
|
+
<!-- Fields -->
|
|
406
|
+
|
|
407
|
+
<!-- Submit -->
|
|
408
|
+
<uiButton class="my-8">Save</uiButton>
|
|
409
|
+
</div>
|
|
410
|
+
</template>
|
|
411
|
+
```
|
|
412
|
+
|
|
413
|
+
## Grid Layouts for Fields
|
|
414
|
+
|
|
415
|
+
Use Tailwind grid for side-by-side fields:
|
|
416
|
+
|
|
417
|
+
```vue
|
|
418
|
+
<div class="grid gap-x-4 md:grid-cols-2 mb-4">
|
|
419
|
+
<!-- Two columns on medium+ screens -->
|
|
420
|
+
</div>
|
|
421
|
+
|
|
422
|
+
<div class="grid gap-x-4 md:grid-cols-3 mb-4">
|
|
423
|
+
<!-- Three columns on medium+ screens -->
|
|
424
|
+
</div>
|
|
425
|
+
```
|
|
426
|
+
|
|
427
|
+
## Checklist
|
|
428
|
+
|
|
429
|
+
- [ ] Astro page with server-side data fetch
|
|
430
|
+
- [ ] Vue component with `JsonSchemaForm`
|
|
431
|
+
- [ ] JSON Schema with validation rules
|
|
432
|
+
- [ ] UI Schema for customizations (if needed)
|
|
433
|
+
- [ ] `useJsonForm` for isDirty, confirmOnLeave
|
|
434
|
+
- [ ] Section titles for grouped fields
|
|
435
|
+
- [ ] Grid layouts for field arrangement
|
|
436
|
+
- [ ] Submit and Reset buttons
|
|
437
|
+
- [ ] `useConfirmation` for reset dialog
|
|
438
|
+
- [ ] `useNotifications` for success/error messages
|
|
439
|
+
- [ ] Loading states during save
|