UNPKG

@conform-to/react

Version:

Conform view adapter for react

838 lines (807 loc) 34.7 kB
'use client'; 'use strict'; Object.defineProperty(exports, '__esModule', { value: true }); var _rollupPluginBabelHelpers = require('../_virtual/_rollupPluginBabelHelpers.js'); var future = require('@conform-to/dom/future'); var react = require('react'); var util = require('./util.js'); var state = require('./state.js'); var intent = require('./intent.js'); var dom = require('./dom.js'); var jsxRuntime = require('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__*/react.createContext({ intentName: future.DEFAULT_INTENT_NAME, observer: future.createGlobalFormsObserver(), serialize: future.serialize, shouldValidate: 'onSubmit' }); var FormContextContext = /*#__PURE__*/react.createContext([]); /** * Provides form context to child components. * Stacks contexts to support nested forms, with latest context taking priority. */ function FormProvider(props) { var stack = react.useContext(FormContextContext); var value = react.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__*/jsxRuntime.jsx(FormContextContext.Provider, { value: value, children: props.children }); } function FormOptionsProvider(props) { var { children } = props, providedOptions = _rollupPluginBabelHelpers.objectWithoutProperties(props, _excluded); var defaultOptions = react.useContext(GlobalFormOptionsContext); var options = react.useMemo(() => _rollupPluginBabelHelpers.objectSpread2(_rollupPluginBabelHelpers.objectSpread2({}, defaultOptions), providedOptions), [defaultOptions, providedOptions]); return /*#__PURE__*/jsxRuntime.jsx(GlobalFormOptionsContext.Provider, { value: options, children: children }); } function useFormContext(formId) { var contexts = react.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$1, setState] = react.useState(() => { var state$1 = state.initializeState({ defaultValue: options.defaultValue, resetKey: INITIAL_KEY }); if (lastResult) { state$1 = state.updateState(state$1, _rollupPluginBabelHelpers.objectSpread2(_rollupPluginBabelHelpers.objectSpread2({}, lastResult), {}, { type: 'initialize', intent: lastResult.submission.intent ? intent.deserializeIntent(lastResult.submission.intent) : null, ctx: { handlers: intent.actionHandlers, reset: defaultValue => state.initializeState({ defaultValue: defaultValue !== null && defaultValue !== void 0 ? defaultValue : options.defaultValue, resetKey: INITIAL_KEY }) } })); } return state$1; }); var keyRef = react.useRef(options.key); var resetKeyRef = react.useRef(state$1.resetKey); var optionsRef = useLatest(options); var lastResultRef = react.useRef(lastResult); var pendingValueRef = react.useRef(); var lastAsyncResultRef = react.useRef(null); var abortControllerRef = react.useRef(null); var handleSubmission = react.useCallback(function (type, result) { var _optionsRef$current$o, _optionsRef$current; var options = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : optionsRef.current; var intent$1 = result.submission.intent ? intent.deserializeIntent(result.submission.intent) : null; setState(state$1 => state.updateState(state$1, _rollupPluginBabelHelpers.objectSpread2(_rollupPluginBabelHelpers.objectSpread2({}, result), {}, { type, intent: intent$1, ctx: { handlers: intent.actionHandlers, reset(defaultValue) { return state.initializeState({ defaultValue: defaultValue !== null && defaultValue !== void 0 ? defaultValue : options.defaultValue }); } } }))); // TODO: move on error handler to a new effect var formElement = dom.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: intent$1 }); }, [formRef, optionsRef]); if (options.key !== keyRef.current) { keyRef.current = options.key; setState(state.initializeState({ defaultValue: options.defaultValue })); } else if (lastResult && lastResult !== lastResultRef.current) { lastResultRef.current = lastResult; handleSubmission('server', lastResult, options); } react.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 = dom.getFormElement(formRef); // Reset the form values if the reset key changes if (formElement && state$1.resetKey !== resetKeyRef.current) { resetKeyRef.current = state$1.resetKey; dom.resetFormValue(formElement, state$1.defaultValue, optionsRef.current.serialize); pendingValueRef.current = undefined; } }, [formRef, state$1.resetKey, state$1.defaultValue, optionsRef]); useSafeLayoutEffect(() => { if (state$1.targetValue) { var formElement = dom.getFormElement(formRef); if (!formElement) { // eslint-disable-next-line no-console console.error('Failed to update form value; No form element found'); return; } dom.updateFormValue(formElement, state$1.targetValue, optionsRef.current.serialize); } pendingValueRef.current = undefined; }, [formRef, state$1.targetValue, optionsRef]); var handleSubmit = react.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 = dom.getSubmitEvent(event); formData = future.getFormData(formElement, submitEvent.submitter); var submission = future.parseSubmission(formData, { intentName: optionsRef.current.intentName }); // Patch missing fields in the submission object for (var element of formElement.elements) { if (future.isFieldElement(element) && element.name) { submission.fields = util.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 = intent.applyIntent(submission); var submissionResult = future.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 ? intent.deserializeIntent(submission.intent) : null, formElement, submitter: submitEvent.submitter, formData, schemaValue: undefined }) : { error: null }; var { syncResult, asyncResult } = util.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 = future.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 = future.report(result.submission, _rollupPluginBabelHelpers.objectSpread2(_rollupPluginBabelHelpers.objectSpread2({}, options), {}, { keepFiles: true })); handleSubmission('server', _submissionResult); } } }); } }, [handleSubmission, optionsRef]); return [state$1, 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 = react.useContext(GlobalFormOptionsContext); var optionsRef = useLatest(options); var globalOptionsRef = useLatest(globalOptions); var fallbackId = react.useId(); var formId = id !== null && id !== void 0 ? id : "form-".concat(fallbackId); var [state$1, handleSubmit] = useConform(formId, _rollupPluginBabelHelpers.objectSpread2(_rollupPluginBabelHelpers.objectSpread2({}, options), {}, { serialize: globalOptions.serialize, intentName: globalOptions.intentName, onError: (_options$onError = options.onError) !== null && _options$onError !== void 0 ? _options$onError : dom.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 util.resolveStandardSchemaResult(actualStandardResult); }); } var resolvedResult = util.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 = util.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 = react.useMemo(() => ({ formId, state: state$1, constraint: constraint !== null && constraint !== void 0 ? constraint : null, handleSubmit, handleInput(event) { var _optionsRef$current$o4, _optionsRef$current4, _globalOptionsRef$cur; if (!future.isFieldElement(event.target) || event.target.name === '' || event.target.form === null || event.target.form !== dom.getFormElement(formId)) { return; } (_optionsRef$current$o4 = (_optionsRef$current4 = optionsRef.current).onInput) === null || _optionsRef$current$o4 === void 0 || _optionsRef$current$o4.call(_optionsRef$current4, _rollupPluginBabelHelpers.objectSpread2(_rollupPluginBabelHelpers.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 (state.isTouched(state$1, event.target.name) ? shouldRevalidate === 'onInput' : shouldValidate === 'onInput') { intent.validate(event.target.name); } }, handleBlur(event) { var _optionsRef$current$o5, _optionsRef$current5, _globalOptionsRef$cur2; if (!future.isFieldElement(event.target) || event.target.name === '' || event.target.form === null || event.target.form !== dom.getFormElement(formId)) { return; } (_optionsRef$current$o5 = (_optionsRef$current5 = optionsRef.current).onBlur) === null || _optionsRef$current$o5 === void 0 || _optionsRef$current$o5.call(_optionsRef$current5, _rollupPluginBabelHelpers.objectSpread2(_rollupPluginBabelHelpers.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 (state.isTouched(state$1, event.target.name) ? shouldRevalidate === 'onBlur' : shouldValidate === 'onBlur') { intent.validate(event.target.name); } } }), [formId, state$1, constraint, handleSubmit, intent, optionsRef, globalOptionsRef]); var form = react.useMemo(() => state.getFormMetadata(context, { serialize: globalOptions.serialize, customize: globalOptions.defineCustomMetadata }), [context, globalOptions.serialize, globalOptions.defineCustomMetadata]); var fields = react.useMemo(() => state.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 = react.useContext(GlobalFormOptionsContext); var context = useFormContext(options.formId); var formMetadata = react.useMemo(() => state.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 = react.useContext(GlobalFormOptionsContext); var context = useFormContext(options.formId); var field = react.useMemo(() => state.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 = react.useContext(GlobalFormOptionsContext); return react.useMemo(() => dom.createIntentDispatcher(() => dom.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 } = react.useContext(GlobalFormOptionsContext); var inputRef = react.useRef(null); var formRef = react.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 = react.useRef({}); 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); var snapshotRef = react.useRef(defaultSnapshot); var optionsRef = react.useRef(options); react.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 = react.useSyncExternalStore(react.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: dom.getRadioGroupValue(input), options: dom.getCheckboxGroupValue(input) } : dom.getInputSnapshot(input); if (future.deepEqual(prev, next)) { return prev; } snapshotRef.current = next; return next; }, () => snapshotRef.current); react.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: react.useCallback(element => { if (!element) { inputRef.current = null; } else if (future.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) { dom.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'; } dom.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) { dom.makeInputFocusable(input); } dom.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: react.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) { future.change(element, typeof value === 'boolean' ? value ? element.value : null : value); } } if (eventDispatched.current.change) { clearTimeout(eventDispatched.current.change); } eventDispatched.current.change = undefined; }, []), focus: react.useCallback(() => { if (!eventDispatched.current.focus) { var element = Array.isArray(inputRef.current) ? inputRef.current[0] : inputRef.current; if (element) { future.focus(element); } } if (eventDispatched.current.focus) { clearTimeout(eventDispatched.current.focus); } eventDispatched.current.focus = undefined; }, []), blur: react.useCallback(() => { if (!eventDispatched.current.blur) { var element = Array.isArray(inputRef.current) ? inputRef.current[0] : inputRef.current; if (element) { future.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 } = react.useContext(GlobalFormOptionsContext); var valueRef = react.useRef(); var formDataRef = react.useRef(null); var value = react.useSyncExternalStore(react.useCallback(callback => { var formElement = dom.getFormElement(formRef); if (formElement) { var formData = future.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 === dom.getFormElement(formRef)) { var _formData = future.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' && future.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' ? react.useEffect : react.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 = react.useRef(value); useSafeLayoutEffect(() => { ref.current = value; }, [value]); return ref; } exports.FormContextContext = FormContextContext; exports.FormOptionsProvider = FormOptionsProvider; exports.FormProvider = FormProvider; exports.GlobalFormOptionsContext = GlobalFormOptionsContext; exports.INITIAL_KEY = INITIAL_KEY; exports.useConform = useConform; exports.useControl = useControl; exports.useField = useField; exports.useForm = useForm; exports.useFormContext = useFormContext; exports.useFormData = useFormData; exports.useFormMetadata = useFormMetadata; exports.useIntent = useIntent; exports.useLatest = useLatest; exports.useSafeLayoutEffect = useSafeLayoutEffect;