@conform-to/react
Version:
Conform view adapter for react
820 lines (791 loc) • 33.9 kB
JavaScript
'use client';
import { objectWithoutProperties as _objectWithoutProperties, objectSpread2 as _objectSpread2 } from '../_virtual/_rollupPluginBabelHelpers.mjs';
import { DEFAULT_INTENT_NAME, createGlobalFormsObserver, serialize, isFieldElement, deepEqual, change, focus, blur, getFormData, parseSubmission, report, createSubmitEvent } from '@conform-to/dom/future';
import { createContext, useContext, useMemo, useId, useRef, useEffect, useSyncExternalStore, useCallback, useState, useLayoutEffect } from 'react';
import { resolveStandardSchemaResult, resolveValidateResult, appendUniqueItem } from './util.mjs';
import { isTouched, getFormMetadata, getFieldset, getField, initializeState, updateState } from './state.mjs';
import { deserializeIntent, actionHandlers, applyIntent } from './intent.mjs';
import { focusFirstInvalidField, getFormElement, createIntentDispatcher, createDefaultSnapshot, getRadioGroupValue, getCheckboxGroupValue, getInputSnapshot, makeInputFocusable, initializeField, resetFormValue, updateFormValue, getSubmitEvent } from './dom.mjs';
import { jsx } from 'react/jsx-runtime';
var _excluded = ["children"];
// Static reset key for consistent hydration during Next.js prerendering
// See: https://nextjs.org/docs/messages/next-prerender-current-time-client
var INITIAL_KEY = 'INITIAL_KEY';
var GlobalFormOptionsContext = /*#__PURE__*/createContext({
intentName: DEFAULT_INTENT_NAME,
observer: createGlobalFormsObserver(),
serialize,
shouldValidate: 'onSubmit'
});
var FormContextContext = /*#__PURE__*/createContext([]);
/**
* Provides form context to child components.
* Stacks contexts to support nested forms, with latest context taking priority.
*/
function FormProvider(props) {
var stack = useContext(FormContextContext);
var value = useMemo(
// Put the latest form context first to ensure that to be the first one found
() => [props.context].concat(stack), [stack, props.context]);
return /*#__PURE__*/jsx(FormContextContext.Provider, {
value: value,
children: props.children
});
}
function FormOptionsProvider(props) {
var {
children
} = props,
providedOptions = _objectWithoutProperties(props, _excluded);
var defaultOptions = useContext(GlobalFormOptionsContext);
var options = useMemo(() => _objectSpread2(_objectSpread2({}, defaultOptions), providedOptions), [defaultOptions, providedOptions]);
return /*#__PURE__*/jsx(GlobalFormOptionsContext.Provider, {
value: options,
children: children
});
}
function useFormContext(formId) {
var contexts = useContext(FormContextContext);
var context = formId ? contexts.find(context => formId === context.formId) : contexts[0];
if (!context) {
throw new Error('No form context found; Have you render a <FormProvider /> with the corresponding form context?');
}
return context;
}
/**
* Core form hook that manages form state, validation, and submission.
* Handles both sync and async validation, intent dispatching, and DOM updates.
*/
function useConform(formRef, options) {
var {
lastResult
} = options;
var [state, setState] = useState(() => {
var state = initializeState({
defaultValue: options.defaultValue,
resetKey: INITIAL_KEY
});
if (lastResult) {
state = updateState(state, _objectSpread2(_objectSpread2({}, lastResult), {}, {
type: 'initialize',
intent: lastResult.submission.intent ? deserializeIntent(lastResult.submission.intent) : null,
ctx: {
handlers: actionHandlers,
reset: defaultValue => initializeState({
defaultValue: defaultValue !== null && defaultValue !== void 0 ? defaultValue : options.defaultValue,
resetKey: INITIAL_KEY
})
}
}));
}
return state;
});
var keyRef = useRef(options.key);
var resetKeyRef = useRef(state.resetKey);
var optionsRef = useLatest(options);
var lastResultRef = useRef(lastResult);
var pendingValueRef = useRef();
var lastAsyncResultRef = useRef(null);
var abortControllerRef = useRef(null);
var handleSubmission = useCallback(function (type, result) {
var _optionsRef$current$o, _optionsRef$current;
var options = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : optionsRef.current;
var intent = result.submission.intent ? deserializeIntent(result.submission.intent) : null;
setState(state => updateState(state, _objectSpread2(_objectSpread2({}, result), {}, {
type,
intent,
ctx: {
handlers: actionHandlers,
reset(defaultValue) {
return initializeState({
defaultValue: defaultValue !== null && defaultValue !== void 0 ? defaultValue : options.defaultValue
});
}
}
})));
// TODO: move on error handler to a new effect
var formElement = getFormElement(formRef);
if (!formElement || !result.error) {
return;
}
(_optionsRef$current$o = (_optionsRef$current = optionsRef.current).onError) === null || _optionsRef$current$o === void 0 || _optionsRef$current$o.call(_optionsRef$current, {
formElement,
error: result.error,
intent
});
}, [formRef, optionsRef]);
if (options.key !== keyRef.current) {
keyRef.current = options.key;
setState(initializeState({
defaultValue: options.defaultValue
}));
} else if (lastResult && lastResult !== lastResultRef.current) {
lastResultRef.current = lastResult;
handleSubmission('server', lastResult, options);
}
useEffect(() => {
return () => {
var _abortControllerRef$c;
// Cancel pending validation request
(_abortControllerRef$c = abortControllerRef.current) === null || _abortControllerRef$c === void 0 || _abortControllerRef$c.abort('The component is unmounted');
};
}, []);
useSafeLayoutEffect(() => {
var formElement = getFormElement(formRef);
// Reset the form values if the reset key changes
if (formElement && state.resetKey !== resetKeyRef.current) {
resetKeyRef.current = state.resetKey;
resetFormValue(formElement, state.defaultValue, optionsRef.current.serialize);
pendingValueRef.current = undefined;
}
}, [formRef, state.resetKey, state.defaultValue, optionsRef]);
useSafeLayoutEffect(() => {
if (state.targetValue) {
var formElement = getFormElement(formRef);
if (!formElement) {
// eslint-disable-next-line no-console
console.error('Failed to update form value; No form element found');
return;
}
updateFormValue(formElement, state.targetValue, optionsRef.current.serialize);
}
pendingValueRef.current = undefined;
}, [formRef, state.targetValue, optionsRef]);
var handleSubmit = useCallback(event => {
var _abortControllerRef$c2, _lastAsyncResultRef$c;
var abortController = new AbortController();
// Keep track of the abort controller so we can cancel the previous request if a new one is made
(_abortControllerRef$c2 = abortControllerRef.current) === null || _abortControllerRef$c2 === void 0 || _abortControllerRef$c2.abort('A new submission is made');
abortControllerRef.current = abortController;
var formData;
var result;
var resolvedValue;
// The form might be re-submitted manually if there was an async validation
if (event.nativeEvent === ((_lastAsyncResultRef$c = lastAsyncResultRef.current) === null || _lastAsyncResultRef$c === void 0 ? void 0 : _lastAsyncResultRef$c.event)) {
formData = lastAsyncResultRef.current.formData;
result = lastAsyncResultRef.current.result;
resolvedValue = lastAsyncResultRef.current.resolvedValue;
} else {
var _optionsRef$current$o2, _optionsRef$current2;
var formElement = event.currentTarget;
var submitEvent = getSubmitEvent(event);
formData = getFormData(formElement, submitEvent.submitter);
var submission = parseSubmission(formData, {
intentName: optionsRef.current.intentName
});
// Patch missing fields in the submission object
for (var element of formElement.elements) {
if (isFieldElement(element) && element.name) {
submission.fields = appendUniqueItem(submission.fields, element.name);
}
}
// Override submission value if the pending value is not applied yet (i.e. batch updates)
if (pendingValueRef.current !== undefined) {
submission.payload = pendingValueRef.current;
}
var value = applyIntent(submission);
var submissionResult = report(submission, {
keepFiles: true,
value
});
// If there is target value, keep track of it as pending value
if (submission.payload !== value) {
var _ref;
pendingValueRef.current = (_ref = value !== null && value !== void 0 ? value : optionsRef.current.defaultValue) !== null && _ref !== void 0 ? _ref : {};
}
var validateResult =
// Skip validation on form reset
value !== undefined ? (_optionsRef$current$o2 = (_optionsRef$current2 = optionsRef.current).onValidate) === null || _optionsRef$current$o2 === void 0 ? void 0 : _optionsRef$current$o2.call(_optionsRef$current2, {
payload: value,
error: {
formErrors: [],
fieldErrors: {}
},
intent: submission.intent ? deserializeIntent(submission.intent) : null,
formElement,
submitter: submitEvent.submitter,
formData,
schemaValue: undefined
}) : {
error: null
};
var {
syncResult,
asyncResult
} = resolveValidateResult(validateResult);
if (typeof syncResult !== 'undefined') {
submissionResult.error = syncResult.error;
resolvedValue = syncResult.value;
}
if (typeof asyncResult !== 'undefined') {
// Update the form when the validation result is resolved
asyncResult.then(_ref2 => {
var {
error,
value
} = _ref2;
// Update the form with the validation result
// There is no need to flush the update in this case
if (!abortController.signal.aborted) {
submissionResult.error = error;
handleSubmission('server', submissionResult);
// If the form is meant to be submitted and there is no error
if (error === null && !submission.intent) {
var _event = createSubmitEvent(submitEvent.submitter);
// Keep track of the submit event so we can skip validation on the next submit
lastAsyncResultRef.current = {
event: _event,
formData,
resolvedValue: value,
result: submissionResult
};
formElement.dispatchEvent(_event);
}
}
});
}
handleSubmission('client', submissionResult);
if (
// If client validation happens
(typeof syncResult !== 'undefined' || typeof asyncResult !== 'undefined') && (
// Either the form is not meant to be submitted (i.e. intent is present) or there is an error / pending validation
submissionResult.submission.intent || submissionResult.error !== null)) {
event.preventDefault();
}
result = submissionResult;
}
// We might not prevent form submission if server validation is required
// But the `onSubmit` handler should be triggered only if there is no intent
if (!event.isDefaultPrevented() && result.submission.intent === null) {
var _optionsRef$current$o3, _optionsRef$current3;
(_optionsRef$current$o3 = (_optionsRef$current3 = optionsRef.current).onSubmit) === null || _optionsRef$current$o3 === void 0 || _optionsRef$current$o3.call(_optionsRef$current3, event, {
formData,
get value() {
if (typeof resolvedValue === 'undefined') {
throw new Error('`value` is not available; Please make sure you have included the value in the `onValidate` result.');
}
return resolvedValue;
},
update(options) {
if (!abortController.signal.aborted) {
var _submissionResult = report(result.submission, _objectSpread2(_objectSpread2({}, options), {}, {
keepFiles: true
}));
handleSubmission('server', _submissionResult);
}
}
});
}
}, [handleSubmission, optionsRef]);
return [state, handleSubmit];
}
/**
* The main React hook for form management. Handles form state, validation, and submission
* while providing access to form metadata, field objects, and form actions.
*
* It can be called in two ways:
* - **Schema first**: Pass a schema as the first argument for automatic validation with type inference
* - **Manual configuration**: Pass options with custom `onValidate` handler for manual validation
*
* @see https://conform.guide/api/react/future/useForm
* @example Schema first setup with zod:
*
* ```tsx
* const { form, fields } = useForm(zodSchema, {
* lastResult,
* shouldValidate: 'onBlur',
* });
*
* return (
* <form {...form.props}>
* <input name={fields.email.name} defaultValue={fields.email.defaultValue} />
* <div>{fields.email.errors}</div>
* </form>
* );
* ```
*
* @example Manual configuration setup with custom validation:
*
* ```tsx
* const { form, fields } = useForm({
* onValidate({ payload, error }) {
* if (!payload.email) {
* error.fieldErrors.email = ['Required'];
* }
* return error;
* }
* });
*
* return (
* <form {...form.props}>
* <input name={fields.email.name} defaultValue={fields.email.defaultValue} />
* <div>{fields.email.errors}</div>
* </form>
* );
* ```
*/
/**
* @deprecated Use `useForm(schema, options)` instead for better type inference.
*/
function useForm(schemaOrOptions, maybeOptions) {
var _options$onError;
var schema;
var options;
if (maybeOptions) {
schema = schemaOrOptions;
options = maybeOptions;
} else {
var fullOptions = schemaOrOptions;
options = fullOptions;
schema = fullOptions.schema;
}
var {
id,
constraint
} = options;
var globalOptions = useContext(GlobalFormOptionsContext);
var optionsRef = useLatest(options);
var globalOptionsRef = useLatest(globalOptions);
var fallbackId = useId();
var formId = id !== null && id !== void 0 ? id : "form-".concat(fallbackId);
var [state, handleSubmit] = useConform(formId, _objectSpread2(_objectSpread2({}, options), {}, {
serialize: globalOptions.serialize,
intentName: globalOptions.intentName,
onError: (_options$onError = options.onError) !== null && _options$onError !== void 0 ? _options$onError : focusFirstInvalidField,
onValidate(ctx) {
var _options$onValidate, _options$onValidate2, _options;
if (schema) {
var standardResult = schema['~standard'].validate(ctx.payload);
if (standardResult instanceof Promise) {
return standardResult.then(actualStandardResult => {
if (typeof options.onValidate === 'function') {
throw new Error('The "onValidate" handler is not supported when used with asynchronous schema validation.');
}
return resolveStandardSchemaResult(actualStandardResult);
});
}
var resolvedResult = resolveStandardSchemaResult(standardResult);
if (!options.onValidate) {
return resolvedResult;
}
// Update the schema error in the context
if (resolvedResult.error) {
ctx.error = resolvedResult.error;
}
ctx.schemaValue = resolvedResult.value;
var validateResult = resolveValidateResult(options.onValidate(ctx));
if (validateResult.syncResult) {
var _validateResult$syncR, _validateResult$syncR2;
(_validateResult$syncR2 = (_validateResult$syncR = validateResult.syncResult).value) !== null && _validateResult$syncR2 !== void 0 ? _validateResult$syncR2 : _validateResult$syncR.value = resolvedResult.value;
}
if (validateResult.asyncResult) {
validateResult.asyncResult = validateResult.asyncResult.then(result => {
var _result$value;
(_result$value = result.value) !== null && _result$value !== void 0 ? _result$value : result.value = resolvedResult.value;
return result;
});
}
return [validateResult.syncResult, validateResult.asyncResult];
}
return (_options$onValidate = (_options$onValidate2 = (_options = options).onValidate) === null || _options$onValidate2 === void 0 ? void 0 : _options$onValidate2.call(_options, ctx)) !== null && _options$onValidate !== void 0 ? _options$onValidate : {
// To avoid conform falling back to server validation,
// if neither schema nor validation handler is provided,
// we just treat it as a valid client submission
error: null
};
}
}));
var intent = useIntent(formId);
var context = useMemo(() => ({
formId,
state,
constraint: constraint !== null && constraint !== void 0 ? constraint : null,
handleSubmit,
handleInput(event) {
var _optionsRef$current$o4, _optionsRef$current4, _globalOptionsRef$cur;
if (!isFieldElement(event.target) || event.target.name === '' || event.target.form === null || event.target.form !== getFormElement(formId)) {
return;
}
(_optionsRef$current$o4 = (_optionsRef$current4 = optionsRef.current).onInput) === null || _optionsRef$current$o4 === void 0 || _optionsRef$current$o4.call(_optionsRef$current4, _objectSpread2(_objectSpread2({}, event), {}, {
target: event.target,
currentTarget: event.target.form
}));
if (event.defaultPrevented) {
return;
}
var {
shouldValidate = globalOptionsRef.current.shouldValidate,
shouldRevalidate = (_globalOptionsRef$cur = globalOptionsRef.current.shouldRevalidate) !== null && _globalOptionsRef$cur !== void 0 ? _globalOptionsRef$cur : shouldValidate
} = optionsRef.current;
if (isTouched(state, event.target.name) ? shouldRevalidate === 'onInput' : shouldValidate === 'onInput') {
intent.validate(event.target.name);
}
},
handleBlur(event) {
var _optionsRef$current$o5, _optionsRef$current5, _globalOptionsRef$cur2;
if (!isFieldElement(event.target) || event.target.name === '' || event.target.form === null || event.target.form !== getFormElement(formId)) {
return;
}
(_optionsRef$current$o5 = (_optionsRef$current5 = optionsRef.current).onBlur) === null || _optionsRef$current$o5 === void 0 || _optionsRef$current$o5.call(_optionsRef$current5, _objectSpread2(_objectSpread2({}, event), {}, {
target: event.target,
currentTarget: event.target.form
}));
if (event.defaultPrevented) {
return;
}
var {
shouldValidate = globalOptionsRef.current.shouldValidate,
shouldRevalidate = (_globalOptionsRef$cur2 = globalOptionsRef.current.shouldRevalidate) !== null && _globalOptionsRef$cur2 !== void 0 ? _globalOptionsRef$cur2 : shouldValidate
} = optionsRef.current;
if (isTouched(state, event.target.name) ? shouldRevalidate === 'onBlur' : shouldValidate === 'onBlur') {
intent.validate(event.target.name);
}
}
}), [formId, state, constraint, handleSubmit, intent, optionsRef, globalOptionsRef]);
var form = useMemo(() => getFormMetadata(context, {
serialize: globalOptions.serialize,
customize: globalOptions.defineCustomMetadata
}), [context, globalOptions.serialize, globalOptions.defineCustomMetadata]);
var fields = useMemo(() => getFieldset(context, {
serialize: globalOptions.serialize,
customize: globalOptions.defineCustomMetadata
}), [context, globalOptions.serialize, globalOptions.defineCustomMetadata]);
return {
form,
fields,
intent
};
}
/**
* A React hook that provides access to form-level metadata and state.
* Requires `FormProvider` context when used in child components.
*
* @see https://conform.guide/api/react/future/useFormMetadata
* @example
* ```tsx
* function ErrorSummary() {
* const form = useFormMetadata();
*
* if (form.valid) return null;
*
* return (
* <div>Please fix {Object.keys(form.fieldErrors).length} errors</div>
* );
* }
* ```
*/
function useFormMetadata() {
var options = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {};
var globalOptions = useContext(GlobalFormOptionsContext);
var context = useFormContext(options.formId);
var formMetadata = useMemo(() => getFormMetadata(context, {
serialize: globalOptions.serialize,
customize: globalOptions.defineCustomMetadata
}), [context, globalOptions.serialize, globalOptions.defineCustomMetadata]);
return formMetadata;
}
/**
* A React hook that provides access to a specific field's metadata and state.
* Requires `FormProvider` context when used in child components.
*
* @see https://conform.guide/api/react/future/useField
* @example
* ```tsx
* function FormField({ name, label }) {
* const field = useField(name);
*
* return (
* <div>
* <label htmlFor={field.id}>{label}</label>
* <input id={field.id} name={field.name} defaultValue={field.defaultValue} />
* {field.errors && <div>{field.errors.join(', ')}</div>}
* </div>
* );
* }
* ```
*/
function useField(name) {
var options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {};
var globalOptions = useContext(GlobalFormOptionsContext);
var context = useFormContext(options.formId);
var field = useMemo(() => getField(context, {
name,
serialize: globalOptions.serialize,
customize: globalOptions.defineCustomMetadata
}), [context, name, globalOptions.serialize, globalOptions.defineCustomMetadata]);
return field;
}
/**
* A React hook that provides an intent dispatcher for programmatic form actions.
* Intent dispatchers allow you to trigger form operations like validation, field updates,
* and array manipulations without manual form submission.
*
* @see https://conform.guide/api/react/future/useIntent
* @example
* ```tsx
* function ResetButton() {
* const buttonRef = useRef<HTMLButtonElement>(null);
* const intent = useIntent(buttonRef);
*
* return (
* <button type="button" ref={buttonRef} onClick={() => intent.reset()}>
* Reset Form
* </button>
* );
* }
* ```
*/
function useIntent(formRef) {
var globalOptions = useContext(GlobalFormOptionsContext);
return useMemo(() => createIntentDispatcher(() => getFormElement(formRef), globalOptions.intentName), [formRef, globalOptions.intentName]);
}
/**
* A React hook that lets you sync the state of an input and dispatch native form events from it.
* This is useful when emulating native input behavior — typically by rendering a hidden base input
* and syncing it with a custom input.
*
* @example
* ```ts
* const control = useControl(options);
* ```
*/
function useControl(options) {
var {
observer
} = useContext(GlobalFormOptionsContext);
var inputRef = useRef(null);
var formRef = useMemo(() => ({
get current() {
var _input$0$form, _input$;
var input = inputRef.current;
if (!input) {
return null;
}
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;
}
}), []);
var eventDispatched = useRef({});
var defaultSnapshot = 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);
var snapshotRef = useRef(defaultSnapshot);
var optionsRef = useRef(options);
useEffect(() => {
optionsRef.current = options;
});
// This is necessary to ensure that input is re-registered
// if the onFocus handler changes
var shouldHandleFocus = typeof (options === null || options === void 0 ? void 0 : options.onFocus) === 'function';
var snapshot = useSyncExternalStore(useCallback(callback => observer.onFieldUpdate(event => {
var input = event.target;
if (Array.isArray(inputRef.current) ? inputRef.current.some(item => item === input) : inputRef.current === input) {
callback();
}
}), [observer]), () => {
var input = inputRef.current;
var prev = snapshotRef.current;
var next = !input ? defaultSnapshot : Array.isArray(input) ? {
value: getRadioGroupValue(input),
options: getCheckboxGroupValue(input)
} : getInputSnapshot(input);
if (deepEqual(prev, next)) {
return prev;
}
snapshotRef.current = next;
return next;
}, () => snapshotRef.current);
useEffect(() => {
var createEventListener = listener => {
return event => {
if (Array.isArray(inputRef.current) ? inputRef.current.some(item => item === event.target) : inputRef.current === event.target) {
var timer = eventDispatched.current[listener];
if (timer) {
clearTimeout(timer);
}
eventDispatched.current[listener] = window.setTimeout(() => {
eventDispatched.current[listener] = undefined;
});
if (listener === 'focus') {
var _optionsRef$current6, _optionsRef$current6$;
(_optionsRef$current6 = optionsRef.current) === null || _optionsRef$current6 === void 0 || (_optionsRef$current6$ = _optionsRef$current6.onFocus) === null || _optionsRef$current6$ === void 0 || _optionsRef$current6$.call(_optionsRef$current6);
}
}
};
};
var inputHandler = createEventListener('change');
var focusHandler = createEventListener('focus');
var blurHandler = createEventListener('blur');
document.addEventListener('input', inputHandler, true);
document.addEventListener('focusin', focusHandler, true);
document.addEventListener('focusout', blurHandler, true);
return () => {
document.removeEventListener('input', inputHandler, true);
document.removeEventListener('focusin', focusHandler, true);
document.removeEventListener('focusout', blurHandler, true);
};
}, []);
return {
value: snapshot.value,
checked: snapshot.checked,
options: snapshot.options,
files: snapshot.files,
formRef,
register: useCallback(element => {
if (!element) {
inputRef.current = null;
} else if (isFieldElement(element)) {
inputRef.current = element;
// Conform excludes hidden type inputs by default when updating form values
// Fix that by using the hidden attribute instead
if (element.type === 'hidden') {
element.hidden = true;
element.removeAttribute('type');
}
if (shouldHandleFocus) {
makeInputFocusable(element);
}
if (element.type === 'checkbox' || element.type === 'radio') {
var _optionsRef$current$v, _optionsRef$current7;
// React set the value as empty string incorrectly when the value is undefined
// This make sure the checkbox value falls back to the default value "on" properly
// @see https://github.com/facebook/react/issues/17590
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';
}
initializeField(element, optionsRef.current);
} else {
var _inputs$0$name, _inputs$, _inputs$0$type, _inputs$2;
var inputs = Array.from(element);
var name = (_inputs$0$name = (_inputs$ = inputs[0]) === null || _inputs$ === void 0 ? void 0 : _inputs$.name) !== null && _inputs$0$name !== void 0 ? _inputs$0$name : '';
var type = (_inputs$0$type = (_inputs$2 = inputs[0]) === null || _inputs$2 === void 0 ? void 0 : _inputs$2.type) !== null && _inputs$0$type !== void 0 ? _inputs$0$type : '';
if (!name || !(type === 'checkbox' || type === 'radio') || !inputs.every(input => input.name === name && input.type === type)) {
throw new Error('You can only register a checkbox or radio group with the same name');
}
inputRef.current = inputs;
for (var input of inputs) {
var _optionsRef$current8;
if (shouldHandleFocus) {
makeInputFocusable(input);
}
initializeField(input, {
// We will not be uitlizing defaultChecked / value on checkbox / radio group
defaultValue: (_optionsRef$current8 = optionsRef.current) === null || _optionsRef$current8 === void 0 ? void 0 : _optionsRef$current8.defaultValue
});
}
}
}, [shouldHandleFocus]),
change: useCallback(value => {
if (!eventDispatched.current.change) {
var _inputRef$current;
var element = Array.isArray(inputRef.current) ? (_inputRef$current = inputRef.current) === null || _inputRef$current === void 0 ? void 0 : _inputRef$current.find(input => {
var wasChecked = input.checked;
var isChecked = Array.isArray(value) ? value.some(item => item === input.value) : input.value === value;
switch (input.type) {
case 'checkbox':
// We assume that only one checkbox can be checked at a time
// So we will pick the first element with checked state changed
return wasChecked !== isChecked;
case 'radio':
// We cannot uncheck a radio button
// So we will pick the first element that should be checked
return isChecked;
default:
return false;
}
}) : inputRef.current;
if (element) {
change(element, typeof value === 'boolean' ? value ? element.value : null : value);
}
}
if (eventDispatched.current.change) {
clearTimeout(eventDispatched.current.change);
}
eventDispatched.current.change = undefined;
}, []),
focus: useCallback(() => {
if (!eventDispatched.current.focus) {
var element = Array.isArray(inputRef.current) ? inputRef.current[0] : inputRef.current;
if (element) {
focus(element);
}
}
if (eventDispatched.current.focus) {
clearTimeout(eventDispatched.current.focus);
}
eventDispatched.current.focus = undefined;
}, []),
blur: useCallback(() => {
if (!eventDispatched.current.blur) {
var element = Array.isArray(inputRef.current) ? inputRef.current[0] : inputRef.current;
if (element) {
blur(element);
}
}
if (eventDispatched.current.blur) {
clearTimeout(eventDispatched.current.blur);
}
eventDispatched.current.blur = undefined;
}, [])
};
}
/**
* A React hook that lets you subscribe to the current `FormData` of a form and derive a custom value from it.
* The selector runs whenever the form's structure or data changes, and the hook re-renders only when the result is deeply different.
*
* @see https://conform.guide/api/react/future/useFormData
* @example
* ```ts
* const value = useFormData(formRef, formData => formData?.get('fieldName') ?? '');
* ```
*/
function useFormData(formRef, select, options) {
var {
observer
} = useContext(GlobalFormOptionsContext);
var valueRef = useRef();
var formDataRef = useRef(null);
var value = useSyncExternalStore(useCallback(callback => {
var formElement = getFormElement(formRef);
if (formElement) {
var formData = getFormData(formElement);
formDataRef.current = options !== null && options !== void 0 && options.acceptFiles ? formData : new URLSearchParams(Array.from(formData).map(_ref3 => {
var [key, value] = _ref3;
return [key, value.toString()];
}));
}
var unsubscribe = observer.onFormUpdate(event => {
if (event.target === getFormElement(formRef)) {
var _formData = getFormData(event.target, event.submitter);
formDataRef.current = options !== null && options !== void 0 && options.acceptFiles ? _formData : new URLSearchParams(Array.from(_formData).map(_ref4 => {
var [key, value] = _ref4;
return [key, value.toString()];
}));
callback();
}
});
return unsubscribe;
}, [observer, formRef, options === null || options === void 0 ? void 0 : options.acceptFiles]), () => {
// @ts-expect-error FIXME
var result = select(formDataRef.current, valueRef.current);
if (typeof valueRef.current !== 'undefined' && deepEqual(result, valueRef.current)) {
return valueRef.current;
}
valueRef.current = result;
return result;
}, () => select(null, undefined));
return value;
}
/**
* useLayoutEffect is client-only.
* This basically makes it a no-op on server
*/
var useSafeLayoutEffect = typeof document === 'undefined' ? useEffect : useLayoutEffect;
/**
* Keep a mutable ref in sync with the latest value.
* Useful to avoid stale closures in event handlers or async callbacks.
*/
function useLatest(value) {
var ref = useRef(value);
useSafeLayoutEffect(() => {
ref.current = value;
}, [value]);
return ref;
}
export { FormContextContext, FormOptionsProvider, FormProvider, GlobalFormOptionsContext, INITIAL_KEY, useConform, useControl, useField, useForm, useFormContext, useFormData, useFormMetadata, useIntent, useLatest, useSafeLayoutEffect };