@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 CHANGED
@@ -96,7 +96,7 @@ const data = ref({
96
96
  </script>
97
97
 
98
98
  <template>
99
- <YamlForm v-model="data" />
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 './useYamlFieldTypes'
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
- <YamlForm v-model="data" :field-types="customTypes">
127
- <template #field-image="{ modelValue, readonly }">
128
- <MyImagePicker v-model="modelValue" :disabled="readonly" />
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
- </YamlForm>
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
- YamlForm.vue (Entry Point)
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
- ├── Collapsible (objects/arrays)
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
- YamlForm (v-model)
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
- ### YamlForm
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 YamlForm - all custom field slots are forwarded.
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
- <YamlForm v-model="data" :field-types="customTypes" />
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
- <YamlForm v-model="data" :field-types="customTypes">
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
- v-model="modelValue"
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
- </YamlForm>
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
- <YamlForm v-model="data" :field-types="customTypes">
477
- <template #field-richtext="{ modelValue, readonly }">
478
- <MyRichTextEditor v-model="modelValue" :read-only="readonly" />
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
- </YamlForm>
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
- <YamlForm v-model="config" />
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
- <YamlForm v-model="article" />
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
- <YamlForm v-model="data" />
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 './useYamlFieldTypes'
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
- <YamlForm v-model="post" :field-types="customTypes">
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
- v-model="modelValue"
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
- v-model="modelValue"
734
+ :model-value="modelValue"
646
735
  :read-only="readonly"
736
+ @update:model-value="updateModelValue"
647
737
  />
648
738
  </template>
649
- </YamlForm>
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
- <YamlForm v-model="data" :field-types="customTypes" />
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
- <YamlForm v-model="data" readonly />
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
- <YamlForm v-model="complexData" />
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
- ├── YamlForm.vue ← Entry component
815
+ ├── YamlFormEditor.vue ← Entry component
724
816
  ├── YamlFormField.vue ← Recursive field component
725
817
  ├── YamlFieldInput.vue ← Input rendering component
726
- └── Collapsible.vue ← Collapsible UI component
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
- YamlForm (defines slot)
910
- forwards
911
- YamlFormField (forwards slot)
912
- forwards
913
- YamlFieldInput (uses slot)
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
- This allows custom components to work at any nesting level.
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
- When multiple types have `detect` functions that match:
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. Types are checked in array order
923
- 2. First matching type wins
924
- 3. More specific types should come before general types
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
- - **YamlForm**: Entry point component for the editor
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
- <YamlForm>
1081
- <template #field-{type}="props">
1082
- <Component v-bind="props" />
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
- </YamlForm>
1557
+ </YamlFormEditor>
1085
1558
  ```
1086
1559
 
1087
1560
  **Type Conversion:**