Skip to content

Commit f2dc1a5

Browse files
committed
sync form and inputs
1 parent ac20f53 commit f2dc1a5

File tree

8 files changed

+333
-62
lines changed

8 files changed

+333
-62
lines changed

src/app/src/components/content/ContentEditorForm.vue

Lines changed: 6 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ watch(() => props.draftItem.status, () => {
4343
})
4444
4545
// Trigger on form changes
46-
watch (contentJSON, (json) => {
46+
watch(contentJSON, (newJSON, oldJSON) => {
4747
if (skipFirstUpdate.value) {
4848
skipFirstUpdate.value = false
4949
return
@@ -53,25 +53,18 @@ watch (contentJSON, (json) => {
5353
return
5454
}
5555
56-
// Do not trigger model updates if the document id has changed
57-
// if (currentDocumentId.value !== document.value?.id) {
58-
// return
59-
// }
60-
61-
// if (content.value === newContent) {
62-
// return
63-
// }
64-
65-
// content.value = newContent
56+
if (JSON.stringify(newJSON) === JSON.stringify(oldJSON)) {
57+
return
58+
}
6659
6760
let content = ''
6861
switch (document.value?.extension) {
6962
case ContentFileExtension.JSON:
70-
content = JSON.stringify(json)
63+
content = JSON.stringify(newJSON)
7164
break
7265
case ContentFileExtension.YAML:
7366
case ContentFileExtension.YML:
74-
content = jsonToYaml(json)
67+
content = jsonToYaml(newJSON)
7568
break
7669
}
7770

src/app/src/components/shared/form/FormPanelInput.vue

Lines changed: 5 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { titleCase } from 'scule'
33
import type { FormItem, FormTree } from '../../../types'
44
import type { PropType } from 'vue'
55
import { computed, ref, watch } from 'vue'
6+
import { applyValueById } from '../../../utils/form'
67
78
const props = defineProps({
89
formItem: {
@@ -40,6 +41,10 @@ const model = ref(computeValue(props.formItem))
4041
4142
// Sync changes back to parent form
4243
watch(model, (newValue) => {
44+
if (newValue === props.formItem.value) {
45+
return
46+
}
47+
4348
form.value = applyValueById(form.value, props.formItem.id, newValue)
4449
})
4550
@@ -71,25 +76,6 @@ function computeValue(formItem: FormItem): unknown {
7176
return null
7277
}
7378
}
74-
75-
function applyValueById(tree: FormTree, id: string, value: unknown): FormTree {
76-
const result = { ...tree }
77-
const paths = id.split('/').filter(Boolean)
78-
79-
let current: Record<string, unknown> = result
80-
for (let i = 0; i < paths.length - 1; i++) {
81-
const key = paths[i]
82-
if (!current[key] || typeof current[key] !== 'object') {
83-
current[key] = {}
84-
}
85-
current = current[key] as Record<string, unknown>
86-
}
87-
88-
const lastKey = paths[paths.length - 1]
89-
current[lastKey] = value
90-
91-
return result
92-
}
9379
</script>
9480

9581
<template>

src/app/src/components/shared/form/FormSchemaBased.vue

Lines changed: 23 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,8 @@ import { computed } from 'vue'
33
import type { FormTree } from '../../../types'
44
import type { PropType } from 'vue'
55
import type { Draft07 } from '@nuxt/content'
6-
import { buildFormTreeFromSchema, applyValuesToFormTree } from '../../../utils/form'
6+
import { buildFormTreeFromSchema, applyValuesToFormTree, getUpdatedTreeItem } from '../../../utils/form'
7+
import { applyValueByPath } from '../../../utils/object'
78
89
const props = defineProps({
910
collectionName: {
@@ -16,45 +17,42 @@ const props = defineProps({
1617
},
1718
})
1819
19-
const data = defineModel<Record<string, unknown>>({ required: true })
20-
21-
console.log('data', data.value)
20+
const model = defineModel<Record<string, unknown>>({ required: true })
2221
2322
const formTree = computed<FormTree>(() => {
2423
return buildFormTreeFromSchema(props.collectionName, props.schema)
2524
})
2625
2726
const formTreeWithValues = computed({
2827
get: () => {
29-
if (!data.value || !formTree.value) {
28+
if (!model.value || !formTree.value) {
3029
return null
3130
}
3231
33-
return applyValuesToFormTree(formTree.value, { [props.collectionName]: data.value })
32+
return applyValuesToFormTree(formTree.value, { [props.collectionName]: model.value })
3433
},
3534
set: (newFormTree) => {
36-
console.log('newFormTree', newFormTree)
35+
const updatedItem = getUpdatedTreeItem(formTreeWithValues.value!, newFormTree!)
36+
if (!updatedItem) {
37+
return
38+
}
39+
40+
// Strip the collection name from the id ("#authors/title" → "title"
41+
const pathSegments = updatedItem.id.split('/')
42+
pathSegments.shift()
43+
const jsonContentCopy = JSON.parse(JSON.stringify(model.value))
44+
model.value = applyValueByPath(jsonContentCopy, pathSegments.join('/'), updatedItem.value)
3745
},
3846
})
39-
40-
// const jsonString = computed({
41-
// get: () => JSON.stringify(model.value, null, 2),
42-
// set: (value: string) => {
43-
// try {
44-
// model.value = JSON.parse(value)
45-
// }
46-
// catch {
47-
// // Invalid JSON, don't update
48-
// }
49-
// },
50-
// })
5147
</script>
5248

5349
<template>
54-
<FormPanelSection
55-
v-for="formItem in formTree[collectionName].children"
56-
:key="formItem.id"
57-
v-model="formTreeWithValues"
58-
:form-item="formItem"
59-
/>
50+
<template v-if="formTreeWithValues">
51+
<FormPanelSection
52+
v-for="formItem in formTreeWithValues[collectionName].children"
53+
:key="formItem.id"
54+
v-model="formTreeWithValues"
55+
:form-item="formItem"
56+
/>
57+
</template>
6058
</template>

src/app/src/utils/form.ts

Lines changed: 29 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -145,8 +145,6 @@ export const buildFormTreeFromSchema = (treeKey: string, schema: Draft07): FormT
145145
// Apply json to form tree values
146146
// Only override properties that are present in the tree
147147
export const applyValuesToFormTree = (tree: FormTree, override: Record<string, unknown>): FormTree => {
148-
console.log('tree', tree)
149-
console.log('override', override)
150148
return Object.keys(tree).reduce((acc, key) => {
151149
// Recursively override if found
152150
if (override[key]) {
@@ -225,3 +223,32 @@ export const applyValueById = (form: FormTree, id: string, value: unknown): Form
225223
}
226224
}, {})
227225
}
226+
227+
// Recursively compare form trees to find the updated item and return it
228+
// Updated item must a be leaf (input) of the form
229+
export const getUpdatedTreeItem = (original: FormTree, updated: FormTree): FormItem | null => {
230+
for (const key of Object.keys(updated)) {
231+
const originalItem = original[key]
232+
const updatedItem = updated[key]
233+
234+
if (!originalItem) {
235+
continue
236+
}
237+
238+
// If both have children, recurse into them
239+
if (originalItem.children && updatedItem.children) {
240+
const result = getUpdatedTreeItem(originalItem.children, updatedItem.children)
241+
if (result) {
242+
return result
243+
}
244+
}
245+
// If it's a leaf node, compare values
246+
else if (!updatedItem.children) {
247+
if (originalItem.value !== updatedItem.value) {
248+
return updatedItem
249+
}
250+
}
251+
}
252+
253+
return null
254+
}

src/app/src/utils/object.ts

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,3 +47,45 @@ export function replaceNullWithEmptyString(obj: Record<string, unknown>): Record
4747
}
4848
return obj
4949
}
50+
51+
// Browse object until key is found based on path, then override value for this key
52+
export const applyValueByPath = (obj: Record<string, unknown>, path: string, value: unknown): Record<string, unknown> => {
53+
const keys = path.split('/')
54+
55+
let current = obj
56+
keys.forEach((key, index) => {
57+
const isLeaf = index === keys.length - 1
58+
if (isLeaf) {
59+
// Array
60+
if (Array.isArray(value) && Array.isArray(current[key])) {
61+
current[key] = value
62+
}
63+
// Object
64+
else if (typeof value === 'object' && typeof current[key] === 'object') {
65+
// Merge objects
66+
Object.assign(current[key] as Record<string, unknown>, value as Record<string, unknown>)
67+
// Remove undefined or null keys
68+
Object.keys(current[key] as Record<string, unknown>).forEach((k) => {
69+
if (
70+
(current[key] as Record<string, unknown>)[k] === undefined
71+
|| (current[key] as Record<string, unknown>)[k] === null
72+
) {
73+
Reflect.deleteProperty(current[key] as Record<string, unknown>, k)
74+
}
75+
})
76+
}
77+
else {
78+
// Set value directly
79+
current[key] = value
80+
}
81+
}
82+
else {
83+
if (!current[key] || typeof current[key] !== 'object') {
84+
current[key] = {}
85+
}
86+
current = current[key] as Record<string, unknown>
87+
}
88+
})
89+
90+
return obj
91+
}

src/app/test/mocks/form.ts

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
import type { FormTree } from '../../src/types'
2+
3+
export const farnabazFormTree: FormTree = {
4+
authors: {
5+
id: '#authors',
6+
title: 'Authors',
7+
type: 'object',
8+
children: {
9+
name: {
10+
id: '#authors/name',
11+
title: 'Name',
12+
type: 'string',
13+
value: 'Ahad Birang',
14+
},
15+
avatar: {
16+
id: '#authors/avatar',
17+
title: 'Avatar',
18+
type: 'object',
19+
children: {
20+
src: {
21+
id: '#authors/avatar/src',
22+
title: 'Src',
23+
type: 'string',
24+
value: 'https://avatars.githubusercontent.com/farnabaz',
25+
},
26+
alt: {
27+
id: '#authors/avatar/alt',
28+
title: 'Alt',
29+
type: 'string',
30+
value: '',
31+
},
32+
},
33+
},
34+
to: {
35+
id: '#authors/to',
36+
title: 'To',
37+
type: 'string',
38+
value: 'https://x.com/farnabaz',
39+
},
40+
username: {
41+
id: '#authors/username',
42+
title: 'Username',
43+
type: 'string',
44+
value: 'farnabaz',
45+
},
46+
},
47+
},
48+
}
49+
50+
export const larbishFormTree: FormTree = {
51+
authors: {
52+
id: '#authors',
53+
title: 'Authors',
54+
type: 'object',
55+
children: {
56+
name: {
57+
id: '#authors/name',
58+
title: 'Name',
59+
type: 'string',
60+
value: 'Baptiste Leproux',
61+
},
62+
avatar: {
63+
id: '#authors/avatar',
64+
title: 'Avatar',
65+
type: 'object',
66+
children: {
67+
src: {
68+
id: '#authors/avatar/src',
69+
title: 'Src',
70+
type: 'string',
71+
value: 'https://avatars.githubusercontent.com/larbish',
72+
},
73+
alt: {
74+
id: '#authors/avatar/alt',
75+
title: 'Alt',
76+
type: 'string',
77+
value: '',
78+
},
79+
},
80+
},
81+
to: {
82+
id: '#authors/to',
83+
title: 'To',
84+
type: 'string',
85+
value: 'https://x.com/_larbish',
86+
},
87+
username: {
88+
id: '#authors/username',
89+
title: 'Username',
90+
type: 'string',
91+
value: 'larbish',
92+
},
93+
},
94+
},
95+
}

0 commit comments

Comments
 (0)