dalila 1.5.13 → 1.6.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/README.md +47 -0
- package/dist/form/form-types.d.ts +181 -0
- package/dist/form/form-types.js +4 -0
- package/dist/form/form.d.ts +71 -0
- package/dist/form/form.js +1073 -0
- package/dist/form/index.d.ts +2 -0
- package/dist/form/index.js +2 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/runtime/bind.js +567 -9
- package/package.json +5 -1
package/README.md
CHANGED
|
@@ -77,6 +77,10 @@ bind(document.getElementById('app')!, ctx);
|
|
|
77
77
|
- [Query](./docs/core/query.md) — Cached queries
|
|
78
78
|
- [Mutations](./docs/core/mutation.md) — Write operations
|
|
79
79
|
|
|
80
|
+
### Forms
|
|
81
|
+
|
|
82
|
+
- [Forms](./docs/forms.md) — DOM-first form management with validation, field arrays, and accessibility
|
|
83
|
+
|
|
80
84
|
### Utilities
|
|
81
85
|
|
|
82
86
|
- [Scheduler](./docs/core/scheduler.md) — Batching and coordination
|
|
@@ -189,6 +193,49 @@ const router = createRouter({
|
|
|
189
193
|
router.start();
|
|
190
194
|
```
|
|
191
195
|
|
|
196
|
+
### Forms
|
|
197
|
+
|
|
198
|
+
```ts
|
|
199
|
+
import { createForm } from 'dalila';
|
|
200
|
+
|
|
201
|
+
const userForm = createForm({
|
|
202
|
+
defaultValues: { name: '', email: '' },
|
|
203
|
+
validate: (data) => {
|
|
204
|
+
const errors: Record<string, string> = {};
|
|
205
|
+
if (!data.name) errors.name = 'Name is required';
|
|
206
|
+
if (!data.email?.includes('@')) errors.email = 'Invalid email';
|
|
207
|
+
return errors;
|
|
208
|
+
}
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
async function handleSubmit(data, { signal }) {
|
|
212
|
+
await fetch('/api/users', {
|
|
213
|
+
method: 'POST',
|
|
214
|
+
body: JSON.stringify(data),
|
|
215
|
+
signal
|
|
216
|
+
});
|
|
217
|
+
}
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
```html
|
|
221
|
+
<form d-form="userForm" d-on-submit="handleSubmit">
|
|
222
|
+
<label>
|
|
223
|
+
Name
|
|
224
|
+
<input d-field="name" />
|
|
225
|
+
</label>
|
|
226
|
+
<span d-error="name"></span>
|
|
227
|
+
|
|
228
|
+
<label>
|
|
229
|
+
Email
|
|
230
|
+
<input d-field="email" type="email" />
|
|
231
|
+
</label>
|
|
232
|
+
<span d-error="email"></span>
|
|
233
|
+
|
|
234
|
+
<button type="submit">Save</button>
|
|
235
|
+
<span d-form-error="userForm"></span>
|
|
236
|
+
</form>
|
|
237
|
+
```
|
|
238
|
+
|
|
192
239
|
## Development
|
|
193
240
|
|
|
194
241
|
```bash
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Type utilities for path-based access
|
|
3
|
+
*/
|
|
4
|
+
export type FieldErrors = Record<string, string>;
|
|
5
|
+
export interface FormSubmitContext {
|
|
6
|
+
signal: AbortSignal;
|
|
7
|
+
}
|
|
8
|
+
export interface FormOptions<T> {
|
|
9
|
+
/**
|
|
10
|
+
* Default values for the form.
|
|
11
|
+
* Can be a static object, a function returning values, or a promise.
|
|
12
|
+
*/
|
|
13
|
+
defaultValues?: Partial<T> | (() => Partial<T>) | (() => Promise<Partial<T>>);
|
|
14
|
+
/**
|
|
15
|
+
* Custom parser for FormData → T.
|
|
16
|
+
* If not provided, uses the built-in parser with dot/bracket notation.
|
|
17
|
+
*/
|
|
18
|
+
parse?: (formEl: HTMLFormElement, fd: FormData) => T;
|
|
19
|
+
/**
|
|
20
|
+
* Client-side validation function.
|
|
21
|
+
* Returns { fieldErrors?, formError? } or just fieldErrors.
|
|
22
|
+
*/
|
|
23
|
+
validate?: (data: T) => FieldErrors | {
|
|
24
|
+
fieldErrors?: FieldErrors;
|
|
25
|
+
formError?: string;
|
|
26
|
+
} | void;
|
|
27
|
+
/**
|
|
28
|
+
* When to run validation:
|
|
29
|
+
* - "submit" (default): only on submit
|
|
30
|
+
* - "blur": on field blur after first submit
|
|
31
|
+
* - "change": on every change after first submit
|
|
32
|
+
*/
|
|
33
|
+
validateOn?: 'submit' | 'blur' | 'change';
|
|
34
|
+
/**
|
|
35
|
+
* Transform server errors into form errors.
|
|
36
|
+
* Useful for mapping backend error formats to field paths.
|
|
37
|
+
*/
|
|
38
|
+
transformServerErrors?: (error: unknown) => {
|
|
39
|
+
fieldErrors?: FieldErrors;
|
|
40
|
+
formError?: string;
|
|
41
|
+
} | void;
|
|
42
|
+
}
|
|
43
|
+
export interface Form<T> {
|
|
44
|
+
/**
|
|
45
|
+
* Creates a submit handler that:
|
|
46
|
+
* - prevents default
|
|
47
|
+
* - collects FormData
|
|
48
|
+
* - parses to T
|
|
49
|
+
* - validates (if configured)
|
|
50
|
+
* - calls handler with data and AbortSignal
|
|
51
|
+
* - cancels previous submit if re-submitted
|
|
52
|
+
*/
|
|
53
|
+
handleSubmit(handler: (data: T, ctx: FormSubmitContext) => Promise<unknown> | unknown): (ev: SubmitEvent) => void;
|
|
54
|
+
/**
|
|
55
|
+
* Reset form to initial/new defaults
|
|
56
|
+
*/
|
|
57
|
+
reset(nextDefaults?: Partial<T>): void;
|
|
58
|
+
/**
|
|
59
|
+
* Set error for a specific field
|
|
60
|
+
*/
|
|
61
|
+
setError(path: string, message: string): void;
|
|
62
|
+
/**
|
|
63
|
+
* Set form-level error
|
|
64
|
+
*/
|
|
65
|
+
setFormError(message: string): void;
|
|
66
|
+
/**
|
|
67
|
+
* Clear errors (all or by prefix)
|
|
68
|
+
*/
|
|
69
|
+
clearErrors(prefix?: string): void;
|
|
70
|
+
/**
|
|
71
|
+
* Get error message for a field
|
|
72
|
+
*/
|
|
73
|
+
error(path: string): string | null;
|
|
74
|
+
/**
|
|
75
|
+
* Get form-level error
|
|
76
|
+
*/
|
|
77
|
+
formError(): string | null;
|
|
78
|
+
/**
|
|
79
|
+
* Check if field has been touched
|
|
80
|
+
*/
|
|
81
|
+
touched(path: string): boolean;
|
|
82
|
+
/**
|
|
83
|
+
* Check if field is dirty (value differs from default)
|
|
84
|
+
*/
|
|
85
|
+
dirty(path: string): boolean;
|
|
86
|
+
/**
|
|
87
|
+
* Check if form is currently submitting
|
|
88
|
+
*/
|
|
89
|
+
submitting(): boolean;
|
|
90
|
+
/**
|
|
91
|
+
* Get submit count
|
|
92
|
+
*/
|
|
93
|
+
submitCount(): number;
|
|
94
|
+
/**
|
|
95
|
+
* Focus first error field (or specific field)
|
|
96
|
+
*/
|
|
97
|
+
focus(path?: string): void;
|
|
98
|
+
/**
|
|
99
|
+
* Internal: register a field element
|
|
100
|
+
* @internal
|
|
101
|
+
*/
|
|
102
|
+
_registerField(path: string, element: HTMLElement): () => void;
|
|
103
|
+
/**
|
|
104
|
+
* Internal: get form element
|
|
105
|
+
* @internal
|
|
106
|
+
*/
|
|
107
|
+
_getFormElement(): HTMLFormElement | null;
|
|
108
|
+
/**
|
|
109
|
+
* Internal: set form element
|
|
110
|
+
* @internal
|
|
111
|
+
*/
|
|
112
|
+
_setFormElement(form: HTMLFormElement): void;
|
|
113
|
+
/**
|
|
114
|
+
* Create or get a field array
|
|
115
|
+
*/
|
|
116
|
+
fieldArray<TItem = unknown>(path: string): FieldArray<TItem>;
|
|
117
|
+
}
|
|
118
|
+
export interface FieldArrayItem<T = unknown> {
|
|
119
|
+
key: string;
|
|
120
|
+
value?: T;
|
|
121
|
+
}
|
|
122
|
+
export interface FieldArray<TItem = unknown> {
|
|
123
|
+
/**
|
|
124
|
+
* Get array of items with stable keys
|
|
125
|
+
*/
|
|
126
|
+
fields(): FieldArrayItem<TItem>[];
|
|
127
|
+
/**
|
|
128
|
+
* Append item(s) to the end
|
|
129
|
+
*/
|
|
130
|
+
append(value: TItem | TItem[]): void;
|
|
131
|
+
/**
|
|
132
|
+
* Remove item by key
|
|
133
|
+
*/
|
|
134
|
+
remove(key: string): void;
|
|
135
|
+
/**
|
|
136
|
+
* Remove item by index
|
|
137
|
+
*/
|
|
138
|
+
removeAt(index: number): void;
|
|
139
|
+
/**
|
|
140
|
+
* Insert item at index
|
|
141
|
+
*/
|
|
142
|
+
insert(index: number, value: TItem): void;
|
|
143
|
+
/**
|
|
144
|
+
* Move item from one index to another
|
|
145
|
+
*/
|
|
146
|
+
move(fromIndex: number, toIndex: number): void;
|
|
147
|
+
/**
|
|
148
|
+
* Swap two items by index
|
|
149
|
+
*/
|
|
150
|
+
swap(indexA: number, indexB: number): void;
|
|
151
|
+
/**
|
|
152
|
+
* Replace entire array
|
|
153
|
+
*/
|
|
154
|
+
replace(values: TItem[]): void;
|
|
155
|
+
/**
|
|
156
|
+
* Update a specific item by key
|
|
157
|
+
*/
|
|
158
|
+
update(key: string, value: TItem): void;
|
|
159
|
+
/**
|
|
160
|
+
* Update a specific item by index
|
|
161
|
+
*/
|
|
162
|
+
updateAt(index: number, value: TItem): void;
|
|
163
|
+
/**
|
|
164
|
+
* Clear all items
|
|
165
|
+
*/
|
|
166
|
+
clear(): void;
|
|
167
|
+
/**
|
|
168
|
+
* Get current length
|
|
169
|
+
*/
|
|
170
|
+
length(): number;
|
|
171
|
+
/**
|
|
172
|
+
* Internal: translate index-based path to key-based path
|
|
173
|
+
* @internal
|
|
174
|
+
*/
|
|
175
|
+
_translatePath(path: string): string | null;
|
|
176
|
+
/**
|
|
177
|
+
* Internal: get current index for a key
|
|
178
|
+
* @internal
|
|
179
|
+
*/
|
|
180
|
+
_getIndex(key: string): number;
|
|
181
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Dalila Forms - DOM-first reactive form management
|
|
3
|
+
*
|
|
4
|
+
* Design principles:
|
|
5
|
+
* - Values live in the DOM (uncontrolled by default)
|
|
6
|
+
* - Meta-state in memory (errors, touched, dirty, submitting)
|
|
7
|
+
* - Declarative HTML via directives
|
|
8
|
+
* - Scope-safe with automatic cleanup
|
|
9
|
+
* - Race-safe submits with AbortController
|
|
10
|
+
* - Field arrays with stable keys
|
|
11
|
+
*/
|
|
12
|
+
import type { Form, FormOptions } from './form-types.js';
|
|
13
|
+
/**
|
|
14
|
+
* Symbol to mark handlers that have been wrapped by handleSubmit().
|
|
15
|
+
* Used by bindForm to avoid double-wrapping.
|
|
16
|
+
*/
|
|
17
|
+
export declare const WRAPPED_HANDLER: unique symbol;
|
|
18
|
+
/**
|
|
19
|
+
* Parse FormData into a nested object structure.
|
|
20
|
+
*
|
|
21
|
+
* Supports:
|
|
22
|
+
* - Simple fields: "email" → { email: "..." }
|
|
23
|
+
* - Nested objects: "user.name" → { user: { name: "..." } }
|
|
24
|
+
* - Arrays: "phones[0].number" → { phones: [{ number: "..." }] }
|
|
25
|
+
* - Checkboxes: single = boolean, multiple = array of values
|
|
26
|
+
* - Select multiple: array of selected values
|
|
27
|
+
* - Radio: single value
|
|
28
|
+
* - Files: File object
|
|
29
|
+
*
|
|
30
|
+
* ## Checkbox Parsing Contract
|
|
31
|
+
*
|
|
32
|
+
* HTML FormData omits unchecked checkboxes entirely. To resolve this ambiguity,
|
|
33
|
+
* parseFormData() inspects the DOM to distinguish between "field missing" vs "checkbox unchecked".
|
|
34
|
+
*
|
|
35
|
+
* ### Single Checkbox (one input with unique name)
|
|
36
|
+
* When there is exactly ONE checkbox with a given name:
|
|
37
|
+
* - Checked (with or without value) → `true`
|
|
38
|
+
* - Unchecked → `false`
|
|
39
|
+
* - Value attribute is ignored (always returns boolean)
|
|
40
|
+
*
|
|
41
|
+
* Example:
|
|
42
|
+
* ```html
|
|
43
|
+
* <input type="checkbox" name="agree" />
|
|
44
|
+
* ```
|
|
45
|
+
* Result: `{ agree: false }` (unchecked) or `{ agree: true }` (checked)
|
|
46
|
+
*
|
|
47
|
+
* ### Multiple Checkboxes (same name, multiple inputs)
|
|
48
|
+
* When there are MULTIPLE checkboxes with the same name:
|
|
49
|
+
* - Result is ALWAYS an array
|
|
50
|
+
* - Some checked → `["value1", "value2"]`
|
|
51
|
+
* - None checked → `[]`
|
|
52
|
+
* - One checked → `["value1"]` (still an array!)
|
|
53
|
+
*
|
|
54
|
+
* Example:
|
|
55
|
+
* ```html
|
|
56
|
+
* <input type="checkbox" name="colors" value="red" checked />
|
|
57
|
+
* <input type="checkbox" name="colors" value="blue" />
|
|
58
|
+
* <input type="checkbox" name="colors" value="green" checked />
|
|
59
|
+
* ```
|
|
60
|
+
* Result: `{ colors: ["red", "green"] }`
|
|
61
|
+
*
|
|
62
|
+
* ### Edge Cases
|
|
63
|
+
* - Radio buttons: Unchecked radio → field absent (standard HTML behavior)
|
|
64
|
+
* - Select multiple: Always returns array (like multiple checkboxes)
|
|
65
|
+
*
|
|
66
|
+
* @param form - The form element to parse (used for DOM inspection)
|
|
67
|
+
* @param fd - FormData instance from the form
|
|
68
|
+
* @returns Parsed form data with nested structure
|
|
69
|
+
*/
|
|
70
|
+
export declare function parseFormData<T = unknown>(form: HTMLFormElement, fd: FormData): T;
|
|
71
|
+
export declare function createForm<T = unknown>(options?: FormOptions<T>): Form<T>;
|