@type32/yaml-editor-form 0.1.5 → 0.2.1

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
@@ -1043,20 +1072,20 @@ This function captures the parent's `modelValue` ref and updates it directly, ma
1043
1072
 
1044
1073
  Even if your color field is deeply nested (`data.theme.colors.primary`), the slot works identically because slots are forwarded at every level.
1045
1074
 
1046
- ### Type Priority
1075
+ ### Type Priority & Detection Order
1047
1076
 
1048
- **Detection Order (NEW in v0.2.0):**
1077
+ **Detection Order:**
1049
1078
 
1050
- Custom types with `detect` functions are now checked **before** default types:
1079
+ Custom types with `detect` functions are **always** checked **before** default types:
1051
1080
 
1052
1081
  1. **Custom types** (checked first) - Your custom types take priority
1053
1082
  2. **Default types** (checked second) - Built-in types as fallback
1054
- 3. First matching type wins
1083
+ 3. **First matching type wins** - Detection stops at first match
1055
1084
 
1056
- This means:
1085
+ This ensures:
1057
1086
  - ✅ Your `color` type will be detected before the default `string` type
1058
1087
  - ✅ Custom types override default detection behavior
1059
- - ✅ More specific types should still have more specific detect functions
1088
+ - ✅ Custom types appear first in "Add Field" dropdowns
1060
1089
 
1061
1090
  **Example:**
1062
1091
  ```typescript
@@ -1070,6 +1099,48 @@ const customTypes = [{
1070
1099
  // Value '#FF0000' will match 'color' before 'string'
1071
1100
  ```
1072
1101
 
1102
+ **Important: Make Your Detect Functions Specific!**
1103
+
1104
+ Since detection stops at the first match, make sure your `detect` functions are specific enough:
1105
+
1106
+ ```typescript
1107
+ // ❌ TOO BROAD - will match ALL strings
1108
+ {
1109
+ type: 'email',
1110
+ baseType: 'string',
1111
+ detect: (v) => typeof v === 'string' // Too general!
1112
+ }
1113
+
1114
+ // ✅ SPECIFIC - only matches email-like strings
1115
+ {
1116
+ type: 'email',
1117
+ baseType: 'string',
1118
+ detect: (v) => typeof v === 'string' && /^[^@]+@[^@]+\.[^@]+$/.test(v)
1119
+ }
1120
+
1121
+ // ✅ SPECIFIC - only matches hex colors
1122
+ {
1123
+ type: 'color',
1124
+ baseType: 'string',
1125
+ detect: (v) => typeof v === 'string' && /^#[0-9A-Fa-f]{6}$/.test(v)
1126
+ }
1127
+
1128
+ // ✅ SPECIFIC - only matches URLs
1129
+ {
1130
+ type: 'url',
1131
+ baseType: 'string',
1132
+ detect: (v) => typeof v === 'string' && /^https?:\/\//.test(v)
1133
+ }
1134
+ ```
1135
+
1136
+ **Troubleshooting Detection Issues:**
1137
+
1138
+ If your custom type isn't being detected:
1139
+ 1. Check that `detect` function is specific enough
1140
+ 2. Ensure `detect` returns `true` for your value
1141
+ 3. Remember: first matching type wins (order matters!)
1142
+ 4. Test your detect function in isolation
1143
+
1073
1144
  **Base Type Conversions:**
1074
1145
 
1075
1146
  When using `baseType`, conversion rules follow this logic:
@@ -1087,12 +1158,91 @@ When using `baseType`, conversion rules follow this logic:
1087
1158
  ]
