Skip to content

Instantly share code, notes, and snippets.

@afraser
Created June 29, 2022 18:52
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save afraser/cb9e8d2ecca52dc316f83c4f4d1d7ce1 to your computer and use it in GitHub Desktop.
Save afraser/cb9e8d2ecca52dc316f83c4f4d1d7ce1 to your computer and use it in GitHub Desktop.
simple form hook
import { useEffect, useMemo, useState } from 'react'
import {
get, set, every, isPlainObject, cloneDeep, cloneDeepWith, isEqual
} from 'lodash'
import { resolver } from 'js/util/validators'
const parseIntOrNull = (val) => {
const intVal = parseInt(val)
return isNaN(intVal) ? null : intVal
}
export default function useForm (initialFields, validators = {}, dependencies = []) {
const [submitting, setSubmitting] = useState(false)
const [fields, _setFields] = useState(initialFields)
const [errors, setErrors] = useState(
cloneDeepWith(fields, value => !isPlainObject(value) ? '' : undefined)
)
const setFields = (keysAndValues) => {
let newFields = cloneDeep(fields)
Object.keys(keysAndValues).forEach(fieldName => {
set(newFields, fieldName, keysAndValues[fieldName])
if (hasError(fieldName)) {
validateField(fieldName, keysAndValues[fieldName])
}
})
_setFields(newFields)
}
const getField = (fieldName) => get(fields, fieldName)
// Returns whether the given field has an error AND has been validated
const getError = (fieldName) => get(errors, fieldName)
const hasError = (fieldName) => getError(fieldName) !== ''
const setError = (fieldName, errorMessage) => {
setErrors(set(cloneDeep(errors), fieldName, errorMessage))
}
const isPristine = useMemo(() =>
isEqual(initialFields, fields)
, [fields, ...dependencies])
// Returns the error message for the given field.
// Returns '' if the field is valid.
const getFieldError = (fieldName, value) => {
if (value === undefined) {
value = getField(fieldName)
}
const validator = get(validators, fieldName)
if (validator == null) {
return ''
}
return resolver({ value, validator })
}
// Validates the given field and updates the errors observable
// returns: whether the field is valid
const validateField = (fieldName, value) => {
const error = getFieldError(fieldName, value)
setError(fieldName, error)
return error === ''
}
// Returns whether the given field is valid without setting errors
const isFieldValid = (fieldName, value) => getFieldError(fieldName, value) === ''
const setField = (fieldName, value) => {
_setFields(set(cloneDeep(fields), fieldName, value))
if (hasError(fieldName)) {
validateField(fieldName, value)
}
}
// Validates all fields with validators and returns whether the form is valid
const validate = () => {
const validFields = Object.keys(validators).map(f => validateField(f))
return every(validFields)
}
const isValid = every(Object.keys(validators).map(f => isFieldValid(f)))
const handleSubmit = (onSubmit) =>
(evt) => {
evt.preventDefault()
if (validate()) {
setSubmitting(true)
onSubmit(fields, setSubmitting)
}
}
const bindNumericInput = (fieldName) => ({
name: fieldName,
type: 'number',
parse: parseIntOrNull,
format: String,
value: getField(fieldName) ?? '',
error: getError(fieldName),
onBlur: () => {
if (get(validators, fieldName) != null) {
validateField(fieldName)
}
},
onChange: value => setField(fieldName, value)
})
const bindInput = (fieldName) => ({
name: fieldName,
value: getField(fieldName) ?? '',
error: getError(fieldName),
onBlur: () => {
if (get(validators, fieldName) != null) {
validateField(fieldName)
}
},
onChange: value => setField(fieldName, value)
})
const bindSelect = (fieldName) => ({
name: fieldName,
value: getField(fieldName),
error: getError(fieldName),
onBlur: () => {
if (get(validators, fieldName) != null) {
validateField(fieldName)
}
},
onChange: value => setField(fieldName, value)
})
const bindFileUpload = (fieldName) => ({
file: getField(fieldName),
inputProps: {
name: fieldName,
error: getError(fieldName),
onChange: evt => {
const value = evt.currentTarget.files[0]
setField(fieldName, value)
if (get(validators, fieldName) != null) {
validateField(fieldName, value)
}
}
}
})
const bindCheckbox = (fieldName) => ({
name: fieldName,
checked: getField(fieldName),
error: getError(fieldName),
onBlur: () => {
if (get(validators, fieldName) != null) {
validateField(fieldName)
}
},
onChange: checked => setField(fieldName, checked)
})
const reset = (fields = initialFields) => {
_setFields(fields)
setErrors(cloneDeepWith(fields, value => !isPlainObject(value) ? '' : undefined))
setSubmitting(false)
}
useEffect(() => {
reset()
}, dependencies)
return {
bindCheckbox,
bindFileUpload,
bindInput,
bindNumericInput,
bindSelect,
errors,
fields,
getError,
getField,
handleSubmit,
isPristine,
isFieldValid,
isValid,
reset,
setError,
setField,
setFields,
submitting,
validate,
validateField
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment