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