@type32/yaml-editor-form 0.1.3 → 0.1.5
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/README.md +531 -58
- package/dist/module.json +1 -1
- package/dist/runtime/components/YamlCollapsible.d.vue.ts +14 -27
- package/dist/runtime/components/YamlCollapsible.vue +5 -9
- package/dist/runtime/components/YamlCollapsible.vue.d.ts +14 -27
- package/dist/runtime/components/YamlFieldInput.d.vue.ts +3 -0
- package/dist/runtime/components/YamlFieldInput.vue +11 -9
- package/dist/runtime/components/YamlFieldInput.vue.d.ts +3 -0
- package/dist/runtime/components/YamlFormEditor.d.vue.ts +2 -0
- package/dist/runtime/components/YamlFormEditor.vue +5 -3
- package/dist/runtime/components/YamlFormEditor.vue.d.ts +2 -0
- package/dist/runtime/components/YamlFormField.d.vue.ts +2 -0
- package/dist/runtime/components/YamlFormField.vue +32 -19
- package/dist/runtime/components/YamlFormField.vue.d.ts +2 -0
- package/dist/runtime/composables/useYamlFieldTypes.js +13 -2
- package/dist/runtime/types/types.d.ts +27 -4
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -96,7 +96,7 @@ const data = ref({
|
|
|
96
96
|
</script>
|
|
97
97
|
|
|
98
98
|
<template>
|
|
99
|
-
<
|
|
99
|
+
<YamlFormEditor v-model="data" />
|
|
100
100
|
</template>
|
|
101
101
|
```
|
|
102
102
|
|
|
@@ -104,7 +104,7 @@ const data = ref({
|
|
|
104
104
|
|
|
105
105
|
```vue
|
|
106
106
|
<script setup lang="ts">
|
|
107
|
-
import type { YamlFieldType } from '
|
|
107
|
+
import type { YamlFieldType } from '@type32/yaml-editor-form'
|
|
108
108
|
|
|
109
109
|
const customTypes: YamlFieldType[] = [
|
|
110
110
|
{
|
|
@@ -112,6 +112,7 @@ const customTypes: YamlFieldType[] = [
|
|
|
112
112
|
label: 'Image',
|
|
113
113
|
icon: 'i-lucide-image',
|
|
114
114
|
defaultValue: '',
|
|
115
|
+
baseType: 'string',
|
|
115
116
|
component: 'image'
|
|
116
117
|
}
|
|
117
118
|
]
|
|
@@ -123,11 +124,15 @@ const data = ref({
|
|
|
123
124
|
</script>
|
|
124
125
|
|
|
125
126
|
<template>
|
|
126
|
-
<
|
|
127
|
-
<template #field-image="{ modelValue, readonly }">
|
|
128
|
-
<MyImagePicker
|
|
127
|
+
<YamlFormEditor v-model="data" :field-types="customTypes">
|
|
128
|
+
<template #field-image="{ modelValue, readonly, updateModelValue }">
|
|
129
|
+
<MyImagePicker
|
|
130
|
+
:model-value="modelValue"
|
|
131
|
+
:disabled="readonly"
|
|
132
|
+
@update:model-value="updateModelValue"
|
|
133
|
+
/>
|
|
129
134
|
</template>
|
|
130
|
-
</
|
|
135
|
+
</YamlFormEditor>
|
|
131
136
|
</template>
|
|
132
137
|
```
|
|
133
138
|
|
|
@@ -136,7 +141,7 @@ const data = ref({
|
|
|
136
141
|
### Component Hierarchy
|
|
137
142
|
|
|
138
143
|
```
|
|
139
|
-
|
|
144
|
+
YamlFormEditor.vue (Entry Point)
|
|
140
145
|
└── YamlFormField.vue (Recursive Component)
|
|
141
146
|
├── YamlFieldInput.vue (Simple Types)
|
|
142
147
|
│ ├── UInput (string)
|
|
@@ -147,7 +152,7 @@ YamlForm.vue (Entry Point)
|
|
|
147
152
|
│ ├── UInputTags (string-array)
|
|
148
153
|
│ └── Custom Slots (user-defined)
|
|
149
154
|
└── YamlFormField.vue (Complex Types - Recursive)
|
|
150
|
-
├──
|
|
155
|
+
├── YamlCollapsible (objects/arrays)
|
|
151
156
|
└── Array/Object rendering
|
|
152
157
|
```
|
|
153
158
|
|
|
@@ -160,7 +165,7 @@ YamlFieldInput (v-model)
|
|
|
160
165
|
↓
|
|
161
166
|
YamlFormField (v-model)
|
|
162
167
|
↓
|
|
163
|
-
|
|
168
|
+
YamlFormEditor (v-model)
|
|
164
169
|
↓
|
|
165
170
|
Parent Component (data binding)
|
|
166
171
|
```
|
|
@@ -179,7 +184,7 @@ Components (Rendering)
|
|
|
179
184
|
|
|
180
185
|
## Component API
|
|
181
186
|
|
|
182
|
-
###
|
|
187
|
+
### YamlFormEditor
|
|
183
188
|
|
|
184
189
|
Main entry point for the editor.
|
|
185
190
|
|
|
@@ -207,8 +212,33 @@ Main entry point for the editor.
|
|
|
207
212
|
All custom field component slots are supported:
|
|
208
213
|
|
|
209
214
|
```vue
|
|
210
|
-
<template #field-{component}="{ modelValue, readonly, valueType }">
|
|
215
|
+
<template #field-{component}="{ modelValue, readonly, valueType, updateModelValue }">
|
|
211
216
|
<!-- Your custom component -->
|
|
217
|
+
<!-- Use :model-value and @update:model-value, NOT v-model -->
|
|
218
|
+
</template>
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
**Slot Props:**
|
|
222
|
+
- `modelValue`: Current field value (read-only prop)
|
|
223
|
+
- `readonly`: Whether field is in read-only mode
|
|
224
|
+
- `valueType`: Type identifier string
|
|
225
|
+
- `updateModelValue`: Function to update value: `(newValue) => void`
|
|
226
|
+
|
|
227
|
+
**Important:** You cannot use `v-model` on slot props (they're read-only). Use `:model-value` and `@update:model-value` instead:
|
|
228
|
+
|
|
229
|
+
```vue
|
|
230
|
+
<!-- ❌ WRONG - v-model doesn't work on slot props -->
|
|
231
|
+
<template #field-color="{ modelValue, readonly }">
|
|
232
|
+
<UColorPicker v-model="modelValue" :disabled="readonly" />
|
|
233
|
+
</template>
|
|
234
|
+
|
|
235
|
+
<!-- ✅ CORRECT - use updateModelValue function -->
|
|
236
|
+
<template #field-color="{ modelValue, readonly, updateModelValue }">
|
|
237
|
+
<UColorPicker
|
|
238
|
+
:model-value="modelValue"
|
|
239
|
+
:disabled="readonly"
|
|
240
|
+
@update:model-value="updateModelValue"
|
|
241
|
+
/>
|
|
212
242
|
</template>
|
|
213
243
|
```
|
|
214
244
|
|
|
@@ -240,7 +270,7 @@ Recursive component that handles individual fields.
|
|
|
240
270
|
|
|
241
271
|
#### Slots
|
|
242
272
|
|
|
243
|
-
Same as
|
|
273
|
+
Same as YamlFormEditor - all custom field slots are forwarded through the recursive hierarchy.
|
|
244
274
|
|
|
245
275
|
### YamlFieldInput
|
|
246
276
|
|
|
@@ -268,26 +298,73 @@ Renders input components for simple types.
|
|
|
268
298
|
#### Slots
|
|
269
299
|
|
|
270
300
|
```vue
|
|
271
|
-
<template #field-{component}="{ modelValue, readonly, valueType }">
|
|
301
|
+
<template #field-{component}="{ modelValue, readonly, valueType, updateModelValue }">
|
|
272
302
|
<!-- Custom input component -->
|
|
303
|
+
<!-- Use updateModelValue function for two-way binding -->
|
|
273
304
|
</template>
|
|
274
305
|
```
|
|
275
306
|
|
|
307
|
+
**Slot Props:**
|
|
308
|
+
- `modelValue`: Current value (read-only)
|
|
309
|
+
- `readonly`: Whether field is read-only
|
|
310
|
+
- `valueType`: Type identifier
|
|
311
|
+
- `updateModelValue`: Update function `(val) => void`
|
|
312
|
+
|
|
276
313
|
## Field Types
|
|
277
314
|
|
|
278
315
|
### Type Definition
|
|
279
316
|
|
|
280
317
|
```typescript
|
|
318
|
+
// Valid base types (type-safe!)
|
|
319
|
+
type YamlBaseType =
|
|
320
|
+
| 'string' // Text primitives
|
|
321
|
+
| 'number' // Numeric primitives
|
|
322
|
+
| 'boolean' // Boolean primitives
|
|
323
|
+
| 'date' // Date without time
|
|
324
|
+
| 'datetime' // Date with time
|
|
325
|
+
| 'string-array' // Array of strings (tags)
|
|
326
|
+
| 'array' // Generic array
|
|
327
|
+
| 'object' // Generic object
|
|
328
|
+
| 'null' // Null value
|
|
329
|
+
|
|
281
330
|
interface YamlFieldType {
|
|
282
|
-
type: string // Unique type identifier
|
|
331
|
+
type: string // Unique type identifier (e.g., 'color', 'email')
|
|
283
332
|
label: string // Display name in dropdowns
|
|
284
333
|
icon: string // Lucide icon name (i-lucide-*)
|
|
285
334
|
defaultValue: any // Default value or factory function
|
|
335
|
+
baseType: YamlBaseType // REQUIRED: Base type for conversion rules
|
|
286
336
|
component?: string // Optional: slot name for custom rendering
|
|
287
337
|
detect?: (value: any) => boolean // Optional: auto-detection function
|
|
288
338
|
}
|
|
289
339
|
```
|
|
290
340
|
|
|
341
|
+
**The `baseType` Field (Type-Safe!):**
|
|
342
|
+
|
|
343
|
+
The `baseType` field is **required** and must be one of the predefined base types. This provides:
|
|
344
|
+
- ✅ **TypeScript autocomplete** - IntelliSense suggests valid base types
|
|
345
|
+
- ✅ **Compile-time safety** - Typos are caught immediately
|
|
346
|
+
- ✅ **Conversion inheritance** - Custom types inherit conversion rules from their base
|
|
347
|
+
- ✅ **Clear semantics** - Explicit relationship between custom and base types
|
|
348
|
+
|
|
349
|
+
**Examples:**
|
|
350
|
+
```typescript
|
|
351
|
+
// ✅ Valid - 'string' is a valid YamlBaseType
|
|
352
|
+
{ type: 'color', baseType: 'string' }
|
|
353
|
+
|
|
354
|
+
// ✅ Valid - 'number' is a valid YamlBaseType
|
|
355
|
+
{ type: 'percentage', baseType: 'number' }
|
|
356
|
+
|
|
357
|
+
// ❌ Invalid - TypeScript error (not a valid base type)
|
|
358
|
+
{ type: 'custom', baseType: 'invalid' } // Type error!
|
|
359
|
+
```
|
|
360
|
+
|
|
361
|
+
**Conversion Inheritance:**
|
|
362
|
+
- A `color` type with `baseType: 'string'` can convert to/from anything a string can
|
|
363
|
+
- A `percentage` type with `baseType: 'number'` inherits number conversions
|
|
364
|
+
- Custom types can also convert directly to/from their base type
|
|
365
|
+
|
|
366
|
+
This enables powerful type hierarchies without duplicating conversion logic.
|
|
367
|
+
|
|
291
368
|
### Built-in Types
|
|
292
369
|
|
|
293
370
|
```typescript
|
|
@@ -401,6 +478,7 @@ export const DEFAULT_FIELD_TYPES: YamlFieldType[] = [
|
|
|
401
478
|
label: 'Email',
|
|
402
479
|
icon: 'i-lucide-mail',
|
|
403
480
|
defaultValue: '',
|
|
481
|
+
baseType: 'string',
|
|
404
482
|
detect: (value) => typeof value === 'string' && /^[^@]+@[^@]+/.test(value)
|
|
405
483
|
}
|
|
406
484
|
]
|
|
@@ -421,13 +499,14 @@ const customTypes: YamlFieldType[] = [
|
|
|
421
499
|
label: 'URL',
|
|
422
500
|
icon: 'i-lucide-link',
|
|
423
501
|
defaultValue: 'https://',
|
|
502
|
+
baseType: 'string',
|
|
424
503
|
detect: (value) => typeof value === 'string' && value.startsWith('http')
|
|
425
504
|
}
|
|
426
505
|
]
|
|
427
506
|
```
|
|
428
507
|
|
|
429
508
|
```vue
|
|
430
|
-
<
|
|
509
|
+
<YamlFormEditor v-model="data" :field-types="customTypes" />
|
|
431
510
|
```
|
|
432
511
|
|
|
433
512
|
### Adding a Runtime Type (With Custom Component)
|
|
@@ -439,23 +518,25 @@ const customTypes: YamlFieldType[] = [
|
|
|
439
518
|
label: 'Color',
|
|
440
519
|
icon: 'i-lucide-palette',
|
|
441
520
|
defaultValue: '#000000',
|
|
521
|
+
baseType: 'string', // Inherits string conversions
|
|
442
522
|
component: 'color', // Enables slot
|
|
443
|
-
detect: (value) => /^#[0-9A-Fa-f]{6}$/.test(value)
|
|
523
|
+
detect: (value) => typeof value === 'string' && /^#[0-9A-Fa-f]{6}$/.test(value)
|
|
444
524
|
}
|
|
445
525
|
]
|
|
446
526
|
```
|
|
447
527
|
|
|
448
528
|
```vue
|
|
449
|
-
<
|
|
450
|
-
<template #field-color="{ modelValue, readonly }">
|
|
529
|
+
<YamlFormEditor v-model="data" :field-types="customTypes">
|
|
530
|
+
<template #field-color="{ modelValue, readonly, updateModelValue }">
|
|
451
531
|
<input
|
|
452
532
|
type="color"
|
|
453
|
-
|
|
533
|
+
:value="modelValue"
|
|
454
534
|
:disabled="readonly"
|
|
535
|
+
@input="(e) => updateModelValue(e.target.value)"
|
|
455
536
|
class="w-full h-10 rounded"
|
|
456
537
|
/>
|
|
457
538
|
</template>
|
|
458
|
-
</
|
|
539
|
+
</YamlFormEditor>
|
|
459
540
|
```
|
|
460
541
|
|
|
461
542
|
### Overriding Built-in Types
|
|
@@ -467,17 +548,22 @@ const customTypes: YamlFieldType[] = [
|
|
|
467
548
|
label: 'Rich Text',
|
|
468
549
|
icon: 'i-lucide-file-text',
|
|
469
550
|
defaultValue: '',
|
|
551
|
+
baseType: 'string',
|
|
470
552
|
component: 'richtext' // Now uses custom component
|
|
471
553
|
}
|
|
472
554
|
]
|
|
473
555
|
```
|
|
474
556
|
|
|
475
557
|
```vue
|
|
476
|
-
<
|
|
477
|
-
<template #field-richtext="{ modelValue, readonly }">
|
|
478
|
-
<MyRichTextEditor
|
|
558
|
+
<YamlFormEditor v-model="data" :field-types="customTypes">
|
|
559
|
+
<template #field-richtext="{ modelValue, readonly, updateModelValue }">
|
|
560
|
+
<MyRichTextEditor
|
|
561
|
+
:model-value="modelValue"
|
|
562
|
+
:read-only="readonly"
|
|
563
|
+
@update:model-value="updateModelValue"
|
|
564
|
+
/>
|
|
479
565
|
</template>
|
|
480
|
-
</
|
|
566
|
+
</YamlFormEditor>
|
|
481
567
|
```
|
|
482
568
|
|
|
483
569
|
## Schema System
|
|
@@ -558,7 +644,7 @@ const config = ref({
|
|
|
558
644
|
</script>
|
|
559
645
|
|
|
560
646
|
<template>
|
|
561
|
-
<
|
|
647
|
+
<YamlFormEditor v-model="config" />
|
|
562
648
|
</template>
|
|
563
649
|
```
|
|
564
650
|
|
|
@@ -577,7 +663,7 @@ const article = ref({
|
|
|
577
663
|
</script>
|
|
578
664
|
|
|
579
665
|
<template>
|
|
580
|
-
<
|
|
666
|
+
<YamlFormEditor v-model="article" />
|
|
581
667
|
</template>
|
|
582
668
|
```
|
|
583
669
|
|
|
@@ -594,7 +680,7 @@ const data = ref({
|
|
|
594
680
|
</script>
|
|
595
681
|
|
|
596
682
|
<template>
|
|
597
|
-
<
|
|
683
|
+
<YamlFormEditor v-model="data" />
|
|
598
684
|
</template>
|
|
599
685
|
```
|
|
600
686
|
|
|
@@ -602,7 +688,7 @@ const data = ref({
|
|
|
602
688
|
|
|
603
689
|
```vue
|
|
604
690
|
<script setup lang="ts">
|
|
605
|
-
import type { YamlFieldType } from '
|
|
691
|
+
import type { YamlFieldType } from '@type32/yaml-editor-form'
|
|
606
692
|
|
|
607
693
|
// Define custom types
|
|
608
694
|
const customTypes: YamlFieldType[] = [
|
|
@@ -611,6 +697,7 @@ const customTypes: YamlFieldType[] = [
|
|
|
611
697
|
label: 'Image',
|
|
612
698
|
icon: 'i-lucide-image',
|
|
613
699
|
defaultValue: '',
|
|
700
|
+
baseType: 'string',
|
|
614
701
|
component: 'image'
|
|
615
702
|
},
|
|
616
703
|
{
|
|
@@ -618,6 +705,7 @@ const customTypes: YamlFieldType[] = [
|
|
|
618
705
|
label: 'Markdown',
|
|
619
706
|
icon: 'i-lucide-file-text',
|
|
620
707
|
defaultValue: '',
|
|
708
|
+
baseType: 'string',
|
|
621
709
|
component: 'markdown'
|
|
622
710
|
}
|
|
623
711
|
]
|
|
@@ -630,23 +718,25 @@ const post = ref({
|
|
|
630
718
|
</script>
|
|
631
719
|
|
|
632
720
|
<template>
|
|
633
|
-
<
|
|
721
|
+
<YamlFormEditor v-model="post" :field-types="customTypes">
|
|
634
722
|
<!-- Image picker component -->
|
|
635
|
-
<template #field-image="{ modelValue, readonly }">
|
|
723
|
+
<template #field-image="{ modelValue, readonly, updateModelValue }">
|
|
636
724
|
<MyImagePicker
|
|
637
|
-
|
|
725
|
+
:model-value="modelValue"
|
|
638
726
|
:disabled="readonly"
|
|
727
|
+
@update:model-value="updateModelValue"
|
|
639
728
|
/>
|
|
640
729
|
</template>
|
|
641
730
|
|
|
642
731
|
<!-- Markdown editor component -->
|
|
643
|
-
<template #field-markdown="{ modelValue, readonly }">
|
|
732
|
+
<template #field-markdown="{ modelValue, readonly, updateModelValue }">
|
|
644
733
|
<MyMarkdownEditor
|
|
645
|
-
|
|
734
|
+
:model-value="modelValue"
|
|
646
735
|
:read-only="readonly"
|
|
736
|
+
@update:model-value="updateModelValue"
|
|
647
737
|
/>
|
|
648
738
|
</template>
|
|
649
|
-
</
|
|
739
|
+
</YamlFormEditor>
|
|
650
740
|
</template>
|
|
651
741
|
```
|
|
652
742
|
|
|
@@ -660,19 +750,21 @@ const customTypes: YamlFieldType[] = [
|
|
|
660
750
|
label: 'UUID',
|
|
661
751
|
icon: 'i-lucide-fingerprint',
|
|
662
752
|
defaultValue: () => crypto.randomUUID(), // Function called each time
|
|
753
|
+
baseType: 'string',
|
|
663
754
|
detect: (v) => /^[0-9a-f]{8}-[0-9a-f]{4}-/.test(v)
|
|
664
755
|
},
|
|
665
756
|
{
|
|
666
757
|
type: 'timestamp',
|
|
667
758
|
label: 'Timestamp',
|
|
668
759
|
icon: 'i-lucide-clock',
|
|
669
|
-
defaultValue: () => new Date().toISOString()
|
|
760
|
+
defaultValue: () => new Date().toISOString(),
|
|
761
|
+
baseType: 'string'
|
|
670
762
|
}
|
|
671
763
|
]
|
|
672
764
|
</script>
|
|
673
765
|
|
|
674
766
|
<template>
|
|
675
|
-
<
|
|
767
|
+
<YamlFormEditor v-model="data" :field-types="customTypes" />
|
|
676
768
|
</template>
|
|
677
769
|
```
|
|
678
770
|
|
|
@@ -680,7 +772,7 @@ const customTypes: YamlFieldType[] = [
|
|
|
680
772
|
|
|
681
773
|
```vue
|
|
682
774
|
<template>
|
|
683
|
-
<
|
|
775
|
+
<YamlFormEditor v-model="data" readonly />
|
|
684
776
|
</template>
|
|
685
777
|
```
|
|
686
778
|
|
|
@@ -712,7 +804,7 @@ const complexData = ref({
|
|
|
712
804
|
</script>
|
|
713
805
|
|
|
714
806
|
<template>
|
|
715
|
-
<
|
|
807
|
+
<YamlFormEditor v-model="complexData" />
|
|
716
808
|
</template>
|
|
717
809
|
```
|
|
718
810
|
|
|
@@ -720,10 +812,10 @@ const complexData = ref({
|
|
|
720
812
|
|
|
721
813
|
```
|
|
722
814
|
components/
|
|
723
|
-
├──
|
|
815
|
+
├── YamlFormEditor.vue ← Entry component
|
|
724
816
|
├── YamlFormField.vue ← Recursive field component
|
|
725
817
|
├── YamlFieldInput.vue ← Input rendering component
|
|
726
|
-
└──
|
|
818
|
+
└── YamlCollapsible.vue ← Collapsible UI component
|
|
727
819
|
|
|
728
820
|
composables/
|
|
729
821
|
└── useYamlFieldTypes.ts ← Type registry & composable
|
|
@@ -750,6 +842,7 @@ types/
|
|
|
750
842
|
label: 'My Type',
|
|
751
843
|
icon: 'i-lucide-my-icon',
|
|
752
844
|
defaultValue: 'default',
|
|
845
|
+
baseType: 'string', // Required: specify base type for conversions
|
|
753
846
|
detect: (value) => /* detection logic */
|
|
754
847
|
}
|
|
755
848
|
```
|
|
@@ -903,27 +996,89 @@ interface YamlFieldInputProps {
|
|
|
903
996
|
|
|
904
997
|
### Slot Forwarding
|
|
905
998
|
|
|
906
|
-
Slots are automatically forwarded through the component hierarchy:
|
|
999
|
+
Slots are automatically forwarded through the component hierarchy using Vue 3's dynamic slot forwarding:
|
|
1000
|
+
|
|
1001
|
+
```
|
|
1002
|
+
YamlFormEditor (receives slot from parent)
|
|
1003
|
+
↓ forwards all slots with v-bind="slotProps"
|
|
1004
|
+
YamlFormField (receives & forwards slots)
|
|
1005
|
+
↓ forwards all slots with v-bind="slotProps"
|
|
1006
|
+
↓ (recursively for nested structures)
|
|
1007
|
+
YamlFieldInput (terminal - uses slot)
|
|
1008
|
+
↓ renders slot: #field-{component}
|
|
1009
|
+
↓ provides props: { modelValue, readonly, valueType, updateModelValue }
|
|
1010
|
+
Custom Component
|
|
1011
|
+
```
|
|
1012
|
+
|
|
1013
|
+
**How It Works:**
|
|
1014
|
+
|
|
1015
|
+
1. **YamlFormEditor** (lines 89-92): Receives slots and forwards to YamlFormField
|
|
1016
|
+
2. **YamlFormField** (lines 634-636): Forwards slots to YamlFieldInput OR itself (for recursion)
|
|
1017
|
+
3. **YamlFieldInput** (lines 88-95): Final destination - renders slot with props
|
|
1018
|
+
|
|
1019
|
+
**Slot Props Flow:**
|
|
907
1020
|
|
|
1021
|
+
The `updateModelValue` function is created at the YamlFieldInput level and allows your custom component to update the value:
|
|
1022
|
+
|
|
1023
|
+
```typescript
|
|
1024
|
+
// In YamlFieldInput.vue
|
|
1025
|
+
:update-model-value="(val: YamlValue) => modelValue = val"
|
|
908
1026
|
```
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
1027
|
+
|
|
1028
|
+
This function captures the parent's `modelValue` ref and updates it directly, maintaining reactivity throughout the hierarchy.
|
|
1029
|
+
|
|
1030
|
+
**Example with Nested Structure:**
|
|
1031
|
+
|
|
1032
|
+
```vue
|
|
1033
|
+
<!-- Works at any depth! -->
|
|
1034
|
+
<YamlFormEditor v-model="data" :field-types="customTypes">
|
|
1035
|
+
<template #field-color="{ modelValue, updateModelValue }">
|
|
1036
|
+
<UColorPicker
|
|
1037
|
+
:model-value="modelValue"
|
|
1038
|
+
@update:model-value="updateModelValue"
|
|
1039
|
+
/>
|
|
1040
|
+
</template>
|
|
1041
|
+
</YamlFormEditor>
|
|
914
1042
|
```
|
|
915
1043
|
|
|
916
|
-
|
|
1044
|
+
Even if your color field is deeply nested (`data.theme.colors.primary`), the slot works identically because slots are forwarded at every level.
|
|
917
1045
|
|
|
918
1046
|
### Type Priority
|
|
919
1047
|
|
|
920
|
-
|
|
1048
|
+
**Detection Order (NEW in v0.2.0):**
|
|
1049
|
+
|
|
1050
|
+
Custom types with `detect` functions are now checked **before** default types:
|
|
1051
|
+
|
|
1052
|
+
1. **Custom types** (checked first) - Your custom types take priority
|
|
1053
|
+
2. **Default types** (checked second) - Built-in types as fallback
|
|
1054
|
+
3. First matching type wins
|
|
1055
|
+
|
|
1056
|
+
This means:
|
|
1057
|
+
- ✅ Your `color` type will be detected before the default `string` type
|
|
1058
|
+
- ✅ Custom types override default detection behavior
|
|
1059
|
+
- ✅ More specific types should still have more specific detect functions
|
|
1060
|
+
|
|
1061
|
+
**Example:**
|
|
1062
|
+
```typescript
|
|
1063
|
+
// Custom color type checked FIRST
|
|
1064
|
+
const customTypes = [{
|
|
1065
|
+
type: 'color',
|
|
1066
|
+
baseType: 'string',
|
|
1067
|
+
detect: (v) => typeof v === 'string' && /^#[0-9A-Fa-f]{6}$/.test(v)
|
|
1068
|
+
}]
|
|
1069
|
+
|
|
1070
|
+
// Value '#FF0000' will match 'color' before 'string'
|
|
1071
|
+
```
|
|
1072
|
+
|
|
1073
|
+
**Base Type Conversions:**
|
|
1074
|
+
|
|
1075
|
+
When using `baseType`, conversion rules follow this logic:
|
|
921
1076
|
|
|
922
|
-
1.
|
|
923
|
-
2.
|
|
924
|
-
3.
|
|
1077
|
+
1. Can convert between type and its baseType (e.g., `color` ↔ `string`)
|
|
1078
|
+
2. Can convert to anything the baseType can (e.g., `color` → `number` because `string` → `number`)
|
|
1079
|
+
3. Custom conversion rules take precedence over inherited rules
|
|
925
1080
|
|
|
926
|
-
**Example order:**
|
|
1081
|
+
**Example order for default types:**
|
|
927
1082
|
```typescript
|
|
928
1083
|
[
|
|
929
1084
|
{ type: 'datetime', detect: (v) => isDateTimeString(v) }, // Specific
|
|
@@ -1023,6 +1178,320 @@ Requires:
|
|
|
1023
1178
|
- reka-ui (via Nuxt UI)
|
|
1024
1179
|
- tailwindcss (via Nuxt)
|
|
1025
1180
|
|
|
1181
|
+
## Advanced Examples
|
|
1182
|
+
|
|
1183
|
+
### Custom String Array Type
|
|
1184
|
+
|
|
1185
|
+
Here's an example of a custom string array type with autocomplete suggestions:
|
|
1186
|
+
|
|
1187
|
+
```vue
|
|
1188
|
+
<script setup lang="ts">
|
|
1189
|
+
import type { YamlFieldType } from '@type32/yaml-editor-form'
|
|
1190
|
+
|
|
1191
|
+
const customTypes: YamlFieldType[] = [
|
|
1192
|
+
{
|
|
1193
|
+
type: 'skills',
|
|
1194
|
+
label: 'Skills',
|
|
1195
|
+
icon: 'i-lucide-sparkles',
|
|
1196
|
+
defaultValue: [],
|
|
1197
|
+
baseType: 'string-array', // Inherits array conversions
|
|
1198
|
+
component: 'skills',
|
|
1199
|
+
detect: (value) => {
|
|
1200
|
+
// Auto-detect arrays with skill-like strings
|
|
1201
|
+
return Array.isArray(value) &&
|
|
1202
|
+
value.length > 0 &&
|
|
1203
|
+
value.every(v => typeof v === 'string' && v.length < 30)
|
|
1204
|
+
}
|
|
1205
|
+
}
|
|
1206
|
+
]
|
|
1207
|
+
|
|
1208
|
+
const profile = ref({
|
|
1209
|
+
name: 'John Doe',
|
|
1210
|
+
skills: ['Vue.js', 'TypeScript', 'Nuxt']
|
|
1211
|
+
})
|
|
1212
|
+
|
|
1213
|
+
// Predefined skill suggestions
|
|
1214
|
+
const skillSuggestions = [
|
|
1215
|
+
'Vue.js', 'React', 'Angular', 'TypeScript', 'JavaScript',
|
|
1216
|
+
'Node.js', 'Python', 'Nuxt', 'Next.js', 'Tailwind CSS'
|
|
1217
|
+
]
|
|
1218
|
+
</script>
|
|
1219
|
+
|
|
1220
|
+
<template>
|
|
1221
|
+
<YamlFormEditor v-model="profile" :field-types="customTypes">
|
|
1222
|
+
<!-- Custom skills input with autocomplete -->
|
|
1223
|
+
<template #field-skills="{ modelValue, readonly, updateModelValue }">
|
|
1224
|
+
<div class="space-y-2">
|
|
1225
|
+
<!-- Display current skills as badges -->
|
|
1226
|
+
<div class="flex flex-wrap gap-2">
|
|
1227
|
+
<UBadge
|
|
1228
|
+
v-for="(skill, index) in (modelValue as string[])"
|
|
1229
|
+
:key="index"
|
|
1230
|
+
color="primary"
|
|
1231
|
+
variant="soft"
|
|
1232
|
+
>
|
|
1233
|
+
{{ skill }}
|
|
1234
|
+
<UButton
|
|
1235
|
+
v-if="!readonly"
|
|
1236
|
+
icon="i-lucide-x"
|
|
1237
|
+
size="2xs"
|
|
1238
|
+
variant="ghost"
|
|
1239
|
+
:padded="false"
|
|
1240
|
+
@click="updateModelValue((modelValue as string[]).filter((_, i) => i !== index))"
|
|
1241
|
+
/>
|
|
1242
|
+
</UBadge>
|
|
1243
|
+
</div>
|
|
1244
|
+
|
|
1245
|
+
<!-- Add new skill with autocomplete -->
|
|
1246
|
+
<UInputMenu
|
|
1247
|
+
v-if="!readonly"
|
|
1248
|
+
:options="skillSuggestions"
|
|
1249
|
+
placeholder="Add skill..."
|
|
1250
|
+
@update:model-value="(newSkill: string) => {
|
|
1251
|
+
if (newSkill && !(modelValue as string[]).includes(newSkill)) {
|
|
1252
|
+
updateModelValue([...(modelValue as string[]), newSkill])
|
|
1253
|
+
}
|
|
1254
|
+
}"
|
|
1255
|
+
/>
|
|
1256
|
+
</div>
|
|
1257
|
+
</template>
|
|
1258
|
+
</YamlFormEditor>
|
|
1259
|
+
</template>
|
|
1260
|
+
```
|
|
1261
|
+
|
|
1262
|
+
### Custom Object Array Type
|
|
1263
|
+
|
|
1264
|
+
Here's an example of a custom object array type with card-based rendering:
|
|
1265
|
+
|
|
1266
|
+
```vue
|
|
1267
|
+
<script setup lang="ts">
|
|
1268
|
+
import type { YamlFieldType } from '@type32/yaml-editor-form'
|
|
1269
|
+
|
|
1270
|
+
const customTypes: YamlFieldType[] = [
|
|
1271
|
+
{
|
|
1272
|
+
type: 'contacts',
|
|
1273
|
+
label: 'Contacts',
|
|
1274
|
+
icon: 'i-lucide-users',
|
|
1275
|
+
defaultValue: [],
|
|
1276
|
+
baseType: 'array', // Inherits array conversions
|
|
1277
|
+
component: 'contacts',
|
|
1278
|
+
detect: (value) => {
|
|
1279
|
+
// Auto-detect arrays of contact-like objects
|
|
1280
|
+
return Array.isArray(value) &&
|
|
1281
|
+
value.length > 0 &&
|
|
1282
|
+
value.every(v =>
|
|
1283
|
+
v && typeof v === 'object' &&
|
|
1284
|
+
('name' in v || 'email' in v)
|
|
1285
|
+
)
|
|
1286
|
+
}
|
|
1287
|
+
}
|
|
1288
|
+
]
|
|
1289
|
+
|
|
1290
|
+
const data = ref({
|
|
1291
|
+
projectName: 'My Project',
|
|
1292
|
+
contacts: [
|
|
1293
|
+
{ name: 'Alice Johnson', email: 'alice@example.com', role: 'Designer' },
|
|
1294
|
+
{ name: 'Bob Smith', email: 'bob@example.com', role: 'Developer' }
|
|
1295
|
+
]
|
|
1296
|
+
})
|
|
1297
|
+
</script>
|
|
1298
|
+
|
|
1299
|
+
<template>
|
|
1300
|
+
<YamlFormEditor v-model="data" :field-types="customTypes">
|
|
1301
|
+
<!-- Custom contacts list with card UI -->
|
|
1302
|
+
<template #field-contacts="{ modelValue, readonly, updateModelValue }">
|
|
1303
|
+
<div class="space-y-3">
|
|
1304
|
+
<!-- Contact cards -->
|
|
1305
|
+
<UCard
|
|
1306
|
+
v-for="(contact, index) in (modelValue as any[])"
|
|
1307
|
+
:key="index"
|
|
1308
|
+
:ui="{ body: { padding: 'p-4' } }"
|
|
1309
|
+
>
|
|
1310
|
+
<div class="flex items-start justify-between gap-3">
|
|
1311
|
+
<div class="flex-1 space-y-2">
|
|
1312
|
+
<!-- Name -->
|
|
1313
|
+
<UInput
|
|
1314
|
+
:model-value="contact.name"
|
|
1315
|
+
placeholder="Name"
|
|
1316
|
+
:disabled="readonly"
|
|
1317
|
+
@update:model-value="(val: string) => {
|
|
1318
|
+
const updated = [...(modelValue as any[])]
|
|
1319
|
+
updated[index] = { ...contact, name: val }
|
|
1320
|
+
updateModelValue(updated)
|
|
1321
|
+
}"
|
|
1322
|
+
/>
|
|
1323
|
+
|
|
1324
|
+
<!-- Email -->
|
|
1325
|
+
<UInput
|
|
1326
|
+
:model-value="contact.email"
|
|
1327
|
+
type="email"
|
|
1328
|
+
placeholder="Email"
|
|
1329
|
+
icon="i-lucide-mail"
|
|
1330
|
+
:disabled="readonly"
|
|
1331
|
+
@update:model-value="(val: string) => {
|
|
1332
|
+
const updated = [...(modelValue as any[])]
|
|
1333
|
+
updated[index] = { ...contact, email: val }
|
|
1334
|
+
updateModelValue(updated)
|
|
1335
|
+
}"
|
|
1336
|
+
/>
|
|
1337
|
+
|
|
1338
|
+
<!-- Role -->
|
|
1339
|
+
<UInput
|
|
1340
|
+
:model-value="contact.role"
|
|
1341
|
+
placeholder="Role"
|
|
1342
|
+
icon="i-lucide-briefcase"
|
|
1343
|
+
:disabled="readonly"
|
|
1344
|
+
@update:model-value="(val: string) => {
|
|
1345
|
+
const updated = [...(modelValue as any[])]
|
|
1346
|
+
updated[index] = { ...contact, role: val }
|
|
1347
|
+
updateModelValue(updated)
|
|
1348
|
+
}"
|
|
1349
|
+
/>
|
|
1350
|
+
</div>
|
|
1351
|
+
|
|
1352
|
+
<!-- Remove button -->
|
|
1353
|
+
<UButton
|
|
1354
|
+
v-if="!readonly"
|
|
1355
|
+
icon="i-lucide-trash-2"
|
|
1356
|
+
color="red"
|
|
1357
|
+
variant="ghost"
|
|
1358
|
+
size="sm"
|
|
1359
|
+
@click="updateModelValue((modelValue as any[]).filter((_, i) => i !== index))"
|
|
1360
|
+
/>
|
|
1361
|
+
</div>
|
|
1362
|
+
</UCard>
|
|
1363
|
+
|
|
1364
|
+
<!-- Add contact button -->
|
|
1365
|
+
<UButton
|
|
1366
|
+
v-if="!readonly"
|
|
1367
|
+
icon="i-lucide-plus"
|
|
1368
|
+
label="Add Contact"
|
|
1369
|
+
variant="outline"
|
|
1370
|
+
block
|
|
1371
|
+
@click="updateModelValue([
|
|
1372
|
+
...(modelValue as any[]),
|
|
1373
|
+
{ name: '', email: '', role: '' }
|
|
1374
|
+
])"
|
|
1375
|
+
/>
|
|
1376
|
+
</div>
|
|
1377
|
+
</template>
|
|
1378
|
+
</YamlFormEditor>
|
|
1379
|
+
</template>
|
|
1380
|
+
```
|
|
1381
|
+
|
|
1382
|
+
### Combined Example
|
|
1383
|
+
|
|
1384
|
+
You can use both custom array types together:
|
|
1385
|
+
|
|
1386
|
+
```vue
|
|
1387
|
+
<script setup lang="ts">
|
|
1388
|
+
import type { YamlFieldType } from '@type32/yaml-editor-form'
|
|
1389
|
+
|
|
1390
|
+
const customTypes: YamlFieldType[] = [
|
|
1391
|
+
// Custom string array
|
|
1392
|
+
{
|
|
1393
|
+
type: 'tags',
|
|
1394
|
+
label: 'Tags',
|
|
1395
|
+
icon: 'i-lucide-tag',
|
|
1396
|
+
defaultValue: [],
|
|
1397
|
+
baseType: 'string-array',
|
|
1398
|
+
component: 'tags',
|
|
1399
|
+
detect: (v) => Array.isArray(v) && v.every(i => typeof i === 'string')
|
|
1400
|
+
},
|
|
1401
|
+
// Custom object array
|
|
1402
|
+
{
|
|
1403
|
+
type: 'team',
|
|
1404
|
+
label: 'Team Members',
|
|
1405
|
+
icon: 'i-lucide-users',
|
|
1406
|
+
defaultValue: [],
|
|
1407
|
+
baseType: 'array',
|
|
1408
|
+
component: 'team',
|
|
1409
|
+
detect: (v) => Array.isArray(v) && v.every(i => i?.name || i?.email)
|
|
1410
|
+
}
|
|
1411
|
+
]
|
|
1412
|
+
|
|
1413
|
+
const project = ref({
|
|
1414
|
+
name: 'Website Redesign',
|
|
1415
|
+
tags: ['frontend', 'design', 'urgent'],
|
|
1416
|
+
team: [
|
|
1417
|
+
{ name: 'Alice', email: 'alice@example.com' },
|
|
1418
|
+
{ name: 'Bob', email: 'bob@example.com' }
|
|
1419
|
+
]
|
|
1420
|
+
})
|
|
1421
|
+
</script>
|
|
1422
|
+
|
|
1423
|
+
<template>
|
|
1424
|
+
<YamlFormEditor v-model="project" :field-types="customTypes">
|
|
1425
|
+
<!-- String array implementation -->
|
|
1426
|
+
<template #field-tags="{ modelValue, readonly, updateModelValue }">
|
|
1427
|
+
<UInputTags
|
|
1428
|
+
:model-value="modelValue as string[]"
|
|
1429
|
+
:disabled="readonly"
|
|
1430
|
+
placeholder="Add tags..."
|
|
1431
|
+
@update:model-value="updateModelValue"
|
|
1432
|
+
/>
|
|
1433
|
+
</template>
|
|
1434
|
+
|
|
1435
|
+
<!-- Object array implementation -->
|
|
1436
|
+
<template #field-team="{ modelValue, readonly, updateModelValue }">
|
|
1437
|
+
<!-- Your custom team member UI here -->
|
|
1438
|
+
<div class="space-y-2">
|
|
1439
|
+
<div
|
|
1440
|
+
v-for="(member, idx) in (modelValue as any[])"
|
|
1441
|
+
:key="idx"
|
|
1442
|
+
class="flex gap-2"
|
|
1443
|
+
>
|
|
1444
|
+
<UInput
|
|
1445
|
+
:model-value="member.name"
|
|
1446
|
+
placeholder="Name"
|
|
1447
|
+
:disabled="readonly"
|
|
1448
|
+
@update:model-value="(val: string) => {
|
|
1449
|
+
const updated = [...(modelValue as any[])]
|
|
1450
|
+
updated[idx] = { ...member, name: val }
|
|
1451
|
+
updateModelValue(updated)
|
|
1452
|
+
}"
|
|
1453
|
+
/>
|
|
1454
|
+
<UButton
|
|
1455
|
+
v-if="!readonly"
|
|
1456
|
+
icon="i-lucide-x"
|
|
1457
|
+
color="red"
|
|
1458
|
+
variant="ghost"
|
|
1459
|
+
@click="updateModelValue((modelValue as any[]).filter((_, i) => i !== idx))"
|
|
1460
|
+
/>
|
|
1461
|
+
</div>
|
|
1462
|
+
<UButton
|
|
1463
|
+
v-if="!readonly"
|
|
1464
|
+
icon="i-lucide-plus"
|
|
1465
|
+
label="Add Member"
|
|
1466
|
+
size="sm"
|
|
1467
|
+
@click="updateModelValue([...(modelValue as any[]), { name: '', email: '' }])"
|
|
1468
|
+
/>
|
|
1469
|
+
</div>
|
|
1470
|
+
</template>
|
|
1471
|
+
</YamlFormEditor>
|
|
1472
|
+
</template>
|
|
1473
|
+
```
|
|
1474
|
+
|
|
1475
|
+
### Key Patterns for Array Types
|
|
1476
|
+
|
|
1477
|
+
**String Arrays (`baseType: 'string-array'`):**
|
|
1478
|
+
- Use for specialized tag inputs, category lists, etc.
|
|
1479
|
+
- Can convert to/from regular arrays and strings
|
|
1480
|
+
- Good for: skills, tags, categories, keywords
|
|
1481
|
+
|
|
1482
|
+
**Object Arrays (`baseType: 'array'`):**
|
|
1483
|
+
- Use for collections with structured data
|
|
1484
|
+
- Provide custom UI for adding/editing/removing items
|
|
1485
|
+
- Good for: contacts, team members, products, events
|
|
1486
|
+
|
|
1487
|
+
**Important Notes:**
|
|
1488
|
+
1. **Type Assertions**: Use `(modelValue as string[])` or `(modelValue as any[])` for type safety
|
|
1489
|
+
2. **Immutability**: Always create new arrays when updating (spread operator `[...]`)
|
|
1490
|
+
3. **Index Management**: Track items by index for updates/deletions
|
|
1491
|
+
4. **Add Operations**: Spread existing array and add new items
|
|
1492
|
+
5. **Remove Operations**: Use `filter()` to remove by index
|
|
1493
|
+
6. **Update Operations**: Clone array, modify specific index, update entire array
|
|
1494
|
+
|
|
1026
1495
|
## License
|
|
1027
1496
|
|
|
1028
1497
|
This component is part of the Vertex project.
|
|
@@ -1046,11 +1515,11 @@ For issues, questions, or feature requests, refer to the main Vertex project doc
|
|
|
1046
1515
|
|
|
1047
1516
|
### Core Components
|
|
1048
1517
|
|
|
1049
|
-
- **
|
|
1518
|
+
- **YamlFormEditor**: Entry point component for the editor
|
|
1050
1519
|
- **YamlFormField**: Recursive component handling individual fields
|
|
1051
1520
|
- **YamlFieldInput**: Input rendering component for simple types
|
|
1521
|
+
- **YamlCollapsible**: UI component for collapsible sections
|
|
1052
1522
|
- **useYamlFieldTypes**: Composable for type registry and management
|
|
1053
|
-
- **Collapsible**: UI component for collapsible sections
|
|
1054
1523
|
|
|
1055
1524
|
### Key Concepts
|
|
1056
1525
|
|
|
@@ -1077,11 +1546,15 @@ For issues, questions, or feature requests, refer to the main Vertex project doc
|
|
|
1077
1546
|
|
|
1078
1547
|
**Add Custom Component:**
|
|
1079
1548
|
```vue
|
|
1080
|
-
<
|
|
1081
|
-
<template #field-{type}="
|
|
1082
|
-
<
|
|
1549
|
+
<YamlFormEditor v-model="data" :field-types="customTypes">
|
|
1550
|
+
<template #field-{type}="{ modelValue, readonly, updateModelValue }">
|
|
1551
|
+
<MyComponent
|
|
1552
|
+
:model-value="modelValue"
|
|
1553
|
+
:disabled="readonly"
|
|
1554
|
+
@update:model-value="updateModelValue"
|
|
1555
|
+
/>
|
|
1083
1556
|
</template>
|
|
1084
|
-
</
|
|
1557
|
+
</YamlFormEditor>
|
|
1085
1558
|
```
|
|
1086
1559
|
|
|
1087
1560
|
**Type Conversion:**
|