dalila 1.9.21 → 1.9.24
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 +10 -5
- package/dist/cli/index.js +27 -5
- package/dist/cli/security-smoke.d.ts +8 -0
- package/dist/cli/security-smoke.js +502 -0
- package/dist/components/ui/runtime.d.ts +3 -1
- package/dist/components/ui/runtime.js +2 -0
- package/dist/components/ui/ui-types.d.ts +8 -8
- package/dist/core/html.d.ts +10 -3
- package/dist/core/html.js +10 -3
- package/dist/core/index.d.ts +1 -0
- package/dist/core/index.js +3 -0
- package/dist/core/observability.d.ts +11 -0
- package/dist/core/observability.js +53 -0
- package/dist/core/persist.js +9 -1
- package/dist/core/signal.d.ts +34 -2
- package/dist/core/signal.js +102 -96
- package/dist/form/field-array.d.ts +12 -0
- package/dist/form/field-array.js +306 -0
- package/dist/form/form-dom.d.ts +6 -0
- package/dist/form/form-dom.js +52 -0
- package/dist/form/form-types.d.ts +24 -15
- package/dist/form/form.d.ts +7 -53
- package/dist/form/form.js +49 -932
- package/dist/form/index.d.ts +2 -2
- package/dist/form/index.js +1 -2
- package/dist/form/parse-form-data.d.ts +13 -0
- package/dist/form/parse-form-data.js +88 -0
- package/dist/form/path-utils.d.ts +25 -0
- package/dist/form/path-utils.js +127 -0
- package/dist/form/path-watchers.d.ts +23 -0
- package/dist/form/path-watchers.js +82 -0
- package/dist/form/validation-pipeline.d.ts +29 -0
- package/dist/form/validation-pipeline.js +223 -0
- package/dist/http/client.js +63 -16
- package/dist/router/index.d.ts +1 -1
- package/dist/router/index.js +1 -1
- package/dist/router/location-utils.d.ts +19 -0
- package/dist/router/location-utils.js +60 -0
- package/dist/router/lru-cache.d.ts +17 -0
- package/dist/router/lru-cache.js +63 -0
- package/dist/router/preload-metadata.d.ts +26 -0
- package/dist/router/preload-metadata.js +65 -0
- package/dist/router/router-lifecycle.d.ts +12 -0
- package/dist/router/router-lifecycle.js +31 -0
- package/dist/router/router-mount-lifecycle.d.ts +11 -0
- package/dist/router/router-mount-lifecycle.js +50 -0
- package/dist/router/router-prefetch.d.ts +22 -0
- package/dist/router/router-prefetch.js +86 -0
- package/dist/router/router-preload-cache.d.ts +25 -0
- package/dist/router/router-preload-cache.js +68 -0
- package/dist/router/router-render-utils.d.ts +14 -0
- package/dist/router/router-render-utils.js +11 -0
- package/dist/router/router-validation.d.ts +4 -0
- package/dist/router/router-validation.js +100 -0
- package/dist/router/router-view-composer.d.ts +16 -0
- package/dist/router/router-view-composer.js +41 -0
- package/dist/router/router.d.ts +12 -3
- package/dist/router/router.js +241 -621
- package/dist/runtime/array-directive-dom.d.ts +4 -0
- package/dist/runtime/array-directive-dom.js +30 -0
- package/dist/runtime/bind.d.ts +82 -1
- package/dist/runtime/bind.js +940 -935
- package/dist/runtime/boundary.d.ts +2 -2
- package/dist/runtime/boundary.js +19 -3
- package/dist/runtime/fromHtml.d.ts +10 -0
- package/dist/runtime/fromHtml.js +6 -4
- package/dist/runtime/html-sinks.d.ts +20 -0
- package/dist/runtime/html-sinks.js +165 -0
- package/dist/runtime/index.d.ts +2 -2
- package/dist/runtime/index.js +1 -1
- package/dist/runtime/internal/components/component-props.d.ts +15 -0
- package/dist/runtime/internal/components/component-props.js +69 -0
- package/dist/runtime/internal/components/component-slots.d.ts +10 -0
- package/dist/runtime/internal/components/component-slots.js +68 -0
- package/dist/runtime/internal/list/list-clone-factory.d.ts +17 -0
- package/dist/runtime/internal/list/list-clone-factory.js +35 -0
- package/dist/runtime/internal/list/list-clone-registry.d.ts +12 -0
- package/dist/runtime/internal/list/list-clone-registry.js +43 -0
- package/dist/runtime/internal/list/list-keying.d.ts +13 -0
- package/dist/runtime/internal/list/list-keying.js +65 -0
- package/dist/runtime/internal/list/list-metadata.d.ts +11 -0
- package/dist/runtime/internal/list/list-metadata.js +21 -0
- package/dist/runtime/internal/list/list-reconcile.d.ts +3 -0
- package/dist/runtime/internal/list/list-reconcile.js +25 -0
- package/dist/runtime/internal/list/list-scheduler.d.ts +16 -0
- package/dist/runtime/internal/list/list-scheduler.js +45 -0
- package/dist/runtime/internal/virtual/virtual-list-helpers.d.ts +48 -0
- package/dist/runtime/internal/virtual/virtual-list-helpers.js +291 -0
- package/package.json +13 -6
- package/scripts/dev-server.cjs +445 -123
|
@@ -0,0 +1,306 @@
|
|
|
1
|
+
import { signal } from '../core/signal.js';
|
|
2
|
+
export function createFieldArray(basePath, options) {
|
|
3
|
+
const keys = signal([]);
|
|
4
|
+
const values = signal(new Map());
|
|
5
|
+
let keyCounter = 0;
|
|
6
|
+
function generateKey() {
|
|
7
|
+
return `${basePath}_${keyCounter++}`;
|
|
8
|
+
}
|
|
9
|
+
function remapMetaState(oldIndices, newIndices) {
|
|
10
|
+
if (!options.errors && !options.touchedSet && !options.dirtySet)
|
|
11
|
+
return;
|
|
12
|
+
const indexMap = new Map();
|
|
13
|
+
for (let i = 0; i < oldIndices.length; i++) {
|
|
14
|
+
indexMap.set(oldIndices[i], newIndices[i]);
|
|
15
|
+
}
|
|
16
|
+
if (options.errors) {
|
|
17
|
+
options.errors.update((prev) => {
|
|
18
|
+
const next = {};
|
|
19
|
+
for (const [path, message] of Object.entries(prev)) {
|
|
20
|
+
const newPath = remapPath(path, indexMap);
|
|
21
|
+
next[newPath] = message;
|
|
22
|
+
}
|
|
23
|
+
return next;
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
if (options.touchedSet) {
|
|
27
|
+
options.touchedSet.update((prev) => {
|
|
28
|
+
const next = new Set();
|
|
29
|
+
for (const path of prev) {
|
|
30
|
+
next.add(remapPath(path, indexMap));
|
|
31
|
+
}
|
|
32
|
+
return next;
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
if (options.dirtySet) {
|
|
36
|
+
options.dirtySet.update((prev) => {
|
|
37
|
+
const next = new Set();
|
|
38
|
+
for (const path of prev) {
|
|
39
|
+
next.add(remapPath(path, indexMap));
|
|
40
|
+
}
|
|
41
|
+
return next;
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
function remapPath(path, indexMap) {
|
|
46
|
+
const regex = new RegExp(`^${escapeRegExp(basePath)}\\[(\\d+)\\](.*)$`);
|
|
47
|
+
const match = path.match(regex);
|
|
48
|
+
if (!match)
|
|
49
|
+
return path;
|
|
50
|
+
const oldIndex = parseInt(match[1], 10);
|
|
51
|
+
const rest = match[2];
|
|
52
|
+
const newIndex = indexMap.get(oldIndex);
|
|
53
|
+
if (newIndex === undefined)
|
|
54
|
+
return path;
|
|
55
|
+
return `${basePath}[${newIndex}]${rest}`;
|
|
56
|
+
}
|
|
57
|
+
function escapeRegExp(string) {
|
|
58
|
+
return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
59
|
+
}
|
|
60
|
+
function fields() {
|
|
61
|
+
const currentKeys = keys();
|
|
62
|
+
const currentValues = values();
|
|
63
|
+
return currentKeys.map((key) => ({
|
|
64
|
+
key,
|
|
65
|
+
value: currentValues.get(key),
|
|
66
|
+
}));
|
|
67
|
+
}
|
|
68
|
+
function length() {
|
|
69
|
+
return keys().length;
|
|
70
|
+
}
|
|
71
|
+
function _getIndex(key) {
|
|
72
|
+
return keys().indexOf(key);
|
|
73
|
+
}
|
|
74
|
+
function _translatePath(path) {
|
|
75
|
+
const match = path.match(/^([^\[]+)\[(\d+)\](.*)$/);
|
|
76
|
+
if (!match)
|
|
77
|
+
return null;
|
|
78
|
+
const [, arrayPath, indexStr, rest] = match;
|
|
79
|
+
if (arrayPath !== basePath)
|
|
80
|
+
return null;
|
|
81
|
+
const index = parseInt(indexStr, 10);
|
|
82
|
+
const currentKeys = keys();
|
|
83
|
+
const key = currentKeys[index];
|
|
84
|
+
if (!key)
|
|
85
|
+
return null;
|
|
86
|
+
return `${arrayPath}:${key}${rest}`;
|
|
87
|
+
}
|
|
88
|
+
function append(value) {
|
|
89
|
+
const items = Array.isArray(value) ? value : [value];
|
|
90
|
+
const newKeys = items.map(() => generateKey());
|
|
91
|
+
keys.update((prev) => [...prev, ...newKeys]);
|
|
92
|
+
values.update((prev) => {
|
|
93
|
+
const next = new Map(prev);
|
|
94
|
+
newKeys.forEach((key, i) => next.set(key, items[i]));
|
|
95
|
+
return next;
|
|
96
|
+
});
|
|
97
|
+
options.onMutate?.();
|
|
98
|
+
}
|
|
99
|
+
function remove(key) {
|
|
100
|
+
const removeIndex = _getIndex(key);
|
|
101
|
+
const currentLength = keys().length;
|
|
102
|
+
keys.update((prev) => prev.filter((k) => k !== key));
|
|
103
|
+
values.update((prev) => {
|
|
104
|
+
const next = new Map(prev);
|
|
105
|
+
next.delete(key);
|
|
106
|
+
return next;
|
|
107
|
+
});
|
|
108
|
+
if (removeIndex >= 0) {
|
|
109
|
+
const prefix = `${basePath}[${removeIndex}]`;
|
|
110
|
+
if (options.errors) {
|
|
111
|
+
options.errors.update((prev) => {
|
|
112
|
+
const next = {};
|
|
113
|
+
for (const [path, message] of Object.entries(prev)) {
|
|
114
|
+
if (!path.startsWith(prefix))
|
|
115
|
+
next[path] = message;
|
|
116
|
+
}
|
|
117
|
+
return next;
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
if (options.touchedSet) {
|
|
121
|
+
options.touchedSet.update((prev) => {
|
|
122
|
+
const next = new Set();
|
|
123
|
+
for (const path of prev) {
|
|
124
|
+
if (!path.startsWith(prefix))
|
|
125
|
+
next.add(path);
|
|
126
|
+
}
|
|
127
|
+
return next;
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
if (options.dirtySet) {
|
|
131
|
+
options.dirtySet.update((prev) => {
|
|
132
|
+
const next = new Set();
|
|
133
|
+
for (const path of prev) {
|
|
134
|
+
if (!path.startsWith(prefix))
|
|
135
|
+
next.add(path);
|
|
136
|
+
}
|
|
137
|
+
return next;
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
const oldIndices = [];
|
|
141
|
+
const newIndices = [];
|
|
142
|
+
for (let i = removeIndex + 1; i < currentLength; i++) {
|
|
143
|
+
oldIndices.push(i);
|
|
144
|
+
newIndices.push(i - 1);
|
|
145
|
+
}
|
|
146
|
+
if (oldIndices.length > 0)
|
|
147
|
+
remapMetaState(oldIndices, newIndices);
|
|
148
|
+
}
|
|
149
|
+
options.onMutate?.();
|
|
150
|
+
}
|
|
151
|
+
function removeAt(index) {
|
|
152
|
+
if (index < 0 || index >= keys().length)
|
|
153
|
+
return;
|
|
154
|
+
const key = keys()[index];
|
|
155
|
+
if (key)
|
|
156
|
+
remove(key);
|
|
157
|
+
}
|
|
158
|
+
function insert(index, value) {
|
|
159
|
+
const len = keys().length;
|
|
160
|
+
if (index < 0 || index > len)
|
|
161
|
+
return;
|
|
162
|
+
const key = generateKey();
|
|
163
|
+
const currentLength = len;
|
|
164
|
+
const oldIndices = [];
|
|
165
|
+
const newIndices = [];
|
|
166
|
+
for (let i = index; i < currentLength; i++) {
|
|
167
|
+
oldIndices.push(i);
|
|
168
|
+
newIndices.push(i + 1);
|
|
169
|
+
}
|
|
170
|
+
if (oldIndices.length > 0)
|
|
171
|
+
remapMetaState(oldIndices, newIndices);
|
|
172
|
+
keys.update((prev) => {
|
|
173
|
+
const next = [...prev];
|
|
174
|
+
next.splice(index, 0, key);
|
|
175
|
+
return next;
|
|
176
|
+
});
|
|
177
|
+
values.update((prev) => {
|
|
178
|
+
const next = new Map(prev);
|
|
179
|
+
next.set(key, value);
|
|
180
|
+
return next;
|
|
181
|
+
});
|
|
182
|
+
options.onMutate?.();
|
|
183
|
+
}
|
|
184
|
+
function move(fromIndex, toIndex) {
|
|
185
|
+
const len = keys().length;
|
|
186
|
+
if (fromIndex < 0 || fromIndex >= len || toIndex < 0 || toIndex >= len)
|
|
187
|
+
return;
|
|
188
|
+
if (fromIndex === toIndex)
|
|
189
|
+
return;
|
|
190
|
+
const oldIndices = [];
|
|
191
|
+
const newIndices = [];
|
|
192
|
+
if (fromIndex < toIndex) {
|
|
193
|
+
oldIndices.push(fromIndex);
|
|
194
|
+
newIndices.push(toIndex);
|
|
195
|
+
for (let i = fromIndex + 1; i <= toIndex; i++) {
|
|
196
|
+
oldIndices.push(i);
|
|
197
|
+
newIndices.push(i - 1);
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
else {
|
|
201
|
+
oldIndices.push(fromIndex);
|
|
202
|
+
newIndices.push(toIndex);
|
|
203
|
+
for (let i = toIndex; i < fromIndex; i++) {
|
|
204
|
+
oldIndices.push(i);
|
|
205
|
+
newIndices.push(i + 1);
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
remapMetaState(oldIndices, newIndices);
|
|
209
|
+
keys.update((prev) => {
|
|
210
|
+
const next = [...prev];
|
|
211
|
+
const [item] = next.splice(fromIndex, 1);
|
|
212
|
+
next.splice(toIndex, 0, item);
|
|
213
|
+
return next;
|
|
214
|
+
});
|
|
215
|
+
options.onMutate?.();
|
|
216
|
+
}
|
|
217
|
+
function swap(indexA, indexB) {
|
|
218
|
+
const len = keys().length;
|
|
219
|
+
if (indexA < 0 || indexA >= len || indexB < 0 || indexB >= len)
|
|
220
|
+
return;
|
|
221
|
+
if (indexA === indexB)
|
|
222
|
+
return;
|
|
223
|
+
remapMetaState([indexA, indexB], [indexB, indexA]);
|
|
224
|
+
keys.update((prev) => {
|
|
225
|
+
const next = [...prev];
|
|
226
|
+
[next[indexA], next[indexB]] = [next[indexB], next[indexA]];
|
|
227
|
+
return next;
|
|
228
|
+
});
|
|
229
|
+
options.onMutate?.();
|
|
230
|
+
}
|
|
231
|
+
function replace(newValues) {
|
|
232
|
+
const newKeys = newValues.map(() => generateKey());
|
|
233
|
+
if (options.errors) {
|
|
234
|
+
options.errors.update((prev) => {
|
|
235
|
+
const next = {};
|
|
236
|
+
for (const [path, message] of Object.entries(prev)) {
|
|
237
|
+
if (!path.startsWith(`${basePath}[`))
|
|
238
|
+
next[path] = message;
|
|
239
|
+
}
|
|
240
|
+
return next;
|
|
241
|
+
});
|
|
242
|
+
}
|
|
243
|
+
if (options.touchedSet) {
|
|
244
|
+
options.touchedSet.update((prev) => {
|
|
245
|
+
const next = new Set();
|
|
246
|
+
for (const path of prev) {
|
|
247
|
+
if (!path.startsWith(`${basePath}[`))
|
|
248
|
+
next.add(path);
|
|
249
|
+
}
|
|
250
|
+
return next;
|
|
251
|
+
});
|
|
252
|
+
}
|
|
253
|
+
if (options.dirtySet) {
|
|
254
|
+
options.dirtySet.update((prev) => {
|
|
255
|
+
const next = new Set();
|
|
256
|
+
for (const path of prev) {
|
|
257
|
+
if (!path.startsWith(`${basePath}[`))
|
|
258
|
+
next.add(path);
|
|
259
|
+
}
|
|
260
|
+
return next;
|
|
261
|
+
});
|
|
262
|
+
}
|
|
263
|
+
keys.set(newKeys);
|
|
264
|
+
values.set(new Map(newKeys.map((key, i) => [key, newValues[i]])));
|
|
265
|
+
options.onMutate?.();
|
|
266
|
+
}
|
|
267
|
+
function update(key, value) {
|
|
268
|
+
values.update((prev) => {
|
|
269
|
+
const next = new Map(prev);
|
|
270
|
+
next.set(key, value);
|
|
271
|
+
return next;
|
|
272
|
+
});
|
|
273
|
+
options.onMutate?.();
|
|
274
|
+
}
|
|
275
|
+
function updateAt(index, value) {
|
|
276
|
+
if (index < 0 || index >= keys().length)
|
|
277
|
+
return;
|
|
278
|
+
const key = keys()[index];
|
|
279
|
+
if (key)
|
|
280
|
+
update(key, value);
|
|
281
|
+
}
|
|
282
|
+
function clear() {
|
|
283
|
+
replace([]);
|
|
284
|
+
}
|
|
285
|
+
if (options.scope) {
|
|
286
|
+
options.scope.onCleanup(() => {
|
|
287
|
+
clear();
|
|
288
|
+
});
|
|
289
|
+
}
|
|
290
|
+
return {
|
|
291
|
+
fields,
|
|
292
|
+
append,
|
|
293
|
+
remove,
|
|
294
|
+
removeAt,
|
|
295
|
+
insert,
|
|
296
|
+
move,
|
|
297
|
+
swap,
|
|
298
|
+
replace,
|
|
299
|
+
update,
|
|
300
|
+
updateAt,
|
|
301
|
+
clear,
|
|
302
|
+
length,
|
|
303
|
+
_getIndex,
|
|
304
|
+
_translatePath,
|
|
305
|
+
};
|
|
306
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export declare function findFormFieldElement(formElement: HTMLFormElement | null, path: string): HTMLElement | null;
|
|
2
|
+
/**
|
|
3
|
+
* Reset all `[d-field]` DOM controls to the provided defaults.
|
|
4
|
+
* Keeps field arrays and meta-state handling to the caller.
|
|
5
|
+
*/
|
|
6
|
+
export declare function resetFormDomFields<T>(formElement: HTMLFormElement | null, defaultValues: Partial<T>): void;
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { cssEscape, getNestedValue } from './path-utils.js';
|
|
2
|
+
export function findFormFieldElement(formElement, path) {
|
|
3
|
+
if (!formElement)
|
|
4
|
+
return null;
|
|
5
|
+
const escapedPath = cssEscape(path);
|
|
6
|
+
return formElement.querySelector(`[d-field][data-field-path="${escapedPath}"], [d-field][name="${escapedPath}"]`);
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* Reset all `[d-field]` DOM controls to the provided defaults.
|
|
10
|
+
* Keeps field arrays and meta-state handling to the caller.
|
|
11
|
+
*/
|
|
12
|
+
export function resetFormDomFields(formElement, defaultValues) {
|
|
13
|
+
if (!formElement)
|
|
14
|
+
return;
|
|
15
|
+
formElement.reset();
|
|
16
|
+
const allFields = formElement.querySelectorAll('[d-field]');
|
|
17
|
+
for (const el of Array.from(allFields)) {
|
|
18
|
+
const fieldPath = el.getAttribute('data-field-path') || el.getAttribute('name');
|
|
19
|
+
if (!fieldPath)
|
|
20
|
+
continue;
|
|
21
|
+
const defaultValue = getNestedValue(defaultValues, fieldPath);
|
|
22
|
+
const input = el;
|
|
23
|
+
if (defaultValue === undefined)
|
|
24
|
+
continue;
|
|
25
|
+
if (input.type === 'checkbox') {
|
|
26
|
+
if (Array.isArray(defaultValue)) {
|
|
27
|
+
input.checked = defaultValue.includes(input.value);
|
|
28
|
+
}
|
|
29
|
+
else {
|
|
30
|
+
input.checked = !!defaultValue;
|
|
31
|
+
}
|
|
32
|
+
continue;
|
|
33
|
+
}
|
|
34
|
+
if (input.type === 'radio') {
|
|
35
|
+
input.checked = input.value === String(defaultValue);
|
|
36
|
+
continue;
|
|
37
|
+
}
|
|
38
|
+
if (input.tagName === 'SELECT') {
|
|
39
|
+
const select = el;
|
|
40
|
+
if (select.multiple && Array.isArray(defaultValue)) {
|
|
41
|
+
for (const option of Array.from(select.options)) {
|
|
42
|
+
option.selected = defaultValue.includes(option.value);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
else {
|
|
46
|
+
select.value = String(defaultValue);
|
|
47
|
+
}
|
|
48
|
+
continue;
|
|
49
|
+
}
|
|
50
|
+
input.value = String(defaultValue);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
@@ -127,21 +127,6 @@ export interface Form<T> {
|
|
|
127
127
|
* Focus first error field (or specific field)
|
|
128
128
|
*/
|
|
129
129
|
focus(path?: string): void;
|
|
130
|
-
/**
|
|
131
|
-
* Internal: register a field element
|
|
132
|
-
* @internal
|
|
133
|
-
*/
|
|
134
|
-
_registerField(path: string, element: HTMLElement): () => void;
|
|
135
|
-
/**
|
|
136
|
-
* Internal: get form element
|
|
137
|
-
* @internal
|
|
138
|
-
*/
|
|
139
|
-
_getFormElement(): HTMLFormElement | null;
|
|
140
|
-
/**
|
|
141
|
-
* Internal: set form element
|
|
142
|
-
* @internal
|
|
143
|
-
*/
|
|
144
|
-
_setFormElement(form: HTMLFormElement): void;
|
|
145
130
|
/**
|
|
146
131
|
* Create or get a field array
|
|
147
132
|
*/
|
|
@@ -151,11 +136,35 @@ export interface Form<T> {
|
|
|
151
136
|
* Returns an idempotent unsubscribe function.
|
|
152
137
|
*/
|
|
153
138
|
watch(path: string, fn: (next: unknown, prev: unknown) => void): () => void;
|
|
139
|
+
/**
|
|
140
|
+
* Get helpers scoped to a specific field path.
|
|
141
|
+
* Useful for reducing repetitive `form.error("x")` / `form.touched("x")` calls.
|
|
142
|
+
*/
|
|
143
|
+
field(path: string): FormFieldRef;
|
|
144
|
+
}
|
|
145
|
+
/**
|
|
146
|
+
* Runtime-only hooks used by `dalila/runtime` form directives.
|
|
147
|
+
* Kept separate from `Form<T>` so app IntelliSense shows only public APIs.
|
|
148
|
+
*/
|
|
149
|
+
export interface FormInternals {
|
|
150
|
+
_registerField(path: string, element: HTMLElement): () => void;
|
|
151
|
+
_getFormElement(): HTMLFormElement | null;
|
|
152
|
+
_setFormElement(form: HTMLFormElement): void;
|
|
154
153
|
}
|
|
154
|
+
/** Internal runtime shape (public form API + runtime hooks). */
|
|
155
|
+
export type InternalForm<T> = Form<T> & FormInternals;
|
|
155
156
|
export interface FieldArrayItem<T = unknown> {
|
|
156
157
|
key: string;
|
|
157
158
|
value?: T;
|
|
158
159
|
}
|
|
160
|
+
export interface FormFieldRef {
|
|
161
|
+
path: string;
|
|
162
|
+
error(): string | null;
|
|
163
|
+
touched(): boolean;
|
|
164
|
+
dirty(): boolean;
|
|
165
|
+
focus(): void;
|
|
166
|
+
watch(fn: (next: unknown, prev: unknown) => void): () => void;
|
|
167
|
+
}
|
|
159
168
|
export interface FieldArray<TItem = unknown> {
|
|
160
169
|
/**
|
|
161
170
|
* Get array of items with stable keys
|
package/dist/form/form.d.ts
CHANGED
|
@@ -9,63 +9,17 @@
|
|
|
9
9
|
* - Race-safe submits with AbortController
|
|
10
10
|
* - Field arrays with stable keys
|
|
11
11
|
*/
|
|
12
|
-
import
|
|
12
|
+
import { parseFormData } from './parse-form-data.js';
|
|
13
|
+
import type { Form, FormOptions, FormSchemaAdapter } from './form-types.js';
|
|
13
14
|
/**
|
|
14
15
|
* Symbol to mark handlers that have been wrapped by handleSubmit().
|
|
15
16
|
* Used by bindForm to avoid double-wrapping.
|
|
16
17
|
*/
|
|
17
18
|
export declare const WRAPPED_HANDLER: unique symbol;
|
|
19
|
+
export { parseFormData };
|
|
20
|
+
export declare function createForm<T = unknown>(options?: FormOptions<T>): Form<T>;
|
|
18
21
|
/**
|
|
19
|
-
*
|
|
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
|
|
22
|
+
* Convenience factory for the common schema-first case.
|
|
23
|
+
* Equivalent to `createForm({ ...options, schema })`.
|
|
69
24
|
*/
|
|
70
|
-
export declare function
|
|
71
|
-
export declare function createForm<T = unknown>(options?: FormOptions<T>): Form<T>;
|
|
25
|
+
export declare function createFormFromSchema<T = unknown>(schema: FormSchemaAdapter<T>, options?: Omit<FormOptions<T>, 'schema'>): Form<T>;
|