@type32/yaml-editor-form 0.1.4 → 0.1.6

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
@@ -100,6 +100,35 @@ const data = ref({
100
100
  </template>
101
101
  ```
102
102
 
103
+ ### With Writable Computed
104
+
105
+ The editor fully supports writable computed refs for v-model:
106
+
107
+ ```vue
108
+ <script setup lang="ts">
109
+ const rawData = ref({
110
+ title: 'Article',
111
+ published: false
112
+ })
113
+
114
+ // Writable computed with getter/setter
115
+ const data = computed({
116
+ get: () => rawData.value,
117
+ set: (value) => {
118
+ console.log('Data updated:', value)
119
+ rawData.value = value
120
+ // You can add validation, transformations, API calls, etc.
121
+ }
122
+ })
123
+ </script>
124
+
125
+ <template>
126
+ <YamlFormEditor v-model="data" />
127
+ </template>
128
+ ```
129
+
130
+ **Note:** The editor properly triggers computed setters by creating new object references instead of mutating in place, ensuring full compatibility with writable computed refs.
131
+
103
132
  ### With Custom Field Types
104
133
 
105
134
  ```vue
@@ -112,6 +141,7 @@ const customTypes: YamlFieldType[] = [
112
141
  label: 'Image',
113
142
  icon: 'i-lucide-image',
114
143
  defaultValue: '',
144
+ baseType: 'string',
115
145
  component: 'image'
116
146
  }
117
147
  ]
@@ -477,6 +507,7 @@ export const DEFAULT_FIELD_TYPES: YamlFieldType[] = [
477
507
  label: 'Email',
478
508
  icon: 'i-lucide-mail',
479
509
  defaultValue: '',
510
+ baseType: 'string',
480
511
  detect: (value) => typeof value === 'string' && /^[^@]+@[^@]+/.test(value)
481
512
  }
482
513
  ]
@@ -497,6 +528,7 @@ const customTypes: YamlFieldType[] = [
497
528
  label: 'URL',
498
529
  icon: 'i-lucide-link',
499
530
  defaultValue: 'https://',
531
+ baseType: 'string',
500
532
  detect: (value) => typeof value === 'string' && value.startsWith('http')
501
533
  }
502
534
  ]
@@ -545,6 +577,7 @@ const customTypes: YamlFieldType[] = [
545
577
  label: 'Rich Text',
546
578
  icon: 'i-lucide-file-text',
547
579
  defaultValue: '',
580
+ baseType: 'string',
548
581
  component: 'richtext' // Now uses custom component
549
582
  }
550
583
  ]
@@ -693,6 +726,7 @@ const customTypes: YamlFieldType[] = [
693
726
  label: 'Image',
694
727
  icon: 'i-lucide-image',
695
728
  defaultValue: '',
729
+ baseType: 'string',
696
730
  component: 'image'
697
731
  },
698
732
  {
@@ -700,6 +734,7 @@ const customTypes: YamlFieldType[] = [
700
734
  label: 'Markdown',
701
735
  icon: 'i-lucide-file-text',
702
736
  defaultValue: '',
737
+ baseType: 'string',
703
738
  component: 'markdown'
704
739
  }
705
740
  ]
@@ -744,13 +779,15 @@ const customTypes: YamlFieldType[] = [
744
779
  label: 'UUID',
745
780
  icon: 'i-lucide-fingerprint',
746
781
  defaultValue: () => crypto.randomUUID(), // Function called each time
782
+ baseType: 'string',
747
783
  detect: (v) => /^[0-9a-f]{8}-[0-9a-f]{4}-/.test(v)
748
784
  },
749
785
  {
750
786
  type: 'timestamp',
751
787
  label: 'Timestamp',
752
788
  icon: 'i-lucide-clock',
753
- defaultValue: () => new Date().toISOString()
789
+ defaultValue: () => new Date().toISOString(),
790
+ baseType: 'string'
754
791
  }
755
792
  ]
756
793
  </script>
@@ -834,6 +871,7 @@ types/
834
871
  label: 'My Type',
835
872
  icon: 'i-lucide-my-icon',
836
873
  defaultValue: 'default',
