@conform-to/react 1.17.0 → 1.18.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.
@@ -1,6 +1,6 @@
1
1
  import { type FieldName, type FormValue, type Serialize, type SubmissionResult, createGlobalFormsObserver } from '@conform-to/dom/future';
2
2
  import { useEffect } from 'react';
3
- import type { FormContext, IntentDispatcher, FormMetadata, Fieldset, GlobalFormOptions, FormOptions, FieldMetadata, Control, Selector, UseFormDataOptions, ValidateHandler, ErrorHandler, SubmitHandler, FormState, FormRef, BaseErrorShape, DefaultErrorShape, BaseSchemaType, InferInput, InferOutput } from './types';
3
+ import type { FormContext, IntentDispatcher, FormMetadata, Fieldset, GlobalFormOptions, FormOptions, FieldMetadata, Control, Selector, UseFormDataOptions, ValidateHandler, ErrorHandler, SubmitHandler, FormState, FormRef, BaseErrorShape, DefaultErrorShape, BaseSchemaType, InferInput, InferOutput, BaseControlProps, StandardControlOptions, DefaultControlValue, CheckedControlOptions, CustomControlOptions } from './types';
4
4
  import { StandardSchemaV1 } from './standard-schema';
5
5
  export declare const INITIAL_KEY = "INITIAL_KEY";
