mirror of
https://github.com/cupcakearmy/formhero.git
synced 2024-12-23 00:26:23 +00:00
100 lines
3.7 KiB
TypeScript
100 lines
3.7 KiB
TypeScript
import * as React from 'react'
|
|
import { useMemo, useState } from 'react'
|
|
|
|
// Possible future ideas
|
|
// TODO: Scroll to error field
|
|
// TODO: Focus on error field
|
|
|
|
export type FieldOptions<G extends string = 'onChange', S extends string = 'value', T = any> = {
|
|
extractor?: useFormExtractor<T> | null
|
|
getter?: G
|
|
setter?: S
|
|
}
|
|
|
|
type RuleFunctionReturn = boolean | string
|
|
type RuleFunction<I, F> = (value: I, data: F) => RuleFunctionReturn | Promise<RuleFunctionReturn>
|
|
type Rule<I, F> = RuleFunction<I, F> | RegExp
|
|
type RuleObject<I, F> = Rule<I, F> | { rule: Rule<I, F>; message: string }
|
|
type RuleSet<I, F> = RuleObject<I, F> | RuleObject<I, F>[]
|
|
|
|
function isSimpleRule<I, F>(obj: RuleObject<I, F>): obj is Rule<I, F> {
|
|
return obj instanceof RegExp || typeof obj === 'function'
|
|
}
|
|
|
|
export type useFormExtractor<T = any> = (from: any) => T
|
|
export const NoExtractor: useFormExtractor = (v: unknown) => v
|
|
export const HTMLInputExtractor: useFormExtractor = (e: React.FormEvent<HTMLInputElement>) => e.currentTarget.value
|
|
export const HTMLCheckboxExtractor: useFormExtractor = (e: React.FormEvent<HTMLInputElement>) => e.currentTarget.checked
|
|
|
|
export type FormOptions<R> = {
|
|
rules: R
|
|
}
|
|
|
|
// F = Type of form
|
|
// R = Rules, derived from F
|
|
// E = Errors, derived from F
|
|
export const useForm = <F extends object, R extends { [K in keyof F]?: RuleSet<F[K], F> }, E extends { [key in keyof R]?: RuleFunctionReturn }>(init: F, options?: FormOptions<R>) => {
|
|
const validators: R = options?.rules ?? ({} as R)
|
|
|
|
const [form, setForm] = useState<F>(init)
|
|
const [errors, setErrors] = useState<E>({} as E)
|
|
const isValid = useMemo(() => {
|
|
return !Object.values(errors).reduce((acc, cur) => acc || cur !== undefined, false)
|
|
}, [errors])
|
|
|
|
const setField = <A extends keyof F>(key: A, value: F[A]) => {
|
|
setForm({
|
|
...form,
|
|
[key]: value,
|
|
})
|
|
}
|
|
|
|
async function applyRule<I>(value: any, rule: Rule<I, F>): Promise<RuleFunctionReturn> {
|
|
if (typeof rule === 'function') return await rule(value, form)
|
|
if (rule instanceof RegExp) return rule.test(value)
|
|
throw new Error(`Unsupported validator: ${rule}`)
|
|
}
|
|
|
|
async function validate<K extends keyof F>(key: K, value: F[K]) {
|
|
const set: RuleSet<F[K], F> | undefined = validators[key] as any
|
|
if (!set) return
|
|
|
|
const rules = Array.isArray(set) ? set : [set]
|
|
let newValue = undefined
|
|
for (const rule of rules) {
|
|
const simple = isSimpleRule(rule)
|
|
const fn = simple ? rule : rule.rule
|
|
const result = await applyRule(value, fn)
|
|
if (result !== true) {
|
|
newValue = simple ? (typeof result === 'string' ? result : true) : rule.message
|
|
break
|
|
}
|
|
}
|
|
setErrors({
|
|
...errors,
|
|
[key]: newValue,
|
|
})
|
|
}
|
|
|
|
// Internal use
|
|
function update<A extends keyof F>(key: A, extractor?: useFormExtractor<F[A]> | null) {
|
|
return (value: any) => {
|
|
const extracted = extractor ? extractor(value) : extractor === undefined ? HTMLInputExtractor(value) : value
|
|
setField(key, extracted)
|
|
validate(key, extracted)
|
|
}
|
|
}
|
|
|
|
type FieldReturn<K extends keyof F, G extends string, S extends string> = { [getter in G]: ReturnType<typeof update<K>> } & { [setter in S]: F[K] }
|
|
function field<K extends keyof F>(key: K): FieldReturn<K, 'onChange', 'value'>
|
|
function field<K extends keyof F, G extends string, S extends string>(key: K, opts: FieldOptions<G, S, F[K]>): FieldReturn<K, G, S>
|
|
function field<K extends keyof F, G extends string, S extends string>(key: K, opts?: FieldOptions<G, S, F[K]>): FieldReturn<K, G, S> {
|
|
return {
|
|
[opts?.getter || 'onChange']: update<K>(key, opts?.extractor),
|
|
[opts?.setter || 'value']: form[key],
|
|
} as FieldReturn<K, G, S>
|
|
}
|
|
|
|
return { form, field, errors, isValid, setForm, setErrors, setField }
|
|
}
|