874
+ baseType: 'string', // Required: specify base type for conversions
837
875
  detect: (value) => /* detection logic */
838
876
  }
839
877
  ```
@@ -1078,12 +1116,42 @@ When using `baseType`, conversion rules follow this logic:
1078
1116
  ]
1079
1117
  ```
1080
1118
 
1119
+ ### Writable Computed Support
1120
+
1121
+ The editor is fully compatible with writable computed refs. All mutations create new object references instead of mutating in place:
1122
+
1123
+ ```typescript
1124
+ // ✅ Creates new object (triggers computed setter)
1125
+ data.value = { ...data.value, newField: 'value' }
1126
+
1127
+ // ❌ Direct mutation (doesn't trigger computed setter)
1128
+ data.value.newField = 'value' // Old approach - now fixed!
1129
+ ```
1130
+
1131
+ **Use Cases for Writable Computed:**
1132
+ - Validation before saving
1133
+ - Transform data on save (e.g., serialize dates)
1134
+ - Sync with external state management (Pinia, Vuex)
1135
+ - Trigger side effects on changes (API calls, logging)
1136
+ - Implement undo/redo functionality
1137
+
1138
+ **Example with Pinia:**
1139
+ ```typescript
1140
+ const store = useMyStore()
1141
+
1142
+ const data = computed({
1143
+ get: () => store.formData,
1144
+ set: (value) => store.updateFormData(value)
1145
+ })
1146
+ ```
1147
+
1081
1148
  ### Performance Considerations
1082
1149
 
1083
1150
  **Reactivity:**
1084
1151
  - Uses Vue 3 `ref` and `computed` for optimal reactivity
1085
1152
  - Deep watching is used only where necessary
1086
1153
  - Recursive rendering is optimized with `v-if` conditionals
1154
+ - **Immutable updates** ensure computed setters are triggered properly
1087
1155
 
1088
1156
  **Large Arrays:**
1089
1157
  - Each array item is independently reactive
@@ -1094,6 +1162,7 @@ When using `baseType`, conversion rules follow this logic:
1094
1162
  - Date helper functions are minimal
1095
1163
  - No global state except type registry
1096
1164
  - Components clean up properly on unmount
1165
+ - Immutable updates create minimal object copies (spread operator is fast)
1097
1166
 
1098
1167
  ### Validation (Future)
1099
1168
 
@@ -1169,6 +1238,320 @@ Requires:
1169
1238
  - reka-ui (via Nuxt UI)
1170
1239
  - tailwindcss (via Nuxt)
1171
1240
 
