@structured-field/widget-editor 0.1.0
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/LICENSE +21 -0
- package/README.md +228 -0
- package/dist/structured-widget-editor.css +1 -0
- package/dist/structured-widget-editor.esm.js +9957 -0
- package/dist/structured-widget-editor.esm.js.map +1 -0
- package/dist/structured-widget-editor.iife.js +31 -0
- package/dist/structured-widget-editor.js +29 -0
- package/dist/structured-widget-editor.js.map +1 -0
- package/package.json +105 -0
- package/src/SchemaForm.vue +142 -0
- package/src/editors/ArrayEditor.vue +108 -0
- package/src/editors/BooleanEditor.vue +44 -0
- package/src/editors/HiddenEditor.vue +33 -0
- package/src/editors/NullableEditor.vue +94 -0
- package/src/editors/NumberEditor.vue +60 -0
- package/src/editors/ObjectEditor.vue +63 -0
- package/src/editors/RelationEditor.vue +208 -0
- package/src/editors/SchemaEditor.vue +68 -0
- package/src/editors/SelectEditor.vue +52 -0
- package/src/editors/StringEditor.vue +62 -0
- package/src/editors/UnionEditor.vue +83 -0
- package/src/index.js +155 -0
- package/src/scss/components/editors.scss +162 -0
- package/src/scss/components/form.scss +137 -0
- package/src/scss/components/relation.scss +134 -0
- package/src/scss/main.scss +3 -0
- package/src/utils.js +38 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 bnznamco
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
# @structured-field/widget-editor
|
|
2
|
+
|
|
3
|
+
A Vue-powered JSON Schema form builder with first-class support for relation fields (ForeignKey, QuerySet) and autocomplete search. Ships as both Vue components and a Web Component custom element.
|
|
4
|
+
|
|
5
|
+
Built for the [django-structured-json-field](https://github.com/bnznamco/django-structured-field) admin widget, but usable standalone with any JSON Schema + REST search endpoint.
|
|
6
|
+
|
|
7
|
+
## Features
|
|
8
|
+
|
|
9
|
+
- **JSON Schema driven** — renders forms from standard JSON Schema (`$ref`, `$defs`, `anyOf`, `oneOf`, `discriminator`)
|
|
10
|
+
- **Relation fields** — ForeignKey (single) and QuerySet (multi) with AJAX autocomplete from a search endpoint
|
|
11
|
+
- **Discriminated unions** — type selector that swaps sub-forms based on a discriminator property
|
|
12
|
+
- **Nullable fields** — togglable Add/Remove for optional nested objects
|
|
13
|
+
- **Array fields** — ordered list with add, remove, and reorder controls
|
|
14
|
+
- **Built with Vue 3** — internally uses Vue SFC components, exported as a Web Component custom element
|
|
15
|
+
- **Theme integration** — styles via CSS custom properties (ships with Django admin compatibility)
|
|
16
|
+
|
|
17
|
+
## Installation
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
npm install @structured-field/widget-editor
|
|
21
|
+
# or
|
|
22
|
+
pnpm add @structured-field/widget-editor
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
## Usage
|
|
26
|
+
|
|
27
|
+
### Web Component (custom element)
|
|
28
|
+
|
|
29
|
+
Register the `<schema-form>` custom element, then use it in any HTML page or framework:
|
|
30
|
+
|
|
31
|
+
```js
|
|
32
|
+
import { registerCustomElement } from '@structured-field/widget-editor';
|
|
33
|
+
import '@structured-field/widget-editor/css';
|
|
34
|
+
|
|
35
|
+
registerCustomElement(); // registers <schema-form>
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
#### Programmatic initialization (recommended)
|
|
39
|
+
|
|
40
|
+
```js
|
|
41
|
+
const el = document.createElement('schema-form');
|
|
42
|
+
|
|
43
|
+
el.schema = {
|
|
44
|
+
type: 'object',
|
|
45
|
+
properties: {
|
|
46
|
+
name: { type: 'string' },
|
|
47
|
+
age: { type: 'integer' },
|
|
48
|
+
},
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
el.initialData = { name: 'Ada', age: 36 };
|
|
52
|
+
|
|
53
|
+
el.addEventListener('change', (e) => {
|
|
54
|
+
console.log('Form value:', e.detail);
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
document.getElementById('my-form').appendChild(el);
|
|
58
|
+
|
|
59
|
+
// Get current value at any time
|
|
60
|
+
const value = el.getValue();
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
#### Declarative HTML
|
|
64
|
+
|
|
65
|
+
```html
|
|
66
|
+
<schema-form
|
|
67
|
+
schema='{"type":"object","properties":{"name":{"type":"string"}}}'
|
|
68
|
+
initial-data='{"name":"Ada"}'
|
|
69
|
+
></schema-form>
|
|
70
|
+
|
|
71
|
+
<script>
|
|
72
|
+
document.querySelector('schema-form').addEventListener('change', (e) => {
|
|
73
|
+
console.log(e.detail);
|
|
74
|
+
});
|
|
75
|
+
</script>
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
### As a Vue component
|
|
79
|
+
|
|
80
|
+
Import `SchemaForm` directly for use inside Vue applications:
|
|
81
|
+
|
|
82
|
+
```vue
|
|
83
|
+
<template>
|
|
84
|
+
<SchemaForm :schema="schema" :initial-data="data" @change="onFormChange" />
|
|
85
|
+
</template>
|
|
86
|
+
|
|
87
|
+
<script setup>
|
|
88
|
+
import { SchemaForm } from '@structured-field/widget-editor';
|
|
89
|
+
import '@structured-field/widget-editor/css';
|
|
90
|
+
|
|
91
|
+
const schema = { /* JSON Schema */ };
|
|
92
|
+
const data = { /* initial form data */ };
|
|
93
|
+
|
|
94
|
+
function onFormChange(value) {
|
|
95
|
+
console.log('Form value:', value);
|
|
96
|
+
}
|
|
97
|
+
</script>
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
### As an IIFE (script tag)
|
|
101
|
+
|
|
102
|
+
#### All-in-one bundle (recommended)
|
|
103
|
+
|
|
104
|
+
A single `<script>` tag that includes both JS and CSS — no separate stylesheet needed:
|
|
105
|
+
|
|
106
|
+
```html
|
|
107
|
+
<!-- Latest version -->
|
|
108
|
+
<script src="https://bnznamco.github.io/structured-widget-editor/latest/structured-widget-editor.iife.js"></script>
|
|
109
|
+
|
|
110
|
+
<!-- Pinned version -->
|
|
111
|
+
<script src="https://bnznamco.github.io/structured-widget-editor/v1.0.0/structured-widget-editor.iife.js"></script>
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
#### Separate JS + CSS
|
|
115
|
+
|
|
116
|
+
```html
|
|
117
|
+
<!-- Latest version -->
|
|
118
|
+
<link rel="stylesheet" href="https://bnznamco.github.io/structured-widget-editor/latest/structured-widget-editor.css">
|
|
119
|
+
<script src="https://bnznamco.github.io/structured-widget-editor/latest/structured-widget-editor.js"></script>
|
|
120
|
+
|
|
121
|
+
<!-- Pinned version -->
|
|
122
|
+
<link rel="stylesheet" href="https://bnznamco.github.io/structured-widget-editor/v1.0.0/structured-widget-editor.css">
|
|
123
|
+
<script src="https://bnznamco.github.io/structured-widget-editor/v1.0.0/structured-widget-editor.js"></script>
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
#### Via jsDelivr (from npm)
|
|
127
|
+
|
|
128
|
+
```html
|
|
129
|
+
<script src="https://cdn.jsdelivr.net/npm/@structured-field/widget-editor@latest/dist/structured-widget-editor.iife.js"></script>
|
|
130
|
+
<!-- or pinned -->
|
|
131
|
+
<script src="https://cdn.jsdelivr.net/npm/@structured-field/widget-editor@1.0.0/dist/structured-widget-editor.iife.js"></script>
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
#### Usage
|
|
135
|
+
|
|
136
|
+
```html
|
|
137
|
+
<script src="https://bnznamco.github.io/structured-widget-editor/latest/structured-widget-editor.iife.js"></script>
|
|
138
|
+
<script>
|
|
139
|
+
StructuredWidgetEditor.registerCustomElement();
|
|
140
|
+
|
|
141
|
+
const el = document.createElement('schema-form');
|
|
142
|
+
el.schema = { /* JSON Schema */ };
|
|
143
|
+
el.initialData = { /* data */ };
|
|
144
|
+
el.addEventListener('change', (e) => console.log(e.detail));
|
|
145
|
+
document.getElementById('my-form').appendChild(el);
|
|
146
|
+
</script>
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
## Relation Fields
|
|
150
|
+
|
|
151
|
+
The editor supports a custom `type: "relation"` schema extension for ForeignKey and QuerySet fields:
|
|
152
|
+
|
|
153
|
+
```json
|
|
154
|
+
{
|
|
155
|
+
"type": "relation",
|
|
156
|
+
"format": "select2",
|
|
157
|
+
"model": "app.ModelName",
|
|
158
|
+
"multiple": false,
|
|
159
|
+
"options": {
|
|
160
|
+
"select2": {
|
|
161
|
+
"placeholder": "Search...",
|
|
162
|
+
"allowClear": true,
|
|
163
|
+
"ajax": {
|
|
164
|
+
"url": "/api/search/app.ModelName/"
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
The search endpoint should return:
|
|
172
|
+
|
|
173
|
+
```json
|
|
174
|
+
{
|
|
175
|
+
"items": [
|
|
176
|
+
{ "id": 1, "name": "Item 1", "model": "app.modelname" },
|
|
177
|
+
{ "id": 2, "name": "Item 2", "model": "app.modelname" }
|
|
178
|
+
],
|
|
179
|
+
"more": false
|
|
180
|
+
}
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
Query parameters sent: `_q` (search term), `page` (pagination).
|
|
184
|
+
|
|
185
|
+
## Editors
|
|
186
|
+
|
|
187
|
+
| Schema type | Editor | Description |
|
|
188
|
+
|---|---|---|
|
|
189
|
+
| `string` | StringEditor | Text input or textarea |
|
|
190
|
+
| `integer` / `number` | NumberEditor | Number input with step |
|
|
191
|
+
| `boolean` | BooleanEditor | Checkbox |
|
|
192
|
+
| `enum` | SelectEditor | Dropdown select |
|
|
193
|
+
| `const` | HiddenEditor | Hidden (not rendered) |
|
|
194
|
+
| `object` | ObjectEditor | Nested fieldset |
|
|
195
|
+
| `array` | ArrayEditor | List with add/remove/reorder |
|
|
196
|
+
| `anyOf [T, null]` | NullableEditor | Togglable wrapper |
|
|
197
|
+
| `oneOf + discriminator` | UnionEditor | Type selector |
|
|
198
|
+
| `relation` | RelationEditor | Autocomplete with search |
|
|
199
|
+
|
|
200
|
+
## Theming
|
|
201
|
+
|
|
202
|
+
All styles use CSS custom properties with sensible defaults. Override them to match your design system:
|
|
203
|
+
|
|
204
|
+
```css
|
|
205
|
+
:root {
|
|
206
|
+
--body-bg: #fff;
|
|
207
|
+
--body-fg: #333;
|
|
208
|
+
--border-color: #ccc;
|
|
209
|
+
--primary: #79aec8;
|
|
210
|
+
--error-fg: #ba2121;
|
|
211
|
+
--darkened-bg: #f0f0f0;
|
|
212
|
+
--object-tools-bg: #888;
|
|
213
|
+
--object-tools-fg: #fff;
|
|
214
|
+
/* ... */
|
|
215
|
+
}
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
## Development
|
|
219
|
+
|
|
220
|
+
```bash
|
|
221
|
+
pnpm install
|
|
222
|
+
pnpm run dev # watch mode
|
|
223
|
+
pnpm run build # production build
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
## License
|
|
227
|
+
|
|
228
|
+
MIT
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
.structured-field-editor{width:100%;border-radius:4px;font-size:.8125rem}.structured-field-editor *,.structured-field-editor *::before,.structured-field-editor *::after{box-sizing:border-box}.structured-field-editor .sf-label{display:block;font-weight:600;margin-bottom:4px;text-transform:capitalize;color:var(--body-quiet-color, #666);font-size:.8125rem}.structured-field-editor .sf-label.required::after{content:" *";color:var(--error-fg, #ba2121)}.structured-field-editor .sf-input{display:block;width:100%;padding:6px 8px;border:1px solid var(--border-color, #ccc);border-radius:4px;background:var(--body-bg, #fff);color:var(--body-fg, #333);font-size:.8125rem;font-family:inherit;transition:border-color .15s}.structured-field-editor .sf-input:focus{outline:none;border-color:var(--primary, #79aec8);box-shadow:0 0 0 2px rgba(121,174,200,.2)}.structured-field-editor .sf-textarea{resize:vertical;min-height:60px}.structured-field-editor .sf-select{appearance:auto;cursor:pointer}.structured-field-editor .sf-checkbox{margin-right:6px;accent-color:var(--primary, #79aec8)}.structured-field-editor .sf-checkbox-label{display:flex;align-items:center;font-weight:normal;cursor:pointer;padding:6px 0}.structured-field-editor .sf-btn{display:inline-flex;align-items:center;gap:4px;padding:4px 12px;background:var(--object-tools-bg, #888);color:var(--object-tools-fg, #fff);font-weight:400;font-size:.6875rem;text-transform:uppercase;letter-spacing:.5px;border-radius:15px;border:none;cursor:pointer;transition:background-color .15s}.structured-field-editor .sf-btn:hover{background-color:var(--object-tools-hover-bg, #666)}.structured-field-editor .sf-btn i{font-size:.6rem}.structured-field-editor .sf-btn-sm{padding:2px 8px;font-size:.625rem}.structured-field-editor .sf-btn-add{background:var(--object-tools-bg, #888);color:var(--object-tools-fg, #fff)}.structured-field-editor .sf-btn-danger{background:var(--delete-button-bg, #ba2121);color:var(--delete-button-fg, #fff)}.structured-field-editor .sf-btn-danger:hover{background:#a41515}.structured-field-editor .sf-error{color:var(--error-fg, #ba2121);font-size:.75rem;margin-top:2px}.structured-field-editor .errors .sf-input{border-color:var(--error-fg, #ba2121)}.structured-field-editor .errorlist{margin:4px 0 0 0;padding:0;list-style:none;color:var(--error-fg, #ba2121);font-size:.75rem}.structured-field-editor .errorlist li{padding:2px 0}.form-row{overflow:visible !important}.structured-field-editor .sf-field{margin-bottom:12px}.structured-field-editor .sf-field-boolean{margin-bottom:8px}.structured-field-editor .sf-object:not(.sf-object-root){border:1px solid var(--border-color, #ccc);border-radius:4px;padding:12px;margin-bottom:12px;background:var(--body-bg, #fff)}.structured-field-editor .sf-object-title{font-weight:600;font-size:.8125rem;color:var(--body-fg, #333);text-transform:capitalize;padding:0 4px}.structured-field-editor .sf-object-fields{display:flex;flex-direction:column}.structured-field-editor .sf-array{margin-bottom:12px}.structured-field-editor .sf-array-header{display:flex;align-items:center;gap:8px;margin-bottom:8px}.structured-field-editor .sf-array-header .sf-label{margin-bottom:0}.structured-field-editor .sf-array-count{display:inline-flex;align-items:center;justify-content:center;min-width:20px;height:20px;padding:0 6px;background:var(--darkened-bg, #f0f0f0);border-radius:10px;font-size:.6875rem;font-weight:600;color:var(--body-quiet-color, #666)}.structured-field-editor .sf-array-items{display:flex;flex-direction:column;gap:8px}.structured-field-editor .sf-array-item{border:1px solid var(--border-color, #ccc);border-radius:4px;background:var(--darkened-bg, #f8f8f8);overflow:hidden}.structured-field-editor .sf-array-item-header{display:flex;align-items:center;justify-content:space-between;padding:4px 8px;background:var(--darkened-bg, #f0f0f0);border-bottom:1px solid var(--border-color, #ccc)}.structured-field-editor .sf-array-item-index{font-size:.6875rem;font-weight:600;color:var(--body-quiet-color, #666)}.structured-field-editor .sf-array-item-actions{display:flex;gap:4px}.structured-field-editor .sf-array-item-body{padding:10px}.structured-field-editor .sf-array-item-body>.sf-object.sf-object-root,.structured-field-editor .sf-array-item-body>.sf-object:not(.sf-object-root){border:none;padding:0;margin:0;background:rgba(0,0,0,0)}.structured-field-editor .sf-nullable{margin-bottom:12px}.structured-field-editor .sf-nullable-header{display:flex;align-items:center;gap:8px;margin-bottom:4px}.structured-field-editor .sf-nullable-header .sf-label{margin-bottom:0}.structured-field-editor .sf-nullable-body{padding-left:0}.structured-field-editor .sf-nullable-body:not(:empty){border:1px solid var(--border-color, #ccc);border-radius:4px;padding:12px;margin-top:4px;background:var(--body-bg, #fff)}.structured-field-editor .sf-union{margin-bottom:12px}.structured-field-editor .sf-union-body{border:1px solid var(--border-color, #ccc);border-radius:4px;padding:12px;margin-top:8px;background:var(--body-bg, #fff)}.structured-field-editor .sf-union-body>.sf-object{border:none;padding:0;margin:0;background:rgba(0,0,0,0)}.structured-field-editor .sf-relation{position:relative}.structured-field-editor .sf-relation-wrapper{display:flex;flex-direction:column;gap:6px}.structured-field-editor .sf-relation-selected{display:flex;flex-wrap:wrap;gap:6px}.structured-field-editor .sf-relation-selected:empty{display:none}.structured-field-editor .sf-relation-tag{display:inline-flex;align-items:center;gap:6px;padding:4px 10px;background:var(--darkened-bg, #f0f0f0);border:1px solid var(--border-color, #ccc);border-radius:4px;font-size:.8125rem;color:var(--body-fg, #333);max-width:100%}.structured-field-editor .sf-relation-tag-text{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.structured-field-editor .sf-relation-tag-remove{display:inline-flex;align-items:center;justify-content:center;width:18px;height:18px;padding:0;border:none;border-radius:50%;background:rgba(0,0,0,0);color:var(--body-quiet-color, #666);cursor:pointer;font-size:.6875rem;flex-shrink:0;transition:color .15s,background .15s}.structured-field-editor .sf-relation-tag-remove:hover{color:var(--error-fg, #ba2121);background:rgba(186,33,33,.1)}.structured-field-editor .sf-relation-search{position:relative}.structured-field-editor .sf-relation-input{padding-left:28px;background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='14' height='14' viewBox='0 0 24 24' fill='none' stroke='%23999' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Ccircle cx='11' cy='11' r='8'/%3E%3Cline x1='21' y1='21' x2='16.65' y2='16.65'/%3E%3C/svg%3E");background-repeat:no-repeat;background-position:8px center;background-size:14px}.structured-field-editor .sf-relation-dropdown{position:absolute;top:100%;left:0;right:0;z-index:1000;max-height:240px;overflow-y:auto;background:var(--body-bg, #fff);border:1px solid var(--border-color, #ccc);border-top:none;border-radius:0 0 4px 4px;box-shadow:0 4px 12px rgba(0,0,0,.1)}.structured-field-editor .sf-relation-dropdown-item{padding:8px 12px;cursor:pointer;font-size:.8125rem;color:var(--body-fg, #333);transition:background .1s;border-bottom:1px solid var(--hairline-color, #eee)}.structured-field-editor .sf-relation-dropdown-item:last-child{border-bottom:none}.structured-field-editor .sf-relation-dropdown-item:hover,.structured-field-editor .sf-relation-dropdown-item.highlighted{background:var(--selected-bg, #e4e4e4)}.structured-field-editor .sf-relation-dropdown-empty{padding:12px;text-align:center;color:var(--body-quiet-color, #999);font-size:.8125rem;font-style:italic}.structured-field-editor .sf-relation-dropdown-more{padding:8px 12px;text-align:center;cursor:pointer;font-size:.75rem;color:var(--link-fg, #417690);font-weight:600;border-top:1px solid var(--hairline-color, #eee);transition:background .1s}.structured-field-editor .sf-relation-dropdown-more:hover{background:var(--darkened-bg, #f0f0f0)}
|