@rokkit/forms 1.0.0-next.124 → 1.0.0-next.127
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 +251 -0
- package/dist/src/display/index.d.ts +5 -0
- package/dist/src/index.d.ts +9 -0
- package/dist/src/input/index.d.ts +3 -0
- package/dist/src/lib/builder.svelte.d.ts +114 -4
- package/dist/src/lib/lookup.svelte.d.ts +87 -0
- package/dist/src/lib/renderers.d.ts +23 -0
- package/package.json +6 -4
- package/src/FieldLayout.svelte +4 -11
- package/src/FormRenderer.svelte +202 -61
- package/src/InfoField.svelte +26 -0
- package/src/Input.svelte +17 -61
- package/src/InputField.svelte +15 -11
- package/src/ValidationReport.svelte +52 -0
- package/src/display/DisplayCardGrid.svelte +68 -0
- package/src/display/DisplayList.svelte +31 -0
- package/src/display/DisplaySection.svelte +20 -0
- package/src/display/DisplayTable.svelte +68 -0
- package/src/display/DisplayValue.svelte +44 -0
- package/src/display/index.js +5 -0
- package/src/index.js +14 -0
- package/src/input/ArrayEditor.svelte +108 -0
- package/src/input/InputCheckbox.svelte +2 -3
- package/src/input/InputColor.svelte +6 -1
- package/src/input/InputDate.svelte +6 -1
- package/src/input/InputDateTime.svelte +6 -1
- package/src/input/InputEmail.svelte +6 -1
- package/src/input/InputFile.svelte +6 -2
- package/src/input/InputMonth.svelte +6 -1
- package/src/input/InputNumber.svelte +6 -1
- package/src/input/InputPassword.svelte +6 -1
- package/src/input/InputRange.svelte +6 -1
- package/src/input/InputSelect.svelte +31 -53
- package/src/input/InputSwitch.svelte +4 -15
- package/src/input/InputTel.svelte +6 -1
- package/src/input/InputText.svelte +6 -1
- package/src/input/InputTextArea.svelte +6 -1
- package/src/input/InputTime.svelte +6 -1
- package/src/input/InputToggle.svelte +28 -0
- package/src/input/InputUrl.svelte +6 -1
- package/src/input/InputWeek.svelte +6 -1
- package/src/input/index.js +3 -1
- package/src/lib/Input.svelte +3 -3
- package/src/lib/builder.svelte.js +425 -30
- package/src/lib/fields.js +2 -2
- package/src/lib/layout.js +2 -2
- package/src/lib/lookup.svelte.js +334 -0
- package/src/lib/renderers.js +83 -0
- package/src/lib/schema.js +1 -1
- package/src/types.js +0 -9
- package/dist/src/forms-old/input/types.d.ts +0 -7
- package/dist/src/forms-old/lib/form.d.ts +0 -95
- package/dist/src/forms-old/lib/index.d.ts +0 -1
- package/dist/src/lib/deprecated/nested.d.ts +0 -48
- package/dist/src/lib/deprecated/nested.spec.d.ts +0 -1
- package/dist/src/lib/deprecated/validator.d.ts +0 -30
- package/dist/src/lib/deprecated/validator.spec.d.ts +0 -1
- package/src/DataEditor.svelte +0 -30
- package/src/ListEditor.svelte +0 -44
- package/src/NestedEditor.svelte +0 -85
- package/src/forms-old/CheckBox.svelte +0 -56
- package/src/forms-old/DataEditor.svelte +0 -30
- package/src/forms-old/FieldLayout.svelte +0 -48
- package/src/forms-old/Form.svelte +0 -17
- package/src/forms-old/Icon.svelte +0 -76
- package/src/forms-old/Item.svelte +0 -25
- package/src/forms-old/ListEditor.svelte +0 -44
- package/src/forms-old/Tabs.svelte +0 -57
- package/src/forms-old/Wrapper.svelte +0 -12
- package/src/forms-old/input/Input.svelte +0 -17
- package/src/forms-old/input/InputField.svelte +0 -70
- package/src/forms-old/input/InputSelect.svelte +0 -23
- package/src/forms-old/input/InputSwitch.svelte +0 -19
- package/src/forms-old/input/types.js +0 -29
- package/src/forms-old/lib/form.js +0 -72
- package/src/forms-old/lib/index.js +0 -12
- package/src/forms-old/mocks/CustomField.svelte +0 -7
- package/src/forms-old/mocks/CustomWrapper.svelte +0 -8
- package/src/forms-old/mocks/Register.svelte +0 -25
- package/src/inp/Input.svelte +0 -17
- package/src/inp/InputField.svelte +0 -69
- package/src/inp/InputSelect.svelte +0 -23
- package/src/inp/InputSwitch.svelte +0 -19
- package/src/lib/deprecated/Form.svelte +0 -17
- package/src/lib/deprecated/FormRenderer.svelte +0 -121
- package/src/lib/deprecated/nested.js +0 -192
- package/src/lib/deprecated/nested.spec.js +0 -512
- package/src/lib/deprecated/validator.js +0 -137
- package/src/lib/deprecated/validator.spec.js +0 -348
package/README.md
ADDED
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
# @rokkit/forms
|
|
2
|
+
|
|
3
|
+
Schema-driven form rendering for Svelte 5. Generate dynamic forms from data, schema, and layout definitions.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
bun add @rokkit/forms
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Core Concepts
|
|
12
|
+
|
|
13
|
+
### FormBuilder
|
|
14
|
+
|
|
15
|
+
Takes `(data, schema, layout)` and produces a reactive `elements[]` array. Each element describes a form field with its type, current value, and merged properties.
|
|
16
|
+
|
|
17
|
+
```js
|
|
18
|
+
import { FormBuilder } from '@rokkit/forms'
|
|
19
|
+
|
|
20
|
+
const builder = new FormBuilder(
|
|
21
|
+
{ name: 'Alice', age: 30 }, // data
|
|
22
|
+
schema, // optional — auto-derived from data
|
|
23
|
+
layout // optional — auto-derived from data
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
// builder.elements → [{ scope, type, value, props }, ...]
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
If `schema` is `null`, it is auto-derived from the data using `deriveSchemaFromValue()`. If `layout` is `null`, it is auto-derived using `deriveLayoutFromValue()`.
|
|
30
|
+
|
|
31
|
+
### Schema
|
|
32
|
+
|
|
33
|
+
JSON-Schema-like type definitions. Supports: `string`, `number`, `integer`, `boolean`, `array`, `object`, `date`.
|
|
34
|
+
|
|
35
|
+
```js
|
|
36
|
+
const schema = {
|
|
37
|
+
type: 'object',
|
|
38
|
+
properties: {
|
|
39
|
+
name: { type: 'string' },
|
|
40
|
+
size: { type: 'string', enum: ['sm', 'md', 'lg'] },
|
|
41
|
+
count: { type: 'integer', min: 0, max: 100 },
|
|
42
|
+
active: { type: 'boolean' }
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
### Layout
|
|
48
|
+
|
|
49
|
+
Controls rendering order, grouping, labels, and component-specific props. Uses JSON Pointer scopes (`#/fieldName`).
|
|
50
|
+
|
|
51
|
+
```js
|
|
52
|
+
const layout = {
|
|
53
|
+
type: 'vertical',
|
|
54
|
+
elements: [
|
|
55
|
+
{ scope: '#/name', label: 'Full Name', props: { placeholder: 'Enter name' } },
|
|
56
|
+
{ scope: '#/size', label: 'Size', props: { options: ['sm', 'md', 'lg'] } },
|
|
57
|
+
{ scope: '#/count', label: 'Count' },
|
|
58
|
+
{ type: 'separator' },
|
|
59
|
+
{ scope: '#/active', label: 'Active' },
|
|
60
|
+
{ scope: '#/total', label: 'Total', readonly: true }
|
|
61
|
+
]
|
|
62
|
+
}
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
### Type Resolution
|
|
66
|
+
|
|
67
|
+
The FormBuilder determines input type from the schema:
|
|
68
|
+
|
|
69
|
+
| Schema type | Condition | Input type |
|
|
70
|
+
|---|---|---|
|
|
71
|
+
| `string` | has `enum` or `options` | `select` |
|
|
72
|
+
| `string` | default | `text` |
|
|
73
|
+
| `boolean` | — | `checkbox` |
|
|
74
|
+
| `number`/`integer` | has `min` and `max` | `range` |
|
|
75
|
+
| `number`/`integer` | default | `number` |
|
|
76
|
+
| any | `readonly: true` | `info` |
|
|
77
|
+
| — | no `scope` | `separator` |
|
|
78
|
+
|
|
79
|
+
### Layout Props
|
|
80
|
+
|
|
81
|
+
The `props` field in layout elements passes component-specific configuration:
|
|
82
|
+
|
|
83
|
+
```js
|
|
84
|
+
// Select with string options
|
|
85
|
+
{ scope: '#/size', props: { options: ['sm', 'md', 'lg'] } }
|
|
86
|
+
|
|
87
|
+
// Select with object options and field mapping
|
|
88
|
+
{ scope: '#/color', props: {
|
|
89
|
+
options: [{ name: 'Red', hex: '#f00' }, { name: 'Blue', hex: '#00f' }],
|
|
90
|
+
fields: { text: 'name', value: 'hex' }
|
|
91
|
+
}}
|
|
92
|
+
|
|
93
|
+
// Readonly info display
|
|
94
|
+
{ scope: '#/total', readonly: true }
|
|
95
|
+
|
|
96
|
+
// Separator (no scope)
|
|
97
|
+
{ type: 'separator' }
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
## Components
|
|
101
|
+
|
|
102
|
+
### FormRenderer
|
|
103
|
+
|
|
104
|
+
Renders a complete form from data + schema + layout:
|
|
105
|
+
|
|
106
|
+
```svelte
|
|
107
|
+
<script>
|
|
108
|
+
import { FormRenderer } from '@rokkit/forms'
|
|
109
|
+
|
|
110
|
+
let data = $state({ name: '', size: 'md', active: true })
|
|
111
|
+
</script>
|
|
112
|
+
|
|
113
|
+
<FormRenderer bind:data {schema} {layout} />
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
**Props:**
|
|
117
|
+
- `data` (bindable) — form data object
|
|
118
|
+
- `schema` — JSON schema (optional, auto-derived)
|
|
119
|
+
- `layout` — layout definition (optional, auto-derived)
|
|
120
|
+
- `onupdate` — callback when data changes
|
|
121
|
+
- `onvalidate` — callback for field validation
|
|
122
|
+
- `child` — snippet for custom field rendering (receives `element`)
|
|
123
|
+
|
|
124
|
+
**Custom field rendering:**
|
|
125
|
+
|
|
126
|
+
```svelte
|
|
127
|
+
<FormRenderer bind:data {schema} {layout}>
|
|
128
|
+
{#snippet child(element)}
|
|
129
|
+
<MyCustomInput value={element.value} {...element.props} />
|
|
130
|
+
{/snippet}
|
|
131
|
+
</FormRenderer>
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
Set `override: true` on layout elements to route them to the `child` snippet.
|
|
135
|
+
|
|
136
|
+
### InputField
|
|
137
|
+
|
|
138
|
+
Wraps an input with label, description, and validation message:
|
|
139
|
+
|
|
140
|
+
```svelte
|
|
141
|
+
<InputField name="email" type="email" value={email} label="Email" onchange={handleChange} />
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
### Input
|
|
145
|
+
|
|
146
|
+
Type dispatcher — renders the appropriate input component based on `type`:
|
|
147
|
+
|
|
148
|
+
- `text`, `email`, `password`, `url`, `tel` — text inputs
|
|
149
|
+
- `number` — numeric input
|
|
150
|
+
- `range` — slider
|
|
151
|
+
- `checkbox` — checkbox
|
|
152
|
+
- `select` — dropdown (uses `@rokkit/ui` Select)
|
|
153
|
+
- `date`, `time`, `datetime-local`, `month`, `week` — date/time inputs
|
|
154
|
+
- `color` — color picker
|
|
155
|
+
- `file` — file upload
|
|
156
|
+
- `textarea` — multiline text
|
|
157
|
+
- `radio` — radio group
|
|
158
|
+
|
|
159
|
+
### InfoField
|
|
160
|
+
|
|
161
|
+
Read-only display of a label and value:
|
|
162
|
+
|
|
163
|
+
```svelte
|
|
164
|
+
<InfoField label="Total" value={42} />
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
## Lib Utilities
|
|
168
|
+
|
|
169
|
+
```js
|
|
170
|
+
import { getSchemaWithLayout, findAttributeByPath } from '@rokkit/forms'
|
|
171
|
+
import { deriveSchemaFromValue } from '@rokkit/forms/lib'
|
|
172
|
+
import { deriveLayoutFromValue } from '@rokkit/forms/lib'
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
- `deriveSchemaFromValue(data)` — infer schema from a data object
|
|
176
|
+
- `deriveLayoutFromValue(data)` — generate default vertical layout from data
|
|
177
|
+
- `getSchemaWithLayout(schema, layout)` — merge schema attributes with layout elements
|
|
178
|
+
- `findAttributeByPath(scope, schema)` — find schema attribute by JSON Pointer path
|
|
179
|
+
|
|
180
|
+
## Theming
|
|
181
|
+
|
|
182
|
+
Form field styles are provided by `@rokkit/themes`. The components use data-attribute selectors:
|
|
183
|
+
|
|
184
|
+
| Selector | Purpose |
|
|
185
|
+
|---|---|
|
|
186
|
+
| `[data-form-root]` | Form container |
|
|
187
|
+
| `[data-form-field]` | Field wrapper |
|
|
188
|
+
| `[data-form-separator]` | Separator between fields |
|
|
189
|
+
| `[data-field-root]` | Input field root |
|
|
190
|
+
| `[data-field]` | Field inner wrapper |
|
|
191
|
+
| `[data-field-type="..."]` | Type-specific layout (checkbox, info, select) |
|
|
192
|
+
| `[data-field-info]` | Info/readonly value display |
|
|
193
|
+
| `[data-input-root]` | Input element wrapper |
|
|
194
|
+
| `[data-description]` | Help text |
|
|
195
|
+
| `[data-message]` | Validation message |
|
|
196
|
+
|
|
197
|
+
## Future Enhancements
|
|
198
|
+
|
|
199
|
+
### Toggle/Switch as Checkbox Alternative
|
|
200
|
+
|
|
201
|
+
Use `@rokkit/ui` Toggle or Switch components as alternatives to the native checkbox for boolean fields. The layout could specify the preferred component:
|
|
202
|
+
|
|
203
|
+
```js
|
|
204
|
+
{ scope: '#/active', label: 'Active', props: { component: 'switch' } }
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
### Toggle as Select Alternative
|
|
208
|
+
|
|
209
|
+
For fields with a small number of options, a Toggle (radio-style button group) can replace Select for better UX:
|
|
210
|
+
|
|
211
|
+
```js
|
|
212
|
+
{ scope: '#/size', label: 'Size', props: { component: 'toggle', options: ['sm', 'md', 'lg'] } }
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
**Constraints:**
|
|
216
|
+
- **Text options**: max 3 options (beyond 3, text labels overflow in compact layouts)
|
|
217
|
+
- **Icon-only options**: max 5 options (icons are more compact)
|
|
218
|
+
- When the option count exceeds these limits, fall back to Select automatically
|
|
219
|
+
|
|
220
|
+
### MultiSelect for Array Values
|
|
221
|
+
|
|
222
|
+
When the value is an array and `options` is present in the layout, render a MultiSelect component instead of a single-value Select:
|
|
223
|
+
|
|
224
|
+
```js
|
|
225
|
+
// Schema: tags is an array
|
|
226
|
+
{ tags: { type: 'array', items: { type: 'string' } } }
|
|
227
|
+
|
|
228
|
+
// Layout: options provided → MultiSelect
|
|
229
|
+
{ scope: '#/tags', label: 'Tags', props: { options: ['bug', 'feature', 'docs'] } }
|
|
230
|
+
```
|
|
231
|
+
|
|
232
|
+
### Connected/Dependent Props
|
|
233
|
+
|
|
234
|
+
Support cascading dependencies between fields — changing one field's value updates another field's options. For example, selecting a country updates the available cities:
|
|
235
|
+
|
|
236
|
+
```js
|
|
237
|
+
const lookups = {
|
|
238
|
+
city: {
|
|
239
|
+
dependsOn: 'country',
|
|
240
|
+
fetch: async (country) => getCitiesForCountry(country)
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
const builder = new FormBuilder(data, schema, layout, lookups)
|
|
245
|
+
await builder.initializeLookups()
|
|
246
|
+
```
|
|
247
|
+
|
|
248
|
+
The `FormBuilder` already has `lookupManager` infrastructure for this pattern. The remaining work is:
|
|
249
|
+
- Auto-wiring lookup state (options, loading) into element props
|
|
250
|
+
- UI indicators for loading state on dependent fields
|
|
251
|
+
- Debouncing rapid changes to parent fields
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export { default as DisplayValue } from "./DisplayValue.svelte";
|
|
2
|
+
export { default as DisplaySection } from "./DisplaySection.svelte";
|
|
3
|
+
export { default as DisplayTable } from "./DisplayTable.svelte";
|
|
4
|
+
export { default as DisplayCardGrid } from "./DisplayCardGrid.svelte";
|
|
5
|
+
export { default as DisplayList } from "./DisplayList.svelte";
|
package/dist/src/index.d.ts
CHANGED
|
@@ -1,5 +1,14 @@
|
|
|
1
1
|
export { FormBuilder } from "./lib/builder.svelte.js";
|
|
2
|
+
export { deriveSchemaFromValue } from "./lib/schema.js";
|
|
3
|
+
export { deriveLayoutFromValue } from "./lib/layout.js";
|
|
2
4
|
export { default as FormRenderer } from "./FormRenderer.svelte";
|
|
3
5
|
export { default as Input } from "./Input.svelte";
|
|
4
6
|
export { default as InputField } from "./InputField.svelte";
|
|
7
|
+
export { default as InfoField } from "./InfoField.svelte";
|
|
8
|
+
export { default as ValidationReport } from "./ValidationReport.svelte";
|
|
9
|
+
export * from "./display";
|
|
5
10
|
export * from "./input";
|
|
11
|
+
export { createLookup, createLookupManager, clearLookupCache } from "./lib/lookup.svelte.js";
|
|
12
|
+
export { validateField, validateAll, createMessage, patterns } from "./lib/validation.js";
|
|
13
|
+
export { defaultRenderers, resolveRenderer } from "./lib/renderers.js";
|
|
14
|
+
export { getSchemaWithLayout, findAttributeByPath } from "./lib/fields.js";
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
export { default as ArrayEditor } from "./ArrayEditor.svelte";
|
|
1
2
|
export { default as InputCheckbox } from "./InputCheckbox.svelte";
|
|
2
3
|
export { default as InputColor } from "./InputColor.svelte";
|
|
3
4
|
export { default as InputDate } from "./InputDate.svelte";
|
|
@@ -10,9 +11,11 @@ export { default as InputPassword } from "./InputPassword.svelte";
|
|
|
10
11
|
export { default as InputRadio } from "./InputRadio.svelte";
|
|
11
12
|
export { default as InputRange } from "./InputRange.svelte";
|
|
12
13
|
export { default as InputSelect } from "./InputSelect.svelte";
|
|
14
|
+
export { default as InputSwitch } from "./InputSwitch.svelte";
|
|
13
15
|
export { default as InputTel } from "./InputTel.svelte";
|
|
14
16
|
export { default as InputText } from "./InputText.svelte";
|
|
15
17
|
export { default as InputTextArea } from "./InputTextArea.svelte";
|
|
16
18
|
export { default as InputTime } from "./InputTime.svelte";
|
|
19
|
+
export { default as InputToggle } from "./InputToggle.svelte";
|
|
17
20
|
export { default as InputUrl } from "./InputUrl.svelte";
|
|
18
21
|
export { default as InputWeek } from "./InputWeek.svelte";
|
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
* @property {Object} [props.message] - Validation message object
|
|
15
15
|
* @property {string} [props.message.state] - Message state: 'error', 'warning', 'info', 'success'
|
|
16
16
|
* @property {string} [props.message.text] - Message text content
|
|
17
|
+
* @property {boolean} [props.dirty] - Whether field value differs from initial
|
|
17
18
|
*/
|
|
18
19
|
/**
|
|
19
20
|
* FormBuilder class for dynamically generating forms from data structures
|
|
@@ -24,11 +25,15 @@ export class FormBuilder {
|
|
|
24
25
|
* @param {Object} [data={}] - Initial data object
|
|
25
26
|
* @param {Object|null} [schema=null] - Optional schema override
|
|
26
27
|
* @param {Object|null} [layout=null] - Optional layout override
|
|
28
|
+
* @param {Object<string, import('./lookup.svelte.js').LookupConfig>} [lookups={}] - Lookup configurations
|
|
27
29
|
*/
|
|
28
|
-
constructor(data?: Object, schema?: Object | null, layout?: Object | null
|
|
30
|
+
constructor(data?: Object, schema?: Object | null, layout?: Object | null, lookups?: {
|
|
31
|
+
[x: string]: import("./lookup.svelte.js").LookupConfig;
|
|
32
|
+
});
|
|
29
33
|
/** @type {FormElement[]} */
|
|
30
34
|
elements: FormElement[];
|
|
31
|
-
|
|
35
|
+
/** Combined schema+layout (scoped elements only) */
|
|
36
|
+
get combined(): any;
|
|
32
37
|
/**
|
|
33
38
|
* Set the data
|
|
34
39
|
* @param {Object} value - New data object
|
|
@@ -69,12 +74,60 @@ export class FormBuilder {
|
|
|
69
74
|
* @returns {Object} Current validation object
|
|
70
75
|
*/
|
|
71
76
|
get validation(): Object;
|
|
77
|
+
/**
|
|
78
|
+
* Get the lookup manager
|
|
79
|
+
* @returns {ReturnType<typeof createLookupManager>|null}
|
|
80
|
+
*/
|
|
81
|
+
get lookupManager(): ReturnType<typeof createLookupManager> | null;
|
|
82
|
+
/**
|
|
83
|
+
* Configure lookups for the form
|
|
84
|
+
* @param {Object<string, import('./lookup.svelte.js').LookupConfig>} lookups - Lookup configurations
|
|
85
|
+
*/
|
|
86
|
+
setLookups(lookups: {
|
|
87
|
+
[x: string]: import("./lookup.svelte.js").LookupConfig;
|
|
88
|
+
}): void;
|
|
89
|
+
/**
|
|
90
|
+
* Get lookup state for a field
|
|
91
|
+
* @param {string} fieldPath - Field path
|
|
92
|
+
* @returns {{ options: any[], loading: boolean, error: string|null, fields: Object, disabled: boolean }|null}
|
|
93
|
+
*/
|
|
94
|
+
getLookupState(fieldPath: string): {
|
|
95
|
+
options: any[];
|
|
96
|
+
loading: boolean;
|
|
97
|
+
error: string | null;
|
|
98
|
+
fields: Object;
|
|
99
|
+
disabled: boolean;
|
|
100
|
+
} | null;
|
|
101
|
+
/**
|
|
102
|
+
* Check if a field is disabled due to unmet lookup dependencies
|
|
103
|
+
* @param {string} path - Field path
|
|
104
|
+
* @returns {boolean}
|
|
105
|
+
*/
|
|
106
|
+
isFieldDisabled(path: string): boolean;
|
|
107
|
+
/**
|
|
108
|
+
* Manually refresh a field's lookup with the current form data
|
|
109
|
+
* @param {string} path - Field path
|
|
110
|
+
* @returns {Promise<void>}
|
|
111
|
+
*/
|
|
112
|
+
refreshLookup(path: string): Promise<void>;
|
|
113
|
+
/**
|
|
114
|
+
* Check if a field has a lookup configured
|
|
115
|
+
* @param {string} fieldPath - Field path
|
|
116
|
+
* @returns {boolean}
|
|
117
|
+
*/
|
|
118
|
+
hasLookup(fieldPath: string): boolean;
|
|
119
|
+
/**
|
|
120
|
+
* Initialize all lookups
|
|
121
|
+
* @returns {Promise<void>}
|
|
122
|
+
*/
|
|
123
|
+
initializeLookups(): Promise<void>;
|
|
72
124
|
/**
|
|
73
125
|
* Update a specific field value
|
|
74
126
|
* @param {string} path - Field path (e.g., 'count', 'settings/distance')
|
|
75
127
|
* @param {any} value - New value
|
|
128
|
+
* @param {boolean} [triggerLookups=true] - Whether to trigger dependent lookups
|
|
76
129
|
*/
|
|
77
|
-
updateField(path: string, value: any): void;
|
|
130
|
+
updateField(path: string, value: any, triggerLookups?: boolean): void;
|
|
78
131
|
/**
|
|
79
132
|
* Get a field value by path
|
|
80
133
|
* @param {string} path - Field path
|
|
@@ -94,7 +147,62 @@ export class FormBuilder {
|
|
|
94
147
|
*/
|
|
95
148
|
clearValidation(): void;
|
|
96
149
|
/**
|
|
97
|
-
*
|
|
150
|
+
* Validate a single field by path
|
|
151
|
+
* @param {string} fieldPath - Field path (without '#/' prefix)
|
|
152
|
+
* @returns {import('./validation.js').ValidationMessage|null} Validation result
|
|
153
|
+
*/
|
|
154
|
+
validateField(fieldPath: string): import("./validation.js").ValidationMessage | null;
|
|
155
|
+
/**
|
|
156
|
+
* Validate all fields, populate validation state
|
|
157
|
+
* @returns {Object} Validation results keyed by field path
|
|
158
|
+
*/
|
|
159
|
+
validate(): Object;
|
|
160
|
+
/**
|
|
161
|
+
* Whether all fields pass validation (no error-state messages)
|
|
162
|
+
* @returns {boolean}
|
|
163
|
+
*/
|
|
164
|
+
get isValid(): boolean;
|
|
165
|
+
/**
|
|
166
|
+
* Array of current error messages with paths
|
|
167
|
+
* @returns {Array<{path: string, state: string, text: string}>}
|
|
168
|
+
*/
|
|
169
|
+
get errors(): Array<{
|
|
170
|
+
path: string;
|
|
171
|
+
state: string;
|
|
172
|
+
text: string;
|
|
173
|
+
}>;
|
|
174
|
+
/**
|
|
175
|
+
* Array of all validation messages with paths, ordered by severity
|
|
176
|
+
* @returns {Array<{path: string, state: string, text: string}>}
|
|
177
|
+
*/
|
|
178
|
+
get messages(): Array<{
|
|
179
|
+
path: string;
|
|
180
|
+
state: string;
|
|
181
|
+
text: string;
|
|
182
|
+
}>;
|
|
183
|
+
/**
|
|
184
|
+
* Whether any field has been modified from its initial value
|
|
185
|
+
* @returns {boolean}
|
|
186
|
+
*/
|
|
187
|
+
get isDirty(): boolean;
|
|
188
|
+
/**
|
|
189
|
+
* Set of field paths that differ from their initial values
|
|
190
|
+
* @returns {Set<string>}
|
|
191
|
+
*/
|
|
192
|
+
get dirtyFields(): Set<string>;
|
|
193
|
+
/**
|
|
194
|
+
* Check if a single field has been modified from its initial value
|
|
195
|
+
* @param {string} fieldPath - Field path (without '#/' prefix)
|
|
196
|
+
* @returns {boolean}
|
|
197
|
+
*/
|
|
198
|
+
isFieldDirty(fieldPath: string): boolean;
|
|
199
|
+
/**
|
|
200
|
+
* Update the initial data snapshot to the current data.
|
|
201
|
+
* Call after a successful save to clear dirty state.
|
|
202
|
+
*/
|
|
203
|
+
snapshot(): void;
|
|
204
|
+
/**
|
|
205
|
+
* Reset form data to initial snapshot and clear validation
|
|
98
206
|
*/
|
|
99
207
|
reset(): void;
|
|
100
208
|
#private;
|
|
@@ -136,5 +244,7 @@ export type FormElement = {
|
|
|
136
244
|
*/
|
|
137
245
|
text?: string | undefined;
|
|
138
246
|
} | undefined;
|
|
247
|
+
dirty?: boolean | undefined;
|
|
139
248
|
};
|
|
140
249
|
};
|
|
250
|
+
import { createLookupManager } from './lookup.svelte.js';
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Creates a lookup provider for a field
|
|
3
|
+
* @param {LookupConfig} config - Lookup configuration
|
|
4
|
+
* @returns {Object} Lookup provider with reactive state
|
|
5
|
+
*/
|
|
6
|
+
export function createLookup(config: LookupConfig): Object;
|
|
7
|
+
/**
|
|
8
|
+
* Creates a lookup manager for a form with multiple lookups
|
|
9
|
+
* @param {Object<string, LookupConfig>} lookupConfigs - Map of field paths to lookup configs
|
|
10
|
+
* @returns {Object} Lookup manager
|
|
11
|
+
*/
|
|
12
|
+
export function createLookupManager(lookupConfigs: {
|
|
13
|
+
[x: string]: LookupConfig;
|
|
14
|
+
}): Object;
|
|
15
|
+
/**
|
|
16
|
+
* Clears the entire lookup cache
|
|
17
|
+
*/
|
|
18
|
+
export function clearLookupCache(): void;
|
|
19
|
+
export type LookupConfig = {
|
|
20
|
+
/**
|
|
21
|
+
* - URL template with optional placeholders (e.g., '/api/cities?country={country}')
|
|
22
|
+
*/
|
|
23
|
+
url?: string | undefined;
|
|
24
|
+
/**
|
|
25
|
+
* - Async function replacing URL template
|
|
26
|
+
*/
|
|
27
|
+
fetch?: ((formData: any) => Promise<any[]>) | undefined;
|
|
28
|
+
/**
|
|
29
|
+
* - Pre-loaded array for filter pattern
|
|
30
|
+
*/
|
|
31
|
+
source?: any[] | undefined;
|
|
32
|
+
/**
|
|
33
|
+
* - Client-side filter applied to source
|
|
34
|
+
*/
|
|
35
|
+
filter?: ((items: any[], formData: any) => any[]) | undefined;
|
|
36
|
+
/**
|
|
37
|
+
* - Custom cache key for fetch hooks (no caching when absent)
|
|
38
|
+
*/
|
|
39
|
+
cacheKey?: ((formData: any) => string) | undefined;
|
|
40
|
+
/**
|
|
41
|
+
* - Field paths this lookup depends on
|
|
42
|
+
*/
|
|
43
|
+
dependsOn?: string[] | undefined;
|
|
44
|
+
/**
|
|
45
|
+
* - Field mapping for the response data
|
|
46
|
+
*/
|
|
47
|
+
fields?: Object | undefined;
|
|
48
|
+
/**
|
|
49
|
+
* - Cache duration in milliseconds (default: 5 minutes)
|
|
50
|
+
*/
|
|
51
|
+
cacheTime?: number | undefined;
|
|
52
|
+
/**
|
|
53
|
+
* - Transform response data to options array
|
|
54
|
+
*/
|
|
55
|
+
transform?: ((data: any) => any[]) | undefined;
|
|
56
|
+
};
|
|
57
|
+
export type LookupState = {
|
|
58
|
+
/**
|
|
59
|
+
* - Current options
|
|
60
|
+
*/
|
|
61
|
+
options: any[];
|
|
62
|
+
/**
|
|
63
|
+
* - Whether lookup is in progress
|
|
64
|
+
*/
|
|
65
|
+
loading: boolean;
|
|
66
|
+
/**
|
|
67
|
+
* - Error message if lookup failed
|
|
68
|
+
*/
|
|
69
|
+
error: string | null;
|
|
70
|
+
/**
|
|
71
|
+
* - Whether field is disabled (deps not met)
|
|
72
|
+
*/
|
|
73
|
+
disabled: boolean;
|
|
74
|
+
};
|
|
75
|
+
/**
|
|
76
|
+
* Cache entry structure
|
|
77
|
+
*/
|
|
78
|
+
export type CacheEntry = {
|
|
79
|
+
/**
|
|
80
|
+
* - Cached options
|
|
81
|
+
*/
|
|
82
|
+
data: any[];
|
|
83
|
+
/**
|
|
84
|
+
* - When the cache was created
|
|
85
|
+
*/
|
|
86
|
+
timestamp: number;
|
|
87
|
+
};
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Resolve a renderer component for a form element.
|
|
3
|
+
*
|
|
4
|
+
* Resolution order:
|
|
5
|
+
* 1. Explicit `renderer` name in element props → custom registry
|
|
6
|
+
* 2. Element type → registry lookup
|
|
7
|
+
* 3. Fallback → InputText
|
|
8
|
+
*
|
|
9
|
+
* @param {{ type: string, props?: { renderer?: string } }} element
|
|
10
|
+
* @param {Record<string, import('svelte').Component>} renderers - Merged registry
|
|
11
|
+
* @returns {import('svelte').Component}
|
|
12
|
+
*/
|
|
13
|
+
export function resolveRenderer(element: {
|
|
14
|
+
type: string;
|
|
15
|
+
props?: {
|
|
16
|
+
renderer?: string;
|
|
17
|
+
};
|
|
18
|
+
}, renderers: Record<string, import("svelte").Component>): import("svelte").Component;
|
|
19
|
+
/**
|
|
20
|
+
* Default renderer registry mapping type strings to components.
|
|
21
|
+
* @type {Record<string, import('svelte').Component>}
|
|
22
|
+
*/
|
|
23
|
+
export const defaultRenderers: Record<string, import("svelte").Component>;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rokkit/forms",
|
|
3
|
-
"version": "1.0.0-next.
|
|
3
|
+
"version": "1.0.0-next.127",
|
|
4
4
|
"module": "src/index.js",
|
|
5
5
|
"author": "Jerry Thomas <me@jerrythomas.name>",
|
|
6
6
|
"license": "MIT",
|
|
@@ -25,14 +25,16 @@
|
|
|
25
25
|
"types": "./dist/index.d.ts",
|
|
26
26
|
"import": "./src/index.js",
|
|
27
27
|
"svelte": "./src/index.js"
|
|
28
|
+
},
|
|
29
|
+
"./lib": {
|
|
30
|
+
"import": "./src/lib/index.js"
|
|
28
31
|
}
|
|
29
32
|
},
|
|
30
33
|
"dependencies": {
|
|
31
34
|
"@rokkit/core": "latest",
|
|
35
|
+
"@rokkit/data": "latest",
|
|
32
36
|
"@rokkit/states": "latest",
|
|
33
37
|
"@rokkit/ui": "latest",
|
|
34
|
-
"
|
|
35
|
-
"ramda": "^0.31.3",
|
|
36
|
-
"valibot": "^1.1.0"
|
|
38
|
+
"ramda": "^0.32.0"
|
|
37
39
|
}
|
|
38
40
|
}
|
package/src/FieldLayout.svelte
CHANGED
|
@@ -4,16 +4,9 @@
|
|
|
4
4
|
import InputField from './input/InputField.svelte'
|
|
5
5
|
import FieldLayout from './FieldLayout.svelte'
|
|
6
6
|
|
|
7
|
-
// const dispatch = createEventDispatcher()
|
|
8
7
|
const registry = getContext('registry')
|
|
9
8
|
|
|
10
|
-
|
|
11
|
-
export let schema = {}
|
|
12
|
-
export let path = []
|
|
13
|
-
|
|
14
|
-
function handle() {
|
|
15
|
-
dispatch('change', value)
|
|
16
|
-
}
|
|
9
|
+
let { value = $bindable({}), schema = {}, path = [], onchange } = $props()
|
|
17
10
|
|
|
18
11
|
let Wrapper = registry.wrappers[schema.wrapper] ?? registry.wrappers.default
|
|
19
12
|
let wrapperProps = omit(['wrapper', 'elements', 'key'], schema)
|
|
@@ -33,15 +26,15 @@
|
|
|
33
26
|
|
|
34
27
|
{#if nested}
|
|
35
28
|
{#if item.key}
|
|
36
|
-
<FieldLayout {...props} schema={item} bind:value={value[item.key]}
|
|
29
|
+
<FieldLayout {...props} schema={item} bind:value={value[item.key]} {onchange} />
|
|
37
30
|
{:else}
|
|
38
|
-
<FieldLayout {...props} schema={item} bind:value
|
|
31
|
+
<FieldLayout {...props} schema={item} bind:value {onchange} />
|
|
39
32
|
{/if}
|
|
40
33
|
{:else if Component}
|
|
41
34
|
<Component {...item.props} value={item.key ? value[item.key] : null} />
|
|
42
35
|
{:else}
|
|
43
36
|
{@const name = elementPath.join('.')}
|
|
44
|
-
<InputField {name} bind:value={value[item.key]} {...item.props}
|
|
37
|
+
<InputField {name} bind:value={value[item.key]} {...item.props} {onchange} />
|
|
45
38
|
{/if}
|
|
46
39
|
{/each}
|
|
47
40
|
</Wrapper>
|