1088
1159
  ```
1089
1160
 
1161
+ ### Writable Computed Support
1162
+
1163
+ **✅ Fully Compatible** - The editor now fully supports writable computed refs through `defineModel` and nested components.
1164
+
1165
+ All internal operations use **immutable updates** to ensure computed setters are properly triggered:
1166
+
1167
+ ```typescript
1168
+ // ✅ All operations create new references (triggers computed setter)
1169
+ data.value = { ...data.value, newField: 'value' } // Add field
1170
+ data.value = { ...data.value, [key]: newValue } // Update field
1171
+ const { [key]: removed, ...rest } = data.value // Remove field
1172
+ data.value = rest
1173
+
1174
+ // ✅ Array operations also use immutable patterns
1175
+ array.value = [...array.value, newItem] // Add item
1176
+ array.value = array.value.filter((_, i) => i !== index) // Remove item
1177
+ const newArr = [...array.value]; newArr[i] = val // Update item
1178
+ array.value = newArr
1179
+ ```
1180
+
1181
+ **✅ Works Through Multiple Component Layers:**
1182
+
1183
+ The editor correctly handles writable computed passed through `defineModel` in nested components:
1184
+
1185
+ ```vue
1186
+ <!-- Parent Component -->
1187
+ <script setup lang="ts">
1188
+ const rawData = ref({ title: 'Article' })
1189
+
1190
+ // Writable computed with validation
1191
+ const data = computed({
1192
+ get: () => rawData.value,
1193
+ set: (value) => {
1194
+ console.log('Data updated:', value)
1195
+ // Add validation, transformations, etc.
1196
+ rawData.value = value
1197
+ }
1198
+ })
1199
+ </script>
1200
+
1201
+ <template>
1202
+ <!-- Works! Passes through DataEditorForm → YamlFormEditor -->
1203
+ <DataEditorForm v-model="data" />
1204
+ </template>
1205
+ ```
1206
+
1207
+ **Use Cases for Writable Computed:**
1208
+ - ✅ Validation before saving
1209
+ - ✅ Transform data on save (e.g., serialize dates)
1210
+ - ✅ Sync with external state management (Pinia, Vuex)
1211
+ - ✅ Trigger side effects on changes (API calls, logging)
1212
+ - ✅ Implement undo/redo functionality
1213
+ - ✅ Complex editor integrations (TipTap, Monaco, etc.)
1214
+
1215
+ **Example with Pinia:**
1216
+ ```typescript
1217
+ const store = useMyStore()
1218
+
1219
+ const data = computed({
1220
+ get: () => store.formData,
1221
+ set: (value) => store.updateFormData(value)
1222
+ })
1223
+ ```
1224
+
1225
+ **Example with TipTap Editor Integration:**
1226
+ ```typescript
1227
+ const editorInstance = defineModel('editorInstance', { required: true })
1228
+ const $ef = useEditorFrontmatter(editorInstance)
1229
+
1230
+ const data = computed({
1231
+ get: () => $ef.getFrontmatter().data || {},
1232
+ set: (newValue) => {
1233
+ // Updates editor content directly
1234
+ $ef.setFrontmatterProperties({ ...newValue })
1235
+ }
1236
+ })
1237
+ ```
1238
+
1090
1239
  ### Performance Considerations
1091
1240
 
1092
1241
  **Reactivity:**
1093
1242
  - Uses Vue 3 `ref` and `computed` for optimal reactivity
1094
1243
  - Deep watching is used only where necessary
1095
1244
  - Recursive rendering is optimized with `v-if` conditionals
1245
+ - **Immutable updates** ensure computed setters are triggered properly
1096
1246
 
1097
1247
  **Large Arrays:**
1098
1248
  - Each array item is independently reactive
@@ -1103,6 +1253,7 @@ When using `baseType`, conversion rules follow this logic:
1103
1253
  - Date helper functions are minimal
1104
1254
  - No global state except type registry
1105
1255
  - Components clean up properly on unmount
1256
+ - Immutable updates create minimal object copies (spread operator is fast)
1106
1257
 
1107
1258
  ### Validation (Future)
1108
1259
 
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.5",
4
+ "version": "0.2.1",
5
5
  "builder": {
6
6
  "@nuxt/module-builder": "1.0.2",
7
7
  "unbuild": "3.6.1"
@@ -26,9 +26,9 @@ type __VLS_ModelProps = {
26
26
  modelValue: YamlFormData;
27
27
  };
28
28
  type __VLS_PublicProps = __VLS_Props & __VLS_ModelProps;
29
- declare var __VLS_19: string, __VLS_20: any;
29
+ declare var __VLS_20: string, __VLS_21: any;
30
30
  type __VLS_Slots = {} & {
31
- [K in NonNullable<typeof __VLS_19>]?: (props: typeof __VLS_20) => any;
31
+ [K in NonNullable<typeof __VLS_20>]?: (props: typeof __VLS_21) => any;
32
32
  };
33
33
  declare const __VLS_base: import("vue").DefineComponent<__VLS_PublicProps, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
34
34
  "update:modelValue": (value: {
@@ -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(() => {
@@ -35,16 +39,22 @@ const addFieldOptions = computed(() => {
35
39
  <YamlFormField
36
40
  v-for="(value, key) in data"
37
41
  :key="String(key)"
38
- v-model="data[key]"
42
+ :model-value="data[key]"
39
43
  :field-key="String(key)"
40
44
  :readonly="readonly"
41
45
  :field-types="fieldTypes"
42
46
  :size="size"
47
+ @update:model-value="(newValue) => {
48
+ data = { ...data, [key]: newValue };
49
+ }"
43
50
  @remove="removeField(String(key))"
44
51
  @update:field-key="(newKey) => {
45
- if (newKey !== key) {
46
- data[newKey] = data[key];
47
- delete data[key];
52
+ if (newKey !== key && data) {
53
+ const { [key]: value, ...rest } = data;
54
+ data = {
55
+ ...rest,
56
+ [newKey]: value
57
+ };
48
58
  }
49
59
  }"
50
60
  >
@@ -26,9 +26,9 @@ type __VLS_ModelProps = {
26
26
  modelValue: YamlFormData;
27
27
  };
28
28
  type __VLS_PublicProps = __VLS_Props & __VLS_ModelProps;
29
- declare var __VLS_19: string, __VLS_20: any;
29
+ declare var __VLS_20: string, __VLS_21: any;
30
30
  type __VLS_Slots = {} & {
31
- [K in NonNullable<typeof __VLS_19>]?: (props: typeof __VLS_20) => any;
31
+ [K in NonNullable<typeof __VLS_20>]?: (props: typeof __VLS_21) => any;
32
32
  };
33
33
  declare const __VLS_base: import("vue").DefineComponent<__VLS_PublicProps, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
34
34
  "update:modelValue": (value: {
@@ -164,20 +164,20 @@ function convertType(newType) {
164
164
  function addArrayItem(itemType) {
165
165
  if (!Array.isArray(modelValue.value)) return;
166
166
  if (itemType) {
167
- modelValue.value.push(getDefaultValue(itemType));
167
+ modelValue.value = [...modelValue.value, getDefaultValue(itemType)];
168
168
  return;
169
169
  }
170
170
  if (modelValue.value.length === 0) {
171
- modelValue.value.push({});
171
+ modelValue.value = [...modelValue.value, {}];
172
172
  return;
173
173
  }
174
174
  const firstItem = modelValue.value[0];
175
175
  const detectedType = detectFieldType(firstItem);
176
- modelValue.value.push(getDefaultValue(detectedType.type));
176
+ modelValue.value = [...modelValue.value, getDefaultValue(detectedType.type)];
177
177
  }
178
178
  function removeArrayItem(index) {
179
179
  if (!Array.isArray(modelValue.value)) return;
180
- modelValue.value.splice(index, 1);
180
+ modelValue.value = modelValue.value.filter((_, i) => i !== index);
181
181
  }
182
182
  function addArrayItemFromTemplate() {
183
183
  if (!Array.isArray(modelValue.value)) return;
@@ -199,9 +199,9 @@ function addArrayItemFromTemplate() {
199
199
  const detectedType = detectFieldType(value);
200
200
  newObject[key] = getDefaultValue(detectedType.type);
201
201
  }
202
- modelValue.value.push(newObject);
202
+ modelValue.value = [...modelValue.value, newObject];
203
203
  } else {
204
- modelValue.value.push({});
204
+ modelValue.value = [...modelValue.value, {}];
205
205
  }
206
206
  }
207
207
  const hasObjectTemplate = computed(() => {
@@ -214,12 +214,16 @@ function addObjectField(fieldType = "string") {
214
214
  if (typeof modelValue.value !== "object" || Array.isArray(modelValue.value) || !modelValue.value || isDateObject(modelValue.value)) return;
215
215
  const obj = modelValue.value;
216
216
  const newKey = `field_${Object.keys(obj).length + 1}`;
217
- obj[newKey] = getDefaultValue(fieldType);
217
+ modelValue.value = {
218
+ ...obj,
219
+ [newKey]: getDefaultValue(fieldType)
220
+ };
218
221
  }
219
222
  function removeObjectField(key) {
220
223
  if (typeof modelValue.value !== "object" || Array.isArray(modelValue.value) || !modelValue.value || isDateObject(modelValue.value)) return;
221
224
  const obj = modelValue.value;
222
- delete obj[key];
225
+ const { [key]: removed, ...rest } = obj;
226
+ modelValue.value = rest;
223
227
  }
224
228
  const isOpen = ref(true);
225
229
  const isArrayItem = computed(() => {
@@ -344,7 +348,11 @@ const addArrayItemOptions = computed(() => {
344
348
  :depth="depth + 1"
345
349
  :field-types="fieldTypes"
346
350
  @update:model-value="(val) => {
347
- if (Array.isArray(modelValue)) modelValue[index] = val;
351
+ if (Array.isArray(modelValue)) {
352
+ const newArray = [...modelValue];
353
+ newArray[index] = val;
354
+ modelValue = newArray;
355
+ }
348
356
  }"
349
357
  @remove="removeArrayItem(index)"
350
358
  :size="size"
@@ -404,15 +412,20 @@ const addArrayItemOptions = computed(() => {
404
412
  :size="size"
405
413
  @update:model-value="(val) => {
406
414
  if (typeof modelValue === 'object' && !Array.isArray(modelValue) && modelValue !== null && !isDateObject(modelValue)) {
407
- modelValue[key] = val;
415
+ modelValue = {
416
+ ...modelValue,
417
+ [key]: val
418
+ };
408
419
  }
409
420
  }"
410
421
  @remove="removeObjectField(String(key))"
411
422
  @update:field-key="(newKey) => {
412
423
  if (newKey !== key && typeof modelValue === 'object' && !Array.isArray(modelValue) && modelValue !== null && !isDateObject(modelValue) && value !== void 0) {
413
- const obj = modelValue;
414
- obj[newKey] = value;
415
- delete obj[key];
424
+ const { [key]: oldValue, ...rest } = modelValue;
425
+ modelValue = {
426
+ ...rest,
427
+ [newKey]: oldValue
428
+ };
416
429
  }
417
430
  }"
418
431
  />
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@type32/yaml-editor-form",
3
- "version": "0.1.5",
3
+ "version": "0.2.1",
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",