@conform-to/react
Version:
Conform view adapter for react
444 lines (427 loc) • 17.7 kB
JavaScript
'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 };