Huge News!Announcing our $40M Series B led by Abstract Ventures.Learn More
Socket
Sign inDemoInstall
Socket

@conform-to/react

Package Overview
Dependencies
Maintainers
1
Versions
66
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

@conform-to/react - npm Package Compare versions

Comparing version 0.5.0-pre.0 to 0.5.0

3

helpers.d.ts
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 {

20

helpers.js

@@ -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 {

@@ -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';

@@ -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": {

@@ -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);

SocketSocket SOC 2 Logo

Product

  • Package Alerts
  • Integrations
  • Docs
  • Pricing
  • FAQ
  • Roadmap
  • Changelog

Packages

npm

Stay in touch

Get open source security insights delivered straight into your inbox.


  • Terms
  • Privacy
  • Security

Made with ⚡️ by Socket Inc