@conform-to/react 1.15.1 → 1.17.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.15.1 / License MIT / Copyright (c) 2025 Edmund Hung
10
+ Version 1.17.0 / License MIT / Copyright (c) 2025 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
 
package/dist/context.js CHANGED
@@ -189,6 +189,7 @@ function getFieldMetadata(context, subjectRef, stateSnapshot) {
189
189
  case 'pattern':
190
190
  case 'step':
191
191
  case 'multiple':
192
+ case 'accept':
192
193
  return (_state$constraint$nam = state.constraint[name]) === null || _state$constraint$nam === void 0 ? void 0 : _state$constraint$nam[key];
193
194
  case 'getFieldList':
194
195
  {
package/dist/context.mjs CHANGED
@@ -185,6 +185,7 @@ function getFieldMetadata(context, subjectRef, stateSnapshot) {
185
185
  case 'pattern':
186
186
  case 'step':
187
187
  case 'multiple':
188
+ case 'accept':
188
189
  return (_state$constraint$nam = state.constraint[name]) === null || _state$constraint$nam === void 0 ? void 0 : _state$constraint$nam[key];
189
190
  case 'getFieldList':
190
191
  {
@@ -34,4 +34,14 @@ export declare function resetFormValue(form: HTMLFormElement, defaultValue: Reco
34
34
  * Each property access returns a function that submits the intent to the form.
35
35
  */
36
36
  export declare function createIntentDispatcher<FormShape extends Record<string, any>>(formElement: HTMLFormElement | (() => HTMLFormElement | null), intentName: string): IntentDispatcher<FormShape>;
37
+ /**
38
+ * Restores values from preserved inputs and removes them.
39
+ * Called when PreserveBoundary mounts.
40
+ */
41
+ export declare function cleanupPreservedInputs(boundary: HTMLElement, form: HTMLFormElement, name?: string): void;
42
+ /**
43
+ * Clones inputs as hidden elements to preserve their values.
44
+ * Called when PreserveBoundary unmounts.
45
+ */
46
+ export declare function preserveInputs(inputs: Iterable<HTMLInputElement | HTMLSelectElement | HTMLTextAreaElement>, form: HTMLFormElement, name?: string): void;
37
47
  //# sourceMappingURL=dom.d.ts.map
@@ -237,7 +237,132 @@ function createIntentDispatcher(formElement, intentName) {
237
237
  }
238
238
  });
239
239
  }
240
+ var PERSIST_ATTR = 'data-conform-persist';
241
+ var containerCache = new WeakMap();
240
242
 
243
+ /**
244
+ * Gets or creates a hidden container for persisted inputs.
245
+ * Using a container div instead of appending directly to <form> provides ~10x
246
+ * better performance (form.elements bookkeeping is expensive at scale).
247
+ */
248
+ function getPersistContainer(form) {
249
+ var container = containerCache.get(form);
250
+
251
+ // Verify container is still attached to the form
252
+ if (container && container.parentNode !== form) {
253
+ container = undefined;
254
+ }
255
+ if (!container) {
256
+ container = form.ownerDocument.createElement('div');
257
+ container.setAttribute(PERSIST_ATTR, '');
258
+ container.hidden = true;
259
+ form.appendChild(container);
260
+ containerCache.set(form, container);
261
+ }
262
+ return container;
263
+ }
264
+
265
+ /**
266
+ * Restores values from preserved inputs and removes them.
267
+ * Called when PreserveBoundary mounts.
268
+ */
269
+ function cleanupPreservedInputs(boundary, form, name) {
270
+ var inputs = boundary.querySelectorAll('input,select,textarea');
271
+ var container = getPersistContainer(form);
272
+ for (var input of inputs) {
273
+ if (!future.isFieldElement(input) || !input.name) {
274
+ continue;
275
+ }
276
+
277
+ // For checkbox/radio, match by field name + value (+ boundary name if provided)
278
+ // For other inputs, match by field name only (+ boundary name if provided)
279
+ var isCheckboxOrRadio = input.type === 'checkbox' || input.type === 'radio';
280
+
281
+ // Query the persist container, not the whole form
282
+ var boundarySelector = name ? "[".concat(PERSIST_ATTR, "=\"").concat(name, "\"]") : '';
283
+ var selector = isCheckboxOrRadio ? "".concat(boundarySelector, "[name=\"").concat(input.name, "\"][value=\"").concat(input.value, "\"]") : "".concat(boundarySelector, "[name=\"").concat(input.name, "\"]");
284
+ var persisted = container.querySelector(selector);
285
+ if (persisted) {
286
+ if (input instanceof HTMLInputElement && persisted instanceof HTMLInputElement) {
287
+ if (isCheckboxOrRadio) {
288
+ input.checked = persisted.checked;
289
+ } else if (input.type === 'file') {
290
+ // Restore files from the persisted input (may be empty)
291
+ input.files = persisted.files;
292
+ } else {
293
+ input.value = persisted.value;
294
+ }
295
+ } else if (input instanceof HTMLSelectElement && persisted instanceof HTMLSelectElement) {
296
+ var _loop = function _loop(option) {
297
+ var _persistedOption$sele;
298
+ var persistedOption = Array.from(persisted.options).find(o => o.value === option.value);
299
+ option.selected = (_persistedOption$sele = persistedOption === null || persistedOption === void 0 ? void 0 : persistedOption.selected) !== null && _persistedOption$sele !== void 0 ? _persistedOption$sele : false;
300
+ };
301
+ for (var option of input.options) {
302
+ _loop(option);
303
+ }
304
+ } else if (input instanceof HTMLTextAreaElement && persisted instanceof HTMLTextAreaElement) {
305
+ input.value = persisted.value;
306
+ }
307
+ persisted.remove();
308
+ }
309
+ }
310
+
311
+ // If name is provided, remove any remaining persisted inputs with this name
312
+ // (handles the case where inputs were removed from the boundary)
313
+ if (name) {
314
+ var remainingPersisted = container.querySelectorAll("[".concat(PERSIST_ATTR, "=\"").concat(name, "\"]"));
315
+ remainingPersisted.forEach(el => el.remove());
316
+ }
317
+ }
318
+
319
+ /**
320
+ * Clones inputs as hidden elements to preserve their values.
321
+ * Called when PreserveBoundary unmounts.
322
+ */
323
+ function preserveInputs(inputs, form, name) {
324
+ // Get the persist container once, outside the loop
325
+ var container = getPersistContainer(form);
326
+ for (var input of inputs) {
327
+ if (!future.isFieldElement(input) || !input.name) {
328
+ continue;
329
+ }
330
+
331
+ // Skip unchecked checkbox/radio (they don't contribute to FormData)
332
+ if (input instanceof HTMLInputElement && (input.type === 'checkbox' || input.type === 'radio') && !input.checked) {
333
+ continue;
334
+ }
335
+
336
+ // Clone the input element
337
+ var clone = input.cloneNode(true);
338
+
339
+ // Mark with name if provided, and hide it
340
+ if (name) {
341
+ clone.setAttribute(PERSIST_ATTR, name);
342
+ }
343
+ clone.hidden = true;
344
+
345
+ // Copy dynamic state that cloneNode doesn't preserve
346
+ if (input instanceof HTMLSelectElement) {
347
+ // cloneNode doesn't copy selected state for options
348
+ for (var i = 0; i < input.options.length; i++) {
349
+ var inputOption = input.options[i];
350
+ var cloneOption = clone.options[i];
351
+ if (inputOption && cloneOption) {
352
+ cloneOption.selected = inputOption.selected;
353
+ }
354
+ }
355
+ } else if (input instanceof HTMLInputElement && input.type === 'file') {
356
+ // cloneNode doesn't copy files
357
+ clone.files = input.files;
358
+ }
359
+
360
+ // Append to persist container (faster than appending directly to form)
361
+ container.appendChild(clone);
362
+ }
363
+ }
364
+
365
+ exports.cleanupPreservedInputs = cleanupPreservedInputs;
241
366
  exports.createDefaultSnapshot = createDefaultSnapshot;
242
367
  exports.createIntentDispatcher = createIntentDispatcher;
243
368
  exports.focusFirstInvalidField = focusFirstInvalidField;
@@ -248,5 +373,6 @@ exports.getRadioGroupValue = getRadioGroupValue;
248
373
  exports.getSubmitEvent = getSubmitEvent;
249
374
  exports.initializeField = initializeField;
250
375
  exports.makeInputFocusable = makeInputFocusable;
376
+ exports.preserveInputs = preserveInputs;
251
377
  exports.resetFormValue = resetFormValue;
252
378
  exports.updateFormValue = updateFormValue;
@@ -233,5 +233,129 @@ function createIntentDispatcher(formElement, intentName) {
233
233
  }
234
234
  });
