Skip to content

Repository files navigation

@jalik/react-form

GitHub package.json versionBuild StatusGitHub last commitGitHub issuesGitHubnpm@jalik/react-form

Why using this library ?

There are other well-established solutions like Formik, React-Hook-Form, Redux Form, etc. and this one is another flavor in the jungle with minimal dependencies and "small" package size.

It is mainly focused on providing a simple and intuitive API for developers while ensuring a smooth and user-friendly experience for end-users.

The features are more oriented for SPA, but it still has added value on classic forms for initialization and validation.

Features

  • Fields props initialization at form level (optional)
  • Management of field state and updates (value and onChange)
  • Tracking of modified fields
  • Tracking of touched fields
  • Various form status info (modified, disabled, validating, submitting...)
  • Form loading using promise (optional)
  • Auto-disabling fields until the form is initialized
  • Auto-disabling fields when the form is disabled, not modified, validating or submitting
  • Parsing of field value when modified (smart typing or custom parser)
  • Replacement of empty string by null on field change and form submit
  • Trim values on form submitting
  • Field validation on change (optional)
  • Field validation on init/load (optional)
  • Field validation on touch (optional)
  • Field validation on submitting (optional)
  • Field and form validation using a custom function or schema (like yup)
  • Form and field errors handling
  • Reset form or fields
  • Handling form submission errors and retries
  • Compatible with custom components libraries
  • TypeScript declarations ♥

Sandbox

You can play with the lib here: https://codesandbox.io/s/jalik-react-form-demo-wx6hg?file=/src/components/UserForm.js

Installing

npm i @jalik/react-form
yarn add @jalik/react-form

Creating a form

import{Button,Field,Form,useForm}from'@jalik/react-form'functionauthenticate(username,password){returnfetch('https://www.mysite.com/api/auth',{method: 'POST',body: JSON.stringify({
username,
password
}),headers: {'content-type': 'application/json'}})}functionSignInForm(){constform=useForm({initialValues: {username: '',password: ''},onSubmit: async(values)=>authenticate(values.username,values.password)})return(<Formcontext={form}><Fieldname="username"/><Fieldname="password"type="password"/><Buttontype="submit">Sign in</Button></Form>)}

Initializing values

Initialize the form with dynamic values

