@structured-field/widget-editor 1.1.1 → 1.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@structured-field/widget-editor",
3
- "version": "1.1.1",
3
+ "version": "1.2.1",
4
4
  "description": "A lightweight JSON Schema form builder with support for relation fields (ForeignKey, QuerySet) and autocomplete search.",
5
5
  "type": "module",
6
6
  "main": "dist/structured-widget-editor.js",
@@ -0,0 +1,108 @@
1
+ /**
2
+ * Base class for creating web component custom editors.
3
+ *
4
+ * Handles the property contract with the structured-widget-editor wrapper:
5
+ * - Receives `schema`, `modelValue`, `path`, and `form` as JS properties.
6
+ * - Provides `emitChange(value)` to dispatch the value back to the form.
7
+ * - Provides `getErrors()` to retrieve validation errors for this field.
8
+ * - Calls `render()` once on `connectedCallback` and `update()` on property changes.
9
+ *
10
+ * @example
11
+ * import { BaseEditorElement } from '@structured-field/widget-editor';
12
+ *
13
+ * class MyColorPicker extends BaseEditorElement {
14
+ * render() {
15
+ * const input = document.createElement('input');
16
+ * input.type = 'color';
17
+ * input.value = this.modelValue || '#000000';
18
+ * input.addEventListener('input', () => this.emitChange(input.value));
19
+ * this._input = input;
20
+ * this.appendChild(input);
21
+ * }
22
+ *
23
+ * update() {
24
+ * if (this._input) this._input.value = this.modelValue || '#000000';
25
+ * }
26
+ * }
27
+ *
28
+ * customElements.define('my-color-picker', MyColorPicker);
29
+ */
30
+ export class BaseEditorElement extends HTMLElement {
31
+ constructor() {
32
+ super();
33
+ this._schema = {};
34
+ this._modelValue = undefined;
35
+ this._path = [];
36
+ this._form = null;
37
+ this._connected = false;
38
+ }
39
+
40
+ /* ── Property contract ─────────────────────────────────────────────── */
41
+
42
+ get schema() { return this._schema; }
43
+ set schema(v) {
44
+ this._schema = v;
45
+ if (this._connected) this.update();
46
+ }
47
+
48
+ get modelValue() { return this._modelValue; }
49
+ set modelValue(v) {
50
+ this._modelValue = v;
51
+ if (this._connected) this.update();
52
+ }
53
+
54
+ get path() { return this._path; }
55
+ set path(v) {
56
+ this._path = v;
57
+ if (this._connected) this.update();
58
+ }
59
+
60
+ get form() { return this._form; }
61
+ set form(v) {
62
+ this._form = v;
63
+ if (this._connected) this.update();
64
+ }
65
+
66
+ /* ── Lifecycle ─────────────────────────────────────────────────────── */
67
+
68
+ connectedCallback() {
69
+ this._connected = true;
70
+ this.render();
71
+ }
72
+
73
+ disconnectedCallback() {
74
+ this._connected = false;
75
+ }
76
+
77
+ /* ── Helpers ───────────────────────────────────────────────────────── */
78
+
79
+ /**
80
+ * Dispatch the new value back to the form.
81
+ * @param {*} value - The new field value.
82
+ */
83
+ emitChange(value) {
84
+ this.dispatchEvent(new CustomEvent('change', { detail: value }));
85
+ }
86
+
87
+ /**
88
+ * Returns the current validation errors for this field.
89
+ * @returns {string[]}
90
+ */
91
+ getErrors() {
92
+ return this._form?.getErrorsForPath?.(this._path) ?? [];
93
+ }
94
+
95
+ /* ── Override points ───────────────────────────────────────────────── */
96
+
97
+ /**
98
+ * Called once when the element is connected to the DOM.
99
+ * Build the initial DOM structure here.
100
+ */
101
+ render() {}
102
+
103
+ /**
104
+ * Called whenever a property (schema, modelValue, path, form) changes
105
+ * after the initial render. Update the DOM here.
106
+ */
107
+ update() {}
108
+ }
@@ -14,6 +14,7 @@
14
14
  <script>
