@conform-to/dom 1.8.2 → 1.9.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/dist/formdata.mjs CHANGED
@@ -1,4 +1,8 @@
1
- import { INTENT } from './submission.mjs';
1
+ import { objectSpread2 as _objectSpread2 } from './_virtual/_rollupPluginBabelHelpers.mjs';
2
+ import { isSubmitter, isGlobalInstance } from './dom.mjs';
3
+ import { isPlainObject, stripFiles, deepEqual } from './util.mjs';
4
+
5
+ var DEFAULT_INTENT_NAME = '__INTENT__';
2
6
 
3
7
  /**
4
8
  * Construct a form data with the submitter value.
@@ -9,253 +13,231 @@ import { INTENT } from './submission.mjs';
9
13
  */
10
14
  function getFormData(form, submitter) {
11
15
  var payload = new FormData(form, submitter);
12
- if (submitter && submitter.type === 'submit' && submitter.name !== '') {
13
- var entries = payload.getAll(submitter.name);
16
+ if (submitter) {
17
+ if (!isSubmitter(submitter)) {
18
+ throw new TypeError('The submitter must be an input or button element with type submit.');
19
+ }
20
+ if (submitter.name) {
21
+ var entries = payload.getAll(submitter.name);
14
22
 
15
- // This assumes the submitter value to be always unique, which should be fine in most cases
16
- if (!entries.includes(submitter.value)) {
17
- payload.append(submitter.name, submitter.value);
23
+ // This assumes the submitter value to be always unique, which should be fine in most cases
24
+ if (!entries.includes(submitter.value)) {
25
+ payload.append(submitter.name, submitter.value);
26
+ }
18
27
  }
19
28
  }
20
29
  return payload;
21
30
  }
22
31
 
23
32
  /**
24
- * Returns the paths from a name based on the JS syntax convention
33
+ * Convert a string path into an array of segments.
34
+ *
25
35
  * @example
26
36
  * ```js
27
- * const paths = getPaths('todos[0].content'); // ['todos', 0, 'content']
37
+ * getPathSegments("object.key"); // ['object', 'key']
38
+ * getPathSegments("array[0].content"); // → ['array', 0, 'content']
39
+ * getPathSegments("todos[]"); // → ['todos', '']
28
40
  * ```
29
41
  */
30
- function getPaths(name) {
31
- if (!name) {
32
- return [];
33
- }
34
- return name.split(/\.|(\[\d*\])/).reduce((result, segment) => {
35
- if (typeof segment !== 'undefined' && segment !== '' && segment !== '__proto__' && segment !== 'constructor' && segment !== 'prototype') {
36
- if (segment.startsWith('[') && segment.endsWith(']')) {
37
- var index = segment.slice(1, -1);
38
- result.push(Number(index));
39
- } else {
40
- result.push(segment);
42
+ function getPathSegments(path) {
43
+ if (!path) return [];
44
+ var tokenRegex = /([^.[\]]+)|\[(\d*)\]/g;
45
+ var segments = [];
46
+ var lastIndex = 0,
47
+ match;
48
+ while (match = tokenRegex.exec(path)) {
49
+ // allow a single “.” between tokens
50
+ if (match.index !== lastIndex) {
51
+ if (!(match.index === lastIndex + 1 && path[lastIndex] === '.')) {
52
+ throw new Error("Invalid path syntax at position ".concat(lastIndex, " in \"").concat(path, "\""));
41
53
  }
42
54
  }
43
- return result;
44
- }, []);
55
+ var [, key, index] = match;
56
+ if (key !== undefined) {
57
+ if (key === '__proto__' || key === 'constructor') {
58
+ throw new Error("Invalid path segment \"".concat(key, "\""));
59
+ }
60
+ segments.push(key);
61
+ } else if (index === '') {
62
+ segments.push('');
63
+ } else {
64
+ var number = Number(index);
65
+ if (!Number.isInteger(number) || number < 0) {
66
+ throw new Error("Invalid path segment: array index must be a non-negative integer, got ".concat(number));
67
+ }
68
+ segments.push(number);
69
+ }
70
+ lastIndex = tokenRegex.lastIndex;
71
+ }
72
+ if (lastIndex !== path.length) {
73
+ throw new Error("Invalid path syntax at position ".concat(lastIndex, " in \"").concat(path, "\""));
74
+ }
75
+ return segments;
45
76
  }
46
77
 
47
78
  /**
48
- * Returns a formatted name from the paths based on the JS syntax convention
79
+ * Returns a formatted name from the path segments based on the dot and bracket notation.
80
+ *
49
81
  * @example
50
82
  * ```js
51
- * const name = formatPaths(['todos', 0, 'content']); // "todos[0].content"
83
+ * formatPathSegments(['object', 'key']); // "object.key"
84
+ * formatPathSegments(['array', 0, 'content']); // → "array[0].content"
85
+ * formatPathSegments(['todos', '']); // → "todos[]"
52
86
  * ```
53
87
  */
54
- function formatPaths(paths) {
55
- return paths.reduce((name, path) => {
56
- if (typeof path === 'number') {
57
- return "".concat(name, "[").concat(Number.isNaN(path) ? '' : path, "]");
58
- }
59
- if (name === '' || path === '') {
60
- return [name, path].join('');
61
- }
62
- return [name, path].join('.');
63
- }, '');
88
+ function formatPathSegments(segments) {
89
+ return segments.reduce((path, segment) => appendPathSegment(path, segment), '');
64
90
  }
65
91
 
66
92
  /**
67
- * Format based on a prefix and a path
68
- */
69
- function formatName(prefix, path) {
70
- return typeof path !== 'undefined' ? formatPaths([...getPaths(prefix), path]) : prefix !== null && prefix !== void 0 ? prefix : '';
71
- }
72
-
73
- /**
74
- * Check if a name match the prefix paths
75
- */
76
- function isPrefix(name, prefix) {
77
- var paths = getPaths(name);
78
- var prefixPaths = getPaths(prefix);
79
- return paths.length >= prefixPaths.length && prefixPaths.every((path, index) => paths[index] === path);
80
- }
81
-
82
- /**
83
- * Compare the parent and child paths to get the relative paths
84
- * Returns null if the child paths do not start with the parent paths
93
+ * Append one more segment onto an existing path string.
94
+ *
95
+ * - segment = `undefined` ⇒ no-op
96
+ * - segment = `""` ⇒ empty brackets "[]"
97
+ * - segment = `number` ⇒ bracket notation "[n]"
98
+ * - segment = `string` ⇒ dot-notation ".prop"
85
99
  */
86
- function getChildPaths(parentNameOrPaths, childName) {
87
- var parentPaths = typeof parentNameOrPaths === 'string' ? getPaths(parentNameOrPaths) : parentNameOrPaths;
88
- var childPaths = getPaths(childName);
89
- if (childPaths.length >= parentPaths.length && parentPaths.every((path, index) => childPaths[index] === path)) {
90
- return childPaths.slice(parentPaths.length);
100
+ function appendPathSegment(path, segment) {
101
+ // 1) nothing to append
102
+ if (typeof segment === 'undefined') {
103
+ return path !== null && path !== void 0 ? path : '';
91
104
  }
92
- return null;
93
- }
94
105
 
95
- /**
96
- * Assign a value to a target object by following the paths
97
- */
98
- function setValue(target, name, valueFn) {
99
- var paths = getPaths(name);
100
- var length = paths.length;
101
- var lastIndex = length - 1;
102
- var index = -1;
103
- var pointer = target;
104
- while (pointer != null && ++index < length) {
105
- var _key = paths[index];
106
- var nextKey = paths[index + 1];
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];
106
+ // 2) explicit empty-segment => empty bracket
107
+ if (segment === '') {
108
+ // even as first segment, "[]" is valid
109
+ return "".concat(path, "[]");
110
110
  }
111
- }
112
111
 
113
- /**
114
- * Retrive the value from a target object by following the paths
115
- */
116
- function getValue(target, name) {
117
- var pointer = target;
118
- for (var path of getPaths(name)) {
119
- if (typeof pointer === 'undefined' || pointer == null) {
120
- break;
121
- }
122
- if (!Object.prototype.hasOwnProperty.call(pointer, path)) {
123
- return;
124
- }
125
- if (isPlainObject(pointer) && typeof path === 'string') {
126
- pointer = pointer[path];
127
- } else if (Array.isArray(pointer) && typeof path === 'number') {
128
- pointer = pointer[path];
129
- } else {
130
- return;
131
- }
112
+ // 3) numeric index => [n]
113
+ if (typeof segment === 'number') {
114
+ return "".concat(path, "[").concat(segment, "]");
132
115
  }
133
- return pointer;
116
+
117
+ // 4) non-empty string => .prop (no leading dot if no base)
118
+ return path ? "".concat(path, ".").concat(segment) : segment;
134
119
  }
135
120
 
136
121
  /**
137
- * Check if the value is a plain object
122
+ * Returns true if `prefix` is a valid leading path of `name`.
123
+ *
124
+ * @example
125
+ * ```js
126
+ * isPrefix("foo.bar.baz", "foo.bar") // → true
127
+ * isPrefix("foo.bar[3].baz", "foo.bar[3]") // → true
128
+ * isPrefix("foo.bar[3].baz", "foo.bar") // → true
129
+ * isPrefix("foo.bar[3].baz", "foo.baz") // → false
130
+ * isPrefix("foo", "foo.bar") // → false
131
+ * ```
138
132
  */
139
- function isPlainObject(obj) {
140
- return !!obj && obj.constructor === Object && Object.getPrototypeOf(obj) === Object.prototype;
141
- }
142
- function isGlobalInstance(obj, className) {
143
- var Ctor = globalThis[className];
144
- return typeof Ctor === 'function' && obj instanceof Ctor;
133
+ function isPrefix(name, prefix) {
134
+ return getRelativePath(name, getPathSegments(prefix)) !== null;
145
135
  }
146
136
 
147
137
  /**
148
- * Normalize value by removing empty object or array, empty string and null values
138
+ * Return the segments of `fullPathStr` that come after the `baseSegments` prefix.
139
+ *
140
+ * @param fullPathStr Full path as a dot/bracket string
141
+ * @param basePath Base path, already parsed into segments
142
+ * @returns The “tail” segments, or `null` if `fullPathStr` isn’t nested under `baseSegments`
143
+ *
144
+ * @example
145
+ * ```js
146
+ * getRelativePath("foo.bar[0].qux", ["foo","bar"]) // → [0, "qux"]
147
+ * getRelativePath("a.b.c.d", ["a","b"]) // → ["c","d"]
148
+ * getRelativePath("foo", ["foo","bar"]) // → null
149
+ * ```
149
150
  */
151
+ function getRelativePath(name, basePath) {
152
+ var fullPath = getPathSegments(name);
150
153
 
151
- function normalize(value) {
152
- var acceptFile = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : true;
153
- if (isPlainObject(value)) {
154
- var obj = Object.keys(value).sort().reduce((result, key) => {
155
- var data = normalize(value[key], acceptFile);
156
- if (typeof data !== 'undefined') {
157
- result[key] = data;
158
- }
159
- return result;
160
- }, {});
161
- if (Object.keys(obj).length === 0) {
162
- return;
163
- }
164
- return obj;
154
+ // if full is at least as long *and* starts with the base…
155
+ if (fullPath.length >= basePath.length && basePath.every((segment, i) => segment === fullPath[i])) {
156
+ return fullPath.slice(basePath.length);
165
157
  }
166
- if (Array.isArray(value)) {
167
- if (value.length === 0) {
168
- return undefined;
169
- }
170
- return value.map(item => normalize(item, acceptFile));
171
- }
172
- if (typeof value === 'string' && value === '' || value === null || isGlobalInstance(value, 'File') && (!acceptFile || value.size === 0)) {
173
- return;
174
- }
175
- return value;
158
+ return null;
176
159
  }
177
160
 
178
161
  /**
179
- * Flatten a tree into a dictionary
162
+ * Assign a value to a target object by following the path segments.
180
163
  */
181
- function flatten(data) {
182
- var _options$resolve;
183
- var options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {};
184
- var result = {};
185
- var resolve = (_options$resolve = options.resolve) !== null && _options$resolve !== void 0 ? _options$resolve : data => data;
186
- function process(data, prefix) {
187
- var value = normalize(resolve(data));
188
- if (typeof value !== 'undefined') {
189
- result[prefix] = value;
164
+ function setValueAtPath(target, pathOrSegments, valueOrFn) {
165
+ var options = arguments.length > 3 && arguments[3] !== undefined ? arguments[3] : {};
166
+ try {
167
+ // 1) normalize + validate path
168
+ var segments = typeof pathOrSegments === 'string' ? getPathSegments(pathOrSegments) : pathOrSegments;
169
+ if (segments.length === 0) {
170
+ throw new Error('Cannot set value at the object root');
190
171
  }
191
- if (Array.isArray(data)) {
192
- for (var i = 0; i < data.length; i++) {
193
- process(data[i], "".concat(prefix, "[").concat(i, "]"));
194
- }
195
- } else if (isPlainObject(data)) {
196
- for (var [_key2, _value] of Object.entries(data)) {
197
- process(_value, prefix ? "".concat(prefix, ".").concat(_key2) : _key2);
198
- }
172
+ if (segments.some((segment, i) => segment === '' && i < segments.length - 1)) {
173
+ throw new Error("Empty brackets '[]' only allowed at end of path (\"".concat(pathOrSegments, "\")"));
199
174
  }
200
- }
201
- if (data) {
202
- var _options$prefix;
203
- process(data, (_options$prefix = options.prefix) !== null && _options$prefix !== void 0 ? _options$prefix : '');
204
- }
205
- return result;
206
- }
207
- function deepEqual(left, right) {
208
- if (Object.is(left, right)) {
209
- return true;
210
- }
211
- if (left == null || right == null) {
212
- return false;
213
- }
214
175
 
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) {
220
- return false;
221
- }
222
- for (var _key3 of prevKeys) {
223
- if (!Object.prototype.hasOwnProperty.call(right, _key3) || !deepEqual(left[_key3], right[_key3])) {
224
- return false;
176
+ // 2) clone root if needed
177
+ var result = options.clone ? _objectSpread2({}, target) : target;
178
+ var pointer = result;
179
+
180
+ // 3) drill down, cloning ancestors
181
+ for (var i = 0; i < segments.length - 1; i++) {
182
+ var currentSegment = segments[i];
183
+ var nextSegment = segments[i + 1];
184
+ var child = pointer[currentSegment];
185
+ if (Array.isArray(child)) {
186
+ child = options.clone ? child.slice() : child;
187
+ } else if (isPlainObject(child)) {
188
+ child = options.clone ? _objectSpread2({}, child) : child;
189
+ } else {
190
+ child = typeof nextSegment === 'number' || nextSegment === '' ? [] : {};
225
191
  }
192
+ pointer[currentSegment] = child;
193
+ pointer = child;
226
194
  }
227
- return true;
228
- }
229
195
 
230
- // Compare arrays
231
- if (Array.isArray(left) && Array.isArray(right)) {
232
- if (left.length !== right.length) {
233
- return false;
234
- }
235
- for (var i = 0; i < left.length; i++) {
236
- if (!deepEqual(left[i], right[i])) {
237
- return false;
196
+ // 4) final set or push
197
+ var last = segments[segments.length - 1];
198
+ var oldValue = pointer[last];
199
+ var newValue = typeof valueOrFn === 'function' ? valueOrFn(oldValue) : valueOrFn;
200
+ if (last === '') {
201
+ if (!Array.isArray(pointer)) {
202
+ throw new Error("Cannot push to non-array at \"".concat(pathOrSegments, "\""));
238
203
  }
204
+ pointer.push(newValue);
205
+ } else {
206
+ pointer[last] = newValue;
207
+ }
208
+ return result;
209
+ } catch (err) {
210
+ if (options !== null && options !== void 0 && options.silent) {
211
+ return target;
239
212
  }
240
- return true;
213
+ throw err;
241
214
  }
242
- return false;
243
215
  }
244
216
 
245
217
  /**
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.
218
+ * Retrive the value from a target object by following the path segments.
252
219
  */
220
+ function getValueAtPath(target, pathOrSegments) {
221
+ var pointer = target;
222
+ var segments = typeof pathOrSegments === 'string' ? getPathSegments(pathOrSegments) : pathOrSegments;
223
+ for (var segment of segments) {
224
+ if (segment === '') {
225
+ throw new Error("Cannot access empty segment \"[]\" in \"".concat(pathOrSegments, "\""));
226
+ }
227
+ if (pointer == null || !Object.prototype.hasOwnProperty.call(pointer, segment)) {
228
+ return undefined;
229
+ }
230
+ pointer = pointer[segment];
231
+ }
232
+ return pointer;
233
+ }
253
234
 
254
235
  /**
255
236
  * Parse `FormData` or `URLSearchParams` into a submission object.
256
237
  * 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.
238
+ * It also includes all the field names and extracts the intent from the submission.
258
239
  *
240
+ * @see https://conform.guide/api/react/future/parseSubmission
259
241
  * @example
260
242
  * ```ts
261
243
  * const formData = new FormData();
@@ -265,7 +247,7 @@ function deepEqual(left, right) {
265
247
  *
266
248
  * parseSubmission(formData)
267
249
  * // {
268
- * // value: { email: 'test@example.com', password: 'secret' },
250
+ * // payload: { email: 'test@example.com', password: 'secret' },
269
251
  * // fields: ['email', 'password'],
270
252
  * // intent: null,
271
253
  * // }
@@ -274,7 +256,7 @@ function deepEqual(left, right) {
274
256
  * formData.append('intent', 'login');
275
257
  * parseSubmission(formData, { intentName: 'intent' })
276
258
  * // {
277
- * // value: { email: 'test@example.com', password: 'secret' },
259
+ * // payload: { email: 'test@example.com', password: 'secret' },
278
260
  * // fields: ['email', 'password'],
279
261
  * // intent: 'login',
280
262
  * // }
@@ -282,22 +264,21 @@ function deepEqual(left, right) {
282
264
  */
283
265
  function parseSubmission(formData, options) {
284
266
  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;
267
+ var intentName = (_options$intentName = options === null || options === void 0 ? void 0 : options.intentName) !== null && _options$intentName !== void 0 ? _options$intentName : DEFAULT_INTENT_NAME;
286
268
  var submission = {
287
- value: {},
269
+ payload: {},
288
270
  fields: [],
289
271
  intent: null
290
272
  };
291
- var _loop = function _loop() {
273
+ for (var _name of new Set(formData.keys())) {
292
274
  var _options$skipEntry;
293
275
  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]);
276
+ var _value = formData.getAll(_name);
277
+ setValueAtPath(submission.payload, _name, _value.length > 1 ? _value : _value[0], {
278
+ silent: true // Avoid errors if the path is invalid
279
+ });
296
280
  submission.fields.push(_name);
297
281
  }
298
- };
299
- for (var _name of new Set(formData.keys())) {
300
- _loop();
301
282
  }
302
283
  if (intentName) {
303
284
  // We take the first value of the intent field if it exists.
@@ -308,17 +289,63 @@ function parseSubmission(formData, options) {
308
289
  }
309
290
  return submission;
310
291
  }
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();
292
+
293
+ /**
294
+ * Creates a SubmissionResult object from a submission, adding validation results and intended values.
295
+ * This function will remove all files in the submission payload by default since
296
+ * file inputs cannot be initialized with files.
297
+ * You can specify `keepFiles: true` to keep the files if needed.
298
+ *
299
+ * @see https://conform.guide/api/react/future/report
300
+ * @example
301
+ * ```ts
302
+ * // Report the submission with the field errors
303
+ * report(submission, {
304
+ * error: {
305
+ * fieldErrors: {
306
+ * email: ['Invalid email format'],
307
+ * password: ['Password is required'],
308
+ * },
309
+ * })
310
+ *
311
+ * // Report the submission with a form error
312
+ * report(submission, {
313
+ * error: {
314
+ * formErrors: ['Invalid credentials'],
315
+ * },
316
+ * })
317
+ *
318
+ * // Reset the form
319
+ * report(submission, {
320
+ * reset: true,
321
+ * })
322
+ * ```
323
+ */
324
+
325
+ function report(submission) {
326
+ var _options$error$formEr, _options$error$fieldE;
327
+ var options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {};
328
+ var intendedValue = options.reset ? null : typeof options.intendedValue === 'undefined' || submission.payload === options.intendedValue ? undefined : options.intendedValue && !options.keepFiles ? stripFiles(options.intendedValue) : options.intendedValue;
329
+ var error = !options.error ? options.error : {
330
+ formErrors: (_options$error$formEr = options.error.formErrors) !== null && _options$error$formEr !== void 0 ? _options$error$formEr : [],
331
+ fieldErrors: (_options$error$fieldE = options.error.fieldErrors) !== null && _options$error$fieldE !== void 0 ? _options$error$fieldE : {}
332
+ };
333
+ if (options.hideFields) {
334
+ for (var _name2 of options.hideFields) {
335
+ var path = getPathSegments(_name2);
336
+ setValueAtPath(submission.payload, path, undefined);
337
+ if (intendedValue) {
338
+ setValueAtPath(intendedValue, path, undefined);
339
+ }
340
+ }
320
341
  }
321
- return value === null || value === void 0 ? void 0 : value.toString();
342
+ return {
343
+ submission: options.keepFiles ? submission : _objectSpread2(_objectSpread2({}, submission), {}, {
344
+ payload: stripFiles(submission.payload)
345
+ }),
346
+ intendedValue,
347
+ error
348
+ };
322
349
  }
323
350
 
324
351
  /**
@@ -349,17 +376,37 @@ function isDirty(
349
376
  * - A plain object that was parsed from form data (i.e. `submission.payload`)
350
377
  */
351
378
  formData, options) {
352
- var _options$serialize;
353
379
  if (!formData) {
354
380
  return;
355
381
  }
356
382
  var formValue = formData instanceof FormData || formData instanceof URLSearchParams ? parseSubmission(formData, {
357
383
  intentName: options === null || options === void 0 ? void 0 : options.intentName,
358
384
  skipEntry: options === null || options === void 0 ? void 0 : options.skipEntry
359
- }).value : formData;
385
+ }).payload : formData;
360
386
  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) {
387
+ var serializeData = value => {
388
+ if (options !== null && options !== void 0 && options.serialize) {
389
+ return options.serialize(value, serialize);
390
+ }
391
+ return serialize(value);
392
+ };
393
+ function normalize(data) {
394
+ var _serializeData;
395
+ var value = (_serializeData = serializeData(data)) !== null && _serializeData !== void 0 ? _serializeData : data;
396
+
397
+ // Removes empty strings, so that bpth empty string and undefined are treated as the same
398
+ if (value === '') {
399
+ return undefined;
400
+ }
401
+ if (isGlobalInstance(value, 'File')) {
402
+ // Remove empty File as well, which happens if no File was selected
403
+ if (value.name === '' && value.size === 0) {
404
+ return undefined;
405
+ }
406
+
407
+ // If the value is a File, no need to serialize it
408
+ return value;
409
+ }
363
410
  if (Array.isArray(value)) {
364
411
  if (value.length === 0) {
365
412
  return undefined;
@@ -384,24 +431,80 @@ formData, options) {
384
431
  }
385
432
  return Object.fromEntries(entries);
386
433
  }
434
+ return value;
435
+ }
436
+ return !deepEqual(normalize(formValue), normalize(defaultValue));
437
+ }
387
438
 
388
- // If the value is null or undefined, treat it as undefined
389
- if (value == null) {
390
- return undefined;
439
+ /**
440
+ * Convert an unknown value into something acceptable for HTML form submission.
441
+ * Returns `undefined` when the value cannot be represented in form data.
442
+ *
443
+ * Input -> Output:
444
+ * - string -> string
445
+ * - null -> '' (empty string)
446
+ * - boolean -> 'on' | '' (checked semantics)
447
+ * - number | bigint -> value.toString()
448
+ * - Date -> value.toISOString()
449
+ * - File -> File
450
+ * - FileList -> File[]
451
+ * - Array -> string[] or File[] if all items serialize to the same kind; otherwise undefined
452
+ * - anything else -> undefined
453
+ */
454
+ function serialize(value) {
455
+ function serializePrimitive(value) {
456
+ if (typeof value === 'string') {
457
+ return value;
391
458
  }
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;
459
+ if (value === null) {
460
+ return '';
396
461
  }
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;
462
+ if (typeof value === 'boolean') {
463
+ return value ? 'on' : '';
464
+ }
465
+ if (typeof value === 'number' || typeof value === 'bigint') {
466
+ return value.toString();
467
+ }
468
+ if (value instanceof Date) {
469
+ return value.toISOString();
470
+ }
471
+ if (isGlobalInstance(value, 'File')) {
472
+ return value;
401
473
  }
402
- return serialize(value, defaultSerialize);
403
474
  }
404
- return !deepEqual(normalize(formValue), normalize(defaultValue));
475
+ if (Array.isArray(value)) {
476
+ var _options = [];
477
+ var files = [];
478
+ for (var item of value) {
479
+ var serialized = serializePrimitive(item);
480
+ if (typeof serialized === 'undefined') {
481
+ return;
482
+ }
483
+ if (typeof serialized === 'string') {
484
+ if (files.length > 0) {
485
+ return;
486
+ }
487
+ _options.push(serialized);
488
+ } else {
489
+ if (_options.length > 0) {
490
+ return;
491
+ }
492
+ files.push(serialized);
493
+ }
494
+ }
495
+ if (_options.length === value.length) {
496
+ return _options;
497
+ }
498
+ if (files.length === value.length) {
499
+ return files;
500
+ }
501
+
502
+ // If not all items are strings or files, return nothing
503
+ }
504
+ if (isGlobalInstance(value, 'FileList')) {
505
+ return Array.from(value);
506
+ }
507
+ return serializePrimitive(value);
405
508
  }
406
509
 
407
- export { deepEqual, defaultSerialize, flatten, formatName, formatPaths, getChildPaths, getFormData, getPaths, getValue, isDirty, isGlobalInstance, isPlainObject, isPrefix, normalize, parseSubmission, setValue };
510
+ export { DEFAULT_INTENT_NAME, appendPathSegment, formatPathSegments, getFormData, getPathSegments, getRelativePath, getValueAtPath, isDirty, isPrefix, parseSubmission, report, serialize, setValueAtPath };