1241
+ ## Advanced Examples
1242
+
1243
+ ### Custom String Array Type
1244
+
1245
+ Here's an example of a custom string array type with autocomplete suggestions:
1246
+
1247
+ ```vue
1248
+ <script setup lang="ts">
1249
+ import type { YamlFieldType } from '@type32/yaml-editor-form'
1250
+
1251
+ const customTypes: YamlFieldType[] = [
1252
+ {
1253
+ type: 'skills',
1254
+ label: 'Skills',
1255
+ icon: 'i-lucide-sparkles',
1256
+ defaultValue: [],
1257
+ baseType: 'string-array', // Inherits array conversions
1258
+ component: 'skills',
1259
+ detect: (value) => {
1260
+ // Auto-detect arrays with skill-like strings
1261
+ return Array.isArray(value) &&
1262
+ value.length > 0 &&
1263
+ value.every(v => typeof v === 'string' && v.length < 30)
1264
+ }
1265
+ }
1266
+ ]
1267
+
1268
+ const profile = ref({
1269
+ name: 'John Doe',
1270
+ skills: ['Vue.js', 'TypeScript', 'Nuxt']
1271
+ })
1272
+
1273
+ // Predefined skill suggestions
1274
+ const skillSuggestions = [
1275
+ 'Vue.js', 'React', 'Angular', 'TypeScript', 'JavaScript',
1276
+ 'Node.js', 'Python', 'Nuxt', 'Next.js', 'Tailwind CSS'
1277
+ ]
1278
+ </script>
1279
+
1280
+ <template>
1281
+ <YamlFormEditor v-model="profile" :field-types="customTypes">
1282
+ <!-- Custom skills input with autocomplete -->
1283
+ <template #field-skills="{ modelValue, readonly, updateModelValue }">
1284
+ <div class="space-y-2">
1285
+ <!-- Display current skills as badges -->
1286
+ <div class="flex flex-wrap gap-2">
1287
+ <UBadge
1288
+ v-for="(skill, index) in (modelValue as string[])"
1289
+ :key="index"
1290
+ color="primary"
1291
+ variant="soft"
1292
+ >
1293
+ {{ skill }}
1294
+ <UButton
1295
+ v-if="!readonly"
1296
+ icon="i-lucide-x"
1297
+ size="2xs"
1298
+ variant="ghost"
1299
+ :padded="false"
1300
+ @click="updateModelValue((modelValue as string[]).filter((_, i) => i !== index))"
1301
+ />
1302
+ </UBadge>
1303
+ </div>
1304
+
1305
+ <!-- Add new skill with autocomplete -->
1306
+ <UInputMenu
1307
+ v-if="!readonly"
1308
+ :options="skillSuggestions"
1309
+ placeholder="Add skill..."
1310
+ @update:model-value="(newSkill: string) => {
1311
+ if (newSkill && !(modelValue as string[]).includes(newSkill)) {
1312
+ updateModelValue([...(modelValue as string[]), newSkill])
1313
+ }
1314
+ }"
1315
+ />
1316
+ </div>
1317
+ </template>
1318
+ </YamlFormEditor>
1319
+ </template>
1320
+ ```
1321
+
1322
+ ### Custom Object Array Type
1323
+
1324
+ Here's an example of a custom object array type with card-based rendering:
1325
+
1326
+ ```vue
1327
+ <script setup lang="ts">
1328
+ import type { YamlFieldType } from '@type32/yaml-editor-form'
1329
+
1330
+ const customTypes: YamlFieldType[] = [
1331
+ {
1332
+ type: 'contacts',
1333
+ label: 'Contacts',
1334
+ icon: 'i-lucide-users',
1335
+ defaultValue: [],
1336
+ baseType: 'array', // Inherits array conversions
1337
+ component: 'contacts',
1338
+ detect: (value) => {
1339
+ // Auto-detect arrays of contact-like objects
1340
+ return Array.isArray(value) &&
1341
+ value.length > 0 &&
1342
+ value.every(v =>
1343
+ v && typeof v === 'object' &&
1344
+ ('name' in v || 'email' in v)
1345
+ )
1346
+ }
1347
+ }
1348
+ ]
1349
+
1350
+ const data = ref({
1351
+ projectName: 'My Project',
1352
+ contacts: [
1353
+ { name: 'Alice Johnson', email: 'alice@example.com', role: 'Designer' },
1354
+ { name: 'Bob Smith', email: 'bob@example.com', role: 'Developer' }
1355
+ ]
1356
+ })
1357
+ </script>
1358
+
1359
+ <template>
1360
+ <YamlFormEditor v-model="data" :field-types="customTypes">
1361
+ <!-- Custom contacts list with card UI -->
1362
+ <template #field-contacts="{ modelValue, readonly, updateModelValue }">
1363
+ <div class="space-y-3">
1364
+ <!-- Contact cards -->
1365
+ <UCard
1366
+ v-for="(contact, index) in (modelValue as any[])"
1367
+ :key="index"
1368
+ :ui="{ body: { padding: 'p-4' } }"
1369
+ >
1370
+ <div class="flex items-start justify-between gap-3">
1371
+ <div class="flex-1 space-y-2">
1372
+ <!-- Name -->
1373
+ <UInput
1374
+ :model-value="contact.name"
1375
+ placeholder="Name"
1376
+ :disabled="readonly"
1377
+ @update:model-value="(val: string) => {
1378
+ const updated = [...(modelValue as any[])]
1379
+ updated[index] = { ...contact, name: val }
1380
+ updateModelValue(updated)
1381
+ }"
1382
+ />
1383
+
1384
+ <!-- Email -->
1385
+ <UInput
1386
+ :model-value="contact.email"
1387
+ type="email"
1388
+ placeholder="Email"
1389
+ icon="i-lucide-mail"
1390
+ :disabled="readonly"
1391
+ @update:model-value="(val: string) => {
1392
+ const updated = [...(modelValue as any[])]
1393
+ updated[index] = { ...contact, email: val }
1394
+ updateModelValue(updated)
1395
+ }"
1396
+ />
1397
+
1398
+ <!-- Role -->
1399
+ <UInput
1400
+ :model-value="contact.role"
1401
+ placeholder="Role"
1402
+ icon="i-lucide-briefcase"
1403
+ :disabled="readonly"
1404
+ @update:model-value="(val: string) => {
1405
+ const updated = [...(modelValue as any[])]
1406
+ updated[index] = { ...contact, role: val }
1407
+ updateModelValue(updated)
1408
+ }"
1409
+ />
1410
+ </div>
1411
+
1412
+ <!-- Remove button -->
1413
+ <UButton
1414
+ v-if="!readonly"
1415
+ icon="i-lucide-trash-2"
1416
+ color="red"
1417
+ variant="ghost"
1418
+ size="sm"
1419
+ @click="updateModelValue((modelValue as any[]).filter((_, i) => i !== index))"
1420
+ />
1421
+ </div>
1422
+ </UCard>
1423
+
1424
+ <!-- Add contact button -->
1425
+ <UButton
1426
+ v-if="!readonly"
1427
+ icon="i-lucide-plus"
1428
+ label="Add Contact"
1429
+ variant="outline"
1430
+ block
1431
+ @click="updateModelValue([
1432
+ ...(modelValue as any[]),
1433
+ { name: '', email: '', role: '' }
1434
+ ])"
1435
+ />
1436
+ </div>
1437
+ </template>
1438
+ </YamlFormEditor>
1439
+ </template>
1440
+ ```
1441
+
1442
+ ### Combined Example
1443
+
1444
+ You can use both custom array types together:
1445
+
1446
+ ```vue
1447
+ <script setup lang="ts">
1448
+ import type { YamlFieldType } from '@type32/yaml-editor-form'
1449
+
1450
+ const customTypes: YamlFieldType[] = [
1451
+ // Custom string array
1452
+ {
1453
+ type: 'tags',
1454
+ label: 'Tags',
1455
+ icon: 'i-lucide-tag',
1456
+ defaultValue: [],
1457
+ baseType: 'string-array',
1458
+ component: 'tags',
1459
+ detect: (v) => Array.isArray(v) && v.every(i => typeof i === 'string')
1460
+ },
1461
+ // Custom object array
1462
+ {
1463
+ type: 'team',
1464
+ label: 'Team Members',
1465
+ icon: 'i-lucide-users',
1466
+ defaultValue: [],
1467
+ baseType: 'array',
1468
+ component: 'team',
1469
+ detect: (v) => Array.isArray(v) && v.every(i => i?.name || i?.email)
1470
+ }
1471
+ ]
1472
+
1473
+ const project = ref({
1474
+ name: 'Website Redesign',
1475
+ tags: ['frontend', 'design', 'urgent'],
1476
+ team: [
1477
+ { name: 'Alice', email: 'alice@example.com' },
1478
+ { name: 'Bob', email: 'bob@example.com' }
1479
+ ]
1480
+ })
1481
+ </script>
1482
+
1483
+ <template>
1484
+ <YamlFormEditor v-model="project" :field-types="customTypes">
1485
+ <!-- String array implementation -->
1486
+ <template #field-tags="{ modelValue, readonly, updateModelValue }">
1487
+ <UInputTags
1488
+ :model-value="modelValue as string[]"
1489
+ :disabled="readonly"
1490
+ placeholder="Add tags..."
1491
+ @update:model-value="updateModelValue"
1492
+ />
1493
+ </template>
1494
+
1495
+ <!-- Object array implementation -->
1496
+ <template #field-team="{ modelValue, readonly, updateModelValue }">
1497
+ <!-- Your custom team member UI here -->
1498
+ <div class="space-y-2">
1499
+ <div
1500
+ v-for="(member, idx) in (modelValue as any[])"
1501
+ :key="idx"
1502
+ class="flex gap-2"
1503
+ >
1504
+ <UInput
1505
+ :model-value="member.name"
1506
+ placeholder="Name"
1507
+ :disabled="readonly"
1508
+ @update:model-value="(val: string) => {
1509
+ const updated = [...(modelValue as any[])]
1510
+ updated[idx] = { ...member, name: val }
1511
+ updateModelValue(updated)
1512
+ }"
1513
+ />
1514
+ <UButton
1515
+ v-if="!readonly"
1516
+ icon="i-lucide-x"
1517
+ color="red"
1518
+ variant="ghost"
1519
+ @click="updateModelValue((modelValue as any[]).filter((_, i) => i !== idx))"
1520
+ />
1521
+ </div>
1522
+ <UButton
1523
+ v-if="!readonly"
1524
+ icon="i-lucide-plus"
1525
+ label="Add Member"
1526
+ size="sm"
1527
+ @click="updateModelValue([...(modelValue as any[]), { name: '', email: '' }])"
1528
+ />
1529
+ </div>
1530
+ </template>
1531
+ </YamlFormEditor>
1532
+ </template>
1533
+ ```
1534
+
1535
+ ### Key Patterns for Array Types
1536
+
1537
+ **String Arrays (`baseType: 'string-array'`):**
1538
+ - Use for specialized tag inputs, category lists, etc.
1539
+ - Can convert to/from regular arrays and strings
1540
+ - Good for: skills, tags, categories, keywords
1541
+
1542
+ **Object Arrays (`baseType: 'array'`):**
1543
+ - Use for collections with structured data
1544
+ - Provide custom UI for adding/editing/removing items
1545
+ - Good for: contacts, team members, products, events
1546
+
1547
+ **Important Notes:**
1548
+ 1. **Type Assertions**: Use `(modelValue as string[])` or `(modelValue as any[])` for type safety
1549
+ 2. **Immutability**: Always create new arrays when updating (spread operator `[...]`)
1550
+ 3. **Index Management**: Track items by index for updates/deletions
1551
+ 4. **Add Operations**: Spread existing array and add new items
1552
+ 5. **Remove Operations**: Use `filter()` to remove by index
1553
+ 6. **Update Operations**: Clone array, modify specific index, update entire array
1554
+
1172
1555
  ## License