import{Button,Field,Form,useForm}from'@jalik/react-form'import{useParams}from'react-router'asyncfunctionuseUserLoader(id){if(id!=null){constdata=awaitfetch(`/api/user/${id}`)returndata.json()}returnnull}functionUserFormPage(){constparams=useParams()constinitialValues=useUserLoader(params.id)constform=useForm({
initialValues,// Update the form values when initialValues changes.// It is usually wanted when loading dynamic values and if you want the form to be updated.// IMPORTANT: when using reinitialize, make sure that initialValues is stable// (loaded from an API or memoized) to avoid infinite rerenders.reinitialize: true,onSubmit: async(values)=>({saved: true})})return(<Formcontext={form}><Fieldname="firstName"/><Fieldname="lastName"/><Buttontype="submit">Save</Button></Form>)}

Initialize the form using a load function

This is handy if you are not using a specific loading library.

import{Button,Field,Form,useForm}from'@jalik/react-form'import{useParams}from'react-router'asyncfunctionloadUser(id){constdata=awaitfetch(`/api/user/${id}`)returndata.json()}functionUserFormPage(props){constparams=useParams()constform=useForm({// initialValues: null,// IMPORTANT: load is called every time the function changes.// So make sure to wrap the function in useCallback() if necessary.load: useCallback(()=>loadUser(params.id),[]),reinitialize: true,onSubmit: async(values)=>({saved: true})})return(<Formcontext={form}><Fieldname="firstName"/><Fieldname="lastName"/><Buttontype="submit">Save</Button></Form>)}

Validating a form

Validating using a schema

You can use any validation library, but you will need to create wrappers and the implementation depends on how the library works.
Here is an example using @jalik/schema to validate the form.

import{Button,Field,FieldError,Form,useForm}from'@jalik/react-form'importSchemafrom'@jalik/schema'/** * Returns field props based on schema constraints. * Handy when using classic HTML validation. * @param schema */exportfunctioncreateFieldInitializer(schema){// function called by initializeFieldreturn(name)=>{try{constfield=schema.getField(name)return{id: `field-${name}`,min: field.getMin(),max: field.getMax(),required: field.isRequired()}}catch(error){// field not foundreturnnull}}}/** * Validates the field using the schema. * @param schema */exportfunctioncreateFieldValidator(schema){// function called by validateFieldreturnasync(name,value)=>{try{schema.getField(name).validate(value)}catch(error){if(errorinstanceofValidationError){returnerror}return{[name]: newError(`Field "${name}" is unknown`)}}}}/** * The function returned validates the form (all fields) using the schema. * @param schema */exportfunctioncreateFormValidator(schema){// function called by validatereturnasync(values)=>schema.getErrors(values)}constSignInFormSchema=newSchema({username: {type: 'string',required: true,minLength: 1},password: {type: 'string',required: true,minLength: 1}})functionSignInForm(){constform=useForm({initialValues: {username: '',password: ''},// This function sets the fields props (min, max...) using a schema.initializeField: createFieldInitializer(SignInFormSchema),// This function validates all fields (even the missing ones).validate: createFormValidator(SignInFormSchema),// This function validates a single field.validateField: createFieldValidator(SignInFormSchema),onSubmit: async(values)=>({success: true})})return(<Formcontext={form}><Fieldname="username"/><FieldErrorname="username"/><Fieldname="password"/><FieldErrorname="password"/><Buttontype="submit">Sign in</Button></Form>)}

Linking form inputs to you form instance

To link and initialize your components with a form instance, use getButtonProps(), getFieldProps() and getFormProps().

import{useFormContext}from'@jalik/react-form'import{MyCustomButton,MyCustomForm,MyCustomInput}from'my-custom-ui'exportfunctionFormButton(props){constform=useFormContext()return<MyCustomButton{...form.getButtonProps(props)}/>}exportfunctionFormInput(props){const{ name }=propsconstform=useFormContext()return<MyCustomInput{...form.getFieldProps(name,props)}/>}exportfunctionFormWrapper(props){constform=useFormContext()return<MyCustomForm{...form.getFormProps(props)}/>}

Or use the provided components <Button>, <Field> and <Form>.

import{Button,Field,Form}from'@jalik/react-form'import{MyCustomButton,MyCustomForm,MyCustomInput}from'my-custom-ui'exportfunctionFormButton(props){return<Button{...props}component={MyCustomButton}/>}exportfunctionFormInput(props){return<Field{...props}component={MyCustomInput}/>}exportfunctionFormWrapper(props){return<Form{...props}component={MyCustomForm}/>}

API

Hooks

useForm (options)

This is where the magic happens, this hook defines the form state and its behavior.

import{useForm}from'@jalik/react-form'constform=useForm({// optional, what to do after a successful submit: 'clear' | 'initialize' | 'reset' | nullafterSubmit: 'reset',// optional, used to debug formdebug: false,// optional, used to disable all fields and buttonsdisabled: false,disableOnSubmit: true,disableOnValidate: true,// optional, disables the submit button if form is not modifieddisableSubmitIfNotModified: false,// optional, disables the submit button if form is not validdisableSubmitIfNotValid: false,// optional, enable native HTML validation on submitenableHTMLValidation: false,// optional, update form when status changesforceUpdateOnStatusChange: false,// optional, used to set initial valuesinitialValues: undefined,// optional, sets initial errorsinitialErrors: {},// optional, sets initial modified fieldsinitialModified: {},// optional, sets initial touched fieldsinitialTouched: {},// optional, used to replace empty string by null on change and on submitnullify: false,// optional, prevent native action on submitpreventDefaultOnSubmit: true,// optional, used to initialize form everytime initialValues changesreinitialize: false,// optional, used to debounce submitsubmitDelay: 100,// optional, used to remove extra spaces on blurtrimOnBlur: false,// optional, used to remove extra spaces on submittrimOnSubmit: false,// optional, used to debounce validationvalidateDelay: 400,// optional, used to validate field on changevalidateOnChange: false,// optional, used to validate all fields on initializationvalidateOnInit: false,// optional, used to validate all fields on submitvalidateOnSubmit: true,// optional, used to validate field on touchvalidateOnTouch: false,// optional, used to set field props dynamicallyinitializeField: (name,formState)=>({className: formState.modifiedFields[name] ? 'input-modified' : undefined,required: name==='username'}),// optional, used to load initial valuesload: async()=>({id: 1,username: 'test'}),// REQUIRED, called when form is submittedonSubmit: async(values)=>({success: true}),// optional, called when form has been successfully submittedonSuccess: (result,values)=>{// resuls contains the value returned by onSubmit, in this example { success: true }},// optional, called when form values have changedonValuesChange: (values,previousValues)=>{},// optional, called when a field value changed// mutation contains all pending changes in a flat object ({field: value})// values contains the next form valuestransform: (mutation,values)=>{// in this example, if lastname or firstname changed,// we set the value of "username" like "john.c"if(mutation.lastname||mutation.firstname){mutation.username=[values.firstname,(values.lastname||'')[0]].join('.').toLowerCase()}returnmutation},// optional, used to validate all fields (expect a promise)validate: async(values)=>{consterrors={}if(!values.username){// error can be a stringerrors.username='field is required'// or an Errorerrors.username=newError('field is required')}returnerrors},// optional, used to validate a single field (expect a promise)validateField: async(name,value,values)=>{if(name==='username'&&!value){// error can be a stringreturn'field is required'// or an ErrorreturnnewError('field is required')}}})

useFormContext ()

This hook returns the form context and functions.

import{useFormContext}from'@jalik/react-form'const{// FORM STATE// tells if the form is disabled
disabled,// tells if the form has been initialized
initialized,// tells if the form was modified
modified,// tells if the form was touched
touched,// FORM UTILS// returns the button props
getButtonProps,// returns the field props
getFieldProps,// handler for onChange
handleChange,// handler for onBlur
handleBlur,// handler for onReset
handleReset,// handler for onChange((value) => {}) instead of onChange((event) => {})
handleSetValue,// handler for onSubmit
handleSubmit,// LOADING
load,// tells if the form is loading
loading,// loading error (if any)
loadError,// FIELD VALUES// clears the form (values, errors...)
clearValues,// returns the field initial value by name
getInitialValue,// returns the field initial value by name
getValue,// initial values (used when form is reset)
initialValues,// removes fields (used for dynamic forms)
removeValues,// resets all or given fields to their initial values
resetValues,// sets the initial values
setInitialValues,// sets value of a field
setValue,// sets values of multiple fields
setValues,// the form values
values,// LIST MANAGEMENT
appendListItem,
insertListItem,
moveListItem,
prependListItem,
removeListItem,
replaceListItem,
swapListItem,// FIELD ERRORS// clears all errors
clearErrors,// fields errors
errors,// tells if the form has errors
hasError,// sets a single field error
setError,// sets fields errors
setErrors,// FIELD STATE MANAGEMENT// clears all or given fields
clearModifiedFields,// clears all or given fields
clearTouchedFields,// returns the modified fields
getModifiedFields,// returns the touched fields
getTouchedFields,// check if a field was modified
isModified,// check if a field was touched
isTouched,// the list of modified fields
modifiedFields,// resets modified fields to their initial values
resetModifiedFields,// resets touched fields to their initial values
resetTouchedFields,// set a single modified field
setModifiedField,// set all or given modified field
setModifiedFields,// set a single touched field
setTouchedField,// set all or given touched field
setTouchedFields,// the list of touched fields
touchedFields,// FORM SUBMISSION// submits the form with values (validate first)
submit,// the number of times the form was submitted.// resets to zero when submission succeeds.
submitCount,// the submit error (if any)
submitError,// the submit result (returned by onSubmit)
submitResult,// tells if the form was submitted (changes to false when form is modified)
submitted,// tells if the form is submitting
submitting,// VALIDATION// tells if the form will trigger a validation// can be a boolean or a list of fields to validate
needValidation,// sets the validation state of the form
setValidated,// sets the validation error
setValidateError,// tells if the form is validating
setValidating,// validates all fields
validate,// the validation error (if any)// happens only when an error is thrown during validation// it's different from the field validation errors
validateError,// validates given fields
validateFields,// tells if the form was successfully validated
validated,// tells if a field should be validated on change
validateOnChange,// tells if all fields should be validated on initialization
validateOnInit,// tells if all fields should be validated on submit
validateOnSubmit,// tells if a field should be validated on touch
validateOnTouch,// tells if the form is validating
validating,// WATCH
watch,
watchers,}=useFormContext()

useFieldArray (options)

This hook returns utils to manage an array of fields.

import{useFieldArray,useForm}from'@jalik/react-form'functionItemListForm(){constform=useForm({initialValues: {items: [{name: 'Item 1'}]},onSubmit: (values)=>Promise.resolve(values)})const{
items,
append,
prepend,
remove,
insert,
replace,
move,
swap,
handleAppend,
handlePrepend,
handleRemove
}=useFieldArray({context: form,name: 'items',defaultValue: {name: 'New Item'}})return(<div>{items.map((item,index)=>(<divkey={item.key}><Fieldname={item.name}/><buttontype="button"onClick={handleRemove(index)}>Remove</button></div>))}<buttontype="button"onClick={handleAppend}>Add Item</button></div>)}

Components

Some components are provided to ease forms building.

<Button>

This component is synced with the form, so whenever the form is disabled (because it is loading, validating or submitting), the button disabled.

import{Button}from'@jalik/react-form'functionSubmitButton(){return(<Buttontype="submit">Submit</Button>)}

<Field>

This component handles the field value and logic.
The name is required.

import{Field}from'@jalik/react-form'import{Switch}from'@mantine/core'functionparseBoolean(value){return/^true|1$/gi.test(value)}exportfunctionAcceptTermsField(){return(<Fieldcomponent={Switch}name="acceptTerms"parser={parseBoolean}type="checkbox"value="true"/>)}exportfunctionCountryField(){return(<Fieldname="country"type="select"options={[{label: 'French Polynesia',value: 'pf'},{label: 'New Zealand',value: 'nz'}]}/>)}

<FieldError>

This component automatically displays the field error (if any).

import{Field,FieldError}from'@jalik/react-form'exportfunctionPasswordField(){return(<><Fieldname="password"type="password"/><FieldErrorname="password"/></>)}

<Form>

This component contains the form context, so any component nested in a Form can access the form context using useFormContext().

import{Button,Field,Form,useForm}from'@jalik/react-form'exportfunctionSignInForm(){constform=useForm({onSubmit: (values)=>Promise.resolve(true)})return(<Formcontext={form}><Fieldname="username"/><Fieldname="password"/><Buttontype="submit">Sign in</Button></Form>)}

Changelog

History of releases is in the changelog.

License

The code is released under the MIT License.

About

An easy way to manage forms with React.

Resources

Contributing

Stars

0 stars

Watchers

1 watching

Forks

Releases

Packages

Used by

Contributors

Languages