Skip to content

Commit a968135

Browse files
authored
feat: dasc #181 handle arrays, #196 annotate based on schema (#198)
1 parent edf60ab commit a968135

11 files changed

Lines changed: 741 additions & 172 deletions

File tree

nx/blocks/form/data/model.js

Lines changed: 21 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,9 @@ import { daFetch } from 'https://da.live/blocks/shared/utils.js';
33
import HTMLConverter from '../utils/html2json.js';
44
import JSONConverter from '../utils/json2html.js';
55
import { Validator } from '../../../deps/da-form/dist/index.js';
6-
import { annotateProp, setValueByPath } from '../utils/utils.js';
6+
import { annotateFromSchema, pruneRecursive } from '../utils/utils.js';
7+
import { getValueByPointer, setValueByPointer, removeArrayItemByPointer } from '../utils/pointer.js';
8+
import generateEmptyObject from '../utils/generator.js';
79

810
/**
911
* A data model that represents a piece of structured content.
@@ -27,15 +29,14 @@ export default class FormModel {
2729
this._path = path;
2830
this._schemas = schemas;
2931
this._schema = schemas[this._json.metadata.schemaName];
30-
this._annotated = annotateProp('data', this._json.data, this._schema, this._schema);
32+
this._annotated = annotateFromSchema('data', this._schema, this._schema, this._json.data, '', false);
3133
}
3234

3335
clone() {
3436
return new FormModel({
3537
path: this._path,
36-
html: this._html,
37-
json: JSON.parse(JSON.stringify(this._json)), // Deep copy of JSON
38-
schemas: this._schemas, // or clone this too if needed
38+
json: JSON.parse(JSON.stringify(this._json)),
39+
schemas: this._schemas,
3940
});
4041
}
4142

@@ -50,13 +51,24 @@ export default class FormModel {
5051
}
5152

5253
updateHtml() {
53-
const html = JSONConverter(this._json);
54-
this._html = html;
54+
const prunedData = pruneRecursive(this._json.data);
55+
const json = { ...this._json, data: prunedData ?? {} };
56+
this._html = JSONConverter(json);
5557
}
5658

5759
updateProperty({ name, value }) {
58-
setValueByPath(this._json, name, value);
59-
this.updateHtml();
60+
setValueByPointer(this._json, name, value);
61+
}
62+
63+
addArrayItem(pointer, itemsSchema) {
64+
const array = getValueByPointer(this._json, pointer) ?? [];
65+
const newItem = generateEmptyObject(itemsSchema ?? {}, new Set(), this._schema);
66+
array.push(newItem);
67+
setValueByPointer(this._json, pointer, array);
68+
}
69+
70+
removeArrayItem(pointer) {
71+
return removeArrayItemByPointer(this._json, pointer);
6072
}
6173

6274
async saveHtml() {

nx/blocks/form/form.js

Lines changed: 29 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,6 @@ import './views/editor.js';
1414
import './views/sidebar.js';
1515
import './views/preview.js';
1616

17-
import generateEmptyObject from './utils/generator.js';
18-
1917
// External Web Components
2018
await import('../../public/sl/components.js');
2119

@@ -62,7 +60,7 @@ class FormEditor extends LitElement {
6260

6361
const title = this.details.name;
6462

65-
const data = generateEmptyObject(this._schemas[schemaId]);
63+
const data = {};
6664
const metadata = { title, schemaName: schemaId };
6765
const emptyForm = { data, metadata };
6866

@@ -80,6 +78,28 @@ class FormEditor extends LitElement {
8078
await this.formModel.saveHtml();
8179
}
8280

81+
async handleAddItem({ detail }) {
82+
const { pointer, itemsSchema } = detail;
83+
this.formModel.addArrayItem(pointer, itemsSchema);
84+
85+
// Update the view with the new values
86+
this.formModel = this.formModel.clone();
87+
88+
// Persist the data
89+
await this.formModel.saveHtml();
90+
}
91+
92+
async handleRemoveItem({ detail }) {
93+
const { pointer } = detail;
94+
if (!this.formModel.removeArrayItem(pointer)) return;
95+
96+
// Update the view with the new values
97+
this.formModel = this.formModel.clone();
98+
99+
// Persist the data
100+
await this.formModel.saveHtml();
101+
}
102+
83103
renderSchemaSelector() {
84104
return html`
85105
<p class="da-form-title">Please select a schema to get started</p>
@@ -103,7 +123,12 @@ class FormEditor extends LitElement {
103123

104124
return html`
105125
<div class="da-form-editor">
106-
<da-form-editor @update=${this.handleUpdate} .formModel=${this.formModel}></da-form-editor>
126+
<da-form-editor
127+
@update=${this.handleUpdate}
128+
@add-item=${this.handleAddItem}
129+
@remove-item=${this.handleRemoveItem}
130+
.formModel=${this.formModel}
131+
></da-form-editor>
107132
<da-form-preview .formModel=${this.formModel}></da-form-preview>
108133
</div>`;
109134
}

nx/blocks/form/utils/generator.js

Lines changed: 5 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -25,10 +25,8 @@ export default function generateEmptyObject(
2525
return generateEmptyObject(schema.oneOf[0], requiredFields, rootSchema);
2626
}
2727

28-
// If field has enum values, return the first one if it's required
29-
if (schema.enum && schema.enum.length > 0 && requiredFields.size > 0) {
30-
return schema.enum[0];
31-
}
28+
// Use schema default when present, otherwise undefined (no auto-picking enum[0] or 0)
29+
const useDefault = (value) => value ?? undefined;
3230

3331
const { type } = schema;
3432

@@ -50,34 +48,19 @@ export default function generateEmptyObject(
5048
}
5149

5250
case 'array': {
53-
// If array items have enum and array is required, include first item
54-
if (schema.items?.enum && requiredFields.size > 0) {
55-
return [schema.items.enum[0]];
56-
}
57-
// If array items are objects and array is required, generate one empty object
58-
if (requiredFields.size > 0 && schema.items
59-
&& (schema.items.type === 'object' || schema.items.properties)) {
60-
return [generateEmptyObject(schema.items, new Set(), rootSchema)];
61-
}
62-
// If array items have a $ref, resolve it to check if it's an object
63-
if (requiredFields.size > 0 && schema.items?.$ref) {
51+
if (requiredFields.size > 0 && schema.items) {
6452
return [generateEmptyObject(schema.items, new Set(), rootSchema)];
6553
}
6654
return [];
6755
}
6856

6957
case 'string':
70-
return schema.enum && schema.enum.length > 0 ? schema.enum[0] : '';
71-
7258
case 'number':
7359
case 'integer':
74-
return schema.enum && schema.enum.length > 0 ? schema.enum[0] : 0;
60+
return useDefault(schema.default);
7561

7662
case 'boolean':
77-
return schema.enum && schema.enum.length > 0 ? schema.enum[0] : false;
78-
79-
case 'null':
80-
return null;
63+
return schema.default ?? false;
8164

8265
default:
8366
return null;

nx/blocks/form/utils/pointer.js

Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
/**
2+
* RFC 6901 JSON Pointer utilities.
3+
* No external dependencies — manual implementation.
4+
*/
5+
6+
/** Escape a segment for use in a pointer. ~ → ~0, / → ~1 */
7+
function escapeSegment(segment) {
8+
if (typeof segment !== 'string') return String(segment);
9+
return segment.replace(/~/g, '~0').replace(/\//g, '~1');
10+
}
11+
12+
/** Unescape a segment. ~1 → /, ~0 → ~ (order matters) */
13+
function unescapeSegment(segment) {
14+
if (typeof segment !== 'string') return segment;
15+
return segment.replace(/~1/g, '/').replace(/~0/g, '~');
16+
}
17+
18+
/**
19+
* Parse a pointer into segments. /data/items/0 → ['data', 'items', '0']
20+
* @param {string} pointer - RFC 6901 pointer (e.g. "/data/items/0")
21+
* @returns {string[]} Unescaped segments (empty array for root)
22+
*/
23+
function parsePointer(pointer) {
24+
if (!pointer || typeof pointer !== 'string') return [];
25+
const trimmed = pointer.startsWith('/') ? pointer.slice(1) : pointer;
26+
if (!trimmed) return [];
27+
return trimmed.split('/').map(unescapeSegment);
28+
}
29+
30+
/**
31+
* Append a segment to a pointer.
32+
* @param {string} pointer - Base pointer (e.g. "/data")
33+
* @param {string|number} segment - Segment to append (e.g. "items" or 0)
34+
* @returns {string} New pointer (e.g. "/data/items")
35+
*/
36+
export function append(pointer, segment) {
37+
const normalized = pointer === '' || pointer === '/' ? '' : pointer.replace(/\/$/, '');
38+
const escaped = escapeSegment(String(segment));
39+
return normalized ? `${normalized}/${escaped}` : `/${escaped}`;
40+
}
41+
42+
/**
43+
* Get value at pointer in object.
44+
* @param {Object} obj - Root object
45+
* @param {string} pointer - RFC 6901 pointer (e.g. "/data/items/0/name")
46+
* @returns {*} Value at path, or undefined
47+
*/
48+
export function getValueByPointer(obj, pointer) {
49+
const segments = parsePointer(pointer);
50+
let current = obj;
51+
for (const seg of segments) {
52+
if (current == null) return undefined;
53+
current = current[seg];
54+
}
55+
return current;
56+
}
57+
58+
/**
59+
* Set value at pointer. Creates missing intermediate objects/arrays.
60+
* @param {Object} obj - Root object to modify
61+
* @param {string} pointer - RFC 6901 pointer (e.g. "/data/items/0/name")
62+
* @param {*} value - Value to set
63+
*/
64+
export function setValueByPointer(obj, pointer, value) {
65+
const segments = parsePointer(pointer);
66+
if (segments.length === 0) return;
67+
68+
let current = obj;
69+
for (let i = 0; i < segments.length - 1; i += 1) {
70+
const seg = segments[i];
71+
const nextSeg = segments[i + 1];
72+
const isNextArrayIndex = /^\d+$/.test(String(nextSeg));
73+
74+
if (!(seg in current)) {
75+
current[seg] = isNextArrayIndex ? [] : {};
76+
}
77+
current = current[seg];
78+
}
79+
80+
const lastSeg = segments[segments.length - 1];
81+
current[lastSeg] = value;
82+
}
83+
84+
/**
85+
* Remove array item at pointer. Pointer must point to an array element.
86+
* @param {Object} obj - Root object to modify
87+
* @param {string} pointer - Pointer to array item (e.g. "/data/items/0")
88+
* @returns {boolean} True if removed, false otherwise
89+
*/
90+
export function removeArrayItemByPointer(obj, pointer) {
91+
const segments = parsePointer(pointer);
92+
if (segments.length < 2) return false;
93+
94+
const lastSeg = segments[segments.length - 1];
95+
const index = parseInt(lastSeg, 10);
96+
if (!Number.isInteger(index) || index < 0) return false;
97+
98+
const parentSegments = segments.slice(0, -1);
99+
let current = obj;
100+
for (let i = 0; i < parentSegments.length - 1; i += 1) {
101+
const seg = parentSegments[i];
102+
if (current == null || !(seg in current)) return false;
103+
current = current[seg];
104+
}
105+
106+
const parentKey = parentSegments[parentSegments.length - 1];
107+
const array = current?.[parentKey];
108+
if (current == null || !(parentKey in current) || !Array.isArray(array)) return false;
109+
110+
if (index < 0 || index >= array.length) return false;
111+
112+
array.splice(index, 1);
113+
return true;
114+
}

0 commit comments

Comments
 (0)