UNPKG

@conform-to/react

Version:

Conform view adapter for react

444 lines (427 loc) 17.7 kB
'use client'; import { objectSpread2 as _objectSpread2 } from '../_virtual/_rollupPluginBabelHelpers.mjs'; import { isFieldElement } from '@conform-to/dom'; import { DEFAULT_INTENT_NAME, defaultSerialize } from '@conform-to/dom/future'; import { useContext, useMemo, useId, createContext } from 'react'; import { focusFirstInvalidField, getFormElement, createIntentDispatcher } from './dom.mjs'; import { useLatest, useConform } from './hooks.mjs'; import { isTouched, getFormMetadata, getFieldset, getField } from './state.mjs'; import { resolveSerialize, isStandardSchemaV1, validateStandardSchemaV1, resolveValidateResult } from './util.mjs'; import { jsx } from 'react/jsx-runtime'; function configureForms() { var _config$intentName, _config$shouldValidat, _ref, _config$shouldRevalid, _config$isSchema, _config$validateSchem; var config = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {}; /** * Global serializer that composes the user-provided serializer with the default serializer. */ var globalSerialize = resolveSerialize(config.serialize, defaultSerialize); /** * Global configuration with defaults applied */ var globalConfig = _objectSpread2(_objectSpread2({}, config), {}, { intentName: (_config$intentName = config.intentName) !== null && _config$intentName !== void 0 ? _config$intentName : DEFAULT_INTENT_NAME, shouldValidate: (_config$shouldValidat = config.shouldValidate) !== null && _config$shouldValidat !== void 0 ? _config$shouldValidat : 'onSubmit', shouldRevalidate: (_ref = (_config$shouldRevalid = config.shouldRevalidate) !== null && _config$shouldRevalid !== void 0 ? _config$shouldRevalid : config.shouldValidate) !== null && _ref !== void 0 ? _ref : 'onSubmit', isSchema: (_config$isSchema = config.isSchema) !== null && _config$isSchema !== void 0 ? _config$isSchema : isStandardSchemaV1, validateSchema: (_config$validateSchem = config.validateSchema) !== null && _config$validateSchem !== void 0 ? _config$validateSchem : validateStandardSchemaV1 }); /** * React context */ var ReactFormContext = /*#__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(ReactFormContext); 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(ReactFormContext.Provider, { value: value, children: props.children }); } function useFormContext(formId) { var contexts = useContext(ReactFormContext); var context = formId ? contexts.find(context => formId === context.formId) : contexts[0]; if (!context) { throw new Error('No form context found. ' + 'Wrap your component with <FormProvider context={form.context}> ' + 'where `form` is returned from useForm().'); } return context; } /** * 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 * * **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> * ); * ``` * * **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> * ); * ``` */ function useForm(schemaOrOptions, maybeOptions) { var _options$constraint, _globalConfig$getCons, _options$id, _options$onError; var schema; var options; if (globalConfig.isSchema(schemaOrOptions)) { schema = schemaOrOptions; options = maybeOptions !== null && maybeOptions !== void 0 ? maybeOptions : {}; } else { options = schemaOrOptions; } var constraint = (_options$constraint = options.constraint) !== null && _options$constraint !== void 0 ? _options$constraint : schema ? (_globalConfig$getCons = globalConfig.getConstraints) === null || _globalConfig$getCons === void 0 ? void 0 : _globalConfig$getCons.call(globalConfig, schema) : undefined; var optionsRef = useLatest(options); var serialize = useMemo(() => resolveSerialize(options.serialize, globalSerialize), [options.serialize]); var fallbackId = useId(); var formId = (_options$id = options.id) !== null && _options$id !== void 0 ? _options$id : "form-".concat(fallbackId); var [state, handleSubmit] = useConform(formId, _objectSpread2(_objectSpread2({}, options), {}, { serialize, intentName: globalConfig.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 schemaResult = globalConfig.validateSchema(schema, ctx.payload, options.schemaOptions); if (schemaResult instanceof Promise) { return schemaResult.then(resolvedResult => { if (typeof options.onValidate === 'function') { throw new Error('The "onValidate" handler is not supported when used with asynchronous schema validation.'); } return resolvedResult; }); } if (!options.onValidate) { return schemaResult; } // Update the schema error in the context if (schemaResult.error) { ctx.error = schemaResult.error; } var schemaValue = schemaResult.value; ctx.schemaValue = schemaValue; 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 = schemaValue; } 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 = schemaValue; 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, serialize, constraint: constraint !== null && constraint !== void 0 ? constraint : null, handleSubmit, handleInput(event) { var _optionsRef$current$o, _optionsRef$current, _optionsRef$current$s, _ref2, _optionsRef$current$s2; if (!(isFieldElement(event.target) || event.target instanceof HTMLFieldSetElement) || event.target.name === '' || event.target.form === null || event.target.form !== getFormElement(formId)) { return; } (_optionsRef$current$o = (_optionsRef$current = optionsRef.current).onInput) === null || _optionsRef$current$o === void 0 || _optionsRef$current$o.call(_optionsRef$current, _objectSpread2(_objectSpread2({}, event), {}, { target: event.target, currentTarget: event.target.form })); if (event.defaultPrevented) { return; } var shouldValidate = (_optionsRef$current$s = optionsRef.current.shouldValidate) !== null && _optionsRef$current$s !== void 0 ? _optionsRef$current$s : globalConfig.shouldValidate; var shouldRevalidate = (_ref2 = (_optionsRef$current$s2 = optionsRef.current.shouldRevalidate) !== null && _optionsRef$current$s2 !== void 0 ? _optionsRef$current$s2 : optionsRef.current.shouldValidate) !== null && _ref2 !== void 0 ? _ref2 : globalConfig.shouldRevalidate; if (isTouched(state, event.target.name) ? shouldRevalidate === 'onInput' : shouldValidate === 'onInput') { intent.validate(event.target.name); } }, handleBlur(event) { var _optionsRef$current$o2, _optionsRef$current2, _optionsRef$current$s3, _ref3, _optionsRef$current$s4; if (!(isFieldElement(event.target) || event.target instanceof HTMLFieldSetElement) || event.target.name === '' || event.target.form === null || event.target.form !== getFormElement(formId)) { return; } (_optionsRef$current$o2 = (_optionsRef$current2 = optionsRef.current).onBlur) === null || _optionsRef$current$o2 === void 0 || _optionsRef$current$o2.call(_optionsRef$current2, _objectSpread2(_objectSpread2({}, event), {}, { target: event.target, currentTarget: event.target.form })); if (event.defaultPrevented) { return; } var shouldValidate = (_optionsRef$current$s3 = optionsRef.current.shouldValidate) !== null && _optionsRef$current$s3 !== void 0 ? _optionsRef$current$s3 : globalConfig.shouldValidate; var shouldRevalidate = (_ref3 = (_optionsRef$current$s4 = optionsRef.current.shouldRevalidate) !== null && _optionsRef$current$s4 !== void 0 ? _optionsRef$current$s4 : optionsRef.current.shouldValidate) !== null && _ref3 !== void 0 ? _ref3 : globalConfig.shouldRevalidate; if (isTouched(state, event.target.name) ? shouldRevalidate === 'onBlur' : shouldValidate === 'onBlur') { intent.validate(event.target.name); } } }), [formId, state, serialize, constraint, handleSubmit, intent, optionsRef]); var form = useMemo(() => getFormMetadata(context, { extendFormMetadata: globalConfig.extendFormMetadata, extendFieldMetadata: globalConfig.extendFieldMetadata }), [context]); var fields = useMemo(() => getFieldset(context, { extendFieldMetadata: globalConfig.extendFieldMetadata }), [context]); 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 context = useFormContext(options.formId); var formMetadata = useMemo(() => getFormMetadata(context, { extendFormMetadata: globalConfig.extendFormMetadata, extendFieldMetadata: globalConfig.extendFieldMetadata }), [context]); 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 context = useFormContext(options.formId); var field = useMemo(() => getField(context, { name, extendFieldMetadata: globalConfig.extendFieldMetadata }), [context, name]); 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) { return useMemo(() => createIntentDispatcher(() => getFormElement(formRef), globalConfig.intentName), [formRef]); } return { FormProvider, useForm, useFormMetadata, useField, useIntent, config: globalConfig }; } var defaultForms = configureForms(); /** * Provides form context to child components. * Stacks contexts to support nested forms, with latest context taking priority. */ var FormProvider = defaultForms.FormProvider; /** * 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 * * **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> * ); * ``` * * **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> * ); * ``` */ var useForm = defaultForms.useForm; /** * 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> * ); * } * ``` */ var useFormMetadata = defaultForms.useFormMetadata; /** * 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> * ); * } * ``` */ var useField = defaultForms.useField; /** * 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> * ); * } * ``` */ var useIntent = defaultForms.useIntent; export { FormProvider, configureForms, useField, useForm, useFormMetadata, useIntent };