@conform-to/react 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/README.md CHANGED
@@ -7,7 +7,7 @@
7
7
  ╚══════╝ ╚═════╝ ╚═╝ ╚══╝ ╚═╝ ╚═════╝ ╚═╝ ╚═╝ ╚═╝ ╚═╝
8
8
  ```
9
9
 
10
- Version 1.17.1 / License MIT / Copyright (c) 2026 Edmund Hung
10
+ Version 1.19.0 / License MIT / Copyright (c) 2026 Edmund Hung
11
11
 
12
12
  Progressively enhance HTML forms with React. Build resilient, type-safe forms with no hassle using web standards.
13
13
 
@@ -1,27 +1,14 @@
1
1
  import { Serialize } from '@conform-to/dom/future';
2
- import type { ErrorContext, FormRef, InputSnapshot, IntentDispatcher } from './types';
2
+ import type { ControlOptions, ErrorContext, FormRef, IntentDispatcher } from './types';
3
3
  export declare function getFormElement(formRef: FormRef | undefined): HTMLFormElement | null;
4
4
  export declare function getSubmitEvent(event: React.FormEvent<HTMLFormElement>): SubmitEvent;
5
5
  export declare function initializeField(element: HTMLInputElement | HTMLSelectElement | HTMLTextAreaElement, options: {
6
- defaultValue?: string | string[] | File | File[] | null | undefined;
6
+ defaultValue?: unknown;
7
7
  defaultChecked?: boolean | undefined;
8
8
  value?: string | undefined;
9
9
  } | undefined): void;
10
- /**
11
- * Makes hidden form inputs focusable with visually hidden styles
12
- */
13
- export declare function makeInputFocusable(element: HTMLInputElement | HTMLSelectElement | HTMLTextAreaElement): void;
14
- export declare function getRadioGroupValue(inputs: Array<HTMLInputElement>): string | undefined;
15
- export declare function getCheckboxGroupValue(inputs: Array<HTMLInputElement>): string[] | undefined;
16
- export declare function getInputSnapshot(input: HTMLInputElement | HTMLSelectElement | HTMLTextAreaElement): InputSnapshot;
17
- /**
18
- * Creates an InputSnapshot based on the provided options:
19
- * - checkbox/radio: value / defaultChecked
20
- * - file inputs: defaultValue is File or FileList
21
- * - select multiple: defaultValue is string array
22
- * - others: defaultValue is string
23
- */
24
- export declare function createDefaultSnapshot(defaultValue: string | string[] | File | File[] | FileList | null | undefined, defaultChecked: boolean | undefined, value: string | undefined): InputSnapshot;
10
+ export declare function resolveControlPayload(input: HTMLInputElement | HTMLSelectElement | HTMLTextAreaElement | HTMLFieldSetElement | Array<HTMLInputElement>): unknown;
11
+ export declare function deriveDefaultPayload(options: ControlOptions): unknown;
25
12
  /**
26
13
  * Focuses the first field with validation errors on default form submission.
27
14
  * Does nothing if the submission was triggered with a specific intent (e.g. validate / insert)
@@ -4,17 +4,20 @@ Object.defineProperty(exports, '__esModule', { value: true });
4
4
 
5
5
  var future = require('@conform-to/dom/future');
6
6
  var intent = require('./intent.js');
7
+ var state = require('./state.js');
7
8
 
8
9
  function getFormElement(formRef) {
9
- var _element$form;
10
- if (typeof formRef === 'string') {
11
- return document.forms.namedItem(formRef);
10
+ if (typeof formRef === 'undefined') {
11
+ return null;
12
12
  }
13
- var element = formRef === null || formRef === void 0 ? void 0 : formRef.current;
14
- if (element instanceof HTMLFormElement) {
15
- return element;
13
+ if (typeof formRef !== 'string') {
14
+ var element = formRef.current;
15
+ if (!element) {
16
+ return null;
17
+ }
18
+ return future.isGlobalInstance(element, 'HTMLFormElement') ? element : element.form;
16
19
  }
17
- return (_element$form = element === null || element === void 0 ? void 0 : element.form) !== null && _element$form !== void 0 ? _element$form : null;
20
+ return document.forms.namedItem(formRef);
18
21
  }
19
22
  function getSubmitEvent(event) {
20
23
  if (event.type !== 'submit') {
@@ -45,125 +48,69 @@ function initializeField(element, options) {
45
48
  });
46
49
  element.dataset.conform = 'initialized';
47
50
  }
48
-
49
- /**
50
- * Makes hidden form inputs focusable with visually hidden styles
51
- */
52
- function makeInputFocusable(element) {
53
- if (!element.hidden && element.type !== 'hidden') {
54
- return;
55
- }
56
-
57
- // Style the element to be visually hidden
58
- element.style.position = 'absolute';
59
- element.style.width = '1px';
60
- element.style.height = '1px';
61
- element.style.padding = '0';
62
- element.style.margin = '-1px';
63
- element.style.overflow = 'hidden';
64
- element.style.clip = 'rect(0,0,0,0)';
65
- element.style.whiteSpace = 'nowrap';
66
- element.style.border = '0';
67
-
68
- // Hide the element from screen readers
69
- element.setAttribute('aria-hidden', 'true');
70
-
71
- // Make sure people won't tab to this element
72
- element.tabIndex = -1;
73
-
74
- // Set the element to be visible again so it can be focused
75
- if (element.hidden) {
76
- element.hidden = false;
77
- }
78
- if (element.type === 'hidden') {
79
- element.setAttribute('type', 'text');
80
- }
81
- }
82
- function getRadioGroupValue(inputs) {
83
- for (var input of inputs) {
84
- if (input.type === 'radio' && input.checked) {
85
- return input.value;
86
- }
87
- }
88
- }
89
- function getCheckboxGroupValue(inputs) {
90
- var values;
91
- for (var input of inputs) {
92
- if (input.type === 'checkbox') {
93
- var _values;
94
- (_values = values) !== null && _values !== void 0 ? _values : values = [];
95
- if (input.checked) {
96
- values.push(input.value);
51
+ function resolveControlPayload(input) {
52
+ if (Array.isArray(input)) {
53
+ var options;
54
+ for (var element of input) {
55
+ if (element.type === 'radio' && element.checked) {
56
+ return element.value;
57
+ }
58
+ if (element.type === 'checkbox') {
59
+ var _options;
60
+ (_options = options) !== null && _options !== void 0 ? _options : options = [];
61
+ if (element.checked) {
62
+ options.push(element.value);
63
+ }
97
64
  }
98
65
  }
66
+ return options;
99
67
  }
100
- return values;
101
- }
102
- function getInputSnapshot(input) {
103
68
  if (input instanceof HTMLInputElement) {
104
69
  switch (input.type) {
105
70
  case 'file':
106
- return {
107
- files: input.files ? Array.from(input.files) : undefined
108
- };
71
+ {
72
+ return input.files ? Array.from(input.files) : [];
73
+ }
109
74
  case 'radio':
110
75
  case 'checkbox':
111
- return {
112
- value: input.value,
113
- checked: input.checked
114
- };
76
+ return input.checked ? input.value : null;
115
77
  }
116
78
  } else if (input instanceof HTMLSelectElement && input.multiple) {
117
- return {
118
- options: Array.from(input.selectedOptions).map(option => option.value)
119
- };
120
- }
121
- return {
122
- value: input.value
123
- };
124
- }
125
-
126
- /**
127
- * Creates an InputSnapshot based on the provided options:
128
- * - checkbox/radio: value / defaultChecked
129
- * - file inputs: defaultValue is File or FileList
130
- * - select multiple: defaultValue is string array
131
- * - others: defaultValue is string
132
- */
133
- function createDefaultSnapshot(defaultValue, defaultChecked, value) {
134
- if (typeof value === 'string' || typeof defaultChecked === 'boolean') {
135
- return {
136
- value: value !== null && value !== void 0 ? value : 'on',
137
- checked: defaultChecked
138
- };
139
- }
140
- if (typeof defaultValue === 'string') {
141
- return {
142
- value: defaultValue
143
- };
144
- }
145
- if (Array.isArray(defaultValue)) {
146
- if (defaultValue.every(item => typeof item === 'string')) {
147
- return {
148
- options: defaultValue
149
- };
150
- } else {
151
- return {
152
- files: defaultValue
153
- };
79
+ return Array.from(input.selectedOptions).map(option => option.value);
80
+ } else if (input instanceof HTMLFieldSetElement) {
81
+ if (input.elements.length === 0) {
82
+ return null;
154
83
  }
84
+ var result = {};
85
+ var entries = new Map();
86
+ for (var _element of input.elements) {
87
+ if (future.isFieldElement(_element)) {
88
+ var payload = resolveControlPayload(_element);
89
+ var value = entries.get(_element.name);
90
+ if (_element.type === 'checkbox') {
91
+ entries.set(_element.name, value === undefined ? payload : (Array.isArray(value) ? [...value, payload] : [value, payload]).filter(v => v !== null));
92
+ } else if (_element.type === 'radio') {
93
+ entries.set(_element.name, value == null ? payload : payload === null ? value : payload);
94
+ } else {
95
+ entries.set(_element.name, value === undefined ? payload : Array.isArray(value) ? [...value, payload] : [value, payload]);
96
+ }
97
+ }
98
+ }
99
+ for (var [name, _value] of entries) {
100
+ future.setPathValue(result, name, _value);
101
+ }
102
+ return future.getPathValue(result, input.name);
155
103
  }
156
- if (future.isGlobalInstance(defaultValue, 'File')) {
157
- return {
158
- files: [defaultValue]
159
- };
104
+ return input.value;
105
+ }
106
+ function deriveDefaultPayload(options) {
107
+ if ('defaultChecked' in options && typeof options.defaultChecked === 'boolean') {
108
+ var _options$value2;
109
+ return options.defaultChecked ? (_options$value2 = options.value) !== null && _options$value2 !== void 0 ? _options$value2 : 'on' : null;
160
110
  }
161
- if (future.isGlobalInstance(defaultValue, 'FileList')) {
162
- return {
163
- files: Array.from(defaultValue)
164
- };
111
+ if ('defaultValue' in options) {
112
+ return options.defaultValue;
165
113
  }
166
- return {};
167
114
  }
168
115
 
169
116
  /**
@@ -175,22 +122,33 @@ function focusFirstInvalidField(ctx) {
175
122
  return;
176
123
  }
177
124
  for (var element of ctx.formElement.elements) {
178
- var _ctx$error$fieldError;
179
- if (future.isFieldElement(element) && (_ctx$error$fieldError = ctx.error.fieldErrors[element.name]) !== null && _ctx$error$fieldError !== void 0 && _ctx$error$fieldError.length) {
125
+ if (!(future.isFieldElement(element) || element instanceof HTMLFieldSetElement) || element.name === '' || !state.hasFieldError(ctx.error, element.name)) {
126
+ continue;
127
+ }
128
+
129
+ // Treat fieldset as a focusable field only if it is hidden
130
+ if (element.type === 'fieldset' && !element.hidden) {
131
+ continue;
132
+ }
133
+ if (element.hidden || element.type === 'hidden' || element.type === 'fieldset') {
134
+ future.focus(element);
135
+ } else {
180
136
  element.focus();
181
- break;
182
137
  }
138
+ break;
183
139
  }
184
140
  }
185
141
  function updateFormValue(form, targetValue, serialize) {
186
142
  for (var element of form.elements) {
187
143
  if (future.isFieldElement(element) && element.name && element.type !== 'hidden') {
188
- var fieldValue = future.getValueAtPath(targetValue, element.name);
144
+ var fieldValue = future.getPathValue(targetValue, element.name);
189
145
  if (element.type === 'file' && fieldValue === undefined) {
190
146
  // Do not update file inputs unless there's a target value
191
147
  continue;
192
148
  }
193
- var value = serialize(fieldValue);
149
+ var value = serialize(fieldValue, {
150
+ name: element.name
151
+ });
194
152
 
195
153
  // Treat undefined as null to clear the field value
196
154
  future.change(element, value !== undefined ? value : null, {
@@ -202,8 +160,10 @@ function updateFormValue(form, targetValue, serialize) {
202
160
  function resetFormValue(form, defaultValue, serialize) {
203
161
  for (var element of form.elements) {
204
162
  if (future.isFieldElement(element) && element.name && element.type !== 'hidden' && element.type !== 'file') {
205
- var fieldValue = future.getValueAtPath(defaultValue, element.name);
206
- var value = serialize(fieldValue);
163
+ var fieldValue = future.getPathValue(defaultValue, element.name);
164
+ var value = serialize(fieldValue, {
165
+ name: element.name
166
+ });
207
167
  future.updateField(element, {
208
168
  defaultValue: value !== undefined ? value : null
209
169
  });
@@ -363,16 +323,13 @@ function preserveInputs(inputs, form, name) {
363
323
  }
364
324
 
365
325
  exports.cleanupPreservedInputs = cleanupPreservedInputs;
366
- exports.createDefaultSnapshot = createDefaultSnapshot;
367
326
  exports.createIntentDispatcher = createIntentDispatcher;
327
+ exports.deriveDefaultPayload = deriveDefaultPayload;
368
328
  exports.focusFirstInvalidField = focusFirstInvalidField;
369
- exports.getCheckboxGroupValue = getCheckboxGroupValue;
370
329
  exports.getFormElement = getFormElement;
371
- exports.getInputSnapshot = getInputSnapshot;
372
- exports.getRadioGroupValue = getRadioGroupValue;
373
330
  exports.getSubmitEvent = getSubmitEvent;
374
331
  exports.initializeField = initializeField;
375
- exports.makeInputFocusable = makeInputFocusable;
376
332
  exports.preserveInputs = preserveInputs;
377
333
  exports.resetFormValue = resetFormValue;
334
+ exports.resolveControlPayload = resolveControlPayload;
378
335
  exports.updateFormValue = updateFormValue;
@@ -1,16 +1,19 @@
1
- import { change, updateField, isGlobalInstance, isFieldElement, requestIntent, getValueAtPath } from '@conform-to/dom/future';
1
+ import { isGlobalInstance, change, updateField, isFieldElement, setPathValue, getPathValue, focus, requestIntent } from '@conform-to/dom/future';
2
2
  import { serializeIntent } from './intent.mjs';
3
+ import { hasFieldError } from './state.mjs';
3
4
 
4
5
  function getFormElement(formRef) {
5
- var _element$form;
6
- if (typeof formRef === 'string') {
7
- return document.forms.namedItem(formRef);
6
+ if (typeof formRef === 'undefined') {
7
+ return null;
8
8
  }
9
- var element = formRef === null || formRef === void 0 ? void 0 : formRef.current;
10
- if (element instanceof HTMLFormElement) {
11
- return element;
9
+ if (typeof formRef !== 'string') {
10
+ var element = formRef.current;
11
+ if (!element) {
12
+ return null;
13
+ }
14
+ return isGlobalInstance(element, 'HTMLFormElement') ? element : element.form;
12
15
  }
13
- return (_element$form = element === null || element === void 0 ? void 0 : element.form) !== null && _element$form !== void 0 ? _element$form : null;
16
+ return document.forms.namedItem(formRef);
14
17
  }
15
18
  function getSubmitEvent(event) {
16
19
  if (event.type !== 'submit') {
@@ -41,125 +44,69 @@ function initializeField(element, options) {
41
44
  });
42
45
  element.dataset.conform = 'initialized';
43
46
  }
44
-
45
- /**
46
- * Makes hidden form inputs focusable with visually hidden styles
47
- */
48
- function makeInputFocusable(element) {
49
- if (!element.hidden && element.type !== 'hidden') {
50
- return;
51
- }
52
-
53
- // Style the element to be visually hidden
54
- element.style.position = 'absolute';
55
- element.style.width = '1px';
56
- element.style.height = '1px';
57
- element.style.padding = '0';
58
- element.style.margin = '-1px';
59
- element.style.overflow = 'hidden';
60
- element.style.clip = 'rect(0,0,0,0)';
61
- element.style.whiteSpace = 'nowrap';
62
- element.style.border = '0';
63
-
64
- // Hide the element from screen readers
65
- element.setAttribute('aria-hidden', 'true');
66
-
67
- // Make sure people won't tab to this element
68
- element.tabIndex = -1;
69
-
70
- // Set the element to be visible again so it can be focused
71
- if (element.hidden) {
72
- element.hidden = false;
73
- }
74
- if (element.type === 'hidden') {
75
- element.setAttribute('type', 'text');
76
- }
77
- }
78
- function getRadioGroupValue(inputs) {
79
- for (var input of inputs) {
80
- if (input.type === 'radio' && input.checked) {
81
- return input.value;
82
- }
83
- }
84
- }
85
- function getCheckboxGroupValue(inputs) {
86
- var values;
87
- for (var input of inputs) {
88
- if (input.type === 'checkbox') {
89
- var _values;
90
- (_values = values) !== null && _values !== void 0 ? _values : values = [];
91
- if (input.checked) {
92
- values.push(input.value);
47
+ function resolveControlPayload(input) {
48
+ if (Array.isArray(input)) {
49
+ var options;
50
+ for (var element of input) {
51
+ if (element.type === 'radio' && element.checked) {
52
+ return element.value;
53
+ }
54
+ if (element.type === 'checkbox') {
55
+ var _options;
56
+ (_options = options) !== null && _options !== void 0 ? _options : options = [];
57
+ if (element.checked) {
58
+ options.push(element.value);
59
+ }
93
60
  }
94
61
  }
62
+ return options;
95
63
  }
96
- return values;
97
- }
98
- function getInputSnapshot(input) {
99
64
  if (input instanceof HTMLInputElement) {
100
65
  switch (input.type) {
101
66
  case 'file':
102
- return {
103
- files: input.files ? Array.from(input.files) : undefined
104
- };
67
+ {
68
+ return input.files ? Array.from(input.files) : [];
69
+ }
105
70
  case 'radio':
106
71
  case 'checkbox':
107
- return {
108
- value: input.value,
109
- checked: input.checked
110
- };
72
+ return input.checked ? input.value : null;
111
73
  }
112
74
  } else if (input instanceof HTMLSelectElement && input.multiple) {
113
- return {
114
- options: Array.from(input.selectedOptions).map(option => option.value)
115
- };
116
- }
117
- return {
118
- value: input.value
119
- };
120
- }
121
-
122
- /**
123
- * Creates an InputSnapshot based on the provided options:
124
- * - checkbox/radio: value / defaultChecked
125
- * - file inputs: defaultValue is File or FileList
126
- * - select multiple: defaultValue is string array
127
- * - others: defaultValue is string
128
- */
129
- function createDefaultSnapshot(defaultValue, defaultChecked, value) {
130
- if (typeof value === 'string' || typeof defaultChecked === 'boolean') {
131
- return {
132
- value: value !== null && value !== void 0 ? value : 'on',
133
- checked: defaultChecked
134
- };
135
- }
136
- if (typeof defaultValue === 'string') {
137
- return {
138
- value: defaultValue
139
- };
140
- }
141
- if (Array.isArray(defaultValue)) {
142
- if (defaultValue.every(item => typeof item === 'string')) {
143
- return {
144
- options: defaultValue
145
- };
146
- } else {
147
- return {
148
- files: defaultValue
149
- };
75
+ return Array.from(input.selectedOptions).map(option => option.value);
76
+ } else if (input instanceof HTMLFieldSetElement) {
77
+ if (input.elements.length === 0) {
78
+ return null;
150
79
  }
80
+ var result = {};
81
+ var entries = new Map();
82
+ for (var _element of input.elements) {
83
+ if (isFieldElement(_element)) {
84
+ var payload = resolveControlPayload(_element);
85
+ var value = entries.get(_element.name);
86
+ if (_element.type === 'checkbox') {
87
+ entries.set(_element.name, value === undefined ? payload : (Array.isArray(value) ? [...value, payload] : [value, payload]).filter(v => v !== null));
88
+ } else if (_element.type === 'radio') {
89
+ entries.set(_element.name, value == null ? payload : payload === null ? value : payload);
90
+ } else {
91
+ entries.set(_element.name, value === undefined ? payload : Array.isArray(value) ? [...value, payload] : [value, payload]);
92
+ }
93
+ }
94
+ }
95
+ for (var [name, _value] of entries) {
96
+ setPathValue(result, name, _value);
97
+ }
98
+ return getPathValue(result, input.name);
151
99
  }
152
- if (isGlobalInstance(defaultValue, 'File')) {
153
- return {
154
- files: [defaultValue]
155
- };
100
+ return input.value;
101
+ }
102
+ function deriveDefaultPayload(options) {
103
+ if ('defaultChecked' in options && typeof options.defaultChecked === 'boolean') {
104
+ var _options$value2;
105
+ return options.defaultChecked ? (_options$value2 = options.value) !== null && _options$value2 !== void 0 ? _options$value2 : 'on' : null;
156
106
  }
157
- if (isGlobalInstance(defaultValue, 'FileList')) {
158
- return {
159
- files: Array.from(defaultValue)
160
- };
107
+ if ('defaultValue' in options) {
108
+ return options.defaultValue;
161
109
  }
162
- return {};
163
110
  }
164
111
 
165
112
  /**
@@ -171,22 +118,33 @@ function focusFirstInvalidField(ctx) {
171
118
  return;
172
119
  }
173
120
  for (var element of ctx.formElement.elements) {
174
- var _ctx$error$fieldError;
175
- if (isFieldElement(element) && (_ctx$error$fieldError = ctx.error.fieldErrors[element.name]) !== null && _ctx$error$fieldError !== void 0 && _ctx$error$fieldError.length) {
121
+ if (!(isFieldElement(element) || element instanceof HTMLFieldSetElement) || element.name === '' || !hasFieldError(ctx.error, element.name)) {
122
+ continue;
123
+ }
124
+
125
+ // Treat fieldset as a focusable field only if it is hidden
126
+ if (element.type === 'fieldset' && !element.hidden) {
127
+ continue;
128
+ }
129
+ if (element.hidden || element.type === 'hidden' || element.type === 'fieldset') {
130
+ focus(element);
131
+ } else {
176
132
  element.focus();
177
- break;
178
133
  }
134
+ break;
179
135
  }
180
136
  }
181
137
  function updateFormValue(form, targetValue, serialize) {
182
138
  for (var element of form.elements) {
183
139
  if (isFieldElement(element) && element.name && element.type !== 'hidden') {
184
- var fieldValue = getValueAtPath(targetValue, element.name);
140
+ var fieldValue = getPathValue(targetValue, element.name);
185
141
  if (element.type === 'file' && fieldValue === undefined) {
186
142
  // Do not update file inputs unless there's a target value
187
143
  continue;
188
144
  }
189
- var value = serialize(fieldValue);
145
+ var value = serialize(fieldValue, {
146
+ name: element.name
147
+ });
190
148
 
191
149
  // Treat undefined as null to clear the field value
192
150
  change(element, value !== undefined ? value : null, {
@@ -198,8 +156,10 @@ function updateFormValue(form, targetValue, serialize) {
198
156
  function resetFormValue(form, defaultValue, serialize) {
199
157
  for (var element of form.elements) {
200
158
  if (isFieldElement(element) && element.name && element.type !== 'hidden' && element.type !== 'file') {
201
- var fieldValue = getValueAtPath(defaultValue, element.name);
202
- var value = serialize(fieldValue);
159
+ var fieldValue = getPathValue(defaultValue, element.name);
160
+ var value = serialize(fieldValue, {
161
+ name: element.name
162
+ });
203
163
  updateField(element, {
204
164
  defaultValue: value !== undefined ? value : null
205
165
  });
@@ -358,4 +318,4 @@ function preserveInputs(inputs, form, name) {
358
318
  }
359
319
  }
360
320
 
361
- export { cleanupPreservedInputs, createDefaultSnapshot, createIntentDispatcher, focusFirstInvalidField, getCheckboxGroupValue, getFormElement, getInputSnapshot, getRadioGroupValue, getSubmitEvent, initializeField, makeInputFocusable, preserveInputs, resetFormValue, updateFormValue };
321
+ export { cleanupPreservedInputs, createIntentDispatcher, deriveDefaultPayload, focusFirstInvalidField, getFormElement, getSubmitEvent, initializeField, preserveInputs, resetFormValue, resolveControlPayload, updateFormValue };