235
235
  }
236
+ var PERSIST_ATTR = 'data-conform-persist';
237
+ var containerCache = new WeakMap();
236
238
 
237
- export { createDefaultSnapshot, createIntentDispatcher, focusFirstInvalidField, getCheckboxGroupValue, getFormElement, getInputSnapshot, getRadioGroupValue, getSubmitEvent, initializeField, makeInputFocusable, resetFormValue, updateFormValue };
239
+ /**
240
+ * Gets or creates a hidden container for persisted inputs.
241
+ * Using a container div instead of appending directly to <form> provides ~10x
242
+ * better performance (form.elements bookkeeping is expensive at scale).
243
+ */
244
+ function getPersistContainer(form) {
245
+ var container = containerCache.get(form);
246
+
247
+ // Verify container is still attached to the form
248
+ if (container && container.parentNode !== form) {
249
+ container = undefined;
250
+ }
251
+ if (!container) {
252
+ container = form.ownerDocument.createElement('div');
253
+ container.setAttribute(PERSIST_ATTR, '');
254
+ container.hidden = true;
255
+ form.appendChild(container);
256
+ containerCache.set(form, container);
257
+ }
258
+ return container;
259
+ }
260
+
261
+ /**
262
+ * Restores values from preserved inputs and removes them.
263
+ * Called when PreserveBoundary mounts.
264
+ */
265
+ function cleanupPreservedInputs(boundary, form, name) {
266
+ var inputs = boundary.querySelectorAll('input,select,textarea');
267
+ var container = getPersistContainer(form);
268
+ for (var input of inputs) {
269
+ if (!isFieldElement(input) || !input.name) {
270
+ continue;
271
+ }
272
+
273
+ // For checkbox/radio, match by field name + value (+ boundary name if provided)
274
+ // For other inputs, match by field name only (+ boundary name if provided)
275
+ var isCheckboxOrRadio = input.type === 'checkbox' || input.type === 'radio';
276
+
277
+ // Query the persist container, not the whole form
278
+ var boundarySelector = name ? "[".concat(PERSIST_ATTR, "=\"").concat(name, "\"]") : '';
279
+ var selector = isCheckboxOrRadio ? "".concat(boundarySelector, "[name=\"").concat(input.name, "\"][value=\"").concat(input.value, "\"]") : "".concat(boundarySelector, "[name=\"").concat(input.name, "\"]");
280
+ var persisted = container.querySelector(selector);
281
+ if (persisted) {
282
+ if (input instanceof HTMLInputElement && persisted instanceof HTMLInputElement) {
283
+ if (isCheckboxOrRadio) {
284
+ input.checked = persisted.checked;
285
+ } else if (input.type === 'file') {
286
+ // Restore files from the persisted input (may be empty)
287
+ input.files = persisted.files;
288
+ } else {
289
+ input.value = persisted.value;
290
+ }
291
+ } else if (input instanceof HTMLSelectElement && persisted instanceof HTMLSelectElement) {
292
+ var _loop = function _loop(option) {
293
+ var _persistedOption$sele;
294
+ var persistedOption = Array.from(persisted.options).find(o => o.value === option.value);
295
+ option.selected = (_persistedOption$sele = persistedOption === null || persistedOption === void 0 ? void 0 : persistedOption.selected) !== null && _persistedOption$sele !== void 0 ? _persistedOption$sele : false;
296
+ };
297
+ for (var option of input.options) {
298
+ _loop(option);
299
+ }
300
+ } else if (input instanceof HTMLTextAreaElement && persisted instanceof HTMLTextAreaElement) {
301
+ input.value = persisted.value;
302
+ }
303
+ persisted.remove();
304
+ }
305
+ }
306
+
307
+ // If name is provided, remove any remaining persisted inputs with this name
308
+ // (handles the case where inputs were removed from the boundary)
309
+ if (name) {
310
+ var remainingPersisted = container.querySelectorAll("[".concat(PERSIST_ATTR, "=\"").concat(name, "\"]"));
311
+ remainingPersisted.forEach(el => el.remove());
312
+ }
313
+ }
314
+
315
+ /**
316
+ * Clones inputs as hidden elements to preserve their values.
317
+ * Called when PreserveBoundary unmounts.
318
+ */
319
+ function preserveInputs(inputs, form, name) {
320
+ // Get the persist container once, outside the loop
321
+ var container = getPersistContainer(form);
322
+ for (var input of inputs) {
323
+ if (!isFieldElement(input) || !input.name) {
324
+ continue;
325
+ }
326
+
327
+ // Skip unchecked checkbox/radio (they don't contribute to FormData)
328
+ if (input instanceof HTMLInputElement && (input.type === 'checkbox' || input.type === 'radio') && !input.checked) {
329
+ continue;
330
+ }
331
+
332
+ // Clone the input element
333
+ var clone = input.cloneNode(true);
334
+
335
+ // Mark with name if provided, and hide it
336
+ if (name) {
337
+ clone.setAttribute(PERSIST_ATTR, name);
338
+ }
339
+ clone.hidden = true;
340
+
341
+ // Copy dynamic state that cloneNode doesn't preserve
342
+ if (input instanceof HTMLSelectElement) {
343
+ // cloneNode doesn't copy selected state for options
344
+ for (var i = 0; i < input.options.length; i++) {
345
+ var inputOption = input.options[i];
346
+ var cloneOption = clone.options[i];
347
+ if (inputOption && cloneOption) {
348
+ cloneOption.selected = inputOption.selected;
349
+ }
350
+ }
351
+ } else if (input instanceof HTMLInputElement && input.type === 'file') {
352
+ // cloneNode doesn't copy files
353
+ clone.files = input.files;
354
+ }
355
+
356
+ // Append to persist container (faster than appending directly to form)
357
+ container.appendChild(clone);
358
+ }
359
+ }
360
+
361
+ export { cleanupPreservedInputs, createDefaultSnapshot, createIntentDispatcher, focusFirstInvalidField, getCheckboxGroupValue, getFormElement, getInputSnapshot, getRadioGroupValue, getSubmitEvent, initializeField, makeInputFocusable, preserveInputs, resetFormValue, updateFormValue };
@@ -0,0 +1,43 @@
1
+ import { FieldName } from '@conform-to/dom';
2
+ import { StandardSchemaV1 } from './standard-schema';
3
+ import { FormRef, FormsConfig, FormContext, FormMetadata, FormOptions, Fieldset, FieldMetadata, InferOutput, InferInput, IntentDispatcher } from './types';
4
+ export declare function configureForms<BaseErrorShape = string, BaseSchema = StandardSchemaV1, CustomFormMetadata extends Record<string, unknown> = {}, CustomFieldMetadata extends Record<string, unknown> = {}>(config?: Partial<FormsConfig<BaseErrorShape, BaseSchema, CustomFormMetadata, CustomFieldMetadata>>): {
5
+ FormProvider: (props: {
6
+ context: FormContext<BaseErrorShape>;
7
+ children: React.ReactNode;
8
+ }) => React.ReactElement;
9
+ useForm: {
10
+ <Schema extends BaseSchema, ErrorShape extends BaseErrorShape = BaseErrorShape, Value = InferOutput<Schema>>(schema: Schema, options: FormOptions<InferInput<Schema> extends Record<string, any> ? InferInput<Schema> : never, ErrorShape, Value, Schema, string extends ErrorShape ? never : "onValidate">): {
11
+ form: FormMetadata<ErrorShape, CustomFormMetadata, CustomFieldMetadata>;
12
+ fields: Fieldset<InferInput<Schema>, ErrorShape, CustomFieldMetadata>;
13
+ intent: IntentDispatcher<InferInput<Schema> extends Record<string, any> ? InferInput<Schema> : never>;
14
+ };
15
+ <FormShape extends Record<string, any> = Record<string, any>, ErrorShape extends BaseErrorShape = BaseErrorShape, Value = undefined>(options: FormOptions<FormShape, ErrorShape, Value, undefined, undefined extends Value ? "onValidate" : never> & {
16
+ /**
17
+ * @deprecated Use `useForm(schema, options)` instead for better type inference.
18
+ *
19
+ * Optional standard schema for validation (e.g., Zod, Valibot, Yup).
20
+ * Removes the need for manual onValidate setup.
21
+ */
22
+ schema: StandardSchemaV1<FormShape, Value>;
23
+ }): {
24
+ form: FormMetadata<ErrorShape, CustomFormMetadata, CustomFieldMetadata>;
25
+ fields: Fieldset<FormShape, ErrorShape, CustomFieldMetadata>;
26
+ intent: IntentDispatcher<FormShape>;
27
+ };
28
+ <FormShape extends Record<string, any> = Record<string, any>, ErrorShape extends BaseErrorShape = BaseErrorShape, Value = undefined>(options: FormOptions<FormShape, ErrorShape, Value, undefined, "onValidate">): {
29
+ form: FormMetadata<ErrorShape, CustomFormMetadata, CustomFieldMetadata>;
30
+ fields: Fieldset<FormShape, ErrorShape, CustomFieldMetadata>;
31
+ intent: IntentDispatcher<FormShape>;
32
+ };
33
+ };
34
+ useFormMetadata: (options?: {
35
+ formId?: string;
36
+ }) => FormMetadata<BaseErrorShape, CustomFormMetadata, CustomFieldMetadata>;
37
+ useField: <FieldShape = any>(name: FieldName<FieldShape>, options?: {
38
+ formId?: string;
39
+ }) => FieldMetadata<FieldShape, BaseErrorShape, CustomFieldMetadata>;
40
+ useIntent: <FormShape extends Record<string, any>>(formRef: FormRef) => IntentDispatcher<FormShape>;
41
+ config: FormsConfig<BaseErrorShape, BaseSchema, CustomFormMetadata, CustomFieldMetadata>;
42
+ };
43
+ //# sourceMappingURL=forms.d.ts.map