@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 +157 -6
- package/dist/module.json +1 -1
- package/dist/runtime/components/YamlFormEditor.d.vue.ts +2 -2
- package/dist/runtime/components/YamlFormEditor.vue +16 -6
- package/dist/runtime/components/YamlFormEditor.vue.d.ts +2 -2
- package/dist/runtime/components/YamlFormField.vue +26 -13
- package/package.json +1 -1
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
|
|
1077
|
+
**Detection Order:**
|
|
1049
1078
|
|
|
1050
|
-
Custom types with `detect` functions are
|
|
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
|
|
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
|
-
- ✅
|
|
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
|
@@ -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
|
|
29
|
+
declare var __VLS_20: string, __VLS_21: any;
|
|
30
30
|
type __VLS_Slots = {} & {
|
|
31
|
-
[K in NonNullable<typeof
|
|
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
|
|
19
|
+
data.value = {
|
|
20
|
+
...data.value,
|
|
21
|
+
[newKey]: getDefaultValue(fieldType)
|
|
22
|
+
};
|
|
20
23
|
}
|
|
21
24
|
function removeField(key) {
|
|
22
25
|
if (data.value) {
|
|
23
|
-
|
|
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
|
-
|
|
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
|
-
|
|
47
|
-
|
|
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
|
|
29
|
+
declare var __VLS_20: string, __VLS_21: any;
|
|
30
30
|
type __VLS_Slots = {} & {
|
|
31
|
-
[K in NonNullable<typeof
|
|
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.
|
|
167
|
+
modelValue.value = [...modelValue.value, getDefaultValue(itemType)];
|
|
168
168
|
return;
|
|
169
169
|
}
|
|
170
170
|
if (modelValue.value.length === 0) {
|
|
171
|
-
modelValue.value.
|
|
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.
|
|
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.
|
|
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.
|
|
202
|
+
modelValue.value = [...modelValue.value, newObject];
|
|
203
203
|
} else {
|
|
204
|
-
modelValue.value.
|
|
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
|
-
|
|
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
|
-
|
|
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))
|
|
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
|
|
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
|
|
414
|
-
|
|
415
|
-
|
|
424
|
+
const { [key]: oldValue, ...rest } = modelValue;
|
|
425
|
+
modelValue = {
|
|
426
|
+
...rest,
|
|
427
|
+
[newKey]: oldValue
|
|
428
|
+
};
|
|
416
429
|
}
|
|
417
430
|
}"
|
|
418
431
|
/>
|