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.2.0 to 0.3.0-pre.0

8

helpers.d.ts

@@ -1,8 +0,8 @@

import { type FieldProps } from '@conform-to/dom';
import { type FieldConfig, type Primitive } from '@conform-to/dom';
import { type InputHTMLAttributes, type SelectHTMLAttributes, type TextareaHTMLAttributes } from 'react';
export declare function input<Type extends string | number | Date | boolean | undefined>(props: FieldProps<Type>, { type, value }?: {
export declare function input<Schema extends Primitive>(config: FieldConfig<Schema>, { type, value }?: {
type?: string;
value?: string;
}): InputHTMLAttributes<HTMLInputElement>;
export declare function select<T extends any>(props: FieldProps<T>): SelectHTMLAttributes<HTMLSelectElement>;
export declare function textarea<T extends string | undefined>(props: FieldProps<T>): TextareaHTMLAttributes<HTMLTextAreaElement>;
export declare function select<Schema extends Primitive | Array<Primitive>>(config: FieldConfig<Schema>): SelectHTMLAttributes<HTMLSelectElement>;
export declare function textarea<Schema extends Primitive>(config: FieldConfig<Schema>): TextareaHTMLAttributes<HTMLTextAreaElement>;

@@ -5,3 +5,3 @@ 'use strict';

function input(props) {
function input(config) {
var {

@@ -14,12 +14,12 @@ type,

type,
name: props.name,
form: props.form,
required: props.required,
minLength: props.minLength,
maxLength: props.maxLength,
min: props.min,
max: props.max,
step: props.step,
pattern: props.pattern,
multiple: props.multiple
name: config.name,
form: config.form,
required: config.required,
minLength: config.minLength,
maxLength: config.maxLength,
min: config.min,
max: config.max,
step: config.step,
pattern: config.pattern,
multiple: config.multiple
};

@@ -29,7 +29,5 @@

attributes.value = value !== null && value !== void 0 ? value : 'on';
attributes.defaultChecked = props.defaultValue === attributes.value;
attributes.defaultChecked = config.defaultValue === attributes.value;
} else {
var _props$defaultValue;
attributes.defaultValue = "".concat((_props$defaultValue = props.defaultValue) !== null && _props$defaultValue !== void 0 ? _props$defaultValue : '');
attributes.defaultValue = config.defaultValue;
}

@@ -39,23 +37,23 @@

}
function select(props) {
var _props$defaultValue2;
function select(config) {
var _config$defaultValue;
return {
name: props.name,
form: props.form,
defaultValue: props.multiple ? Array.isArray(props.defaultValue) ? props.defaultValue : [] : "".concat((_props$defaultValue2 = props.defaultValue) !== null && _props$defaultValue2 !== void 0 ? _props$defaultValue2 : ''),
required: props.required,
multiple: props.multiple
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
};
}
function textarea(props) {
var _props$defaultValue3;
function textarea(config) {
var _config$defaultValue2;
return {
name: props.name,
form: props.form,
defaultValue: "".concat((_props$defaultValue3 = props.defaultValue) !== null && _props$defaultValue3 !== void 0 ? _props$defaultValue3 : ''),
required: props.required,
minLength: props.minLength,
maxLength: props.maxLength
name: config.name,
form: config.form,
defaultValue: "".concat((_config$defaultValue2 = config.defaultValue) !== null && _config$defaultValue2 !== void 0 ? _config$defaultValue2 : ''),
required: config.required,
minLength: config.minLength,
maxLength: config.maxLength
};

@@ -62,0 +60,0 @@ }

@@ -1,64 +0,143 @@

import { type FieldProps, type Schema, type FieldsetData } from '@conform-to/dom';
import { type ButtonHTMLAttributes, type FormEventHandler, type FormHTMLAttributes, type RefObject, type ReactElement } from 'react';
import { type FieldConfig, type FieldError, type FieldValue, type FieldElement, type FieldsetConstraint, type ListCommand, type Primitive } from '@conform-to/dom';
import { type InputHTMLAttributes, type FormEvent, type RefObject } from 'react';
export interface FormConfig {
/**
* Decide when the error should be reported initially.
* Default to `onSubmit`
* Define when the error should be reported initially.
* Support "onSubmit", "onChange", "onBlur".
*
* Default to `onSubmit`.
*/
initialReport?: 'onSubmit' | 'onChange' | 'onBlur';
/**
* Native browser report will be used before hydation if it is set to `true`.
* Default to `false`
* Enable native validation before hydation.
*
* Default to `false`.
*/
fallbackNative?: boolean;
/**
* The form could be submitted even if there is invalid input control if it is set to `true`.
* Default to `false`
* Accept form submission regardless of the form validity.
*
* Default to `false`.
*/
noValidate?: boolean;
/**
* The submit handler will be triggered only when the form is valid.
* Or when noValidate is set to `true`
* A function to be called when the form should be (re)validated.
*/
onSubmit?: FormHTMLAttributes<HTMLFormElement>['onSubmit'];
onReset?: FormHTMLAttributes<HTMLFormElement>['onReset'];
validate?: (form: HTMLFormElement, submitter?: HTMLInputElement | HTMLButtonElement | null) => void;
/**
* The submit event handler of the form. It will be called
* only when the form is considered valid.
*/
onSubmit?: (event: FormEvent<HTMLFormElement>) => void;
}
/**
* Properties to be applied to the form element
*/
interface FormProps {
ref: RefObject<HTMLFormElement>;
onSubmit: Required<FormHTMLAttributes<HTMLFormElement>>['onSubmit'];
onReset: Required<FormHTMLAttributes<HTMLFormElement>>['onReset'];
noValidate: Required<FormHTMLAttributes<HTMLFormElement>>['noValidate'];
onSubmit: (event: FormEvent<HTMLFormElement>) => void;
noValidate: boolean;
}
export declare function useForm({ onReset, onSubmit, noValidate, fallbackNative, initialReport, }?: FormConfig): FormProps;
export declare type FieldsetConfig<Type> = Partial<Pick<FieldProps<Type>, 'name' | 'form' | 'defaultValue' | 'error'>>;
interface FieldsetProps {
ref: RefObject<HTMLFieldSetElement>;
/**
* Returns properties required to hook into form events.
* Applied custom validation and define when error should be reported.
*
* @see https://github.com/edmundhung/conform/tree/v0.3.0-pre.0/packages/conform-react/README.md#useform
*/
export declare function useForm(config?: FormConfig): FormProps;
/**
* All the information of the field, including state and config.
*/
export declare type Field<Schema> = {
config: FieldConfig<Schema>;
error?: string;
};
/**
* A set of field information.
*/
export declare type Fieldset<Schema extends Record<string, any>> = {
[Key in keyof Schema]-?: Field<Schema[Key]>;
};
export interface FieldsetConfig<Schema extends Record<string, any>> {
/**
* The prefix used to generate the name of nested fields.
*/
name?: string;
/**
* An object representing the initial value of the fieldset.
*/
defaultValue?: FieldValue<Schema>;
/**
* An object describing the initial error of each field
*/
initialError?: FieldError<Schema>['details'];
/**
* An object describing the constraint of each field
*/
constraint?: FieldsetConstraint<Schema>;
/**
* The id of the form, connecting each field to a form remotely.
*/
form?: string;
onInput: FormEventHandler<HTMLFieldSetElement>;
onInvalid: FormEventHandler<HTMLFieldSetElement>;
}
export declare function useFieldset<Type extends Record<string, any>>(schema: Schema<Type>, config?: FieldsetConfig<Type>): [FieldsetProps, {
[Key in keyof Type]-?: FieldProps<Type[Key]>;
}];
interface FieldListControl<T> {
prepend(defaultValue?: FieldsetData<T, string>): ButtonHTMLAttributes<HTMLButtonElement>;
append(defaultValue?: FieldsetData<T, string>): ButtonHTMLAttributes<HTMLButtonElement>;
replace(index: number, defaultValue: FieldsetData<T, string>): ButtonHTMLAttributes<HTMLButtonElement>;
remove(index: number): ButtonHTMLAttributes<HTMLButtonElement>;
reorder(fromIndex: number, toIndex: number): ButtonHTMLAttributes<HTMLButtonElement>;
/**
* Returns all the information about the fieldset.
*
* @see https://github.com/edmundhung/conform/tree/v0.3.0-pre.0/packages/conform-react/README.md#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 ControlButtonProps {
name?: string;
value?: string;
form?: string;
formNoValidate: true;
}
export declare function useFieldList<Payload>(props: FieldProps<Array<Payload>>): [
declare type CommandPayload<Schema, Type extends ListCommand<FieldValue<Schema>>['type']> = Extract<ListCommand<FieldValue<Schema>>, {
type: Type;
}>['payload'];
/**
* A group of helpers for configuring a list control button
*/
interface ListControl<Schema> {
prepend(payload?: CommandPayload<Schema, 'prepend'>): ControlButtonProps;
append(payload?: CommandPayload<Schema, 'append'>): ControlButtonProps;
replace(payload: CommandPayload<Schema, 'replace'>): ControlButtonProps;
remove(payload: CommandPayload<Schema, 'remove'>): ControlButtonProps;
reorder(payload: CommandPayload<Schema, 'reorder'>): ControlButtonProps;
}
/**
* Returns a list of key and config, with a group of helpers
* configuring buttons for list manipulation
*
* @see https://github.com/edmundhung/conform/tree/v0.3.0-pre.0/packages/conform-react/README.md#usefieldlist
*/
export declare function useFieldList<Payload = any>(ref: RefObject<HTMLFormElement | HTMLFieldSetElement>, config: FieldConfig<Array<Payload>>): [
Array<{
key: string;
props: FieldProps<Payload>;
config: FieldConfig<Payload>;
}>,
FieldListControl<Payload>
ListControl<Payload>
];
interface ShadowInputProps extends InputHTMLAttributes<HTMLInputElement> {
ref: RefObject<HTMLInputElement>;
}
interface InputControl {
value: string;
onChange: (value: string) => void;
onChange: (eventOrValue: {
target: {
value: string;
};
} | string) => void;
onBlur: () => void;
onInvalid: (event: FormEvent<FieldElement>) => void;
}
export declare function useControlledInput<T extends string | number | Date | undefined>(field: FieldProps<T>): [ReactElement, InputControl];
/**
* 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.
*
* @see https://github.com/edmundhung/conform/tree/v0.3.0-pre.0/packages/conform-react/README.md#usecontrolledinput
*/
export declare function useControlledInput<Schema extends Primitive = Primitive>(field: FieldConfig<Schema>): [ShadowInputProps, InputControl];
export {};

@@ -8,412 +8,488 @@ 'use strict';

var react = require('react');
var helpers = require('./helpers.js');
/**
* Returns properties required to hook into form events.
* Applied custom validation and define when error should be reported.
*
* @see https://github.com/edmundhung/conform/tree/v0.3.0-pre.0/packages/conform-react/README.md#useform
*/
function useForm() {
var config = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {};
var {
onReset,
onSubmit,
noValidate = false,
fallbackNative = false,
initialReport = 'onSubmit'
} = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {};
validate
} = config;
var ref = react.useRef(null);
var [formNoValidate, setFormNoValidate] = react.useState(noValidate || !fallbackNative);
var handleSubmit = event => {
if (!noValidate) {
dom.setFieldState(event.currentTarget, {
touched: true
});
if (!dom.shouldSkipValidate(event.nativeEvent) && !event.currentTarget.reportValidity()) {
return event.preventDefault();
}
}
onSubmit === null || onSubmit === void 0 ? void 0 : onSubmit(event);
};
var handleReset = event => {
dom.setFieldState(event.currentTarget, {
touched: false
});
onReset === null || onReset === void 0 ? void 0 : onReset(event);
};
var [noValidate, setNoValidate] = react.useState(config.noValidate || !config.fallbackNative);
react.useEffect(() => {
setFormNoValidate(true);
setNoValidate(true);
}, []);
react.useEffect(() => {
if (noValidate) {
return;
}
// Initialize form validation messages
if (ref.current) {
validate === null || validate === void 0 ? void 0 : validate(ref.current);
} // Revalidate the form when input value is changed
var handleChange = event => {
var _event$target;
if (!ref.current || !dom.isFieldElement(event.target) || ((_event$target = event.target) === null || _event$target === void 0 ? void 0 : _event$target.form) !== ref.current) {
var handleInput = event => {
var field = event.target;
var form = ref.current;
if (!form || !dom.isFieldElement(field) || field.form !== form) {
return;
}
if (initialReport === 'onChange') {
dom.setFieldState(event.target, {
touched: true
});
validate === null || validate === void 0 ? void 0 : validate(form);
if (!config.noValidate) {
if (config.initialReport === 'onChange') {
field.dataset.conformTouched = 'true';
} // Field validity might be changed due to cross reference
for (var _field of form.elements) {
if (dom.isFieldElement(_field) && _field.dataset.conformTouched) {
// Report latest error for all touched fields
_field.checkValidity();
}
}
}
dom.reportValidity(ref.current);
};
var handleBlur = event => {
var _event$target2;
var field = event.target;
var form = ref.current;
if (!ref.current || !dom.isFieldElement(event.target) || ((_event$target2 = event.target) === null || _event$target2 === void 0 ? void 0 : _event$target2.form) !== ref.current) {
if (!form || !dom.isFieldElement(field) || field.form !== form || config.noValidate || config.initialReport !== 'onBlur') {
return;
}
if (initialReport === 'onBlur') {
dom.setFieldState(event.target, {
touched: true
});
field.dataset.conformTouched = 'true';
field.reportValidity();
};
var handleReset = event => {
var form = ref.current;
if (!form || event.target !== form) {
return;
} // Reset all field state
for (var field of form.elements) {
if (dom.isFieldElement(field)) {
delete field.dataset.conformTouched;
}
}
/**
* The reset event is triggered before form reset happens.
* This make sure the form to be revalidated with initial values.
*/
dom.reportValidity(ref.current);
setTimeout(() => {
validate === null || validate === void 0 ? void 0 : validate(form);
}, 0);
};
/**
* The input event handler will be triggered in capturing phase in order to
* allow follow-up action in the bubble phase based on the latest validity
* E.g. `useFieldset` reset the error of valid field after checking the
* validity in the bubble phase.
*/
document.addEventListener('input', handleChange);
document.addEventListener('focusout', handleBlur);
document.addEventListener('input', handleInput, true);
document.addEventListener('blur', handleBlur, true);
document.addEventListener('reset', handleReset);
return () => {
document.removeEventListener('input', handleChange);
document.removeEventListener('focusout', handleBlur);
document.removeEventListener('input', handleInput, true);
document.removeEventListener('blur', handleBlur, true);
document.removeEventListener('reset', handleReset);
};
}, [noValidate, initialReport]);
}, [validate, config.initialReport, config.noValidate]);
return {
ref,
onSubmit: handleSubmit,
onReset: handleReset,
noValidate: formNoValidate
};
}
function useFieldset(schema) {
var config = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {};
var ref = react.useRef(null);
var [errorMessage, dispatch] = react.useReducer((state, action) => {
switch (action.type) {
case 'report':
{
var {
key,
message
} = action.payload;
noValidate,
if (state[key] === message) {
return state;
onSubmit(event) {
var form = event.currentTarget;
var nativeEvent = event.nativeEvent;
var submitter = nativeEvent.submitter instanceof HTMLButtonElement || nativeEvent.submitter instanceof HTMLInputElement ? nativeEvent.submitter : null; // Validating the form with the submitter value
validate === null || validate === void 0 ? void 0 : validate(form, submitter);
/**
* It checks defaultPrevented to confirm if the submission is intentional
* This is utilized by `useFieldList` to modify the list state when the submit
* event is captured and revalidate the form with new fields without triggering
* a form submission at the same time.
*/
if (!config.noValidate && !(submitter !== null && submitter !== void 0 && submitter.formNoValidate) && !event.defaultPrevented) {
// Mark all fields as touched
for (var field of form.elements) {
if (dom.isFieldElement(field)) {
field.dataset.conformTouched = 'true';
}
} // Check the validity of the form
return _rollupPluginBabelHelpers.objectSpread2(_rollupPluginBabelHelpers.objectSpread2({}, state), {}, {
[key]: message
});
if (!event.currentTarget.reportValidity()) {
event.preventDefault();
}
}
case 'migrate':
{
var {
keys,
error
} = action.payload;
var nextState = state;
if (!event.defaultPrevented) {
var _config$onSubmit;
for (var _key of Object.keys(keys)) {
var prevError = state[_key];
var nextError = error === null || error === void 0 ? void 0 : error[_key];
(_config$onSubmit = config.onSubmit) === null || _config$onSubmit === void 0 ? void 0 : _config$onSubmit.call(config, event);
}
}
if (typeof nextError === 'string' && prevError !== nextError) {
return _rollupPluginBabelHelpers.objectSpread2(_rollupPluginBabelHelpers.objectSpread2({}, nextState), {}, {
[_key]: nextError
});
}
}
};
}
/**
* All the information of the field, including state and config.
*/
return nextState;
}
function useFieldset(ref, config) {
var [error, setError] = react.useState(() => {
var result = {};
case 'cleanup':
{
var {
fieldset
} = action.payload;
var updates = [];
for (var [key, _error] of Object.entries((_config$initialError = config === null || config === void 0 ? void 0 : config.initialError) !== null && _config$initialError !== void 0 ? _config$initialError : {})) {
var _config$initialError;
for (var [_key2, _message] of Object.entries(state)) {
if (!_message) {
continue;
}
if (_error !== null && _error !== void 0 && _error.message) {
result[key] = _error.message;
}
}
var fields = dom.getFieldElements(fieldset, _key2);
return result;
});
react.useEffect(() => {
/**
* Reset the error state of each field if its validity is changed.
*
* This is a workaround as no official way is provided to notify
* when the validity of the field is changed from `invalid` to `valid`.
*/
var resetError = form => {
setError(prev => {
var next = prev;
if (fields.every(field => field.validity.valid)) {
updates.push([_key2, '']);
for (var field of form.elements) {
if (dom.isFieldElement(field)) {
var key = dom.getKey(field.name, config === null || config === void 0 ? void 0 : config.name);
if (key) {
var _next$key, _next;
var prevMessage = (_next$key = (_next = next) === null || _next === void 0 ? void 0 : _next[key]) !== null && _next$key !== void 0 ? _next$key : '';
var nextMessage = field.validationMessage;
/**
* Techincally, checking prevMessage not being empty while nextMessage being empty
* is sufficient for our usecase. It checks if the message is changed instead to allow
* the hook to be useful independently.
*/
if (prevMessage !== '' && prevMessage !== nextMessage) {
next = _rollupPluginBabelHelpers.objectSpread2(_rollupPluginBabelHelpers.objectSpread2({}, next), {}, {
[key]: nextMessage
});
}
}
}
}
if (updates.length === 0) {
return state;
}
return next;
});
};
return _rollupPluginBabelHelpers.objectSpread2(_rollupPluginBabelHelpers.objectSpread2({}, state), Object.fromEntries(updates));
}
var handleInput = event => {
var form = dom.getFormElement(ref.current);
var field = event.target;
case 'reset':
{
return {};
}
}
}, {}, () => Object.fromEntries(Object.keys(schema.fields).reduce((result, name) => {
var _config$error;
if (!form || !dom.isFieldElement(field) || field.form !== form) {
return;
}
var error = (_config$error = config.error) === null || _config$error === void 0 ? void 0 : _config$error[name];
resetError(form);
};
if (typeof error === 'string') {
result.push([name, error]);
}
var invalidHandler = event => {
var form = dom.getFormElement(ref.current);
var field = event.target;
return result;
}, [])));
react.useEffect(() => {
var _schema$validate;
if (!form || !dom.isFieldElement(field) || field.form !== form) {
return;
}
var fieldset = ref.current;
var key = dom.getKey(field.name, config === null || config === void 0 ? void 0 : config.name); // Update the error only if the field belongs to the fieldset
if (!fieldset) {
console.warn('No fieldset ref found; You must pass the fieldsetProps to the fieldset element');
return;
}
if (key) {
setError(prev => {
var _prev$key;
if (!(fieldset !== null && fieldset !== void 0 && fieldset.form)) {
console.warn('No form element is linked to the fieldset; Do you forgot setting the form attribute?');
}
var prevMessage = (_prev$key = prev === null || prev === void 0 ? void 0 : prev[key]) !== null && _prev$key !== void 0 ? _prev$key : '';
(_schema$validate = schema.validate) === null || _schema$validate === void 0 ? void 0 : _schema$validate.call(schema, fieldset);
dispatch({
type: 'cleanup',
payload: {
fieldset
if (prevMessage === field.validationMessage) {
return prev;
}
return _rollupPluginBabelHelpers.objectSpread2(_rollupPluginBabelHelpers.objectSpread2({}, prev), {}, {
[key]: field.validationMessage
});
});
event.preventDefault();
}
});
};
var resetHandler = e => {
if (e.target !== fieldset.form) {
var submitHandler = event => {
var form = dom.getFormElement(ref.current);
if (!form || event.target !== form) {
return;
}
} // This helps resetting error that fullfilled by the submitter
dispatch({
type: 'reset'
});
setTimeout(() => {
var _schema$validate2;
// Delay revalidation until reset is completed
(_schema$validate2 = schema.validate) === null || _schema$validate2 === void 0 ? void 0 : _schema$validate2.call(schema, fieldset);
}, 0);
resetError(form);
};
var resetHandler = event => {
var form = dom.getFormElement(ref.current);
if (!form || event.target !== form) {
return;
}
setError({});
};
document.addEventListener('input', handleInput); // The invalid event does not bubble and so listening on the capturing pharse is needed
document.addEventListener('invalid', invalidHandler, true);
document.addEventListener('submit', submitHandler);
document.addEventListener('reset', resetHandler);
return () => {
document.removeEventListener('input', handleInput);
document.removeEventListener('invalid', invalidHandler, true);
document.removeEventListener('submit', submitHandler);
document.removeEventListener('reset', resetHandler);
};
}, // eslint-disable-next-line react-hooks/exhaustive-deps
[schema.validate]);
}, [ref, config === null || config === void 0 ? void 0 : config.name]);
react.useEffect(() => {
dispatch({
type: 'migrate',
payload: {
keys: Object.keys(schema.fields),
error: config.error
}
});
}, [config.error, schema.fields]);
return [{
ref,
name: config.name,
form: config.form,
setError(prev => {
var next = prev;
onInput(e) {
var _schema$validate3;
for (var [key, _error2] of Object.entries((_config$initialError2 = config === null || config === void 0 ? void 0 : config.initialError) !== null && _config$initialError2 !== void 0 ? _config$initialError2 : {})) {
var _config$initialError2;
var fieldset = e.currentTarget;
(_schema$validate3 = schema.validate) === null || _schema$validate3 === void 0 ? void 0 : _schema$validate3.call(schema, fieldset);
dispatch({
type: 'cleanup',
payload: {
fieldset
if (next[key] !== (_error2 === null || _error2 === void 0 ? void 0 : _error2.message)) {
var _error2$message;
next = _rollupPluginBabelHelpers.objectSpread2(_rollupPluginBabelHelpers.objectSpread2({}, next), {}, {
[key]: (_error2$message = _error2 === null || _error2 === void 0 ? void 0 : _error2.message) !== null && _error2$message !== void 0 ? _error2$message : ''
});
}
});
},
}
onInvalid(e) {
var element = dom.isFieldElement(e.target) ? e.target : null;
var key = Object.keys(schema.fields).find(key => (element === null || element === void 0 ? void 0 : element.name) === dom.getName([e.currentTarget.name, key]));
return next;
});
}, [config === null || config === void 0 ? void 0 : config.name, config === null || config === void 0 ? void 0 : config.initialError]);
/**
* This allows us constructing the field at runtime as we have no information
* about which fields would be available. The proxy will also help tracking
* the usage of each field for optimization in the future.
*/
if (!element || !key) {
return new Proxy({}, {
get(_target, key) {
var _constraint, _config$defaultValue, _config$initialError3, _config$initialError4, _error$key;
if (typeof key !== 'string') {
return;
} // Disable browser report
}
e.preventDefault();
dispatch({
type: 'report',
payload: {
key,
message: element.validationMessage
}
});
var constraint = config === null || config === void 0 ? void 0 : (_constraint = config.constraint) === null || _constraint === void 0 ? void 0 : _constraint[key];
var field = {
config: _rollupPluginBabelHelpers.objectSpread2({
name: config !== null && config !== void 0 && config.name ? "".concat(config.name, ".").concat(key) : key,
form: config === null || config === void 0 ? void 0 : config.form,
defaultValue: config === null || config === void 0 ? void 0 : (_config$defaultValue = config.defaultValue) === null || _config$defaultValue === void 0 ? void 0 : _config$defaultValue[key],
initialError: config === null || config === void 0 ? void 0 : (_config$initialError3 = config.initialError) === null || _config$initialError3 === void 0 ? void 0 : (_config$initialError4 = _config$initialError3[key]) === null || _config$initialError4 === void 0 ? void 0 : _config$initialError4.details
}, constraint),
error: (_error$key = error === null || error === void 0 ? void 0 : error[key]) !== null && _error$key !== void 0 ? _error$key : ''
};
return field;
}
}, dom.getFieldProps(schema, _rollupPluginBabelHelpers.objectSpread2(_rollupPluginBabelHelpers.objectSpread2({}, config), {}, {
error: Object.assign({}, config.error, errorMessage)
}))];
});
}
function useFieldList(props) {
/**
* Returns a list of key and config, with a group of helpers
* configuring buttons for list manipulation
*
* @see https://github.com/edmundhung/conform/tree/v0.3.0-pre.0/packages/conform-react/README.md#usefieldlist
*/
function useFieldList(ref, config) {
var [entries, setEntries] = react.useState(() => {
var _props$defaultValue;
var _config$defaultValue2;
return Object.entries((_props$defaultValue = props.defaultValue) !== null && _props$defaultValue !== void 0 ? _props$defaultValue : [undefined]);
return Object.entries((_config$defaultValue2 = config.defaultValue) !== null && _config$defaultValue2 !== void 0 ? _config$defaultValue2 : [undefined]);
});
var list = entries.map((_ref, index) => {
var _props$defaultValue2, _props$error;
var _config$defaultValue3, _config$initialError5, _config$initialError6;
var [key, defaultValue] = _ref;
return {
key: "".concat(key),
props: _rollupPluginBabelHelpers.objectSpread2(_rollupPluginBabelHelpers.objectSpread2({}, props), {}, {
name: "".concat(props.name, "[").concat(index, "]"),
defaultValue: defaultValue !== null && defaultValue !== void 0 ? defaultValue : (_props$defaultValue2 = props.defaultValue) === null || _props$defaultValue2 === void 0 ? void 0 : _props$defaultValue2[index],
error: (_props$error = props.error) === null || _props$error === void 0 ? void 0 : _props$error[index],
multiple: false
key,
config: _rollupPluginBabelHelpers.objectSpread2(_rollupPluginBabelHelpers.objectSpread2({}, config), {}, {
name: "".concat(config.name, "[").concat(index, "]"),
defaultValue: defaultValue !== null && defaultValue !== void 0 ? defaultValue : (_config$defaultValue3 = config.defaultValue) === null || _config$defaultValue3 === void 0 ? void 0 : _config$defaultValue3[index],
initialError: (_config$initialError5 = config.initialError) === null || _config$initialError5 === void 0 ? void 0 : (_config$initialError6 = _config$initialError5[index]) === null || _config$initialError6 === void 0 ? void 0 : _config$initialError6.details
})
};
});
var controls = {
prepend(defaultValue) {
return _rollupPluginBabelHelpers.objectSpread2(_rollupPluginBabelHelpers.objectSpread2({}, dom.getControlButtonProps(props.name, 'prepend', {
defaultValue
})), {}, {
onClick(e) {
setEntries(entries => dom.applyControlCommand([...entries], 'prepend', {
defaultValue: ["".concat(Date.now()), defaultValue]
}));
e.preventDefault();
}
/***
* This use proxy to capture all information about the command and
* have it encoded in the value.
*/
});
},
var control = new Proxy({}, {
get(_target, type) {
return function () {
var payload = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {};
return {
name: dom.listCommandKey,
value: dom.serializeListCommand(config.name, {
type,
payload
}),
form: config.form,
formNoValidate: true
};
};
}
append(defaultValue) {
return _rollupPluginBabelHelpers.objectSpread2(_rollupPluginBabelHelpers.objectSpread2({}, dom.getControlButtonProps(props.name, 'append', {
defaultValue
})), {}, {
onClick(e) {
setEntries(entries => dom.applyControlCommand([...entries], 'append', {
defaultValue: ["".concat(Date.now()), defaultValue]
}));
e.preventDefault();
}
});
react.useEffect(() => {
setEntries(prevEntries => {
var _config$defaultValue4;
});
},
var nextEntries = Object.entries((_config$defaultValue4 = config.defaultValue) !== null && _config$defaultValue4 !== void 0 ? _config$defaultValue4 : [undefined]);
replace(index, defaultValue) {
return _rollupPluginBabelHelpers.objectSpread2(_rollupPluginBabelHelpers.objectSpread2({}, dom.getControlButtonProps(props.name, 'replace', {
index,
defaultValue
})), {}, {
onClick(e) {
setEntries(entries => dom.applyControlCommand([...entries], 'replace', {
defaultValue: ["".concat(Date.now()), defaultValue],
index
}));
e.preventDefault();
}
if (prevEntries.length !== nextEntries.length) {
return nextEntries;
}
});
},
for (var i = 0; i < prevEntries.length; i++) {
var [prevKey, prevValue] = prevEntries[i];
var [nextKey, nextValue] = nextEntries[i];
remove(index) {
return _rollupPluginBabelHelpers.objectSpread2(_rollupPluginBabelHelpers.objectSpread2({}, dom.getControlButtonProps(props.name, 'remove', {
index
})), {}, {
onClick(e) {
setEntries(entries => dom.applyControlCommand([...entries], 'remove', {
index
}));
e.preventDefault();
if (prevKey !== nextKey || prevValue !== nextValue) {
return nextEntries;
}
} // No need to rerender in this case
});
},
reorder(fromIndex, toIndex) {
return _rollupPluginBabelHelpers.objectSpread2(_rollupPluginBabelHelpers.objectSpread2({}, dom.getControlButtonProps(props.name, 'reorder', {
from: fromIndex,
to: toIndex
})), {}, {
onClick(e) {
if (fromIndex !== toIndex) {
setEntries(entries => dom.applyControlCommand([...entries], 'reorder', {
from: fromIndex,
to: toIndex
}));
}
return prevEntries;
});
e.preventDefault();
}
var submitHandler = event => {
var form = dom.getFormElement(ref.current);
});
}
if (!form || event.target !== form || !(event.submitter instanceof HTMLButtonElement) || event.submitter.name !== dom.listCommandKey) {
return;
}
};
react.useEffect(() => {
var _props$defaultValue3;
var [name, command] = dom.parseListCommand(event.submitter.value);
setEntries(Object.entries((_props$defaultValue3 = props.defaultValue) !== null && _props$defaultValue3 !== void 0 ? _props$defaultValue3 : [undefined]));
}, [props.defaultValue]);
return [list, controls];
if (name !== config.name) {
// Ensure the scope of the listener are limited to specific field name
return;
}
switch (command.type) {
case 'append':
case 'prepend':
case 'replace':
command.payload.defaultValue = ["".concat(Date.now()), command.payload.defaultValue];
break;
}
setEntries(entries => dom.updateList([...(entries !== null && entries !== void 0 ? entries : [])], command));
event.preventDefault();
};
var resetHandler = event => {
var _config$defaultValue5;
var form = dom.getFormElement(ref.current);
if (!form || event.target !== form) {
return;
}
setEntries(Object.entries((_config$defaultValue5 = config.defaultValue) !== null && _config$defaultValue5 !== void 0 ? _config$defaultValue5 : []));
};
document.addEventListener('submit', submitHandler, true);
document.addEventListener('reset', resetHandler);
return () => {
document.removeEventListener('submit', submitHandler, true);
document.removeEventListener('reset', resetHandler);
};
}, [ref, config.name, config.defaultValue]);
return [list, control];
}
/**
* 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.
*
* @see https://github.com/edmundhung/conform/tree/v0.3.0-pre.0/packages/conform-react/README.md#usecontrolledinput
*/
function useControlledInput(field) {
var _ref$current$value, _ref$current, _field$defaultValue;
var _field$defaultValue;
var ref = react.useRef(null);
var input = react.useMemo(() => /*#__PURE__*/react.createElement('input', {
ref,
name: field.name,
form: field.form,
defaultValue: field.defaultValue,
required: field.required,
minLength: field.minLength,
maxLength: field.maxLength,
min: field.min,
max: field.max,
step: field.step,
pattern: field.pattern,
hidden: true,
'aria-hidden': true
}), [field.name, field.form, field.defaultValue, field.required, field.minLength, field.maxLength, field.min, field.max, field.step, field.pattern]);
return [input, {
value: (_ref$current$value = (_ref$current = ref.current) === null || _ref$current === void 0 ? void 0 : _ref$current.value) !== null && _ref$current$value !== void 0 ? _ref$current$value : "".concat((_field$defaultValue = field.defaultValue) !== null && _field$defaultValue !== void 0 ? _field$defaultValue : ''),
onChange: value => {
if (!ref.current) {
return;
}
var [value, setValue] = react.useState("".concat((_field$defaultValue = field.defaultValue) !== null && _field$defaultValue !== void 0 ? _field$defaultValue : ''));
ref.current.value = value;
ref.current.dispatchEvent(new InputEvent('input', {
bubbles: true
}));
},
onBlur: () => {
var _ref$current2;
var handleChange = eventOrValue => {
if (!ref.current) {
return;
}
(_ref$current2 = ref.current) === null || _ref$current2 === void 0 ? void 0 : _ref$current2.dispatchEvent(new FocusEvent('focusout', {
bubbles: true
}));
}
var newValue = typeof eventOrValue === 'string' ? eventOrValue : eventOrValue.target.value;
ref.current.value = newValue;
ref.current.dispatchEvent(new InputEvent('input', {
bubbles: true
}));
setValue(newValue);
};
var handleBlur = () => {
var _ref$current;
(_ref$current = ref.current) === null || _ref$current === void 0 ? void 0 : _ref$current.dispatchEvent(new FocusEvent('blur', {
bubbles: true
}));
};
var handleInvalid = event => {
event.preventDefault();
};
return [_rollupPluginBabelHelpers.objectSpread2({
ref,
hidden: true
}, helpers.input(field, {
type: 'text'
})), {
value,
onChange: handleChange,
onBlur: handleBlur,
onInvalid: handleInvalid
}];

@@ -420,0 +496,0 @@ }

@@ -1,3 +0,3 @@

export { type FieldProps, type FormState, type Schema, type Submission, parse, getFieldElements, } from '@conform-to/dom';
export { type FormState, type FieldsetConstraint, type Schema, type Submission, createSubmission, createValidate, } from '@conform-to/dom';
export * from './hooks';
export * as conform from './helpers';

@@ -11,9 +11,9 @@ 'use strict';

Object.defineProperty(exports, 'getFieldElements', {
Object.defineProperty(exports, 'createSubmission', {
enumerable: true,
get: function () { return dom.getFieldElements; }
get: function () { return dom.createSubmission; }
});
Object.defineProperty(exports, 'parse', {
Object.defineProperty(exports, 'createValidate', {
enumerable: true,
get: function () { return dom.parse; }
get: function () { return dom.createValidate; }
});

@@ -20,0 +20,0 @@ exports.useControlledInput = hooks.useControlledInput;

@@ -1,2 +0,2 @@

function input(props) {
function input(config) {
var {

@@ -9,12 +9,12 @@ type,

type,
name: props.name,
form: props.form,
required: props.required,
minLength: props.minLength,
maxLength: props.maxLength,
min: props.min,
max: props.max,
step: props.step,
pattern: props.pattern,
multiple: props.multiple
name: config.name,
form: config.form,
required: config.required,
minLength: config.minLength,
maxLength: config.maxLength,
min: config.min,
max: config.max,
step: config.step,
pattern: config.pattern,
multiple: config.multiple
};

@@ -24,7 +24,5 @@

attributes.value = value !== null && value !== void 0 ? value : 'on';
attributes.defaultChecked = props.defaultValue === attributes.value;
attributes.defaultChecked = config.defaultValue === attributes.value;
} else {
var _props$defaultValue;
attributes.defaultValue = "".concat((_props$defaultValue = props.defaultValue) !== null && _props$defaultValue !== void 0 ? _props$defaultValue : '');
attributes.defaultValue = config.defaultValue;
}

@@ -34,23 +32,23 @@

}
function select(props) {
var _props$defaultValue2;
function select(config) {
var _config$defaultValue;
return {
name: props.name,
form: props.form,
defaultValue: props.multiple ? Array.isArray(props.defaultValue) ? props.defaultValue : [] : "".concat((_props$defaultValue2 = props.defaultValue) !== null && _props$defaultValue2 !== void 0 ? _props$defaultValue2 : ''),
required: props.required,
multiple: props.multiple
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
};
}
function textarea(props) {
var _props$defaultValue3;
function textarea(config) {
var _config$defaultValue2;
return {
name: props.name,
form: props.form,
defaultValue: "".concat((_props$defaultValue3 = props.defaultValue) !== null && _props$defaultValue3 !== void 0 ? _props$defaultValue3 : ''),
required: props.required,
minLength: props.minLength,
maxLength: props.maxLength
name: config.name,
form: config.form,
defaultValue: "".concat((_config$defaultValue2 = config.defaultValue) !== null && _config$defaultValue2 !== void 0 ? _config$defaultValue2 : ''),
required: config.required,
minLength: config.minLength,
maxLength: config.maxLength
};

@@ -57,0 +55,0 @@ }

import { objectSpread2 as _objectSpread2 } from './_virtual/_rollupPluginBabelHelpers.js';
import { getFieldElements, isFieldElement, getName, getFieldProps, setFieldState, shouldSkipValidate, reportValidity, getControlButtonProps, applyControlCommand } from '@conform-to/dom';
import { useRef, useState, useEffect, useReducer, useMemo, createElement } from 'react';
import { isFieldElement, listCommandKey, serializeListCommand, getFormElement, getKey, parseListCommand, updateList } from '@conform-to/dom';
import { useRef, useState, useEffect } from 'react';
import { input } from './helpers.js';
/**
* Returns properties required to hook into form events.
* Applied custom validation and define when error should be reported.
*
* @see https://github.com/edmundhung/conform/tree/v0.3.0-pre.0/packages/conform-react/README.md#useform
*/
function useForm() {
var config = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {};
var {
onReset,
onSubmit,
noValidate = false,
fallbackNative = false,
initialReport = 'onSubmit'
} = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {};
validate
} = config;
var ref = useRef(null);
var [formNoValidate, setFormNoValidate] = useState(noValidate || !fallbackNative);
var handleSubmit = event => {
if (!noValidate) {
setFieldState(event.currentTarget, {
touched: true
});
if (!shouldSkipValidate(event.nativeEvent) && !event.currentTarget.reportValidity()) {
return event.preventDefault();
}
}
onSubmit === null || onSubmit === void 0 ? void 0 : onSubmit(event);
};
var handleReset = event => {
setFieldState(event.currentTarget, {
touched: false
});
onReset === null || onReset === void 0 ? void 0 : onReset(event);
};
var [noValidate, setNoValidate] = useState(config.noValidate || !config.fallbackNative);
useEffect(() => {
setFormNoValidate(true);
setNoValidate(true);
}, []);
useEffect(() => {
if (noValidate) {
return;
}
// Initialize form validation messages
if (ref.current) {
validate === null || validate === void 0 ? void 0 : validate(ref.current);
} // Revalidate the form when input value is changed
var handleChange = event => {
var _event$target;
if (!ref.current || !isFieldElement(event.target) || ((_event$target = event.target) === null || _event$target === void 0 ? void 0 : _event$target.form) !== ref.current) {
var handleInput = event => {
var field = event.target;
var form = ref.current;
if (!form || !isFieldElement(field) || field.form !== form) {
return;
}
if (initialReport === 'onChange') {
setFieldState(event.target, {
touched: true
});
validate === null || validate === void 0 ? void 0 : validate(form);
if (!config.noValidate) {
if (config.initialReport === 'onChange') {
field.dataset.conformTouched = 'true';
} // Field validity might be changed due to cross reference
for (var _field of form.elements) {
if (isFieldElement(_field) && _field.dataset.conformTouched) {
// Report latest error for all touched fields
_field.checkValidity();
}
}
}
reportValidity(ref.current);
};
var handleBlur = event => {
var _event$target2;
var field = event.target;
var form = ref.current;
if (!ref.current || !isFieldElement(event.target) || ((_event$target2 = event.target) === null || _event$target2 === void 0 ? void 0 : _event$target2.form) !== ref.current) {
if (!form || !isFieldElement(field) || field.form !== form || config.noValidate || config.initialReport !== 'onBlur') {
return;
}
if (initialReport === 'onBlur') {
setFieldState(event.target, {
touched: true
});
field.dataset.conformTouched = 'true';
field.reportValidity();
};
var handleReset = event => {
var form = ref.current;
if (!form || event.target !== form) {
return;
} // Reset all field state
for (var field of form.elements) {
if (isFieldElement(field)) {
delete field.dataset.conformTouched;
}
}
/**
* The reset event is triggered before form reset happens.
* This make sure the form to be revalidated with initial values.
*/
reportValidity(ref.current);
setTimeout(() => {
validate === null || validate === void 0 ? void 0 : validate(form);
}, 0);
};
/**
* The input event handler will be triggered in capturing phase in order to
* allow follow-up action in the bubble phase based on the latest validity
* E.g. `useFieldset` reset the error of valid field after checking the
* validity in the bubble phase.
*/
document.addEventListener('input', handleChange);
document.addEventListener('focusout', handleBlur);
document.addEventListener('input', handleInput, true);
document.addEventListener('blur', handleBlur, true);
document.addEventListener('reset', handleReset);
return () => {
document.removeEventListener('input', handleChange);
document.removeEventListener('focusout', handleBlur);
document.removeEventListener('input', handleInput, true);
document.removeEventListener('blur', handleBlur, true);
document.removeEventListener('reset', handleReset);
};
}, [noValidate, initialReport]);
}, [validate, config.initialReport, config.noValidate]);
return {
ref,
onSubmit: handleSubmit,
onReset: handleReset,
noValidate: formNoValidate
};
}
function useFieldset(schema) {
var config = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {};
var ref = useRef(null);
var [errorMessage, dispatch] = useReducer((state, action) => {
switch (action.type) {
case 'report':
{
var {
key,
message
} = action.payload;
noValidate,
if (state[key] === message) {
return state;
onSubmit(event) {
var form = event.currentTarget;
var nativeEvent = event.nativeEvent;
var submitter = nativeEvent.submitter instanceof HTMLButtonElement || nativeEvent.submitter instanceof HTMLInputElement ? nativeEvent.submitter : null; // Validating the form with the submitter value
validate === null || validate === void 0 ? void 0 : validate(form, submitter);
/**
* It checks defaultPrevented to confirm if the submission is intentional
* This is utilized by `useFieldList` to modify the list state when the submit
* event is captured and revalidate the form with new fields without triggering
* a form submission at the same time.
*/
if (!config.noValidate && !(submitter !== null && submitter !== void 0 && submitter.formNoValidate) && !event.defaultPrevented) {
// Mark all fields as touched
for (var field of form.elements) {
if (isFieldElement(field)) {
field.dataset.conformTouched = 'true';
}
} // Check the validity of the form
return _objectSpread2(_objectSpread2({}, state), {}, {
[key]: message
});
if (!event.currentTarget.reportValidity()) {
event.preventDefault();
}
}
case 'migrate':
{
var {
keys,
error
} = action.payload;
var nextState = state;
if (!event.defaultPrevented) {
var _config$onSubmit;
for (var _key of Object.keys(keys)) {
var prevError = state[_key];
var nextError = error === null || error === void 0 ? void 0 : error[_key];
(_config$onSubmit = config.onSubmit) === null || _config$onSubmit === void 0 ? void 0 : _config$onSubmit.call(config, event);
}
}
if (typeof nextError === 'string' && prevError !== nextError) {
return _objectSpread2(_objectSpread2({}, nextState), {}, {
[_key]: nextError
});
}
}
};
}
/**
* All the information of the field, including state and config.
*/
return nextState;
}
function useFieldset(ref, config) {
var [error, setError] = useState(() => {
var result = {};
case 'cleanup':
{
var {
fieldset
} = action.payload;
var updates = [];
for (var [key, _error] of Object.entries((_config$initialError = config === null || config === void 0 ? void 0 : config.initialError) !== null && _config$initialError !== void 0 ? _config$initialError : {})) {
var _config$initialError;
for (var [_key2, _message] of Object.entries(state)) {
if (!_message) {
continue;
}
if (_error !== null && _error !== void 0 && _error.message) {
result[key] = _error.message;
}
}
var fields = getFieldElements(fieldset, _key2);
return result;
});
useEffect(() => {
/**
* Reset the error state of each field if its validity is changed.
*
* This is a workaround as no official way is provided to notify
* when the validity of the field is changed from `invalid` to `valid`.
*/
var resetError = form => {
setError(prev => {
var next = prev;
if (fields.every(field => field.validity.valid)) {
updates.push([_key2, '']);
for (var field of form.elements) {
if (isFieldElement(field)) {
var key = getKey(field.name, config === null || config === void 0 ? void 0 : config.name);
if (key) {
var _next$key, _next;
var prevMessage = (_next$key = (_next = next) === null || _next === void 0 ? void 0 : _next[key]) !== null && _next$key !== void 0 ? _next$key : '';
var nextMessage = field.validationMessage;
/**
* Techincally, checking prevMessage not being empty while nextMessage being empty
* is sufficient for our usecase. It checks if the message is changed instead to allow
* the hook to be useful independently.
*/
if (prevMessage !== '' && prevMessage !== nextMessage) {
next = _objectSpread2(_objectSpread2({}, next), {}, {
[key]: nextMessage
});
}
}
}
}
if (updates.length === 0) {
return state;
}
return next;
});
};
return _objectSpread2(_objectSpread2({}, state), Object.fromEntries(updates));
}
var handleInput = event => {
var form = getFormElement(ref.current);
var field = event.target;
case 'reset':
{
return {};
}
}
}, {}, () => Object.fromEntries(Object.keys(schema.fields).reduce((result, name) => {
var _config$error;
if (!form || !isFieldElement(field) || field.form !== form) {
return;
}
var error = (_config$error = config.error) === null || _config$error === void 0 ? void 0 : _config$error[name];
resetError(form);
};
if (typeof error === 'string') {
result.push([name, error]);
}
var invalidHandler = event => {
var form = getFormElement(ref.current);
var field = event.target;
return result;
}, [])));
useEffect(() => {
var _schema$validate;
if (!form || !isFieldElement(field) || field.form !== form) {
return;
}
var fieldset = ref.current;
var key = getKey(field.name, config === null || config === void 0 ? void 0 : config.name); // Update the error only if the field belongs to the fieldset
if (!fieldset) {
console.warn('No fieldset ref found; You must pass the fieldsetProps to the fieldset element');
return;
}
if (key) {
setError(prev => {
var _prev$key;
if (!(fieldset !== null && fieldset !== void 0 && fieldset.form)) {
console.warn('No form element is linked to the fieldset; Do you forgot setting the form attribute?');
}
var prevMessage = (_prev$key = prev === null || prev === void 0 ? void 0 : prev[key]) !== null && _prev$key !== void 0 ? _prev$key : '';
(_schema$validate = schema.validate) === null || _schema$validate === void 0 ? void 0 : _schema$validate.call(schema, fieldset);
dispatch({
type: 'cleanup',
payload: {
fieldset
if (prevMessage === field.validationMessage) {
return prev;
}
return _objectSpread2(_objectSpread2({}, prev), {}, {
[key]: field.validationMessage
});
});
event.preventDefault();
}
});
};
var resetHandler = e => {
if (e.target !== fieldset.form) {
var submitHandler = event => {
var form = getFormElement(ref.current);
if (!form || event.target !== form) {
return;
}
} // This helps resetting error that fullfilled by the submitter
dispatch({
type: 'reset'
});
setTimeout(() => {
var _schema$validate2;
// Delay revalidation until reset is completed
(_schema$validate2 = schema.validate) === null || _schema$validate2 === void 0 ? void 0 : _schema$validate2.call(schema, fieldset);
}, 0);
resetError(form);
};
var resetHandler = event => {
var form = getFormElement(ref.current);
if (!form || event.target !== form) {
return;
}
setError({});
};
document.addEventListener('input', handleInput); // The invalid event does not bubble and so listening on the capturing pharse is needed
document.addEventListener('invalid', invalidHandler, true);
document.addEventListener('submit', submitHandler);
document.addEventListener('reset', resetHandler);
return () => {
document.removeEventListener('input', handleInput);
document.removeEventListener('invalid', invalidHandler, true);
document.removeEventListener('submit', submitHandler);
document.removeEventListener('reset', resetHandler);
};
}, // eslint-disable-next-line react-hooks/exhaustive-deps
[schema.validate]);
}, [ref, config === null || config === void 0 ? void 0 : config.name]);
useEffect(() => {
dispatch({
type: 'migrate',
payload: {
keys: Object.keys(schema.fields),
error: config.error
}
});
}, [config.error, schema.fields]);
return [{
ref,
name: config.name,
form: config.form,
setError(prev => {
var next = prev;
onInput(e) {
var _schema$validate3;
for (var [key, _error2] of Object.entries((_config$initialError2 = config === null || config === void 0 ? void 0 : config.initialError) !== null && _config$initialError2 !== void 0 ? _config$initialError2 : {})) {
var _config$initialError2;
var fieldset = e.currentTarget;
(_schema$validate3 = schema.validate) === null || _schema$validate3 === void 0 ? void 0 : _schema$validate3.call(schema, fieldset);
dispatch({
type: 'cleanup',
payload: {
fieldset
if (next[key] !== (_error2 === null || _error2 === void 0 ? void 0 : _error2.message)) {
var _error2$message;
next = _objectSpread2(_objectSpread2({}, next), {}, {
[key]: (_error2$message = _error2 === null || _error2 === void 0 ? void 0 : _error2.message) !== null && _error2$message !== void 0 ? _error2$message : ''
});
}
});
},
}
onInvalid(e) {
var element = isFieldElement(e.target) ? e.target : null;
var key = Object.keys(schema.fields).find(key => (element === null || element === void 0 ? void 0 : element.name) === getName([e.currentTarget.name, key]));
return next;
});
}, [config === null || config === void 0 ? void 0 : config.name, config === null || config === void 0 ? void 0 : config.initialError]);
/**
* This allows us constructing the field at runtime as we have no information
* about which fields would be available. The proxy will also help tracking
* the usage of each field for optimization in the future.
*/
if (!element || !key) {
return new Proxy({}, {
get(_target, key) {
var _constraint, _config$defaultValue, _config$initialError3, _config$initialError4, _error$key;
if (typeof key !== 'string') {
return;
} // Disable browser report
}
e.preventDefault();
dispatch({
type: 'report',
payload: {
key,
message: element.validationMessage
}
});
var constraint = config === null || config === void 0 ? void 0 : (_constraint = config.constraint) === null || _constraint === void 0 ? void 0 : _constraint[key];
var field = {
config: _objectSpread2({
name: config !== null && config !== void 0 && config.name ? "".concat(config.name, ".").concat(key) : key,
form: config === null || config === void 0 ? void 0 : config.form,
defaultValue: config === null || config === void 0 ? void 0 : (_config$defaultValue = config.defaultValue) === null || _config$defaultValue === void 0 ? void 0 : _config$defaultValue[key],
initialError: config === null || config === void 0 ? void 0 : (_config$initialError3 = config.initialError) === null || _config$initialError3 === void 0 ? void 0 : (_config$initialError4 = _config$initialError3[key]) === null || _config$initialError4 === void 0 ? void 0 : _config$initialError4.details
}, constraint),
error: (_error$key = error === null || error === void 0 ? void 0 : error[key]) !== null && _error$key !== void 0 ? _error$key : ''
};
return field;
}
}, getFieldProps(schema, _objectSpread2(_objectSpread2({}, config), {}, {
error: Object.assign({}, config.error, errorMessage)
}))];
});
}
function useFieldList(props) {
/**
* Returns a list of key and config, with a group of helpers
* configuring buttons for list manipulation
*
* @see https://github.com/edmundhung/conform/tree/v0.3.0-pre.0/packages/conform-react/README.md#usefieldlist
*/
function useFieldList(ref, config) {
var [entries, setEntries] = useState(() => {
var _props$defaultValue;
var _config$defaultValue2;
return Object.entries((_props$defaultValue = props.defaultValue) !== null && _props$defaultValue !== void 0 ? _props$defaultValue : [undefined]);
return Object.entries((_config$defaultValue2 = config.defaultValue) !== null && _config$defaultValue2 !== void 0 ? _config$defaultValue2 : [undefined]);
});
var list = entries.map((_ref, index) => {
var _props$defaultValue2, _props$error;
var _config$defaultValue3, _config$initialError5, _config$initialError6;
var [key, defaultValue] = _ref;
return {
key: "".concat(key),
props: _objectSpread2(_objectSpread2({}, props), {}, {
name: "".concat(props.name, "[").concat(index, "]"),
defaultValue: defaultValue !== null && defaultValue !== void 0 ? defaultValue : (_props$defaultValue2 = props.defaultValue) === null || _props$defaultValue2 === void 0 ? void 0 : _props$defaultValue2[index],
error: (_props$error = props.error) === null || _props$error === void 0 ? void 0 : _props$error[index],
multiple: false
key,
config: _objectSpread2(_objectSpread2({}, config), {}, {
name: "".concat(config.name, "[").concat(index, "]"),
defaultValue: defaultValue !== null && defaultValue !== void 0 ? defaultValue : (_config$defaultValue3 = config.defaultValue) === null || _config$defaultValue3 === void 0 ? void 0 : _config$defaultValue3[index],
initialError: (_config$initialError5 = config.initialError) === null || _config$initialError5 === void 0 ? void 0 : (_config$initialError6 = _config$initialError5[index]) === null || _config$initialError6 === void 0 ? void 0 : _config$initialError6.details
})
};
});
var controls = {
prepend(defaultValue) {
return _objectSpread2(_objectSpread2({}, getControlButtonProps(props.name, 'prepend', {
defaultValue
})), {}, {
onClick(e) {
setEntries(entries => applyControlCommand([...entries], 'prepend', {
defaultValue: ["".concat(Date.now()), defaultValue]
}));
e.preventDefault();
}
/***
* This use proxy to capture all information about the command and
* have it encoded in the value.
*/
});
},
var control = new Proxy({}, {
get(_target, type) {
return function () {
var payload = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {};
return {
name: listCommandKey,
value: serializeListCommand(config.name, {
type,
payload
}),
form: config.form,
formNoValidate: true
};
};
}
append(defaultValue) {
return _objectSpread2(_objectSpread2({}, getControlButtonProps(props.name, 'append', {
defaultValue
})), {}, {
onClick(e) {
setEntries(entries => applyControlCommand([...entries], 'append', {
defaultValue: ["".concat(Date.now()), defaultValue]
}));
e.preventDefault();
}
});
useEffect(() => {
setEntries(prevEntries => {
var _config$defaultValue4;
});
},
var nextEntries = Object.entries((_config$defaultValue4 = config.defaultValue) !== null && _config$defaultValue4 !== void 0 ? _config$defaultValue4 : [undefined]);
replace(index, defaultValue) {
return _objectSpread2(_objectSpread2({}, getControlButtonProps(props.name, 'replace', {
index,
defaultValue
})), {}, {
onClick(e) {
setEntries(entries => applyControlCommand([...entries], 'replace', {
defaultValue: ["".concat(Date.now()), defaultValue],
index
}));
e.preventDefault();
}
if (prevEntries.length !== nextEntries.length) {
return nextEntries;
}
});
},
for (var i = 0; i < prevEntries.length; i++) {
var [prevKey, prevValue] = prevEntries[i];
var [nextKey, nextValue] = nextEntries[i];
remove(index) {
return _objectSpread2(_objectSpread2({}, getControlButtonProps(props.name, 'remove', {
index
})), {}, {
onClick(e) {
setEntries(entries => applyControlCommand([...entries], 'remove', {
index
}));
e.preventDefault();
if (prevKey !== nextKey || prevValue !== nextValue) {
return nextEntries;
}
} // No need to rerender in this case
});
},
reorder(fromIndex, toIndex) {
return _objectSpread2(_objectSpread2({}, getControlButtonProps(props.name, 'reorder', {
from: fromIndex,
to: toIndex
})), {}, {
onClick(e) {
if (fromIndex !== toIndex) {
setEntries(entries => applyControlCommand([...entries], 'reorder', {
from: fromIndex,
to: toIndex
}));
}
return prevEntries;
});
e.preventDefault();
}
var submitHandler = event => {
var form = getFormElement(ref.current);
});
}
if (!form || event.target !== form || !(event.submitter instanceof HTMLButtonElement) || event.submitter.name !== listCommandKey) {
return;
}
};
useEffect(() => {
var _props$defaultValue3;
var [name, command] = parseListCommand(event.submitter.value);
setEntries(Object.entries((_props$defaultValue3 = props.defaultValue) !== null && _props$defaultValue3 !== void 0 ? _props$defaultValue3 : [undefined]));
}, [props.defaultValue]);
return [list, controls];
if (name !== config.name) {
// Ensure the scope of the listener are limited to specific field name
return;
}
switch (command.type) {
case 'append':
case 'prepend':
case 'replace':
command.payload.defaultValue = ["".concat(Date.now()), command.payload.defaultValue];
break;
}
setEntries(entries => updateList([...(entries !== null && entries !== void 0 ? entries : [])], command));
event.preventDefault();
};
var resetHandler = event => {
var _config$defaultValue5;
var form = getFormElement(ref.current);
if (!form || event.target !== form) {
return;
}
setEntries(Object.entries((_config$defaultValue5 = config.defaultValue) !== null && _config$defaultValue5 !== void 0 ? _config$defaultValue5 : []));
};
document.addEventListener('submit', submitHandler, true);
document.addEventListener('reset', resetHandler);
return () => {
document.removeEventListener('submit', submitHandler, true);
document.removeEventListener('reset', resetHandler);
};
}, [ref, config.name, config.defaultValue]);
return [list, control];
}
/**
* 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.
*
* @see https://github.com/edmundhung/conform/tree/v0.3.0-pre.0/packages/conform-react/README.md#usecontrolledinput
*/
function useControlledInput(field) {
var _ref$current$value, _ref$current, _field$defaultValue;
var _field$defaultValue;
var ref = useRef(null);
var input = useMemo(() => /*#__PURE__*/createElement('input', {
ref,
name: field.name,
form: field.form,
defaultValue: field.defaultValue,
required: field.required,
minLength: field.minLength,
maxLength: field.maxLength,
min: field.min,
max: field.max,
step: field.step,
pattern: field.pattern,
hidden: true,
'aria-hidden': true
}), [field.name, field.form, field.defaultValue, field.required, field.minLength, field.maxLength, field.min, field.max, field.step, field.pattern]);
return [input, {
value: (_ref$current$value = (_ref$current = ref.current) === null || _ref$current === void 0 ? void 0 : _ref$current.value) !== null && _ref$current$value !== void 0 ? _ref$current$value : "".concat((_field$defaultValue = field.defaultValue) !== null && _field$defaultValue !== void 0 ? _field$defaultValue : ''),
onChange: value => {
if (!ref.current) {
return;
}
var [value, setValue] = useState("".concat((_field$defaultValue = field.defaultValue) !== null && _field$defaultValue !== void 0 ? _field$defaultValue : ''));
ref.current.value = value;
ref.current.dispatchEvent(new InputEvent('input', {
bubbles: true
}));
},
onBlur: () => {
var _ref$current2;
var handleChange = eventOrValue => {
if (!ref.current) {
return;
}
(_ref$current2 = ref.current) === null || _ref$current2 === void 0 ? void 0 : _ref$current2.dispatchEvent(new FocusEvent('focusout', {
bubbles: true
}));
}
var newValue = typeof eventOrValue === 'string' ? eventOrValue : eventOrValue.target.value;
ref.current.value = newValue;
ref.current.dispatchEvent(new InputEvent('input', {
bubbles: true
}));
setValue(newValue);
};
var handleBlur = () => {
var _ref$current;
(_ref$current = ref.current) === null || _ref$current === void 0 ? void 0 : _ref$current.dispatchEvent(new FocusEvent('blur', {
bubbles: true
}));
};
var handleInvalid = event => {
event.preventDefault();
};
return [_objectSpread2({
ref,
hidden: true
}, input(field, {
type: 'text'
})), {
value,
onChange: handleChange,
onBlur: handleBlur,
onInvalid: handleInvalid
}];

@@ -415,0 +491,0 @@ }

@@ -1,4 +0,4 @@

export { getFieldElements, parse } from '@conform-to/dom';
export { createSubmission, createValidate } 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.2.0",
"version": "0.3.0-pre.0",
"main": "index.js",

@@ -23,3 +23,3 @@ "module": "module/index.js",

"dependencies": {
"@conform-to/dom": "0.2.0"
"@conform-to/dom": "0.3.0-pre.0"
},

@@ -26,0 +26,0 @@ "peerDependencies": {

@@ -17,19 +17,20 @@ # @conform-to/react

By default, the browser calls [reportValidity()](https://developer.mozilla.org/en-US/docs/Web/API/HTMLFormElement/reportValidity) on the form element when you submit the form. 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 it is submitted. This checks the validity of all the fields in it and reports if there are errors through the bubbles.
This hook enhances this behaviour by allowing the developers to decide the best timing to start reporting errors using the `initialReport` option. This could start as earliest as the user typing or as late as the user submit the form.
This hook enhances the form validation behaviour in 3 parts:
But, setting `initialReport` to `onSubmit` still works different from the native browser behaviour, which basically calls `reportValidity()` only at the time a submit event is received. The `useForm` hook introduces a **touched** state to each fields. It will eagerly report the validity of the field once it is touched. Any errors reported later will be updated as soon as new errors are found.
1. It lets you hook up custom validation logic into different form events. For example, revalidation will be triggered whenever something changed.
2. It provides options for you to decide the best timing to start reporting errors. This could be as earliest as the user start typing, or also as late as the user try submitting the form.
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.
Feel free to **SKIP** this if the native browser behaviour fullfills your need.
```tsx
import { useForm } from '@conform-to/react';
function RandomForm() {
function LoginForm() {
const formProps = useForm({
/**
* Decide when the error should be reported initially.
* The options are `onSubmit`, `onBlur` or `onChange`.
* Default to `onSubmit`
* Define when the error should be reported initially.
* Support "onSubmit", "onChange", "onBlur".
*
* Default to `onSubmit`.
*/

@@ -39,11 +40,12 @@ initialReport: 'onBlur',

/**
* Native browser report will be enabled before hydation
* if this is set to `true`. Default to `false`.
* Enable native validation before hydation.
*
* Default to `false`.
*/
fallbackNative: true,
fallbackNative: false,
/**
* The form could be submitted regardless of the validity
* of the form if this is set to `true`. Default to
* `false`.
* Accept form submission regardless of the form validity.
*
* Default to `false`.
*/

@@ -53,9 +55,5 @@ noValidate: false,

/**
* Form submit handler
*
* It will NOT be called if
* (1) one of the fields is invalid, and
* (2) noValidate is set to false
* A function to be called when the form should be (re)validated.
*/
onSubmit(e) {
validate(form, submitter) {
// ...

@@ -65,5 +63,5 @@ },

/**
* Form reset handler
* The submit event handler of the form.
*/
onReset(e) {
onSubmit(event) {
// ...

@@ -73,3 +71,9 @@ },

return <form {...formProps}>{/* ... */}</form>;
return (
<form {...formProps}>
<input type="email" name="email" required />
<input type="password" name="password" required />
<button type="submit">Login</button>
</form>
);
}

@@ -81,13 +85,18 @@ ```

It is a group of properties required to setup the form. They can also be set explicitly as shown below:
It is a group of properties properties required to hook into form events. They can also be set explicitly as shown below:
```tsx
<form
ref={formProps.ref}
onSubmit={formProps.onSubmit}
onReset={formProps.onReset}
noValidate={formProps.noValidate}
>
{/* ... */}
</form>
function RandomForm() {
const formProps = useForm();
return (
<form
ref={formProps.ref}
onSubmit={formProps.onSubmit}
noValidate={formProps.noValidate}
>
{/* ... */}
</form>
);
}
```

@@ -97,2 +106,54 @@

<details>
<summary>Does it work with custom form component like Remix Form?</summary>
Yes! It will fallback to native form submission if the submit event handler is omitted or the event is not default prevented.
```tsx
import { useFrom } from '@conform-to/react';
import { Form } from '@remix-run/react';
function LoginForm() {
const formProps = useForm();
return (
<Form method="post" action="/login" {...formProps}>
{/* ... */}
</Form>
);
}
```
</details>
<details>
<summary>Is the `validate` function required?</summary>
The `validate` 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>
---

@@ -102,131 +163,147 @@

This hook prepares all the config you need to setup the fieldset based on the provided schema.
This hook can be used to monitor the state of each field and help fields configuration. It lets you:
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 using the [conform](#conform) helpers.
```tsx
import { useFieldset } from '@conform-to/react';
import { useForm, useFieldset } from '@conform-to/react';
/**
* Schema of the fieldset
*
* Defining a schema manually could be error-prone. It
* is strongly recommended to use a schema validation
* library with a schema resolver.
*
* Currently only Zod is supported and Yup support is
* coming soon. Please check the corresponding package
* for the setup required
* Consider the schema as follow:
*/
const schema = /*
Assuming this to be a schema for book and it looks like this:
type Book = {
name: string;
isbn: string;
};
type Book = {
name: string;
isbn: string;
}
*/
function BookFieldset() {
const [
fieldsetProps,
const formProps = useForm();
const { name, isbn } = useFieldset<Book>(
/**
* The variables `name` and `isbn` are FieldProps objects
* They are used to configure the field (input, select, textarea)
*
* Please check the docs of the `conform` helpers for how to
* use them together
* A ref object of the form element or fieldset element
*/
formProps.ref,
{
name,
isbn,
},
] = useFieldset(schema, {
/**
* Name of the fieldset
* Required only for nested fieldset.
*/
name: 'book',
/**
* The prefix used to generate the name of nested fields.
*/
name: 'book',
/**
* Id of the form
* Required only if the fieldset is placed out of the form
*/
form: 'random-form-id',
/**
* An object representing the initial value of the fieldset.
*/
defaultValue: {
isbn: '0340013818',
},
/**
* Default value of the fieldset
*/
defaultValue: {
isbn: '0340013818',
},
/**
* An object describing the initial error of each field
*/
initialError: {
isbn: 'Invalid ISBN',
},
/**
* Error reported by the server
*/
error: {
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',
},
});
);
const {
/**
* This would be `book.isbn` instead of `isbn`
* if the `name` option is provided
*/
name,
/**
* Latest error of the field
* This would be 'Invalid ISBN' initially as specified
* in the initialError config
*/
console.log(book.error);
/**
* This would be `random-form-id`
* because of the `form` option provided
*/
form,
/**
* This would be `book.isbn` instead of `isbn`
* if the `name` option is provided
*/
console.log(book.config.name);
/**
* This would be `0340013818` if specified
* on the `initalValue` option
*/
defaultValue,
/**
* This would be `0340013818` if specified
* on the `initalValue` option
*/
console.log(book.config.defaultValue);
/**
* Current error message
* This would be 'Invalid ISBN' initially if specified
* on the `error` option
*/
error,
/**
* Initial error message
* This would be 'Invalid ISBN' if specified
*/
console.log(book.config.initialError);
/**
* Constraint of the field (required, minLength etc)
*
* For example, the constraint of the isbn field could be:
* {
* required: true,
* pattern: '[0-9]{10,13}'
* }
*/
...constraint,
} = isbn;
/**
* This would be `random-form-id`
* because of the `form` option provided
*/
console.log(book.config.form);
return (
<fieldset {...fieldsetProps}>
{/* ... */}
</fieldset>)
);
/**
* 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(book.config.required);
console.log(book.config.pattern);
return <form {...formProps}>{/* ... */}</form>;
}
```
If you don't have direct access to the form ref, you can also pass a fieldset ref.
```tsx
import { useFieldset } from '@conform-to/react';
import { useRef } from 'react';
function Fieldset() {
const ref = useRef();
const fieldset = useFieldset(ref);
return <fieldset ref={ref}>{/* ... */}</fieldset>;
}
```
<details>
<summary>What is `fieldsetProps`?</summary>
<summary>Is it required to provide the FieldsetConfig to `useFieldset`?</summary>
It is a group of properties required to setup the fieldset. They can also be set explicitly as shown below:
No. The only thing required is the ref object. All the config is optional. You can always pass them to each fields manually.
```tsx
<fieldset
ref={fieldsetProps.ref}
name={fieldsetProps.name}
form={fieldsetProps.form}
onInput={fieldsetProps.onInput}
onInvalid={fieldsetProps.onInvalid}
>
{/* ... */}
</fieldset>
import { useForm, useFieldset } from '@conform-to/react';
function SubscriptionForm() {
const formProps = useForm();
const { email } = useFieldset(formProps.ref);
return (
<form {...formProps}>
<input
type="email"
name={email.config.name}
defaultValue="support@conform.dev"
required
/>
</form>
);
}
```

@@ -237,61 +314,23 @@

<details>
<summary>How is a schema looks like?</summary>
<summary>Why does `useFieldset` require a ref object of the form or fieldset?</summary>
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.
```tsx
import type { Schema } from '@conform-to/react';
function ExampleForm() {
const formRef = useRef();
const inputRef = useRef();
/**
* Defining a schema manually
*/
const bookSchema: Schema<{
name: string;
isbn: string;
quantity?: number;
}> = {
/**
* Define the fields with its constraint together
*/
fields: {
name: {
required: true,
},
isbn: {
required: true,
minLength: 10,
maxLength: 13,
pattern: '[0-9]{10,13}',
},
quantity: {
min: '0',
},
},
useEffect(() => {
// Both statements will log `true`
console.log(formRef.current === inputRef.current.form);
console.log(formRef.current.elements.namedItem('title') === inputRef.current)
}, []);
/**
* Customise validation behaviour
* Fallbacks to native browser validation if not specified
*/
validate(fieldset) {
/**
* Lookup the field elements using the fieldset element
*/
const [name] = getFieldElements(fieldset, 'name');
if (name.validity.valueMissing) {
/**
* Setting error message based on validity
*/
name.setCustomValidity('Required');
} else if (name.value === 'something') {
/**
* Setting error message based on custom constraint
*/
name.setCustomValidity('Please enter a valid name');
} else {
/**
* Clearing the error message (Important!)
*/
name.setCustomValidity('');
}
},
};
return (
<form ref={formRef}>
<input ref={inputRef} name="title">
</form>
);
}
```

@@ -305,31 +344,76 @@

This hook is used in combination with `useFieldset` to handle array structure:
It returns a list of key and config, with a group of helpers configuring buttons for list manipulation
```tsx
import { useFieldset, useFieldList } from '@conform-to/react';
import { useRef } from 'react';
/**
* Consider the schema as follow:
*
* type Collection = {
* books: Array<{ name: string; isbn: string; }>
* }
*/
type Book = {
name: string;
isbn: string;
};
function CollectionForm() {
const [fieldsetProps, { books }] = useFieldset(collectionSchema);
const [bookList, control] = useFieldList(books);
type Collection = {
books: Book[];
};
function CollectionFieldset() {
const ref = useRef();
const { books } = useFieldset<Collection>(ref);
const [bookList, control] = useFieldList(ref, books);
return (
<fieldset {...fieldsetProps}>
<fieldset ref={ref}>
{bookList.map((book, index) => (
<div key={book.key}>
{/* `book.props` is a FieldProps object similar to `books` */}
<BookFieldset {...book.props}>
{/* To setup the fields */}
<input
name={`${book.config.name}.name`}
defaultValue={book.config.defaultValue.name}
/>
<input
name={`${book.config.name}.isbn`}
defaultValue={book.config.defaultValue.isbn}
/>
{/* To setup a delete button */}
<button {...control.remove(index)}>Delete</button>
<button {...control.remove({ index })}>Delete</button>
</div>
))}
{/* To setup a button that can append a new row with optional default value */}
<button {...control.append({ defaultValue: { name: '', isbn: '' } })}>
add
</button>
</fieldset>
);
}
```
This hook can also be used in combination with `useFieldset` to distribute the config:
```tsx
import { useForm, useFieldset, useFieldList } from '@conform-to/react';
import { useRef } from 'react';
function CollectionFieldset() {
const ref = useRef();
const { books } = useFieldset<Collection>(ref);
const [bookList, control] = useFieldList(ref, books);
return (
<fieldset ref={ref}>
{bookList.map((book, index) => (
<div key={book.key}>
{/* `book.props` is a FieldConfig object similar to `books` */}
<BookFieldset {...book.config}>
{/* To setup a delete button */}
<button {...control.remove({ index })}>Delete</button>
</div>
))}
{/* To setup a button that can append a new row */}

@@ -347,3 +431,4 @@ <button {...control.append()}>add</button>

function BookFieldset({ name, form, defaultValue, error }) {
const [fieldsetProps, { name, isbn }] = useFieldset(bookSchema, {
const ref = useRef();
const { name, isbn } = useFieldset(ref, {
name,

@@ -356,3 +441,3 @@ form,

return (
<fieldset {...fieldsetProps}>
<fieldset ref={ref}>
{/* ... */}

@@ -369,15 +454,15 @@ </fieldset>

// To append a new row with optional defaultValue
<button {...controls.append(defaultValue)}>Append</button>;
<button {...controls.append({ defaultValue })}>Append</button>;
// To prepend a new row with optional defaultValue
<button {...controls.prepend(defaultValue)}>Prepend</button>;
<button {...controls.prepend({ defaultValue })}>Prepend</button>;
// To remove a row by index
<button {...controls.remove(index)}>Remove</button>;
<button {...controls.remove({ index })}>Remove</button>;
// To replace a row with another defaultValue
<button {...controls.replace(index, defaultValue)}>Replace</button>;
<button {...controls.replace({ index, defaultValue })}>Replace</button>;
// To reorder a particular row to an another index
<button {...controls.reorder(fromIndex, toIndex)}>Reorder</button>;
<button {...controls.reorder({ from, to })}>Reorder</button>;
```

@@ -391,16 +476,18 @@

This hooks creates a shadow input that would be used to validate against the schema. Mainly used to get around problem integrating with controlled component.
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.
```tsx
import { useControlledInput } from '@conform-to/react';
import { useFieldset, useControlledInput } from '@conform-to/react';
import { Select, MenuItem } from '@mui/material';
import { useRef } from 'react';
function RandomFieldset() {
const [fieldsetProps, { category }] = useFieldset(schema);
const [input, control] = useControlledInput(category);
function MuiForm() {
const ref = useRef();
const { category } = useFieldset(schema);
const [inputProps, control] = useControlledInput(category);
return (
<fieldset {...fieldsetProps}>
{/* Render the shadow input somewhere within the fieldset */}
{input}
<fieldset ref={ref}>
{/* Render a shadow input somewhere */}
<input {...inputProps} />

@@ -410,7 +497,8 @@ {/* MUI Select is a controlled component */}

label="Category"
value={control.value ?? ''}
onChange={(e) => control.onChange(e.target.value)}
onBlur={() => control.onBlur()}
error={Boolean(category.error)}
helperText={category.error}
value={control.value}
onChange={control.onChange}
onBlur={control.onBlur}
inputProps={{
onInvalid: control.onInvalid
}}
>

@@ -431,12 +519,14 @@ <MenuItem value="">Please select</MenuItem>

It provides several helpers to setup a native input field quickly:
It provides several helpers to configure a native input field quickly:
```tsx
import { conform } from '@conform-to/react';
import { useFieldset, conform } from '@conform-to/react';
import { useRef } from 'react';
function RandomForm() {
const [setupFieldset, { cateogry }] = useFieldset(/* ... */);
const ref = useRef();
const { cateogry } = useFieldset(ref);
return (
<fieldset {...setupFieldset}>
<fieldset ref={ref}>
<input {...conform.input(cateogry, { type: 'text' })} />

@@ -454,6 +544,7 @@ <textarea {...conform.textarea(cateogry)} />

function RandomForm() {
const [setupFieldset, { cateogry }] = useFieldset(/* ... */);
const ref = useRef();
const { cateogry } = useFieldset(ref);
return (
<fieldset {...setupFieldset}>
<fieldset ref={ref}>
<input

@@ -460,0 +551,0 @@ type="text"

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