UNPKG

@conform-to/react

Version:

Conform view adapter for react

453 lines (434 loc) 18 kB
'use client'; 'use strict'; Object.defineProperty(exports, '__esModule', { value: true }); var _rollupPluginBabelHelpers = require('../_virtual/_rollupPluginBabelHelpers.js'); var dom$1 = require('@conform-to/dom'); var future = require('@conform-to/dom/future'); var react = require('react'); var dom = require('./dom.js'); var hooks = require('./hooks.js'); var state = require('./state.js'); var util = require('./util.js'); var jsxRuntime = require('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 = util.resolveSerialize(config.serialize, future.defaultSerialize); /** * Global configuration with defaults applied */ var globalConfig = _rollupPluginBabelHelpers.objectSpread2(_rollupPluginBabelHelpers.objectSpread2({}, config), {}, { intentName: (_config$intentName = config.intentName) !== null && _config$intentName !== void 0 ? _config$intentName : future.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 : util.isStandardSchemaV1, validateSchema: (_config$validateSchem = config.validateSchema) !== null && _config$validateSchem !== void 0 ? _config$validateSchem : util.validateStandardSchemaV1 }); /** * React context */ var ReactFormContext = /*#__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(ReactFormContext); 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(ReactFormContext.Provider, { value: value, children: props.children }); } function useFormContext(formId) { var contexts = react.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 = hooks.useLatest(options); var serialize = react.useMemo(() => util.resolveSerialize(options.serialize, globalSerialize), [options.serialize]); var fallbackId = react.useId(); var formId = (_options$id = options.id) !== null && _options$id !== void 0 ? _options$id : "form-".concat(fallbackId); var [state$1, handleSubmit] = hooks.useConform(formId, _rollupPluginBabelHelpers.objectSpread2(_rollupPluginBabelHelpers.objectSpread2({}, options), {}, { serialize, intentName: globalConfig.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 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 = 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 = 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 = react.useMemo(() => ({ formId, state: state$1, 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 (!(dom$1.isFieldElement(event.target) || event.target instanceof HTMLFieldSetElement) || event.target.name === '' || event.target.form === null || event.target.form !== dom.getFormElement(formId)) { return; } (_optionsRef$current$o = (_optionsRef$current = optionsRef.current).onInput) === null || _optionsRef$current$o === void 0 || _optionsRef$current$o.call(_optionsRef$current, _rollupPluginBabelHelpers.objectSpread2(_rollupPluginBabelHelpers.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 (state.isTouched(state$1, 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 (!(dom$1.isFieldElement(event.target) || event.target instanceof HTMLFieldSetElement) || event.target.name === '' || event.target.form === null || event.target.form !== dom.getFormElement(formId)) { return; } (_optionsRef$current$o2 = (_optionsRef$current2 = optionsRef.current).onBlur) === null || _optionsRef$current$o2 === void 0 || _optionsRef$current$o2.call(_optionsRef$current2, _rollupPluginBabelHelpers.objectSpread2(_rollupPluginBabelHelpers.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 (state.isTouched(state$1, event.target.name) ? shouldRevalidate === 'onBlur' : shouldValidate === 'onBlur') { intent.validate(event.target.name); } } }), [formId, state$1, serialize, constraint, handleSubmit, intent, optionsRef]); var form = react.useMemo(() => state.getFormMetadata(context, { extendFormMetadata: globalConfig.extendFormMetadata, extendFieldMetadata: globalConfig.extendFieldMetadata }), [context]); var fields = react.useMemo(() => state.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 = react.useMemo(() => state.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 = react.useMemo(() => state.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 react.useMemo(() => dom.createIntentDispatcher(() => dom.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; exports.FormProvider = FormProvider; exports.configureForms = configureForms; exports.useField = useField; exports.useForm = useForm; exports.useFormMetadata = useFormMetadata; exports.useIntent = useIntent;