6
6
  export declare const GlobalFormOptionsContext: import("react").Context<GlobalFormOptions & {
@@ -194,7 +194,7 @@ export declare function useField<FieldShape = any>(name: FieldName<FieldShape>,
194
194
  export declare function useIntent<FormShape extends Record<string, any>>(formRef: FormRef): IntentDispatcher<FormShape>;
195
195
  /**
196
196
  * A React hook that lets you sync the state of an input and dispatch native form events from it.
197
- * This is useful when emulating native input behavior — typically by rendering a hidden base input
197
+ * This is useful when emulating native input behavior — typically by rendering a hidden base control
198
198
  * and syncing it with a custom input.
199
199
  *
200
200
  * @example
@@ -202,28 +202,9 @@ export declare function useIntent<FormShape extends Record<string, any>>(formRef
202
202
  * const control = useControl(options);
203
203
  * ```
204
204
  */
205
- export declare function useControl(options?: {
206
- /**
207
- * The initial value of the base input. It will be used to set the value
208
- * when the input is first registered.
209
- */
210
- defaultValue?: string | string[] | File | File[] | null | undefined;
211
- /**
212
- * Whether the base input should be checked by default. It will be applied
213
- * when the input is first registered.
214
- */
215
- defaultChecked?: boolean | undefined;
216
- /**
217
- * The value of a checkbox or radio input when checked. This sets the
218
- * value attribute of the base input.
219
- */
220
- value?: string;
221
- /**
222
- * A callback function that is triggered when the base input is focused.
223
- * Use this to delegate focus to a custom input.
224
- */
225
- onFocus?: () => void;
226
- }): Control;
205
+ export declare function useControl<Value, DefaultValue>(options: CustomControlOptions<Value, DefaultValue>): Control<Value, DefaultValue, Value>;
206
+ export declare function useControl<Value extends DefaultControlValue>(options?: StandardControlOptions<Value>): Control<Value>;
207
+ export declare function useControl(options: CheckedControlOptions): Control<boolean, string>;
227
208
  /**
228
209
  * A React hook that lets you subscribe to the current `FormData` of a form and derive a custom value from it.
229
210
  * The selector runs whenever the form's structure or data changes, and the hook re-renders only when the result is deeply different.
@@ -268,4 +249,37 @@ export declare const useSafeLayoutEffect: typeof useEffect;
268
249
  * Useful to avoid stale closures in event handlers or async callbacks.
269
250
  */
270
251
  export declare function useLatest<Value>(value: Value): import("react").MutableRefObject<Value>;
252
+ /**
253
+ * A component that renders hidden base control(s) based on the shape of defaultValue.
254
+ * Used with useControl to sync complex values with form data.
255
+ *
256
+ * @example
257
+ * ```tsx
258
+ * const control = useControl<{ street: string; city: string }>({
259
+ * defaultValue: { street: '123 Main St', city: 'Anytown' },
260
+ * parse(payload) {
261
+ * if (
262
+ * typeof payload === 'object' &&
263
+ * payload !== null &&
264
+ * 'street' in payload &&
265
+ * 'city' in payload &&
266
+ * typeof payload.street === 'string' &&
267
+ * typeof payload.city === 'string'
268
+ * ) {
269
+ * return payload;
270
+ * }
271
+ *
272
+ * throw new Error('Unexpected payload shape');
273
+ * },
274
+ * });
275
+ *
276
+ * <BaseControl
277
+ * type="fieldset"
278
+ * name="address"
279
+ * ref={control.register}
280
+ * defaultValue={control.defaultValue}
281
+ * />
282
+ * ```
283
+ */
284
+ export declare const BaseControl: import("react").ForwardRefExoticComponent<BaseControlProps & import("react").RefAttributes<HTMLInputElement | HTMLSelectElement | HTMLTextAreaElement | HTMLFieldSetElement>>;
271
285
  //# sourceMappingURL=hooks.d.ts.map
@@ -10,11 +10,15 @@ var util = require('./util.js');
10
10
  var state = require('./state.js');
11
11
  var intent = require('./intent.js');
12
12
  var dom = require('./dom.js');
13
+ var reactDom = require('react-dom');
13
14
  var jsxRuntime = require('react/jsx-runtime');
14
15
 
15
- var _excluded = ["children"];
16
- // Static reset key for consistent hydration during Next.js prerendering
17
- // See: https://nextjs.org/docs/messages/next-prerender-current-time-client
16
+ var _excluded = ["children"],
17
+ _excluded2 = ["name", "form", "defaultValue", "hidden"],
18
+ _excluded3 = ["defaultValue", "multiple", "hidden"],
19
+ _excluded4 = ["defaultValue", "hidden"],
20
+ _excluded5 = ["defaultValue", "value", "hidden"],
21
+ _excluded6 = ["defaultValue", "hidden"];
18
22
  var INITIAL_KEY = 'INITIAL_KEY';
19
23
  var GlobalFormOptionsContext = /*#__PURE__*/react.createContext({
20
24
  intentName: future.DEFAULT_INTENT_NAME,
@@ -150,6 +154,10 @@ function useConform(formRef, options) {
150
154
  var finalResult = intent.applyIntent(result, intent$1, {
151
155
  handlers: intent.intentHandlers
152
156
  });
157
+ var formElement = dom.getFormElement(formRef);
158
+ if (formElement && (finalResult.reset || typeof finalResult.targetValue !== 'undefined')) {
159
+ future.dispatchInternalUpdateEvent(formElement);
160
+ }
153
161
  setState(state$1 => state.updateState(state$1, _rollupPluginBabelHelpers.objectSpread2(_rollupPluginBabelHelpers.objectSpread2({}, finalResult), {}, {
154
162
  type,
155
163
  intent: intent$1,
@@ -165,7 +173,6 @@ function useConform(formRef, options) {
165
173
  })));
166
174
 
167
175
  // TODO: move on error handler to a new effect
168
- var formElement = dom.getFormElement(formRef);
169
176
  if (formElement && result.error) {
170
177
  var _optionsRef$current$o, _optionsRef$current;
171
178
  (_optionsRef$current$o = (_optionsRef$current = optionsRef.current).onError) === null || _optionsRef$current$o === void 0 || _optionsRef$current$o.call(_optionsRef$current, {
@@ -178,6 +185,10 @@ function useConform(formRef, options) {
178
185
  }, [formRef, optionsRef]);
179
186
  if (options.key !== keyRef.current) {
180
187
  keyRef.current = options.key;
188
+ var formElement = dom.getFormElement(formRef);
189
+ if (formElement) {
190
+ future.dispatchInternalUpdateEvent(formElement);
191
+ }
181
192
  setState(state.initializeState({
182
193
  defaultValue: options.defaultValue
183
194
  }));
@@ -204,52 +215,56 @@ function useConform(formRef, options) {
204
215
  }, [formRef, state$1.resetKey, state$1.defaultValue, optionsRef]);
205
216
  useSafeLayoutEffect(() => {
206
217
  if (state$1.targetValue) {
207
- var formElement = dom.getFormElement(formRef);
208
- if (!formElement) {
218
+ var _formElement = dom.getFormElement(formRef);
219
+ if (!_formElement) {
209
220
  // eslint-disable-next-line no-console
210
221
  console.error('Failed to update form value; No form element found');
211
222
  return;
212
223
  }
213
- dom.updateFormValue(formElement, state$1.targetValue, optionsRef.current.serialize);
224
+ dom.updateFormValue(_formElement, state$1.targetValue, optionsRef.current.serialize);
214
225
  }
215
226
  pendingValueRef.current = undefined;
216
227
  }, [formRef, state$1.targetValue, optionsRef]);
217
228
  var handleSubmit = react.useCallback(event => {
218
- var _abortControllerRef$c2, _lastAsyncResultRef$c;
229
+ var _abortControllerRef$c2;
219
230
  var abortController = new AbortController();
220
231
 
221
232
  // Keep track of the abort controller so we can cancel the previous request if a new one is made
222
233
  (_abortControllerRef$c2 = abortControllerRef.current) === null || _abortControllerRef$c2 === void 0 || _abortControllerRef$c2.abort('A new submission is made');
223
234
  abortControllerRef.current = abortController;
224
- var formData;
225
235
  var result;
226
236
  var resolvedValue;
237
+ var formElement = event.currentTarget;
238
+ var submitEvent = dom.getSubmitEvent(event);
239
+ var formData = future.getFormData(formElement, submitEvent.submitter);
240
+ var submission = future.parseSubmission(formData, {
241
+ intentName: optionsRef.current.intentName
242
+ });
227
243
 
228
- // The form might be re-submitted manually if there was an async validation
229
- if (event.nativeEvent === ((_lastAsyncResultRef$c = lastAsyncResultRef.current) === null || _lastAsyncResultRef$c === void 0 ? void 0 : _lastAsyncResultRef$c.event)) {
230
- formData = lastAsyncResultRef.current.formData;
231
- result = lastAsyncResultRef.current.result;
232
- resolvedValue = lastAsyncResultRef.current.resolvedValue;
233
- } else {
234
- var _optionsRef$current$o2, _optionsRef$current2;
235
- var formElement = event.currentTarget;
236
- var submitEvent = dom.getSubmitEvent(event);
237
- formData = future.getFormData(formElement, submitEvent.submitter);
238
- var submission = future.parseSubmission(formData, {
239
- intentName: optionsRef.current.intentName
240
- });
241
-
242
- // Patch missing fields in the submission object
243
- for (var element of formElement.elements) {
244
- if (future.isFieldElement(element) && element.name) {
245
- submission.fields = util.appendUniqueItem(submission.fields, element.name);
246
- }
244
+ // Patch missing fields in the submission object
245
+ for (var element of formElement.elements) {
246
+ if (future.isFieldElement(element) && element.name) {
247
+ submission.fields = util.appendUniqueItem(submission.fields, element.name);
247
248
  }
249
+ }
248
250
 
249
- // Override submission value if the pending value is not applied yet (i.e. batch updates)
250
- if (pendingValueRef.current !== undefined) {
251
- submission.payload = pendingValueRef.current;
252
- }
251
+ // Override submission value if the pending value is not applied yet (i.e. batch updates)
252
+ if (pendingValueRef.current !== undefined) {
253
+ submission.payload = pendingValueRef.current;
254
+ }
255
+ var lastAsyncResult = lastAsyncResultRef.current;
256
+
257
+ // Clear the last async result so it won't affect the next submission
258
+ lastAsyncResultRef.current = null;
259
+ if (lastAsyncResult &&
260
+ // Only default submission will be re-submitted after async validation
261
+ !submission.intent &&
262
+ // Ensure the submission payload is the same as the one being validated
263
+ future.deepEqual(submission.payload, lastAsyncResult.result.submission.payload)) {
264
+ result = lastAsyncResult.result;
265
+ resolvedValue = lastAsyncResult.resolvedValue;
266
+ } else {
267
+ var _optionsRef$current$o2, _optionsRef$current2;
253
268
  var value = intent.resolveIntent(submission);
254
269
  var submissionResult = future.report(submission, {
255
270
  keepFiles: true,
@@ -294,16 +309,20 @@ function useConform(formRef, options) {
294
309
 
295
310
  // If the form is meant to be submitted and there is no error
296
311
  if (submissionResult.error === null && !submission.intent) {
297
- var _event = future.createSubmitEvent(submitEvent.submitter);
298
-
299
- // Keep track of the submit event so we can skip validation on the next submit
300
- lastAsyncResultRef.current = {
301
- event: _event,
302
- formData,
303
- resolvedValue: value,
304
- result: submissionResult
305
- };
306
- formElement.dispatchEvent(_event);
312
+ // Keep track of the validated payload and resume submission on the next task.
313
+ // Calling requestSubmit() directly from the async callback, or from a
314
+ // microtask, can still be ignored before the native submission lifecycle
315
+ // has fully settled.
316
+ setTimeout(() => {
317
+ if (abortController.signal.aborted) {
318
+ return;
319
+ }
320
+ lastAsyncResultRef.current = {
321
+ resolvedValue: value,
322
+ result: submissionResult
323
+ };
324
+ future.requestSubmit(formElement, submitEvent.submitter);
325
+ }, 0);
307
326
  }
308
327
  }
309
328
  });
@@ -618,7 +637,7 @@ function useIntent(formRef) {
618
637
 
619
638
  /**
620
639
  * A React hook that lets you sync the state of an input and dispatch native form events from it.
621
- * This is useful when emulating native input behavior — typically by rendering a hidden base input
640
+ * This is useful when emulating native input behavior — typically by rendering a hidden base control
622
641
  * and syncing it with a custom input.
623
642
  *
624
643
  * @example
@@ -626,7 +645,9 @@ function useIntent(formRef) {
626
645
  * const control = useControl(options);
627
646
  * ```
628
647
  */
629
- function useControl(options) {
648
+
649
+ function useControl() {
650
+ var options = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {};
630
651
  var {
631
652
  observer
632
653
  } = react.useContext(GlobalFormOptionsContext);
@@ -641,29 +662,40 @@ function useControl(options) {
641
662
  return Array.isArray(input) ? (_input$0$form = (_input$ = input[0]) === null || _input$ === void 0 ? void 0 : _input$.form) !== null && _input$0$form !== void 0 ? _input$0$form : null : input.form;
642
663
  }
643
664
  }), []);
665
+ var [defaultValue, setDefaultValue] = react.useState(() => dom.deriveDefaultPayload(options));
666
+ var pendingDefaultValueSyncRef = react.useRef(false);
667
+
668
+ /**
669
+ * Keep defaultValue in sync with external option updates during render.
670
+ * This is required for structural controls where hidden descendants must be
671
+ * rendered in the same cycle as form state updates (e.g. update intents).
672
+ */
673
+ if (pendingDefaultValueSyncRef.current && inputRef.current && future.isGlobalInstance(inputRef.current, 'HTMLFieldSetElement')) {
674
+ pendingDefaultValueSyncRef.current = false;
675
+ setDefaultValue(() => dom.deriveDefaultPayload(options));
676
+ }
644
677
  var eventDispatched = react.useRef({});
645
- var defaultSnapshot = dom.createDefaultSnapshot(options === null || options === void 0 ? void 0 : options.defaultValue, options === null || options === void 0 ? void 0 : options.defaultChecked, options === null || options === void 0 ? void 0 : options.value);
646
- var snapshotRef = react.useRef(defaultSnapshot);
678
+ var snapshotRef = react.useRef(defaultValue);
647
679
  var optionsRef = react.useRef(options);
648
680
  react.useEffect(() => {
649
681
  optionsRef.current = options;
650
682
  });
651
-
652
- // This is necessary to ensure that input is re-registered
653
- // if the onFocus handler changes
654
- var shouldHandleFocus = typeof (options === null || options === void 0 ? void 0 : options.onFocus) === 'function';
655
- var snapshot = react.useSyncExternalStore(react.useCallback(callback => observer.onFieldUpdate(event => {
683
+ react.useEffect(() => observer.onInternalUpdate(event => {
684
+ var input = inputRef.current;
685
+ if (input && input instanceof HTMLFieldSetElement && event.target === input.form) {
686
+ pendingDefaultValueSyncRef.current = true;
687
+ }
688
+ }), [observer]);
689
+ var payloadSnapshot = react.useSyncExternalStore(react.useCallback(callback => observer.onFieldUpdate(event => {
690
+ var _inputRef$current;
656
691
  var input = event.target;
657
- if (Array.isArray(inputRef.current) ? inputRef.current.some(item => item === input) : inputRef.current === input) {
692
+ if (Array.isArray(inputRef.current) ? inputRef.current.some(item => item === input) : (_inputRef$current = inputRef.current) === null || _inputRef$current === void 0 ? void 0 : _inputRef$current.contains(input)) {
658
693
  callback();
659
694
  }
660
695
  }), [observer]), () => {
661
696
  var input = inputRef.current;
662
697
  var prev = snapshotRef.current;
663
- var next = !input ? defaultSnapshot : Array.isArray(input) ? {
664
- value: dom.getRadioGroupValue(input),
665
- options: dom.getCheckboxGroupValue(input)
666
- } : dom.getInputSnapshot(input);
698
+ var next = input ? dom.resolveControlPayload(input) : defaultValue;
667
699
  if (future.deepEqual(prev, next)) {
668
700
  return prev;
669
701
  }
@@ -673,7 +705,8 @@ function useControl(options) {
673
705
  react.useEffect(() => {
674
706
  var createEventListener = listener => {
675
707
  return event => {
676
- if (Array.isArray(inputRef.current) ? inputRef.current.some(item => item === event.target) : inputRef.current === event.target) {
708
+ var _inputRef$current2;
709
+ if (Array.isArray(inputRef.current) ? inputRef.current.some(item => item === event.target) : event.target instanceof Node && ((_inputRef$current2 = inputRef.current) === null || _inputRef$current2 === void 0 ? void 0 : _inputRef$current2.contains(event.target))) {
677
710
  var timer = eventDispatched.current[listener];
678
711
  if (timer) {
679
712
  clearTimeout(timer);
@@ -701,10 +734,62 @@ function useControl(options) {
701
734
  };
702
735
  }, []);
703
736
  return {
704
- value: snapshot.value,
705
- checked: snapshot.checked,
706
- options: snapshot.options,
707
- files: snapshot.files,
737
+ defaultValue,
738
+ get payload() {
739
+ if (payloadSnapshot != null && 'parse' in options) {
740
+ try {
741
+ return options.parse(payloadSnapshot);
742
+ } catch (error) {
743
+ var payloadText = '';
744
+ try {
745
+ payloadText = JSON.stringify(payloadSnapshot, null, 2);
746
+ } catch (_unused) {
747
+ payloadText = '<unserializable payload>';
748
+ }
749
+ throw new Error("Failed to parse the payload. Received ".concat(payloadText, "."), {
750
+ cause: error
751
+ });
752
+ }
753
+ }
754
+ return payloadSnapshot;
755
+ },
756
+ get value() {
757
+ if (payloadSnapshot === null) {
758
+ return '';
759
+ }
760
+ if (typeof payloadSnapshot === 'string') {
761
+ return payloadSnapshot;
762
+ }
763
+ return undefined;
764
+ },
765
+ get checked() {
766
+ if (payloadSnapshot === null) {
767
+ return false;
768
+ }
769
+ var value = 'value' in options && options.value ? options.value : 'on';
770
+ if (payloadSnapshot === value) {
771
+ return true;
772
+ }
773
+ return undefined;
774
+ },
775
+ get options() {
776
+ if (payloadSnapshot === null) {
777
+ return [];
778
+ }
779
+ if (Array.isArray(payloadSnapshot) && payloadSnapshot.every(item => typeof item === 'string')) {
780
+ return payloadSnapshot;
781
+ }
782
+ return undefined;
783
+ },
784
+ get files() {
785
+ if (payloadSnapshot === null) {
786
+ return [];
787
+ }
788
+ if (Array.isArray(payloadSnapshot) && payloadSnapshot.every(item => future.isGlobalInstance(item, 'File'))) {
789
+ return payloadSnapshot;
790
+ }
791
+ return undefined;
792
+ },
708
793
  formRef,
709
794
  register: react.useCallback(element => {
710
795
  if (!element) {
@@ -718,17 +803,16 @@ function useControl(options) {
718
803
  element.hidden = true;
719
804
  element.removeAttribute('type');
720
805
  }
721
- if (shouldHandleFocus) {
722
- dom.makeInputFocusable(element);
723
- }
724
806
  if (element.type === 'checkbox' || element.type === 'radio') {
725
- var _optionsRef$current$v, _optionsRef$current7;
726
807
  // React set the value as empty string incorrectly when the value is undefined
727
808
  // This make sure the checkbox value falls back to the default value "on" properly
728
809
  // @see https://github.com/facebook/react/issues/17590
729
- element.value = (_optionsRef$current$v = (_optionsRef$current7 = optionsRef.current) === null || _optionsRef$current7 === void 0 ? void 0 : _optionsRef$current7.value) !== null && _optionsRef$current$v !== void 0 ? _optionsRef$current$v : 'on';
810
+ var value = 'value' in optionsRef.current && optionsRef.current.value ? optionsRef.current.value : 'on';
811
+ element.value = value;
730
812
  }
731
813
  dom.initializeField(element, optionsRef.current);
814
+ } else if (element instanceof HTMLFieldSetElement) {
815
+ inputRef.current = element;
732
816
  } else {
733
817
  var _inputs$0$name, _inputs$, _inputs$0$type, _inputs$2;
734
818
  var inputs = Array.from(element);
@@ -738,39 +822,36 @@ function useControl(options) {
738
822
  throw new Error('You can only register a checkbox or radio group with the same name');
739
823
  }
740
824
  inputRef.current = inputs;
741
- for (var input of inputs) {
742
- var _optionsRef$current8;
743
- if (shouldHandleFocus) {
744
- dom.makeInputFocusable(input);
825
+ if ('defaultValue' in optionsRef.current) {
826
+ for (var input of inputs) {
827
+ var _optionsRef$current7;
828
+ dom.initializeField(input, {
829
+ // We will not be uitlizing defaultChecked / value on checkbox / radio group
830
+ defaultValue: (_optionsRef$current7 = optionsRef.current) === null || _optionsRef$current7 === void 0 ? void 0 : _optionsRef$current7.defaultValue
831
+ });
745
832
  }
746
- dom.initializeField(input, {
747
- // We will not be uitlizing defaultChecked / value on checkbox / radio group
748
- defaultValue: (_optionsRef$current8 = optionsRef.current) === null || _optionsRef$current8 === void 0 ? void 0 : _optionsRef$current8.defaultValue
749
- });
750
833
  }
751
834
  }
752
- }, [shouldHandleFocus]),
835
+ }, []),
753
836
  change: react.useCallback(value => {
754
837
  if (!eventDispatched.current.change) {
755
- var _inputRef$current;
756
- var element = Array.isArray(inputRef.current) ? (_inputRef$current = inputRef.current) === null || _inputRef$current === void 0 ? void 0 : _inputRef$current.find(input => {
757
- var wasChecked = input.checked;
758
- var isChecked = Array.isArray(value) ? value.some(item => item === input.value) : input.value === value;
759
- switch (input.type) {
760
- case 'checkbox':
761
- // We assume that only one checkbox can be checked at a time
762
- // So we will pick the first element with checked state changed
763
- return wasChecked !== isChecked;
764
- case 'radio':
765
- // We cannot uncheck a radio button
766
- // So we will pick the first element that should be checked
767
- return isChecked;
768
- default:
769
- return false;
770
- }
771
- }) : inputRef.current;
838
+ var element = inputRef.current;
839
+ var isFieldset = element instanceof HTMLFieldSetElement;
840
+ var serializedValue = value == null ? value : 'serialize' in optionsRef.current && optionsRef.current.serialize ? optionsRef.current.serialize(value) : value;
841
+ if (isFieldset) {
842
+ // Fieldset mode renders hidden descendant inputs from defaultValue.
843
+ // Flush this update before dispatching events so listeners see the
844
+ // latest form structure in the same input/change cycle.
845
+ reactDom.flushSync(() => {
846
+ setDefaultValue(serializedValue);
847
+ });
848
+ }
772
849
  if (element) {
773
- future.change(element, typeof value === 'boolean' ? value ? element.value : null : value);
850
+ future.change(element, serializedValue, {
851
+ // Sometimes no change is made on the inputs but done through DOM mutation.
852
+ // But we still want to dispatch the event to notify listeners.
853
+ forceDispatch: isFieldset
854
+ });
774
855
  }
775
856
  }
776
857
  if (eventDispatched.current.change) {
@@ -881,6 +962,149 @@ function useLatest(value) {
881
962
  return ref;
882
963
  }
883
964
 
965
+ /**
966
+ * A component that renders hidden base control(s) based on the shape of defaultValue.
967
+ * Used with useControl to sync complex values with form data.
968
+ *
969
+ * @example
970
+ * ```tsx
971
+ * const control = useControl<{ street: string; city: string }>({
972
+ * defaultValue: { street: '123 Main St', city: 'Anytown' },
973
+ * parse(payload) {
974
+ * if (
975
+ * typeof payload === 'object' &&
976
+ * payload !== null &&
977
+ * 'street' in payload &&
978
+ * 'city' in payload &&
979
+ * typeof payload.street === 'string' &&
980
+ * typeof payload.city === 'string'
981
+ * ) {
982
+ * return payload;
983
+ * }
984
+ *
985
+ * throw new Error('Unexpected payload shape');
986
+ * },
987
+ * });
988
+ *
989
+ * <BaseControl
990
+ * type="fieldset"
991
+ * name="address"
992
+ * ref={control.register}
993
+ * defaultValue={control.defaultValue}
994
+ * />
995
+ * ```
996
+ */
997
+ var BaseControl = /*#__PURE__*/react.forwardRef(function BaseControl(props, ref) {
998
+ function formatValue(value) {
999
+ var serialized = future.serialize(value);
1000
+ if (typeof serialized === 'string') {
1001
+ return serialized;
1002
+ }
1003
+
1004
+ // null, undefined, File, or array - fallback to empty string
1005
+ return '';
1006
+ }
1007
+ function renderInput(name, value, form) {
1008
+ if (Array.isArray(value)) {
1009
+ return value.map((item, index) => renderInput("".concat(name, "[").concat(index, "]"), item, form));
1010
+ }
1011
+ if (future.isPlainObject(value)) {
1012
+ return Object.entries(value).map(_ref5 => {
1013
+ var [key, val] = _ref5;
1014
+ return renderInput("".concat(name, ".").concat(key), val, form);
1015
+ });
1016
+ }
1017
+ return /*#__PURE__*/jsxRuntime.jsx("input", {
1018
+ name: name,
1019
+ defaultValue: formatValue(value),
1020
+ form: form
1021
+ }, name);
1022
+ }
1023
+ if (props.type === 'fieldset') {
1024
+ var {
1025
+ name,
1026
+ form,
1027
+ defaultValue: _defaultValue,
1028
+ hidden: _hidden = true
1029
+ } = props,
1030
+ fieldsetProps = _rollupPluginBabelHelpers.objectWithoutProperties(props, _excluded2);
1031
+ return /*#__PURE__*/jsxRuntime.jsx("fieldset", _rollupPluginBabelHelpers.objectSpread2(_rollupPluginBabelHelpers.objectSpread2({}, fieldsetProps), {}, {
1032
+ ref: ref,
1033
+ name: name,
1034
+ form: form,
1035
+ hidden: _hidden,
1036
+ children: _defaultValue != null ? renderInput(name, _defaultValue, form) : null
1037
+ }));
1038
+ }
1039
+ if (props.type === 'select') {
1040
+ var {
1041
+ defaultValue: _defaultValue2,
1042
+ multiple = Array.isArray(_defaultValue2),
1043
+ hidden: _hidden2 = true
1044
+ } = props,
1045
+ selectProps = _rollupPluginBabelHelpers.objectWithoutProperties(props, _excluded3);
1046
+ if (multiple) {
1047
+ var defaultOptions = Array.isArray(_defaultValue2) ? _defaultValue2.map(formatValue) : [formatValue(_defaultValue2)];
1048
+ return /*#__PURE__*/jsxRuntime.jsx("select", _rollupPluginBabelHelpers.objectSpread2(_rollupPluginBabelHelpers.objectSpread2({}, selectProps), {}, {
1049
+ ref: ref,
1050
+ defaultValue: defaultOptions,
1051
+ hidden: _hidden2,
1052
+ multiple: true,
1053
+ children: defaultOptions.map((option, index) => /*#__PURE__*/jsxRuntime.jsx("option", {
1054
+ value: option,
1055
+ children: option
1056
+ }, index))
1057
+ }));
1058
+ }
1059
+ var defaultOption = formatValue(_defaultValue2);
1060
+ return /*#__PURE__*/jsxRuntime.jsx("select", _rollupPluginBabelHelpers.objectSpread2(_rollupPluginBabelHelpers.objectSpread2({}, selectProps), {}, {
1061
+ ref: ref,
1062
+ defaultValue: defaultOption,
1063
+ hidden: _hidden2,
1064
+ children: [defaultOption].map((option, index) => /*#__PURE__*/jsxRuntime.jsx("option", {
1065
+ value: option,
1066
+ children: option
1067
+ }, index))
1068
+ }));
1069
+ }
1070
+ if (props.type === 'textarea') {
1071
+ var {
1072
+ defaultValue: _defaultValue3,
1073
+ hidden: _hidden3 = true
1074
+ } = props,
1075
+ textareaProps = _rollupPluginBabelHelpers.objectWithoutProperties(props, _excluded4);
1076
+ return /*#__PURE__*/jsxRuntime.jsx("textarea", _rollupPluginBabelHelpers.objectSpread2(_rollupPluginBabelHelpers.objectSpread2({}, textareaProps), {}, {
1077
+ defaultValue: formatValue(_defaultValue3),
1078
+ ref: ref,
1079
+ hidden: _hidden3
1080
+ }));
1081
+ }
1082
+ if (props.type === 'checkbox' || props.type === 'radio') {
1083
+ var {
1084
+ defaultValue: _defaultValue4 = 'on',
1085
+ value = _defaultValue4,
1086
+ hidden: _hidden4 = true
1087
+ } = props,
1088
+ _inputProps = _rollupPluginBabelHelpers.objectWithoutProperties(props, _excluded5);
1089
+ return /*#__PURE__*/jsxRuntime.jsx("input", _rollupPluginBabelHelpers.objectSpread2(_rollupPluginBabelHelpers.objectSpread2({}, _inputProps), {}, {
1090
+ ref: ref,
1091
+ value: value,
1092
+ hidden: _hidden4
1093
+ }));
1094
+ }
1095
+ var {
1096
+ defaultValue,
1097
+ hidden = true
1098
+ } = props,
1099
+ inputProps = _rollupPluginBabelHelpers.objectWithoutProperties(props, _excluded6);
1100
+ return /*#__PURE__*/jsxRuntime.jsx("input", _rollupPluginBabelHelpers.objectSpread2(_rollupPluginBabelHelpers.objectSpread2({}, inputProps), {}, {
1101
+ ref: ref,
1102
+ defaultValue: defaultValue !== undefined ? formatValue(defaultValue) : undefined,
1103
+ hidden: hidden
1104
+ }));
1105
+ });
1106
+
1107
+ exports.BaseControl = BaseControl;
884
1108
  exports.FormContextContext = FormContextContext;
885
1109
  exports.FormOptionsProvider = FormOptionsProvider;
886
1110
  exports.FormProvider = FormProvider;