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.
Files changed (90) hide show
  1. package/README.md +10 -5
  2. package/dist/cli/index.js +27 -5
  3. package/dist/cli/security-smoke.d.ts +8 -0
  4. package/dist/cli/security-smoke.js +502 -0
  5. package/dist/components/ui/runtime.d.ts +3 -1
  6. package/dist/components/ui/runtime.js +2 -0
  7. package/dist/components/ui/ui-types.d.ts +8 -8
  8. package/dist/core/html.d.ts +10 -3
  9. package/dist/core/html.js +10 -3
  10. package/dist/core/index.d.ts +1 -0
  11. package/dist/core/index.js +3 -0
  12. package/dist/core/observability.d.ts +11 -0
  13. package/dist/core/observability.js +53 -0
  14. package/dist/core/persist.js +9 -1
  15. package/dist/core/signal.d.ts +34 -2
  16. package/dist/core/signal.js +102 -96
  17. package/dist/form/field-array.d.ts +12 -0
  18. package/dist/form/field-array.js +306 -0
  19. package/dist/form/form-dom.d.ts +6 -0
  20. package/dist/form/form-dom.js +52 -0
  21. package/dist/form/form-types.d.ts +24 -15
  22. package/dist/form/form.d.ts +7 -53
  23. package/dist/form/form.js +49 -932
  24. package/dist/form/index.d.ts +2 -2
  25. package/dist/form/index.js +1 -2
  26. package/dist/form/parse-form-data.d.ts +13 -0
  27. package/dist/form/parse-form-data.js +88 -0
  28. package/dist/form/path-utils.d.ts +25 -0
  29. package/dist/form/path-utils.js +127 -0
  30. package/dist/form/path-watchers.d.ts +23 -0
  31. package/dist/form/path-watchers.js +82 -0
  32. package/dist/form/validation-pipeline.d.ts +29 -0
  33. package/dist/form/validation-pipeline.js +223 -0
  34. package/dist/http/client.js +63 -16
  35. package/dist/router/index.d.ts +1 -1
  36. package/dist/router/index.js +1 -1
  37. package/dist/router/location-utils.d.ts +19 -0
  38. package/dist/router/location-utils.js +60 -0
  39. package/dist/router/lru-cache.d.ts +17 -0
  40. package/dist/router/lru-cache.js +63 -0
  41. package/dist/router/preload-metadata.d.ts +26 -0
  42. package/dist/router/preload-metadata.js +65 -0
  43. package/dist/router/router-lifecycle.d.ts +12 -0
  44. package/dist/router/router-lifecycle.js +31 -0
  45. package/dist/router/router-mount-lifecycle.d.ts +11 -0
  46. package/dist/router/router-mount-lifecycle.js +50 -0
  47. package/dist/router/router-prefetch.d.ts +22 -0
  48. package/dist/router/router-prefetch.js +86 -0
  49. package/dist/router/router-preload-cache.d.ts +25 -0
  50. package/dist/router/router-preload-cache.js +68 -0
  51. package/dist/router/router-render-utils.d.ts +14 -0
  52. package/dist/router/router-render-utils.js +11 -0
  53. package/dist/router/router-validation.d.ts +4 -0
  54. package/dist/router/router-validation.js +100 -0
  55. package/dist/router/router-view-composer.d.ts +16 -0
  56. package/dist/router/router-view-composer.js +41 -0
  57. package/dist/router/router.d.ts +12 -3
  58. package/dist/router/router.js +241 -621
  59. package/dist/runtime/array-directive-dom.d.ts +4 -0
  60. package/dist/runtime/array-directive-dom.js +30 -0
  61. package/dist/runtime/bind.d.ts +82 -1
  62. package/dist/runtime/bind.js +940 -935
  63. package/dist/runtime/boundary.d.ts +2 -2
  64. package/dist/runtime/boundary.js +19 -3
  65. package/dist/runtime/fromHtml.d.ts +10 -0
  66. package/dist/runtime/fromHtml.js +6 -4
  67. package/dist/runtime/html-sinks.d.ts +20 -0
  68. package/dist/runtime/html-sinks.js +165 -0
  69. package/dist/runtime/index.d.ts +2 -2
  70. package/dist/runtime/index.js +1 -1
  71. package/dist/runtime/internal/components/component-props.d.ts +15 -0
  72. package/dist/runtime/internal/components/component-props.js +69 -0
  73. package/dist/runtime/internal/components/component-slots.d.ts +10 -0
  74. package/dist/runtime/internal/components/component-slots.js +68 -0
  75. package/dist/runtime/internal/list/list-clone-factory.d.ts +17 -0
  76. package/dist/runtime/internal/list/list-clone-factory.js +35 -0
  77. package/dist/runtime/internal/list/list-clone-registry.d.ts +12 -0
  78. package/dist/runtime/internal/list/list-clone-registry.js +43 -0
  79. package/dist/runtime/internal/list/list-keying.d.ts +13 -0
  80. package/dist/runtime/internal/list/list-keying.js +65 -0
  81. package/dist/runtime/internal/list/list-metadata.d.ts +11 -0
  82. package/dist/runtime/internal/list/list-metadata.js +21 -0
  83. package/dist/runtime/internal/list/list-reconcile.d.ts +3 -0
  84. package/dist/runtime/internal/list/list-reconcile.js +25 -0
  85. package/dist/runtime/internal/list/list-scheduler.d.ts +16 -0
  86. package/dist/runtime/internal/list/list-scheduler.js +45 -0
  87. package/dist/runtime/internal/virtual/virtual-list-helpers.d.ts +48 -0
  88. package/dist/runtime/internal/virtual/virtual-list-helpers.js +291 -0
  89. package/package.json +13 -6
  90. 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
@@ -9,63 +9,17 @@
9
9
  * - Race-safe submits with AbortController
10
10
  * - Field arrays with stable keys
11
11
  */
12
- import type { Form, FormOptions } from './form-types.js';
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
- * 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
22
+ * Convenience factory for the common schema-first case.
23
+ * Equivalent to `createForm({ ...options, schema })`.
69
24
  */
70
- export declare function parseFormData<T = unknown>(form: HTMLFormElement, fd: FormData): T;
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>;