@conform-to/react
Advanced tools
Comparing version 0.6.1 to 0.6.2
@@ -1,2 +0,3 @@ | ||
import { type FieldConfig, type Primitive, VALIDATION_UNDEFINED, VALIDATION_SKIPPED, INTENT } from '@conform-to/dom'; | ||
import { INTENT } from '@conform-to/dom'; | ||
import { type FieldConfig, type Primitive, VALIDATION_UNDEFINED, VALIDATION_SKIPPED } from './hooks'; | ||
import type { CSSProperties, HTMLInputTypeAttribute } from 'react'; | ||
@@ -3,0 +4,0 @@ interface FormControlProps { |
@@ -7,2 +7,3 @@ 'use strict'; | ||
var dom = require('@conform-to/dom'); | ||
var hooks = require('./hooks.js'); | ||
@@ -93,12 +94,6 @@ /** | ||
}); | ||
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.VALIDATION_SKIPPED = hooks.VALIDATION_SKIPPED; | ||
exports.VALIDATION_UNDEFINED = hooks.VALIDATION_UNDEFINED; | ||
exports.input = input; | ||
exports.select = select; | ||
exports.textarea = textarea; |
@@ -1,5 +0,26 @@ | ||
import { type FieldConfig, type FieldElement, type FieldValue, type FieldsetConstraint, type FormMethod, type FormEncType, type Submission } from '@conform-to/dom'; | ||
import { type FieldConstraint, type FieldElement, type FieldsetConstraint, type Submission, type KeysOf, type ResolveType, getFormEncType, getFormMethod } from '@conform-to/dom'; | ||
import { type FormEvent, type RefObject } from 'react'; | ||
export interface FormConfig<Schema extends Record<string, any>, ClientSubmission extends Submission | Submission<Schema> = Submission> { | ||
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 | string[]>; | ||
form?: string; | ||
descriptionId?: string; | ||
errorId?: string; | ||
/** | ||
* 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>> : Schema extends Record<string, any> ? { | ||
[Key in KeysOf<Schema>]?: FieldValue<ResolveType<Schema, Key>>; | ||
} : any; | ||
export interface FormConfig<Output extends Record<string, any>, Input extends Record<string, any> = Output> { | ||
/** | ||
* If the form id is provided, Id for label, | ||
@@ -34,3 +55,3 @@ * input and error elements will be derived. | ||
*/ | ||
defaultValue?: FieldValue<Schema>; | ||
defaultValue?: FieldValue<Input>; | ||
/** | ||
@@ -43,3 +64,3 @@ * An object describing the result of the last submission | ||
*/ | ||
constraint?: FieldsetConstraint<Schema>; | ||
constraint?: FieldsetConstraint<Input>; | ||
/** | ||
@@ -63,3 +84,3 @@ * Enable native validation before hydation. | ||
formData: FormData; | ||
}) => ClientSubmission; | ||
}) => Submission | Submission<Output>; | ||
/** | ||
@@ -71,6 +92,6 @@ * The submit event handler of the form. It will be called | ||
formData: FormData; | ||
submission: ClientSubmission; | ||
submission: Submission; | ||
action: string; | ||
encType: FormEncType; | ||
method: FormMethod; | ||
encType: ReturnType<typeof getFormEncType>; | ||
method: ReturnType<typeof getFormMethod>; | ||
}) => void; | ||
@@ -103,3 +124,3 @@ } | ||
*/ | ||
export declare function useForm<Schema extends Record<string, any>, ClientSubmission extends Submission | Submission<Schema> = Submission>(config?: FormConfig<Schema, ClientSubmission>): [Form, Fieldset<Schema>]; | ||
export declare function useForm<Output extends Record<string, any>, Input extends Record<string, any> = Output>(config?: FormConfig<Output, Input>): [Form, Fieldset<Input>]; | ||
/** | ||
@@ -109,3 +130,3 @@ * A set of field configuration | ||
export type Fieldset<Schema extends Record<string, any>> = { | ||
[Key in keyof Schema]-?: FieldConfig<Schema[Key]>; | ||
[Key in KeysOf<Schema>]-?: FieldConfig<ResolveType<Schema, Key>>; | ||
}; | ||
@@ -142,4 +163,3 @@ export interface FieldsetConfig<Schema extends Record<string, any>> { | ||
/** | ||
* Returns a list of key and config, with a group of helpers | ||
* configuring buttons for list manipulation | ||
* Returns a list of key and field config. | ||
* | ||
@@ -174,2 +194,33 @@ * @see https://conform.guide/api/react#usefieldlist | ||
}): [RefObject<RefShape>, InputControl]; | ||
export declare const VALIDATION_UNDEFINED = "__undefined__"; | ||
export declare const VALIDATION_SKIPPED = "__skipped__"; | ||
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>; | ||
acceptMultipleErrors?: ({ name, intent, payload, }: { | ||
name: string; | ||
intent: string; | ||
payload: Record<string, any>; | ||
}) => boolean; | ||
formatMessages?: ({ name, validity, constraint, defaultErrors, }: { | ||
name: string; | ||
validity: ValidityState; | ||
constraint: Record<string, boolean>; | ||
defaultErrors: string[]; | ||
}) => string[]; | ||
}): Submission; | ||
export declare function reportSubmission(form: HTMLFormElement, submission: Submission): void; | ||
/** | ||
* Check if the current focus is on a intent button. | ||
*/ | ||
export declare function isFocusedOnIntentButton(form: HTMLFormElement, intent: string): boolean; | ||
export {}; |
559
hooks.js
@@ -10,2 +10,118 @@ 'use strict'; | ||
/** | ||
* Normalize error to an array of string. | ||
*/ | ||
function normalizeError(error) { | ||
if (!error) { | ||
// This treat both empty string and undefined as no error. | ||
return []; | ||
} | ||
return [].concat(error); | ||
} | ||
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); | ||
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) { | ||
return; | ||
} | ||
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 paths = dom.getPaths(name); | ||
if (paths.length === 1) { | ||
result[paths[0]] = normalizeError(message); | ||
} | ||
} | ||
return result; | ||
}); | ||
react.useEffect(() => { | ||
var handleInvalid = event => { | ||
var form = dom.getFormElement(ref.current); | ||
var element = event.target; | ||
if (!dom.isFieldElement(element) || element.form !== form || !element.dataset.conformTouched) { | ||
return; | ||
} | ||
var key = element.name; | ||
if (config.name) { | ||
var scopePaths = dom.getPaths(config.name); | ||
var fieldPaths = dom.getPaths(element.name); | ||
for (var i = 0; i <= scopePaths.length; i++) { | ||
var path = fieldPaths[i]; | ||
if (i < scopePaths.length) { | ||
// Skip if the field is not in the scope | ||
if (path !== scopePaths[i]) { | ||
return; | ||
} | ||
} else { | ||
key = path; | ||
} | ||
} | ||
} | ||
setError(prev => { | ||
if (element.validationMessage === dom.getValidationMessage(prev[key])) { | ||
return prev; | ||
} | ||
return _rollupPluginBabelHelpers.objectSpread2(_rollupPluginBabelHelpers.objectSpread2({}, prev), {}, { | ||
[key]: 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]; | ||
} | ||
/** | ||
* Returns properties required to hook into form events. | ||
@@ -17,12 +133,11 @@ * Applied custom validation and define when error should be reported. | ||
function useForm() { | ||
var _config$lastSubmissio, _config$ref, _ref2, _config$lastSubmissio2; | ||
var _ref, _config$lastSubmissio2; | ||
var config = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {}; | ||
var configRef = react.useRef(config); | ||
var formRef = react.useRef(null); | ||
var [lastSubmission, setLastSubmission] = react.useState((_config$lastSubmissio = config.lastSubmission) !== null && _config$lastSubmissio !== void 0 ? _config$lastSubmissio : null); | ||
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(() => { | ||
if (!config.lastSubmission) { | ||
return []; | ||
} | ||
return [].concat(config.lastSubmission.error['']); | ||
var _config$lastSubmissio; | ||
return normalizeError((_config$lastSubmissio = config.lastSubmission) === null || _config$lastSubmissio === void 0 ? void 0 : _config$lastSubmissio.error['']); | ||
}); | ||
@@ -35,13 +150,8 @@ var initialError = react.useMemo(() => { | ||
var scope = dom.getScope(submission.intent); | ||
return Object.entries(submission.error).reduce((result, _ref) => { | ||
var [name, message] = _ref; | ||
if (name !== '' && (scope === null || scope === name)) { | ||
result[name] = message; | ||
} | ||
return result; | ||
}, {}); | ||
return scope === null ? submission.error : { | ||
[scope]: submission.error[scope] | ||
}; | ||
}, [config.lastSubmission]); | ||
var ref = (_config$ref = config.ref) !== null && _config$ref !== void 0 ? _config$ref : formRef; | ||
var fieldset = useFieldset(ref, { | ||
defaultValue: (_ref2 = (_config$lastSubmissio2 = config.lastSubmission) === null || _config$lastSubmissio2 === void 0 ? void 0 : _config$lastSubmissio2.payload) !== null && _ref2 !== void 0 ? _ref2 : config.defaultValue, | ||
defaultValue: (_ref = (_config$lastSubmissio2 = config.lastSubmission) === null || _config$lastSubmissio2 === void 0 ? void 0 : _config$lastSubmissio2.payload) !== null && _ref !== void 0 ? _ref : config.defaultValue, | ||
initialError, | ||
@@ -51,36 +161,7 @@ constraint: config.constraint, | ||
}); | ||
var [noValidate, setNoValidate] = react.useState(config.noValidate || !config.fallbackNative); | ||
useSafeLayoutEffect(() => { | ||
configRef.current = config; | ||
}); | ||
react.useEffect(() => { | ||
setNoValidate(true); | ||
}, []); | ||
react.useEffect(() => { | ||
var form = ref.current; | ||
var submission = config.lastSubmission; | ||
if (!form || !submission) { | ||
return; | ||
} | ||
var listCommand = dom.parseListCommand(submission.intent); | ||
if (listCommand) { | ||
form.dispatchEvent(new CustomEvent('conform/list', { | ||
detail: submission.intent | ||
})); | ||
} | ||
setLastSubmission(submission); | ||
}, [ref, config.lastSubmission]); | ||
react.useEffect(() => { | ||
var form = ref.current; | ||
if (!form || !lastSubmission) { | ||
return; | ||
} | ||
dom.reportSubmission(form, lastSubmission); | ||
}, [ref, lastSubmission]); | ||
react.useEffect(() => { | ||
// Revalidate the form when input value is changed | ||
var handleInput = event => { | ||
// custom validate handler | ||
var createValidateHandler = name => event => { | ||
var field = event.target; | ||
var form = ref.current; | ||
var formConfig = configRef.current; | ||
var { | ||
@@ -90,30 +171,14 @@ initialReport = 'onSubmit', | ||
shouldRevalidate = 'onInput' | ||
} = formConfig; | ||
if (!form || !dom.isFieldElement(field) || field.form !== form) { | ||
} = configRef.current; | ||
if (!form || !dom.isFocusableFormControl(field) || field.form !== form) { | ||
return; | ||
} | ||
if (field.dataset.conformTouched ? shouldRevalidate === 'onInput' : shouldValidate === 'onInput') { | ||
if (field.dataset.conformTouched ? shouldRevalidate === name : shouldValidate === name) { | ||
dom.requestIntent(form, dom.validate(field.name)); | ||
} | ||
}; | ||
var handleBlur = event => { | ||
var field = event.target; | ||
var form = ref.current; | ||
var formConfig = configRef.current; | ||
var { | ||
initialReport = 'onSubmit', | ||
shouldValidate = initialReport === 'onChange' ? 'onInput' : initialReport, | ||
shouldRevalidate = 'onInput' | ||
} = formConfig; | ||
if (!form || !dom.isFieldElement(field) || field.form !== form) { | ||
return; | ||
} | ||
if (field.dataset.conformTouched ? shouldRevalidate === 'onBlur' : shouldValidate === 'onBlur') { | ||
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 !== dom.FORM_ERROR_ELEMENT_NAME) { | ||
if (!form || !dom.isFieldElement(field) || field.form !== form || field.name !== FORM_ERROR_ELEMENT_NAME) { | ||
return; | ||
@@ -133,10 +198,10 @@ } | ||
// Reset all field state | ||
for (var field of form.elements) { | ||
if (dom.isFieldElement(field)) { | ||
delete field.dataset.conformTouched; | ||
field.setCustomValidity(''); | ||
} | ||
for (var element of dom.getFormControls(form)) { | ||
delete element.dataset.conformTouched; | ||
element.setCustomValidity(''); | ||
} | ||
setErrors([]); | ||
}; | ||
var handleInput = createValidateHandler('onInput'); | ||
var handleBlur = createValidateHandler('onBlur'); | ||
document.addEventListener('input', handleInput, true); | ||
@@ -152,3 +217,3 @@ document.addEventListener('blur', handleBlur, true); | ||
}; | ||
}, [ref]); | ||
}, [ref, configRef]); | ||
var form = { | ||
@@ -169,30 +234,28 @@ ref, | ||
try { | ||
var _config$onValidate; | ||
var _config$onValidate, _config$onValidate2; | ||
var formData = dom.getFormData(form, submitter); | ||
var getSubmission = (_config$onValidate = config.onValidate) !== null && _config$onValidate !== void 0 ? _config$onValidate : context => dom.parse(context.formData); | ||
var submission = getSubmission({ | ||
var submission = (_config$onValidate = (_config$onValidate2 = config.onValidate) === null || _config$onValidate2 === void 0 ? void 0 : _config$onValidate2.call(config, { | ||
form, | ||
formData | ||
}); | ||
if (!config.noValidate && !(submitter !== null && submitter !== void 0 && submitter.formNoValidate) && Object.entries(submission.error).some(_ref3 => { | ||
var [, message] = _ref3; | ||
return message !== '' && ![].concat(message).includes(dom.VALIDATION_UNDEFINED); | ||
}) || typeof config.onValidate !== 'undefined' && (submission.intent.startsWith('validate') || submission.intent.startsWith('list')) && Object.entries(submission.error).every(_ref4 => { | ||
var [, message] = _ref4; | ||
return ![].concat(message).includes(dom.VALIDATION_UNDEFINED); | ||
})) { | ||
var listCommand = dom.parseListCommand(submission.intent); | ||
if (listCommand) { | ||
form.dispatchEvent(new CustomEvent('conform/list', { | ||
detail: submission.intent | ||
})); | ||
} | ||
setLastSubmission(submission); | ||
})) !== null && _config$onValidate !== void 0 ? _config$onValidate : dom.parse(formData); | ||
var messages = Object.entries(submission.error).reduce((messages, _ref2) => { | ||
var [, message] = _ref2; | ||
return messages.concat(normalizeError(message)); | ||
}, []); | ||
var shouldValidate = !config.noValidate && !(submitter !== null && submitter !== void 0 && submitter.formNoValidate); | ||
var shouldFallbackToServer = messages.includes(VALIDATION_UNDEFINED); | ||
var hasClientValidation = typeof config.onValidate !== 'undefined'; | ||
var isValid = messages.length === 0; | ||
if (hasClientValidation && (dom.isSubmitting(submission.intent) ? shouldValidate && !isValid : !shouldFallbackToServer)) { | ||
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, _rollupPluginBabelHelpers.objectSpread2({ | ||
(_config$onSubmit = config.onSubmit) === null || _config$onSubmit === void 0 ? void 0 : _config$onSubmit.call(config, event, { | ||
formData, | ||
submission | ||
}, dom.getFormAttributes(form, submitter))); | ||
submission, | ||
action: dom.getFormAction(nativeEvent), | ||
encType: dom.getFormEncType(nativeEvent), | ||
method: dom.getFormMethod(nativeEvent) | ||
}); | ||
} | ||
@@ -222,64 +285,7 @@ } catch (e) { | ||
function useFieldset(ref, config) { | ||
var configRef = react.useRef(config); | ||
var [error, setError] = react.useState(() => { | ||
var initialError = config === null || config === void 0 ? void 0 : config.initialError; | ||
if (!initialError) { | ||
return {}; | ||
} | ||
var result = {}; | ||
for (var [name, message] of Object.entries(initialError)) { | ||
var [key, ...paths] = dom.getPaths(name); | ||
if (typeof key === 'string' && paths.length === 0) { | ||
result[key] = [].concat(message !== null && message !== void 0 ? message : []); | ||
} | ||
} | ||
return result; | ||
var [error] = useFormError(ref, { | ||
initialError: config.initialError, | ||
name: config.name | ||
}); | ||
useSafeLayoutEffect(() => { | ||
configRef.current = config; | ||
}); | ||
react.useEffect(() => { | ||
var invalidHandler = event => { | ||
var _configRef$current$na; | ||
var form = dom.getFormElement(ref.current); | ||
var field = event.target; | ||
var fieldsetName = (_configRef$current$na = configRef.current.name) !== null && _configRef$current$na !== void 0 ? _configRef$current$na : ''; | ||
if (!form || !dom.isFieldElement(field) || field.form !== form || !field.name.startsWith(fieldsetName)) { | ||
return; | ||
} | ||
var [key, ...paths] = dom.getPaths(fieldsetName.length > 0 ? field.name.slice(fieldsetName.length + 1) : field.name); | ||
// Update the error only if the field belongs to the fieldset | ||
if (typeof key === 'string' && paths.length === 0) { | ||
if (field.dataset.conformTouched) { | ||
setError(prev => { | ||
var prevMessage = dom.getValidationMessage(prev === null || prev === void 0 ? void 0 : prev[key]); | ||
if (prevMessage === field.validationMessage) { | ||
return prev; | ||
} | ||
return _rollupPluginBabelHelpers.objectSpread2(_rollupPluginBabelHelpers.objectSpread2({}, prev), {}, { | ||
[key]: dom.getErrors(field.validationMessage) | ||
}); | ||
}); | ||
} | ||
event.preventDefault(); | ||
} | ||
}; | ||
var resetHandler = event => { | ||
var form = dom.getFormElement(ref.current); | ||
if (!form || event.target !== form) { | ||
return; | ||
} | ||
setError({}); | ||
}; | ||
// The invalid event does not bubble and so listening on the capturing pharse is needed | ||
document.addEventListener('invalid', invalidHandler, true); | ||
document.addEventListener('reset', resetHandler); | ||
return () => { | ||
document.removeEventListener('invalid', invalidHandler, true); | ||
document.removeEventListener('reset', resetHandler); | ||
}; | ||
}, [ref]); | ||
/** | ||
@@ -299,4 +305,4 @@ * This allows us constructing the field at runtime as we have no information | ||
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, _ref5) => { | ||
var [name, message] = _ref5; | ||
var initialError = Object.entries((_fieldsetConfig$initi = fieldsetConfig.initialError) !== null && _fieldsetConfig$initi !== void 0 ? _fieldsetConfig$initi : {}).reduce((result, _ref3) => { | ||
var [name, message] = _ref3; | ||
var [field, ...paths] = dom.getPaths(name); | ||
@@ -327,4 +333,3 @@ if (field === key) { | ||
/** | ||
* Returns a list of key and config, with a group of helpers | ||
* configuring buttons for list manipulation | ||
* Returns a list of key and field config. | ||
* | ||
@@ -334,13 +339,6 @@ * @see https://conform.guide/api/react#usefieldlist | ||
function useFieldList(ref, config) { | ||
var configRef = react.useRef(config); | ||
var [error, setError] = react.useState(() => { | ||
var initialError = []; | ||
for (var [name, message] of Object.entries((_config$initialError = config === null || config === void 0 ? void 0 : config.initialError) !== null && _config$initialError !== void 0 ? _config$initialError : {})) { | ||
var _config$initialError; | ||
var [index, ...paths] = dom.getPaths(name); | ||
if (typeof index === 'number' && paths.length === 0) { | ||
initialError[index] = [].concat(message !== null && message !== void 0 ? message : []); | ||
} | ||
} | ||
return initialError; | ||
var configRef = useConfigRef(config); | ||
var [error, setError] = useFormError(ref, { | ||
initialError: config.initialError, | ||
name: config.name | ||
}); | ||
@@ -351,32 +349,5 @@ var [entries, setEntries] = react.useState(() => { | ||
}); | ||
useSafeLayoutEffect(() => { | ||
configRef.current = config; | ||
}); | ||
react.useEffect(() => { | ||
var invalidHandler = event => { | ||
var _configRef$current$na2; | ||
var conformHandler = event => { | ||
var form = dom.getFormElement(ref.current); | ||
var field = event.target; | ||
var prefix = (_configRef$current$na2 = configRef.current.name) !== null && _configRef$current$na2 !== void 0 ? _configRef$current$na2 : ''; | ||
if (!form || !dom.isFieldElement(field) || field.form !== form || !field.name.startsWith(prefix)) { | ||
return; | ||
} | ||
var [index, ...paths] = dom.getPaths(prefix.length > 0 ? field.name.slice(prefix.length) : field.name); | ||
// Update the error only if the field belongs to the fieldset | ||
if (typeof index === 'number' && paths.length === 0) { | ||
if (field.dataset.conformTouched) { | ||
setError(prev => { | ||
var prevMessage = dom.getValidationMessage(prev === null || prev === void 0 ? void 0 : prev[index]); | ||
if (prevMessage === field.validationMessage) { | ||
return prev; | ||
} | ||
return [...prev.slice(0, index), dom.getErrors(field.validationMessage), ...prev.slice(index + 1)]; | ||
}); | ||
} | ||
event.preventDefault(); | ||
} | ||
}; | ||
var listHandler = event => { | ||
var form = dom.getFormElement(ref.current); | ||
if (!form || event.target !== form) { | ||
@@ -409,2 +380,8 @@ return; | ||
setError(error => { | ||
var errorList = []; | ||
for (var [key, messages] of Object.entries(error)) { | ||
if (typeof key === 'number') { | ||
errorList[key] = messages; | ||
} | ||
} | ||
switch (command.type) { | ||
@@ -414,3 +391,3 @@ case 'append': | ||
case 'replace': | ||
return dom.updateList([...error], _rollupPluginBabelHelpers.objectSpread2(_rollupPluginBabelHelpers.objectSpread2({}, command), {}, { | ||
errorList = dom.updateList(errorList, _rollupPluginBabelHelpers.objectSpread2(_rollupPluginBabelHelpers.objectSpread2({}, command), {}, { | ||
payload: _rollupPluginBabelHelpers.objectSpread2(_rollupPluginBabelHelpers.objectSpread2({}, command.payload), {}, { | ||
@@ -420,7 +397,10 @@ defaultValue: undefined | ||
})); | ||
break; | ||
default: | ||
{ | ||
return dom.updateList([...error], command); | ||
errorList = dom.updateList(errorList, command); | ||
break; | ||
} | ||
} | ||
return Object.assign({}, errorList); | ||
}); | ||
@@ -435,22 +415,19 @@ }; | ||
setEntries(Object.entries((_configRef$current$de = configRef.current.defaultValue) !== null && _configRef$current$de !== void 0 ? _configRef$current$de : [undefined])); | ||
setError([]); | ||
}; | ||
// @ts-expect-error Custom event: conform/list | ||
document.addEventListener('conform/list', listHandler, true); | ||
document.addEventListener('invalid', invalidHandler, true); | ||
// @ts-expect-error Custom event: conform | ||
document.addEventListener('conform', conformHandler, true); | ||
document.addEventListener('reset', resetHandler); | ||
return () => { | ||
// @ts-expect-error Custom event: conform/list | ||
document.removeEventListener('conform/list', listHandler, true); | ||
document.removeEventListener('invalid', invalidHandler, true); | ||
// @ts-expect-error Custom event: conform | ||
document.removeEventListener('conform', conformHandler, true); | ||
document.removeEventListener('reset', resetHandler); | ||
}; | ||
}, [ref]); | ||
return entries.map((_ref6, index) => { | ||
var _config$initialError2, _config$defaultValue2; | ||
var [key, defaultValue] = _ref6; | ||
}, [ref, configRef, setError]); | ||
return entries.map((_ref4, index) => { | ||
var _config$initialError, _config$defaultValue2; | ||
var [key, defaultValue] = _ref4; | ||
var errors = error[index]; | ||
var initialError = Object.entries((_config$initialError2 = config.initialError) !== null && _config$initialError2 !== void 0 ? _config$initialError2 : {}).reduce((result, _ref7) => { | ||
var [name, message] = _ref7; | ||
var initialError = Object.entries((_config$initialError = config.initialError) !== null && _config$initialError !== void 0 ? _config$initialError : {}).reduce((result, _ref5) => { | ||
var [name, message] = _ref5; | ||
var [field, ...paths] = dom.getPaths(name); | ||
@@ -517,3 +494,3 @@ if (field === index) { | ||
var ref = react.useRef(null); | ||
var optionsRef = react.useRef(options); | ||
var optionsRef = useConfigRef(options); | ||
var changeDispatched = react.useRef(false); | ||
@@ -523,5 +500,2 @@ var focusDispatched = react.useRef(false); | ||
useSafeLayoutEffect(() => { | ||
optionsRef.current = options; | ||
}); | ||
useSafeLayoutEffect(() => { | ||
var getInputElement = () => { | ||
@@ -654,6 +628,154 @@ var _optionsRef$current$g, _optionsRef$current, _optionsRef$current$g2; | ||
}; | ||
}, []); | ||
}, [optionsRef]); | ||
return [ref, control]; | ||
} | ||
var VALIDATION_UNDEFINED = '__undefined__'; | ||
var VALIDATION_SKIPPED = '__skipped__'; | ||
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); | ||
} | ||
} | ||
return errors; | ||
}; | ||
var formatMessages = (_options$formatMessag = options === null || options === void 0 ? void 0 : options.formatMessages) !== null && _options$formatMessag !== void 0 ? _options$formatMessag : _ref6 => { | ||
var { | ||
defaultErrors | ||
} = _ref6; | ||
return defaultErrors; | ||
}; | ||
return dom.parse(formData, { | ||
resolve(payload, intent) { | ||
var error = {}; | ||
var constraintPattern = /^constraint[A-Z][^A-Z]*$/; | ||
var _loop = function _loop(element) { | ||
if (dom.isFieldElement(element)) { | ||
var _options$acceptMultip, _options$acceptMultip2; | ||
var name = element.name !== FORM_ERROR_ELEMENT_NAME ? element.name : ''; | ||
var constraint = Object.entries(element.dataset).reduce((result, _ref7) => { | ||
var [name, attributeValue = ''] = _ref7; | ||
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(element.value, { | ||
formData, | ||
attributeValue | ||
}); | ||
} else { | ||
console.warn("Found an \"".concat(constraintName, "\" constraint with undefined definition; Please specify it on the validateConstraint API.")); | ||
} | ||
} | ||
return result; | ||
}, {}); | ||
var errors = formatMessages({ | ||
name, | ||
validity: element.validity, | ||
constraint, | ||
defaultErrors: getDefaultErrors(element.validity, constraint) | ||
}); | ||
var shouldAcceptMultipleErrors = (_options$acceptMultip = options === null || options === void 0 ? void 0 : (_options$acceptMultip2 = options.acceptMultipleErrors) === null || _options$acceptMultip2 === void 0 ? void 0 : _options$acceptMultip2.call(options, { | ||
name, | ||
payload, | ||
intent | ||
})) !== null && _options$acceptMultip !== void 0 ? _options$acceptMultip : false; | ||
if (errors.length > 0) { | ||
error[name] = shouldAcceptMultipleErrors ? errors : errors[0]; | ||
} | ||
} | ||
}; | ||
for (var element of options.form.elements) { | ||
_loop(element); | ||
} | ||
return { | ||
error | ||
}; | ||
} | ||
}); | ||
} | ||
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 === '') { | ||
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 instanceof RadioNodeList) { | ||
for (var field of item) { | ||
if (field.type !== 'radio') { | ||
console.warn('Repeated field name is not supported.'); | ||
continue; | ||
} | ||
} | ||
} | ||
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 scope = dom.getScope(submission.intent); | ||
for (var element of dom.getFormControls(form)) { | ||
var _elementName = element.name !== FORM_ERROR_ELEMENT_NAME ? element.name : ''; | ||
var messages = normalizeError(submission.error[_elementName]); | ||
if (scope === null || scope === _elementName) { | ||
element.dataset.conformTouched = 'true'; | ||
} | ||
if (!messages.includes(VALIDATION_SKIPPED) && !messages.includes(VALIDATION_UNDEFINED)) { | ||
var invalidEvent = new Event('invalid', { | ||
cancelable: true | ||
}); | ||
element.setCustomValidity(dom.getValidationMessage(messages)); | ||
element.dispatchEvent(invalidEvent); | ||
} | ||
} | ||
if (dom.isSubmitting(submission.intent) || isFocusedOnIntentButton(form, submission.intent)) { | ||
if (scope) { | ||
dom.focusFormControl(form, scope); | ||
} else { | ||
dom.focusFirstInvalidControl(form); | ||
} | ||
} | ||
} | ||
/** | ||
* Check if the current focus is on a intent button. | ||
*/ | ||
function isFocusedOnIntentButton(form, intent) { | ||
var element = document.activeElement; | ||
return dom.isFieldElement(element) && element.type === 'submit' && element.form === form && element.name === dom.INTENT && element.value === intent; | ||
} | ||
exports.FORM_ERROR_ELEMENT_NAME = FORM_ERROR_ELEMENT_NAME; | ||
exports.VALIDATION_SKIPPED = VALIDATION_SKIPPED; | ||
exports.VALIDATION_UNDEFINED = VALIDATION_UNDEFINED; | ||
exports.isFocusedOnIntentButton = isFocusedOnIntentButton; | ||
exports.reportSubmission = reportSubmission; | ||
exports.useFieldList = useFieldList; | ||
@@ -663,1 +785,2 @@ exports.useFieldset = useFieldset; | ||
exports.useInputEvent = useInputEvent; | ||
exports.validateConstraint = validateConstraint; |
@@ -1,3 +0,3 @@ | ||
export { type FieldConfig, type FieldsetConstraint, type Submission, parse, validateConstraint, list, validate, requestIntent, isFieldElement, } from '@conform-to/dom'; | ||
export { type Fieldset, type FieldsetConfig, type FormConfig, useForm, useFieldset, useFieldList, useInputEvent, } from './hooks'; | ||
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'; | ||
export * as conform from './helpers'; |
@@ -31,6 +31,2 @@ 'use strict'; | ||
}); | ||
Object.defineProperty(exports, 'validateConstraint', { | ||
enumerable: true, | ||
get: function () { return dom.validateConstraint; } | ||
}); | ||
exports.useFieldList = hooks.useFieldList; | ||
@@ -40,2 +36,3 @@ exports.useFieldset = hooks.useFieldset; | ||
exports.useInputEvent = hooks.useInputEvent; | ||
exports.validateConstraint = hooks.validateConstraint; | ||
exports.conform = helpers; |
import { objectSpread2 as _objectSpread2 } from './_virtual/_rollupPluginBabelHelpers.js'; | ||
export { INTENT, VALIDATION_SKIPPED, VALIDATION_UNDEFINED } from '@conform-to/dom'; | ||
export { INTENT } from '@conform-to/dom'; | ||
export { VALIDATION_SKIPPED, VALIDATION_UNDEFINED } from './hooks.js'; | ||
@@ -4,0 +5,0 @@ /** |
import { objectSpread2 as _objectSpread2 } from './_virtual/_rollupPluginBabelHelpers.js'; | ||
import { getScope, parseListCommand, reportSubmission, getFormData, parse, VALIDATION_UNDEFINED, getFormAttributes, getPaths, getName, isFieldElement, requestIntent, validate, FORM_ERROR_ELEMENT_NAME, getErrors, getFormElement, getValidationMessage, updateList } from '@conform-to/dom'; | ||
import { useRef, useState, useMemo, useEffect, useLayoutEffect } from 'react'; | ||
import { getScope, getFormData, parse, isSubmitting, getFormAction, getFormEncType, getFormMethod, getPaths, getName, isFieldElement, getErrors, getFormControls, getFormElement, parseListCommand, updateList, getValidationMessage, focusFormControl, focusFirstInvalidControl, INTENT, isFocusableFormControl, requestIntent, validate } from '@conform-to/dom'; | ||
import { useState, useMemo, useEffect, useRef, useCallback, useLayoutEffect } from 'react'; | ||
/** | ||
* Normalize error to an array of string. | ||
*/ | ||
function normalizeError(error) { | ||
if (!error) { | ||
// This treat both empty string and undefined as no error. | ||
return []; | ||
} | ||
return [].concat(error); | ||
} | ||
function useNoValidate(defaultNoValidate, validateBeforeHydrate) { | ||
var [noValidate, setNoValidate] = useState(defaultNoValidate || !validateBeforeHydrate); | ||
useEffect(() => { | ||
setNoValidate(true); | ||
}, []); | ||
return noValidate; | ||
} | ||
function useFormRef(userProvidedRef) { | ||
var formRef = useRef(null); | ||
return userProvidedRef !== null && userProvidedRef !== void 0 ? userProvidedRef : formRef; | ||
} | ||
function useConfigRef(config) { | ||
var ref = useRef(config); | ||
useSafeLayoutEffect(() => { | ||
ref.current = config; | ||
}); | ||
return ref; | ||
} | ||
function useFormReporter(ref, lastSubmission) { | ||
var [submission, setSubmission] = useState(lastSubmission); | ||
var report = useCallback((form, submission) => { | ||
var event = new CustomEvent('conform', { | ||
detail: submission.intent | ||
}); | ||
form.dispatchEvent(event); | ||
setSubmission(submission); | ||
}, []); | ||
useEffect(() => { | ||
var form = ref.current; | ||
if (!form || !lastSubmission) { | ||
return; | ||
} | ||
report(form, lastSubmission); | ||
}, [ref, lastSubmission, report]); | ||
useEffect(() => { | ||
var form = ref.current; | ||
if (!form || !submission) { | ||
return; | ||
} | ||
reportSubmission(form, submission); | ||
}, [ref, submission]); | ||
return report; | ||
} | ||
function useFormError(ref, config) { | ||
var [error, setError] = useState(() => { | ||
if (!config.initialError) { | ||
return {}; | ||
} | ||
var result = {}; | ||
for (var [name, message] of Object.entries(config.initialError)) { | ||
var paths = getPaths(name); | ||
if (paths.length === 1) { | ||
result[paths[0]] = normalizeError(message); | ||
} | ||
} | ||
return result; | ||
}); | ||
useEffect(() => { | ||
var handleInvalid = event => { | ||
var form = getFormElement(ref.current); | ||
var element = event.target; | ||
if (!isFieldElement(element) || element.form !== form || !element.dataset.conformTouched) { | ||
return; | ||
} | ||
var key = element.name; | ||
if (config.name) { | ||
var scopePaths = getPaths(config.name); | ||
var fieldPaths = getPaths(element.name); | ||
for (var i = 0; i <= scopePaths.length; i++) { | ||
var path = fieldPaths[i]; | ||
if (i < scopePaths.length) { | ||
// Skip if the field is not in the scope | ||
if (path !== scopePaths[i]) { | ||
return; | ||
} | ||
} else { | ||
key = path; | ||
} | ||
} | ||
} | ||
setError(prev => { | ||
if (element.validationMessage === getValidationMessage(prev[key])) { | ||
return prev; | ||
} | ||
return _objectSpread2(_objectSpread2({}, prev), {}, { | ||
[key]: getErrors(element.validationMessage) | ||
}); | ||
}); | ||
event.preventDefault(); | ||
}; | ||
var handleReset = event => { | ||
var form = 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]; | ||
} | ||
/** | ||
* Returns properties required to hook into form events. | ||
@@ -12,12 +128,11 @@ * Applied custom validation and define when error should be reported. | ||
function useForm() { | ||
var _config$lastSubmissio, _config$ref, _ref2, _config$lastSubmissio2; | ||
var _ref, _config$lastSubmissio2; | ||
var config = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {}; | ||
var configRef = useRef(config); | ||
var formRef = useRef(null); | ||
var [lastSubmission, setLastSubmission] = useState((_config$lastSubmissio = config.lastSubmission) !== null && _config$lastSubmissio !== void 0 ? _config$lastSubmissio : null); | ||
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] = useState(() => { | ||
if (!config.lastSubmission) { | ||
return []; | ||
} | ||
return [].concat(config.lastSubmission.error['']); | ||
var _config$lastSubmissio; | ||
return normalizeError((_config$lastSubmissio = config.lastSubmission) === null || _config$lastSubmissio === void 0 ? void 0 : _config$lastSubmissio.error['']); | ||
}); | ||
@@ -30,13 +145,8 @@ var initialError = useMemo(() => { | ||
var scope = getScope(submission.intent); | ||
return Object.entries(submission.error).reduce((result, _ref) => { | ||
var [name, message] = _ref; | ||
if (name !== '' && (scope === null || scope === name)) { | ||
result[name] = message; | ||
} | ||
return result; | ||
}, {}); | ||
return scope === null ? submission.error : { | ||
[scope]: submission.error[scope] | ||
}; | ||
}, [config.lastSubmission]); | ||
var ref = (_config$ref = config.ref) !== null && _config$ref !== void 0 ? _config$ref : formRef; | ||
var fieldset = useFieldset(ref, { | ||
defaultValue: (_ref2 = (_config$lastSubmissio2 = config.lastSubmission) === null || _config$lastSubmissio2 === void 0 ? void 0 : _config$lastSubmissio2.payload) !== null && _ref2 !== void 0 ? _ref2 : config.defaultValue, | ||
defaultValue: (_ref = (_config$lastSubmissio2 = config.lastSubmission) === null || _config$lastSubmissio2 === void 0 ? void 0 : _config$lastSubmissio2.payload) !== null && _ref !== void 0 ? _ref : config.defaultValue, | ||
initialError, | ||
@@ -46,36 +156,7 @@ constraint: config.constraint, | ||
}); | ||
var [noValidate, setNoValidate] = useState(config.noValidate || !config.fallbackNative); | ||
useSafeLayoutEffect(() => { | ||
configRef.current = config; | ||
}); | ||
useEffect(() => { | ||
setNoValidate(true); | ||
}, []); | ||
useEffect(() => { | ||
var form = ref.current; | ||
var submission = config.lastSubmission; | ||
if (!form || !submission) { | ||
return; | ||
} | ||
var listCommand = parseListCommand(submission.intent); | ||
if (listCommand) { | ||
form.dispatchEvent(new CustomEvent('conform/list', { | ||
detail: submission.intent | ||
})); | ||
} | ||
setLastSubmission(submission); | ||
}, [ref, config.lastSubmission]); | ||
useEffect(() => { | ||
var form = ref.current; | ||
if (!form || !lastSubmission) { | ||
return; | ||
} | ||
reportSubmission(form, lastSubmission); | ||
}, [ref, lastSubmission]); | ||
useEffect(() => { | ||
// Revalidate the form when input value is changed | ||
var handleInput = event => { | ||
// custom validate handler | ||
var createValidateHandler = name => event => { | ||
var field = event.target; | ||
var form = ref.current; | ||
var formConfig = configRef.current; | ||
var { | ||
@@ -85,26 +166,10 @@ initialReport = 'onSubmit', | ||
shouldRevalidate = 'onInput' | ||
} = formConfig; | ||
if (!form || !isFieldElement(field) || field.form !== form) { | ||
} = configRef.current; | ||
if (!form || !isFocusableFormControl(field) || field.form !== form) { | ||
return; | ||
} | ||
if (field.dataset.conformTouched ? shouldRevalidate === 'onInput' : shouldValidate === 'onInput') { | ||
if (field.dataset.conformTouched ? shouldRevalidate === name : shouldValidate === name) { | ||
requestIntent(form, validate(field.name)); | ||
} | ||
}; | ||
var handleBlur = event => { | ||
var field = event.target; | ||
var form = ref.current; | ||
var formConfig = configRef.current; | ||
var { | ||
initialReport = 'onSubmit', | ||
shouldValidate = initialReport === 'onChange' ? 'onInput' : initialReport, | ||
shouldRevalidate = 'onInput' | ||
} = formConfig; | ||
if (!form || !isFieldElement(field) || field.form !== form) { | ||
return; | ||
} | ||
if (field.dataset.conformTouched ? shouldRevalidate === 'onBlur' : shouldValidate === 'onBlur') { | ||
requestIntent(form, validate(field.name)); | ||
} | ||
}; | ||
var handleInvalid = event => { | ||
@@ -128,10 +193,10 @@ var form = ref.current; | ||
// Reset all field state | ||
for (var field of form.elements) { | ||
if (isFieldElement(field)) { | ||
delete field.dataset.conformTouched; | ||
field.setCustomValidity(''); | ||
} | ||
for (var element of getFormControls(form)) { | ||
delete element.dataset.conformTouched; | ||
element.setCustomValidity(''); | ||
} | ||
setErrors([]); | ||
}; | ||
var handleInput = createValidateHandler('onInput'); | ||
var handleBlur = createValidateHandler('onBlur'); | ||
document.addEventListener('input', handleInput, true); | ||
@@ -147,3 +212,3 @@ document.addEventListener('blur', handleBlur, true); | ||
}; | ||
}, [ref]); | ||
}, [ref, configRef]); | ||
var form = { | ||
@@ -164,30 +229,28 @@ ref, | ||
try { | ||
var _config$onValidate; | ||
var _config$onValidate, _config$onValidate2; | ||
var formData = getFormData(form, submitter); | ||
var getSubmission = (_config$onValidate = config.onValidate) !== null && _config$onValidate !== void 0 ? _config$onValidate : context => parse(context.formData); | ||
var submission = getSubmission({ | ||
var submission = (_config$onValidate = (_config$onValidate2 = config.onValidate) === null || _config$onValidate2 === void 0 ? void 0 : _config$onValidate2.call(config, { | ||
form, | ||
formData | ||
}); | ||
if (!config.noValidate && !(submitter !== null && submitter !== void 0 && submitter.formNoValidate) && Object.entries(submission.error).some(_ref3 => { | ||
var [, message] = _ref3; | ||
return message !== '' && ![].concat(message).includes(VALIDATION_UNDEFINED); | ||
}) || typeof config.onValidate !== 'undefined' && (submission.intent.startsWith('validate') || submission.intent.startsWith('list')) && Object.entries(submission.error).every(_ref4 => { | ||
var [, message] = _ref4; | ||
return ![].concat(message).includes(VALIDATION_UNDEFINED); | ||
})) { | ||
var listCommand = parseListCommand(submission.intent); | ||
if (listCommand) { | ||
form.dispatchEvent(new CustomEvent('conform/list', { | ||
detail: submission.intent | ||
})); | ||
} | ||
setLastSubmission(submission); | ||
})) !== null && _config$onValidate !== void 0 ? _config$onValidate : parse(formData); | ||
var messages = Object.entries(submission.error).reduce((messages, _ref2) => { | ||
var [, message] = _ref2; | ||
return messages.concat(normalizeError(message)); | ||
}, []); | ||
var shouldValidate = !config.noValidate && !(submitter !== null && submitter !== void 0 && submitter.formNoValidate); | ||
var shouldFallbackToServer = messages.includes(VALIDATION_UNDEFINED); | ||
var hasClientValidation = typeof config.onValidate !== 'undefined'; | ||
var isValid = messages.length === 0; | ||
if (hasClientValidation && (isSubmitting(submission.intent) ? shouldValidate && !isValid : !shouldFallbackToServer)) { | ||
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, _objectSpread2({ | ||
(_config$onSubmit = config.onSubmit) === null || _config$onSubmit === void 0 ? void 0 : _config$onSubmit.call(config, event, { | ||
formData, | ||
submission | ||
}, getFormAttributes(form, submitter))); | ||
submission, | ||
action: getFormAction(nativeEvent), | ||
encType: getFormEncType(nativeEvent), | ||
method: getFormMethod(nativeEvent) | ||
}); | ||
} | ||
@@ -217,64 +280,7 @@ } catch (e) { | ||
function useFieldset(ref, config) { | ||
var configRef = useRef(config); | ||
var [error, setError] = useState(() => { | ||
var initialError = config === null || config === void 0 ? void 0 : config.initialError; | ||
if (!initialError) { | ||
return {}; | ||
} | ||
var result = {}; | ||
for (var [name, message] of Object.entries(initialError)) { | ||
var [key, ...paths] = getPaths(name); | ||
if (typeof key === 'string' && paths.length === 0) { | ||
result[key] = [].concat(message !== null && message !== void 0 ? message : []); | ||
} | ||
} | ||
return result; | ||
var [error] = useFormError(ref, { | ||
initialError: config.initialError, | ||
name: config.name | ||
}); | ||
useSafeLayoutEffect(() => { | ||
configRef.current = config; | ||
}); | ||
useEffect(() => { | ||
var invalidHandler = event => { | ||
var _configRef$current$na; | ||
var form = getFormElement(ref.current); | ||
var field = event.target; | ||
var fieldsetName = (_configRef$current$na = configRef.current.name) !== null && _configRef$current$na !== void 0 ? _configRef$current$na : ''; | ||
if (!form || !isFieldElement(field) || field.form !== form || !field.name.startsWith(fieldsetName)) { | ||
return; | ||
} | ||
var [key, ...paths] = getPaths(fieldsetName.length > 0 ? field.name.slice(fieldsetName.length + 1) : field.name); | ||
// Update the error only if the field belongs to the fieldset | ||
if (typeof key === 'string' && paths.length === 0) { | ||
if (field.dataset.conformTouched) { | ||
setError(prev => { | ||
var prevMessage = getValidationMessage(prev === null || prev === void 0 ? void 0 : prev[key]); | ||
if (prevMessage === field.validationMessage) { | ||
return prev; | ||
} | ||
return _objectSpread2(_objectSpread2({}, prev), {}, { | ||
[key]: getErrors(field.validationMessage) | ||
}); | ||
}); | ||
} | ||
event.preventDefault(); | ||
} | ||
}; | ||
var resetHandler = event => { | ||
var form = getFormElement(ref.current); | ||
if (!form || event.target !== form) { | ||
return; | ||
} | ||
setError({}); | ||
}; | ||
// The invalid event does not bubble and so listening on the capturing pharse is needed | ||
document.addEventListener('invalid', invalidHandler, true); | ||
document.addEventListener('reset', resetHandler); | ||
return () => { | ||
document.removeEventListener('invalid', invalidHandler, true); | ||
document.removeEventListener('reset', resetHandler); | ||
}; | ||
}, [ref]); | ||
/** | ||
@@ -294,4 +300,4 @@ * This allows us constructing the field at runtime as we have no information | ||
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, _ref5) => { | ||
var [name, message] = _ref5; | ||
var initialError = Object.entries((_fieldsetConfig$initi = fieldsetConfig.initialError) !== null && _fieldsetConfig$initi !== void 0 ? _fieldsetConfig$initi : {}).reduce((result, _ref3) => { | ||
var [name, message] = _ref3; | ||
var [field, ...paths] = getPaths(name); | ||
@@ -322,4 +328,3 @@ if (field === key) { | ||
/** | ||
* Returns a list of key and config, with a group of helpers | ||
* configuring buttons for list manipulation | ||
* Returns a list of key and field config. | ||
* | ||
@@ -329,13 +334,6 @@ * @see https://conform.guide/api/react#usefieldlist | ||
function useFieldList(ref, config) { | ||
var configRef = useRef(config); | ||
var [error, setError] = useState(() => { | ||
var initialError = []; | ||
for (var [name, message] of Object.entries((_config$initialError = config === null || config === void 0 ? void 0 : config.initialError) !== null && _config$initialError !== void 0 ? _config$initialError : {})) { | ||
var _config$initialError; | ||
var [index, ...paths] = getPaths(name); | ||
if (typeof index === 'number' && paths.length === 0) { | ||
initialError[index] = [].concat(message !== null && message !== void 0 ? message : []); | ||
} | ||
} | ||
return initialError; | ||
var configRef = useConfigRef(config); | ||
var [error, setError] = useFormError(ref, { | ||
initialError: config.initialError, | ||
name: config.name | ||
}); | ||
@@ -346,32 +344,5 @@ var [entries, setEntries] = useState(() => { | ||
}); | ||
useSafeLayoutEffect(() => { | ||
configRef.current = config; | ||
}); | ||
useEffect(() => { | ||
var invalidHandler = event => { | ||
var _configRef$current$na2; | ||
var conformHandler = event => { | ||
var form = getFormElement(ref.current); | ||
var field = event.target; | ||
var prefix = (_configRef$current$na2 = configRef.current.name) !== null && _configRef$current$na2 !== void 0 ? _configRef$current$na2 : ''; | ||
if (!form || !isFieldElement(field) || field.form !== form || !field.name.startsWith(prefix)) { | ||
return; | ||
} | ||
var [index, ...paths] = getPaths(prefix.length > 0 ? field.name.slice(prefix.length) : field.name); | ||
// Update the error only if the field belongs to the fieldset | ||
if (typeof index === 'number' && paths.length === 0) { | ||
if (field.dataset.conformTouched) { | ||
setError(prev => { | ||
var prevMessage = getValidationMessage(prev === null || prev === void 0 ? void 0 : prev[index]); | ||
if (prevMessage === field.validationMessage) { | ||
return prev; | ||
} | ||
return [...prev.slice(0, index), getErrors(field.validationMessage), ...prev.slice(index + 1)]; | ||
}); | ||
} | ||
event.preventDefault(); | ||
} | ||
}; | ||
var listHandler = event => { | ||
var form = getFormElement(ref.current); | ||
if (!form || event.target !== form) { | ||
@@ -404,2 +375,8 @@ return; | ||
setError(error => { | ||
var errorList = []; | ||
for (var [key, messages] of Object.entries(error)) { | ||
if (typeof key === 'number') { | ||
errorList[key] = messages; | ||
} | ||
} | ||
switch (command.type) { | ||
@@ -409,3 +386,3 @@ case 'append': | ||
case 'replace': | ||
return updateList([...error], _objectSpread2(_objectSpread2({}, command), {}, { | ||
errorList = updateList(errorList, _objectSpread2(_objectSpread2({}, command), {}, { | ||
payload: _objectSpread2(_objectSpread2({}, command.payload), {}, { | ||
@@ -415,7 +392,10 @@ defaultValue: undefined | ||
})); | ||
break; | ||
default: | ||
{ | ||
return updateList([...error], command); | ||
errorList = updateList(errorList, command); | ||
break; | ||
} | ||
} | ||
return Object.assign({}, errorList); | ||
}); | ||
@@ -430,22 +410,19 @@ }; | ||
setEntries(Object.entries((_configRef$current$de = configRef.current.defaultValue) !== null && _configRef$current$de !== void 0 ? _configRef$current$de : [undefined])); | ||
setError([]); | ||
}; | ||
// @ts-expect-error Custom event: conform/list | ||
document.addEventListener('conform/list', listHandler, true); | ||
document.addEventListener('invalid', invalidHandler, true); | ||
// @ts-expect-error Custom event: conform | ||
document.addEventListener('conform', conformHandler, true); | ||
document.addEventListener('reset', resetHandler); | ||
return () => { | ||
// @ts-expect-error Custom event: conform/list | ||
document.removeEventListener('conform/list', listHandler, true); | ||
document.removeEventListener('invalid', invalidHandler, true); | ||
// @ts-expect-error Custom event: conform | ||
document.removeEventListener('conform', conformHandler, true); | ||
document.removeEventListener('reset', resetHandler); | ||
}; | ||
}, [ref]); | ||
return entries.map((_ref6, index) => { | ||
var _config$initialError2, _config$defaultValue2; | ||
var [key, defaultValue] = _ref6; | ||
}, [ref, configRef, setError]); | ||
return entries.map((_ref4, index) => { | ||
var _config$initialError, _config$defaultValue2; | ||
var [key, defaultValue] = _ref4; | ||
var errors = error[index]; | ||
var initialError = Object.entries((_config$initialError2 = config.initialError) !== null && _config$initialError2 !== void 0 ? _config$initialError2 : {}).reduce((result, _ref7) => { | ||
var [name, message] = _ref7; | ||
var initialError = Object.entries((_config$initialError = config.initialError) !== null && _config$initialError !== void 0 ? _config$initialError : {}).reduce((result, _ref5) => { | ||
var [name, message] = _ref5; | ||
var [field, ...paths] = getPaths(name); | ||
@@ -512,3 +489,3 @@ if (field === index) { | ||
var ref = useRef(null); | ||
var optionsRef = useRef(options); | ||
var optionsRef = useConfigRef(options); | ||
var changeDispatched = useRef(false); | ||
@@ -518,5 +495,2 @@ var focusDispatched = useRef(false); | ||
useSafeLayoutEffect(() => { | ||
optionsRef.current = options; | ||
}); | ||
useSafeLayoutEffect(() => { | ||
var getInputElement = () => { | ||
@@ -649,6 +623,149 @@ var _optionsRef$current$g, _optionsRef$current, _optionsRef$current$g2; | ||
}; | ||
}, []); | ||
}, [optionsRef]); | ||
return [ref, control]; | ||
} | ||
var VALIDATION_UNDEFINED = '__undefined__'; | ||
var VALIDATION_SKIPPED = '__skipped__'; | ||
var FORM_ERROR_ELEMENT_NAME = '__form__'; | ||
export { useFieldList, useFieldset, useForm, useInputEvent }; | ||
/** | ||
* 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); | ||
} | ||
} | ||
return errors; | ||
}; | ||
var formatMessages = (_options$formatMessag = options === null || options === void 0 ? void 0 : options.formatMessages) !== null && _options$formatMessag !== void 0 ? _options$formatMessag : _ref6 => { | ||
var { | ||
defaultErrors | ||
} = _ref6; | ||
return defaultErrors; | ||
}; | ||
return parse(formData, { | ||
resolve(payload, intent) { | ||
var error = {}; | ||
var constraintPattern = /^constraint[A-Z][^A-Z]*$/; | ||
var _loop = function _loop(element) { | ||
if (isFieldElement(element)) { | ||
var _options$acceptMultip, _options$acceptMultip2; | ||
var name = element.name !== FORM_ERROR_ELEMENT_NAME ? element.name : ''; | ||
var constraint = Object.entries(element.dataset).reduce((result, _ref7) => { | ||
var [name, attributeValue = ''] = _ref7; | ||
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(element.value, { | ||
formData, | ||
attributeValue | ||
}); | ||
} else { | ||
console.warn("Found an \"".concat(constraintName, "\" constraint with undefined definition; Please specify it on the validateConstraint API.")); | ||
} | ||
} | ||
return result; | ||
}, {}); | ||
var errors = formatMessages({ | ||
name, | ||
validity: element.validity, | ||
constraint, | ||
defaultErrors: getDefaultErrors(element.validity, constraint) | ||
}); | ||
var shouldAcceptMultipleErrors = (_options$acceptMultip = options === null || options === void 0 ? void 0 : (_options$acceptMultip2 = options.acceptMultipleErrors) === null || _options$acceptMultip2 === void 0 ? void 0 : _options$acceptMultip2.call(options, { | ||
name, | ||
payload, | ||
intent | ||
})) !== null && _options$acceptMultip !== void 0 ? _options$acceptMultip : false; | ||
if (errors.length > 0) { | ||
error[name] = shouldAcceptMultipleErrors ? errors : errors[0]; | ||
} | ||
} | ||
}; | ||
for (var element of options.form.elements) { | ||
_loop(element); | ||
} | ||
return { | ||
error | ||
}; | ||
} | ||
}); | ||
} | ||
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 === '') { | ||
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 instanceof RadioNodeList) { | ||
for (var field of item) { | ||
if (field.type !== 'radio') { | ||
console.warn('Repeated field name is not supported.'); | ||
continue; | ||
} | ||
} | ||
} | ||
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 scope = getScope(submission.intent); | ||
for (var element of getFormControls(form)) { | ||
var _elementName = element.name !== FORM_ERROR_ELEMENT_NAME ? element.name : ''; | ||
var messages = normalizeError(submission.error[_elementName]); | ||
if (scope === null || scope === _elementName) { | ||
element.dataset.conformTouched = 'true'; | ||
} | ||
if (!messages.includes(VALIDATION_SKIPPED) && !messages.includes(VALIDATION_UNDEFINED)) { | ||
var invalidEvent = new Event('invalid', { | ||
cancelable: true | ||
}); | ||
element.setCustomValidity(getValidationMessage(messages)); | ||
element.dispatchEvent(invalidEvent); | ||
} | ||
} | ||
if (isSubmitting(submission.intent) || isFocusedOnIntentButton(form, submission.intent)) { | ||
if (scope) { | ||
focusFormControl(form, scope); | ||
} else { | ||
focusFirstInvalidControl(form); | ||
} | ||
} | ||
} | ||
/** | ||
* Check if the current focus is on a intent button. | ||
*/ | ||
function isFocusedOnIntentButton(form, intent) { | ||
var element = document.activeElement; | ||
return isFieldElement(element) && element.type === 'submit' && element.form === form && element.name === INTENT && element.value === intent; | ||
} | ||
export { FORM_ERROR_ELEMENT_NAME, VALIDATION_SKIPPED, VALIDATION_UNDEFINED, isFocusedOnIntentButton, reportSubmission, useFieldList, useFieldset, useForm, useInputEvent, validateConstraint }; |
@@ -1,4 +0,4 @@ | ||
export { isFieldElement, list, parse, requestIntent, validate, validateConstraint } from '@conform-to/dom'; | ||
export { useFieldList, useFieldset, useForm, useInputEvent } from './hooks.js'; | ||
export { isFieldElement, list, parse, requestIntent, validate } from '@conform-to/dom'; | ||
export { useFieldList, useFieldset, useForm, useInputEvent, validateConstraint } from './hooks.js'; | ||
import * as helpers from './helpers.js'; | ||
export { helpers as conform }; |
@@ -6,3 +6,3 @@ { | ||
"license": "MIT", | ||
"version": "0.6.1", | ||
"version": "0.6.2", | ||
"main": "index.js", | ||
@@ -24,3 +24,3 @@ "module": "module/index.js", | ||
"dependencies": { | ||
"@conform-to/dom": "0.6.1" | ||
"@conform-to/dom": "0.6.2" | ||
}, | ||
@@ -27,0 +27,0 @@ "peerDependencies": { |
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
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
98382
15
2076
+ Added@conform-to/dom@0.6.2(transitive)
- Removed@conform-to/dom@0.6.1(transitive)
Updated@conform-to/dom@0.6.2