yet-another-form v0.1.5
Yet Another Form π€¦ββοΈ
Yet another form-state management library. There are a bunch of them. This one is another take on this problem.
Motivation
- Size. The gil was to create the smallest form management library. We use
size-limitto check the size of the library after each commit.
Getting started
Install the
yet-another-form# npm npm install yet-another-form #yarn yarn add yet-another-form #pnpm pnpm add yet-another-formImport
useFormhook into yor componentimport { useForm } from 'yet-another-form/react' const AddUser = ({ onAddUser }) => { const { Form, setValue, values } = useForm({ onSubmit: onAddUser }) return ( <Form> <input name="name" onChange={setValue} value={values.name} /> <input name="email" onChange={setValue} value={values.email} /> <button>Add user</button> </Form> ) }
Usage
Validation
yet-another-form supports form-level and field-level validations.
Form-level validation
To validate on the form-level, you need to pass a validate function with the form config. This function will receive the latest form values and should return either an object with errors, or the promise, that resolves to that object, or something in between:
- object with errors
validate(values) { return { name: values.name ? undefined : 'name is required', email: values.email ? undefined : 'email is required', } } - promise
validate(values) { return validateUserDataOnServer(values) }- while the promise is resolving, the
isValidatingproperty of form status will be set totrueandisValidwill be set tofalse
- while the promise is resolving, the
- something in between
validate(values) { return { name: values.name ? undefined : 'name is required', email: validateEmailOnServer(values.email) } }- non-async errors will be applied to form state right away
- while async validations are in progress, the form and fields
isValidatingproperties will be set totrueandisValidproperties will befalse - for fields with async validation, the previous error will be returned while validation is in progress
Form-level validation runs on each change. But it can be debounced through the debounce property in the form config (ms).
Field-level validation
For field-level validation, you can pass a validate function (sync or async) to the useFormField hook config. The function will receive the field value as the first argument and the latest form values as the second.
Submission
When the form is submitted, a few things happen:
- it will set
isSubmittingflag totrue - if the form is invalid, it will touch all fields with errors and will prevent form submission
- if the form has an
onSubmithandler, it will call it, with form values, and a "bag" with form status andresetmethod - after data was submitted, the
isSubmittingflag will be set tofalse - if the form does not have an
onSubmithandler, it will fall back to default browser behavior for the<form>element, i.e., form data will be sent to the provided endpoint, and the browser will reload the page.
API reference
useForm
The useForm is the main hook that creates and configures the form store.
const formContext = useForm(config)Form config
debounceValidation: numberβ if set, then form validation will happen only once within the defined periodinitialValues: anyβ default values for the form- if
initialValueschanges, the form will be reset. Previous and next values are compared deeply, so you don't need to memorize those (useuseMemo), but the latter might improve performance. onSubmit(values, formBag) => Promise<void> | voidβ submit handler. Is called when form is submitted: either the HTML<form>element is submitted, orformContext.submitis called.formBagβ is an object with a few helpful props and methods:isDirty: booleanβ indicates whether any field was changed or notisValid: booleanβ indicates whether the form has any errors or notdirtyFields: string[]β list of fields that had been changed -errorFields: string[]β list of fields that have errorserrorFields: string[]β list of fields that have errorsvalidatingFields: string[]β list of fields that are validating (in case of async validation)reset() => voidβ resets the form and sets submitted values as default valuesvalidate(values) => any | Promise<any>validation function. It will receive form values and should return an object that contains errors for fields. More about validation
Form context
useForm hook returns an object with the current form state and several helpers and setters to update the form state.
Form: ReactComponentβ thin wrapper around HTML<form>element. It wraps<form>into form context provider, and "binds" itsonSubmithandler with the form store- all props passed to the
<Form>component will be passed to the underlying<form>element, evenonSubmit <Form>forwards the ref to the underlying<form>element, so you can pass react refs to it
- all props passed to the
isDirty: booleanβ indicates whether any field was changed or notisSubmitting: booleanβ indicates whether the form is submitting dataisValid: booleanβ indicates whether the form has any errors or notdirtyFields: string[]β list of fields that had been changed- auto-subscribable, which means that if it was not read from the
useFormhook, the react component will not be updated, if it changes
- auto-subscribable, which means that if it was not read from the
errorFields: string[]β list of fields that have errors- auto-subscribable, which means that if it was not read from the
useFormhook, the react component will not be updated, if it changes
- auto-subscribable, which means that if it was not read from the
validatingFields: string[]β list of fields that are validating (in case of async validation)- auto-subscribable, which means that if it was not read from the
useFormhook, the react component will not be updated, if it changes
- auto-subscribable, which means that if it was not read from the
formContext: FormContextβ a form store, in case you need to pass it to other hooks manuallysetError: functionβ sets error for the form or a particular field- support several signatures:
(error: string | undefined, fieldPath: string) => void(error: Promise<string | undefined>, fieldPath: string) => void(fieldPath: string) => (error: string | undefined) => void(fieldPath: string) => (error: Promise<string | undefined>) => void
- it keeps the reference between rerenders, so it is safe to pass it to
useEffectoruseCallback
- support several signatures:
setTouched: functionβ sets whether particular field or fields were touched or not- support several signatures:
(event: BlurEvent) => void(event: BlurEvent, fieldPath: string) => void(touched: boolean, fieldPath: string) => void(fieldPath: string) => (touched: boolean) => void(fieldPath: string) => (event: BlurEvent) => void
- can be passed to
onBlurhandler. In this case, will set the touched value to true when input loses focus - it keeps the reference between rerenders, so it is safe to pass it to
useEffectoruseCallback
- support several signatures:
setValue: functionβ sets value for the form or a particular field- support several signatures:
(event: ChangeEvent) => void(event: ChangeEvent, fieldPath: string) => void(value: any, fieldPath: string) => void(fieldPath: string) => (event: ChangeEvent) => void(fieldPath: string) => (value: any) => void
- can be passed to
onChangehandler. In this case, it will try to read the field path from the input name - it keeps the reference between rerenders, so it is safe to pass it to
useEffectoruseCallback
- support several signatures:
reset(initialValues?: any) => voidβ resets the form to its initial values- if new values are passed, it will set them as form initial values and will validate the form
submit() => voidβ submits the form
useFormField
The useFormField hook is used to get data and setters for a particular field. It should be used within form context (in a sub-tree of the Form component, returned by the useForm hook).
If you want to use the useFormField hook in the same code block as the useForm and not in the <Form> sub-tree, you need to pass the FormContext to the hook as part of the hook config.
const field = useFormField('address.city')
// or with manually passed form context
const { formContext } = useForm()
const field = useFormField('address', { formContext })Arguments
path: stringβ the path of a particular field in the form state- is required
- supports object notation, i.e.
address.cityoritems[0].title
configβ optional hook configformContext: FormContextβ form context- in case
useFormFieldhook is used outside form context provider, you need to manually pass form context as a second argument
- in case
validate(value: any, formValues: any) => string | undefined | Promise<string | undefined>β field-level validation function
Field context
useFormField hook returns an object with the current field state and several setters to update a field.
error: string | undefinedβ field error, if anyisDirty: booleanβ indicates whether the field was changed or notisValidating: booleanβ indicates whether the field is being validatedtouched: booleanβ indicates whether the field was touchedvalue: anyβ field valuesetError(error: string | undefined | Primise<string | undefined>β sets error for the field- it keeps the reference between rerenders, so it is safe to pass it to
useEffectoruseCallback
- it keeps the reference between rerenders, so it is safe to pass it to
setTouched(touched: boolean | BlurEvent) =>β sets whether the field was touched or not- can be passed to
onBlurhandler. In this case, will set the touched value totrue, when input loses focus - it keeps the reference between rerenders, so it is safe to pass it to
useEffectoruseCallback
- can be passed to
setValue(value: any | ChangeEvent) => voidβ sets value for the field- can be passed to
onChangehandler - it keeps the reference between rerenders, so it is safe to pass it to
useEffectoruseCallback
- can be passed to
useFormState
The useFormState hook returns a form state. It should be used within form context (in a sub-tree of the Form component, returned by the useForm hook).
If you want to use the useFormState hook in the same code block as the useForm and not in the <Form> sub-tree, you need to pass the FormContext to the hook as its first argument.
const formState = useFormState()
// or with manually passed form context
const { formContext } = useForm()
const formState = useFormState(formContext)Arguments
formContext: FormContextβΒ form context- optional
- in case
useFormStatehook is used outside form context provider, you need to manually pass form context to it
Form state
useFormState hook returns an object with the current form state.
isDirty: booleanβ indicates whether any field was changed or notisSubmitting: booleanβ indicates whether the form is submitting dataisValid: booleanβ indicates whether the form has any errors or notdirtyFields: string[]β list of fields that had been changed- auto-subscribable, which means that if it was not read from the
useFormhook, the react component will not be updated, if it changes
- auto-subscribable, which means that if it was not read from the
errorFields: string[]β list of fields that have errors- auto-subscribable, which means that if it was not read from the
useFormhook, the react component will not be updated, if it changes
- auto-subscribable, which means that if it was not read from the
validatingFields: string[]β list of fields that are validating (in case of async validation)- auto-subscribable, which means that if it was not read from the
useFormhook, the react component will not be updated, if it changes
- auto-subscribable, which means that if it was not read from the
Credits
Part of the code, typings and API design was inspired by other form state management libraries: