Last active
November 12, 2019 18:49
-
-
Save KacperKozak/d6757f960db72481191d996e14a1f4bf to your computer and use it in GitHub Desktop.
Simple form hook validation for React
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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> | |
) | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 } | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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