@conform-to/react
Advanced tools
Comparing version 0.6.0-pre.0 to 0.6.0
@@ -1,4 +0,4 @@ | ||
import { type FieldConfig, VALIDATION_SKIPPED, VALIDATION_UNDEFINED } from '@conform-to/dom'; | ||
import { type FieldConfig, type Primitive, VALIDATION_UNDEFINED, VALIDATION_SKIPPED, INTENT } from '@conform-to/dom'; | ||
import type { CSSProperties, HTMLInputTypeAttribute } from 'react'; | ||
interface FieldProps { | ||
interface FormControlProps { | ||
id?: string; | ||
@@ -11,7 +11,7 @@ name: string; | ||
style?: CSSProperties; | ||
'aria-invalid': boolean; | ||
'aria-describedby'?: string; | ||
'aria-invalid'?: boolean; | ||
'aria-hidden'?: boolean; | ||
} | ||
interface InputProps<Schema> extends FieldProps { | ||
interface InputProps<Schema> extends FormControlProps { | ||
type?: HTMLInputTypeAttribute; | ||
@@ -29,7 +29,7 @@ minLength?: number; | ||
} | ||
interface SelectProps extends FieldProps { | ||
interface SelectProps extends FormControlProps { | ||
defaultValue?: string | number | readonly string[] | undefined; | ||
multiple?: boolean; | ||
} | ||
interface TextareaProps extends FieldProps { | ||
interface TextareaProps extends FormControlProps { | ||
minLength?: number; | ||
@@ -51,10 +51,9 @@ maxLength?: number; | ||
}): InputProps<Schema>; | ||
export declare function input<Schema extends any>(config: FieldConfig<Schema>, options?: InputOptions): InputProps<Schema>; | ||
export declare function select<Schema>(config: FieldConfig<Schema>, options?: { | ||
export declare function input<Schema extends Primitive>(config: FieldConfig<Schema>, options?: InputOptions): InputProps<Schema>; | ||
export declare function select(config: FieldConfig<Primitive | Primitive[]>, options?: { | ||
hidden?: boolean; | ||
}): SelectProps; | ||
export declare function textarea<Schema>(config: FieldConfig<Schema>, options?: { | ||
export declare function textarea(config: FieldConfig<Primitive>, options?: { | ||
hidden?: boolean; | ||
}): TextareaProps; | ||
export declare const intent = "__intent__"; | ||
export { VALIDATION_UNDEFINED, VALIDATION_SKIPPED }; | ||
export { INTENT, VALIDATION_UNDEFINED, VALIDATION_SKIPPED }; |
112
helpers.js
@@ -5,2 +5,3 @@ 'use strict'; | ||
var _rollupPluginBabelHelpers = require('./_virtual/_rollupPluginBabelHelpers.js'); | ||
var dom = require('@conform-to/dom'); | ||
@@ -23,11 +24,31 @@ | ||
}; | ||
function getFormControlProps(config, options) { | ||
var _config$error; | ||
var props = { | ||
id: config.id, | ||
name: config.name, | ||
form: config.form, | ||
required: config.required | ||
}; | ||
if (config.id) { | ||
props.id = config.id; | ||
props['aria-describedby'] = config.errorId; | ||
} | ||
if (config.errorId && (_config$error = config.error) !== null && _config$error !== void 0 && _config$error.length) { | ||
props['aria-invalid'] = true; | ||
} | ||
if (config.initialError && Object.entries(config.initialError).length > 0) { | ||
props.autoFocus = true; | ||
} | ||
if (options !== null && options !== void 0 && options.hidden) { | ||
props.style = hiddenStyle; | ||
props.tabIndex = -1; | ||
props['aria-hidden'] = true; | ||
} | ||
return props; | ||
} | ||
function input(config) { | ||
var _config$initialError; | ||
var options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {}; | ||
var attributes = { | ||
id: config.id, | ||
var props = _rollupPluginBabelHelpers.objectSpread2(_rollupPluginBabelHelpers.objectSpread2({}, getFormControlProps(config, options)), {}, { | ||
type: options.type, | ||
name: config.name, | ||
form: config.form, | ||
required: config.required, | ||
minLength: config.minLength, | ||
@@ -39,71 +60,33 @@ maxLength: config.maxLength, | ||
pattern: config.pattern, | ||
multiple: config.multiple, | ||
'aria-invalid': Boolean((_config$initialError = config.initialError) === null || _config$initialError === void 0 ? void 0 : _config$initialError.length), | ||
'aria-describedby': config.errorId | ||
}; | ||
if (options !== null && options !== void 0 && options.hidden) { | ||
attributes.style = hiddenStyle; | ||
attributes.tabIndex = -1; | ||
attributes['aria-hidden'] = true; | ||
} | ||
if (config.initialError && Object.entries(config.initialError).length > 0) { | ||
attributes.autoFocus = true; | ||
} | ||
multiple: config.multiple | ||
}); | ||
if (options.type === 'checkbox' || options.type === 'radio') { | ||
var _options$value; | ||
attributes.value = (_options$value = options.value) !== null && _options$value !== void 0 ? _options$value : 'on'; | ||
attributes.defaultChecked = config.defaultValue === attributes.value; | ||
props.value = (_options$value = options.value) !== null && _options$value !== void 0 ? _options$value : 'on'; | ||
props.defaultChecked = config.defaultValue === props.value; | ||
} else if (options.type !== 'file') { | ||
attributes.defaultValue = config.defaultValue; | ||
props.defaultValue = config.defaultValue; | ||
} | ||
return attributes; | ||
return props; | ||
} | ||
function select(config, options) { | ||
var _config$defaultValue, _config$initialError2; | ||
var attributes = { | ||
id: config.id, | ||
name: config.name, | ||
form: config.form, | ||
defaultValue: config.multiple ? Array.isArray(config.defaultValue) ? config.defaultValue : [] : "".concat((_config$defaultValue = config.defaultValue) !== null && _config$defaultValue !== void 0 ? _config$defaultValue : ''), | ||
required: config.required, | ||
multiple: config.multiple, | ||
'aria-invalid': Boolean((_config$initialError2 = config.initialError) === null || _config$initialError2 === void 0 ? void 0 : _config$initialError2.length), | ||
'aria-describedby': config.errorId | ||
}; | ||
if (options !== null && options !== void 0 && options.hidden) { | ||
attributes.style = hiddenStyle; | ||
attributes.tabIndex = -1; | ||
attributes['aria-hidden'] = true; | ||
} | ||
if (config.initialError && Object.entries(config.initialError).length > 0) { | ||
attributes.autoFocus = true; | ||
} | ||
return attributes; | ||
var props = _rollupPluginBabelHelpers.objectSpread2(_rollupPluginBabelHelpers.objectSpread2({}, getFormControlProps(config, options)), {}, { | ||
defaultValue: config.defaultValue, | ||
multiple: config.multiple | ||
}); | ||
return props; | ||
} | ||
function textarea(config, options) { | ||
var _config$defaultValue2, _config$initialError3; | ||
var attributes = { | ||
id: config.id, | ||
name: config.name, | ||
form: config.form, | ||
defaultValue: "".concat((_config$defaultValue2 = config.defaultValue) !== null && _config$defaultValue2 !== void 0 ? _config$defaultValue2 : ''), | ||
required: config.required, | ||
var props = _rollupPluginBabelHelpers.objectSpread2(_rollupPluginBabelHelpers.objectSpread2({}, getFormControlProps(config, options)), {}, { | ||
defaultValue: config.defaultValue, | ||
minLength: config.minLength, | ||
maxLength: config.maxLength, | ||
autoFocus: Boolean(config.initialError), | ||
'aria-invalid': Boolean((_config$initialError3 = config.initialError) === null || _config$initialError3 === void 0 ? void 0 : _config$initialError3.length), | ||
'aria-describedby': config.errorId | ||
}; | ||
if (options !== null && options !== void 0 && options.hidden) { | ||
attributes.style = hiddenStyle; | ||
attributes.tabIndex = -1; | ||
attributes['aria-hidden'] = true; | ||
} | ||
if (config.initialError && Object.entries(config.initialError).length > 0) { | ||
attributes.autoFocus = true; | ||
} | ||
return attributes; | ||
maxLength: config.maxLength | ||
}); | ||
return props; | ||
} | ||
var intent = '__intent__'; | ||
Object.defineProperty(exports, 'INTENT', { | ||
enumerable: true, | ||
get: function () { return dom.INTENT; } | ||
}); | ||
Object.defineProperty(exports, 'VALIDATION_SKIPPED', { | ||
@@ -118,4 +101,3 @@ enumerable: true, | ||
exports.input = input; | ||
exports.intent = intent; | ||
exports.select = select; | ||
exports.textarea = textarea; |
@@ -21,5 +21,5 @@ import { type FieldConfig, type FieldElement, type FieldValue, type FieldsetConstraint, type FormMethod, type FormEncType, type Submission } from '@conform-to/dom'; | ||
/** | ||
* An object describing the state from the last submission | ||
* An object describing the result of the last submission | ||
*/ | ||
state?: Submission; | ||
lastSubmission?: Submission; | ||
/** | ||
@@ -64,13 +64,16 @@ * An object describing the constraint of each field | ||
interface FormProps { | ||
id?: string; | ||
ref: RefObject<HTMLFormElement>; | ||
id?: string; | ||
onSubmit: (event: FormEvent<HTMLFormElement>) => void; | ||
noValidate: boolean; | ||
'aria-invalid'?: 'true'; | ||
'aria-describedby'?: string; | ||
} | ||
interface Form<Schema extends Record<string, any>> { | ||
interface Form { | ||
id?: string; | ||
errorId?: string; | ||
error: string; | ||
errors: string[]; | ||
ref: RefObject<HTMLFormElement>; | ||
error: string; | ||
props: FormProps; | ||
config: FieldsetConfig<Schema>; | ||
} | ||
@@ -83,16 +86,8 @@ /** | ||
*/ | ||
export declare function useForm<Schema extends Record<string, any>, ClientSubmission extends Submission | Submission<Schema> = Submission>(config?: FormConfig<Schema, ClientSubmission>): [Form<Schema>, Fieldset<Schema>]; | ||
export declare function useForm<Schema extends Record<string, any>, ClientSubmission extends Submission | Submission<Schema> = Submission>(config?: FormConfig<Schema, ClientSubmission>): [Form, Fieldset<Schema>]; | ||
/** | ||
* All the information of the field, including state and config. | ||
* A set of field configuration | ||
*/ | ||
export type Field<Schema> = { | ||
config: FieldConfig<Schema>; | ||
error?: string; | ||
errors?: string[]; | ||
}; | ||
/** | ||
* A set of field information. | ||
*/ | ||
export type Fieldset<Schema extends Record<string, any>> = { | ||
[Key in keyof Schema]-?: Field<Schema[Key]>; | ||
[Key in keyof Schema]-?: FieldConfig<Schema[Key]>; | ||
}; | ||
@@ -117,3 +112,3 @@ export interface FieldsetConfig<Schema extends Record<string, any>> { | ||
/** | ||
* The id of the form, connecting each field to a form remotely. | ||
* The id of the form, connecting each field to a form remotely | ||
*/ | ||
@@ -137,6 +132,3 @@ form?: string; | ||
key: string; | ||
error: string | undefined; | ||
errors: string[] | undefined; | ||
config: FieldConfig<Payload>; | ||
}>; | ||
} & FieldConfig<Payload>>; | ||
interface InputControl { | ||
@@ -143,0 +135,0 @@ change: (eventOrValue: { |
91
hooks.js
@@ -16,16 +16,15 @@ 'use strict'; | ||
function useForm() { | ||
var _config$state; | ||
var _config$lastSubmissio; | ||
var config = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {}; | ||
var configRef = react.useRef(config); | ||
var ref = react.useRef(null); | ||
var [lastSubmission, setLastSubmission] = react.useState((_config$state = config.state) !== null && _config$state !== void 0 ? _config$state : null); | ||
var [error, setError] = react.useState(() => { | ||
if (!config.state) { | ||
return ''; | ||
var [lastSubmission, setLastSubmission] = react.useState((_config$lastSubmissio = config.lastSubmission) !== null && _config$lastSubmissio !== void 0 ? _config$lastSubmissio : null); | ||
var [errors, setErrors] = react.useState(() => { | ||
if (!config.lastSubmission) { | ||
return []; | ||
} | ||
var message = config.state.error['']; | ||
return dom.getValidationMessage(message); | ||
return [].concat(config.lastSubmission.error['']); | ||
}); | ||
var [uncontrolledState, setUncontrolledState] = react.useState(() => { | ||
var submission = config.state; | ||
var submission = config.lastSubmission; | ||
if (!submission) { | ||
@@ -36,2 +35,3 @@ return { | ||
} | ||
var scope = dom.getScope(submission.intent); | ||
return { | ||
@@ -41,3 +41,3 @@ defaultValue: submission.payload, | ||
var [name, message] = _ref; | ||
if (name !== '' && dom.shouldValidate(submission.intent, name)) { | ||
if (name !== '' && (scope === null || scope === name)) { | ||
result[name] = message; | ||
@@ -63,3 +63,3 @@ } | ||
var form = ref.current; | ||
var submission = config.state; | ||
var submission = config.lastSubmission; | ||
if (!form || !submission) { | ||
@@ -75,3 +75,3 @@ return; | ||
setLastSubmission(submission); | ||
}, [config.state]); | ||
}, [config.lastSubmission]); | ||
react.useEffect(() => { | ||
@@ -111,3 +111,3 @@ var form = ref.current; | ||
var field = event.target; | ||
if (!form || !dom.isFieldElement(field) || field.form !== form || field.name !== '__form__') { | ||
if (!form || !dom.isFieldElement(field) || field.form !== form || field.name !== dom.FORM_ERROR_ELEMENT_NAME) { | ||
return; | ||
@@ -117,3 +117,3 @@ } | ||
if (field.dataset.conformTouched) { | ||
setError(field.validationMessage); | ||
setErrors(dom.getErrors(field.validationMessage)); | ||
} | ||
@@ -132,7 +132,6 @@ }; | ||
delete field.dataset.conformTouched; | ||
field.setAttribute('aria-invalid', 'false'); | ||
field.setCustomValidity(''); | ||
} | ||
} | ||
setError(''); | ||
setErrors([]); | ||
setUncontrolledState({ | ||
@@ -161,8 +160,7 @@ defaultValue: formConfig.defaultValue | ||
var form = { | ||
id: config.id, | ||
ref, | ||
error, | ||
error: errors[0], | ||
errors, | ||
props: { | ||
ref, | ||
id: config.id, | ||
noValidate, | ||
@@ -210,5 +208,13 @@ onSubmit(event) { | ||
} | ||
}, | ||
config: fieldsetConfig | ||
} | ||
}; | ||
if (config.id) { | ||
form.id = config.id; | ||
form.errorId = "".concat(config.id, "-error"); | ||
form.props.id = form.id; | ||
form.props['aria-describedby'] = form.errorId; | ||
} | ||
if (form.errorId && form.errors.length > 0) { | ||
form.props['aria-invalid'] = 'true'; | ||
} | ||
return [form, fieldset]; | ||
@@ -218,3 +224,3 @@ } | ||
/** | ||
* All the information of the field, including state and config. | ||
* A set of field configuration | ||
*/ | ||
@@ -247,3 +253,4 @@ | ||
for (var [key, _error] of Object.entries(uncontrolledState.initialError)) { | ||
result[key] = dom.getErrors(dom.getValidationMessage(_error === null || _error === void 0 ? void 0 : _error[''])); | ||
var _error$; | ||
result[key] = [].concat((_error$ = _error === null || _error === void 0 ? void 0 : _error['']) !== null && _error$ !== void 0 ? _error$ : []); | ||
} | ||
@@ -269,6 +276,2 @@ return result; | ||
if (field.dataset.conformTouched) { | ||
// Update the aria attribute only if it is set | ||
if (field.getAttribute('aria-invalid')) { | ||
field.setAttribute('aria-invalid', field.validationMessage !== '' ? 'true' : 'false'); | ||
} | ||
setError(prev => { | ||
@@ -325,15 +328,13 @@ var prevMessage = dom.getValidationMessage(prev === null || prev === void 0 ? void 0 : prev[key]); | ||
var errors = error === null || error === void 0 ? void 0 : error[key]; | ||
var field = { | ||
config: _rollupPluginBabelHelpers.objectSpread2({ | ||
name: fieldsetConfig.name ? "".concat(fieldsetConfig.name, ".").concat(key) : key, | ||
defaultValue: uncontrolledState.defaultValue[key], | ||
initialError: uncontrolledState.initialError[key] | ||
}, constraint), | ||
var field = _rollupPluginBabelHelpers.objectSpread2(_rollupPluginBabelHelpers.objectSpread2({}, constraint), {}, { | ||
name: fieldsetConfig.name ? "".concat(fieldsetConfig.name, ".").concat(key) : key, | ||
defaultValue: uncontrolledState.defaultValue[key], | ||
initialError: uncontrolledState.initialError[key], | ||
error: errors === null || errors === void 0 ? void 0 : errors[0], | ||
errors | ||
}; | ||
}); | ||
if (fieldsetConfig.form) { | ||
field.config.form = fieldsetConfig.form; | ||
field.config.id = "".concat(fieldsetConfig.form, "-").concat(field.config.name); | ||
field.config.errorId = "".concat(field.config.id, "-error"); | ||
field.form = fieldsetConfig.form; | ||
field.id = "".concat(fieldsetConfig.form, "-").concat(field.name); | ||
field.errorId = "".concat(field.id, "-error"); | ||
} | ||
@@ -371,3 +372,6 @@ return field; | ||
}); | ||
var [error, setError] = react.useState(() => uncontrolledState.initialError.map(error => dom.getErrors(dom.getValidationMessage(error === null || error === void 0 ? void 0 : error[''])))); | ||
var [error, setError] = react.useState(() => uncontrolledState.initialError.map(error => { | ||
var _error$2; | ||
return [].concat((_error$2 = error === null || error === void 0 ? void 0 : error['']) !== null && _error$2 !== void 0 ? _error$2 : []); | ||
})); | ||
var [entries, setEntries] = react.useState(() => { | ||
@@ -482,3 +486,5 @@ var _config$defaultValue3; | ||
defaultValue: defaultValue !== null && defaultValue !== void 0 ? defaultValue : uncontrolledState.defaultValue[index], | ||
initialError: uncontrolledState.initialError[index] | ||
initialError: uncontrolledState.initialError[index], | ||
error: errors === null || errors === void 0 ? void 0 : errors[0], | ||
errors | ||
}; | ||
@@ -490,8 +496,5 @@ if (config.form) { | ||
} | ||
return { | ||
key, | ||
error: errors === null || errors === void 0 ? void 0 : errors[0], | ||
errors, | ||
config: fieldConfig | ||
}; | ||
return _rollupPluginBabelHelpers.objectSpread2({ | ||
key | ||
}, fieldConfig); | ||
}); | ||
@@ -498,0 +501,0 @@ } |
@@ -1,3 +0,3 @@ | ||
export { type FieldConfig, type FieldsetConstraint, type Submission, getFormElements, list, validate, requestIntent, requestSubmit, parse, validateConstraint, } from '@conform-to/dom'; | ||
export * from './hooks'; | ||
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 * as conform from './helpers'; |
@@ -11,5 +11,5 @@ 'use strict'; | ||
Object.defineProperty(exports, 'getFormElements', { | ||
Object.defineProperty(exports, 'isFieldElement', { | ||
enumerable: true, | ||
get: function () { return dom.getFormElements; } | ||
get: function () { return dom.isFieldElement; } | ||
}); | ||
@@ -28,6 +28,2 @@ Object.defineProperty(exports, 'list', { | ||
}); | ||
Object.defineProperty(exports, 'requestSubmit', { | ||
enumerable: true, | ||
get: function () { return dom.requestSubmit; } | ||
}); | ||
Object.defineProperty(exports, 'validate', { | ||
@@ -34,0 +30,0 @@ enumerable: true, |
@@ -1,2 +0,3 @@ | ||
export { VALIDATION_SKIPPED, VALIDATION_UNDEFINED } from '@conform-to/dom'; | ||
import { objectSpread2 as _objectSpread2 } from './_virtual/_rollupPluginBabelHelpers.js'; | ||
export { INTENT, VALIDATION_SKIPPED, VALIDATION_UNDEFINED } from '@conform-to/dom'; | ||
@@ -18,11 +19,31 @@ /** | ||
}; | ||
function getFormControlProps(config, options) { | ||
var _config$error; | ||
var props = { | ||
id: config.id, | ||
name: config.name, | ||
form: config.form, | ||
required: config.required | ||
}; | ||
if (config.id) { | ||
props.id = config.id; | ||
props['aria-describedby'] = config.errorId; | ||
} | ||
if (config.errorId && (_config$error = config.error) !== null && _config$error !== void 0 && _config$error.length) { | ||
props['aria-invalid'] = true; | ||
} | ||
if (config.initialError && Object.entries(config.initialError).length > 0) { | ||
props.autoFocus = true; | ||
} | ||
if (options !== null && options !== void 0 && options.hidden) { | ||
props.style = hiddenStyle; | ||
props.tabIndex = -1; | ||
props['aria-hidden'] = true; | ||
} | ||
return props; | ||
} | ||
function input(config) { | ||
var _config$initialError; | ||
var options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {}; | ||
var attributes = { | ||
id: config.id, | ||
var props = _objectSpread2(_objectSpread2({}, getFormControlProps(config, options)), {}, { | ||
type: options.type, | ||
name: config.name, | ||
form: config.form, | ||
required: config.required, | ||
minLength: config.minLength, | ||
@@ -34,71 +55,29 @@ maxLength: config.maxLength, | ||
pattern: config.pattern, | ||
multiple: config.multiple, | ||
'aria-invalid': Boolean((_config$initialError = config.initialError) === null || _config$initialError === void 0 ? void 0 : _config$initialError.length), | ||
'aria-describedby': config.errorId | ||
}; | ||
if (options !== null && options !== void 0 && options.hidden) { | ||
attributes.style = hiddenStyle; | ||
attributes.tabIndex = -1; | ||
attributes['aria-hidden'] = true; | ||
} | ||
if (config.initialError && Object.entries(config.initialError).length > 0) { | ||
attributes.autoFocus = true; | ||
} | ||
multiple: config.multiple | ||
}); | ||
if (options.type === 'checkbox' || options.type === 'radio') { | ||
var _options$value; | ||
attributes.value = (_options$value = options.value) !== null && _options$value !== void 0 ? _options$value : 'on'; | ||
attributes.defaultChecked = config.defaultValue === attributes.value; | ||
props.value = (_options$value = options.value) !== null && _options$value !== void 0 ? _options$value : 'on'; | ||
props.defaultChecked = config.defaultValue === props.value; | ||
} else if (options.type !== 'file') { | ||
attributes.defaultValue = config.defaultValue; | ||
props.defaultValue = config.defaultValue; | ||
} | ||
return attributes; | ||
return props; | ||
} | ||
function select(config, options) { | ||
var _config$defaultValue, _config$initialError2; | ||
var attributes = { | ||
id: config.id, | ||
name: config.name, | ||
form: config.form, | ||
defaultValue: config.multiple ? Array.isArray(config.defaultValue) ? config.defaultValue : [] : "".concat((_config$defaultValue = config.defaultValue) !== null && _config$defaultValue !== void 0 ? _config$defaultValue : ''), | ||
required: config.required, | ||
multiple: config.multiple, | ||
'aria-invalid': Boolean((_config$initialError2 = config.initialError) === null || _config$initialError2 === void 0 ? void 0 : _config$initialError2.length), | ||
'aria-describedby': config.errorId | ||
}; | ||
if (options !== null && options !== void 0 && options.hidden) { | ||
attributes.style = hiddenStyle; | ||
attributes.tabIndex = -1; | ||
attributes['aria-hidden'] = true; | ||
} | ||
if (config.initialError && Object.entries(config.initialError).length > 0) { | ||
attributes.autoFocus = true; | ||
} | ||
return attributes; | ||
var props = _objectSpread2(_objectSpread2({}, getFormControlProps(config, options)), {}, { | ||
defaultValue: config.defaultValue, | ||
multiple: config.multiple | ||
}); | ||
return props; | ||
} | ||
function textarea(config, options) { | ||
var _config$defaultValue2, _config$initialError3; | ||
var attributes = { | ||
id: config.id, | ||
name: config.name, | ||
form: config.form, | ||
defaultValue: "".concat((_config$defaultValue2 = config.defaultValue) !== null && _config$defaultValue2 !== void 0 ? _config$defaultValue2 : ''), | ||
required: config.required, | ||
var props = _objectSpread2(_objectSpread2({}, getFormControlProps(config, options)), {}, { | ||
defaultValue: config.defaultValue, | ||
minLength: config.minLength, | ||
maxLength: config.maxLength, | ||
autoFocus: Boolean(config.initialError), | ||
'aria-invalid': Boolean((_config$initialError3 = config.initialError) === null || _config$initialError3 === void 0 ? void 0 : _config$initialError3.length), | ||
'aria-describedby': config.errorId | ||
}; | ||
if (options !== null && options !== void 0 && options.hidden) { | ||
attributes.style = hiddenStyle; | ||
attributes.tabIndex = -1; | ||
attributes['aria-hidden'] = true; | ||
} | ||
if (config.initialError && Object.entries(config.initialError).length > 0) { | ||
attributes.autoFocus = true; | ||
} | ||
return attributes; | ||
maxLength: config.maxLength | ||
}); | ||
return props; | ||
} | ||
var intent = '__intent__'; | ||
export { input, intent, select, textarea }; | ||
export { input, select, textarea }; |
import { objectSpread2 as _objectSpread2 } from './_virtual/_rollupPluginBabelHelpers.js'; | ||
import { getValidationMessage, shouldValidate, parseListCommand, reportSubmission, getFormData, parse, VALIDATION_UNDEFINED, getFormAttributes, getPaths, getName, getErrors, isFieldElement, requestIntent, validate, getFormElement, updateList } from '@conform-to/dom'; | ||
import { getScope, parseListCommand, reportSubmission, getFormData, parse, VALIDATION_UNDEFINED, getFormAttributes, getPaths, getName, isFieldElement, requestIntent, validate, getFormElement, FORM_ERROR_ELEMENT_NAME, getErrors, getValidationMessage, updateList } from '@conform-to/dom'; | ||
import { useRef, useState, useEffect, useMemo, useLayoutEffect } from 'react'; | ||
@@ -12,16 +12,15 @@ | ||
function useForm() { | ||
var _config$state; | ||
var _config$lastSubmissio; | ||
var config = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {}; | ||
var configRef = useRef(config); | ||
var ref = useRef(null); | ||
var [lastSubmission, setLastSubmission] = useState((_config$state = config.state) !== null && _config$state !== void 0 ? _config$state : null); | ||
var [error, setError] = useState(() => { | ||
if (!config.state) { | ||
return ''; | ||
var [lastSubmission, setLastSubmission] = useState((_config$lastSubmissio = config.lastSubmission) !== null && _config$lastSubmissio !== void 0 ? _config$lastSubmissio : null); | ||
var [errors, setErrors] = useState(() => { | ||
if (!config.lastSubmission) { | ||
return []; | ||
} | ||
var message = config.state.error['']; | ||
return getValidationMessage(message); | ||
return [].concat(config.lastSubmission.error['']); | ||
}); | ||
var [uncontrolledState, setUncontrolledState] = useState(() => { | ||
var submission = config.state; | ||
var submission = config.lastSubmission; | ||
if (!submission) { | ||
@@ -32,2 +31,3 @@ return { | ||
} | ||
var scope = getScope(submission.intent); | ||
return { | ||
@@ -37,3 +37,3 @@ defaultValue: submission.payload, | ||
var [name, message] = _ref; | ||
if (name !== '' && shouldValidate(submission.intent, name)) { | ||
if (name !== '' && (scope === null || scope === name)) { | ||
result[name] = message; | ||
@@ -59,3 +59,3 @@ } | ||
var form = ref.current; | ||
var submission = config.state; | ||
var submission = config.lastSubmission; | ||
if (!form || !submission) { | ||
@@ -71,3 +71,3 @@ return; | ||
setLastSubmission(submission); | ||
}, [config.state]); | ||
}, [config.lastSubmission]); | ||
useEffect(() => { | ||
@@ -107,3 +107,3 @@ var form = ref.current; | ||
var field = event.target; | ||
if (!form || !isFieldElement(field) || field.form !== form || field.name !== '__form__') { | ||
if (!form || !isFieldElement(field) || field.form !== form || field.name !== FORM_ERROR_ELEMENT_NAME) { | ||
return; | ||
@@ -113,3 +113,3 @@ } | ||
if (field.dataset.conformTouched) { | ||
setError(field.validationMessage); | ||
setErrors(getErrors(field.validationMessage)); | ||
} | ||
@@ -128,7 +128,6 @@ }; | ||
delete field.dataset.conformTouched; | ||
field.setAttribute('aria-invalid', 'false'); | ||
field.setCustomValidity(''); | ||
} | ||
} | ||
setError(''); | ||
setErrors([]); | ||
setUncontrolledState({ | ||
@@ -157,8 +156,7 @@ defaultValue: formConfig.defaultValue | ||
var form = { | ||
id: config.id, | ||
ref, | ||
error, | ||
error: errors[0], | ||
errors, | ||
props: { | ||
ref, | ||
id: config.id, | ||
noValidate, | ||
@@ -206,5 +204,13 @@ onSubmit(event) { | ||
} | ||
}, | ||
config: fieldsetConfig | ||
} | ||
}; | ||
if (config.id) { | ||
form.id = config.id; | ||
form.errorId = "".concat(config.id, "-error"); | ||
form.props.id = form.id; | ||
form.props['aria-describedby'] = form.errorId; | ||
} | ||
if (form.errorId && form.errors.length > 0) { | ||
form.props['aria-invalid'] = 'true'; | ||
} | ||
return [form, fieldset]; | ||
@@ -214,3 +220,3 @@ } | ||
/** | ||
* All the information of the field, including state and config. | ||
* A set of field configuration | ||
*/ | ||
@@ -243,3 +249,4 @@ | ||
for (var [key, _error] of Object.entries(uncontrolledState.initialError)) { | ||
result[key] = getErrors(getValidationMessage(_error === null || _error === void 0 ? void 0 : _error[''])); | ||
var _error$; | ||
result[key] = [].concat((_error$ = _error === null || _error === void 0 ? void 0 : _error['']) !== null && _error$ !== void 0 ? _error$ : []); | ||
} | ||
@@ -265,6 +272,2 @@ return result; | ||
if (field.dataset.conformTouched) { | ||
// Update the aria attribute only if it is set | ||
if (field.getAttribute('aria-invalid')) { | ||
field.setAttribute('aria-invalid', field.validationMessage !== '' ? 'true' : 'false'); | ||
} | ||
setError(prev => { | ||
@@ -321,15 +324,13 @@ var prevMessage = getValidationMessage(prev === null || prev === void 0 ? void 0 : prev[key]); | ||
var errors = error === null || error === void 0 ? void 0 : error[key]; | ||
var field = { | ||
config: _objectSpread2({ | ||
name: fieldsetConfig.name ? "".concat(fieldsetConfig.name, ".").concat(key) : key, | ||
defaultValue: uncontrolledState.defaultValue[key], | ||
initialError: uncontrolledState.initialError[key] | ||
}, constraint), | ||
var field = _objectSpread2(_objectSpread2({}, constraint), {}, { | ||
name: fieldsetConfig.name ? "".concat(fieldsetConfig.name, ".").concat(key) : key, | ||
defaultValue: uncontrolledState.defaultValue[key], | ||
initialError: uncontrolledState.initialError[key], | ||
error: errors === null || errors === void 0 ? void 0 : errors[0], | ||
errors | ||
}; | ||
}); | ||
if (fieldsetConfig.form) { | ||
field.config.form = fieldsetConfig.form; | ||
field.config.id = "".concat(fieldsetConfig.form, "-").concat(field.config.name); | ||
field.config.errorId = "".concat(field.config.id, "-error"); | ||
field.form = fieldsetConfig.form; | ||
field.id = "".concat(fieldsetConfig.form, "-").concat(field.name); | ||
field.errorId = "".concat(field.id, "-error"); | ||
} | ||
@@ -367,3 +368,6 @@ return field; | ||
}); | ||
var [error, setError] = useState(() => uncontrolledState.initialError.map(error => getErrors(getValidationMessage(error === null || error === void 0 ? void 0 : error[''])))); | ||
var [error, setError] = useState(() => uncontrolledState.initialError.map(error => { | ||
var _error$2; | ||
return [].concat((_error$2 = error === null || error === void 0 ? void 0 : error['']) !== null && _error$2 !== void 0 ? _error$2 : []); | ||
})); | ||
var [entries, setEntries] = useState(() => { | ||
@@ -478,3 +482,5 @@ var _config$defaultValue3; | ||
defaultValue: defaultValue !== null && defaultValue !== void 0 ? defaultValue : uncontrolledState.defaultValue[index], | ||
initialError: uncontrolledState.initialError[index] | ||
initialError: uncontrolledState.initialError[index], | ||
error: errors === null || errors === void 0 ? void 0 : errors[0], | ||
errors | ||
}; | ||
@@ -486,8 +492,5 @@ if (config.form) { | ||
} | ||
return { | ||
key, | ||
error: errors === null || errors === void 0 ? void 0 : errors[0], | ||
errors, | ||
config: fieldConfig | ||
}; | ||
return _objectSpread2({ | ||
key | ||
}, fieldConfig); | ||
}); | ||
@@ -494,0 +497,0 @@ } |
@@ -1,4 +0,4 @@ | ||
export { getFormElements, list, parse, requestIntent, requestSubmit, validate, validateConstraint } from '@conform-to/dom'; | ||
export { isFieldElement, list, parse, requestIntent, validate, validateConstraint } from '@conform-to/dom'; | ||
export { useFieldList, useFieldset, useForm, useInputEvent } from './hooks.js'; | ||
import * as helpers from './helpers.js'; | ||
export { helpers as conform }; |
@@ -5,3 +5,3 @@ { | ||
"license": "MIT", | ||
"version": "0.6.0-pre.0", | ||
"version": "0.6.0", | ||
"main": "index.js", | ||
@@ -23,3 +23,3 @@ "module": "module/index.js", | ||
"dependencies": { | ||
"@conform-to/dom": "0.6.0-pre.0" | ||
"@conform-to/dom": "0.6.0" | ||
}, | ||
@@ -26,0 +26,0 @@ "peerDependencies": { |
308
README.md
@@ -14,9 +14,8 @@ # @conform-to/react | ||
- [conform](#conform) | ||
- [parse](#parse) | ||
- [validateConstraint](#validateconstraint) | ||
- [list](#list) | ||
- [validate](#validate) | ||
- [requestIntent](#requestintent) | ||
- [getFormElements](#getformelements) | ||
- [hasError](#haserror) | ||
- [parse](#parse) | ||
- [shouldValidate](#shouldvalidate) | ||
- [isFieldElement](#isfieldelement) | ||
@@ -31,5 +30,5 @@ <!-- /aside --> | ||
- Enabling customizing form validation behaviour. | ||
- Capturing the error message and removes the error bubbles. | ||
- Preparing all properties required to configure the dom elements. | ||
- Enabling customizing validation logic. | ||
- Capturing error message and removes the error bubbles. | ||
- Preparing all properties required to configure the form elements. | ||
@@ -58,8 +57,8 @@ ```tsx | ||
*/ | ||
defaultValue: undefined; | ||
defaultValue: undefined, | ||
/** | ||
* An object describing the state from the last submission | ||
* The last submission result from the server | ||
*/ | ||
state: undefined; | ||
lastSubmission: undefined, | ||
@@ -69,3 +68,3 @@ /** | ||
*/ | ||
constraint: undefined; | ||
constraint: undefined, | ||
@@ -97,3 +96,3 @@ /** | ||
*/ | ||
onSubmit(event, { formData, submission }) { | ||
onSubmit(event, { formData, submission, action, encType, method }) { | ||
// ... | ||
@@ -173,3 +172,3 @@ }, | ||
form.ref, | ||
address.config, | ||
address, | ||
); | ||
@@ -181,9 +180,9 @@ | ||
<legned>Address</legend> | ||
<input {...conform.input(street.config)} /> | ||
<input {...conform.input(street)} /> | ||
<div>{street.error}</div> | ||
<input {...conform.input(zipcode.config)} /> | ||
<input {...conform.input(zipcode)} /> | ||
<div>{zipcode.error}</div> | ||
<input {...conform.input(city.config)} /> | ||
<input {...conform.input(city)} /> | ||
<div>{city.error}</div> | ||
<input {...conform.input(country.config)} /> | ||
<input {...conform.input(country)} /> | ||
<div>{country.error}</div> | ||
@@ -214,3 +213,3 @@ </fieldset> | ||
**conform** utilises the DOM as its context provider / input registry, which maintains a link between each input / button / fieldset with the form through the [form property](https://developer.mozilla.org/en-US/docs/Web/API/HTMLInputElement#properties). The ref object allows it to restrict the scope to elements associated to the same form only. | ||
**conform** utilises the DOM as its context provider / input registry, which maintains a link between each input / button / fieldset with the form through the [form property](https://developer.mozilla.org/en-US/docs/Web/API/HTMLInputElement#properties). The ref object allows it to restrict the scope to form elements associated to the same form only. | ||
@@ -242,3 +241,3 @@ ```tsx | ||
This hook enables you to work with [array](/docs/configuration.md#array) and support [list](#list) command button builder to modify a list. It can also be used with [useFieldset](#usefieldset) for [nested list](/docs/configuration.md#nested-list) at the same time. | ||
This hook enables you to work with [array](/docs/configuration.md#array) and support the [list](#list) intent button builder to modify a list. It can also be used with [useFieldset](#usefieldset) for [nested list](/docs/configuration.md#nested-list) at the same time. | ||
@@ -257,10 +256,10 @@ ```tsx | ||
const [form, { items }] = useForm<Schema>(); | ||
const list = useFieldList(form.ref, items.config); | ||
const itemsList = useFieldList(form.ref, items); | ||
return ( | ||
<fieldset ref={ref}> | ||
{list.map((item, index) => ( | ||
{itemsList.map((item, index) => ( | ||
<div key={item.key}> | ||
{/* Setup an input per item */} | ||
<input {...conform.input(item.config)} /> | ||
<input {...conform.input(item)} /> | ||
@@ -271,3 +270,3 @@ {/* Error of each item */} | ||
{/* Setup a delete button (Note: It is `items` not `item`) */} | ||
<button {...list.remove(items.config.name, { index })}>Delete</button> | ||
<button {...list.remove(items.name, { index })}>Delete</button> | ||
</div> | ||
@@ -277,5 +276,3 @@ ))} | ||
{/* Setup a button that can append a new row with optional default value */} | ||
<button {...list.append(items.config.name, { defaultValue: '' })}> | ||
add | ||
</button> | ||
<button {...list.append(items.name, { defaultValue: '' })}>add</button> | ||
</fieldset> | ||
@@ -299,5 +296,5 @@ ); | ||
const [form, { category }] = useForm(); | ||
const [value, setValue] = useState(category.config.defaultValue ?? ''); | ||
const [value, setValue] = useState(category.defaultValue ?? ''); | ||
const [ref, control] = useInputEvent({ | ||
onReset: () => setValue(category.config.defaultValue ?? ''), | ||
onReset: () => setValue(category.defaultValue ?? ''), | ||
}); | ||
@@ -311,3 +308,3 @@ const inputRef = useRef<HTMLInputElement>(null); | ||
ref={ref} | ||
{...conform.input(category.config, { hidden: true })} | ||
{...conform.input(category, { hidden: true })} | ||
onChange={(e) => setValue(e.target.value)} | ||
@@ -340,5 +337,5 @@ onFocus={() => inputRef.current?.focus()} | ||
It provides several helpers to remove the boilerplate when configuring a form control. | ||
It provides several helpers to remove the boilerplate when configuring a form control and derives attributes for [accessibility](/docs/accessibility.md#configuration) concerns and helps [focus management](/docs/focus-management.md#focusing-before-javascript-is-loaded). | ||
You are recommended to create a wrapper on top if you need to integrate with custom input component. As the helper derives attributes for [accessibility](/docs/accessibility.md#configuration) concerns and helps [focus management](/docs/focus-management.md#focusing-before-javascript-is-loaded). | ||
You can also create a wrapper on top if you need to integrate with custom input component. | ||
@@ -357,27 +354,27 @@ Before: | ||
type="text" | ||
name={title.config.name} | ||
form={title.config.form} | ||
defaultValue={title.config.defaultValue} | ||
requried={title.config.required} | ||
minLength={title.config.minLength} | ||
maxLength={title.config.maxLength} | ||
min={title.config.min} | ||
max={title.config.max} | ||
multiple={title.config.multiple} | ||
pattern={title.config.pattern} | ||
name={title.name} | ||
form={title.form} | ||
defaultValue={title.defaultValue} | ||
requried={title.required} | ||
minLength={title.minLength} | ||
maxLength={title.maxLength} | ||
min={title.min} | ||
max={title.max} | ||
multiple={title.multiple} | ||
pattern={title.pattern} | ||
/> | ||
<textarea | ||
name={description.config.name} | ||
form={description.config.form} | ||
defaultValue={description.config.defaultValue} | ||
requried={description.config.required} | ||
minLength={description.config.minLength} | ||
maxLength={description.config.maxLength} | ||
name={description.name} | ||
form={description.form} | ||
defaultValue={description.defaultValue} | ||
requried={description.required} | ||
minLength={description.minLength} | ||
maxLength={description.maxLength} | ||
/> | ||
<select | ||
name={category.config.name} | ||
form={category.config.form} | ||
defaultValue={category.config.defaultValue} | ||
requried={category.config.required} | ||
multiple={category.config.multiple} | ||
name={category.name} | ||
form={category.form} | ||
defaultValue={category.defaultValue} | ||
requried={category.required} | ||
multiple={category.multiple} | ||
> | ||
@@ -401,5 +398,5 @@ {/* ... */} | ||
<form {...form.props}> | ||
<input {...conform.input(title.config, { type: 'text' })} /> | ||
<textarea {...conform.textarea(description.config)} /> | ||
<select {...conform.select(category.config)}>{/* ... */}</select> | ||
<input {...conform.input(title, { type: 'text' })} /> | ||
<textarea {...conform.textarea(description)} /> | ||
<select {...conform.select(category)}>{/* ... */}</select> | ||
</form> | ||
@@ -412,5 +409,113 @@ ); | ||
### parse | ||
It parses the formData based on the [naming convention](/docs/configuration.md#naming-convention) with the validation result from the resolver. | ||
```tsx | ||
import { parse } from '@conform-to/react'; | ||
const formData = new FormData(); | ||
const submission = parse(formData, { | ||
resolve({ email, password }) { | ||
const error: Record<string, string> = {}; | ||
if (typeof email !== 'string') { | ||
error.email = 'Email is required'; | ||
} else if (!/^[^@]+@[^@]+$/.test(email)) { | ||
error.email = 'Email is invalid'; | ||
} | ||
if (typeof password !== 'string') { | ||
error.password = 'Password is required'; | ||
} | ||
if (error.email || error.password) { | ||
return { error }; | ||
} | ||
return { | ||
value: { email, password }, | ||
}; | ||
}, | ||
}); | ||
``` | ||
--- | ||
### validateConstraint | ||
This enable Constraint Validation with ability to enable custom constraint using data-attribute and customizing error messages. By default, the error message would be the attribute that triggered the error (e.g. `required` / `type` / 'minLength' etc). | ||
```tsx | ||
import { useForm, validateConstraint } from '@conform-to/react'; | ||
import { Form } from 'react-router-dom'; | ||
export default function SignupForm() { | ||
const [form, { email, password, confirmPassword }] = useForm({ | ||
onValidate(context) { | ||
// This enables validating each field based on the validity state and custom cosntraint if defined | ||
return validateConstraint( | ||
...context, | ||
constraint: { | ||
// Define custom constraint | ||
match(value, { formData, attributeValue }) { | ||
// Check if the value of the field match the value of another field | ||
return value === formData.get(attributeValue); | ||
}, | ||
}); | ||
} | ||
}); | ||
return ( | ||
<Form method="post" {...form.props}> | ||
<div> | ||
<label>Email</label> | ||
<input | ||
name="email" | ||
type="email" | ||
required | ||
pattern="[^@]+@[^@]+\\.[^@]+" | ||
/> | ||
{email.error === 'required' ? ( | ||
<div>Email is required</div> | ||
) : email.error === 'type' ? ( | ||
<div>Email is invalid</div> | ||
) : null} | ||
</div> | ||
<div> | ||
<label>Password</label> | ||
<input | ||
name="password" | ||
type="password" | ||
required | ||
/> | ||
{password.error === 'required' ? ( | ||
<div>Password is required</div> | ||
) : null} | ||
</div> | ||
<div> | ||
<label>Confirm Password</label> | ||
<input | ||
name="confirmPassword" | ||
type="password" | ||
required | ||
data-constraint-match="password" | ||
/> | ||
{confirmPassword.error === 'required' ? ( | ||
<div>Confirm Password is required</div> | ||
) : confirmPassword.error === 'match' ? ( | ||
<div>Password does not match</div> | ||
) : null} | ||
</div> | ||
<button>Signup</button> | ||
</Form> | ||
); | ||
} | ||
``` | ||
--- | ||
### list | ||
It provides serveral helpers to configure a command button for [modifying a list](/docs/commands.md#modifying-a-list). | ||
It provides serveral helpers to configure an intent button for [modifying a list](/docs/commands.md#modifying-a-list). | ||
@@ -448,3 +553,3 @@ ```tsx | ||
It returns the properties required to configure a command button for [validation](/docs/commands.md#validation). | ||
It returns the properties required to configure an intent button for [validation](/docs/commands.md#validation). | ||
@@ -485,3 +590,3 @@ ```tsx | ||
const [form, { tasks }] = useForm(); | ||
const taskList = useFieldList(form.ref, tasks.config); | ||
const taskList = useFieldList(form.ref, tasks); | ||
@@ -496,3 +601,3 @@ const handleDrop = (from, to) => | ||
<div key={task.key}> | ||
<input {...conform.input(task.config)} /> | ||
<input {...conform.input(task)} /> | ||
</div> | ||
@@ -509,79 +614,20 @@ ))} | ||
### getFormElements | ||
### isFieldElement | ||
It returns all _input_ / _select_ / _textarea_ or _button_ in the forms. Useful when looping through the form elements to validate each field manually. | ||
This is an utility for checking if the provided element is a form element (_input_ / _select_ / _textarea_ or _button_) which also works as a type guard. | ||
```tsx | ||
import { useForm, parse, getFormElements } from '@conform-to/react'; | ||
export default function LoginForm() { | ||
const [form] = useForm({ | ||
onValidate({ form, formData }) { | ||
const submission = parse(formData); | ||
for (const element of getFormElements(form)) { | ||
switch (element.name) { | ||
case 'email': { | ||
if (element.validity.valueMissing) { | ||
submission.error.push([element.name, 'Email is required']); | ||
} else if (element.validity.typeMismatch) { | ||
submission.error.push([element.name, 'Email is invalid']); | ||
} | ||
break; | ||
} | ||
case 'password': { | ||
if (element.validity.valueMissing) { | ||
submission.error.push([element.name, 'Password is required']); | ||
} | ||
break; | ||
} | ||
function Example() { | ||
return ( | ||
<form | ||
onFocus={(event) => { | ||
if (isFieldElement(event.target)) { | ||
// event.target is now considered one of the form elements type | ||
} | ||
} | ||
return submission; | ||
}, | ||
// .... | ||
}); | ||
// ... | ||
}} | ||
> | ||
{/* ... */} | ||
</form> | ||
); | ||
} | ||
``` | ||
--- | ||
### parse | ||
It parses the formData based on the [naming convention](/docs/submission). | ||
```tsx | ||
import { parse } from '@conform-to/react'; | ||
const formData = new FormData(); | ||
const submission = parse(formData); | ||
console.log(submission); | ||
``` | ||
--- | ||
### shouldValidate | ||
This helper checks if the scope of validation includes a specific field by checking the submission: | ||
```tsx | ||
import { shouldValidate } from '@conform-to/react'; | ||
/** | ||
* The submission intent give us hint on what should be valdiated. | ||
* If the intent is 'validate/:field', only the field with name matching must be validated. | ||
* If the intent is undefined, everything should be validated (Default submission) | ||
*/ | ||
const intent = 'validate/email'; | ||
// This will log 'true' | ||
console.log(shouldValidate(intent, 'email')); | ||
// This will log 'false' | ||
console.log(shouldValidate(intent, 'password')); | ||
``` |
612
88679
14
1816
+ Added@conform-to/dom@0.6.0(transitive)
- Removed@conform-to/dom@0.6.0-pre.0(transitive)
Updated@conform-to/dom@0.6.0