Skip to content

Instantly share code, notes, and snippets.

@suyanhanx
Forked from buhichan/hook-form.tsx
Created November 6, 2019 09:03
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 suyanhanx/9ecc8b2d287fbdaefdbbbc2ef6178cb4 to your computer and use it in GitHub Desktop.
Save suyanhanx/9ecc8b2d287fbdaefdbbbc2ef6178cb4 to your computer and use it in GitHub Desktop.
react hook form
import * as React from "react"
import { BehaviorSubject } from 'rxjs';
const ERROR_DETAIL = Symbol("errors")
const HAS_ERROR = Symbol("has error")
const InternalSymbolKeys = [ERROR_DETAIL,HAS_ERROR]
enum FieldType {
arrayItem,
property,
nested,
}
const ERROR_FORM_NOT_INITIALIZED = "FORM_NOT_INITIALIZED"
type ItemOf<T> = T extends Array<infer V> ? V : never
type Validator<V> = (v:V)=>ErrorMap<V>
type ErrorMap<V> = {[k in keyof V]?:string|undefined|Array<Record<string,any>>|Record<string,any>}
type FormOf<T> = Partial<T> & {
[ERROR_DETAIL]:ErrorMap<Partial<T>>,
[HAS_ERROR]:boolean
}
const emptyMap = {}
const emptyFormValue = {
[ERROR_DETAIL]:{},
[HAS_ERROR]:false
}
interface Field<T> {
value:T
onChange:(v:T)=>void,
error:string|null
}
type HookFormOptions<P> = {
mapFieldProps:(value:any,onChange:(eventOrValue:any)=>void,error:string|undefined)=>P,
// initialValues:T
// validator?:Validator<T>
};
// type FormState<T> = {
// initialValues:T,
// validator?:Validator<T>,
// errors:ErrorMap<T>,
// hasError:boolean
// }
if(typeof Symbol === undefined){
throw new Error("hook-form requires supports for Symbol!")
}
export function makeHookForm<P>(options:HookFormOptions<P>){
type FieldProperty<T> = (k:keyof T)=>P
type FieldArray<T> = <K extends keyof T>(k:K)=>{
fields:{
field:FieldProperty<ItemOf<T[K]>>
remove:()=>void,
value:ItemOf<T[K]>
}[],
value:T[K],
add:(v:any)=>void,
}
type FieldMap<T> = <K extends keyof T>(k:K)=>{
field:FieldProperty<Exclude<T[K],undefined>>,
value:T[K],
}
function getFormOnChangeFunction(key:string|number,form:any,updateForm:any,type:FieldType):(eventOrValue:any)=>void{
// const symbol = Symbol.for('set '+key)
switch(type){
case FieldType.nested:
return function onChange(partial:any){
const finalValue = Array.isArray(partial) ? [...partial] : {...form[key],...partial}
return updateForm({
[key]:finalValue,
})
}
case FieldType.property:{
return function onChange(eventOrValue:any){
if(eventOrValue && eventOrValue.target){
let target = eventOrValue.target
if('value' in target){
eventOrValue = target.value
}
}
if(eventOrValue === undefined){
eventOrValue = null
}
return updateForm({
[key]:eventOrValue,
})
}
}
case FieldType.arrayItem:{
//key is index, form is a list
return function onChange(partial:any){
const newList = form.slice()
newList[key as number] = {...form[key],...partial}
return updateForm(newList)
}
}
}
// return form[symbol]
}
function makeFormPropertyField<U>(form:Partial<U>|null,updateForm:any,errorMap:ErrorMap<any>){
return <K extends keyof U>(key:K)=>{
return options.mapFieldProps(
form ? form[key] != undefined ? form[key] : undefined : undefined ,
getFormOnChangeFunction(key,form,updateForm,FieldType.property),
errorMap[key] as string|undefined
)
}
}
function makeFormMapField<U>(form:Partial<U>|null,updateForm:any,errorMap:ErrorMap<any>){
return <K extends keyof U>(key:K)=>{
const onChange = getFormOnChangeFunction(key,form,updateForm,FieldType.nested)
const value = form && (form as any)[key] || emptyFormValue
const error = errorMap && errorMap[key] || emptyMap as any
return {
value,
field:makeFormPropertyField<U[K]>(value,onChange,error),
array:makeFormArrayField<U[K]>(value,onChange,error),
map:makeFormMapField<U[K]>(value,onChange,error)
}
}
}
const emptyArray = [] as any[]
function makeFormArrayField<U>(parent:Partial<U>|null,updateParent:any,errorMap:ErrorMap<any>){
return <K extends keyof U>(key:K)=>{
const onChange = getFormOnChangeFunction(key,parent,updateParent,FieldType.nested)
const value:any[] = parent && (parent as any)[key] instanceof Array ? (parent as any)[key] : emptyArray
const error = errorMap[key] as any
return {
error: error,
value: value,
fields: value.map((v,i)=>{
const onArrayItemChange = getFormOnChangeFunction(i,value,onChange,FieldType.arrayItem)
return {
value:v,
field:makeFormPropertyField<ItemOf<U[K]>>(v,onArrayItemChange,error && error[i] || emptyMap),
map:makeFormMapField<ItemOf<U[K]>>(v,onArrayItemChange,error && error[i] || emptyMap),
array:makeFormArrayField<ItemOf<U[K]>>(v,onArrayItemChange,error && error[i] || emptyMap),
remove:()=>{
const clone = value.slice()
clone.splice(i,1)
updateParent({
...parent,
[key]:clone
})
}
}
}),
add: (newItem:any)=>{
onChange(value.concat(newItem))
},
onChange: (newItems:any[])=>{
onChange(newItems)
}
}
}
}
/**
*
* @param initialValues IMPORTANT: initialValues cannot change in every render, or else it will become an infinite loop!!!
* @param validator validator.
*/
function useForm<T=any>(initialValues: Partial<T> | null, validator?:Validator<Partial<T>>){
const [form,setForm] = React.useState<FormOf<T> | null>(null)
const updateForm = React.useMemo(()=>(partialUpdates:FormOf<T>|null)=>{
setForm(old=>{
if(!partialUpdates){
return null
}
const newFormValues = {
...old || {},
...partialUpdates,
} as Partial<T>
const newErrors:ErrorMap<Partial<T>> = validator ? validator(newFormValues) : {}
const finalNewValues = {
...newFormValues,
[ERROR_DETAIL]:newErrors,
[HAS_ERROR]:doHaveError(newErrors)
}
return finalNewValues
})
},[validator])
React.useEffect(()=>{
updateForm(initialValues as any)
},[initialValues])
const hasError = form && form[HAS_ERROR] || false
const field =( makeFormPropertyField(form,updateForm,form && form[ERROR_DETAIL] || emptyMap)) as (k:keyof T)=>P
const fieldArray = (makeFormArrayField(form,updateForm,form && form[ERROR_DETAIL] || emptyMap) as any) as FieldArray<T>
const fieldMap = ( makeFormMapField(form,updateForm,form && form[ERROR_DETAIL] || emptyMap) as any) as FieldMap<T>
return {
value: form as Partial<T> | null, //hide symbols
onChange: updateForm as (v:Partial<T>)=>void,
hasError,
field,
fieldArray,
fieldMap,
setError(err:any){
updateForm({
[ERROR_DETAIL]:err,
[HAS_ERROR]:true
} as any)
}
}
}
return useForm
}
function doHaveError(form:any):boolean{
if(form instanceof Array){
return form.some(doHaveError)
}else if(typeof form === 'object'){
return Object.keys(form).some(k=>doHaveError(form[k]))
}else{
return !!form
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment