elements-kit 0.18.2 → 0.19.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.
@@ -0,0 +1,408 @@
1
+ import { f as it, l as describe, o as afterEach } from "../benchmark.CX_oY03V-CSLAXL5f.mjs";
2
+ import { t as globalExpect } from "../test.BmQO5GaM-Ddz9cDxc.mjs";
3
+ import { FormObject, defaultTransforms, dropUnchecked, skipButtons, skipEmpty, uncheckedAs } from "./form-object.mjs";
4
+ //#region src/utilities/form-object.test.ts
5
+ afterEach(() => {
6
+ document.body.innerHTML = "";
7
+ });
8
+ /** Build a form from an HTML string and attach it to the document. */
9
+ function makeForm(html) {
10
+ const form = document.createElement("form");
11
+ form.innerHTML = html;
12
+ document.body.appendChild(form);
13
+ return form;
14
+ }
15
+ describe("FormObject.toObject", () => {
16
+ it("reads flat fields into a flat object", () => {
17
+ globalExpect(new FormObject(makeForm(`
18
+ <input name="email" value="a@b.com" />
19
+ <input name="age" value="30" />
20
+ `)).toObject()).toEqual({
21
+ email: "a@b.com",
22
+ age: "30"
23
+ });
24
+ });
25
+ it("nests dot-notation names into nested objects", () => {
26
+ globalExpect(new FormObject(makeForm(`
27
+ <input name="user.name" value="Wael" />
28
+ <input name="user.address.city" value="Tunis" />
29
+ `)).toObject()).toEqual({ user: {
30
+ name: "Wael",
31
+ address: { city: "Tunis" }
32
+ } });
33
+ });
34
+ it("builds arrays from numeric segments", () => {
35
+ globalExpect(new FormObject(makeForm(`
36
+ <input name="tags.0" value="a" />
37
+ <input name="tags.1" value="b" />
38
+ `)).toObject()).toEqual({ tags: ["a", "b"] });
39
+ });
40
+ it("builds arrays of objects from mixed numeric + key segments", () => {
41
+ globalExpect(new FormObject(makeForm(`
42
+ <input name="items.0.id" value="1" />
43
+ <input name="items.1.id" value="2" />
44
+ `)).toObject()).toEqual({ items: [{ id: "1" }, { id: "2" }] });
45
+ });
46
+ it("reads textarea values", () => {
47
+ globalExpect(new FormObject(makeForm(`<textarea name="bio">hello</textarea>`)).toObject()).toEqual({ bio: "hello" });
48
+ });
49
+ it("uses a checked checkbox's value, defaulting to 'on'", () => {
50
+ globalExpect(new FormObject(makeForm(`<input type="checkbox" name="agree" checked />`)).toObject()).toEqual({ agree: "on" });
51
+ });
52
+ it("omits an unchecked checkbox by default", () => {
53
+ globalExpect(new FormObject(makeForm(`<input type="checkbox" name="agree" />`)).toObject()).toEqual({});
54
+ });
55
+ it("includes an unchecked checkbox as false via uncheckedAs", () => {
56
+ globalExpect(new FormObject(makeForm(`<input type="checkbox" name="agree" />`), { transforms: [skipButtons, uncheckedAs(false)] }).toObject()).toEqual({ agree: false });
57
+ });
58
+ it("collects a `[]` checkbox group into an array of checked values", () => {
59
+ globalExpect(new FormObject(makeForm(`
60
+ <input type="checkbox" name="colors[]" value="red" checked />
61
+ <input type="checkbox" name="colors[]" value="green" />
62
+ <input type="checkbox" name="colors[]" value="blue" checked />
63
+ `)).toObject()).toEqual({ colors: ["red", "blue"] });
64
+ });
65
+ it("reads the selected radio value", () => {
66
+ globalExpect(new FormObject(makeForm(`
67
+ <input type="radio" name="plan" value="free" />
68
+ <input type="radio" name="plan" value="pro" checked />
69
+ `)).toObject()).toEqual({ plan: "pro" });
70
+ });
71
+ it("reads a single select value", () => {
72
+ globalExpect(new FormObject(makeForm(`
73
+ <select name="country">
74
+ <option value="tn">Tunisia</option>
75
+ <option value="fr" selected>France</option>
76
+ </select>
77
+ `)).toObject()).toEqual({ country: "fr" });
78
+ });
79
+ it("reads a multiple select as an array", () => {
80
+ globalExpect(new FormObject(makeForm(`
81
+ <select name="langs" multiple>
82
+ <option value="js" selected>JS</option>
83
+ <option value="ts" selected>TS</option>
84
+ <option value="go">Go</option>
85
+ </select>
86
+ `)).toObject()).toEqual({ langs: ["js", "ts"] });
87
+ });
88
+ it("skips disabled controls by default", () => {
89
+ globalExpect(new FormObject(makeForm(`
90
+ <input name="a" value="1" />
91
+ <input name="b" value="2" disabled />
92
+ `)).toObject()).toEqual({ a: "1" });
93
+ });
94
+ it("includes disabled controls when skipDisabled is omitted", () => {
95
+ globalExpect(new FormObject(makeForm(`
96
+ <input name="a" value="1" />
97
+ <input name="b" value="2" disabled />
98
+ `), { transforms: [skipButtons, dropUnchecked] }).toObject()).toEqual({
99
+ a: "1",
100
+ b: "2"
101
+ });
102
+ });
103
+ it("skips unnamed controls", () => {
104
+ globalExpect(new FormObject(makeForm(`
105
+ <input value="ignored" />
106
+ <input name="kept" value="ok" />
107
+ `)).toObject()).toEqual({ kept: "ok" });
108
+ });
109
+ it("yields File objects for file inputs", () => {
110
+ const form = makeForm(`<input type="file" name="doc" />`);
111
+ const input = form.elements.namedItem("doc");
112
+ const file = new File(["data"], "report.txt", { type: "text/plain" });
113
+ const dt = new DataTransfer();
114
+ dt.items.add(file);
115
+ input.files = dt.files;
116
+ const result = new FormObject(form).toObject();
117
+ globalExpect(result.doc).toBeInstanceOf(File);
118
+ globalExpect(result.doc.name).toBe("report.txt");
119
+ });
120
+ });
121
+ describe("FormObject same-name controls (bare name → last value wins)", () => {
122
+ it("keeps the last value when repeated text inputs share a bare name", () => {
123
+ globalExpect(new FormObject(makeForm(`
124
+ <input name="x" value="a" />
125
+ <input name="x" value="b" />
126
+ `)).toObject()).toEqual({ x: "b" });
127
+ });
128
+ it("keeps the last value across mixed control kinds with a bare name", () => {
129
+ globalExpect(new FormObject(makeForm(`
130
+ <input name="y" value="a" />
131
+ <select name="y"><option value="b" selected>b</option></select>
132
+ `)).toObject()).toEqual({ y: "b" });
133
+ });
134
+ it("keeps the last checked value for a bare checkbox group", () => {
135
+ globalExpect(new FormObject(makeForm(`
136
+ <input type="checkbox" name="c" value="r" checked />
137
+ <input type="checkbox" name="c" value="g" checked />
138
+ `)).toObject()).toEqual({ c: "g" });
139
+ });
140
+ it("lets a checked checkbox override a preceding hidden fallback (idiom)", () => {
141
+ globalExpect(new FormObject(makeForm(`
142
+ <input type="hidden" name="agree" value="0" />
143
+ <input type="checkbox" name="agree" value="1" checked />
144
+ `)).toObject()).toEqual({ agree: "1" });
145
+ });
146
+ it("keeps the hidden fallback when the checkbox is unchecked", () => {
147
+ globalExpect(new FormObject(makeForm(`
148
+ <input type="hidden" name="agree" value="0" />
149
+ <input type="checkbox" name="agree" value="1" />
150
+ `)).toObject()).toEqual({ agree: "0" });
151
+ });
152
+ it("keeps a pure radio group as a scalar", () => {
153
+ globalExpect(new FormObject(makeForm(`
154
+ <input type="radio" name="r" value="a" />
155
+ <input type="radio" name="r" value="b" checked />
156
+ `)).toObject()).toEqual({ r: "b" });
157
+ });
158
+ });
159
+ describe("FormObject `[]` array fields", () => {
160
+ it("wraps a single `[]` control in an array", () => {
161
+ globalExpect(new FormObject(makeForm(`<input type="checkbox" name="c[]" value="x" checked />`)).toObject()).toEqual({ c: ["x"] });
162
+ });
163
+ it("appends every surviving `[]` control in document order", () => {
164
+ globalExpect(new FormObject(makeForm(`
165
+ <input type="checkbox" name="c[]" value="x" checked />
166
+ <input type="checkbox" name="c[]" value="y" checked />
167
+ `)).toObject()).toEqual({ c: ["x", "y"] });
168
+ });
169
+ it("yields an empty array for a declared-but-empty `[]` field", () => {
170
+ globalExpect(new FormObject(makeForm(`<input type="checkbox" name="c[]" value="x" />`)).toObject()).toEqual({ c: [] });
171
+ });
172
+ it("nests a `[]` array under a dot-path", () => {
173
+ globalExpect(new FormObject(makeForm(`
174
+ <input name="user.tags[]" value="a" />
175
+ <input name="user.tags[]" value="b" />
176
+ `)).toObject()).toEqual({ user: { tags: ["a", "b"] } });
177
+ });
178
+ it("hidden + `[]` checkbox: array with both when checked, hidden-only when unchecked", () => {
179
+ const checked = makeForm(`
180
+ <input type="hidden" name="a[]" value="0" />
181
+ <input type="checkbox" name="a[]" value="1" checked />
182
+ `);
183
+ const unchecked = makeForm(`
184
+ <input type="hidden" name="a[]" value="0" />
185
+ <input type="checkbox" name="a[]" value="1" />
186
+ `);
187
+ globalExpect(new FormObject(checked).toObject()).toEqual({ a: ["0", "1"] });
188
+ globalExpect(new FormObject(unchecked).toObject()).toEqual({ a: ["0"] });
189
+ });
190
+ it("builds an array of objects via explicit indices", () => {
191
+ globalExpect(new FormObject(makeForm(`
192
+ <input name="items.0.id" value="1" />
193
+ <input name="items.1.id" value="2" />
194
+ `)).toObject()).toEqual({ items: [{ id: "1" }, { id: "2" }] });
195
+ });
196
+ });
197
+ describe("FormObject disabled controls", () => {
198
+ it("skips a disabled checked checkbox under the default pipeline", () => {
199
+ globalExpect(new FormObject(makeForm(`<input type="checkbox" name="a" value="1" checked disabled />`)).toObject()).toEqual({});
200
+ });
201
+ it("includes a disabled checked checkbox when skipDisabled is omitted", () => {
202
+ globalExpect(new FormObject(makeForm(`<input type="checkbox" name="a" value="1" checked disabled />`), { transforms: [skipButtons, dropUnchecked] }).toObject()).toEqual({ a: "1" });
203
+ });
204
+ it("still drops a disabled UNCHECKED checkbox via dropUnchecked even without skipDisabled", () => {
205
+ globalExpect(new FormObject(makeForm(`<input type="checkbox" name="a" value="1" disabled />`), { transforms: [skipButtons, dropUnchecked] }).toObject()).toEqual({});
206
+ });
207
+ it("appends an included disabled member to a `[]` array", () => {
208
+ globalExpect(new FormObject(makeForm(`
209
+ <input type="checkbox" name="g[]" value="1" checked />
210
+ <input type="checkbox" name="g[]" value="2" checked disabled />
211
+ `), { transforms: [skipButtons, dropUnchecked] }).toObject()).toEqual({ g: ["1", "2"] });
212
+ });
213
+ it("drops a disabled `[]` member under the default pipeline, keeping the array", () => {
214
+ globalExpect(new FormObject(makeForm(`
215
+ <input type="checkbox" name="g[]" value="1" checked />
216
+ <input type="checkbox" name="g[]" value="2" checked disabled />
217
+ `)).toObject()).toEqual({ g: ["1"] });
218
+ });
219
+ it("a disabled bare checkbox leaves only the hidden value under the default pipeline", () => {
220
+ globalExpect(new FormObject(makeForm(`
221
+ <input type="hidden" name="a" value="0" />
222
+ <input type="checkbox" name="a" value="1" checked disabled />
223
+ `)).toObject()).toEqual({ a: "0" });
224
+ });
225
+ it("an included disabled checkbox overrides the hidden value (bare, last wins)", () => {
226
+ globalExpect(new FormObject(makeForm(`
227
+ <input type="hidden" name="a" value="0" />
228
+ <input type="checkbox" name="a" value="1" checked disabled />
229
+ `), { transforms: [skipButtons, dropUnchecked] }).toObject()).toEqual({ a: "1" });
230
+ });
231
+ it("does not write disabled controls in fromObject", () => {
232
+ const form = makeForm(`<input name="a" value="orig" disabled />`);
233
+ new FormObject(form).fromObject({ a: "changed" });
234
+ globalExpect(form.elements.namedItem("a").value).toBe("orig");
235
+ });
236
+ });
237
+ describe("FormObject path edge cases", () => {
238
+ it("does not pollute Object.prototype via __proto__ segments", () => {
239
+ globalExpect(new FormObject(makeForm(`<input name="__proto__.polluted" value="yes" />`)).toObject()).toEqual({});
240
+ globalExpect({}.polluted).toBeUndefined();
241
+ });
242
+ it("ignores constructor / prototype segments", () => {
243
+ globalExpect(new FormObject(makeForm(`
244
+ <input name="constructor.prototype.x" value="bad" />
245
+ <input name="a.prototype.b" value="bad" />
246
+ `)).toObject()).toEqual({});
247
+ });
248
+ it("leaves gaps in sparse array indices as holes", () => {
249
+ globalExpect(new FormObject(makeForm(`
250
+ <input name="t.0" value="a" />
251
+ <input name="t.2" value="c" />
252
+ `)).toObject()).toEqual({ t: [
253
+ "a",
254
+ void 0,
255
+ "c"
256
+ ] });
257
+ });
258
+ it("builds deeply nested arrays of objects", () => {
259
+ globalExpect(new FormObject(makeForm(`
260
+ <input name="a.0.b.0.c" value="1" />
261
+ <input name="a.0.b.1.c" value="2" />
262
+ `)).toObject()).toEqual({ a: [{ b: [{ c: "1" }, { c: "2" }] }] });
263
+ });
264
+ it("treats numeric top-level names as object keys, not an array", () => {
265
+ globalExpect(new FormObject(makeForm(`
266
+ <input name="0" value="a" />
267
+ <input name="1" value="b" />
268
+ `)).toObject()).toEqual({
269
+ "0": "a",
270
+ "1": "b"
271
+ });
272
+ });
273
+ });
274
+ describe("FormObject transforms", () => {
275
+ it("skips named buttons by default", () => {
276
+ globalExpect(new FormObject(makeForm(`
277
+ <input name="email" value="a@b.com" />
278
+ <button name="action" value="save">Save</button>
279
+ <input type="submit" name="submit" value="go" />
280
+ `)).toObject()).toEqual({ email: "a@b.com" });
281
+ });
282
+ it("drops unchecked checkboxes and radios by default", () => {
283
+ globalExpect(new FormObject(makeForm(`
284
+ <input type="checkbox" name="agree" />
285
+ <input type="radio" name="plan" value="free" />
286
+ <input type="radio" name="plan" value="pro" />
287
+ `)).toObject()).toEqual({});
288
+ });
289
+ it("uncheckedAs(false) includes unchecked checkbox but still drops unchecked radio", () => {
290
+ globalExpect(new FormObject(makeForm(`
291
+ <input type="checkbox" name="agree" />
292
+ <input type="radio" name="plan" value="free" />
293
+ <input type="radio" name="plan" value="pro" />
294
+ `), { transforms: [skipButtons, uncheckedAs(false)] }).toObject()).toEqual({ agree: false });
295
+ });
296
+ it("skipEmpty drops empty-string fields", () => {
297
+ globalExpect(new FormObject(makeForm(`
298
+ <input name="filled" value="x" />
299
+ <input name="blank" value="" />
300
+ `), { transforms: [...defaultTransforms, skipEmpty] }).toObject()).toEqual({ filled: "x" });
301
+ });
302
+ it("runs a custom transform that rewrites the value", () => {
303
+ const form = makeForm(`<input name="name" value=" Wael " />`);
304
+ const trim = (f) => ({
305
+ ...f,
306
+ value: typeof f.value === "string" ? f.value.trim() : f.value
307
+ });
308
+ globalExpect(new FormObject(form, { transforms: [...defaultTransforms, trim] }).toObject()).toEqual({ name: "Wael" });
309
+ });
310
+ it("runs a custom transform that renames the field path", () => {
311
+ const form = makeForm(`<input name="legacy" value="v" />`);
312
+ const rename = (f) => f.name === "legacy" ? {
313
+ ...f,
314
+ name: "modern"
315
+ } : f;
316
+ globalExpect(new FormObject(form, { transforms: [...defaultTransforms, rename] }).toObject()).toEqual({ modern: "v" });
317
+ });
318
+ it("with an empty pipeline emits everything (disabled, buttons, unchecked)", () => {
319
+ globalExpect(new FormObject(makeForm(`
320
+ <input name="a" value="1" disabled />
321
+ <button name="b" value="x">x</button>
322
+ <input type="checkbox" name="c" value="on" />
323
+ `), { transforms: [] }).toObject()).toEqual({
324
+ a: "1",
325
+ b: "x",
326
+ c: "on"
327
+ });
328
+ });
329
+ });
330
+ describe("FormObject.fromObject", () => {
331
+ it("writes nested values back onto controls", () => {
332
+ const form = makeForm(`
333
+ <input name="user.name" />
334
+ <input name="user.address.city" />
335
+ `);
336
+ new FormObject(form).fromObject({ user: {
337
+ name: "Wael",
338
+ address: { city: "Tunis" }
339
+ } });
340
+ globalExpect(form.elements.namedItem("user.name").value).toBe("Wael");
341
+ globalExpect(form.elements.namedItem("user.address.city").value).toBe("Tunis");
342
+ });
343
+ it("writes array values back into numeric-segment controls", () => {
344
+ const form = makeForm(`
345
+ <input name="tags.0" />
346
+ <input name="tags.1" />
347
+ `);
348
+ new FormObject(form).fromObject({ tags: ["a", "b"] });
349
+ globalExpect(form.elements.namedItem("tags.0").value).toBe("a");
350
+ globalExpect(form.elements.namedItem("tags.1").value).toBe("b");
351
+ });
352
+ it("round-trips a `[]` checkbox group through fromObject", () => {
353
+ const form = makeForm(`
354
+ <input type="checkbox" name="colors[]" value="red" />
355
+ <input type="checkbox" name="colors[]" value="green" />
356
+ <input type="checkbox" name="colors[]" value="blue" />
357
+ `);
358
+ new FormObject(form).fromObject({ colors: ["red", "blue"] });
359
+ globalExpect(new FormObject(form).toObject()).toEqual({ colors: ["red", "blue"] });
360
+ });
361
+ it("sets checkbox checked state from a boolean-ish value", () => {
362
+ const form = makeForm(`<input type="checkbox" name="agree" value="yes" />`);
363
+ new FormObject(form).fromObject({ agree: "yes" });
364
+ globalExpect(form.elements.namedItem("agree").checked).toBe(true);
365
+ });
366
+ it("selects the matching radio", () => {
367
+ const form = makeForm(`
368
+ <input type="radio" name="plan" value="free" />
369
+ <input type="radio" name="plan" value="pro" />
370
+ `);
371
+ new FormObject(form).fromObject({ plan: "pro" });
372
+ globalExpect(form.querySelector("input[name=plan]:checked")?.value).toBe("pro");
373
+ });
374
+ it("leaves controls untouched when the path is missing", () => {
375
+ const form = makeForm(`<input name="keep" value="original" />`);
376
+ new FormObject(form).fromObject({ other: "x" });
377
+ globalExpect(form.elements.namedItem("keep").value).toBe("original");
378
+ });
379
+ it("round-trips a form through toObject -> fromObject -> toObject", () => {
380
+ const html = `
381
+ <input name="user.name" value="Wael" />
382
+ <input name="tags.0" value="a" />
383
+ <input name="tags.1" value="b" />
384
+ <input type="checkbox" name="agree" checked />
385
+ <select name="country">
386
+ <option value="tn">Tunisia</option>
387
+ <option value="fr" selected>France</option>
388
+ </select>
389
+ `;
390
+ const source = new FormObject(makeForm(html)).toObject();
391
+ const target = makeForm(html);
392
+ new FormObject(target).clear();
393
+ globalExpect(new FormObject(target).fromObject(source).toObject()).toEqual(source);
394
+ });
395
+ it("returns this for chaining", () => {
396
+ const data = new FormObject(makeForm(`<input name="a" />`));
397
+ globalExpect(data.fromObject({ a: "1" })).toBe(data);
398
+ globalExpect(data.clear()).toBe(data);
399
+ });
400
+ });
401
+ describe("FormObject serialization", () => {
402
+ it("supports JSON.stringify via toJSON", () => {
403
+ const form = makeForm(`<input name="user.name" value="Wael" />`);
404
+ globalExpect(JSON.stringify(new FormObject(form))).toBe(JSON.stringify({ user: { name: "Wael" } }));
405
+ });
406
+ });
407
+ //#endregion
408
+ export {};
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "elements-kit",
3
3
  "type": "module",
