Skip to content

Instantly share code, notes, and snippets.

@KacperKozak
Last active November 12, 2019 18:49
Show Gist options
  • Save KacperKozak/d6757f960db72481191d996e14a1f4bf to your computer and use it in GitHub Desktop.
Save KacperKozak/d6757f960db72481191d996e14a1f4bf to your computer and use it in GitHub Desktop.
Simple form hook validation for React
interface FormData {
message: string
email: string
}
const App = () => {
const { submitHandler, values, updateFieldValue, touchField, getFieldError } = useForm<FormData>(
{
message: required('Message is required'),
email: pipe(
required('E-mail is required'),
emailValidator('E-mail is invalid'),
),
},
{
initialState: {
message: '',
email: '',
},
},
)
return (
<form onSubmit={submitHandler(data => console.log(data))}>
<label>
E-mail:
<input
value={values['email']}
onChange={e => updateFieldValue('email', e.target.value)}
onBlur={() => touchField('email')}
/>
<p>{getFieldError('email')}</p>
</label>
<label>
Message:
<input
value={values['message']}
onChange={e => updateFieldValue('message', e.target.value)}
onBlur={() => touchField('message')}
/>
<p>{getFieldError('message')}</p>
</label>
</form>
)
}
import { useState } from 'react'
export type ValidatorResult = string | undefined
export type Validator<T> = (value: T) => ValidatorResult
export type Validators<TData> = { [TKey in keyof TData]?: Validator<TData[TKey]> }
interface UseFormOptions<TData> {
initialState: TData
}
export const useForm = <TData extends {}>(
validators: Validators<TData>,
options: UseFormOptions<TData>,
) => {
type FieldName = keyof TData
const [values, setValues] = useState<TData>(options.initialState)
type ValidatorErrors = Record<FieldName, ValidatorResult>
type ShowErrors = Partial<Record<FieldName, boolean>>
const getAllErrors = (): ValidatorErrors =>
Object.fromEntries(
Object.entries(validators).map(entries => {
const [key, validator] = entries as [FieldName, Validator<TData[FieldName]>]
const error = validator(values[key])
return [key, error]
}),
) as ValidatorErrors
const [errors, setErrors] = useState<ValidatorErrors>(getAllErrors())
const [showFieldError, setShowFieldError] = useState<ShowErrors>({})
const [showAllErrors, setAllError] = useState(false)
const validateField = <T extends FieldName>(
fieldName: T,
value: TData[T] = values[fieldName],
) => {
const validator = validators[fieldName]
if (validator) {
return validator(value)
}
return undefined
}
const updateFieldValue = <T extends FieldName>(fieldName: T, value: TData[T]) => {
setValues({
...values,
[fieldName]: value,
})
setErrors({
...errors,
[fieldName]: validateField(fieldName, value),
})
}
const touchField = (fieldName: FieldName) => {
setShowFieldError({
...showFieldError,
[fieldName]: true,
})
setErrors({
...errors,
[fieldName]: validateField(fieldName),
})
}
const getFieldError = (fieldName: FieldName) => {
if ((showAllErrors || showFieldError[fieldName]) && errors[fieldName]) {
return errors[fieldName]
}
return undefined
}
const submitHandler = (callback: (values: TData) => void) => () => {
setAllError(true)
const allErrors = getAllErrors()
setErrors(allErrors)
const hasErrors = Object.values(allErrors).some(error => typeof error === 'string')
if (!hasErrors) {
callback(values)
}
}
return { submitHandler, updateFieldValue, touchField, getFieldError, values }
}
import { Validator } from './types'
export const pipe = <T>(...validators: Validator<T>[]) => (value: T) => {
for (const validator of validators) {
const result = validator(value)
if (result) {
return result
}
}
return undefined
}
export const required = (errorMessage: string): Validator<string> => value =>
value.trim() ? undefined : errorMessage
const emailRegexp = /[a-z0-9!#$%&'*+/=?^_`{|}~-]+(?:\.[a-z0-9!#$%&'*+/=?^_`{|}~-]+)*@(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?/i
export const emailValidator = (errorMessage: string): Validator<string> => value => {
if (value !== '' && !emailRegexp.exec(value)) {
return errorMessage
}
return undefined
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment