@conform-to/dom 1.17.1 → 1.19.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/formdata.js CHANGED
@@ -9,12 +9,48 @@ var standardSchema = require('./standard-schema.js');
9
9
 
10
10
  var DEFAULT_INTENT_NAME = '__INTENT__';
11
11
 
12
+ /**
13
+ * Returns whether an error payload contains a meaningful value.
14
+ * Empty strings and empty arrays are treated as no error.
15
+ */
16
+ function hasError(error) {
17
+ return error != null && error !== '' && (!Array.isArray(error) || error.length > 0);
18
+ }
19
+
20
+ /**
21
+ * Normalizes a form error object by removing empty error payloads such as
22
+ * empty strings and empty arrays.
23
+ *
24
+ * Returns `null` when no form-level or field-level errors remain.
25
+ */
26
+ function normalizeFormError(error) {
27
+ var _error$fieldErrors;
28
+ if (error === null) {
29
+ return null;
30
+ }
31
+ var formErrors = hasError(error.formErrors) ? error.formErrors : null;
32
+ var fieldErrors = Object.entries((_error$fieldErrors = error.fieldErrors) !== null && _error$fieldErrors !== void 0 ? _error$fieldErrors : {}).reduce((result, _ref) => {
33
+ var [name, value] = _ref;
34
+ if (hasError(value)) {
35
+ result[name] = value;
36
+ }
37
+ return result;
38
+ }, {});
39
+ if (formErrors === null && Object.keys(fieldErrors).length === 0) {
40
+ return null;
41
+ }
42
+ return {
43
+ formErrors,
44
+ fieldErrors
45
+ };
46
+ }
47
+
12
48
  /**
13
49
  * Construct a form data with the submitter value.
14
50
  * It utilizes the submitter argument on the FormData constructor from modern browsers
15
51
  * with fallback to append the submitter value in case it is not unsupported.
16
52
  *
17
- * @see https://developer.mozilla.org/en-US/docs/Web/API/FormData/FormData#parameters
53
+ * See https://developer.mozilla.org/en-US/docs/Web/API/FormData/FormData#parameters
18
54
  */
19
55
  function getFormData(form, submitter) {
20
56
  var payload = new FormData(form, submitter);
@@ -37,14 +73,14 @@ function getFormData(form, submitter) {
37
73
  /**
38
74
  * Convert a string path into an array of segments.
39
75
  *
40
- * @example
76
+ * **Example:**
41
77
  * ```js
42
- * getPathSegments("object.key"); // → ['object', 'key']
43
- * getPathSegments("array[0].content"); // → ['array', 0, 'content']
44
- * getPathSegments("todos[]"); // → ['todos', '']
78
+ * parsePath("object.key"); // → ['object', 'key']
79
+ * parsePath("array[0].content"); // → ['array', 0, 'content']
80
+ * parsePath("todos[]"); // → ['todos', '']
45
81
  * ```
46
82
  */
47
- function getPathSegments(path) {
83
+ function parsePath(path) {
48
84
  if (!path) return [];
49
85
  var tokenRegex = /([^.[\]]+)|\[(\d*)\]/g;
50
86
  var segments = [];
@@ -83,15 +119,15 @@ function getPathSegments(path) {
83
119
  /**
84
120
  * Returns a formatted name from the path segments based on the dot and bracket notation.
85
121
  *
86
- * @example
122
+ * **Example:**
87
123
  * ```js
88
- * formatPathSegments(['object', 'key']); // → "object.key"
89
- * formatPathSegments(['array', 0, 'content']); // → "array[0].content"
90
- * formatPathSegments(['todos', '']); // → "todos[]"
124
+ * formatPath(['object', 'key']); // → "object.key"
125
+ * formatPath(['array', 0, 'content']); // → "array[0].content"
126
+ * formatPath(['todos', '']); // → "todos[]"
91
127
  * ```
92
128
  */
93
- function formatPathSegments(segments) {
94
- return segments.reduce((path, segment) => appendPathSegment(path, segment), '');
129
+ function formatPath(segments) {
130
+ return segments.reduce((path, segment) => appendPath(path, segment), '');
95
131
  }
96
132
 
97
133
  /**
@@ -102,63 +138,66 @@ function formatPathSegments(segments) {
102
138
  * - segment = `number` ⇒ bracket notation "[n]"
103
139
  * - segment = `string` ⇒ dot-notation ".prop"
104
140
  */
105
- function appendPathSegment(path, segment) {
141
+ function appendPath(path, segment) {
142
+ var base = path !== null && path !== void 0 ? path : '';
143
+
106
144
  // 1) nothing to append
107
145
  if (typeof segment === 'undefined') {
108
- return path !== null && path !== void 0 ? path : '';
146
+ return base;
109
147
  }
110
148
 
111
149
  // 2) explicit empty-segment => empty bracket
112
150
  if (segment === '') {
113
151
  // even as first segment, "[]" is valid
114
- return "".concat(path, "[]");
152
+ return "".concat(base, "[]");
115
153
  }
116
154
 
117
155
  // 3) numeric index => [n]
118
156
  if (typeof segment === 'number') {
119
- return "".concat(path, "[").concat(segment, "]");
157
+ return "".concat(base, "[").concat(segment, "]");
120
158
  }
121
159
 
122
160
  // 4) non-empty string => .prop (no leading dot if no base)
123
- return path ? "".concat(path, ".").concat(segment) : segment;
161
+ return base ? "".concat(base, ".").concat(segment) : segment;
124
162
  }
125
163
 
126
164
  /**
127
165
  * Returns true if `prefix` is a valid leading path of `name`.
128
166
  *
129
- * @example
167
+ * **Example:**
130
168
  * ```js
131
- * isPrefix("foo.bar.baz", "foo.bar") // → true
132
- * isPrefix("foo.bar[3].baz", "foo.bar[3]") // → true
133
- * isPrefix("foo.bar[3].baz", "foo.bar") // → true
134
- * isPrefix("foo.bar[3].baz", "foo.baz") // → false
135
- * isPrefix("foo", "foo.bar") // → false
169
+ * isPathPrefix("foo.bar.baz", "foo.bar") // → true
170
+ * isPathPrefix("foo.bar[3].baz", "foo.bar[3]") // → true
171
+ * isPathPrefix("foo.bar[3].baz", "foo.bar") // → true
172
+ * isPathPrefix("foo.bar[3].baz", "foo.baz") // → false
173
+ * isPathPrefix("foo", "foo.bar") // → false
136
174
  * ```
137
175
  */
138
- function isPrefix(name, prefix) {
139
- return getRelativePath(name, getPathSegments(prefix)) !== null;
176
+ function isPathPrefix(name, prefix) {
177
+ return getRelativePath(name, parsePath(prefix)) !== null;
140
178
  }
141
179
 
142
180
  /**
143
- * Return the segments of `fullPathStr` that come after the `baseSegments` prefix.
181
+ * Return the segments of `fullPath` that come after the `basePath` prefix.
144
182
  *
145
- * @param fullPathStr Full path as a dot/bracket string
146
- * @param basePath Base path, already parsed into segments
147
- * @returns The “tail” segments, or `null` if `fullPathStr` isn’t nested under `baseSegments`
183
+ * @param fullPath Full path as a dot/bracket string or array of segments
184
+ * @param basePath Base path as a dot/bracket string or array of segments
185
+ * @returns The “tail” segments, or `null` if `fullPath` isn’t nested under `basePath`
148
186
  *
149
- * @example
187
+ * **Example:**
150
188
  * ```js
151
189
  * getRelativePath("foo.bar[0].qux", ["foo","bar"]) // → [0, "qux"]
152
190
  * getRelativePath("a.b.c.d", ["a","b"]) // → ["c","d"]
153
191
  * getRelativePath("foo", ["foo","bar"]) // → null
154
192
  * ```
155
193
  */
156
- function getRelativePath(name, basePath) {
157
- var fullPath = getPathSegments(name);
194
+ function getRelativePath(fullPath, basePath) {
195
+ var fullPathSegments = typeof fullPath === 'string' ? parsePath(fullPath) : fullPath;
196
+ var basePathSegments = typeof basePath === 'string' ? parsePath(basePath) : basePath;
158
197
 
159
198
  // if full is at least as long *and* starts with the base…
160
- if (fullPath.length >= basePath.length && basePath.every((segment, i) => segment === fullPath[i])) {
161
- return fullPath.slice(basePath.length);
199
+ if (fullPathSegments.length >= basePathSegments.length && basePathSegments.every((segment, i) => segment === fullPathSegments[i])) {
200
+ return fullPathSegments.slice(basePathSegments.length);
162
201
  }
163
202
  return null;
164
203
  }
@@ -166,11 +205,11 @@ function getRelativePath(name, basePath) {
166
205
  /**
167
206
  * Assign a value to a target object by following the path segments.
168
207
  */
169
- function setValueAtPath(target, pathOrSegments, valueOrFn) {
208
+ function setPathValue(target, pathOrSegments, valueOrFn) {
170
209
  var options = arguments.length > 3 && arguments[3] !== undefined ? arguments[3] : {};
171
210
  try {
172
211
  // 1) normalize + validate path
173
- var segments = typeof pathOrSegments === 'string' ? getPathSegments(pathOrSegments) : pathOrSegments;
212
+ var segments = typeof pathOrSegments === 'string' ? parsePath(pathOrSegments) : pathOrSegments;
174
213
  if (segments.length === 0) {
175
214
  throw new Error('Cannot set value at the object root');
176
215
  }
@@ -222,9 +261,9 @@ function setValueAtPath(target, pathOrSegments, valueOrFn) {
222
261
  /**
223
262
  * Retrive the value from a target object by following the path segments.
224
263
  */
225
- function getValueAtPath(target, pathOrSegments) {
264
+ function getPathValue(target, pathOrSegments) {
226
265
  var pointer = target;
227
- var segments = typeof pathOrSegments === 'string' ? getPathSegments(pathOrSegments) : pathOrSegments;
266
+ var segments = typeof pathOrSegments === 'string' ? parsePath(pathOrSegments) : pathOrSegments;
228
267
  for (var segment of segments) {
229
268
  if (segment === '') {
230
269
  throw new Error("Cannot access empty segment \"[]\" in \"".concat(pathOrSegments, "\""));
@@ -264,8 +303,9 @@ function isEmptyValue(value) {
264
303
  * This function structures the form values based on the naming convention.
265
304
  * It also includes all the field names and extracts the intent from the submission.
266
305
  *
267
- * @see https://conform.guide/api/react/future/parseSubmission
268
- * @example
306
+ * See https://conform.guide/api/react/future/parseSubmission
307
+ *
308
+ * **Example:**
269
309
  * ```ts
270
310
  * const formData = new FormData();
271
311
  *
@@ -301,29 +341,27 @@ function parseSubmission(formData, options) {
301
341
  var _options$skipEntry;
302
342
  if (_name !== intentName && !(options !== null && options !== void 0 && (_options$skipEntry = options.skipEntry) !== null && _options$skipEntry !== void 0 && _options$skipEntry.call(options, _name))) {
303
343
  var _options$stripEmptyVa;
304
- var _value = formData.getAll(_name);
305
- var segments = getPathSegments(_name);
344
+ var value = formData.getAll(_name);
345
+ var segments = parsePath(_name);
306
346
 
307
347
  // If the name ends with [], remove the empty segment and keep the full array
308
348
  // Otherwise, unwrap single values
309
349
  if (segments.length > 0 && segments[segments.length - 1] === '') {
310
350
  segments.pop();
311
351
  } else {
312
- _value = _value.length > 1 ? _value : _value[0];
352
+ value = value.length > 1 ? value : value[0];
313
353
  }
314
-
315
- // Check if the value is empty and should be skipped (defaults to true)
316
- var stripEmptyValues = (_options$stripEmptyVa = options === null || options === void 0 ? void 0 : options.stripEmptyValues) !== null && _options$stripEmptyVa !== void 0 ? _options$stripEmptyVa : true;
354
+ var stripEmptyValues = (_options$stripEmptyVa = options === null || options === void 0 ? void 0 : options.stripEmptyValues) !== null && _options$stripEmptyVa !== void 0 ? _options$stripEmptyVa : false;
317
355
  if (stripEmptyValues) {
318
356
  // For arrays, filter out individual empty items
319
- if (Array.isArray(_value)) {
320
- _value = _value.filter(item => !isEmptyValue(item));
357
+ if (Array.isArray(value)) {
358
+ value = value.filter(item => !isEmptyValue(item));
321
359
  }
322
- if (isEmptyValue(_value)) {
323
- _value = undefined;
360
+ if (isEmptyValue(value)) {
361
+ value = undefined;
324
362
  }
325
363
  }
326
- setValueAtPath(submission.payload, segments, _value, {
364
+ setPathValue(submission.payload, segments, value, {
327
365
  silent: true // Avoid errors if the path is invalid
328
366
  });
329
367
  submission.fields.push(_name);
@@ -345,8 +383,9 @@ function parseSubmission(formData, options) {
345
383
  * file inputs cannot be initialized with files.
346
384
  * You can specify `keepFiles: true` to keep the files if needed.
347
385
  *
348
- * @see https://conform.guide/api/react/future/report
349
- * @example
386
+ * See https://conform.guide/api/react/future/report
387
+ *
388
+ * **Example:**
350
389
  * ```ts
351
390
  * // Report the submission with the field errors
352
391
  * report(submission, {
@@ -377,32 +416,19 @@ function report(submission) {
377
416
  var error;
378
417
  if (options.error == null) {
379
418
  error = options.error;
380
- } else {
419
+ } else if ('issues' in options.error) {
381
420
  var _options$error$issues;
382
421
  error = standardSchema.formatIssues((_options$error$issues = options.error.issues) !== null && _options$error$issues !== void 0 ? _options$error$issues : []);
383
- if (options.error.formErrors) {
384
- error.formErrors.push(...options.error.formErrors);
385
- }
386
- if (options.error.fieldErrors) {
387
- for (var [_name2, messages] of Object.entries(options.error.fieldErrors)) {
388
- if (messages.length === 0) {
389
- continue;
390
- }
391
- if (!error.fieldErrors[_name2]) {
392
- error.fieldErrors[_name2] = messages;
393
- } else {
394
- error.fieldErrors[_name2].push(...messages);
395
- }
396
- }
397
- }
422
+ } else {
423
+ error = normalizeFormError(options.error);
398
424
  }
399
425
  var targetValue = typeof options.value === 'undefined' || submission.payload === options.value && !options.reset ? undefined : options.value && !options.keepFiles ? util.stripFiles(options.value) : (_options$value = options.value) !== null && _options$value !== void 0 ? _options$value : {};
400
426
  if (options.hideFields) {
401
- for (var _name3 of options.hideFields) {
402
- var path = getPathSegments(_name3);
403
- setValueAtPath(submission.payload, path, undefined);
427
+ for (var _name2 of options.hideFields) {
428
+ var path = parsePath(_name2);
429
+ setPathValue(submission.payload, path, undefined);
404
430
  if (targetValue) {
405
- setValueAtPath(targetValue, path, undefined);
431
+ setPathValue(targetValue, path, undefined);
406
432
  }
407
433
  }
408
434
  }
@@ -419,8 +445,9 @@ function report(submission) {
419
445
  /**
420
446
  * A utility function that checks whether the current form data differs from the default values.
421
447
  *
422
- * @see https://conform.guide/api/react/future/isDirty
423
- * @example Enable a submit button only if the form is dirty
448
+ * See https://conform.guide/api/react/future/isDirty
449
+ *
450
+ * **Example: Enable a submit button only if the form is dirty**
424
451
  *
425
452
  * ```tsx
426
453
  * const dirty = useFormData(
@@ -451,59 +478,16 @@ formData, options) {
451
478
  intentName: options === null || options === void 0 ? void 0 : options.intentName,
452
479
  skipEntry: options === null || options === void 0 ? void 0 : options.skipEntry
453
480
  }).payload : formData;
454
- var defaultValue = options === null || options === void 0 ? void 0 : options.defaultValue;
455
- var serializeData = value => {
481
+ var serialize = (value, context) => {
456
482
  if (options !== null && options !== void 0 && options.serialize) {
457
- return options.serialize(value, serialize);
483
+ return options.serialize(value, {
484
+ name: context.name,
485
+ defaultSerialize
486
+ });
458
487
  }
459
- return serialize(value);
488
+ return defaultSerialize(value);
460
489
  };
461
- function normalize(data) {
462
- var value = serializeData(data);
463
- if (typeof value === 'undefined') {
464
- value = data;
465
- }
466
-
467
- // Removes empty strings, so that both empty string, empty file, null and undefined are treated as the same
468
- if (value === '' || value === null) {
469
- return undefined;
470
- }
471
- if (dom.isGlobalInstance(value, 'File')) {
472
- // Remove empty File as well, which happens if no File was selected
473
- if (value.name === '' && value.size === 0) {
474
- return undefined;
475
- }
476
-
477
- // If the value is a File, no need to serialize it
478
- return value;
479
- }
480
- if (Array.isArray(value)) {
481
- if (value.length === 0) {
482
- return undefined;
483
- }
484
- var array = value.map(normalize);
485
- if (array.length === 1 && (typeof array[0] === 'string' || array[0] === undefined)) {
486
- return array[0];
487
- }
488
- return array;
489
- }
490
- if (util.isPlainObject(value)) {
491
- var entries = Object.entries(value).reduce((list, _ref) => {
492
- var [key, value] = _ref;
493
- var normalizedValue = normalize(value);
494
- if (typeof normalizedValue !== 'undefined') {
495
- list.push([key, normalizedValue]);
496
- }
497
- return list;
498
- }, []);
499
- if (entries.length === 0) {
500
- return undefined;
501
- }
502
- return Object.fromEntries(entries);
503
- }
504
- return value;
505
- }
506
- return !util.deepEqual(normalize(formValue), normalize(defaultValue));
490
+ return !util.deepEqual(normalize(formValue, serialize), normalize(options === null || options === void 0 ? void 0 : options.defaultValue, serialize));
507
491
  }
508
492
 
509
493
  /**
@@ -515,25 +499,25 @@ formData, options) {
515
499
  * - null -> '' (empty string)
516
500
  * - boolean -> 'on' | '' (checked semantics)
517
501
  * - number | bigint -> value.toString()
518
- * - Date -> value.toISOString()
502
+ * - Date -> value.toISOString() without trailing `Z`
519
503
  * - File -> File
520
504
  * - FileList -> File[]
521
505
  * - Array -> string[] or File[] if all items serialize to the same kind; otherwise undefined
522
506
  * - anything else -> undefined
523
507
  */
524
- function serialize(value) {
508
+ function defaultSerialize(value) {
525
509
  function serializePrimitive(value) {
526
510
  if (typeof value === 'string' || value === null) {
527
511
  return value;
528
512
  }
529
513
  if (typeof value === 'boolean') {
530
- return value ? 'on' : '';
514
+ return value ? 'on' : null;
531
515
  }
532
516
  if (typeof value === 'number' || typeof value === 'bigint') {
533
517
  return value.toString();
534
518
  }
535
519
  if (value instanceof Date) {
536
- return value.toISOString();
520
+ return value.toISOString().slice(0, -1);
537
521
  }
538
522
  if (dom.isGlobalInstance(value, 'File')) {
539
523
  return value;
@@ -576,10 +560,68 @@ function serialize(value) {
576
560
  return serializePrimitive(value);
577
561
  }
578
562
 
563
+ /**
564
+ * Recursively serializes a value using the provided serialize function,
565
+ * collapsing empty leaves (`null`, `''`, empty files) to `undefined`
566
+ * and removing empty containers (objects with no remaining keys, empty arrays).
567
+ *
568
+ * When serialize returns `undefined` for a value (i.e. it can't be represented
569
+ * as form data), the raw value is kept and recursed into if it's an object or array.
570
+ *
571
+ * Single-element arrays where the element is a string or undefined are unwrapped
572
+ * to handle the case where a multi-value field (e.g. checkboxes) has only one value.
573
+ */
574
+ function normalize(value) {
575
+ var serialize = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : defaultSerialize;
576
+ var name = arguments.length > 2 ? arguments[2] : undefined;
577
+ var data = serialize(value, {
578
+ name
579
+ });
580
+ if (typeof data === 'undefined') {
581
+ data = value;
582
+ }
583
+ if (data === '' || data === null) {
584
+ return undefined;
585
+ }
586
+ if (dom.isGlobalInstance(data, 'File')) {
587
+ if (data.name === '' && data.size === 0) {
588
+ return undefined;
589
+ }
590
+ return data;
591
+ }
592
+ if (Array.isArray(data)) {
593
+ if (data.length === 0) {
594
+ return undefined;
595
+ }
596
+ var array = data.map((item, index) => normalize(item, serialize, appendPath(name, index)));
597
+ if (array.length === 1 && (typeof array[0] === 'string' || array[0] === undefined)) {
598
+ return array[0];
599
+ }
600
+ return array;
601
+ }
602
+ if (util.isPlainObject(data)) {
603
+ var entries = Object.entries(data).reduce((list, _ref2) => {
604
+ var [key, value] = _ref2;
605
+ var normalizedValue = normalize(value, serialize, appendPath(name, key));
606
+ if (typeof normalizedValue !== 'undefined') {
607
+ list.push([key, normalizedValue]);
608
+ }
609
+ return list;
610
+ }, []);
611
+ if (entries.length === 0) {
612
+ return undefined;
613
+ }
614
+ return Object.fromEntries(entries);
615
+ }
616
+ return data;
617
+ }
618
+
579
619
  /**
580
620
  * Retrieve a field value from FormData with optional type guards.
581
621
  *
582
- * @example
622
+ * **Example:**
623
+ *
624
+ * ```ts
583
625
  * // Basic field access: return `unknown`
584
626
  * const email = getFieldValue(formData, 'email');
585
627
  * // String type: returns `string`
@@ -594,6 +636,7 @@ function serialize(value) {
594
636
  * const items = getFieldValue<Item[]>(formData, 'items', { type: 'object', array: true });
595
637
  * // Optional string type: returns `string | undefined`
596
638
  * const bio = getFieldValue(formData, 'bio', { type: 'string', optional: true });
639
+ * ```
597
640
  */
598
641
 
599
642
  function getFieldValue(formData, name, options) {
@@ -609,11 +652,9 @@ function getFieldValue(formData, name, options) {
609
652
  // Get value based on array option
610
653
  value = array ? formData.getAll(name) : formData.get(name);
611
654
  } else {
612
- // Parse formData and use getValueAtPath
613
- var _submission = parseSubmission(formData, {
614
- stripEmptyValues: false
615
- });
616
- value = getValueAtPath(_submission.payload, name);
655
+ // Parse formData and use getPathValue
656
+ var _submission = parseSubmission(formData);
657
+ value = getPathValue(_submission.payload, name);
617
658
  }
618
659
 
619
660
  // If optional and value is undefined, skip validation and return early
@@ -648,16 +689,19 @@ function getFieldValue(formData, name, options) {
648
689
  }
649
690
 
650
691
  exports.DEFAULT_INTENT_NAME = DEFAULT_INTENT_NAME;
651
- exports.appendPathSegment = appendPathSegment;
652
- exports.formatPathSegments = formatPathSegments;
692
+ exports.appendPath = appendPath;
693
+ exports.defaultSerialize = defaultSerialize;
694
+ exports.formatPath = formatPath;
653
695
  exports.getFieldValue = getFieldValue;
654
696
  exports.getFormData = getFormData;
655
- exports.getPathSegments = getPathSegments;
697
+ exports.getPathValue = getPathValue;
656
698
  exports.getRelativePath = getRelativePath;
657
- exports.getValueAtPath = getValueAtPath;
699
+ exports.hasError = hasError;
658
700
  exports.isDirty = isDirty;
659
- exports.isPrefix = isPrefix;
701
+ exports.isPathPrefix = isPathPrefix;
702
+ exports.normalize = normalize;
703
+ exports.normalizeFormError = normalizeFormError;
704
+ exports.parsePath = parsePath;
660
705
  exports.parseSubmission = parseSubmission;
661
706
  exports.report = report;
662
- exports.serialize = serialize;
663
- exports.setValueAtPath = setValueAtPath;
707
+ exports.setPathValue = setPathValue;