4
- "version": "0.18.2",
4
+ "version": "0.19.0",
5
5
  "description": "A lightweight reactive UI library that transforms native HTMLElements into reactive components with signals. Ideal for framework-agnostic applications and web components.",
6
6
  "keywords": [
7
7
  "webcomponents",
@@ -90,15 +90,15 @@
90
90
  "types": "./dist/utilities/*.d.mts"
91
91
  },
92
92
  "./ui/overlay": {
93
- "source": "./src/ui/overlay/gestures.ts",
94
- "import": "./dist/ui/overlay/gestures.mjs",
95
- "types": "./dist/ui/overlay/gestures.d.mts"
93
+ "source": "./src/ui/overlay/index.ts",
94
+ "import": "./dist/ui/overlay/index.mjs",
95
+ "types": "./dist/ui/overlay/index.d.mts"
96
96
  },
97
97
  "./ui/*": "./dist/ui/*"
98
98
  },
99
99
  "scripts": {
100
100
  "build": "tsdown",
101
- "build:docs": "pnpm build && pnpm --filter elements-kit-docs build && pnpm build:storybook && rm -rf docs/dist/storybook && cp -R storybook/storybook-static docs/dist/storybook",
101
+ "build:docs": "pnpm build && pnpm --filter elements-kit-docs build",
102
102
  "build:storybook": "pnpm --filter elements-kit-storybook build",
103
103
  "storybook": "pnpm --filter elements-kit-storybook dev",
104
104
  "watch": "tsdown -w",