@conform-to/dom 1.6.0 → 1.7.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 CHANGED
@@ -7,7 +7,7 @@
7
7
  ╚══════╝ ╚═════╝ ╚═╝ ╚══╝ ╚═╝ ╚═════╝ ╚═╝ ╚═╝ ╚═╝ ╚═╝
8
8
  ```
9
9
 
10
- Version 1.6.0 / License MIT / Copyright (c) 2024 Edmund Hung
10
+ Version 1.7.0 / License MIT / Copyright (c) 2024 Edmund Hung
11
11
 
12
12
  A type-safe form validation library utilizing web fundamentals to progressively enhance HTML Forms with full support for server frameworks like Remix and Next.js.
13
13
 
package/dist/dom.d.ts ADDED
@@ -0,0 +1,65 @@
1
+ /**
2
+ * Element that user can interact with,
3
+ * includes `<input>`, `<select>` and `<textarea>`.
4
+ */
5
+ export type FieldElement = HTMLInputElement | HTMLSelectElement | HTMLTextAreaElement;
6
+ /**
7
+ * Form Control element. It can either be a submit button or a submit input.
8
+ */
9
+ export type Submitter = HTMLInputElement | HTMLButtonElement;
10
+ export declare function isInputElement(element: Element): element is HTMLInputElement;
11
+ export declare function isSelectElement(element: Element): element is HTMLSelectElement;
12
+ export declare function isTextAreaElement(element: Element): element is HTMLTextAreaElement;
13
+ /**
14
+ * A type guard to check if the provided element is a field element, which
15
+ * is a form control excluding submit, button and reset type.
16
+ */
17
+ export declare function isFieldElement(element: unknown): element is FieldElement;
18
+ /**
19
+ * Resolves the action from the submit event
20
+ * with respect to the submitter `formaction` attribute.
21
+ */
22
+ export declare function getFormAction(event: SubmitEvent): string;
23
+ /**
24
+ * Resolves the encoding type from the submit event
25
+ * with respect to the submitter `formenctype` attribute.
26
+ */
27
+ export declare function getFormEncType(event: SubmitEvent): 'application/x-www-form-urlencoded' | 'multipart/form-data';
28
+ /**
29
+ * Resolves the method from the submit event
30
+ * with respect to the submitter `formmethod` attribute.
31
+ */
32
+ export declare function getFormMethod(event: SubmitEvent): 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE';
33
+ /**
34
+ * Trigger a form submit event with an optional submitter.
35
+ * If the submitter is not mounted, it will be appended to the form and removed after submission.
36
+ */
37
+ export declare function requestSubmit(form: HTMLFormElement | null | undefined, submitter: Submitter | null): void;
38
+ export declare function createFileList(value: File | File[]): FileList;
39
+ type InputCallback = (event: {
40
+ type: 'input' | 'reset' | 'mutation';
41
+ target: HTMLInputElement | HTMLSelectElement | HTMLTextAreaElement;
42
+ }) => void;
43
+ type FormCallback = (event: {
44
+ type: 'submit' | 'input' | 'reset' | 'mutation';
45
+ target: HTMLFormElement;
46
+ submitter?: HTMLInputElement | HTMLButtonElement | null;
47
+ }) => void;
48
+ export declare function createGlobalFormsObserver(): {
49
+ onFieldUpdate(callback: InputCallback): () => void;
50
+ onFormUpdate(callback: FormCallback): () => void;
51
+ dispose(): void;
52
+ };
53
+ export declare function change(element: HTMLInputElement | HTMLSelectElement | HTMLTextAreaElement, value: string | string[] | File | File[] | FileList | null): void;
54
+ export declare function focus(element: HTMLInputElement | HTMLSelectElement | HTMLTextAreaElement): void;
55
+ export declare function blur(element: HTMLInputElement | HTMLSelectElement | HTMLTextAreaElement): void;
56
+ export declare function normalizeFieldValue(value: unknown): [string[] | null, FileList | null];
57
+ /**
58
+ * Updates the DOM element with the provided value and defaultValue.
59
+ */
60
+ export declare function updateField(element: HTMLInputElement | HTMLSelectElement | HTMLTextAreaElement, options: {
61
+ value?: unknown;
62
+ defaultValue?: unknown;
63
+ }): void;
64
+ export {};
65
+ //# sourceMappingURL=dom.d.ts.map
package/dist/dom.js ADDED
@@ -0,0 +1,453 @@
1
+ 'use strict';
2
+
3
+ Object.defineProperty(exports, '__esModule', { value: true });
4
+
5
+ var util = require('./util.js');
6
+
7
+ /**
8
+ * Element that user can interact with,
9
+ * includes `<input>`, `<select>` and `<textarea>`.
10
+ */
11
+
12
+ /**
13
+ * Form Control element. It can either be a submit button or a submit input.
14
+ */
15
+
16
+ function isInputElement(element) {
17
+ return element.tagName === 'INPUT';
18
+ }
19
+ function isSelectElement(element) {
20
+ return element.tagName === 'SELECT';
21
+ }
22
+ function isTextAreaElement(element) {
23
+ return element.tagName === 'TEXTAREA';
24
+ }
25
+
26
+ /**
27
+ * A type guard to check if the provided element is a field element, which
28
+ * is a form control excluding submit, button and reset type.
29
+ */
30
+ function isFieldElement(element) {
31
+ if (element instanceof Element) {
32
+ if (isInputElement(element)) {
33
+ return element.type !== 'submit' && element.type !== 'button' && element.type !== 'reset';
34
+ }
35
+ if (isSelectElement(element) || isTextAreaElement(element)) {
36
+ return true;
37
+ }
38
+ }
39
+ return false;
40
+ }
41
+
42
+ /**
43
+ * Resolves the action from the submit event
44
+ * with respect to the submitter `formaction` attribute.
45
+ */
46
+ function getFormAction(event) {
47
+ var _ref, _submitter$getAttribu;
48
+ var form = event.target;
49
+ var submitter = event.submitter;
50
+ return (_ref = (_submitter$getAttribu = submitter === null || submitter === void 0 ? void 0 : submitter.getAttribute('formaction')) !== null && _submitter$getAttribu !== void 0 ? _submitter$getAttribu : form.getAttribute('action')) !== null && _ref !== void 0 ? _ref : "".concat(location.pathname).concat(location.search);
51
+ }
52
+
53
+ /**
54
+ * Resolves the encoding type from the submit event
55
+ * with respect to the submitter `formenctype` attribute.
56
+ */
57
+ function getFormEncType(event) {
58
+ var _submitter$getAttribu2;
59
+ var form = event.target;
60
+ var submitter = event.submitter;
61
+ var encType = (_submitter$getAttribu2 = submitter === null || submitter === void 0 ? void 0 : submitter.getAttribute('formenctype')) !== null && _submitter$getAttribu2 !== void 0 ? _submitter$getAttribu2 : form.enctype;
62
+ if (encType === 'multipart/form-data') {
63
+ return encType;
64
+ }
65
+ return 'application/x-www-form-urlencoded';
66
+ }
67
+
68
+ /**
69
+ * Resolves the method from the submit event
70
+ * with respect to the submitter `formmethod` attribute.
71
+ */
72
+ function getFormMethod(event) {
73
+ var _ref2, _submitter$getAttribu3;
74
+ var form = event.target;
75
+ var submitter = event.submitter;
76
+ var method = (_ref2 = (_submitter$getAttribu3 = submitter === null || submitter === void 0 ? void 0 : submitter.getAttribute('formmethod')) !== null && _submitter$getAttribu3 !== void 0 ? _submitter$getAttribu3 : form.getAttribute('method')) === null || _ref2 === void 0 ? void 0 : _ref2.toUpperCase();
77
+ switch (method) {
78
+ case 'POST':
79
+ case 'PUT':
80
+ case 'PATCH':
81
+ case 'DELETE':
82
+ return method;
83
+ }
84
+ return 'GET';
85
+ }
86
+
87
+ /**
88
+ * Trigger a form submit event with an optional submitter.
89
+ * If the submitter is not mounted, it will be appended to the form and removed after submission.
90
+ */
91
+ function requestSubmit(form, submitter) {
92
+ util.invariant(!!form, 'Failed to submit the form. The element provided is null or undefined.');
93
+ if (typeof form.requestSubmit === 'function') {
94
+ form.requestSubmit(submitter);
95
+ } else {
96
+ var _event = new SubmitEvent('submit', {
97
+ bubbles: true,
98
+ cancelable: true,
99
+ submitter
100
+ });
101
+ form.dispatchEvent(_event);
102
+ }
103
+ }
104
+ function createFileList(value) {
105
+ var dataTransfer = new DataTransfer();
106
+ if (Array.isArray(value)) {
107
+ for (var file of value) {
108
+ dataTransfer.items.add(file);
109
+ }
110
+ } else {
111
+ dataTransfer.items.add(value);
112
+ }
113
+ return dataTransfer.files;
114
+ }
115
+ function createGlobalFormsObserver() {
116
+ var inputListeners = new Set();
117
+ var formListeners = new Set();
118
+ var cleanup = null;
119
+ function initialize() {
120
+ var observer = new MutationObserver(handleMutation);
121
+ observer.observe(document.body, {
122
+ subtree: true,
123
+ childList: true,
124
+ attributeFilter: ['form', 'name', 'data-conform']
125
+ });
126
+ document.addEventListener('input', handleInput);
127
+ document.addEventListener('reset', handleReset);
128
+ document.addEventListener('submit', handleSubmit, true);
129
+ return () => {
130
+ document.removeEventListener('input', handleInput);
131
+ document.removeEventListener('reset', handleReset);
132
+ document.removeEventListener('submit', handleSubmit, true);
133
+ observer.disconnect();
134
+ };
135
+ }
136
+ function handleInput(event) {
137
+ var target = event.target;
138
+ if (isFieldElement(target)) {
139
+ inputListeners.forEach(callback => callback({
140
+ type: 'input',
141
+ target
142
+ }));
143
+ var form = target.form;
144
+ if (form) {
145
+ formListeners.forEach(callback => callback({
146
+ type: 'input',
147
+ target: form
148
+ }));
149
+ }
150
+ }
151
+ }
152
+ function handleReset(event) {
153
+ var form = event.target;
154
+ if (form instanceof HTMLFormElement) {
155
+ // Reset event is fired before the form is reset, so we need to wait for the next tick
156
+ setTimeout(() => {
157
+ formListeners.forEach(callback => {
158
+ callback({
159
+ type: 'reset',
160
+ target: form
161
+ });
162
+ });
163
+ var _loop = function _loop(target) {
164
+ if (isFieldElement(target)) {
165
+ inputListeners.forEach(callback => {
166
+ callback({
167
+ type: 'reset',
168
+ target
169
+ });
170
+ });
171
+ }
172
+ };
173
+ for (var target of form.elements) {
174
+ _loop(target);
175
+ }
176
+ });
177
+ }
178
+ }
179
+ function handleSubmit(event) {
180
+ var target = event.target;
181
+ var submitter = event.submitter;
182
+ if (target instanceof HTMLFormElement) {
183
+ formListeners.forEach(callback => callback({
184
+ type: 'submit',
185
+ target,
186
+ submitter
187
+ }));
188
+ }
189
+ }
190
+ function handleMutation(mutations) {
191
+ var seenForms = new Set();
192
+ var seenInputs = new Set();
193
+ var collectInputs = node => {
194
+ if (isFieldElement(node)) {
195
+ return [node];
196
+ }
197
+ return node instanceof Element ? Array.from(node.querySelectorAll('input,select,textarea')) : [];
198
+ };
199
+ for (var mutation of mutations) {
200
+ switch (mutation.type) {
201
+ case 'childList':
202
+ {
203
+ var nodes = [...mutation.addedNodes, ...mutation.removedNodes];
204
+ for (var node of nodes) {
205
+ for (var input of collectInputs(node)) {
206
+ seenInputs.add(input);
207
+ if (input.form) {
208
+ seenForms.add(input.form);
209
+ }
210
+ }
211
+ }
212
+ break;
213
+ }
214
+ case 'attributes':
215
+ {
216
+ if (isFieldElement(mutation.target)) {
217
+ seenInputs.add(mutation.target);
218
+ if (mutation.target.form) {
219
+ seenForms.add(mutation.target.form);
220
+ }
221
+ }
222
+ break;
223
+ }
224
+ }
225
+ }
226
+ var _loop2 = function _loop2(target) {
227
+ formListeners.forEach(callback => {
228
+ callback({
229
+ type: 'mutation',
230
+ target
231
+ });
232
+ });
233
+ };
234
+ for (var target of seenForms) {
235
+ _loop2(target);
236
+ }
237
+ var _loop3 = function _loop3(_target) {
238
+ inputListeners.forEach(callback => {
239
+ callback({
240
+ type: 'mutation',
241
+ target: _target
242
+ });
243
+ });
244
+ };
245
+ for (var _target of seenInputs) {
246
+ _loop3(_target);
247
+ }
248
+ }
249
+ return {
250
+ onFieldUpdate(callback) {
251
+ var _cleanup;
252
+ cleanup = (_cleanup = cleanup) !== null && _cleanup !== void 0 ? _cleanup : initialize();
253
+ inputListeners.add(callback);
254
+ return () => {
255
+ inputListeners.delete(callback);
256
+ };
257
+ },
258
+ onFormUpdate(callback) {
259
+ var _cleanup2;
260
+ cleanup = (_cleanup2 = cleanup) !== null && _cleanup2 !== void 0 ? _cleanup2 : initialize();
261
+ formListeners.add(callback);
262
+ return () => {
263
+ formListeners.delete(callback);
264
+ };
265
+ },
266
+ dispose() {
267
+ var _cleanup3;
268
+ (_cleanup3 = cleanup) === null || _cleanup3 === void 0 || _cleanup3();
269
+ cleanup = null;
270
+ inputListeners.clear();
271
+ formListeners.clear();
272
+ }
273
+ };
274
+ }
275
+ function change(element, value) {
276
+ // The value should be set to the element before dispatching the event
277
+ updateField(element, {
278
+ value
279
+ });
280
+
281
+ // Dispatch input event with the updated input value
282
+ element.dispatchEvent(new InputEvent('input', {
283
+ bubbles: true
284
+ }));
285
+ // Dispatch change event (necessary for select to update the selected option)
286
+ element.dispatchEvent(new Event('change', {
287
+ bubbles: true
288
+ }));
289
+ }
290
+ function focus(element) {
291
+ // Only focusin event will be bubbled
292
+ element.dispatchEvent(new FocusEvent('focusin', {
293
+ bubbles: true
294
+ }));
295
+ element.dispatchEvent(new FocusEvent('focus'));
296
+ }
297
+ function blur(element) {
298
+ // Only focusout event will be bubbled
299
+ element.dispatchEvent(new FocusEvent('focusout', {
300
+ bubbles: true
301
+ }));
302
+ element.dispatchEvent(new FocusEvent('blur'));
303
+ }
304
+ function normalizeFieldValue(value) {
305
+ if (typeof value === 'undefined') {
306
+ return [null, null];
307
+ }
308
+ if (value === null) {
309
+ return [[], createFileList([])];
310
+ }
311
+ if (typeof value === 'string') {
312
+ return [[value], null];
313
+ }
314
+ if (Array.isArray(value)) {
315
+ if (value.every(item => typeof item === 'string')) {
316
+ return [Array.from(value), null];
317
+ }
318
+ if (value.every(item => item instanceof File)) {
319
+ return [null, createFileList(value)];
320
+ }
321
+ }
322
+ if (value instanceof FileList) {
323
+ return [null, value];
324
+ }
325
+ if (value instanceof File) {
326
+ return [null, createFileList([value])];
327
+ }
328
+ return [null, null];
329
+ }
330
+
331
+ /**
332
+ * Updates the DOM element with the provided value and defaultValue.
333
+ */
334
+ function updateField(element, options) {
335
+ var _value$;
336
+ var [value, file] = normalizeFieldValue(options.value);
337
+ var [defaultValue] = normalizeFieldValue(options.defaultValue);
338
+ if (isInputElement(element)) {
339
+ switch (element.type) {
340
+ case 'file':
341
+ {
342
+ element.files = file;
343
+ return;
344
+ }
345
+ case 'checkbox':
346
+ case 'radio':
347
+ {
348
+ if (value) {
349
+ var checked = value.includes(element.value);
350
+ if (element.type === 'checkbox' ? checked !== element.checked : checked) {
351
+ // Simulate a click to update the checked state
352
+ element.click();
353
+ }
354
+ element.checked = checked;
355
+ }
356
+ if (defaultValue) {
357
+ element.defaultChecked = defaultValue.includes(element.value);
358
+ }
359
+ return;
360
+ }
361
+ }
362
+ } else if (isSelectElement(element)) {
363
+ var shouldUnselect = value && value.length === 0;
364
+ for (var option of element.options) {
365
+ if (value) {
366
+ var index = value.indexOf(option.value);
367
+ var selected = index > -1;
368
+
369
+ // Update the selected state of the option
370
+ if (option.selected !== selected) {
371
+ option.selected = selected;
372
+ }
373
+
374
+ // Remove the option from the value array
375
+ if (selected) {
376
+ value.splice(index, 1);
377
+ }
378
+ }
379
+ if (defaultValue) {
380
+ var _index = defaultValue.indexOf(option.value);
381
+ var _selected = _index > -1;
382
+
383
+ // Update the selected state of the option
384
+ if (option.defaultSelected !== _selected) {
385
+ option.defaultSelected = _selected;
386
+ }
387
+
388
+ // Remove the option from the defaultValue array
389
+ if (_selected) {
390
+ defaultValue.splice(_index, 1);
391
+ }
392
+ }
393
+ }
394
+
395
+ // We have already removed all selected options from the value and defaultValue array at this point
396
+ var missingOptions = new Set([...(value !== null && value !== void 0 ? value : []), ...(defaultValue !== null && defaultValue !== void 0 ? defaultValue : [])]);
397
+ for (var optionValue of missingOptions) {
398
+ element.options.add(new Option(optionValue, optionValue, defaultValue === null || defaultValue === void 0 ? void 0 : defaultValue.includes(optionValue), value === null || value === void 0 ? void 0 : value.includes(optionValue)));
399
+ }
400
+
401
+ // If the select element is not multiple and the value is an empty array, unset the selected index
402
+ // This is to prevent the select element from showing the first option as selected
403
+ if (shouldUnselect) {
404
+ element.selectedIndex = -1;
405
+ }
406
+ return;
407
+ }
408
+ var inputValue = (_value$ = value === null || value === void 0 ? void 0 : value[0]) !== null && _value$ !== void 0 ? _value$ : '';
409
+ if (element.value !== inputValue) {
410
+ /**
411
+ * Triggering react custom change event
412
+ * Solution based on dom-testing-library
413
+ * @see https://github.com/facebook/react/issues/10135#issuecomment-401496776
414
+ * @see https://github.com/testing-library/dom-testing-library/blob/main/src/events.js#L104-L123
415
+ */
416
+ var {
417
+ set: valueSetter
418
+ } = Object.getOwnPropertyDescriptor(element, 'value') || {};
419
+ var prototype = Object.getPrototypeOf(element);
420
+ var {
421
+ set: prototypeValueSetter
422
+ } = Object.getOwnPropertyDescriptor(prototype, 'value') || {};
423
+ if (prototypeValueSetter && valueSetter !== prototypeValueSetter) {
424
+ prototypeValueSetter.call(element, inputValue);
425
+ } else {
426
+ if (valueSetter) {
427
+ valueSetter.call(element, inputValue);
428
+ } else {
429
+ throw new Error('The given element does not have a value setter');
430
+ }
431
+ }
432
+ }
433
+ if (defaultValue) {
434
+ var _defaultValue$;
435
+ element.defaultValue = (_defaultValue$ = defaultValue[0]) !== null && _defaultValue$ !== void 0 ? _defaultValue$ : '';
436
+ }
437
+ }
438
+
439
+ exports.blur = blur;
440
+ exports.change = change;
441
+ exports.createFileList = createFileList;
442
+ exports.createGlobalFormsObserver = createGlobalFormsObserver;
443
+ exports.focus = focus;
444
+ exports.getFormAction = getFormAction;
445
+ exports.getFormEncType = getFormEncType;
446
+ exports.getFormMethod = getFormMethod;
447
+ exports.isFieldElement = isFieldElement;
448
+ exports.isInputElement = isInputElement;
449
+ exports.isSelectElement = isSelectElement;
450
+ exports.isTextAreaElement = isTextAreaElement;
451
+ exports.normalizeFieldValue = normalizeFieldValue;
452
+ exports.requestSubmit = requestSubmit;
453
+ exports.updateField = updateField;