1173
1556
 
1174
1557
  This component is part of the Vertex project.
package/dist/module.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@type32/yaml-editor-form",
3
3
  "configKey": "yamlEditorForm",
4
- "version": "0.1.4",
4
+ "version": "0.1.6",
5
5
  "builder": {
6
6
  "@nuxt/module-builder": "1.0.2",
7
7
  "unbuild": "3.6.1"
@@ -16,11 +16,15 @@ watch(data, () => {
16
16
  function addField(fieldType = "string") {
17
17
  if (!data.value) data.value = {};
18
18
  const newKey = `field_${Object.keys(data.value).length + 1}`;
19
- data.value[newKey] = getDefaultValue(fieldType);
19
+ data.value = {
20
+ ...data.value,
21
+ [newKey]: getDefaultValue(fieldType)
22
+ };
20
23
  }
21
24
  function removeField(key) {
22
25
  if (data.value) {
23
- delete data.value[key];
26
+ const { [key]: removed, ...rest } = data.value;
27
+ data.value = rest;
24
28
  }
25
29
  }
26
30
  const addFieldOptions = computed(() => {
@@ -42,9 +46,12 @@ const addFieldOptions = computed(() => {
42
46
  :size="size"
43
47
  @remove="removeField(String(key))"
44
48
  @update:field-key="(newKey) => {
45
- if (newKey !== key) {
46
- data[newKey] = data[key];
47
- delete data[key];
49
+ if (newKey !== key && data) {
50
+ const { [key]: value, ...rest } = data;
51
+ data = {
52
+ ...rest,
53
+ [newKey]: value
54
+ };
48
55
  }
49
56
  }"
50
57
  >
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@type32/yaml-editor-form",
3
- "version": "0.1.4",
3
+ "version": "0.1.6",
4
4
  "description": "YAML Editor Form Component for Nuxt.",
5
5
  "repository": "https://github.com/CTRL-Neo-Studios/yaml-editor-form",
6
6
  "license": "MIT",