@conform-to/react
Advanced tools
Comparing version 0.9.1 to 1.0.0-pre.0
@@ -1,12 +0,7 @@ | ||
import { INTENT, VALIDATION_UNDEFINED, VALIDATION_SKIPPED } from '@conform-to/dom'; | ||
import type { FieldConfig, Primitive } from './hooks.js'; | ||
import type { CSSProperties, HTMLInputTypeAttribute } from 'react'; | ||
interface FormElementProps { | ||
id?: string; | ||
import type { FormMetadata, FieldMetadata, Pretty } from './context'; | ||
type FormControlProps = { | ||
id: string; | ||
name: string; | ||
form?: string; | ||
'aria-describedby'?: string; | ||
'aria-invalid'?: boolean; | ||
} | ||
interface FormControlProps extends FormElementProps { | ||
form: string; | ||
required?: boolean; | ||
@@ -16,11 +11,13 @@ autoFocus?: boolean; | ||
style?: CSSProperties; | ||
'aria-describedby'?: string; | ||
'aria-invalid'?: boolean; | ||
'aria-hidden'?: boolean; | ||
} | ||
interface InputProps<Schema> extends FormControlProps { | ||
type?: HTMLInputTypeAttribute; | ||
}; | ||
type InputProps = Pretty<FormControlProps & { | ||
type?: Exclude<HTMLInputTypeAttribute, 'submit' | 'reset' | 'button'>; | ||
minLength?: number; | ||
maxLength?: number; | ||
min?: Schema extends number ? number : string | number; | ||
max?: Schema extends number ? number : string | number; | ||
step?: Schema extends number ? number : string | number; | ||
min?: string | number; | ||
max?: string | number; | ||
step?: string | number; | ||
pattern?: string; | ||
@@ -31,12 +28,13 @@ multiple?: boolean; | ||
defaultValue?: string; | ||
} | ||
interface SelectProps extends FormControlProps { | ||
}>; | ||
type SelectProps = Pretty<FormControlProps & { | ||
defaultValue?: string | number | readonly string[] | undefined; | ||
multiple?: boolean; | ||
} | ||
interface TextareaProps extends FormControlProps { | ||
}>; | ||
type TextareaProps = Pretty<FormControlProps & { | ||
minLength?: number; | ||
maxLength?: number; | ||
defaultValue?: string; | ||
} | ||
}>; | ||
type Primitive = string | number | boolean | Date | null | undefined; | ||
type BaseOptions = { | ||
@@ -51,2 +49,6 @@ ariaAttributes?: true; | ||
}; | ||
type FormOptions<Schema extends Record<string, any>> = BaseOptions & { | ||
onSubmit?: (event: React.FormEvent<HTMLFormElement>, context: ReturnType<FormMetadata<Schema>['onSubmit']>) => void; | ||
onReset?: (event: React.FormEvent<HTMLFormElement>) => void; | ||
}; | ||
type InputOptions = ControlOptions & ({ | ||
@@ -64,13 +66,23 @@ type: 'checkbox' | 'radio'; | ||
}; | ||
export declare function input<Schema extends Primitive | unknown>(config: FieldConfig<Schema>, options?: InputOptions): InputProps<Schema>; | ||
export declare function input<Schema extends File | File[]>(config: FieldConfig<Schema>, options: InputOptions & { | ||
export declare function input<Schema extends Primitive | unknown>(field: FieldMetadata<Schema>, options?: InputOptions): InputProps; | ||
export declare function input<Schema extends File | File[]>(field: FieldMetadata<Schema>, options: InputOptions & { | ||
type: 'file'; | ||
}): InputProps<Schema>; | ||
export declare function select<Schema extends Primitive | Primitive[] | undefined | unknown>(config: FieldConfig<Schema>, options?: ControlOptions): SelectProps; | ||
export declare function textarea<Schema extends Primitive | undefined | unknown>(config: FieldConfig<Schema>, options?: ControlOptions): TextareaProps; | ||
export declare function fieldset<Schema extends Record<string, unknown> | undefined | unknown>(config: FieldConfig<Schema>, options?: BaseOptions): FormControlProps; | ||
export declare function collection<Schema extends Array<string | boolean> | string | boolean | undefined | unknown>(config: FieldConfig<Schema>, options: BaseOptions & { | ||
}): InputProps; | ||
export declare function select<Schema extends Primitive | Primitive[] | undefined | unknown>(metadata: FieldMetadata<Schema>, options?: ControlOptions): SelectProps; | ||
export declare function textarea<Schema extends Primitive | undefined | unknown>(metadata: FieldMetadata<Schema>, options?: ControlOptions): TextareaProps; | ||
export declare function form<Schema extends Record<string, any>>(metadata: FormMetadata<Schema>, options?: FormOptions<Schema>): { | ||
id: string; | ||
onSubmit: (event: React.FormEvent<HTMLFormElement>) => void; | ||
onReset: (event: import("react").FormEvent<HTMLFormElement>) => void; | ||
noValidate: boolean; | ||
}; | ||
export declare function fieldset<Schema extends Record<string, any> | undefined | unknown>(metadata: FieldMetadata<Schema>, options?: BaseOptions): { | ||
id: string; | ||
name: import("@conform-to/dom").FieldName<Schema>; | ||
form: string; | ||
}; | ||
export declare function collection<Schema extends Array<string | boolean> | string | boolean | undefined | unknown>(metadata: FieldMetadata<Schema>, options: BaseOptions & { | ||
type: 'checkbox' | 'radio'; | ||
options: string[]; | ||
}): Array<InputProps<Schema> & Pick<Required<InputProps<Schema>>, 'type' | 'value'>>; | ||
export { INTENT, VALIDATION_UNDEFINED, VALIDATION_SKIPPED }; | ||
}): Array<InputProps & Pick<Required<InputProps>, 'type' | 'value'>>; | ||
export {}; |
145
helpers.js
@@ -6,3 +6,2 @@ 'use strict'; | ||
var _rollupPluginBabelHelpers = require('./_virtual/_rollupPluginBabelHelpers.js'); | ||
var dom = require('@conform-to/dom'); | ||
@@ -21,27 +20,21 @@ /** | ||
} | ||
function getFormElementProps(config) { | ||
var _options$ariaAttribut, _config$error, _config$error2; | ||
function getAriaAttributes(metadata) { | ||
var options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {}; | ||
var hasAriaAttributes = (_options$ariaAttribut = options.ariaAttributes) !== null && _options$ariaAttribut !== void 0 ? _options$ariaAttribut : true; | ||
if (typeof options.ariaAttributes !== 'undefined' && !options.ariaAttributes) { | ||
return {}; | ||
} | ||
return cleanup({ | ||
id: config.id, | ||
name: config.name, | ||
form: config.form, | ||
'aria-invalid': hasAriaAttributes && config.errorId && (_config$error = config.error) !== null && _config$error !== void 0 && _config$error.length ? true : undefined, | ||
'aria-describedby': hasAriaAttributes ? [config.errorId && (_config$error2 = config.error) !== null && _config$error2 !== void 0 && _config$error2.length ? config.errorId : undefined, config.descriptionId && options.ariaAttributes !== false && options.description ? config.descriptionId : undefined].reduce((result, id) => { | ||
if (!result) { | ||
return id; | ||
} | ||
if (!id) { | ||
return result; | ||
} | ||
return "".concat(result, " ").concat(id); | ||
}) : undefined | ||
'aria-invalid': !metadata.valid || undefined, | ||
'aria-describedby': metadata.valid ? options.description ? metadata.descriptionId : undefined : "".concat(metadata.errorId, " ").concat(options.description ? metadata.descriptionId : '').trim() | ||
}); | ||
} | ||
function getFormControlProps(config, options) { | ||
return cleanup(_rollupPluginBabelHelpers.objectSpread2(_rollupPluginBabelHelpers.objectSpread2({}, getFormElementProps(config, options)), {}, { | ||
required: config.required, | ||
autoFocus: config.initialError && Object.entries(config.initialError).length > 0 ? true : undefined | ||
}, options !== null && options !== void 0 && options.hidden ? hiddenProps : undefined)); | ||
function getFormControlProps(metadata, options) { | ||
var _metadata$constraint; | ||
return cleanup(_rollupPluginBabelHelpers.objectSpread2(_rollupPluginBabelHelpers.objectSpread2({ | ||
id: metadata.id, | ||
name: metadata.name, | ||
form: metadata.formId, | ||
required: ((_metadata$constraint = metadata.constraint) === null || _metadata$constraint === void 0 ? void 0 : _metadata$constraint.required) || undefined, | ||
autoFocus: !metadata.valid || undefined | ||
}, options !== null && options !== void 0 && options.hidden ? hiddenProps : undefined), getAriaAttributes(metadata, options))); | ||
} | ||
@@ -67,13 +60,14 @@ var hiddenProps = { | ||
}; | ||
function input(config) { | ||
function input(field) { | ||
var _field$constraint, _field$constraint2, _field$constraint3, _field$constraint4, _field$constraint5, _field$constraint6, _field$constraint7; | ||
var options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {}; | ||
var props = _rollupPluginBabelHelpers.objectSpread2(_rollupPluginBabelHelpers.objectSpread2({}, getFormControlProps(config, options)), {}, { | ||
var props = _rollupPluginBabelHelpers.objectSpread2(_rollupPluginBabelHelpers.objectSpread2({}, getFormControlProps(field, options)), {}, { | ||
type: options.type, | ||
minLength: config.minLength, | ||
maxLength: config.maxLength, | ||
min: config.min, | ||
max: config.max, | ||
step: config.step, | ||
pattern: config.pattern, | ||
multiple: config.multiple | ||
minLength: (_field$constraint = field.constraint) === null || _field$constraint === void 0 ? void 0 : _field$constraint.minLength, | ||
maxLength: (_field$constraint2 = field.constraint) === null || _field$constraint2 === void 0 ? void 0 : _field$constraint2.maxLength, | ||
min: (_field$constraint3 = field.constraint) === null || _field$constraint3 === void 0 ? void 0 : _field$constraint3.min, | ||
max: (_field$constraint4 = field.constraint) === null || _field$constraint4 === void 0 ? void 0 : _field$constraint4.max, | ||
step: (_field$constraint5 = field.constraint) === null || _field$constraint5 === void 0 ? void 0 : _field$constraint5.step, | ||
pattern: (_field$constraint6 = field.constraint) === null || _field$constraint6 === void 0 ? void 0 : _field$constraint6.pattern, | ||
multiple: (_field$constraint7 = field.constraint) === null || _field$constraint7 === void 0 ? void 0 : _field$constraint7.multiple | ||
}); | ||
@@ -83,51 +77,68 @@ if (options.type === 'checkbox' || options.type === 'radio') { | ||
props.value = (_options$value = options.value) !== null && _options$value !== void 0 ? _options$value : 'on'; | ||
props.defaultChecked = config.defaultValue === props.value; | ||
props.defaultChecked = typeof field.initialValue === 'boolean' ? field.initialValue : field.initialValue === props.value; | ||
} else if (options.type !== 'file') { | ||
props.defaultValue = config.defaultValue; | ||
var _field$initialValue; | ||
props.defaultValue = (_field$initialValue = field.initialValue) === null || _field$initialValue === void 0 ? void 0 : _field$initialValue.toString(); | ||
} | ||
return cleanup(props); | ||
} | ||
function select(config, options) { | ||
return cleanup(_rollupPluginBabelHelpers.objectSpread2(_rollupPluginBabelHelpers.objectSpread2({}, getFormControlProps(config, options)), {}, { | ||
defaultValue: config.defaultValue, | ||
multiple: config.multiple | ||
function select(metadata, options) { | ||
var _metadata$initialValu, _metadata$constraint2; | ||
return cleanup(_rollupPluginBabelHelpers.objectSpread2(_rollupPluginBabelHelpers.objectSpread2({}, getFormControlProps(metadata, options)), {}, { | ||
defaultValue: (_metadata$initialValu = metadata.initialValue) === null || _metadata$initialValu === void 0 ? void 0 : _metadata$initialValu.toString(), | ||
multiple: (_metadata$constraint2 = metadata.constraint) === null || _metadata$constraint2 === void 0 ? void 0 : _metadata$constraint2.multiple | ||
})); | ||
} | ||
function textarea(config, options) { | ||
return cleanup(_rollupPluginBabelHelpers.objectSpread2(_rollupPluginBabelHelpers.objectSpread2({}, getFormControlProps(config, options)), {}, { | ||
defaultValue: config.defaultValue, | ||
minLength: config.minLength, | ||
maxLength: config.maxLength | ||
function textarea(metadata, options) { | ||
var _metadata$initialValu2, _metadata$constraint3, _metadata$constraint4; | ||
return cleanup(_rollupPluginBabelHelpers.objectSpread2(_rollupPluginBabelHelpers.objectSpread2({}, getFormControlProps(metadata, options)), {}, { | ||
defaultValue: (_metadata$initialValu2 = metadata.initialValue) === null || _metadata$initialValu2 === void 0 ? void 0 : _metadata$initialValu2.toString(), | ||
minLength: (_metadata$constraint3 = metadata.constraint) === null || _metadata$constraint3 === void 0 ? void 0 : _metadata$constraint3.minLength, | ||
maxLength: (_metadata$constraint4 = metadata.constraint) === null || _metadata$constraint4 === void 0 ? void 0 : _metadata$constraint4.maxLength | ||
})); | ||
} | ||
function fieldset(config, options) { | ||
return getFormElementProps(config, options); | ||
function form(metadata, options) { | ||
var onSubmit = options === null || options === void 0 ? void 0 : options.onSubmit; | ||
var onReset = options === null || options === void 0 ? void 0 : options.onReset; | ||
return cleanup(_rollupPluginBabelHelpers.objectSpread2({ | ||
id: metadata.id, | ||
onSubmit: typeof onSubmit !== 'function' ? metadata.onSubmit : event => { | ||
var context = metadata.onSubmit(event); | ||
if (!event.defaultPrevented) { | ||
onSubmit(event, context); | ||
} | ||
}, | ||
onReset: typeof onReset !== 'function' ? metadata.onReset : event => { | ||
metadata.onReset(event); | ||
onReset(event); | ||
}, | ||
noValidate: metadata.noValidate | ||
}, getAriaAttributes(metadata, options))); | ||
} | ||
function collection(config, options) { | ||
return options.options.map(value => cleanup(_rollupPluginBabelHelpers.objectSpread2(_rollupPluginBabelHelpers.objectSpread2({}, getFormControlProps(config, options)), {}, { | ||
id: config.id ? "".concat(config.id, "-").concat(value) : undefined, | ||
type: options.type, | ||
value, | ||
defaultChecked: options.type === 'checkbox' && Array.isArray(config.defaultValue) ? config.defaultValue.includes(value) : config.defaultValue === value, | ||
// The required attribute doesn't make sense for checkbox group | ||
// As it would require all checkboxes to be checked instead of at least one | ||
// overriden with `undefiend` so it gets cleaned up | ||
required: options.type === 'checkbox' ? undefined : config.required | ||
}))); | ||
function fieldset(metadata, options) { | ||
return cleanup(_rollupPluginBabelHelpers.objectSpread2({ | ||
id: metadata.id, | ||
name: metadata.name, | ||
form: metadata.formId | ||
}, getAriaAttributes(metadata, options))); | ||
} | ||
function collection(metadata, options) { | ||
return options.options.map(value => { | ||
var _metadata$constraint5; | ||
return cleanup(_rollupPluginBabelHelpers.objectSpread2(_rollupPluginBabelHelpers.objectSpread2({}, getFormControlProps(metadata, options)), {}, { | ||
id: "".concat(metadata.id, "-").concat(value), | ||
type: options.type, | ||
value, | ||
defaultChecked: options.type === 'checkbox' && Array.isArray(metadata.initialValue) ? metadata.initialValue.includes(value) : metadata.initialValue === value, | ||
// The required attribute doesn't make sense for checkbox group | ||
// As it would require all checkboxes to be checked instead of at least one | ||
// It is overriden with `undefiend` so it could be cleaned upW properly | ||
required: options.type === 'checkbox' ? undefined : (_metadata$constraint5 = metadata.constraint) === null || _metadata$constraint5 === void 0 ? void 0 : _metadata$constraint5.required | ||
})); | ||
}); | ||
} | ||
Object.defineProperty(exports, 'INTENT', { | ||
enumerable: true, | ||
get: function () { return dom.INTENT; } | ||
}); | ||
Object.defineProperty(exports, 'VALIDATION_SKIPPED', { | ||
enumerable: true, | ||
get: function () { return dom.VALIDATION_SKIPPED; } | ||
}); | ||
Object.defineProperty(exports, 'VALIDATION_UNDEFINED', { | ||
enumerable: true, | ||
get: function () { return dom.VALIDATION_UNDEFINED; } | ||
}); | ||
exports.collection = collection; | ||
exports.fieldset = fieldset; | ||
exports.form = form; | ||
exports.hiddenProps = hiddenProps; | ||
@@ -134,0 +145,0 @@ exports.input = input; |
244
hooks.d.ts
@@ -1,31 +0,12 @@ | ||
import { type FieldConstraint, type FieldElement, type FieldsetConstraint, type Submission, type KeysOf, type ResolveType, getFormEncType, getFormMethod, parseIntent } from '@conform-to/dom'; | ||
import { type FormEvent, type RefObject } from 'react'; | ||
export type Primitive = null | undefined | string | number | boolean | Date; | ||
export interface FieldConfig<Schema> extends FieldConstraint<Schema> { | ||
id?: string; | ||
name: string; | ||
defaultValue?: FieldValue<Schema>; | ||
initialError?: Record<string, string[]>; | ||
form?: string; | ||
descriptionId?: string; | ||
errorId?: string; | ||
import { type UnionKeyof, type UnionKeyType, type FieldName, type Form, type FormOptions } from '@conform-to/dom'; | ||
import { useEffect } from 'react'; | ||
import { type FormMetadata, type FieldMetadata, type Pretty } from './context'; | ||
/** | ||
* useLayoutEffect is client-only. | ||
* This basically makes it a no-op on server | ||
*/ | ||
export declare const useSafeLayoutEffect: typeof useEffect; | ||
export declare function useFormId(preferredId?: string): string; | ||
export declare function useForm<Schema extends Record<string, any>>(options: Pretty<FormOptions<Schema> & { | ||
/** | ||
* The frist error of the field | ||
*/ | ||
error?: string; | ||
/** | ||
* All of the field errors | ||
*/ | ||
errors?: string[]; | ||
} | ||
export type FieldValue<Schema> = Schema extends Primitive ? string : Schema extends File ? File : Schema extends Array<infer InnerType> ? Array<FieldValue<InnerType>> : unknown extends Schema ? any : Record<string, any> extends Schema ? { | ||
[Key in KeysOf<Schema>]?: FieldValue<ResolveType<Schema, Key>>; | ||
} : any; | ||
type SubmissionResult = { | ||
intent: Submission['intent']; | ||
payload: Submission['payload'] | null; | ||
error: Submission['error']; | ||
}; | ||
export interface FormConfig<Output extends Record<string, any>, Input extends Record<string, any> = Output> { | ||
/** | ||
* If the form id is provided, Id for label, | ||
@@ -36,174 +17,39 @@ * input and error elements will be derived. | ||
/** | ||
* A form ref object. Conform will fallback to its own ref object if it is not provided. | ||
*/ | ||
ref?: RefObject<HTMLFormElement>; | ||
/** | ||
* Define when conform should start validation. | ||
* Support "onSubmit", "onInput", "onBlur". | ||
* Enable constraint validation before the dom is hydated. | ||
* | ||
* @default "onSubmit" | ||
* Default to `true`. | ||
*/ | ||
shouldValidate?: 'onSubmit' | 'onBlur' | 'onInput'; | ||
/** | ||
* Define when conform should revalidate again. | ||
* Support "onSubmit", "onInput", "onBlur". | ||
* | ||
* @default shouldValidate, or "onSubmit" if shouldValidate is not provided. | ||
*/ | ||
shouldRevalidate?: 'onSubmit' | 'onBlur' | 'onInput'; | ||
/** | ||
* An object representing the initial value of the form. | ||
*/ | ||
defaultValue?: FieldValue<Input>; | ||
/** | ||
* An object describing the result of the last submission | ||
*/ | ||
lastSubmission?: SubmissionResult | null; | ||
/** | ||
* An object describing the constraint of each field | ||
*/ | ||
constraint?: FieldsetConstraint<Input>; | ||
/** | ||
* Enable native validation before hydation. | ||
* | ||
* Default to `false`. | ||
*/ | ||
fallbackNative?: boolean; | ||
/** | ||
* Accept form submission regardless of the form validity. | ||
* | ||
* Default to `false`. | ||
*/ | ||
noValidate?: boolean; | ||
/** | ||
* A function to be called when the form should be (re)validated. | ||
*/ | ||
onValidate?: ({ form, formData, }: { | ||
form: HTMLFormElement; | ||
formData: FormData; | ||
}) => Submission | Submission<Output>; | ||
/** | ||
* The submit event handler of the form. It will be called | ||
* only when the form is considered valid. | ||
*/ | ||
onSubmit?: (event: FormEvent<HTMLFormElement>, context: { | ||
formData: FormData; | ||
submission: Submission; | ||
action: string; | ||
encType: ReturnType<typeof getFormEncType>; | ||
method: ReturnType<typeof getFormMethod>; | ||
}) => void; | ||
} | ||
/** | ||
* Properties to be applied to the form element | ||
*/ | ||
interface FormProps { | ||
id?: string; | ||
ref: RefObject<HTMLFormElement>; | ||
onSubmit: (event: FormEvent<HTMLFormElement>) => void; | ||
noValidate: boolean; | ||
'aria-invalid'?: 'true'; | ||
'aria-describedby'?: string; | ||
} | ||
interface Form { | ||
id?: string; | ||
errorId?: string; | ||
error: string | undefined; | ||
errors: string[]; | ||
ref: RefObject<HTMLFormElement>; | ||
props: FormProps; | ||
} | ||
/** | ||
* Returns properties required to hook into form events. | ||
* Applied custom validation and define when error should be reported. | ||
* | ||
* @see https://conform.guide/api/react#useform | ||
*/ | ||
export declare function useForm<Output extends Record<string, any>, Input extends Record<string, any> = Output>(config?: FormConfig<Output, Input>): [Form, Fieldset<Input>]; | ||
/** | ||
* A set of field configuration | ||
*/ | ||
export type Fieldset<Schema extends Record<string, any> | undefined> = { | ||
[Key in KeysOf<Schema>]-?: FieldConfig<ResolveType<Schema, Key>>; | ||
defaultNoValidate?: boolean; | ||
}>): { | ||
form: FormMetadata<Schema>; | ||
context: Form<Schema>; | ||
fields: Pretty<FieldsetMetadata<Schema>>; | ||
}; | ||
export interface FieldsetConfig<Schema extends Record<string, any> | undefined> { | ||
/** | ||
* The prefix used to generate the name of nested fields. | ||
*/ | ||
name?: string; | ||
/** | ||
* An object representing the initial value of the fieldset. | ||
*/ | ||
defaultValue?: FieldValue<Schema>; | ||
/** | ||
* An object describing the initial error of each field | ||
*/ | ||
initialError?: Record<string, string[]>; | ||
/** | ||
* An object describing the constraint of each field | ||
*/ | ||
constraint?: FieldsetConstraint<Schema>; | ||
/** | ||
* The id of the form, connecting each field to a form remotely | ||
*/ | ||
form?: string; | ||
} | ||
/** | ||
* Returns all the information about the fieldset. | ||
* | ||
* @see https://conform.guide/api/react#usefieldset | ||
*/ | ||
export declare function useFieldset<Schema extends Record<string, any> | undefined>(ref: RefObject<HTMLFormElement | HTMLFieldSetElement>, config: FieldsetConfig<Schema>): Fieldset<Schema>; | ||
export declare function useFieldset<Schema extends Record<string, any> | undefined>(ref: RefObject<HTMLFormElement | HTMLFieldSetElement>, config: FieldConfig<Schema>): Fieldset<Schema>; | ||
/** | ||
* Returns a list of key and field config. | ||
* | ||
* @see https://conform.guide/api/react#usefieldlist | ||
*/ | ||
export declare function useFieldList<Schema extends Array<any> | undefined>(ref: RefObject<HTMLFormElement | HTMLFieldSetElement>, config: FieldConfig<Schema>): Array<{ | ||
key: string; | ||
} & FieldConfig<Schema extends Array<infer Item> ? Item : never>>; | ||
interface InputControl { | ||
change: (eventOrValue: { | ||
target: { | ||
value: string; | ||
}; | ||
} | string | boolean) => void; | ||
focus: () => void; | ||
blur: () => void; | ||
} | ||
/** | ||
* Returns a ref object and a set of helpers that dispatch corresponding dom event. | ||
* | ||
* @see https://conform.guide/api/react#useinputevent | ||
*/ | ||
export declare function useInputEvent(options: { | ||
ref: RefObject<FieldElement> | (() => Element | RadioNodeList | FieldElement | null | undefined); | ||
onInput?: (event: Event) => void; | ||
onFocus?: (event: FocusEvent) => void; | ||
onBlur?: (event: FocusEvent) => void; | ||
onReset?: (event: Event) => void; | ||
}): InputControl; | ||
export declare const FORM_ERROR_ELEMENT_NAME = "__form__"; | ||
/** | ||
* Validate the form with the Constraint Validation API | ||
* @see https://conform.guide/api/react#validateconstraint | ||
*/ | ||
export declare function validateConstraint(options: { | ||
form: HTMLFormElement; | ||
formData?: FormData; | ||
constraint?: Record<Lowercase<string>, (value: string, context: { | ||
formData: FormData; | ||
attributeValue: string; | ||
}) => boolean>; | ||
formatMessages?: ({ name, validity, constraint, defaultErrors, }: { | ||
name: string; | ||
validity: ValidityState; | ||
constraint: Record<string, boolean>; | ||
defaultErrors: string[]; | ||
}) => string[]; | ||
}): Submission; | ||
export declare function getUniqueKey(): string; | ||
export declare function reportSubmission(form: HTMLFormElement, submission: SubmissionResult): void; | ||
export declare function getScope(intent: ReturnType<typeof parseIntent>): string | null; | ||
export {}; | ||
export declare function useFormMetadata<Schema extends Record<string, any>>(options: { | ||
formId: string; | ||
context?: Form<Schema>; | ||
defaultNoValidate?: boolean; | ||
}): FormMetadata<Schema>; | ||
export type FieldsetMetadata<Schema> = Schema extends Array<any> ? { | ||
[Key in keyof Schema]: FieldMetadata<Schema[Key]>; | ||
} : Schema extends { | ||
[key in string]?: any; | ||
} ? { | ||
[Key in UnionKeyof<Schema>]: FieldMetadata<UnionKeyType<Schema, Key>>; | ||
} : Record<string | number, FieldMetadata<any>>; | ||
export declare function useFieldset<Schema>(options: { | ||
formId: string; | ||
name?: FieldName<Schema>; | ||
context?: Form; | ||
}): Pretty<FieldsetMetadata<Schema>>; | ||
export type Item<List> = List extends Array<infer Item> ? Item : any; | ||
export declare function useFieldList<Schema>(options: { | ||
formId: string; | ||
name: FieldName<Schema>; | ||
context?: Form; | ||
}): Array<FieldMetadata<Item<Schema>>>; | ||
export declare function useField<Schema>(options: { | ||
formId: string; | ||
name: FieldName<Schema>; | ||
context?: Form; | ||
}): FieldMetadata<Schema>; |
824
hooks.js
@@ -5,737 +5,161 @@ 'use strict'; | ||
var _rollupPluginBabelHelpers = require('./_virtual/_rollupPluginBabelHelpers.js'); | ||
var dom = require('@conform-to/dom'); | ||
var react = require('react'); | ||
var context = require('./context.js'); | ||
/** | ||
* Properties to be applied to the form element | ||
* useLayoutEffect is client-only. | ||
* This basically makes it a no-op on server | ||
*/ | ||
var useSafeLayoutEffect = typeof document === 'undefined' ? react.useEffect : react.useLayoutEffect; | ||
function useFormId(preferredId) { | ||
var id = react.useId(); | ||
return preferredId !== null && preferredId !== void 0 ? preferredId : id; | ||
} | ||
function useForm(options) { | ||
var formId = useFormId(options.id); | ||
var initializeForm = () => dom.createForm(formId, options); | ||
var [form, setForm] = react.useState(initializeForm); | ||
function useNoValidate(defaultNoValidate, validateBeforeHydrate) { | ||
var [noValidate, setNoValidate] = react.useState(defaultNoValidate || !validateBeforeHydrate); | ||
react.useEffect(() => { | ||
setNoValidate(true); | ||
}, []); | ||
return noValidate; | ||
} | ||
function useFormRef(userProvidedRef) { | ||
var formRef = react.useRef(null); | ||
return userProvidedRef !== null && userProvidedRef !== void 0 ? userProvidedRef : formRef; | ||
} | ||
function useConfigRef(config) { | ||
var ref = react.useRef(config); | ||
// If id changes, reinitialize the form immediately | ||
if (formId !== form.id) { | ||
setForm(initializeForm); | ||
} | ||
var optionsRef = react.useRef(options); | ||
var metadata = useFormMetadata({ | ||
formId, | ||
context: form, | ||
defaultNoValidate: options.defaultNoValidate | ||
}); | ||
var fields = useFieldset({ | ||
formId, | ||
context: form | ||
}); | ||
useSafeLayoutEffect(() => form.initialize(), [form]); | ||
useSafeLayoutEffect(() => { | ||
ref.current = config; | ||
}); | ||
return ref; | ||
} | ||
function useFormReporter(ref, lastSubmission) { | ||
var [submission, setSubmission] = react.useState(lastSubmission); | ||
var report = react.useCallback((form, submission) => { | ||
var event = new CustomEvent('conform', { | ||
detail: submission.intent | ||
}); | ||
form.dispatchEvent(event); | ||
setSubmission(submission); | ||
}, []); | ||
react.useEffect(() => { | ||
var form = ref.current; | ||
if (!form || !lastSubmission) { | ||
if (options.lastResult === optionsRef.current.lastResult) { | ||
// If there is no change, do nothing | ||
return; | ||
} | ||
if (!lastSubmission.payload) { | ||
// If the default value is empty, we can safely reset the form. | ||
// This ensure the behavior is consistent with and without JS. | ||
form.reset(); | ||
// There is no need to report the submission anymore. | ||
return; | ||
if (options.lastResult) { | ||
form.report(options.lastResult); | ||
} else { | ||
var _document$forms$named; | ||
(_document$forms$named = document.forms.namedItem(form.id)) === null || _document$forms$named === void 0 ? void 0 : _document$forms$named.reset(); | ||
} | ||
report(form, lastSubmission); | ||
}, [ref, lastSubmission, report]); | ||
react.useEffect(() => { | ||
var form = ref.current; | ||
if (!form || !submission) { | ||
return; | ||
} | ||
reportSubmission(form, submission); | ||
}, [ref, submission]); | ||
return report; | ||
} | ||
function useFormError(ref, config) { | ||
var [error, setError] = react.useState(() => { | ||
if (!config.initialError) { | ||
return {}; | ||
} | ||
var result = {}; | ||
for (var [name, message] of Object.entries(config.initialError)) { | ||
var [path, ...restPaths] = dom.getPaths(name); | ||
if (typeof path !== 'undefined' && restPaths.length === 0) { | ||
result[path] = message; | ||
} | ||
} | ||
return result; | ||
}, [form, options.lastResult]); | ||
useSafeLayoutEffect(() => { | ||
optionsRef.current = options; | ||
form.update(options); | ||
}); | ||
react.useEffect(() => { | ||
var handleInvalid = event => { | ||
var _config$name; | ||
var form = dom.getFormElement(ref.current); | ||
var element = event.target; | ||
var prefix = (_config$name = config.name) !== null && _config$name !== void 0 ? _config$name : ''; | ||
if (!dom.isFieldElement(element) || element.form !== form || !element.name.startsWith(prefix) || !element.dataset.conformTouched) { | ||
return; | ||
} | ||
var name = element.name.slice(prefix.length); | ||
var [path, ...restPaths] = dom.getPaths(name); | ||
if (typeof path === 'undefined' || restPaths.length > 0) { | ||
return; | ||
} | ||
setError(prev => { | ||
if (element.validationMessage === dom.getValidationMessage(prev[path])) { | ||
return prev; | ||
} | ||
return _rollupPluginBabelHelpers.objectSpread2(_rollupPluginBabelHelpers.objectSpread2({}, prev), {}, { | ||
[path]: dom.getErrors(element.validationMessage) | ||
}); | ||
}); | ||
event.preventDefault(); | ||
}; | ||
var handleReset = event => { | ||
var form = dom.getFormElement(ref.current); | ||
if (form && event.target === form) { | ||
setError({}); | ||
} | ||
}; | ||
document.addEventListener('reset', handleReset); | ||
document.addEventListener('invalid', handleInvalid, true); | ||
return () => { | ||
document.removeEventListener('reset', handleReset); | ||
document.removeEventListener('invalid', handleInvalid, true); | ||
}; | ||
}, [ref, config.name]); | ||
return [error, setError]; | ||
return { | ||
context: form, | ||
fields, | ||
form: metadata | ||
}; | ||
} | ||
/** | ||
* Returns properties required to hook into form events. | ||
* Applied custom validation and define when error should be reported. | ||
* | ||
* @see https://conform.guide/api/react#useform | ||
*/ | ||
function useForm() { | ||
var _config$lastSubmissio3, _config$lastSubmissio4; | ||
var config = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {}; | ||
var configRef = useConfigRef(config); | ||
var ref = useFormRef(config.ref); | ||
var noValidate = useNoValidate(config.noValidate, config.fallbackNative); | ||
var report = useFormReporter(ref, config.lastSubmission); | ||
var [errors, setErrors] = react.useState(() => { | ||
var _config$lastSubmissio, _config$lastSubmissio2; | ||
return (_config$lastSubmissio = (_config$lastSubmissio2 = config.lastSubmission) === null || _config$lastSubmissio2 === void 0 ? void 0 : _config$lastSubmissio2.error['']) !== null && _config$lastSubmissio !== void 0 ? _config$lastSubmissio : []; | ||
function useFormMetadata(options) { | ||
var _options$defaultNoVal; | ||
var subjectRef = context.useSubjectRef(); | ||
var form = context.useFormStore(options.formId, options.context); | ||
var context$1 = context.useFormContext(form, subjectRef); | ||
var metadata = context.getBaseMetadata(options.formId, context$1, { | ||
subjectRef | ||
}); | ||
var initialError = react.useMemo(() => { | ||
var _submission$error$sco; | ||
var submission = config.lastSubmission; | ||
if (!submission) { | ||
return {}; | ||
var [noValidate, setNoValidate] = react.useState((_options$defaultNoVal = options.defaultNoValidate) !== null && _options$defaultNoVal !== void 0 ? _options$defaultNoVal : true); | ||
useSafeLayoutEffect(() => { | ||
// This is necessary to fix an issue in strict mode with related to our proxy setup | ||
// It avoids the component from being rerendered without re-rendering the child | ||
// Which reset the proxy but failed to capture its usage within child component | ||
if (!noValidate) { | ||
setNoValidate(true); | ||
} | ||
var intent = dom.parseIntent(submission.intent); | ||
var scope = getScope(intent); | ||
if (typeof scope !== 'string') { | ||
return submission.error; | ||
} | ||
return { | ||
[scope]: (_submission$error$sco = submission.error[scope]) !== null && _submission$error$sco !== void 0 ? _submission$error$sco : [] | ||
}; | ||
}, [config.lastSubmission]); | ||
// This payload from lastSubmission is only useful before hydration | ||
// After hydration, any new payload on lastSubmission will be ignored | ||
var [defaultValueFromLastSubmission, setDefaultValueFromLastSubmission] = react.useState( // @ts-expect-error defaultValue is not in Submission type | ||
(_config$lastSubmissio3 = (_config$lastSubmissio4 = config.lastSubmission) === null || _config$lastSubmissio4 === void 0 ? void 0 : _config$lastSubmissio4.payload) !== null && _config$lastSubmissio3 !== void 0 ? _config$lastSubmissio3 : null); | ||
var fieldset = useFieldset(ref, { | ||
defaultValue: defaultValueFromLastSubmission !== null && defaultValueFromLastSubmission !== void 0 ? defaultValueFromLastSubmission : config.defaultValue, | ||
initialError, | ||
constraint: config.constraint, | ||
form: config.id | ||
}); | ||
react.useEffect(() => { | ||
// custom validate handler | ||
var createValidateHandler = type => event => { | ||
var field = event.target; | ||
var form = ref.current; | ||
var { | ||
shouldValidate = 'onSubmit', | ||
shouldRevalidate = shouldValidate | ||
} = configRef.current; | ||
if (!form || !dom.isFocusableFormControl(field) || field.form !== form || !field.name) { | ||
return; | ||
} | ||
if (field.dataset.conformTouched ? shouldRevalidate === type : shouldValidate === type) { | ||
dom.requestIntent(form, dom.validate(field.name)); | ||
} | ||
}; | ||
var handleInvalid = event => { | ||
var form = ref.current; | ||
var field = event.target; | ||
if (!form || !dom.isFieldElement(field) || field.form !== form || field.name !== FORM_ERROR_ELEMENT_NAME) { | ||
return; | ||
} | ||
event.preventDefault(); | ||
if (field.dataset.conformTouched) { | ||
setErrors(dom.getErrors(field.validationMessage)); | ||
} | ||
}; | ||
var handleReset = event => { | ||
var form = ref.current; | ||
if (!form || event.target !== form) { | ||
return; | ||
} | ||
// Reset all field state | ||
for (var _element of dom.getFormControls(form)) { | ||
delete _element.dataset.conformTouched; | ||
_element.setCustomValidity(''); | ||
} | ||
setErrors([]); | ||
setDefaultValueFromLastSubmission(null); | ||
}; | ||
var handleInput = createValidateHandler('onInput'); | ||
var handleBlur = createValidateHandler('onBlur'); | ||
document.addEventListener('input', handleInput, true); | ||
document.addEventListener('blur', handleBlur, true); | ||
document.addEventListener('invalid', handleInvalid, true); | ||
document.addEventListener('reset', handleReset); | ||
return () => { | ||
document.removeEventListener('input', handleInput, true); | ||
document.removeEventListener('blur', handleBlur, true); | ||
document.removeEventListener('invalid', handleInvalid, true); | ||
document.removeEventListener('reset', handleReset); | ||
}; | ||
}, [ref, configRef]); | ||
var form = { | ||
ref, | ||
error: errors[0], | ||
errors, | ||
props: { | ||
ref, | ||
noValidate, | ||
onSubmit(event) { | ||
var form = event.currentTarget; | ||
var nativeEvent = event.nativeEvent; | ||
var submitter = nativeEvent.submitter; | ||
if (event.defaultPrevented) { | ||
return; | ||
} | ||
try { | ||
var _config$onValidate, _config$onValidate2; | ||
var formData = dom.getFormData(form, submitter); | ||
var submission = (_config$onValidate = (_config$onValidate2 = config.onValidate) === null || _config$onValidate2 === void 0 ? void 0 : _config$onValidate2.call(config, { | ||
form, | ||
formData | ||
})) !== null && _config$onValidate !== void 0 ? _config$onValidate : dom.parse(formData); | ||
var { | ||
errors: _errors, | ||
shouldServerValidate | ||
} = Object.entries(submission.error).reduce((result, _ref) => { | ||
var [, error] = _ref; | ||
for (var message of error) { | ||
if (message === dom.VALIDATION_UNDEFINED) { | ||
result.shouldServerValidate = true; | ||
} else if (message !== dom.VALIDATION_SKIPPED) { | ||
result.errors.push(message); | ||
} | ||
}, [noValidate]); | ||
return new Proxy(metadata, { | ||
get(target, key, receiver) { | ||
switch (key) { | ||
case 'onSubmit': | ||
return event => { | ||
var submitEvent = event.nativeEvent; | ||
var result = form.submit(submitEvent); | ||
if (submitEvent.defaultPrevented) { | ||
event.preventDefault(); | ||
} | ||
return result; | ||
}, { | ||
errors: [], | ||
shouldServerValidate: false | ||
}); | ||
if ( | ||
// has client validation | ||
typeof config.onValidate !== 'undefined' && | ||
// not necessary to validate on the server | ||
!shouldServerValidate && ( | ||
// client validation failed or non submit intent | ||
!config.noValidate && !(submitter !== null && submitter !== void 0 && submitter.formNoValidate) && _errors.length > 0 || dom.parseIntent(submission.intent) !== null)) { | ||
report(form, submission); | ||
event.preventDefault(); | ||
} else { | ||
var _config$onSubmit; | ||
(_config$onSubmit = config.onSubmit) === null || _config$onSubmit === void 0 ? void 0 : _config$onSubmit.call(config, event, { | ||
formData, | ||
submission, | ||
action: dom.getFormAction(nativeEvent), | ||
encType: dom.getFormEncType(nativeEvent), | ||
method: dom.getFormMethod(nativeEvent) | ||
}); | ||
} | ||
} catch (error) { | ||
// eslint-disable-next-line no-console | ||
console.warn('Client validation failed', error); | ||
} | ||
}; | ||
case 'onReset': | ||
return event => form.reset(event.nativeEvent); | ||
case 'noValidate': | ||
return noValidate; | ||
} | ||
return Reflect.get(target, key, receiver); | ||
} | ||
}; | ||
if (config.id) { | ||
form.id = config.id; | ||
form.errorId = "".concat(config.id, "-error"); | ||
form.props.id = form.id; | ||
} | ||
if (form.errorId && form.errors.length > 0) { | ||
form.props['aria-invalid'] = 'true'; | ||
form.props['aria-describedby'] = form.errorId; | ||
} | ||
return [form, fieldset]; | ||
}); | ||
} | ||
/** | ||
* A set of field configuration | ||
*/ | ||
/** | ||
* Returns all the information about the fieldset. | ||
* | ||
* @see https://conform.guide/api/react#usefieldset | ||
*/ | ||
function useFieldset(ref, config) { | ||
var [error] = useFormError(ref, { | ||
initialError: config.initialError, | ||
name: config.name | ||
}); | ||
/** | ||
* This allows us constructing the field at runtime as we have no information | ||
* about which fields would be available. The proxy will also help tracking | ||
* the usage of each field for optimization in the future. | ||
*/ | ||
function useFieldset(options) { | ||
var subjectRef = context.useSubjectRef(); | ||
var form = context.useFormStore(options.formId, options.context); | ||
var context$1 = context.useFormContext(form, subjectRef); | ||
return new Proxy({}, { | ||
get(_target, key) { | ||
var _fieldsetConfig$const, _fieldsetConfig$initi, _fieldsetConfig$defau; | ||
if (typeof key !== 'string') { | ||
return; | ||
} | ||
var fieldsetConfig = config; | ||
var constraint = (_fieldsetConfig$const = fieldsetConfig.constraint) === null || _fieldsetConfig$const === void 0 ? void 0 : _fieldsetConfig$const[key]; | ||
var errors = error === null || error === void 0 ? void 0 : error[key]; | ||
var initialError = Object.entries((_fieldsetConfig$initi = fieldsetConfig.initialError) !== null && _fieldsetConfig$initi !== void 0 ? _fieldsetConfig$initi : {}).reduce((result, _ref2) => { | ||
var [name, message] = _ref2; | ||
var [field, ...paths] = dom.getPaths(name); | ||
if (field === key) { | ||
result[dom.getName(paths)] = message; | ||
} | ||
return result; | ||
}, {}); | ||
var field = _rollupPluginBabelHelpers.objectSpread2(_rollupPluginBabelHelpers.objectSpread2({}, constraint), {}, { | ||
name: fieldsetConfig.name ? "".concat(fieldsetConfig.name, ".").concat(key) : key, | ||
// @ts-expect-error The FieldValue type might need a rework | ||
defaultValue: (_fieldsetConfig$defau = fieldsetConfig.defaultValue) === null || _fieldsetConfig$defau === void 0 ? void 0 : _fieldsetConfig$defau[key], | ||
initialError, | ||
error: errors === null || errors === void 0 ? void 0 : errors[0], | ||
errors | ||
get(target, prop, receiver) { | ||
var getMetadata = key => context.getFieldMetadata(options.formId, context$1, { | ||
name: options.name, | ||
key: key, | ||
subjectRef | ||
}); | ||
if (fieldsetConfig.form) { | ||
field.form = fieldsetConfig.form; | ||
field.id = "".concat(fieldsetConfig.form, "-").concat(field.name); | ||
field.errorId = "".concat(field.id, "-error"); | ||
field.descriptionId = "".concat(field.id, "-description"); | ||
} | ||
return field; | ||
} | ||
}); | ||
} | ||
/** | ||
* Returns a list of key and field config. | ||
* | ||
* @see https://conform.guide/api/react#usefieldlist | ||
*/ | ||
function useFieldList(ref, config) { | ||
var configRef = useConfigRef(config); | ||
var [error, setError] = useFormError(ref, { | ||
initialError: config.initialError, | ||
name: config.name | ||
}); | ||
var [entries, setEntries] = react.useState(() => { | ||
var _config$defaultValue; | ||
return Object.entries((_config$defaultValue = config.defaultValue) !== null && _config$defaultValue !== void 0 ? _config$defaultValue : []); | ||
}); | ||
react.useEffect(() => { | ||
var conformHandler = event => { | ||
var form = dom.getFormElement(ref.current); | ||
if (!form || event.target !== form) { | ||
return; | ||
// To support array destructuring | ||
if (prop === Symbol.iterator) { | ||
var _index = 0; | ||
return () => ({ | ||
next: () => ({ | ||
value: getMetadata(_index++), | ||
done: false | ||
}) | ||
}); | ||
} | ||
var intent = dom.parseIntent(event.detail); | ||
if ((intent === null || intent === void 0 ? void 0 : intent.type) !== 'list' || (intent === null || intent === void 0 ? void 0 : intent.payload.name) !== configRef.current.name) { | ||
return; | ||
var index = Number(prop); | ||
if (typeof prop === 'string') { | ||
return getMetadata(Number.isNaN(index) ? prop : index); | ||
} | ||
setEntries(entries => { | ||
var list = [...entries]; | ||
switch (intent.payload.operation) { | ||
case 'append': | ||
case 'prepend': | ||
case 'insert': | ||
case 'replace': | ||
return dom.updateList(list, _rollupPluginBabelHelpers.objectSpread2(_rollupPluginBabelHelpers.objectSpread2({}, intent.payload), {}, { | ||
defaultValue: [ | ||
// Generate a random key to avoid conflicts | ||
getUniqueKey(), intent.payload.defaultValue] | ||
})); | ||
default: | ||
return dom.updateList(list, intent.payload); | ||
} | ||
}); | ||
setError(error => { | ||
var errorList = []; | ||
for (var [key, messages] of Object.entries(error)) { | ||
if (typeof key === 'number') { | ||
errorList[key] = messages; | ||
} | ||
} | ||
switch (intent.payload.operation) { | ||
case 'append': | ||
case 'prepend': | ||
case 'insert': | ||
case 'replace': | ||
errorList = dom.updateList(errorList, _rollupPluginBabelHelpers.objectSpread2(_rollupPluginBabelHelpers.objectSpread2({}, intent.payload), {}, { | ||
defaultValue: undefined | ||
})); | ||
break; | ||
default: | ||
errorList = dom.updateList(errorList, intent.payload); | ||
break; | ||
} | ||
return Object.assign({}, errorList); | ||
}); | ||
}; | ||
var resetHandler = event => { | ||
var _configRef$current$de; | ||
var form = dom.getFormElement(ref.current); | ||
if (!form || event.target !== form) { | ||
return; | ||
} | ||
setEntries(Object.entries((_configRef$current$de = configRef.current.defaultValue) !== null && _configRef$current$de !== void 0 ? _configRef$current$de : [])); | ||
}; | ||
// @ts-expect-error Custom event: conform | ||
document.addEventListener('conform', conformHandler, true); | ||
document.addEventListener('reset', resetHandler); | ||
return () => { | ||
// @ts-expect-error Custom event: conform | ||
document.removeEventListener('conform', conformHandler, true); | ||
document.removeEventListener('reset', resetHandler); | ||
}; | ||
}, [ref, configRef, setError]); | ||
return entries.map((_ref3, index) => { | ||
var _config$initialError; | ||
var [key, defaultValue] = _ref3; | ||
var errors = error[index]; | ||
var initialError = Object.entries((_config$initialError = config.initialError) !== null && _config$initialError !== void 0 ? _config$initialError : {}).reduce((result, _ref4) => { | ||
var [name, message] = _ref4; | ||
var [field, ...paths] = dom.getPaths(name); | ||
if (field === index) { | ||
result[dom.getName(paths)] = message; | ||
} | ||
return result; | ||
}, {}); | ||
var fieldConfig = { | ||
name: "".concat(config.name, "[").concat(index, "]"), | ||
defaultValue, | ||
initialError, | ||
error: errors === null || errors === void 0 ? void 0 : errors[0], | ||
errors | ||
}; | ||
if (config.form) { | ||
fieldConfig.form = config.form; | ||
fieldConfig.id = "".concat(config.form, "-").concat(config.name, "-").concat(index); | ||
fieldConfig.errorId = "".concat(fieldConfig.id, "-error"); | ||
fieldConfig.descriptionId = "".concat(fieldConfig.id, "-description"); | ||
return Reflect.get(target, prop, receiver); | ||
} | ||
return _rollupPluginBabelHelpers.objectSpread2({ | ||
key | ||
}, fieldConfig); | ||
}); | ||
} | ||
/** | ||
* useLayoutEffect is client-only. | ||
* This basically makes it a no-op on server | ||
*/ | ||
var useSafeLayoutEffect = typeof document === 'undefined' ? react.useEffect : react.useLayoutEffect; | ||
/** | ||
* Returns a ref object and a set of helpers that dispatch corresponding dom event. | ||
* | ||
* @see https://conform.guide/api/react#useinputevent | ||
*/ | ||
function useInputEvent(options) { | ||
var optionsRef = useConfigRef(options); | ||
var eventDispatched = react.useRef({ | ||
onInput: false, | ||
onFocus: false, | ||
onBlur: false | ||
}); | ||
useSafeLayoutEffect(() => { | ||
var createEventListener = listener => { | ||
return event => { | ||
var _optionsRef$current, _optionsRef$current2, _optionsRef$current3; | ||
var element = typeof ((_optionsRef$current = optionsRef.current) === null || _optionsRef$current === void 0 ? void 0 : _optionsRef$current.ref) === 'function' ? (_optionsRef$current2 = optionsRef.current) === null || _optionsRef$current2 === void 0 ? void 0 : _optionsRef$current2.ref() : (_optionsRef$current3 = optionsRef.current) === null || _optionsRef$current3 === void 0 ? void 0 : _optionsRef$current3.ref.current; | ||
if (dom.isFieldElement(element) && (listener === 'onReset' ? event.target === element.form : event.target === element)) { | ||
var _optionsRef$current4, _optionsRef$current4$; | ||
if (listener !== 'onReset') { | ||
eventDispatched.current[listener] = true; | ||
} | ||
(_optionsRef$current4 = optionsRef.current) === null || _optionsRef$current4 === void 0 || (_optionsRef$current4$ = _optionsRef$current4[listener]) === null || _optionsRef$current4$ === void 0 ? void 0 : _optionsRef$current4$.call(_optionsRef$current4, event); | ||
} | ||
}; | ||
}; | ||
var inputHandler = createEventListener('onInput'); | ||
var focusHandler = createEventListener('onFocus'); | ||
var blurHandler = createEventListener('onBlur'); | ||
var resetHandler = createEventListener('onReset'); | ||
// focus/blur event does not bubble | ||
document.addEventListener('input', inputHandler, true); | ||
document.addEventListener('focus', focusHandler, true); | ||
document.addEventListener('blur', blurHandler, true); | ||
document.addEventListener('reset', resetHandler); | ||
return () => { | ||
document.removeEventListener('input', inputHandler, true); | ||
document.removeEventListener('focus', focusHandler, true); | ||
document.removeEventListener('blur', blurHandler, true); | ||
document.removeEventListener('reset', resetHandler); | ||
}; | ||
}, []); | ||
var control = react.useMemo(() => { | ||
var dispatch = (listener, fn) => { | ||
if (!eventDispatched.current[listener]) { | ||
var _optionsRef$current5, _optionsRef$current6, _optionsRef$current7; | ||
var _element2 = typeof ((_optionsRef$current5 = optionsRef.current) === null || _optionsRef$current5 === void 0 ? void 0 : _optionsRef$current5.ref) === 'function' ? (_optionsRef$current6 = optionsRef.current) === null || _optionsRef$current6 === void 0 ? void 0 : _optionsRef$current6.ref() : (_optionsRef$current7 = optionsRef.current) === null || _optionsRef$current7 === void 0 ? void 0 : _optionsRef$current7.ref.current; | ||
if (!dom.isFieldElement(_element2)) { | ||
// eslint-disable-next-line no-console | ||
console.warn('Failed to dispatch event; is the input mounted?'); | ||
return; | ||
} | ||
// To avoid recursion | ||
eventDispatched.current[listener] = true; | ||
fn(_element2); | ||
} | ||
eventDispatched.current[listener] = false; | ||
}; | ||
return { | ||
change(eventOrValue) { | ||
dispatch('onInput', element => { | ||
if (element instanceof HTMLInputElement && (element.type === 'checkbox' || element.type === 'radio')) { | ||
if (typeof eventOrValue !== 'boolean') { | ||
throw new Error('You should pass a boolean when changing a checkbox or radio input'); | ||
} | ||
element.checked = eventOrValue; | ||
} else { | ||
if (typeof eventOrValue === 'boolean') { | ||
throw new Error('You can pass a boolean only when changing a checkbox or radio input'); | ||
} | ||
var _value = typeof eventOrValue === 'string' ? eventOrValue : eventOrValue.target.value; | ||
// No change event will triggered on React if `element.value` is updated | ||
// before dispatching the event | ||
if (element.value !== _value) { | ||
/** | ||
* Triggering react custom change event | ||
* Solution based on dom-testing-library | ||
* @see https://github.com/facebook/react/issues/10135#issuecomment-401496776 | ||
* @see https://github.com/testing-library/dom-testing-library/blob/main/src/events.js#L104-L123 | ||
*/ | ||
var { | ||
set: valueSetter | ||
} = Object.getOwnPropertyDescriptor(element, 'value') || {}; | ||
var prototype = Object.getPrototypeOf(element); | ||
var { | ||
set: prototypeValueSetter | ||
} = Object.getOwnPropertyDescriptor(prototype, 'value') || {}; | ||
if (prototypeValueSetter && valueSetter !== prototypeValueSetter) { | ||
prototypeValueSetter.call(element, _value); | ||
} else { | ||
if (valueSetter) { | ||
valueSetter.call(element, _value); | ||
} else { | ||
throw new Error('The given element does not have a value setter'); | ||
} | ||
} | ||
} | ||
} | ||
// Dispatch input event with the updated input value | ||
element.dispatchEvent(new InputEvent('input', { | ||
bubbles: true | ||
})); | ||
// Dispatch change event (necessary for select to update the selected option) | ||
element.dispatchEvent(new Event('change', { | ||
bubbles: true | ||
})); | ||
}); | ||
}, | ||
focus() { | ||
dispatch('onFocus', element => { | ||
element.dispatchEvent(new FocusEvent('focusin', { | ||
bubbles: true | ||
})); | ||
element.dispatchEvent(new FocusEvent('focus')); | ||
}); | ||
}, | ||
blur() { | ||
dispatch('onBlur', element => { | ||
element.dispatchEvent(new FocusEvent('focusout', { | ||
bubbles: true | ||
})); | ||
element.dispatchEvent(new FocusEvent('blur')); | ||
}); | ||
} | ||
}; | ||
}, [optionsRef]); | ||
return control; | ||
} | ||
var FORM_ERROR_ELEMENT_NAME = '__form__'; | ||
/** | ||
* Validate the form with the Constraint Validation API | ||
* @see https://conform.guide/api/react#validateconstraint | ||
*/ | ||
function validateConstraint(options) { | ||
var _options$formData, _options$formatMessag; | ||
var formData = (_options$formData = options === null || options === void 0 ? void 0 : options.formData) !== null && _options$formData !== void 0 ? _options$formData : new FormData(options.form); | ||
var getDefaultErrors = (validity, result) => { | ||
var errors = []; | ||
if (validity.valueMissing) errors.push('required'); | ||
if (validity.typeMismatch || validity.badInput) errors.push('type'); | ||
if (validity.tooShort) errors.push('minLength'); | ||
if (validity.rangeUnderflow) errors.push('min'); | ||
if (validity.stepMismatch) errors.push('step'); | ||
if (validity.tooLong) errors.push('maxLength'); | ||
if (validity.rangeOverflow) errors.push('max'); | ||
if (validity.patternMismatch) errors.push('pattern'); | ||
for (var [constraintName, valid] of Object.entries(result)) { | ||
if (!valid) { | ||
errors.push(constraintName); | ||
} | ||
function useFieldList(options) { | ||
var _context$initialValue; | ||
var subjectRef = context.useSubjectRef({ | ||
initialValue: { | ||
name: [options.name] | ||
} | ||
return errors; | ||
}; | ||
var formatMessages = (_options$formatMessag = options === null || options === void 0 ? void 0 : options.formatMessages) !== null && _options$formatMessag !== void 0 ? _options$formatMessag : _ref5 => { | ||
var { | ||
defaultErrors | ||
} = _ref5; | ||
return defaultErrors; | ||
}; | ||
return dom.parse(formData, { | ||
resolve() { | ||
var error = {}; | ||
var constraintPattern = /^constraint[A-Z][^A-Z]*$/; | ||
var _loop = function _loop(_element3) { | ||
if (dom.isFieldElement(_element3)) { | ||
var name = _element3.name !== FORM_ERROR_ELEMENT_NAME ? _element3.name : ''; | ||
var constraint = Object.entries(_element3.dataset).reduce((result, _ref6) => { | ||
var [name, attributeValue = ''] = _ref6; | ||
if (constraintPattern.test(name)) { | ||
var _options$constraint; | ||
var constraintName = name.slice(10).toLowerCase(); | ||
var _validate = (_options$constraint = options.constraint) === null || _options$constraint === void 0 ? void 0 : _options$constraint[constraintName]; | ||
if (typeof _validate === 'function') { | ||
result[constraintName] = _validate(_element3.value, { | ||
formData, | ||
attributeValue | ||
}); | ||
} else { | ||
// eslint-disable-next-line no-console | ||
console.warn("Found an \"".concat(constraintName, "\" constraint with undefined definition; Please specify it on the validateConstraint API.")); | ||
} | ||
} | ||
return result; | ||
}, {}); | ||
var errors = formatMessages({ | ||
name, | ||
validity: _element3.validity, | ||
constraint, | ||
defaultErrors: getDefaultErrors(_element3.validity, constraint) | ||
}); | ||
if (errors.length > 0) { | ||
error[name] = errors; | ||
} | ||
} | ||
}; | ||
for (var _element3 of options.form.elements) { | ||
_loop(_element3); | ||
} | ||
return { | ||
error | ||
}; | ||
} | ||
}); | ||
} | ||
function getUniqueKey() { | ||
var [value] = crypto.getRandomValues(new Uint32Array(1)); | ||
if (!value) { | ||
throw new Error('Fail to generate an unique key'); | ||
var form = context.useFormStore(options.formId, options.context); | ||
var context$1 = context.useFormContext(form, subjectRef); | ||
var initialValue = (_context$initialValue = context$1.initialValue[options.name]) !== null && _context$initialValue !== void 0 ? _context$initialValue : []; | ||
if (!Array.isArray(initialValue)) { | ||
throw new Error('The initial value at the given name is not a list'); | ||
} | ||
return value.toString(36); | ||
return Array(initialValue.length).fill(0).map((_, index) => context.getFieldMetadata(options.formId, context$1, { | ||
name: options.name, | ||
key: index, | ||
subjectRef | ||
})); | ||
} | ||
function reportSubmission(form, submission) { | ||
for (var [name, message] of Object.entries(submission.error)) { | ||
// There is no need to create a placeholder button if all we want is to reset the error | ||
if (message.length === 0) { | ||
continue; | ||
} | ||
// We can't use empty string as button name | ||
// As `form.element.namedItem('')` will always returns null | ||
var elementName = name ? name : FORM_ERROR_ELEMENT_NAME; | ||
var item = form.elements.namedItem(elementName); | ||
if (item === null) { | ||
// Create placeholder button to keep the error without contributing to the form data | ||
var button = document.createElement('button'); | ||
button.name = elementName; | ||
button.hidden = true; | ||
button.dataset.conformTouched = 'true'; | ||
form.appendChild(button); | ||
} | ||
} | ||
var intent = dom.parseIntent(submission.intent); | ||
var scope = getScope(intent); | ||
for (var _element4 of dom.getFormControls(form)) { | ||
var _submission$error$_el; | ||
var _elementName = _element4.name !== FORM_ERROR_ELEMENT_NAME ? _element4.name : ''; | ||
var messages = (_submission$error$_el = submission.error[_elementName]) !== null && _submission$error$_el !== void 0 ? _submission$error$_el : []; | ||
if (scope === null || scope === _elementName) { | ||
_element4.dataset.conformTouched = 'true'; | ||
} | ||
if (!messages.includes(dom.VALIDATION_SKIPPED) && !messages.includes(dom.VALIDATION_UNDEFINED)) { | ||
var invalidEvent = new Event('invalid', { | ||
cancelable: true | ||
}); | ||
_element4.setCustomValidity(dom.getValidationMessage(messages)); | ||
_element4.dispatchEvent(invalidEvent); | ||
} | ||
} | ||
if (!intent) { | ||
dom.focusFirstInvalidControl(form); | ||
} | ||
function useField(options) { | ||
var subjectRef = context.useSubjectRef(); | ||
var form = context.useFormStore(options.formId, options.context); | ||
var context$1 = context.useFormContext(form, subjectRef); | ||
var metadata = context.getFieldMetadata(options.formId, context$1, { | ||
name: options.name, | ||
subjectRef | ||
}); | ||
return metadata; | ||
} | ||
function getScope(intent) { | ||
switch (intent === null || intent === void 0 ? void 0 : intent.type) { | ||
case 'validate': | ||
return intent.payload; | ||
case 'list': | ||
return intent.payload.name; | ||
} | ||
return null; | ||
} | ||
exports.FORM_ERROR_ELEMENT_NAME = FORM_ERROR_ELEMENT_NAME; | ||
exports.getScope = getScope; | ||
exports.getUniqueKey = getUniqueKey; | ||
exports.reportSubmission = reportSubmission; | ||
exports.useField = useField; | ||
exports.useFieldList = useFieldList; | ||
exports.useFieldset = useFieldset; | ||
exports.useForm = useForm; | ||
exports.useInputEvent = useInputEvent; | ||
exports.validateConstraint = validateConstraint; | ||
exports.useFormId = useFormId; | ||
exports.useFormMetadata = useFormMetadata; | ||
exports.useSafeLayoutEffect = useSafeLayoutEffect; |
@@ -1,3 +0,6 @@ | ||
export { type FieldsetConstraint, type Submission, parse, list, validate, requestIntent, isFieldElement, } from '@conform-to/dom'; | ||
export { type Fieldset, type FieldConfig, type FieldsetConfig, type FormConfig, useForm, useFieldset, useFieldList, useInputEvent, validateConstraint, } from './hooks.js'; | ||
export * as conform from './helpers.js'; | ||
export { type Submission, type FieldName, requestIntent, isFieldElement, } from '@conform-to/dom'; | ||
export { type Field, type FieldMetadata as FieldConfig, FormProvider, FormStateInput, } from './context'; | ||
export { useForm, useFormMetadata, useFieldset, useFieldList, useField, } from './hooks'; | ||
export { useInputEvent } from './integrations'; | ||
export * as conform from './helpers'; | ||
export * as intent from './intent'; |
23
index.js
@@ -6,4 +6,7 @@ 'use strict'; | ||
var dom = require('@conform-to/dom'); | ||
var context = require('./context.js'); | ||
var hooks = require('./hooks.js'); | ||
var integrations = require('./integrations.js'); | ||
var helpers = require('./helpers.js'); | ||
var intent = require('./intent.js'); | ||
@@ -16,10 +19,2 @@ | ||
}); | ||
Object.defineProperty(exports, 'list', { | ||
enumerable: true, | ||
get: function () { return dom.list; } | ||
}); | ||
Object.defineProperty(exports, 'parse', { | ||
enumerable: true, | ||
get: function () { return dom.parse; } | ||
}); | ||
Object.defineProperty(exports, 'requestIntent', { | ||
@@ -29,11 +24,11 @@ enumerable: true, | ||
}); | ||
Object.defineProperty(exports, 'validate', { | ||
enumerable: true, | ||
get: function () { return dom.validate; } | ||
}); | ||
exports.FormProvider = context.FormProvider; | ||
exports.FormStateInput = context.FormStateInput; | ||
exports.useField = hooks.useField; | ||
exports.useFieldList = hooks.useFieldList; | ||
exports.useFieldset = hooks.useFieldset; | ||
exports.useForm = hooks.useForm; | ||
exports.useInputEvent = hooks.useInputEvent; | ||
exports.validateConstraint = hooks.validateConstraint; | ||
exports.useFormMetadata = hooks.useFormMetadata; | ||
exports.useInputEvent = integrations.useInputEvent; | ||
exports.conform = helpers; | ||
exports.intent = intent; |
@@ -6,3 +6,3 @@ { | ||
"license": "MIT", | ||
"version": "0.9.1", | ||
"version": "1.0.0-pre.0", | ||
"main": "index.js", | ||
@@ -34,6 +34,6 @@ "module": "index.mjs", | ||
"dependencies": { | ||
"@conform-to/dom": "0.9.1" | ||
"@conform-to/dom": "1.0.0-pre.0" | ||
}, | ||
"peerDependencies": { | ||
"react": ">=16.8" | ||
"react": ">=18" | ||
}, | ||
@@ -40,0 +40,0 @@ "keywords": [ |
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
23
82878
1607
1
+ Added@conform-to/dom@1.0.0-pre.0(transitive)
- Removed@conform-to/dom@0.9.1(transitive)
Updated@conform-to/dom@1.0.0-pre.0