import React, { useEffect, useState } from 'react' export type FieldOptions = { extractor?: useFormExtractor getter: G setter: S } type RuleFunctionReturn = boolean | string type RuleFunction = (value: I) => RuleFunctionReturn | Promise type Rule = RuleFunction | RegExp type RuleObject = Rule | { rule: Rule; message: string } type RuleSet = RuleObject | RuleObject[] function isSimpleRule(obj: RuleObject): obj is Rule { return obj instanceof RegExp || typeof obj === 'function' } export type useFormExtractor = (from: any) => any export const HTMLInputExtractor: useFormExtractor = (e: React.FormEvent) => e.currentTarget.value export const HTMLCheckboxExtractor: useFormExtractor = (e: React.FormEvent) => e.currentTarget.checked export type FormOptions = { rules: R // fields: FieldOptions } // Form = Type of form // R = Rules, derived from F // E = Errors, derived from F export const useForm =
}, E extends { [key in keyof R]?: RuleFunctionReturn }>(init: Form, options?: FormOptions) => { const validators: R = options?.rules ?? ({} as R) const [form, setForm] = useState(init) const [errors, setErrors] = useState({} as E) const [isValid, setIsValid] = useState(true) useEffect(() => { setIsValid(!Object.values(errors).reduce((acc, cur) => acc || cur !== undefined, false)) }, [errors]) const setField = (key: A, value: Form[A]) => { setForm({ ...form, [key]: value, }) } async function applyRule(value: any, rule: Rule): Promise { if (typeof rule === 'function') return await rule(value) if (rule instanceof RegExp) return rule.test(value) throw new Error(`Unsupported validator: ${rule}`) } async function validate(key: K, value: Form[K]) { const set: RuleSet | 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, }) } function update(key: A, extractor?: (e: RAW) => Form[A]) { return (value: RAW) => { const extracted = extractor ? extractor(value) : HTMLInputExtractor(value) setField(key, extracted) validate(key, extracted) } } type FieldReturn = { [getter in G]: ReturnType> } & { [setter in S]: Form[K] } function field(key: K): FieldReturn function field(key: K, opts: FieldOptions): FieldReturn function field(key: K, opts?: FieldOptions): FieldReturn { return { [opts?.getter || 'onChange']: update(key, opts?.extractor), [opts?.setter || 'value']: form[key], } as FieldReturn } return { form, field, errors, isValid, setForm, setErrors, setField } }