@conform-to/dom 1.7.2 → 1.8.1
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 +1 -1
- package/dist/dom.d.ts +3 -1
- package/dist/dom.js +76 -47
- package/dist/dom.mjs +75 -47
- package/dist/formdata.d.ts +136 -1
- package/dist/formdata.js +190 -21
- package/dist/formdata.mjs +188 -22
- package/dist/index.d.ts +1 -1
- package/dist/index.js +2 -0
- package/dist/index.mjs +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
╚══════╝ ╚═════╝ ╚═╝ ╚══╝ ╚═╝ ╚═════╝ ╚═╝ ╚═╝ ╚═╝ ╚═╝
|
|
8
8
|
```
|
|
9
9
|
|
|
10
|
-
Version 1.
|
|
10
|
+
Version 1.8.1 / 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
CHANGED
|
@@ -53,9 +53,11 @@ export declare function createGlobalFormsObserver(): {
|
|
|
53
53
|
export declare function change(element: HTMLInputElement | HTMLSelectElement | HTMLTextAreaElement, value: string | string[] | File | File[] | FileList | null): void;
|
|
54
54
|
export declare function focus(element: HTMLInputElement | HTMLSelectElement | HTMLTextAreaElement): void;
|
|
55
55
|
export declare function blur(element: HTMLInputElement | HTMLSelectElement | HTMLTextAreaElement): void;
|
|
56
|
-
export declare function
|
|
56
|
+
export declare function normalizeStringValues(value: unknown): string[] | undefined;
|
|
57
|
+
export declare function normalizeFileValues(value: unknown): FileList | undefined;
|
|
57
58
|
/**
|
|
58
59
|
* Updates the DOM element with the provided value and defaultValue.
|
|
60
|
+
* If the value or defaultValue is undefined, it will keep the current value instead
|
|
59
61
|
*/
|
|
60
62
|
export declare function updateField(element: HTMLInputElement | HTMLSelectElement | HTMLTextAreaElement, options: {
|
|
61
63
|
value?: unknown;
|
package/dist/dom.js
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
Object.defineProperty(exports, '__esModule', { value: true });
|
|
4
4
|
|
|
5
|
+
var formdata = require('./formdata.js');
|
|
5
6
|
var util = require('./util.js');
|
|
6
7
|
|
|
7
8
|
/**
|
|
@@ -121,6 +122,8 @@ function createGlobalFormsObserver() {
|
|
|
121
122
|
observer.observe(document.body, {
|
|
122
123
|
subtree: true,
|
|
123
124
|
childList: true,
|
|
125
|
+
attributes: true,
|
|
126
|
+
attributeOldValue: true,
|
|
124
127
|
attributeFilter: ['form', 'name', 'data-conform']
|
|
125
128
|
});
|
|
126
129
|
document.addEventListener('input', handleInput);
|
|
@@ -187,6 +190,15 @@ function createGlobalFormsObserver() {
|
|
|
187
190
|
}));
|
|
188
191
|
}
|
|
189
192
|
}
|
|
193
|
+
function getAssociatedFormElement(formId, node) {
|
|
194
|
+
if (formId !== null) {
|
|
195
|
+
return document.forms.namedItem(formId);
|
|
196
|
+
}
|
|
197
|
+
if (node instanceof Element) {
|
|
198
|
+
return node.closest('form');
|
|
199
|
+
}
|
|
200
|
+
return null;
|
|
201
|
+
}
|
|
190
202
|
function handleMutation(mutations) {
|
|
191
203
|
var seenForms = new Set();
|
|
192
204
|
var seenInputs = new Set();
|
|
@@ -196,16 +208,27 @@ function createGlobalFormsObserver() {
|
|
|
196
208
|
}
|
|
197
209
|
return node instanceof Element ? Array.from(node.querySelectorAll('input,select,textarea')) : [];
|
|
198
210
|
};
|
|
211
|
+
var collectForms = node => {
|
|
212
|
+
if (node instanceof HTMLFormElement) {
|
|
213
|
+
return [node];
|
|
214
|
+
}
|
|
215
|
+
return node instanceof Element ? Array.from(node.querySelectorAll('form')) : [];
|
|
216
|
+
};
|
|
199
217
|
for (var mutation of mutations) {
|
|
200
218
|
switch (mutation.type) {
|
|
201
219
|
case 'childList':
|
|
202
220
|
{
|
|
203
221
|
var nodes = [...mutation.addedNodes, ...mutation.removedNodes];
|
|
204
222
|
for (var node of nodes) {
|
|
223
|
+
for (var form of collectForms(node)) {
|
|
224
|
+
seenForms.add(form);
|
|
225
|
+
}
|
|
205
226
|
for (var input of collectInputs(node)) {
|
|
227
|
+
var _input$form;
|
|
206
228
|
seenInputs.add(input);
|
|
207
|
-
|
|
208
|
-
|
|
229
|
+
var _form = (_input$form = input.form) !== null && _input$form !== void 0 ? _input$form : getAssociatedFormElement(input.getAttribute('form'), mutation.target);
|
|
230
|
+
if (_form) {
|
|
231
|
+
seenForms.add(_form);
|
|
209
232
|
}
|
|
210
233
|
}
|
|
211
234
|
}
|
|
@@ -218,6 +241,12 @@ function createGlobalFormsObserver() {
|
|
|
218
241
|
if (mutation.target.form) {
|
|
219
242
|
seenForms.add(mutation.target.form);
|
|
220
243
|
}
|
|
244
|
+
if (mutation.attributeName === 'form') {
|
|
245
|
+
var oldForm = getAssociatedFormElement(mutation.oldValue, mutation.target);
|
|
246
|
+
if (oldForm) {
|
|
247
|
+
seenForms.add(oldForm);
|
|
248
|
+
}
|
|
249
|
+
}
|
|
221
250
|
}
|
|
222
251
|
break;
|
|
223
252
|
}
|
|
@@ -301,69 +330,68 @@ function blur(element) {
|
|
|
301
330
|
}));
|
|
302
331
|
element.dispatchEvent(new FocusEvent('blur'));
|
|
303
332
|
}
|
|
304
|
-
function
|
|
305
|
-
if (typeof value === 'undefined')
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
if (value ===
|
|
309
|
-
return
|
|
333
|
+
function normalizeStringValues(value) {
|
|
334
|
+
if (typeof value === 'undefined') return undefined;
|
|
335
|
+
if (value === null) return [];
|
|
336
|
+
if (typeof value === 'string') return [value];
|
|
337
|
+
if (Array.isArray(value) && value.every(v => typeof v === 'string')) {
|
|
338
|
+
return Array.from(value);
|
|
310
339
|
}
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
if (
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
}
|
|
321
|
-
}
|
|
322
|
-
if (value instanceof FileList) {
|
|
323
|
-
return [null, value];
|
|
324
|
-
}
|
|
325
|
-
if (value instanceof File) {
|
|
326
|
-
return [null, createFileList([value])];
|
|
340
|
+
throw new Error('Expected string or string[] value for string based input');
|
|
341
|
+
}
|
|
342
|
+
function normalizeFileValues(value) {
|
|
343
|
+
if (typeof value === 'undefined') return undefined;
|
|
344
|
+
if (value === null) return createFileList([]);
|
|
345
|
+
if (formdata.isGlobalInstance(value, 'File')) return createFileList([value]);
|
|
346
|
+
if (formdata.isGlobalInstance(value, 'FileList')) return value;
|
|
347
|
+
if (Array.isArray(value) && value.every(item => formdata.isGlobalInstance(item, 'File'))) {
|
|
348
|
+
return createFileList(value);
|
|
327
349
|
}
|
|
328
|
-
|
|
350
|
+
throw new Error('Expected File, FileList or File[] for file input');
|
|
329
351
|
}
|
|
330
352
|
|
|
331
353
|
/**
|
|
332
354
|
* Updates the DOM element with the provided value and defaultValue.
|
|
355
|
+
* If the value or defaultValue is undefined, it will keep the current value instead
|
|
333
356
|
*/
|
|
334
357
|
function updateField(element, options) {
|
|
335
358
|
var _value$;
|
|
336
|
-
var [value, file] = normalizeFieldValue(options.value);
|
|
337
|
-
var [defaultValue] = normalizeFieldValue(options.defaultValue);
|
|
338
359
|
if (isInputElement(element)) {
|
|
339
360
|
switch (element.type) {
|
|
340
361
|
case 'file':
|
|
341
362
|
{
|
|
342
|
-
|
|
363
|
+
var files = normalizeFileValues(options.value);
|
|
364
|
+
if (files) {
|
|
365
|
+
element.files = files;
|
|
366
|
+
}
|
|
343
367
|
return;
|
|
344
368
|
}
|
|
345
369
|
case 'checkbox':
|
|
346
370
|
case 'radio':
|
|
347
371
|
{
|
|
348
|
-
|
|
349
|
-
|
|
372
|
+
var _value = normalizeStringValues(options.value);
|
|
373
|
+
var _defaultValue = normalizeStringValues(options.defaultValue);
|
|
374
|
+
if (_value) {
|
|
375
|
+
var checked = _value.includes(element.value);
|
|
350
376
|
if (element.type === 'checkbox' ? checked !== element.checked : checked) {
|
|
351
377
|
// Simulate a click to update the checked state
|
|
352
378
|
element.click();
|
|
353
379
|
}
|
|
354
380
|
element.checked = checked;
|
|
355
381
|
}
|
|
356
|
-
if (
|
|
357
|
-
element.defaultChecked =
|
|
382
|
+
if (_defaultValue) {
|
|
383
|
+
element.defaultChecked = _defaultValue.includes(element.value);
|
|
358
384
|
}
|
|
359
385
|
return;
|
|
360
386
|
}
|
|
361
387
|
}
|
|
362
388
|
} else if (isSelectElement(element)) {
|
|
363
|
-
var
|
|
389
|
+
var _value2 = normalizeStringValues(options.value);
|
|
390
|
+
var _defaultValue2 = normalizeStringValues(options.defaultValue);
|
|
391
|
+
var shouldUnselect = _value2 && _value2.length === 0;
|
|
364
392
|
for (var option of element.options) {
|
|
365
|
-
if (
|
|
366
|
-
var index =
|
|
393
|
+
if (_value2) {
|
|
394
|
+
var index = _value2.indexOf(option.value);
|
|
367
395
|
var selected = index > -1;
|
|
368
396
|
|
|
369
397
|
// Update the selected state of the option
|
|
@@ -373,11 +401,11 @@ function updateField(element, options) {
|
|
|
373
401
|
|
|
374
402
|
// Remove the option from the value array
|
|
375
403
|
if (selected) {
|
|
376
|
-
|
|
404
|
+
_value2.splice(index, 1);
|
|
377
405
|
}
|
|
378
406
|
}
|
|
379
|
-
if (
|
|
380
|
-
var _index =
|
|
407
|
+
if (_defaultValue2) {
|
|
408
|
+
var _index = _defaultValue2.indexOf(option.value);
|
|
381
409
|
var _selected = _index > -1;
|
|
382
410
|
|
|
383
411
|
// Update the selected state of the option
|
|
@@ -387,15 +415,15 @@ function updateField(element, options) {
|
|
|
387
415
|
|
|
388
416
|
// Remove the option from the defaultValue array
|
|
389
417
|
if (_selected) {
|
|
390
|
-
|
|
418
|
+
_defaultValue2.splice(_index, 1);
|
|
391
419
|
}
|
|
392
420
|
}
|
|
393
421
|
}
|
|
394
422
|
|
|
395
423
|
// We have already removed all selected options from the value and defaultValue array at this point
|
|
396
|
-
var missingOptions = new Set([...(
|
|
424
|
+
var missingOptions = new Set([...(_value2 !== null && _value2 !== void 0 ? _value2 : []), ...(_defaultValue2 !== null && _defaultValue2 !== void 0 ? _defaultValue2 : [])]);
|
|
397
425
|
for (var optionValue of missingOptions) {
|
|
398
|
-
element.options.add(new Option(optionValue, optionValue,
|
|
426
|
+
element.options.add(new Option(optionValue, optionValue, _defaultValue2 === null || _defaultValue2 === void 0 ? void 0 : _defaultValue2.includes(optionValue), _value2 === null || _value2 === void 0 ? void 0 : _value2.includes(optionValue)));
|
|
399
427
|
}
|
|
400
428
|
|
|
401
429
|
// If the select element is not multiple and the value is an empty array, unset the selected index
|
|
@@ -405,6 +433,8 @@ function updateField(element, options) {
|
|
|
405
433
|
}
|
|
406
434
|
return;
|
|
407
435
|
}
|
|
436
|
+
var value = normalizeStringValues(options.value);
|
|
437
|
+
var defaultValue = normalizeStringValues(options.defaultValue);
|
|
408
438
|
var inputValue = (_value$ = value === null || value === void 0 ? void 0 : value[0]) !== null && _value$ !== void 0 ? _value$ : '';
|
|
409
439
|
if (element.value !== inputValue) {
|
|
410
440
|
/**
|
|
@@ -422,12 +452,10 @@ function updateField(element, options) {
|
|
|
422
452
|
} = Object.getOwnPropertyDescriptor(prototype, 'value') || {};
|
|
423
453
|
if (prototypeValueSetter && valueSetter !== prototypeValueSetter) {
|
|
424
454
|
prototypeValueSetter.call(element, inputValue);
|
|
455
|
+
} else if (valueSetter) {
|
|
456
|
+
valueSetter.call(element, inputValue);
|
|
425
457
|
} else {
|
|
426
|
-
|
|
427
|
-
valueSetter.call(element, inputValue);
|
|
428
|
-
} else {
|
|
429
|
-
throw new Error('The given element does not have a value setter');
|
|
430
|
-
}
|
|
458
|
+
throw new Error('The given element does not have a value setter');
|
|
431
459
|
}
|
|
432
460
|
}
|
|
433
461
|
if (defaultValue) {
|
|
@@ -448,6 +476,7 @@ exports.isFieldElement = isFieldElement;
|
|
|
448
476
|
exports.isInputElement = isInputElement;
|
|
449
477
|
exports.isSelectElement = isSelectElement;
|
|
450
478
|
exports.isTextAreaElement = isTextAreaElement;
|
|
451
|
-
exports.
|
|
479
|
+
exports.normalizeFileValues = normalizeFileValues;
|
|
480
|
+
exports.normalizeStringValues = normalizeStringValues;
|
|
452
481
|
exports.requestSubmit = requestSubmit;
|
|
453
482
|
exports.updateField = updateField;
|
package/dist/dom.mjs
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { isGlobalInstance } from './formdata.mjs';
|
|
1
2
|
import { invariant } from './util.mjs';
|
|
2
3
|
|
|
3
4
|
/**
|
|
@@ -117,6 +118,8 @@ function createGlobalFormsObserver() {
|
|
|
117
118
|
observer.observe(document.body, {
|
|
118
119
|
subtree: true,
|
|
119
120
|
childList: true,
|
|
121
|
+
attributes: true,
|
|
122
|
+
attributeOldValue: true,
|
|
120
123
|
attributeFilter: ['form', 'name', 'data-conform']
|
|
121
124
|
});
|
|
122
125
|
document.addEventListener('input', handleInput);
|
|
@@ -183,6 +186,15 @@ function createGlobalFormsObserver() {
|
|
|
183
186
|
}));
|
|
184
187
|
}
|
|
185
188
|
}
|
|
189
|
+
function getAssociatedFormElement(formId, node) {
|
|
190
|
+
if (formId !== null) {
|
|
191
|
+
return document.forms.namedItem(formId);
|
|
192
|
+
}
|
|
193
|
+
if (node instanceof Element) {
|
|
194
|
+
return node.closest('form');
|
|
195
|
+
}
|
|
196
|
+
return null;
|
|
197
|
+
}
|
|
186
198
|
function handleMutation(mutations) {
|
|
187
199
|
var seenForms = new Set();
|
|
188
200
|
var seenInputs = new Set();
|
|
@@ -192,16 +204,27 @@ function createGlobalFormsObserver() {
|
|
|
192
204
|
}
|
|
193
205
|
return node instanceof Element ? Array.from(node.querySelectorAll('input,select,textarea')) : [];
|
|
194
206
|
};
|
|
207
|
+
var collectForms = node => {
|
|
208
|
+
if (node instanceof HTMLFormElement) {
|
|
209
|
+
return [node];
|
|
210
|
+
}
|
|
211
|
+
return node instanceof Element ? Array.from(node.querySelectorAll('form')) : [];
|
|
212
|
+
};
|
|
195
213
|
for (var mutation of mutations) {
|
|
196
214
|
switch (mutation.type) {
|
|
197
215
|
case 'childList':
|
|
198
216
|
{
|
|
199
217
|
var nodes = [...mutation.addedNodes, ...mutation.removedNodes];
|
|
200
218
|
for (var node of nodes) {
|
|
219
|
+
for (var form of collectForms(node)) {
|
|
220
|
+
seenForms.add(form);
|
|
221
|
+
}
|
|
201
222
|
for (var input of collectInputs(node)) {
|
|
223
|
+
var _input$form;
|
|
202
224
|
seenInputs.add(input);
|
|
203
|
-
|
|
204
|
-
|
|
225
|
+
var _form = (_input$form = input.form) !== null && _input$form !== void 0 ? _input$form : getAssociatedFormElement(input.getAttribute('form'), mutation.target);
|
|
226
|
+
if (_form) {
|
|
227
|
+
seenForms.add(_form);
|
|
205
228
|
}
|
|
206
229
|
}
|
|
207
230
|
}
|
|
@@ -214,6 +237,12 @@ function createGlobalFormsObserver() {
|
|
|
214
237
|
if (mutation.target.form) {
|
|
215
238
|
seenForms.add(mutation.target.form);
|
|
216
239
|
}
|
|
240
|
+
if (mutation.attributeName === 'form') {
|
|
241
|
+
var oldForm = getAssociatedFormElement(mutation.oldValue, mutation.target);
|
|
242
|
+
if (oldForm) {
|
|
243
|
+
seenForms.add(oldForm);
|
|
244
|
+
}
|
|
245
|
+
}
|
|
217
246
|
}
|
|
218
247
|
break;
|
|
219
248
|
}
|
|
@@ -297,69 +326,68 @@ function blur(element) {
|
|
|
297
326
|
}));
|
|
298
327
|
element.dispatchEvent(new FocusEvent('blur'));
|
|
299
328
|
}
|
|
300
|
-
function
|
|
301
|
-
if (typeof value === 'undefined')
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
if (value ===
|
|
305
|
-
return
|
|
329
|
+
function normalizeStringValues(value) {
|
|
330
|
+
if (typeof value === 'undefined') return undefined;
|
|
331
|
+
if (value === null) return [];
|
|
332
|
+
if (typeof value === 'string') return [value];
|
|
333
|
+
if (Array.isArray(value) && value.every(v => typeof v === 'string')) {
|
|
334
|
+
return Array.from(value);
|
|
306
335
|
}
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
if (
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
}
|
|
317
|
-
}
|
|
318
|
-
if (value instanceof FileList) {
|
|
319
|
-
return [null, value];
|
|
320
|
-
}
|
|
321
|
-
if (value instanceof File) {
|
|
322
|
-
return [null, createFileList([value])];
|
|
336
|
+
throw new Error('Expected string or string[] value for string based input');
|
|
337
|
+
}
|
|
338
|
+
function normalizeFileValues(value) {
|
|
339
|
+
if (typeof value === 'undefined') return undefined;
|
|
340
|
+
if (value === null) return createFileList([]);
|
|
341
|
+
if (isGlobalInstance(value, 'File')) return createFileList([value]);
|
|
342
|
+
if (isGlobalInstance(value, 'FileList')) return value;
|
|
343
|
+
if (Array.isArray(value) && value.every(item => isGlobalInstance(item, 'File'))) {
|
|
344
|
+
return createFileList(value);
|
|
323
345
|
}
|
|
324
|
-
|
|
346
|
+
throw new Error('Expected File, FileList or File[] for file input');
|
|
325
347
|
}
|
|
326
348
|
|
|
327
349
|
/**
|
|
328
350
|
* Updates the DOM element with the provided value and defaultValue.
|
|
351
|
+
* If the value or defaultValue is undefined, it will keep the current value instead
|
|
329
352
|
*/
|
|
330
353
|
function updateField(element, options) {
|
|
331
354
|
var _value$;
|
|
332
|
-
var [value, file] = normalizeFieldValue(options.value);
|
|
333
|
-
var [defaultValue] = normalizeFieldValue(options.defaultValue);
|
|
334
355
|
if (isInputElement(element)) {
|
|
335
356
|
switch (element.type) {
|
|
336
357
|
case 'file':
|
|
337
358
|
{
|
|
338
|
-
|
|
359
|
+
var files = normalizeFileValues(options.value);
|
|
360
|
+
if (files) {
|
|
361
|
+
element.files = files;
|
|
362
|
+
}
|
|
339
363
|
return;
|
|
340
364
|
}
|
|
341
365
|
case 'checkbox':
|
|
342
366
|
case 'radio':
|
|
343
367
|
{
|
|
344
|
-
|
|
345
|
-
|
|
368
|
+
var _value = normalizeStringValues(options.value);
|
|
369
|
+
var _defaultValue = normalizeStringValues(options.defaultValue);
|
|
370
|
+
if (_value) {
|
|
371
|
+
var checked = _value.includes(element.value);
|
|
346
372
|
if (element.type === 'checkbox' ? checked !== element.checked : checked) {
|
|
347
373
|
// Simulate a click to update the checked state
|
|
348
374
|
element.click();
|
|
349
375
|
}
|
|
350
376
|
element.checked = checked;
|
|
351
377
|
}
|
|
352
|
-
if (
|
|
353
|
-
element.defaultChecked =
|
|
378
|
+
if (_defaultValue) {
|
|
379
|
+
element.defaultChecked = _defaultValue.includes(element.value);
|
|
354
380
|
}
|
|
355
381
|
return;
|
|
356
382
|
}
|
|
357
383
|
}
|
|
358
384
|
} else if (isSelectElement(element)) {
|
|
359
|
-
var
|
|
385
|
+
var _value2 = normalizeStringValues(options.value);
|
|
386
|
+
var _defaultValue2 = normalizeStringValues(options.defaultValue);
|
|
387
|
+
var shouldUnselect = _value2 && _value2.length === 0;
|
|
360
388
|
for (var option of element.options) {
|
|
361
|
-
if (
|
|
362
|
-
var index =
|
|
389
|
+
if (_value2) {
|
|
390
|
+
var index = _value2.indexOf(option.value);
|
|
363
391
|
var selected = index > -1;
|
|
364
392
|
|
|
365
393
|
// Update the selected state of the option
|
|
@@ -369,11 +397,11 @@ function updateField(element, options) {
|
|
|
369
397
|
|
|
370
398
|
// Remove the option from the value array
|
|
371
399
|
if (selected) {
|
|
372
|
-
|
|
400
|
+
_value2.splice(index, 1);
|
|
373
401
|
}
|
|
374
402
|
}
|
|
375
|
-
if (
|
|
376
|
-
var _index =
|
|
403
|
+
if (_defaultValue2) {
|
|
404
|
+
var _index = _defaultValue2.indexOf(option.value);
|
|
377
405
|
var _selected = _index > -1;
|
|
378
406
|
|
|
379
407
|
// Update the selected state of the option
|
|
@@ -383,15 +411,15 @@ function updateField(element, options) {
|
|
|
383
411
|
|
|
384
412
|
// Remove the option from the defaultValue array
|
|
385
413
|
if (_selected) {
|
|
386
|
-
|
|
414
|
+
_defaultValue2.splice(_index, 1);
|
|
387
415
|
}
|
|
388
416
|
}
|
|
389
417
|
}
|
|
390
418
|
|
|
391
419
|
// We have already removed all selected options from the value and defaultValue array at this point
|
|
392
|
-
var missingOptions = new Set([...(
|
|
420
|
+
var missingOptions = new Set([...(_value2 !== null && _value2 !== void 0 ? _value2 : []), ...(_defaultValue2 !== null && _defaultValue2 !== void 0 ? _defaultValue2 : [])]);
|
|
393
421
|
for (var optionValue of missingOptions) {
|
|
394
|
-
element.options.add(new Option(optionValue, optionValue,
|
|
422
|
+
element.options.add(new Option(optionValue, optionValue, _defaultValue2 === null || _defaultValue2 === void 0 ? void 0 : _defaultValue2.includes(optionValue), _value2 === null || _value2 === void 0 ? void 0 : _value2.includes(optionValue)));
|
|
395
423
|
}
|
|
396
424
|
|
|
397
425
|
// If the select element is not multiple and the value is an empty array, unset the selected index
|
|
@@ -401,6 +429,8 @@ function updateField(element, options) {
|
|
|
401
429
|
}
|
|
402
430
|
return;
|
|
403
431
|
}
|
|
432
|
+
var value = normalizeStringValues(options.value);
|
|
433
|
+
var defaultValue = normalizeStringValues(options.defaultValue);
|
|
404
434
|
var inputValue = (_value$ = value === null || value === void 0 ? void 0 : value[0]) !== null && _value$ !== void 0 ? _value$ : '';
|
|
405
435
|
if (element.value !== inputValue) {
|
|
406
436
|
/**
|
|
@@ -418,12 +448,10 @@ function updateField(element, options) {
|
|
|
418
448
|
} = Object.getOwnPropertyDescriptor(prototype, 'value') || {};
|
|
419
449
|
if (prototypeValueSetter && valueSetter !== prototypeValueSetter) {
|
|
420
450
|
prototypeValueSetter.call(element, inputValue);
|
|
451
|
+
} else if (valueSetter) {
|
|
452
|
+
valueSetter.call(element, inputValue);
|
|
421
453
|
} else {
|
|
422
|
-
|
|
423
|
-
valueSetter.call(element, inputValue);
|
|
424
|
-
} else {
|
|
425
|
-
throw new Error('The given element does not have a value setter');
|
|
426
|
-
}
|
|
454
|
+
throw new Error('The given element does not have a value setter');
|
|
427
455
|
}
|
|
428
456
|
}
|
|
429
457
|
if (defaultValue) {
|
|
@@ -432,4 +460,4 @@ function updateField(element, options) {
|
|
|
432
460
|
}
|
|
433
461
|
}
|
|
434
462
|
|
|
435
|
-
export { blur, change, createFileList, createGlobalFormsObserver, focus, getFormAction, getFormEncType, getFormMethod, isFieldElement, isInputElement, isSelectElement, isTextAreaElement,
|
|
463
|
+
export { blur, change, createFileList, createGlobalFormsObserver, focus, getFormAction, getFormEncType, getFormMethod, isFieldElement, isInputElement, isSelectElement, isTextAreaElement, normalizeFileValues, normalizeStringValues, requestSubmit, updateField };
|
package/dist/formdata.d.ts
CHANGED
|
@@ -64,6 +64,141 @@ export declare function flatten(data: unknown, options?: {
|
|
|
64
64
|
resolve?: (data: unknown) => unknown;
|
|
65
65
|
prefix?: string;
|
|
66
66
|
}): Record<string, unknown>;
|
|
67
|
-
export declare function deepEqual
|
|
67
|
+
export declare function deepEqual(left: unknown, right: unknown): boolean;
|
|
68
|
+
export type JsonPrimitive = string | number | boolean | null;
|
|
69
|
+
/**
|
|
70
|
+
* The form value of a submission. This is usually constructed from a FormData or URLSearchParams.
|
|
71
|
+
* It may contains JSON primitives if the value is updated based on a form intent.
|
|
72
|
+
*/
|
|
73
|
+
export type FormValue<Type extends JsonPrimitive | FormDataEntryValue = JsonPrimitive | FormDataEntryValue> = Type | FormValue<Type | null>[] | {
|
|
74
|
+
[key: string]: FormValue<Type>;
|
|
75
|
+
};
|
|
76
|
+
/**
|
|
77
|
+
* The data of a form submission.
|
|
78
|
+
*/
|
|
79
|
+
export type Submission<ValueType extends FormDataEntryValue = FormDataEntryValue> = {
|
|
80
|
+
/**
|
|
81
|
+
* The form value structured following the naming convention.
|
|
82
|
+
*/
|
|
83
|
+
value: Record<string, FormValue<ValueType>>;
|
|
84
|
+
/**
|
|
85
|
+
* The field names that are included in the FormData or URLSearchParams.
|
|
86
|
+
*/
|
|
87
|
+
fields: string[];
|
|
88
|
+
/**
|
|
89
|
+
* The intent of the submission. This is usally included by specifying a name and value on a submit button.
|
|
90
|
+
*/
|
|
91
|
+
intent: string | null;
|
|
92
|
+
};
|
|
93
|
+
/**
|
|
94
|
+
* Parse `FormData` or `URLSearchParams` into a submission object.
|
|
95
|
+
* This function structures the form values based on the naming convention.
|
|
96
|
+
* It also includes all the field names and the intent if the `intentName` option is provided.
|
|
97
|
+
*
|
|
98
|
+
* @example
|
|
99
|
+
* ```ts
|
|
100
|
+
* const formData = new FormData();
|
|
101
|
+
*
|
|
102
|
+
* formData.append('email', 'test@example.com');
|
|
103
|
+
* formData.append('password', 'secret');
|
|
104
|
+
*
|
|
105
|
+
* parseSubmission(formData)
|
|
106
|
+
* // {
|
|
107
|
+
* // value: { email: 'test@example.com', password: 'secret' },
|
|
108
|
+
* // fields: ['email', 'password'],
|
|
109
|
+
* // intent: null,
|
|
110
|
+
* // }
|
|
111
|
+
*
|
|
112
|
+
* // If you have an intent field
|
|
113
|
+
* formData.append('intent', 'login');
|
|
114
|
+
* parseSubmission(formData, { intentName: 'intent' })
|
|
115
|
+
* // {
|
|
116
|
+
* // value: { email: 'test@example.com', password: 'secret' },
|
|
117
|
+
* // fields: ['email', 'password'],
|
|
118
|
+
* // intent: 'login',
|
|
119
|
+
* // }
|
|
120
|
+
* ```
|
|
121
|
+
*/
|
|
122
|
+
export declare function parseSubmission(formData: FormData | URLSearchParams, options?: {
|
|
123
|
+
/**
|
|
124
|
+
* The name of the submit button that triggered the form submission.
|
|
125
|
+
* Used to extract the submission's intent.
|
|
126
|
+
*/
|
|
127
|
+
intentName?: string;
|
|
128
|
+
/**
|
|
129
|
+
* A filter function that excludes specific entries from being parsed.
|
|
130
|
+
* Return `true` to skip the entry.
|
|
131
|
+
*/
|
|
132
|
+
skipEntry?: (name: string) => boolean;
|
|
133
|
+
}): Submission;
|
|
134
|
+
export type ParseSubmissionOptions = Required<Parameters<typeof parseSubmission>>[1];
|
|
135
|
+
export declare function defaultSerialize(value: unknown): FormDataEntryValue | undefined;
|
|
136
|
+
/**
|
|
137
|
+
* A utility function that checks whether the current form data differs from the default values.
|
|
138
|
+
*
|
|
139
|
+
* @see https://conform.guide/api/react/future/isDirty
|
|
140
|
+
* @example Enable a submit button only if the form is dirty
|
|
141
|
+
*
|
|
142
|
+
* ```tsx
|
|
143
|
+
* const dirty = useFormData(
|
|
144
|
+
* formRef,
|
|
145
|
+
* (formData) => isDirty(formData, { defaultValue }) ?? false,
|
|
146
|
+
* );
|
|
147
|
+
*
|
|
148
|
+
* return (
|
|
149
|
+
* <button type="submit" disabled={!dirty}>
|
|
150
|
+
* Save changes
|
|
151
|
+
* </button>
|
|
152
|
+
* );
|
|
153
|
+
* ```
|
|
154
|
+
*/
|
|
155
|
+
export declare function isDirty(
|
|
156
|
+
/**
|
|
157
|
+
* The current form data to compare. It can be:
|
|
158
|
+
*
|
|
159
|
+
* - A `FormData` object
|
|
160
|
+
* - A `URLSearchParams` object
|
|
161
|
+
* - A plain object that was parsed from form data (i.e. `submission.payload`)
|
|
162
|
+
*/
|
|
163
|
+
formData: FormData | URLSearchParams | FormValue<FormDataEntryValue> | null, options?: {
|
|
164
|
+
/**
|
|
165
|
+
* An object representing the default values of the form to compare against.
|
|
166
|
+
* Defaults to an empty object if not provided.
|
|
167
|
+
*/
|
|
168
|
+
defaultValue?: unknown;
|
|
169
|
+
/**
|
|
170
|
+
* The name of the submit button that triggered the submission.
|
|
171
|
+
* It will be excluded from the dirty comparison.
|
|
172
|
+
*/
|
|
173
|
+
intentName?: string;
|
|
174
|
+
/**
|
|
175
|
+
* A function to serialize values in defaultValue before comparing them to the form data.
|
|
176
|
+
* If not provided, a default serializer is used that behaves as follows:
|
|
177
|
+
*
|
|
178
|
+
* - string / File:
|
|
179
|
+
* - Returned as-is
|
|
180
|
+
* - boolean:
|
|
181
|
+
* - true → 'on'
|
|
182
|
+
* - false → undefined
|
|
183
|
+
* - number / bigint:
|
|
184
|
+
* - Converted to string using `.toString()`
|
|
185
|
+
* - Date:
|
|
186
|
+
* - Converted to ISO string using `.toISOString()`
|
|
187
|
+
*/
|
|
188
|
+
serialize?: (value: unknown, defaultSerialize: (value: unknown) => FormDataEntryValue | undefined) => FormDataEntryValue | undefined;
|
|
189
|
+
/**
|
|
190
|
+
* A function to exclude specific fields from the comparison.
|
|
191
|
+
* Useful for ignoring hidden inputs like CSRF tokens or internal fields added by frameworks
|
|
192
|
+
* (e.g. Next.js uses hidden inputs to support server actions).
|
|
193
|
+
*
|
|
194
|
+
* @example
|
|
195
|
+
* ```ts
|
|
196
|
+
* isDirty(formData, {
|
|
197
|
+
* skipEntry: (name) => name === 'csrf-token',
|
|
198
|
+
* });
|
|
199
|
+
* ```
|
|
200
|
+
*/
|
|
201
|
+
skipEntry?: (name: string) => boolean;
|
|
202
|
+
}): boolean | undefined;
|
|
68
203
|
export {};
|
|
69
204
|
//# sourceMappingURL=formdata.d.ts.map
|
package/dist/formdata.js
CHANGED
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
Object.defineProperty(exports, '__esModule', { value: true });
|
|
4
4
|
|
|
5
|
+
var submission = require('./submission.js');
|
|
6
|
+
|
|
5
7
|
/**
|
|
6
8
|
* Construct a form data with the submitter value.
|
|
7
9
|
* It utilizes the submitter argument on the FormData constructor from modern browsers
|
|
@@ -104,11 +106,11 @@ function setValue(target, name, valueFn) {
|
|
|
104
106
|
var index = -1;
|
|
105
107
|
var pointer = target;
|
|
106
108
|
while (pointer != null && ++index < length) {
|
|
107
|
-
var
|
|
109
|
+
var _key = paths[index];
|
|
108
110
|
var nextKey = paths[index + 1];
|
|
109
|
-
var newValue = index != lastIndex ? Object.prototype.hasOwnProperty.call(pointer,
|
|
110
|
-
pointer[
|
|
111
|
-
pointer = pointer[
|
|
111
|
+
var newValue = index != lastIndex ? Object.prototype.hasOwnProperty.call(pointer, _key) && pointer[_key] !== null ? pointer[_key] : typeof nextKey === 'number' ? [] : {} : valueFn(pointer[_key]);
|
|
112
|
+
pointer[_key] = newValue;
|
|
113
|
+
pointer = pointer[_key];
|
|
112
114
|
}
|
|
113
115
|
}
|
|
114
116
|
|
|
@@ -195,8 +197,8 @@ function flatten(data) {
|
|
|
195
197
|
process(data[i], "".concat(prefix, "[").concat(i, "]"));
|
|
196
198
|
}
|
|
197
199
|
} else if (isPlainObject(data)) {
|
|
198
|
-
for (var [
|
|
199
|
-
process(_value, prefix ? "".concat(prefix, ".").concat(
|
|
200
|
+
for (var [_key2, _value] of Object.entries(data)) {
|
|
201
|
+
process(_value, prefix ? "".concat(prefix, ".").concat(_key2) : _key2);
|
|
200
202
|
}
|
|
201
203
|
}
|
|
202
204
|
}
|
|
@@ -206,34 +208,36 @@ function flatten(data) {
|
|
|
206
208
|
}
|
|
207
209
|
return result;
|
|
208
210
|
}
|
|
209
|
-
function deepEqual(
|
|
210
|
-
if (
|
|
211
|
+
function deepEqual(left, right) {
|
|
212
|
+
if (Object.is(left, right)) {
|
|
211
213
|
return true;
|
|
212
214
|
}
|
|
213
|
-
if (
|
|
215
|
+
if (left == null || right == null) {
|
|
214
216
|
return false;
|
|
215
217
|
}
|
|
216
|
-
|
|
217
|
-
|
|
218
|
+
|
|
219
|
+
// Compare plain objects
|
|
220
|
+
if (isPlainObject(left) && isPlainObject(right)) {
|
|
221
|
+
var prevKeys = Object.keys(left);
|
|
222
|
+
var nextKeys = Object.keys(right);
|
|
223
|
+
if (prevKeys.length !== nextKeys.length) {
|
|
218
224
|
return false;
|
|
219
225
|
}
|
|
220
|
-
for (var
|
|
221
|
-
if (!deepEqual(
|
|
226
|
+
for (var _key3 of prevKeys) {
|
|
227
|
+
if (!Object.prototype.hasOwnProperty.call(right, _key3) || !deepEqual(left[_key3], right[_key3])) {
|
|
222
228
|
return false;
|
|
223
229
|
}
|
|
224
230
|
}
|
|
225
231
|
return true;
|
|
226
232
|
}
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
if (
|
|
233
|
+
|
|
234
|
+
// Compare arrays
|
|
235
|
+
if (Array.isArray(left) && Array.isArray(right)) {
|
|
236
|
+
if (left.length !== right.length) {
|
|
231
237
|
return false;
|
|
232
238
|
}
|
|
233
|
-
for (var
|
|
234
|
-
if (!
|
|
235
|
-
// @ts-expect-error FIXME
|
|
236
|
-
!deepEqual(prev[key], next[key])) {
|
|
239
|
+
for (var i = 0; i < left.length; i++) {
|
|
240
|
+
if (!deepEqual(left[i], right[i])) {
|
|
237
241
|
return false;
|
|
238
242
|
}
|
|
239
243
|
}
|
|
@@ -242,7 +246,170 @@ function deepEqual(prev, next) {
|
|
|
242
246
|
return false;
|
|
243
247
|
}
|
|
244
248
|
|
|
249
|
+
/**
|
|
250
|
+
* The form value of a submission. This is usually constructed from a FormData or URLSearchParams.
|
|
251
|
+
* It may contains JSON primitives if the value is updated based on a form intent.
|
|
252
|
+
*/
|
|
253
|
+
|
|
254
|
+
/**
|
|
255
|
+
* The data of a form submission.
|
|
256
|
+
*/
|
|
257
|
+
|
|
258
|
+
/**
|
|
259
|
+
* Parse `FormData` or `URLSearchParams` into a submission object.
|
|
260
|
+
* This function structures the form values based on the naming convention.
|
|
261
|
+
* It also includes all the field names and the intent if the `intentName` option is provided.
|
|
262
|
+
*
|
|
263
|
+
* @example
|
|
264
|
+
* ```ts
|
|
265
|
+
* const formData = new FormData();
|
|
266
|
+
*
|
|
267
|
+
* formData.append('email', 'test@example.com');
|
|
268
|
+
* formData.append('password', 'secret');
|
|
269
|
+
*
|
|
270
|
+
* parseSubmission(formData)
|
|
271
|
+
* // {
|
|
272
|
+
* // value: { email: 'test@example.com', password: 'secret' },
|
|
273
|
+
* // fields: ['email', 'password'],
|
|
274
|
+
* // intent: null,
|
|
275
|
+
* // }
|
|
276
|
+
*
|
|
277
|
+
* // If you have an intent field
|
|
278
|
+
* formData.append('intent', 'login');
|
|
279
|
+
* parseSubmission(formData, { intentName: 'intent' })
|
|
280
|
+
* // {
|
|
281
|
+
* // value: { email: 'test@example.com', password: 'secret' },
|
|
282
|
+
* // fields: ['email', 'password'],
|
|
283
|
+
* // intent: 'login',
|
|
284
|
+
* // }
|
|
285
|
+
* ```
|
|
286
|
+
*/
|
|
287
|
+
function parseSubmission(formData, options) {
|
|
288
|
+
var _options$intentName;
|
|
289
|
+
var intentName = (_options$intentName = options === null || options === void 0 ? void 0 : options.intentName) !== null && _options$intentName !== void 0 ? _options$intentName : submission.INTENT;
|
|
290
|
+
var submission$1 = {
|
|
291
|
+
value: {},
|
|
292
|
+
fields: [],
|
|
293
|
+
intent: null
|
|
294
|
+
};
|
|
295
|
+
var _loop = function _loop() {
|
|
296
|
+
var _options$skipEntry;
|
|
297
|
+
if (_name !== intentName && !(options !== null && options !== void 0 && (_options$skipEntry = options.skipEntry) !== null && _options$skipEntry !== void 0 && _options$skipEntry.call(options, _name))) {
|
|
298
|
+
var _value2 = formData.getAll(_name);
|
|
299
|
+
setValue(submission$1.value, _name, () => _value2.length > 1 ? _value2 : _value2[0]);
|
|
300
|
+
submission$1.fields.push(_name);
|
|
301
|
+
}
|
|
302
|
+
};
|
|
303
|
+
for (var _name of new Set(formData.keys())) {
|
|
304
|
+
_loop();
|
|
305
|
+
}
|
|
306
|
+
if (intentName) {
|
|
307
|
+
// We take the first value of the intent field if it exists.
|
|
308
|
+
var intent = formData.get(intentName);
|
|
309
|
+
if (typeof intent === 'string') {
|
|
310
|
+
submission$1.intent = intent;
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
return submission$1;
|
|
314
|
+
}
|
|
315
|
+
function defaultSerialize(value) {
|
|
316
|
+
if (typeof value === 'string' || isGlobalInstance(value, 'File')) {
|
|
317
|
+
return value;
|
|
318
|
+
}
|
|
319
|
+
if (typeof value === 'boolean') {
|
|
320
|
+
return value ? 'on' : undefined;
|
|
321
|
+
}
|
|
322
|
+
if (value instanceof Date) {
|
|
323
|
+
return value.toISOString();
|
|
324
|
+
}
|
|
325
|
+
return value === null || value === void 0 ? void 0 : value.toString();
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
/**
|
|
329
|
+
* A utility function that checks whether the current form data differs from the default values.
|
|
330
|
+
*
|
|
331
|
+
* @see https://conform.guide/api/react/future/isDirty
|
|
332
|
+
* @example Enable a submit button only if the form is dirty
|
|
333
|
+
*
|
|
334
|
+
* ```tsx
|
|
335
|
+
* const dirty = useFormData(
|
|
336
|
+
* formRef,
|
|
337
|
+
* (formData) => isDirty(formData, { defaultValue }) ?? false,
|
|
338
|
+
* );
|
|
339
|
+
*
|
|
340
|
+
* return (
|
|
341
|
+
* <button type="submit" disabled={!dirty}>
|
|
342
|
+
* Save changes
|
|
343
|
+
* </button>
|
|
344
|
+
* );
|
|
345
|
+
* ```
|
|
346
|
+
*/
|
|
347
|
+
function isDirty(
|
|
348
|
+
/**
|
|
349
|
+
* The current form data to compare. It can be:
|
|
350
|
+
*
|
|
351
|
+
* - A `FormData` object
|
|
352
|
+
* - A `URLSearchParams` object
|
|
353
|
+
* - A plain object that was parsed from form data (i.e. `submission.payload`)
|
|
354
|
+
*/
|
|
355
|
+
formData, options) {
|
|
356
|
+
var _options$serialize;
|
|
357
|
+
if (!formData) {
|
|
358
|
+
return;
|
|
359
|
+
}
|
|
360
|
+
var formValue = formData instanceof FormData || formData instanceof URLSearchParams ? parseSubmission(formData, {
|
|
361
|
+
intentName: options === null || options === void 0 ? void 0 : options.intentName,
|
|
362
|
+
skipEntry: options === null || options === void 0 ? void 0 : options.skipEntry
|
|
363
|
+
}).value : formData;
|
|
364
|
+
var defaultValue = options === null || options === void 0 ? void 0 : options.defaultValue;
|
|
365
|
+
var serialize = (_options$serialize = options === null || options === void 0 ? void 0 : options.serialize) !== null && _options$serialize !== void 0 ? _options$serialize : defaultSerialize;
|
|
366
|
+
function normalize(value) {
|
|
367
|
+
if (Array.isArray(value)) {
|
|
368
|
+
if (value.length === 0) {
|
|
369
|
+
return undefined;
|
|
370
|
+
}
|
|
371
|
+
var array = value.map(normalize);
|
|
372
|
+
if (array.length === 1 && (typeof array[0] === 'string' || array[0] === undefined)) {
|
|
373
|
+
return array[0];
|
|
374
|
+
}
|
|
375
|
+
return array;
|
|
376
|
+
}
|
|
377
|
+
if (isPlainObject(value)) {
|
|
378
|
+
var entries = Object.entries(value).reduce((list, _ref) => {
|
|
379
|
+
var [key, value] = _ref;
|
|
380
|
+
var normalizedValue = normalize(value);
|
|
381
|
+
if (typeof normalizedValue !== 'undefined') {
|
|
382
|
+
list.push([key, normalizedValue]);
|
|
383
|
+
}
|
|
384
|
+
return list;
|
|
385
|
+
}, []);
|
|
386
|
+
if (entries.length === 0) {
|
|
387
|
+
return undefined;
|
|
388
|
+
}
|
|
389
|
+
return Object.fromEntries(entries);
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
// If the value is null or undefined, treat it as undefined
|
|
393
|
+
if (value == null) {
|
|
394
|
+
return undefined;
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
// Removes empty strings, so that bpth empty string and undefined are treated as the same
|
|
398
|
+
if (typeof value === 'string' && value === '') {
|
|
399
|
+
return undefined;
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
// Remove empty File as well, which happens if no File was selected
|
|
403
|
+
if (isGlobalInstance(value, 'File') && value.name === '' && value.size === 0) {
|
|
404
|
+
return undefined;
|
|
405
|
+
}
|
|
406
|
+
return serialize(value, defaultSerialize);
|
|
407
|
+
}
|
|
408
|
+
return !deepEqual(normalize(formValue), normalize(defaultValue));
|
|
409
|
+
}
|
|
410
|
+
|
|
245
411
|
exports.deepEqual = deepEqual;
|
|
412
|
+
exports.defaultSerialize = defaultSerialize;
|
|
246
413
|
exports.flatten = flatten;
|
|
247
414
|
exports.formatName = formatName;
|
|
248
415
|
exports.formatPaths = formatPaths;
|
|
@@ -250,8 +417,10 @@ exports.getChildPaths = getChildPaths;
|
|
|
250
417
|
exports.getFormData = getFormData;
|
|
251
418
|
exports.getPaths = getPaths;
|
|
252
419
|
exports.getValue = getValue;
|
|
420
|
+
exports.isDirty = isDirty;
|
|
253
421
|
exports.isGlobalInstance = isGlobalInstance;
|
|
254
422
|
exports.isPlainObject = isPlainObject;
|
|
255
423
|
exports.isPrefix = isPrefix;
|
|
256
424
|
exports.normalize = normalize;
|
|
425
|
+
exports.parseSubmission = parseSubmission;
|
|
257
426
|
exports.setValue = setValue;
|
package/dist/formdata.mjs
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { INTENT } from './submission.mjs';
|
|
2
|
+
|
|
1
3
|
/**
|
|
2
4
|
* Construct a form data with the submitter value.
|
|
3
5
|
* It utilizes the submitter argument on the FormData constructor from modern browsers
|
|
@@ -100,11 +102,11 @@ function setValue(target, name, valueFn) {
|
|
|
100
102
|
var index = -1;
|
|
101
103
|
var pointer = target;
|
|
102
104
|
while (pointer != null && ++index < length) {
|
|
103
|
-
var
|
|
105
|
+
var _key = paths[index];
|
|
104
106
|
var nextKey = paths[index + 1];
|
|
105
|
-
var newValue = index != lastIndex ? Object.prototype.hasOwnProperty.call(pointer,
|
|
106
|
-
pointer[
|
|
107
|
-
pointer = pointer[
|
|
107
|
+
var newValue = index != lastIndex ? Object.prototype.hasOwnProperty.call(pointer, _key) && pointer[_key] !== null ? pointer[_key] : typeof nextKey === 'number' ? [] : {} : valueFn(pointer[_key]);
|
|
108
|
+
pointer[_key] = newValue;
|
|
109
|
+
pointer = pointer[_key];
|
|
108
110
|
}
|
|
109
111
|
}
|
|
110
112
|
|
|
@@ -191,8 +193,8 @@ function flatten(data) {
|
|
|
191
193
|
process(data[i], "".concat(prefix, "[").concat(i, "]"));
|
|
192
194
|
}
|
|
193
195
|
} else if (isPlainObject(data)) {
|
|
194
|
-
for (var [
|
|
195
|
-
process(_value, prefix ? "".concat(prefix, ".").concat(
|
|
196
|
+
for (var [_key2, _value] of Object.entries(data)) {
|
|
197
|
+
process(_value, prefix ? "".concat(prefix, ".").concat(_key2) : _key2);
|
|
196
198
|
}
|
|
197
199
|
}
|
|
198
200
|
}
|
|
@@ -202,34 +204,36 @@ function flatten(data) {
|
|
|
202
204
|
}
|
|
203
205
|
return result;
|
|
204
206
|
}
|
|
205
|
-
function deepEqual(
|
|
206
|
-
if (
|
|
207
|
+
function deepEqual(left, right) {
|
|
208
|
+
if (Object.is(left, right)) {
|
|
207
209
|
return true;
|
|
208
210
|
}
|
|
209
|
-
if (
|
|
211
|
+
if (left == null || right == null) {
|
|
210
212
|
return false;
|
|
211
213
|
}
|
|
212
|
-
|
|
213
|
-
|
|
214
|
+
|
|
215
|
+
// Compare plain objects
|
|
216
|
+
if (isPlainObject(left) && isPlainObject(right)) {
|
|
217
|
+
var prevKeys = Object.keys(left);
|
|
218
|
+
var nextKeys = Object.keys(right);
|
|
219
|
+
if (prevKeys.length !== nextKeys.length) {
|
|
214
220
|
return false;
|
|
215
221
|
}
|
|
216
|
-
for (var
|
|
217
|
-
if (!deepEqual(
|
|
222
|
+
for (var _key3 of prevKeys) {
|
|
223
|
+
if (!Object.prototype.hasOwnProperty.call(right, _key3) || !deepEqual(left[_key3], right[_key3])) {
|
|
218
224
|
return false;
|
|
219
225
|
}
|
|
220
226
|
}
|
|
221
227
|
return true;
|
|
222
228
|
}
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
if (
|
|
229
|
+
|
|
230
|
+
// Compare arrays
|
|
231
|
+
if (Array.isArray(left) && Array.isArray(right)) {
|
|
232
|
+
if (left.length !== right.length) {
|
|
227
233
|
return false;
|
|
228
234
|
}
|
|
229
|
-
for (var
|
|
230
|
-
if (!
|
|
231
|
-
// @ts-expect-error FIXME
|
|
232
|
-
!deepEqual(prev[key], next[key])) {
|
|
235
|
+
for (var i = 0; i < left.length; i++) {
|
|
236
|
+
if (!deepEqual(left[i], right[i])) {
|
|
233
237
|
return false;
|
|
234
238
|
}
|
|
235
239
|
}
|
|
@@ -238,4 +242,166 @@ function deepEqual(prev, next) {
|
|
|
238
242
|
return false;
|
|
239
243
|
}
|
|
240
244
|
|
|
241
|
-
|
|
245
|
+
/**
|
|
246
|
+
* The form value of a submission. This is usually constructed from a FormData or URLSearchParams.
|
|
247
|
+
* It may contains JSON primitives if the value is updated based on a form intent.
|
|
248
|
+
*/
|
|
249
|
+
|
|
250
|
+
/**
|
|
251
|
+
* The data of a form submission.
|
|
252
|
+
*/
|
|
253
|
+
|
|
254
|
+
/**
|
|
255
|
+
* Parse `FormData` or `URLSearchParams` into a submission object.
|
|
256
|
+
* This function structures the form values based on the naming convention.
|
|
257
|
+
* It also includes all the field names and the intent if the `intentName` option is provided.
|
|
258
|
+
*
|
|
259
|
+
* @example
|
|
260
|
+
* ```ts
|
|
261
|
+
* const formData = new FormData();
|
|
262
|
+
*
|
|
263
|
+
* formData.append('email', 'test@example.com');
|
|
264
|
+
* formData.append('password', 'secret');
|
|
265
|
+
*
|
|
266
|
+
* parseSubmission(formData)
|
|
267
|
+
* // {
|
|
268
|
+
* // value: { email: 'test@example.com', password: 'secret' },
|
|
269
|
+
* // fields: ['email', 'password'],
|
|
270
|
+
* // intent: null,
|
|
271
|
+
* // }
|
|
272
|
+
*
|
|
273
|
+
* // If you have an intent field
|
|
274
|
+
* formData.append('intent', 'login');
|
|
275
|
+
* parseSubmission(formData, { intentName: 'intent' })
|
|
276
|
+
* // {
|
|
277
|
+
* // value: { email: 'test@example.com', password: 'secret' },
|
|
278
|
+
* // fields: ['email', 'password'],
|
|
279
|
+
* // intent: 'login',
|
|
280
|
+
* // }
|
|
281
|
+
* ```
|
|
282
|
+
*/
|
|
283
|
+
function parseSubmission(formData, options) {
|
|
284
|
+
var _options$intentName;
|
|
285
|
+
var intentName = (_options$intentName = options === null || options === void 0 ? void 0 : options.intentName) !== null && _options$intentName !== void 0 ? _options$intentName : INTENT;
|
|
286
|
+
var submission = {
|
|
287
|
+
value: {},
|
|
288
|
+
fields: [],
|
|
289
|
+
intent: null
|
|
290
|
+
};
|
|
291
|
+
var _loop = function _loop() {
|
|
292
|
+
var _options$skipEntry;
|
|
293
|
+
if (_name !== intentName && !(options !== null && options !== void 0 && (_options$skipEntry = options.skipEntry) !== null && _options$skipEntry !== void 0 && _options$skipEntry.call(options, _name))) {
|
|
294
|
+
var _value2 = formData.getAll(_name);
|
|
295
|
+
setValue(submission.value, _name, () => _value2.length > 1 ? _value2 : _value2[0]);
|
|
296
|
+
submission.fields.push(_name);
|
|
297
|
+
}
|
|
298
|
+
};
|
|
299
|
+
for (var _name of new Set(formData.keys())) {
|
|
300
|
+
_loop();
|
|
301
|
+
}
|
|
302
|
+
if (intentName) {
|
|
303
|
+
// We take the first value of the intent field if it exists.
|
|
304
|
+
var intent = formData.get(intentName);
|
|
305
|
+
if (typeof intent === 'string') {
|
|
306
|
+
submission.intent = intent;
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
return submission;
|
|
310
|
+
}
|
|
311
|
+
function defaultSerialize(value) {
|
|
312
|
+
if (typeof value === 'string' || isGlobalInstance(value, 'File')) {
|
|
313
|
+
return value;
|
|
314
|
+
}
|
|
315
|
+
if (typeof value === 'boolean') {
|
|
316
|
+
return value ? 'on' : undefined;
|
|
317
|
+
}
|
|
318
|
+
if (value instanceof Date) {
|
|
319
|
+
return value.toISOString();
|
|
320
|
+
}
|
|
321
|
+
return value === null || value === void 0 ? void 0 : value.toString();
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
/**
|
|
325
|
+
* A utility function that checks whether the current form data differs from the default values.
|
|
326
|
+
*
|
|
327
|
+
* @see https://conform.guide/api/react/future/isDirty
|
|
328
|
+
* @example Enable a submit button only if the form is dirty
|
|
329
|
+
*
|
|
330
|
+
* ```tsx
|
|
331
|
+
* const dirty = useFormData(
|
|
332
|
+
* formRef,
|
|
333
|
+
* (formData) => isDirty(formData, { defaultValue }) ?? false,
|
|
334
|
+
* );
|
|
335
|
+
*
|
|
336
|
+
* return (
|
|
337
|
+
* <button type="submit" disabled={!dirty}>
|
|
338
|
+
* Save changes
|
|
339
|
+
* </button>
|
|
340
|
+
* );
|
|
341
|
+
* ```
|
|
342
|
+
*/
|
|
343
|
+
function isDirty(
|
|
344
|
+
/**
|
|
345
|
+
* The current form data to compare. It can be:
|
|
346
|
+
*
|
|
347
|
+
* - A `FormData` object
|
|
348
|
+
* - A `URLSearchParams` object
|
|
349
|
+
* - A plain object that was parsed from form data (i.e. `submission.payload`)
|
|
350
|
+
*/
|
|
351
|
+
formData, options) {
|
|
352
|
+
var _options$serialize;
|
|
353
|
+
if (!formData) {
|
|
354
|
+
return;
|
|
355
|
+
}
|
|
356
|
+
var formValue = formData instanceof FormData || formData instanceof URLSearchParams ? parseSubmission(formData, {
|
|
357
|
+
intentName: options === null || options === void 0 ? void 0 : options.intentName,
|
|
358
|
+
skipEntry: options === null || options === void 0 ? void 0 : options.skipEntry
|
|
359
|
+
}).value : formData;
|
|
360
|
+
var defaultValue = options === null || options === void 0 ? void 0 : options.defaultValue;
|
|
361
|
+
var serialize = (_options$serialize = options === null || options === void 0 ? void 0 : options.serialize) !== null && _options$serialize !== void 0 ? _options$serialize : defaultSerialize;
|
|
362
|
+
function normalize(value) {
|
|
363
|
+
if (Array.isArray(value)) {
|
|
364
|
+
if (value.length === 0) {
|
|
365
|
+
return undefined;
|
|
366
|
+
}
|
|
367
|
+
var array = value.map(normalize);
|
|
368
|
+
if (array.length === 1 && (typeof array[0] === 'string' || array[0] === undefined)) {
|
|
369
|
+
return array[0];
|
|
370
|
+
}
|
|
371
|
+
return array;
|
|
372
|
+
}
|
|
373
|
+
if (isPlainObject(value)) {
|
|
374
|
+
var entries = Object.entries(value).reduce((list, _ref) => {
|
|
375
|
+
var [key, value] = _ref;
|
|
376
|
+
var normalizedValue = normalize(value);
|
|
377
|
+
if (typeof normalizedValue !== 'undefined') {
|
|
378
|
+
list.push([key, normalizedValue]);
|
|
379
|
+
}
|
|
380
|
+
return list;
|
|
381
|
+
}, []);
|
|
382
|
+
if (entries.length === 0) {
|
|
383
|
+
return undefined;
|
|
384
|
+
}
|
|
385
|
+
return Object.fromEntries(entries);
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
// If the value is null or undefined, treat it as undefined
|
|
389
|
+
if (value == null) {
|
|
390
|
+
return undefined;
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
// Removes empty strings, so that bpth empty string and undefined are treated as the same
|
|
394
|
+
if (typeof value === 'string' && value === '') {
|
|
395
|
+
return undefined;
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
// Remove empty File as well, which happens if no File was selected
|
|
399
|
+
if (isGlobalInstance(value, 'File') && value.name === '' && value.size === 0) {
|
|
400
|
+
return undefined;
|
|
401
|
+
}
|
|
402
|
+
return serialize(value, defaultSerialize);
|
|
403
|
+
}
|
|
404
|
+
return !deepEqual(normalize(formValue), normalize(defaultValue));
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
export { deepEqual, defaultSerialize, flatten, formatName, formatPaths, getChildPaths, getFormData, getPaths, getValue, isDirty, isGlobalInstance, isPlainObject, isPrefix, normalize, parseSubmission, setValue };
|
package/dist/index.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
export { type Combine, type Constraint, type ControlButtonProps, type FormId, type FieldName, type DefaultValue, type FormValue, type FormOptions, type FormState, type FormContext, type SubscriptionSubject, type SubscriptionScope, createFormContext as unstable_createFormContext, } from './form';
|
|
2
2
|
export { type FieldElement, isFieldElement, updateField as unstable_updateField, createFileList, createGlobalFormsObserver as unstable_createGlobalFormsObserver, focus as unstable_focus, change as unstable_change, blur as unstable_blur, } from './dom';
|
|
3
3
|
export { type Submission, type SubmissionResult, type Intent, INTENT, STATE, serializeIntent, parse, } from './submission';
|
|
4
|
-
export { getPaths, formatPaths, isPrefix, isGlobalInstance, deepEqual as unstable_deepEqual, } from './formdata';
|
|
4
|
+
export { getFormData, getPaths, formatPaths, isPrefix, isGlobalInstance, deepEqual as unstable_deepEqual, isDirty as unstable_isDirty, } from './formdata';
|
|
5
5
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.js
CHANGED
|
@@ -22,7 +22,9 @@ exports.STATE = submission.STATE;
|
|
|
22
22
|
exports.parse = submission.parse;
|
|
23
23
|
exports.serializeIntent = submission.serializeIntent;
|
|
24
24
|
exports.formatPaths = formdata.formatPaths;
|
|
25
|
+
exports.getFormData = formdata.getFormData;
|
|
25
26
|
exports.getPaths = formdata.getPaths;
|
|
26
27
|
exports.isGlobalInstance = formdata.isGlobalInstance;
|
|
27
28
|
exports.isPrefix = formdata.isPrefix;
|
|
28
29
|
exports.unstable_deepEqual = formdata.deepEqual;
|
|
30
|
+
exports.unstable_isDirty = formdata.isDirty;
|
package/dist/index.mjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
export { createFormContext as unstable_createFormContext } from './form.mjs';
|
|
2
2
|
export { createFileList, isFieldElement, blur as unstable_blur, change as unstable_change, createGlobalFormsObserver as unstable_createGlobalFormsObserver, focus as unstable_focus, updateField as unstable_updateField } from './dom.mjs';
|
|
3
3
|
export { INTENT, STATE, parse, serializeIntent } from './submission.mjs';
|
|
4
|
-
export { formatPaths, getPaths, isGlobalInstance, isPrefix, deepEqual as unstable_deepEqual } from './formdata.mjs';
|
|
4
|
+
export { formatPaths, getFormData, getPaths, isGlobalInstance, isPrefix, deepEqual as unstable_deepEqual, isDirty as unstable_isDirty } from './formdata.mjs';
|
package/package.json
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
"description": "A set of opinionated helpers built on top of the Constraint Validation API",
|
|
4
4
|
"homepage": "https://conform.guide",
|
|
5
5
|
"license": "MIT",
|
|
6
|
-
"version": "1.
|
|
6
|
+
"version": "1.8.1",
|
|
7
7
|
"main": "./dist/index.js",
|
|
8
8
|
"module": "./dist/index.mjs",
|
|
9
9
|
"types": "./dist/index.d.ts",
|