15
15
  import SchemaEditor from './editors/SchemaEditor.vue';
16
16
  import { deepClone } from './utils';
17
+ import { applyConditionals, hasConditionals } from './conditionals';
17
18
 
18
19
  export default {
19
20
  name: 'SchemaForm',
@@ -22,9 +23,17 @@ export default {
22
23
  schema: { type: [Object, String], default: () => ({}) },
23
24
  initialData: { default: undefined },
24
25
  errors: { type: Object, default: () => ({}) },
26
+ customEditors: { type: Array, default: () => [] },
27
+ language: { type: String, default: '' },
25
28
  },
26
29
  emits: ['change'],
27
30
  expose: ['getValue'],
31
+ provide() {
32
+ return {
33
+ customEditors: () => this.customEditors,
34
+ language: () => this.language,
35
+ };
36
+ },
28
37
  data() {
29
38
  const parsedSchema = typeof this.schema === 'string' ? JSON.parse(this.schema) : this.schema;
30
39
  const defs = parsedSchema.$defs || parsedSchema.definitions || {};
@@ -42,6 +51,7 @@ export default {
42
51
  return {
43
52
  resolveSchema: (s) => this.resolveSchema(s),
44
53
  getSchemaAtPath: (p) => this.getSchemaAtPath(p),
54
+ getEffectiveSchemaAtPath: (p) => this.getEffectiveSchemaAtPath(p),
45
55
  getErrorsForPath: (p) => this.getErrorsForPath(p),
46
56
  };
47
57
  },
@@ -124,6 +134,28 @@ export default {
124
134
  return schema;
125
135
  },
126
136
 
137
+ getEffectiveSchemaAtPath(path) {
138
+ let schema = this.resolveSchema(this.rootSchema);
139
+ let value = this.currentValue;
140
+ for (const segment of path) {
141
+ if (!schema) return null;
142
+ if (schema.properties || schema.if || schema.allOf || schema.dependentSchemas) {
143
+ if (hasConditionals(schema)) schema = applyConditionals(schema, value || {}, (s) => this.resolveSchema(s));
144
+ }
145
+ if (schema.properties && schema.properties[segment] !== undefined) {
146
+ schema = this.resolveSchema(schema.properties[segment]);
147
+ value = value != null ? value[segment] : undefined;
148
+ } else if (schema.items) {
149
+ schema = this.resolveSchema(schema.items);
150
+ value = Array.isArray(value) ? value[segment] : undefined;
151
+ } else {
152
+ return null;
153
+ }
154
+ }
155
+ if (schema && hasConditionals(schema)) schema = applyConditionals(schema, value || {});
156
+ return schema;
157
+ },
158
+
127
159
  onValueChange(val) {
128
160
  this.currentValue = val;
129
161
  this.$emit('change', val);
@@ -0,0 +1,248 @@
1
+ // JSON Schema conditional evaluation for form rendering.
2
+ //
3
+ // Supports the standard keywords: `if`/`then`/`else`, `allOf` of those,
4
+ // `dependentSchemas`, and `dependentRequired`. The matcher implements the
5
+ // subset of JSON Schema validation that is meaningful for form-time
6
+ // conditionals on object properties:
7
+ //
8
+ // - `properties: { field: { const, enum, type, not } }`
9
+ // - `required: [...]` (treated as "key is present and not null/undefined")
10
+ // - `not`, `allOf`, `anyOf`, `oneOf` (recursive)
11
+ //
12
+ // The functions are pure: they take a schema + value and return an
13
+ // "effective schema" with `properties`/`required` merged from any matching
14
+ // branches. The renderer uses that effective schema instead of the raw one.
15
+
16
+ function isPresent(value, key) {
17
+ if (value == null || typeof value !== 'object') return false;
18
+ if (!(key in value)) return false;
19
+ const v = value[key];
20
+ return v !== undefined && v !== null && v !== '';
21
+ }
22
+
23
+ function matchesPropertyConstraint(value, constraint) {
24
+ if (!constraint || typeof constraint !== 'object') return true;
25
+ if ('const' in constraint) return value === constraint.const;
26
+ if (Array.isArray(constraint.enum)) return constraint.enum.includes(value);
27
+ if (constraint.type) {
28
+ const t = constraint.type;
29
+ if (t === 'string' && typeof value !== 'string') return false;
30
+ if (t === 'number' && typeof value !== 'number') return false;
31
+ if (t === 'integer' && (typeof value !== 'number' || !Number.isInteger(value))) return false;
32
+ if (t === 'boolean' && typeof value !== 'boolean') return false;
33
+ if (t === 'null' && value !== null) return false;
34
+ if (t === 'array' && !Array.isArray(value)) return false;
35
+ if (t === 'object' && (value == null || typeof value !== 'object' || Array.isArray(value))) return false;
36
+ }
37
+ // Numeric comparators
38
+ if (typeof value === 'number') {
39
+ if (typeof constraint.minimum === 'number' && value < constraint.minimum) return false;
40
+ if (typeof constraint.maximum === 'number' && value > constraint.maximum) return false;
41
+ if (typeof constraint.exclusiveMinimum === 'number' && value <= constraint.exclusiveMinimum) return false;
42
+ if (typeof constraint.exclusiveMaximum === 'number' && value >= constraint.exclusiveMaximum) return false;
43
+ if (typeof constraint.multipleOf === 'number' && constraint.multipleOf > 0) {
44
+ const q = value / constraint.multipleOf;
45
+ if (Math.abs(q - Math.round(q)) > 1e-9) return false;
46
+ }
47
+ }
48
+ // String comparators
49
+ if (typeof value === 'string') {
50
+ if (typeof constraint.minLength === 'number' && value.length < constraint.minLength) return false;
51
+ if (typeof constraint.maxLength === 'number' && value.length > constraint.maxLength) return false;
52
+ if (typeof constraint.pattern === 'string') {
53
+ try {
54
+ if (!new RegExp(constraint.pattern).test(value)) return false;
55
+ } catch (e) {
56
+ // Invalid pattern — treat as non-match rather than throwing in render path.
57
+ return false;
58
+ }
59
+ }
60
+ }
61
+ if (constraint.not) return !matchesSchema(value, constraint.not);
62
+ return true;
63
+ }
64
+
65
+ // Returns true if `value` (an object) satisfies the form-relevant subset of `schema`.
66
+ export function matchesSchema(value, schema) {
67
+ if (!schema || typeof schema !== 'object') return true;
68
+
69
+ if (Array.isArray(schema.required)) {
70
+ for (const k of schema.required) {
71
+ if (!isPresent(value, k)) return false;
72
+ }
73
+ }
74
+
75
+ if (schema.properties && typeof schema.properties === 'object') {
76
+ for (const [k, constraint] of Object.entries(schema.properties)) {
77
+ // Standard JSON Schema: property constraints only apply if the key is present.
78
+ if (value == null || !(k in value)) continue;
79
+ if (!matchesPropertyConstraint(value[k], constraint)) return false;
80
+ }
81
+ }
82
+
83
+ if (schema.not && matchesSchema(value, schema.not)) return false;
84
+ if (Array.isArray(schema.allOf) && !schema.allOf.every((s) => matchesSchema(value, s))) return false;
85
+ if (Array.isArray(schema.anyOf) && !schema.anyOf.some((s) => matchesSchema(value, s))) return false;
86
+ if (Array.isArray(schema.oneOf)) {
87
+ const matched = schema.oneOf.filter((s) => matchesSchema(value, s)).length;
88
+ if (matched !== 1) return false;
89
+ }
90
+
91
+ return true;
92
+ }
93
+
94
+ function insertAfter(properties, anchorKey, newEntries) {
95
+ // Rebuild the property map so newly-added keys appear immediately after
96
+ // their controlling field instead of being appended to the end.
97
+ const existingKeys = Object.keys(properties);
98
+ const anchorIdx = anchorKey ? existingKeys.indexOf(anchorKey) : -1;
99
+ if (anchorIdx === -1) {
100
+ const out = { ...properties };
101
+ for (const [k, v] of newEntries) out[k] = v;
102
+ return out;
103
+ }
104
+ const out = {};
105
+ for (let i = 0; i < existingKeys.length; i++) {
106
+ const key = existingKeys[i];
107
+ out[key] = properties[key];
108
+ if (i === anchorIdx) {
109
+ for (const [k, v] of newEntries) {
110
+ if (!(k in properties)) out[k] = v;
111
+ }
112
+ }
113
+ }
114
+ // Any new keys that already existed in properties have been kept in place;
115
+ // overwrite their values with the merged versions.
116
+ for (const [k, v] of newEntries) {
117
+ if (k in properties) out[k] = v;
118
+ }
119
+ return out;
120
+ }
121
+
122
+ function mergeBranch(target, branch, anchorKey) {
123
+ if (!branch || typeof branch !== 'object') return target;
124
+
125
+ if (branch.properties) {
126
+ const merged = [];
127
+ for (const [k, v] of Object.entries(branch.properties)) {
128
+ const existing = target.properties && target.properties[k];
129
+ merged.push([k, existing ? { ...existing, ...v } : v]);
130
+ }
131
+ target.properties = insertAfter(target.properties || {}, anchorKey, merged);
132
+ }
133
+
134
+ if (Array.isArray(branch.required)) {
135
+ const set = new Set(target.required || []);
136
+ for (const k of branch.required) set.add(k);
137
+ target.required = Array.from(set);
138
+ }
139
+
140
+ // Conditionals can also nest more conditionals — flatten them.
141
+ if (branch.allOf) {
142
+ target.allOf = [...(target.allOf || []), ...branch.allOf];
143
+ }
144
+ if (branch.if) {
145
+ target.allOf = [...(target.allOf || []), { if: branch.if, then: branch.then, else: branch.else }];
146
+ }
147
+ if (branch.dependentSchemas) {
148
+ target.dependentSchemas = { ...(target.dependentSchemas || {}), ...branch.dependentSchemas };
149
+ }
150
+ if (branch.dependentRequired) {
151
+ target.dependentRequired = { ...(target.dependentRequired || {}), ...branch.dependentRequired };
152
+ }
153
+
154
+ return target;
155
+ }
156
+
157
+ // Returns an effective schema for an object schema given the current value.
158
+ // Resolves `if/then/else`, `allOf` of those, `dependentSchemas`, and
159
+ // `dependentRequired`. Idempotent and safe to call on every render.
160
+ //
161
+ // `resolver` (optional) is called on each `then`/`else`/`dependentSchemas`
162
+ // branch before merging, so `$ref` inside conditional branches is followed.
163
+ // Pass `form.resolveSchema` from the renderer.
164
+ export function applyConditionals(schema, value, resolver) {
165
+ const resolve = typeof resolver === 'function' ? resolver : (s) => s;
166
+ if (!schema || typeof schema !== 'object') return schema;
167
+ if (schema.type !== 'object' && !schema.properties) return schema;
168
+
169
+ // Start with a shallow clone of the parts we may mutate.
170
+ let effective = {
171
+ ...schema,
172
+ properties: { ...(schema.properties || {}) },
173
+ required: Array.isArray(schema.required) ? [...schema.required] : [],
174
+ };
175
+
176
+ const safeValue = value && typeof value === 'object' ? value : {};
177
+
178
+ // Pick the controlling field of an `if` clause so newly-added properties
179
+ // can be inserted right after it in render order.
180
+ const anchorOf = (ifClause) => {
181
+ if (!ifClause || typeof ifClause !== 'object') return null;
182
+ const props = ifClause.properties && Object.keys(ifClause.properties);
183
+ if (props && props.length) return props[0];
184
+ if (Array.isArray(ifClause.required) && ifClause.required.length) return ifClause.required[0];
185
+ return null;
186
+ };
187
+
188
+ // Collect rules: top-level if/then/else + every entry in allOf that has one.
189
+ const rules = [];
190
+ if (effective.if) {
191
+ rules.push({ if: effective.if, then: effective.then, else: effective.else, anchor: anchorOf(effective.if) });
192
+ }
193
+ if (Array.isArray(effective.allOf)) {
194
+ for (const entry of effective.allOf) {
195
+ if (entry && typeof entry === 'object' && entry.if) {
196
+ rules.push({ if: entry.if, then: entry.then, else: entry.else, anchor: anchorOf(entry.if) });
197
+ } else if (entry && typeof entry === 'object' && (entry.properties || entry.required)) {
198
+ // Plain allOf branch (e.g. shared base) — always merge.
199
+ mergeBranch(effective, entry);
200
+ }
201
+ }
202
+ }
203
+
204
+ // Iterate to a fixed point so newly-merged rules can themselves trigger further rules.
205
+ // Capped to avoid pathological loops.
206
+ for (let i = 0; i < 8; i++) {
207
+ let changed = false;
208
+ const before = JSON.stringify({ p: effective.properties, r: effective.required });
209
+
210
+ for (const rule of rules) {
211
+ const matched = matchesSchema(safeValue, rule.if);
212
+ const branch = matched ? rule.then : rule.else;
213
+ if (branch) mergeBranch(effective, resolve(branch), rule.anchor);
214
+ }
215
+
216
+ if (effective.dependentSchemas) {
217
+ for (const [key, branch] of Object.entries(effective.dependentSchemas)) {
218
+ if (isPresent(safeValue, key)) mergeBranch(effective, resolve(branch), key);
219
+ }
220
+ }
221
+
222
+ if (effective.dependentRequired) {
223
+ for (const [key, requiredKeys] of Object.entries(effective.dependentRequired)) {
224
+ if (isPresent(safeValue, key) && Array.isArray(requiredKeys)) {
225
+ const set = new Set(effective.required || []);
226
+ for (const k of requiredKeys) set.add(k);
227
+ effective.required = Array.from(set);
228
+ }
229
+ }
230
+ }
231
+
232
+ const after = JSON.stringify({ p: effective.properties, r: effective.required });
233
+ if (after !== before) changed = true;
234
+ if (!changed) break;
235
+ }
236
+
237
+ return effective;
238
+ }
239
+
240
+ // Returns true if the schema declares any form-relevant conditional logic.
241
+ export function hasConditionals(schema) {
242
+ if (!schema || typeof schema !== 'object') return false;
243
+ if (schema.if || schema.dependentSchemas || schema.dependentRequired) return true;
244
+ if (Array.isArray(schema.allOf)) {
245
+ return schema.allOf.some((e) => e && typeof e === 'object' && (e.if || e.dependentSchemas));
246
+ }
247
+ return false;
248
+ }
@@ -2,7 +2,7 @@
2
2
  <div v-if="isRoot" class="sf-object sf-object-root">
3
3
  <div class="sf-object-fields">
4
4
  <SchemaEditor
5
- v-for="(propSchema, key) in (schema.properties || {})"
5
+ v-for="(propSchema, key) in (effectiveSchema.properties || {})"
6
6
  :key="key"
7
7
  :schema="form.resolveSchema(propSchema)"
8
8
  :model-value="(modelValue || {})[key]"
@@ -22,7 +22,7 @@
22
22
  </legend>
23
23
  <div v-show="!collapsed" class="sf-object-fields">
24
24
  <SchemaEditor
25
- v-for="(propSchema, key) in (schema.properties || {})"
25
+ v-for="(propSchema, key) in (effectiveSchema.properties || {})"
26
26
  :key="key"
27
27
  :schema="form.resolveSchema(propSchema)"
28
28
  :model-value="(modelValue || {})[key]"
@@ -37,6 +37,7 @@
37
37
  <script>
38
38
  import SchemaEditor from './SchemaEditor.vue';
39
39
  import SfIcon from './SfIcon.vue';
40
+ import { applyConditionals, hasConditionals } from '../conditionals';
40
41
 
41
42
  export default {
42
43
  name: 'ObjectEditor',
@@ -64,10 +65,14 @@ export default {
64
65
  title() {
65
66
  return this.schema.title || this.humanize(this.path[this.path.length - 1]) || '';
66
67
  },
68
+ effectiveSchema() {
69
+ if (!hasConditionals(this.schema)) return this.schema;
70
+ return applyConditionals(this.schema, this.modelValue || {}, this.form?.resolveSchema);
71
+ },
67
72
  summary() {
68
73
  const val = this.modelValue || {};
69
74
  const parts = [];
70
- for (const key of Object.keys(this.schema.properties || {})) {
75
+ for (const key of Object.keys(this.effectiveSchema.properties || {})) {
71
76
  if (parts.length >= 3) break;
72
77
  const v = val[key];
73
78
  if (v !== null && v !== undefined && v !== '' && typeof v !== 'object') {
@@ -90,7 +95,22 @@ export default {
90
95
  },
91
96
  onChildChange(key, value) {
92
97
  const newVal = { ...(this.modelValue || {}), [key]: value };
93
- this.$emit('update:modelValue', newVal);
98
+ this.$emit('update:modelValue', this.pruneInactive(newVal));
99
+ },
100
+ pruneInactive(value) {
101
+ if (!hasConditionals(this.schema)) return value;
102
+ const effective = applyConditionals(this.schema, value, this.form?.resolveSchema);
103
+ const allowed = new Set(Object.keys(effective.properties || {}));
104
+ let changed = false;
105
+ const out = {};
106
+ for (const k of Object.keys(value)) {
107
+ if (allowed.has(k)) {
108
+ out[k] = value[k];
109
+ } else {
110
+ changed = true;
111
+ }
112
+ }
113
+ return changed ? out : value;
94
114
  },
95
115
  },
96
116
  };
@@ -46,6 +46,9 @@ import SfIcon from './SfIcon.vue';
46
46
  export default {
47
47
  name: 'RelationEditor',
48
48
  components: { SfIcon },
49
+ inject: {
50
+ language: { from: 'language', default: () => () => '' },
51
+ },
49
52
  props: {
50
53
  schema: { type: Object, required: true },
51
54
  modelValue: { default: null },
@@ -151,6 +154,8 @@ export default {
151
154
  const url = new URL(this.searchUrl, window.location.origin);
152
155
  url.searchParams.set('_q', query || '');
153
156
  url.searchParams.set('page', String(page));
157
+ const lang = typeof this.language === 'function' ? this.language() : this.language;
158
+ if (lang) url.searchParams.set('_lang', lang);
154
159
 
155
160
  const response = await fetch(url, { credentials: 'same-origin' });
156
161
  if (!response.ok) return;
@@ -1,5 +1,16 @@
1
1
  <template>
2
+ <WebComponentWrapper
3
+ v-if="isWebComponent"
4
+ ref="editor"
5
+ :tag-name="editorComponent"
6
+ :schema="schema"
7
+ :model-value="modelValue"
8
+ :path="path"
9
+ :form="form"
10
+ @update:model-value="$emit('update:modelValue', $event)"
11
+ />
2
12
  <component
13
+ v-else
3
14
  ref="editor"
4
15
  :is="editorComponent"
5
16
  :schema="schema"
@@ -21,6 +32,7 @@ import ArrayEditor from './ArrayEditor.vue';
21
32
  import NullableEditor from './NullableEditor.vue';
22
33
  import UnionEditor from './UnionEditor.vue';
23
34
  import RelationEditor from './RelationEditor.vue';
35
+ import WebComponentWrapper from './WebComponentWrapper.vue';
24
36
 
25
37
  const MAX_DEPTH = 12;
26
38
 
@@ -37,6 +49,10 @@ export default {
37
49
  NullableEditor,
38
50
  UnionEditor,
39
51
  RelationEditor,
52
+ WebComponentWrapper,
53
+ },
54
+ inject: {
55
+ customEditors: { default: () => () => [] },
40
56
  },
41
57
  props: {
42
58
  schema: { type: Object, required: true },
@@ -58,11 +74,20 @@ export default {
58
74
  },
59
75
  },
60
76
  computed: {
77
+ isWebComponent() {
78
+ const c = this.editorComponent;
79
+ return typeof c === 'string' && c.includes('-');
80
+ },
61
81
  editorComponent() {
62
82
  const schema = this.schema;
63
83
 
64
84
  if (this.path.length > MAX_DEPTH) return 'StringEditor';
65
85
 
86
+ const overrides = this.customEditors();
87
+ for (const override of overrides) {
88
+ if (override.match(schema, this.path)) return override.component;
89
+ }
90
+
66
91
  if (schema.type === 'relation') return 'RelationEditor';
67
92
  if (schema.oneOf && schema.discriminator) return 'UnionEditor';
68
93
  if ('const' in schema) return 'HiddenEditor';
@@ -0,0 +1,55 @@
1
+ <script>
2
+ import { h, ref, watch, onMounted, onBeforeUnmount } from 'vue';
3
+
4
+ export default {
5
+ name: 'WebComponentWrapper',
6
+ props: {
7
+ tagName: { type: String, required: true },
8
+ schema: { type: Object, required: true },
9
+ modelValue: { default: undefined },
10
+ path: { type: Array, default: () => [] },
11
+ form: { type: Object, required: true },
12
+ },
13
+ emits: ['update:modelValue'],
14
+ setup(props, { emit }) {
15
+ const elRef = ref(null);
16
+
17
+ function syncProps() {
18
+ const el = elRef.value;
19
+ if (!el) return;
20
+ el.schema = props.schema;
21
+ el.modelValue = props.modelValue;
22
+ el.path = props.path;
23
+ el.form = props.form;
24
+ }
25
+
26
+ function handleChange(e) {
27
+ const value = e.detail != null
28
+ ? (Array.isArray(e.detail) ? e.detail[0] : e.detail)
29
+ : undefined;
30
+ emit('update:modelValue', value);
31
+ }
32
+
33
+ onMounted(() => {
34
+ syncProps();
35
+ const el = elRef.value;
36
+ if (el) {
37
+ el.addEventListener('update:model-value', handleChange);
38
+ el.addEventListener('change', handleChange);
39
+ }
40
+ });
41
+
42
+ onBeforeUnmount(() => {
43
+ const el = elRef.value;
44
+ if (el) {
45
+ el.removeEventListener('update:model-value', handleChange);
46
+ el.removeEventListener('change', handleChange);
47
+ }
48
+ });
49
+
50
+ watch(() => [props.schema, props.modelValue, props.path, props.form], syncProps, { deep: true });
51
+
52
+ return () => h(props.tagName, { ref: elRef });
53
+ },
54
+ };
55
+ </script>
package/src/index.js CHANGED
@@ -12,6 +12,9 @@ export { default as ArrayEditor } from './editors/ArrayEditor.vue';
12
12
  export { default as NullableEditor } from './editors/NullableEditor.vue';
13
13
  export { default as UnionEditor } from './editors/UnionEditor.vue';
14
14
  export { default as RelationEditor } from './editors/RelationEditor.vue';
15
+ export { default as WebComponentWrapper } from './editors/WebComponentWrapper.vue';
16
+ export { BaseEditorElement } from './BaseEditorElement.js';
17
+ export { applyConditionals, matchesSchema, hasConditionals } from './conditionals.js';
15
18
 
16
19
  import { defineCustomElement } from 'vue';
17
20
  import SchemaFormComponent from './SchemaForm.vue';
@@ -131,7 +131,8 @@
131
131
  border: 1px solid var(--border-color, #ccc);
132
132
  border-radius: 4px;
133
133
  background: var(--darkened-bg, #f8f8f8);
134
- overflow: hidden;
134
+ overflow: visible;
135
+ position: relative;
135
136
  }
136
137
 
137
138
  .sf-array-item {
@@ -156,6 +157,7 @@
156
157
  padding: 4px 8px;
157
158
  background: var(--darkened-bg, #f0f0f0);
158
159
  border-bottom: 1px solid var(--border-color, #ccc);
160
+ border-radius: 4px 4px 0 0;
159
161
  }
160
162
 
161
163
  .sf-array-item-left {