@conform-to/react
Advanced tools
Comparing version 0.5.0-pre.0 to 0.5.0
import type { FieldConfig } from '@conform-to/dom'; | ||
import type { HTMLInputTypeAttribute } from 'react'; | ||
interface FieldProps { | ||
id?: string; | ||
name: string; | ||
@@ -8,2 +9,4 @@ form?: string; | ||
autoFocus?: boolean; | ||
'aria-invalid': boolean; | ||
'aria-describedby'?: string; | ||
} | ||
@@ -10,0 +13,0 @@ interface InputProps<Schema> extends FieldProps { |
@@ -6,4 +6,6 @@ 'use strict'; | ||
function input(config) { | ||
var _config$initialError; | ||
var options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {}; | ||
var attributes = { | ||
id: config.id, | ||
type: options.type, | ||
@@ -19,3 +21,5 @@ name: config.name, | ||
pattern: config.pattern, | ||
multiple: config.multiple | ||
multiple: config.multiple, | ||
'aria-invalid': Boolean((_config$initialError = config.initialError) === null || _config$initialError === void 0 ? void 0 : _config$initialError.length), | ||
'aria-describedby': config.errorId | ||
}; | ||
@@ -35,4 +39,5 @@ if (config.initialError && config.initialError.length > 0) { | ||
function select(config) { | ||
var _config$defaultValue; | ||
var _config$defaultValue, _config$initialError2; | ||
var attributes = { | ||
id: config.id, | ||
name: config.name, | ||
@@ -42,3 +47,5 @@ form: config.form, | ||
required: config.required, | ||
multiple: config.multiple | ||
multiple: config.multiple, | ||
'aria-invalid': Boolean((_config$initialError2 = config.initialError) === null || _config$initialError2 === void 0 ? void 0 : _config$initialError2.length), | ||
'aria-describedby': config.errorId | ||
}; | ||
@@ -51,4 +58,5 @@ if (config.initialError && config.initialError.length > 0) { | ||
function textarea(config) { | ||
var _config$defaultValue2; | ||
var _config$defaultValue2, _config$initialError3; | ||
var attributes = { | ||
id: config.id, | ||
name: config.name, | ||
@@ -60,3 +68,5 @@ form: config.form, | ||
maxLength: config.maxLength, | ||
autoFocus: Boolean(config.initialError) | ||
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 | ||
}; | ||
@@ -63,0 +73,0 @@ if (config.initialError && config.initialError.length > 0) { |
@@ -1,5 +0,10 @@ | ||
import { type FieldConfig, type FieldElement, type FieldValue, type FieldsetConstraint, type ListCommand, type Primitive, type Submission } from '@conform-to/dom'; | ||
import { type FieldConfig, type FieldElement, type FieldValue, type FieldsetConstraint, type Primitive, type Submission } from '@conform-to/dom'; | ||
import { type InputHTMLAttributes, type FormEvent, type RefObject } from 'react'; | ||
export interface FormConfig<Schema extends Record<string, any>> { | ||
/** | ||
* If the form id is provided, Id for label, | ||
* input and error elements will be derived. | ||
*/ | ||
id?: string; | ||
/** | ||
* Validation mode. Default to `client-only`. | ||
@@ -24,2 +29,6 @@ */ | ||
/** | ||
* An object describing the constraint of each field | ||
*/ | ||
constraint?: FieldsetConstraint<Schema>; | ||
/** | ||
* Enable native validation before hydation. | ||
@@ -57,2 +66,3 @@ * | ||
ref: RefObject<HTMLFormElement>; | ||
id?: string; | ||
onSubmit: (event: FormEvent<HTMLFormElement>) => void; | ||
@@ -62,2 +72,3 @@ noValidate: boolean; | ||
interface Form<Schema extends Record<string, any>> { | ||
id?: string; | ||
ref: RefObject<HTMLFormElement>; | ||
@@ -72,5 +83,5 @@ error: string; | ||
* | ||
* @see https://github.com/edmundhung/conform/tree/v0.4.1/packages/conform-react/README.md#useform | ||
* @see https://conform.guide/api/react#useform | ||
*/ | ||
export declare function useForm<Schema extends Record<string, any>>(config?: FormConfig<Schema>): Form<Schema>; | ||
export declare function useForm<Schema extends Record<string, any>>(config?: FormConfig<Schema>): [Form<Schema>, Fieldset<Schema>]; | ||
/** | ||
@@ -114,15 +125,6 @@ * All the information of the field, including state and config. | ||
* | ||
* @see https://github.com/edmundhung/conform/tree/v0.4.1/packages/conform-react/README.md#usefieldset | ||
* @see https://conform.guide/api/react#usefieldset | ||
*/ | ||
export declare function useFieldset<Schema extends Record<string, any>>(ref: RefObject<HTMLFormElement | HTMLFieldSetElement>, config: FieldsetConfig<Schema>): Fieldset<Schema>; | ||
export declare function useFieldset<Schema extends Record<string, any>>(ref: RefObject<HTMLFormElement | HTMLFieldSetElement>, config: FieldConfig<Schema>): Fieldset<Schema>; | ||
interface CommandButtonProps { | ||
name?: string; | ||
value?: string; | ||
form?: string; | ||
formNoValidate: true; | ||
} | ||
declare type ListCommandPayload<Schema, Type extends ListCommand<FieldValue<Schema>>['type']> = Extract<ListCommand<FieldValue<Schema>>, { | ||
type: Type; | ||
}>['payload']; | ||
/** | ||
@@ -132,18 +134,9 @@ * Returns a list of key and config, with a group of helpers | ||
* | ||
* @see https://github.com/edmundhung/conform/tree/v0.4.1/packages/conform-react/README.md#usefieldlist | ||
* @see https://conform.guide/api/react#usefieldlist | ||
*/ | ||
export declare function useFieldList<Payload = any>(ref: RefObject<HTMLFormElement | HTMLFieldSetElement>, config: FieldConfig<Array<Payload>>): [ | ||
Array<{ | ||
key: string; | ||
error: string | undefined; | ||
config: FieldConfig<Payload>; | ||
}>, | ||
{ | ||
prepend(payload?: ListCommandPayload<Payload, 'prepend'>): CommandButtonProps; | ||
append(payload?: ListCommandPayload<Payload, 'append'>): CommandButtonProps; | ||
replace(payload: ListCommandPayload<Payload, 'replace'>): CommandButtonProps; | ||
remove(payload: ListCommandPayload<Payload, 'remove'>): CommandButtonProps; | ||
reorder(payload: ListCommandPayload<Payload, 'reorder'>): CommandButtonProps; | ||
} | ||
]; | ||
export declare function useFieldList<Payload = any>(ref: RefObject<HTMLFormElement | HTMLFieldSetElement>, config: FieldConfig<Array<Payload>>): Array<{ | ||
key: string; | ||
error: string | undefined; | ||
config: FieldConfig<Payload>; | ||
}>; | ||
interface ShadowInputProps extends InputHTMLAttributes<HTMLInputElement> { | ||
@@ -170,3 +163,3 @@ ref: RefObject<HTMLInputElement>; | ||
* | ||
* @see https://github.com/edmundhung/conform/tree/v0.4.1/packages/conform-react/README.md#usecontrolledinput | ||
* @see https://conform.guide/api/react#usecontrolledinput | ||
*/ | ||
@@ -173,0 +166,0 @@ export declare function useControlledInput<Element extends { |
122
hooks.js
@@ -14,3 +14,3 @@ 'use strict'; | ||
* | ||
* @see https://github.com/edmundhung/conform/tree/v0.4.1/packages/conform-react/README.md#useform | ||
* @see https://conform.guide/api/react#useform | ||
*/ | ||
@@ -29,13 +29,22 @@ function useForm() { | ||
}); | ||
var [fieldsetConfig, setFieldsetConfig] = react.useState(() => { | ||
var _config$state$error2, _config$state2, _config$state$value, _config$state3; | ||
var error = (_config$state$error2 = (_config$state2 = config.state) === null || _config$state2 === void 0 ? void 0 : _config$state2.error) !== null && _config$state$error2 !== void 0 ? _config$state$error2 : []; | ||
var [uncontrolledState, setUncontrolledState] = react.useState(() => { | ||
var submission = config.state; | ||
if (!submission) { | ||
return { | ||
defaultValue: config.defaultValue | ||
}; | ||
} | ||
return { | ||
defaultValue: (_config$state$value = (_config$state3 = config.state) === null || _config$state3 === void 0 ? void 0 : _config$state3.value) !== null && _config$state$value !== void 0 ? _config$state$value : config.defaultValue, | ||
initialError: error.filter(_ref2 => { | ||
defaultValue: submission.value, | ||
initialError: submission.error.filter(_ref2 => { | ||
var [name] = _ref2; | ||
return name !== '' && dom.getSubmissionType(name) === null; | ||
return name !== '' && dom.shouldValidate(submission, name); | ||
}) | ||
}; | ||
}); | ||
var fieldsetConfig = _rollupPluginBabelHelpers.objectSpread2(_rollupPluginBabelHelpers.objectSpread2({}, uncontrolledState), {}, { | ||
constraint: config.constraint, | ||
form: config.id | ||
}); | ||
var fieldset = useFieldset(ref, fieldsetConfig); | ||
var [noValidate, setNoValidate] = react.useState(config.noValidate || !config.fallbackNative); | ||
@@ -64,8 +73,5 @@ react.useEffect(() => { | ||
} | ||
if (formConfig.initialReport === 'onChange') { | ||
field.dataset.conformTouched = 'true'; | ||
if (field.dataset.conformTouched || formConfig.initialReport === 'onChange') { | ||
dom.requestCommand(form, dom.validate(field.name)); | ||
} | ||
if (field.dataset.conformTouched) { | ||
dom.requestValidate(form, field.name); | ||
} | ||
}; | ||
@@ -80,4 +86,3 @@ var handleBlur = event => { | ||
if (formConfig.initialReport === 'onBlur' && !field.dataset.conformTouched) { | ||
field.dataset.conformTouched = 'true'; | ||
dom.requestValidate(form, field.name); | ||
dom.requestCommand(form, dom.validate(field.name)); | ||
} | ||
@@ -107,2 +112,3 @@ }; | ||
delete field.dataset.conformTouched; | ||
field.setAttribute('aria-invalid', 'false'); | ||
field.setCustomValidity(''); | ||
@@ -112,3 +118,3 @@ } | ||
setError(''); | ||
setFieldsetConfig({ | ||
setUncontrolledState({ | ||
defaultValue: formConfig.defaultValue, | ||
@@ -136,3 +142,4 @@ initialError: [] | ||
}, []); | ||
return { | ||
var form = { | ||
id: config.id, | ||
ref, | ||
@@ -142,2 +149,3 @@ error, | ||
ref, | ||
id: config.id, | ||
noValidate, | ||
@@ -184,12 +192,2 @@ onSubmit(event) { | ||
} | ||
// Touch all fields only if the submitter is not a command button | ||
if (submission.type === 'submit') { | ||
for (var field of form.elements) { | ||
if (dom.isFieldElement(field)) { | ||
// Mark the field as touched | ||
field.dataset.conformTouched = 'true'; | ||
} | ||
} | ||
} | ||
if (!config.noValidate && !(submitter !== null && submitter !== void 0 && submitter.formNoValidate) && dom.hasError(submission.error) || submission.type === 'validate' && config.mode !== 'server-validation') { | ||
@@ -214,2 +212,3 @@ event.preventDefault(); | ||
}; | ||
return [form, fieldset]; | ||
} | ||
@@ -275,2 +274,6 @@ | ||
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 => { | ||
@@ -330,3 +333,2 @@ var _prev$key; | ||
name: fieldsetConfig.name ? "".concat(fieldsetConfig.name, ".").concat(key) : key, | ||
form: fieldsetConfig.form, | ||
defaultValue: uncontrolledState.defaultValue[key], | ||
@@ -337,2 +339,7 @@ initialError: uncontrolledState.initialError[key] | ||
}; | ||
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"); | ||
} | ||
return field; | ||
@@ -342,2 +349,3 @@ } | ||
} | ||
/** | ||
@@ -347,3 +355,3 @@ * Returns a list of key and config, with a group of helpers | ||
* | ||
* @see https://github.com/edmundhung/conform/tree/v0.4.1/packages/conform-react/README.md#usefieldlist | ||
* @see https://conform.guide/api/react#usefieldlist | ||
*/ | ||
@@ -379,37 +387,2 @@ function useFieldList(ref, config) { | ||
}); | ||
var list = entries.map((_ref3, index) => { | ||
var [key, defaultValue] = _ref3; | ||
return { | ||
key, | ||
error: error[index], | ||
config: { | ||
name: "".concat(config.name, "[").concat(index, "]"), | ||
form: config.form, | ||
defaultValue: defaultValue !== null && defaultValue !== void 0 ? defaultValue : uncontrolledState.defaultValue[index], | ||
initialError: uncontrolledState.initialError[index] | ||
} | ||
}; | ||
}); | ||
/*** | ||
* This use proxy to capture all information about the command and | ||
* have it encoded in the value. | ||
*/ | ||
var command = new Proxy({}, { | ||
get(_target, type) { | ||
return function () { | ||
var payload = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {}; | ||
return { | ||
name: 'conform/list', | ||
value: JSON.stringify({ | ||
type, | ||
scope: config.name, | ||
payload | ||
}), | ||
form: config.form, | ||
formNoValidate: true | ||
}; | ||
}; | ||
} | ||
}); | ||
react.useEffect(() => { | ||
@@ -511,5 +484,20 @@ configRef.current = config; | ||
}, [ref]); | ||
return [list, | ||
// @ts-expect-error proxy type | ||
command]; | ||
return entries.map((_ref3, index) => { | ||
var [key, defaultValue] = _ref3; | ||
var fieldConfig = { | ||
name: "".concat(config.name, "[").concat(index, "]"), | ||
defaultValue: defaultValue !== null && defaultValue !== void 0 ? defaultValue : uncontrolledState.defaultValue[index], | ||
initialError: uncontrolledState.initialError[index] | ||
}; | ||
if (config.form) { | ||
fieldConfig.form = config.form; | ||
fieldConfig.id = "".concat(config.form, "-").concat(config.name); | ||
fieldConfig.errorId = "".concat(fieldConfig.id, "-error"); | ||
} | ||
return { | ||
key, | ||
error: error[index], | ||
config: fieldConfig | ||
}; | ||
}); | ||
} | ||
@@ -521,3 +509,3 @@ /** | ||
* | ||
* @see https://github.com/edmundhung/conform/tree/v0.4.1/packages/conform-react/README.md#usecontrolledinput | ||
* @see https://conform.guide/api/react#usecontrolledinput | ||
*/ | ||
@@ -524,0 +512,0 @@ function useControlledInput(config) { |
@@ -1,3 +0,3 @@ | ||
export { type FieldConfig, type FieldsetConstraint, type Submission, getFormElements, hasError, parse, shouldValidate, } from '@conform-to/dom'; | ||
export { type FieldConfig, type FieldsetConstraint, type Submission, getFormElements, hasError, list, validate, requestCommand, requestSubmit, parse, shouldValidate, } from '@conform-to/dom'; | ||
export * from './hooks'; | ||
export * as conform from './helpers'; |
16
index.js
@@ -19,2 +19,6 @@ 'use strict'; | ||
}); | ||
Object.defineProperty(exports, 'list', { | ||
enumerable: true, | ||
get: function () { return dom.list; } | ||
}); | ||
Object.defineProperty(exports, 'parse', { | ||
@@ -24,2 +28,10 @@ enumerable: true, | ||
}); | ||
Object.defineProperty(exports, 'requestCommand', { | ||
enumerable: true, | ||
get: function () { return dom.requestCommand; } | ||
}); | ||
Object.defineProperty(exports, 'requestSubmit', { | ||
enumerable: true, | ||
get: function () { return dom.requestSubmit; } | ||
}); | ||
Object.defineProperty(exports, 'shouldValidate', { | ||
@@ -29,2 +41,6 @@ enumerable: true, | ||
}); | ||
Object.defineProperty(exports, 'validate', { | ||
enumerable: true, | ||
get: function () { return dom.validate; } | ||
}); | ||
exports.useControlledInput = hooks.useControlledInput; | ||
@@ -31,0 +47,0 @@ exports.useFieldList = hooks.useFieldList; |
function input(config) { | ||
var _config$initialError; | ||
var options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {}; | ||
var attributes = { | ||
id: config.id, | ||
type: options.type, | ||
@@ -14,3 +16,5 @@ name: config.name, | ||
pattern: config.pattern, | ||
multiple: config.multiple | ||
multiple: config.multiple, | ||
'aria-invalid': Boolean((_config$initialError = config.initialError) === null || _config$initialError === void 0 ? void 0 : _config$initialError.length), | ||
'aria-describedby': config.errorId | ||
}; | ||
@@ -30,4 +34,5 @@ if (config.initialError && config.initialError.length > 0) { | ||
function select(config) { | ||
var _config$defaultValue; | ||
var _config$defaultValue, _config$initialError2; | ||
var attributes = { | ||
id: config.id, | ||
name: config.name, | ||
@@ -37,3 +42,5 @@ form: config.form, | ||
required: config.required, | ||
multiple: config.multiple | ||
multiple: config.multiple, | ||
'aria-invalid': Boolean((_config$initialError2 = config.initialError) === null || _config$initialError2 === void 0 ? void 0 : _config$initialError2.length), | ||
'aria-describedby': config.errorId | ||
}; | ||
@@ -46,4 +53,5 @@ if (config.initialError && config.initialError.length > 0) { | ||
function textarea(config) { | ||
var _config$defaultValue2; | ||
var _config$defaultValue2, _config$initialError3; | ||
var attributes = { | ||
id: config.id, | ||
name: config.name, | ||
@@ -55,3 +63,5 @@ form: config.form, | ||
maxLength: config.maxLength, | ||
autoFocus: Boolean(config.initialError) | ||
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 | ||
}; | ||
@@ -58,0 +68,0 @@ if (config.initialError && config.initialError.length > 0) { |
import { objectSpread2 as _objectSpread2 } from './_virtual/_rollupPluginBabelHelpers.js'; | ||
import { getSubmissionType, reportSubmission, getFormData, parse, isFieldElement, hasError, getPaths, getName, requestValidate, getFormElement, parseListCommand, updateList } from '@conform-to/dom'; | ||
import { shouldValidate, reportSubmission, getFormData, parse, isFieldElement, hasError, getPaths, getName, requestCommand, validate, getFormElement, parseListCommand, updateList } from '@conform-to/dom'; | ||
import { useRef, useState, useEffect } from 'react'; | ||
@@ -10,3 +10,3 @@ import { input } from './helpers.js'; | ||
* | ||
* @see https://github.com/edmundhung/conform/tree/v0.4.1/packages/conform-react/README.md#useform | ||
* @see https://conform.guide/api/react#useform | ||
*/ | ||
@@ -25,13 +25,22 @@ function useForm() { | ||
}); | ||
var [fieldsetConfig, setFieldsetConfig] = useState(() => { | ||
var _config$state$error2, _config$state2, _config$state$value, _config$state3; | ||
var error = (_config$state$error2 = (_config$state2 = config.state) === null || _config$state2 === void 0 ? void 0 : _config$state2.error) !== null && _config$state$error2 !== void 0 ? _config$state$error2 : []; | ||
var [uncontrolledState, setUncontrolledState] = useState(() => { | ||
var submission = config.state; | ||
if (!submission) { | ||
return { | ||
defaultValue: config.defaultValue | ||
}; | ||
} | ||
return { | ||
defaultValue: (_config$state$value = (_config$state3 = config.state) === null || _config$state3 === void 0 ? void 0 : _config$state3.value) !== null && _config$state$value !== void 0 ? _config$state$value : config.defaultValue, | ||
initialError: error.filter(_ref2 => { | ||
defaultValue: submission.value, | ||
initialError: submission.error.filter(_ref2 => { | ||
var [name] = _ref2; | ||
return name !== '' && getSubmissionType(name) === null; | ||
return name !== '' && shouldValidate(submission, name); | ||
}) | ||
}; | ||
}); | ||
var fieldsetConfig = _objectSpread2(_objectSpread2({}, uncontrolledState), {}, { | ||
constraint: config.constraint, | ||
form: config.id | ||
}); | ||
var fieldset = useFieldset(ref, fieldsetConfig); | ||
var [noValidate, setNoValidate] = useState(config.noValidate || !config.fallbackNative); | ||
@@ -60,8 +69,5 @@ useEffect(() => { | ||
} | ||
if (formConfig.initialReport === 'onChange') { | ||
field.dataset.conformTouched = 'true'; | ||
if (field.dataset.conformTouched || formConfig.initialReport === 'onChange') { | ||
requestCommand(form, validate(field.name)); | ||
} | ||
if (field.dataset.conformTouched) { | ||
requestValidate(form, field.name); | ||
} | ||
}; | ||
@@ -76,4 +82,3 @@ var handleBlur = event => { | ||
if (formConfig.initialReport === 'onBlur' && !field.dataset.conformTouched) { | ||
field.dataset.conformTouched = 'true'; | ||
requestValidate(form, field.name); | ||
requestCommand(form, validate(field.name)); | ||
} | ||
@@ -103,2 +108,3 @@ }; | ||
delete field.dataset.conformTouched; | ||
field.setAttribute('aria-invalid', 'false'); | ||
field.setCustomValidity(''); | ||
@@ -108,3 +114,3 @@ } | ||
setError(''); | ||
setFieldsetConfig({ | ||
setUncontrolledState({ | ||
defaultValue: formConfig.defaultValue, | ||
@@ -132,3 +138,4 @@ initialError: [] | ||
}, []); | ||
return { | ||
var form = { | ||
id: config.id, | ||
ref, | ||
@@ -138,2 +145,3 @@ error, | ||
ref, | ||
id: config.id, | ||
noValidate, | ||
@@ -180,12 +188,2 @@ onSubmit(event) { | ||
} | ||
// Touch all fields only if the submitter is not a command button | ||
if (submission.type === 'submit') { | ||
for (var field of form.elements) { | ||
if (isFieldElement(field)) { | ||
// Mark the field as touched | ||
field.dataset.conformTouched = 'true'; | ||
} | ||
} | ||
} | ||
if (!config.noValidate && !(submitter !== null && submitter !== void 0 && submitter.formNoValidate) && hasError(submission.error) || submission.type === 'validate' && config.mode !== 'server-validation') { | ||
@@ -210,2 +208,3 @@ event.preventDefault(); | ||
}; | ||
return [form, fieldset]; | ||
} | ||
@@ -271,2 +270,6 @@ | ||
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 => { | ||
@@ -326,3 +329,2 @@ var _prev$key; | ||
name: fieldsetConfig.name ? "".concat(fieldsetConfig.name, ".").concat(key) : key, | ||
form: fieldsetConfig.form, | ||
defaultValue: uncontrolledState.defaultValue[key], | ||
@@ -333,2 +335,7 @@ initialError: uncontrolledState.initialError[key] | ||
}; | ||
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"); | ||
} | ||
return field; | ||
@@ -338,2 +345,3 @@ } | ||
} | ||
/** | ||
@@ -343,3 +351,3 @@ * Returns a list of key and config, with a group of helpers | ||
* | ||
* @see https://github.com/edmundhung/conform/tree/v0.4.1/packages/conform-react/README.md#usefieldlist | ||
* @see https://conform.guide/api/react#usefieldlist | ||
*/ | ||
@@ -375,37 +383,2 @@ function useFieldList(ref, config) { | ||
}); | ||
var list = entries.map((_ref3, index) => { | ||
var [key, defaultValue] = _ref3; | ||
return { | ||
key, | ||
error: error[index], | ||
config: { | ||
name: "".concat(config.name, "[").concat(index, "]"), | ||
form: config.form, | ||
defaultValue: defaultValue !== null && defaultValue !== void 0 ? defaultValue : uncontrolledState.defaultValue[index], | ||
initialError: uncontrolledState.initialError[index] | ||
} | ||
}; | ||
}); | ||
/*** | ||
* This use proxy to capture all information about the command and | ||
* have it encoded in the value. | ||
*/ | ||
var command = new Proxy({}, { | ||
get(_target, type) { | ||
return function () { | ||
var payload = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {}; | ||
return { | ||
name: 'conform/list', | ||
value: JSON.stringify({ | ||
type, | ||
scope: config.name, | ||
payload | ||
}), | ||
form: config.form, | ||
formNoValidate: true | ||
}; | ||
}; | ||
} | ||
}); | ||
useEffect(() => { | ||
@@ -507,5 +480,20 @@ configRef.current = config; | ||
}, [ref]); | ||
return [list, | ||
// @ts-expect-error proxy type | ||
command]; | ||
return entries.map((_ref3, index) => { | ||
var [key, defaultValue] = _ref3; | ||
var fieldConfig = { | ||
name: "".concat(config.name, "[").concat(index, "]"), | ||
defaultValue: defaultValue !== null && defaultValue !== void 0 ? defaultValue : uncontrolledState.defaultValue[index], | ||
initialError: uncontrolledState.initialError[index] | ||
}; | ||
if (config.form) { | ||
fieldConfig.form = config.form; | ||
fieldConfig.id = "".concat(config.form, "-").concat(config.name); | ||
fieldConfig.errorId = "".concat(fieldConfig.id, "-error"); | ||
} | ||
return { | ||
key, | ||
error: error[index], | ||
config: fieldConfig | ||
}; | ||
}); | ||
} | ||
@@ -517,3 +505,3 @@ /** | ||
* | ||
* @see https://github.com/edmundhung/conform/tree/v0.4.1/packages/conform-react/README.md#usecontrolledinput | ||
* @see https://conform.guide/api/react#usecontrolledinput | ||
*/ | ||
@@ -520,0 +508,0 @@ function useControlledInput(config) { |
@@ -1,4 +0,4 @@ | ||
export { getFormElements, hasError, parse, shouldValidate } from '@conform-to/dom'; | ||
export { getFormElements, hasError, list, parse, requestCommand, requestSubmit, shouldValidate, validate } from '@conform-to/dom'; | ||
export { useControlledInput, useFieldList, useFieldset, useForm } from './hooks.js'; | ||
import * as helpers from './helpers.js'; | ||
export { helpers as conform }; |
@@ -5,3 +5,3 @@ { | ||
"license": "MIT", | ||
"version": "0.5.0-pre.0", | ||
"version": "0.5.0", | ||
"main": "index.js", | ||
@@ -23,3 +23,3 @@ "module": "module/index.js", | ||
"dependencies": { | ||
"@conform-to/dom": "0.5.0-pre.0" | ||
"@conform-to/dom": "0.5.0" | ||
}, | ||
@@ -26,0 +26,0 @@ "peerDependencies": { |
504
README.md
@@ -23,9 +23,9 @@ # @conform-to/react | ||
By default, the browser calls the [reportValidity()](https://developer.mozilla.org/en-US/docs/Web/API/HTMLFormElement/reportValidity) API on the form element when it is submitted. This checks the validity of all the fields in it and reports if there are errors through the bubbles. | ||
By default, the browser calls the [reportValidity()](https://developer.mozilla.org/en-US/docs/Web/API/HTMLFormElement/reportValidity) API on the form element when a submission is triggered. This checks the validity of all the fields and reports through the error bubbles. | ||
This hook enhances the form validation behaviour in 3 parts: | ||
This hook enhances the form validation behaviour by: | ||
1. It enhances form validation with custom rules by subscribing to different DOM events and reporting the errors only when it is configured to do so. | ||
2. It unifies client and server validation in one place. | ||
3. It exposes the state of each field in the form of [data attributes](https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes/data-*), such as `data-conform-touched`, allowing flexible styling across your form without the need to manipulate the class names. | ||
- Enabling customizing form validation behaviour. | ||
- Capturing the error message and removes the error bubbles. | ||
- Preparing all properties required to configure the dom elements. | ||
@@ -36,4 +36,10 @@ ```tsx | ||
function LoginForm() { | ||
const form = useForm({ | ||
const [form, { email, password }] = useForm({ | ||
/** | ||
* If the form id is provided, Id for label, | ||
* input and error elements will be derived. | ||
*/ | ||
id: undefined, | ||
/** | ||
* Validation mode. | ||
@@ -65,2 +71,7 @@ * Support "client-only" or "server-validation". | ||
/** | ||
* An object describing the constraint of each field | ||
*/ | ||
constraint: undefined; | ||
/** | ||
* Enable native validation before hydation. | ||
@@ -83,3 +94,3 @@ * | ||
*/ | ||
onValidate({ form, formData, submission }) { | ||
onValidate({ form, formData }) { | ||
// ... | ||
@@ -91,3 +102,3 @@ }, | ||
*/ | ||
onSubmit(event, { form, formData, submission }) { | ||
onSubmit(event, { formData, submission }) { | ||
// ... | ||
@@ -104,7 +115,7 @@ }, | ||
It is a group of properties properties required to hook into form events. They can also be set explicitly as shown below: | ||
It is a group of properties required to hook into form events. They can also be set explicitly as shown below: | ||
```tsx | ||
function RandomForm() { | ||
const form = useForm(); | ||
const [form] = useForm(); | ||
@@ -114,2 +125,3 @@ return ( | ||
ref={form.props.ref} | ||
id={form.props.id} | ||
onSubmit={form.props.onSubmit} | ||
@@ -136,3 +148,3 @@ noValidate={form.props.noValidate} | ||
function LoginForm() { | ||
const form = useForm(); | ||
const [form] = useForm(); | ||
@@ -149,32 +161,2 @@ return ( | ||
<details> | ||
<summary>Is the `onValidate` function required?</summary> | ||
The `onValidate` function is not required if the validation logic can be fully covered by the [native constraints](https://developer.mozilla.org/en-US/docs/Web/Guide/HTML/Constraint_validation#validation-related_attributes), e.g. **required** / **min** / **pattern** etc. | ||
```tsx | ||
import { useForm, useFieldset } from '@conform-to/react'; | ||
function LoginForm() { | ||
const formProps = useForm(); | ||
const { email, password } = useFieldset(formProps.ref); | ||
return ( | ||
<form {...formProps}> | ||
<label> | ||
<input type="email" name="email" required /> | ||
{email.error} | ||
</label> | ||
<label> | ||
<input type="password" name="password" required /> | ||
{password.error} | ||
</label> | ||
<button type="submit">Login</button> | ||
</form> | ||
); | ||
} | ||
``` | ||
</details> | ||
--- | ||
@@ -184,107 +166,37 @@ | ||
This hook can be used to monitor the state of each field and help configuration. It lets you: | ||
This hook enables you to work with [nested object](/docs/configuration.md#nested-object) by monitoring the state of each nested field and prepraing the config required. | ||
1. Capturing errors at the form/fieldset level, removing the need to setup invalid handler on each field. | ||
2. Defining config in one central place. e.g. name, default value and constraint, then distributing it to each field with the [conform](#conform) helpers. | ||
```tsx | ||
import { useForm, useFieldset } from '@conform-to/react'; | ||
import { useForm, useFieldset, conform } from '@conform-to/react'; | ||
/** | ||
* Consider the schema as follow: | ||
*/ | ||
type Book = { | ||
name: string; | ||
isbn: string; | ||
}; | ||
interface Address { | ||
street: string; | ||
zipcode: string; | ||
city: string; | ||
country: string; | ||
} | ||
function BookFieldset() { | ||
const formProps = useForm(); | ||
const { name, isbn } = useFieldset<Book>( | ||
/** | ||
* A ref object of the form element or fieldset element | ||
*/ | ||
formProps.ref, | ||
{ | ||
/** | ||
* The prefix used to generate the name of nested fields. | ||
*/ | ||
name: 'book', | ||
function Example() { | ||
const [form, { address }] = useForm<{ address: Address }>(); | ||
const { city, zipcode, street, country } = useFieldset( | ||
form.ref, | ||
address.config, | ||
); | ||
/** | ||
* An object representing the initial value of the fieldset. | ||
*/ | ||
defaultValue: { | ||
isbn: '0340013818', | ||
}, | ||
/** | ||
* An object describing the initial error of each field | ||
*/ | ||
initialError: { | ||
isbn: 'Invalid ISBN', | ||
}, | ||
/** | ||
* An object describing the constraint of each field | ||
*/ | ||
constraint: { | ||
isbn: { | ||
required: true, | ||
pattern: '[0-9]{10,13}', | ||
}, | ||
}, | ||
/** | ||
* The id of the form. This is required only if you | ||
* are connecting each field to a form remotely. | ||
*/ | ||
form: 'remote-form-id', | ||
}, | ||
return ( | ||
<form {...form.props}> | ||
<fieldset> | ||
<legned>Address</legend> | ||
<input {...conform.input(street.config)} /> | ||
<div>{street.error}</div> | ||
<input {...conform.input(zipcode.config)} /> | ||
<div>{zipcode.error}</div> | ||
<input {...conform.input(city.config)} /> | ||
<div>{city.error}</div> | ||
<input {...conform.input(country.config)} /> | ||
<div>{country.error}</div> | ||
</fieldset> | ||
<button>Submit</button> | ||
</form> | ||
); | ||
/** | ||
* Latest error of the field | ||
* This would be 'Invalid ISBN' initially as specified | ||
* in the initialError config | ||
*/ | ||
console.log(isbn.error); | ||
/** | ||
* This would be `book.isbn` instead of `isbn` | ||
* if the `name` option is provided | ||
*/ | ||
console.log(isbn.config.name); | ||
/** | ||
* This would be `0340013818` if specified | ||
* on the `initalValue` option | ||
*/ | ||
console.log(isbn.config.defaultValue); | ||
/** | ||
* Initial error message | ||
* This would be 'Invalid ISBN' if specified | ||
*/ | ||
console.log(isbn.config.initialError); | ||
/** | ||
* This would be `random-form-id` | ||
* because of the `form` option provided | ||
*/ | ||
console.log(isbn.config.form); | ||
/** | ||
* Constraint of the field (required, minLength etc) | ||
* | ||
* For example, the constraint of the isbn field would be: | ||
* { | ||
* required: true, | ||
* pattern: '[0-9]{10,13}' | ||
* } | ||
*/ | ||
console.log(isbn.config.required); | ||
console.log(isbn.config.pattern); | ||
return <form {...formProps}>{/* ... */}</form>; | ||
} | ||
@@ -296,8 +208,8 @@ ``` | ||
```tsx | ||
import { useFieldset } from '@conform-to/react'; | ||
import { type FieldConfig, useFieldset } from '@conform-to/react'; | ||
import { useRef } from 'react'; | ||
function Fieldset() { | ||
const ref = useRef(); | ||
const fieldset = useFieldset(ref); | ||
function Fieldset(config: FieldConfig<Address>) { | ||
const ref = useRef<HTMLFieldsetElement>(null); | ||
const { city, zipcode, street, country } = useFieldset(ref, config); | ||
@@ -311,3 +223,3 @@ return <fieldset ref={ref}>{/* ... */}</fieldset>; | ||
Unlike most of the form validation library out there, **conform** use the DOM as its context provider. As the dom 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) of these elements. The ref object allows us restricting 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 elements associated to the same form only. | ||
@@ -339,7 +251,6 @@ ```tsx | ||
It returns a list of key, config and error, with helpers to configure [list command](/docs/submission.md#list-command) button. | ||
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. | ||
```tsx | ||
import { useFieldset, useFieldList } from '@conform-to/react'; | ||
import { useRef } from 'react'; | ||
import { useForm, useFieldList, list } from '@conform-to/react'; | ||
@@ -350,9 +261,8 @@ /** | ||
type Schema = { | ||
list: string[]; | ||
items: string[]; | ||
}; | ||
function CollectionFieldset() { | ||
const ref = useRef<HTMLFieldsetElement>(null); | ||
const fieldset = useFieldset<Collection>(ref); | ||
const [list, command] = useFieldList(ref, fieldset.list.config); | ||
function Example() { | ||
const [form, { items }] = useForm<Schema>(); | ||
const list = useFieldList(form.ref, items.config); | ||
@@ -366,12 +276,14 @@ return ( | ||
{/* Error of each book */} | ||
{/* Error of each item */} | ||
<span>{item.error}</span> | ||
{/* To setup a delete button */} | ||
<button {...command.remove({ index })}>Delete</button> | ||
{/* Setup a delete button (Note: It is `items` not `item`) */} | ||
<button {...list.remove(items.config.name, { index })}>Delete</button> | ||
</div> | ||
))} | ||
{/* To setup a button that can append a new row with optional default value */} | ||
<button {...command.append({ defaultValue: '' })}>add</button> | ||
{/* Setup a button that can append a new row with optional default value */} | ||
<button {...list.append(items.config.name, { defaultValue: '' })}> | ||
add | ||
</button> | ||
</fieldset> | ||
@@ -382,80 +294,2 @@ ); | ||
This hook can also be used in combination with `useFieldset` for nested list: | ||
```tsx | ||
import { | ||
type FieldConfig, | ||
useForm, | ||
useFieldset, | ||
useFieldList, | ||
} from '@conform-to/react'; | ||
import { useRef } from 'react'; | ||
/** | ||
* Consider the schema as follow: | ||
*/ | ||
type Schema = { | ||
list: Array<Item>; | ||
}; | ||
type Item = { | ||
title: string; | ||
description: string; | ||
}; | ||
function CollectionFieldset() { | ||
const ref = useRef<HTMLFieldsetElement>(null); | ||
const fieldset = useFieldset<Collection>(ref); | ||
const [list, command] = useFieldList(ref, fieldset.list.config); | ||
return ( | ||
<fieldset ref={ref}> | ||
{list.map((item, index) => ( | ||
<div key={item.key}> | ||
{/* Pass the item config to another fieldset*/} | ||
<ItemFieldset {...item.config} /> | ||
</div> | ||
))} | ||
</fieldset> | ||
); | ||
} | ||
function ItemFieldset(config: FieldConfig<Item>) { | ||
const ref = useRef<HTMLFieldsetElement>(null); | ||
const { title, description } = useFieldset(ref, config); | ||
return ( | ||
<fieldset ref={ref}> | ||
<input {...conform.input(title.config)} /> | ||
<span>{title.error}</span> | ||
<input {...conform.input(description.config)} /> | ||
<span>{description.error}</span> | ||
</fieldset> | ||
); | ||
} | ||
``` | ||
<details> | ||
<summary>What can I do with `command`?</summary> | ||
```tsx | ||
// To append a new row with optional defaultValue | ||
<button {...command.append({ defaultValue })}>Append</button>; | ||
// To prepend a new row with optional defaultValue | ||
<button {...command.prepend({ defaultValue })}>Prepend</button>; | ||
// To remove a row by index | ||
<button {...command.remove({ index })}>Remove</button>; | ||
// To replace a row with another defaultValue | ||
<button {...command.replace({ index, defaultValue })}>Replace</button>; | ||
// To reorder a particular row to an another index | ||
<button {...command.reorder({ from, to })}>Reorder</button>; | ||
``` | ||
</details> | ||
--- | ||
@@ -465,6 +299,6 @@ | ||
It returns the properties required to configure a shadow input for validation. This is particular useful when integrating dropdown and datepicker whichs introduces custom input mode. | ||
It returns the properties required to configure a shadow input for validation and helper to integrate it. This is particularly useful when [integrating custom input components](/docs/integrations.md#custom-input-component) like dropdown and datepicker. | ||
```tsx | ||
import { useFieldset, useControlledInput } from '@conform-to/react'; | ||
import { useForm, useControlledInput } from '@conform-to/react'; | ||
import { Select, MenuItem } from '@mui/material'; | ||
@@ -474,8 +308,7 @@ import { useRef } from 'react'; | ||
function MuiForm() { | ||
const ref = useRef(); | ||
const { category } = useFieldset(schema); | ||
const [form, { category }] = useForm(); | ||
const [inputProps, control] = useControlledInput(category.config); | ||
return ( | ||
<fieldset ref={ref}> | ||
<form {...form.props}> | ||
{/* Render a shadow input somewhere */} | ||
@@ -485,3 +318,3 @@ <input {...inputProps} /> | ||
{/* MUI Select is a controlled component */} | ||
<Select | ||
<TextField | ||
label="Category" | ||
@@ -493,4 +326,5 @@ inputRef={control.ref} | ||
inputProps={{ | ||
onInvalid: control.onInvalid | ||
onInvalid: control.onInvalid, | ||
}} | ||
select | ||
> | ||
@@ -502,3 +336,3 @@ <MenuItem value="">Please select</MenuItem> | ||
</TextField> | ||
</fieldset> | ||
</form> | ||
); | ||
@@ -512,51 +346,36 @@ } | ||
It provides several helpers to configure a native input field quickly: | ||
It provides several helpers to remove the boilerplate when configuring a form control. | ||
```tsx | ||
import { useFieldset, conform } from '@conform-to/react'; | ||
import { useRef } from 'react'; | ||
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). | ||
function RandomForm() { | ||
const ref = useRef(); | ||
const { category } = useFieldset(ref); | ||
Before: | ||
return ( | ||
<fieldset ref={ref}> | ||
<input {...conform.input(category.config, { type: 'text' })} /> | ||
<textarea {...conform.textarea(category.config)} /> | ||
<select {...conform.select(category.config)}>{/* ... */}</select> | ||
</fieldset> | ||
); | ||
} | ||
``` | ||
```tsx | ||
import { useForm } from '@conform-to/react'; | ||
This is equivalent to: | ||
function Example() { | ||
const [form, { title, description, category }] = useForm(); | ||
```tsx | ||
function RandomForm() { | ||
const ref = useRef(); | ||
const { category } = useFieldset(ref); | ||
return ( | ||
<fieldset ref={ref}> | ||
<form {...form.props}> | ||
<input | ||
type="text" | ||
name={category.config.name} | ||
form={category.config.form} | ||
defaultValue={category.config.defaultValue} | ||
requried={category.config.required} | ||
minLength={category.config.minLength} | ||
maxLength={category.config.maxLength} | ||
min={category.config.min} | ||
max={category.config.max} | ||
multiple={category.config.multiple} | ||
pattern={category.config.pattern} | ||
> | ||
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} | ||
/> | ||
<textarea | ||
name={category.config.name} | ||
form={category.config.form} | ||
defaultValue={category.config.defaultValue} | ||
requried={category.config.required} | ||
minLength={category.config.minLength} | ||
maxLength={category.config.maxLength} | ||
name={description.config.name} | ||
form={description.config.form} | ||
defaultValue={description.config.defaultValue} | ||
requried={description.config.required} | ||
minLength={description.config.minLength} | ||
maxLength={description.config.maxLength} | ||
/> | ||
@@ -572,3 +391,3 @@ <select | ||
</select> | ||
</fieldset> | ||
</form> | ||
); | ||
@@ -578,7 +397,118 @@ } | ||
After: | ||
```tsx | ||
import { useForm, conform } from '@conform-to/react'; | ||
function Example() { | ||
const [form, { title, description, category }] = useForm(); | ||
return ( | ||
<form {...form.props}> | ||
<input {...conform.input(title.config, { type: 'text' })} /> | ||
<textarea {...conform.textarea(description.config)} /> | ||
<select {...conform.select(category.config)}>{/* ... */}</select> | ||
</form> | ||
); | ||
} | ||
``` | ||
--- | ||
### list | ||
It provides serveral helpers to configure a command button for [modifying a list](/docs/commands.md#modifying-a-list). | ||
```tsx | ||
import { list } from '@conform-to/react'; | ||
function Example() { | ||
return ( | ||
<form> | ||
{/* To append a new row with optional defaultValue */} | ||
<button {...list.append('name', { defaultValue })}>Append</button> | ||
{/* To prepend a new row with optional defaultValue */} | ||
<button {...list.prepend('name', { defaultValue })}>Prepend</button> | ||
{/* To remove a row by index */} | ||
<button {...list.remove('name', { index })}>Remove</button> | ||
{/* To replace a row with another defaultValue */} | ||
<button {...list.replace('name', { index, defaultValue })}> | ||
Replace | ||
</button> | ||
{/* To reorder a particular row to an another index */} | ||
<button {...list.reorder('name', { from, to })}>Reorder</button> | ||
</form> | ||
); | ||
} | ||
``` | ||
--- | ||
### validate | ||
It returns the properties required to configure a command button for [validation](/docs/commands.md#validation). | ||
```tsx | ||
import { validate } from '@conform-to/react'; | ||
function Example() { | ||
return ( | ||
<form> | ||
{/* To validate a single field by name */} | ||
<button {...validate('email')}>Validate email</button> | ||
{/* To validate the whole form */} | ||
<button {...validate()}>Validate</button> | ||
</form> | ||
); | ||
} | ||
``` | ||
--- | ||
### requestCommand | ||
It lets you [trigger a command](/docs/commands.md#triggering-a-command) without requiring users to click on a button. It supports both [list](#list) and [validate](#validate) command. | ||
```tsx | ||
import { | ||
useForm, | ||
useFieldList, | ||
conform, | ||
list, | ||
requestCommand, | ||
} from '@conform-to/react'; | ||
import DragAndDrop from 'awesome-dnd-example'; | ||
export default function Todos() { | ||
const [form, { tasks }] = useForm(); | ||
const taskList = useFieldList(form.ref, tasks.config); | ||
const handleDrop = (from, to) => | ||
requestCommand(form.ref.current, list.reorder({ from, to })); | ||
return ( | ||
<form {...form.props}> | ||
<DragAndDrop onDrop={handleDrop}> | ||
{taskList.map((task, index) => ( | ||
<div key={task.key}> | ||
<input {...conform.input(task.config)} /> | ||
</div> | ||
))} | ||
</DragAndDrop> | ||
<button>Save</button> | ||
</form> | ||
); | ||
} | ||
``` | ||
--- | ||
### getFormElements | ||
It returns all _input_ / _select_ / _textarea_ or _button_ in the forms. Useful when looping through the form elements to validate each field. | ||
It returns all _input_ / _select_ / _textarea_ or _button_ in the forms. Useful when looping through the form elements to validate each field manually. | ||
@@ -589,3 +519,3 @@ ```tsx | ||
export default function LoginForm() { | ||
const form = useForm({ | ||
const [form] = useForm({ | ||
onValidate({ form, formData }) { | ||
@@ -592,0 +522,0 @@ const submission = parse(formData); |
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
1823
85768
591
+ Added@conform-to/dom@0.5.0(transitive)
- Removed@conform-to/dom@0.5.0-pre.0(transitive)
Updated@conform-to/dom@0.5.0