Last active
July 13, 2020 06:18
-
-
Save buhichan/ebe328496037f848be99649815feb5bf to your computer and use it in GitHub Desktop.
react hook form
This file contains hidden or 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 * as React from "react"; | |
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 type HookFormFieldProperty<T,P> = (k:keyof T)=>P | |
export type HookFormFieldArray<T,P> = { | |
fields:{ | |
field:HookFormFieldProperty<ItemOf<T>,P> | |
fieldArray: <K extends keyof ItemOf<T>>(k: K)=>HookFormFieldArray<ItemOf<T>[K],P>, | |
fieldMap: <K extends keyof ItemOf<T>>(k: K)=>HookFormFieldMap<ItemOf<T>[K],P>, | |
remove:()=>void, | |
value:ItemOf<T>, | |
onChange:(v: ItemOf<T>)=>void, | |
}[], | |
value:T, | |
add:(v:Partial<ItemOf<T>>)=>void, | |
insert:(v:Partial<ItemOf<T>>,index:number)=>void, | |
} | |
export type HookFormFieldMap<T,P> = { | |
field:HookFormFieldProperty<Exclude<T,undefined>,P>, | |
value:T, | |
} | |
export type HookFormInstance<T,P> = { | |
value: T | null, //hide symbols | |
onChange: (v:Partial<T>)=>void, | |
hasError: boolean, | |
field: HookFormFieldProperty<T,P>, | |
fieldArray: <K extends keyof T>(k: K)=>HookFormFieldArray<T[K],P>, | |
fieldMap: <K extends keyof T>(k: K)=>HookFormFieldMap<T[K],P>, | |
setError: (err:any)=>void, | |
} | |
export class HookForm<P>{ | |
constructor(private options:HookFormOptions<P>){ | |
this.useForm = this.useForm.bind(this) | |
} | |
private getFormOnChangeFunction(key:string|number|symbol,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 && 'value' in eventOrValue.target){ | |
eventOrValue = 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] | |
} | |
private makeFormPropertyField<U>(form:Partial<U>|null,updateForm:any,errorMap:ErrorMap<any>){ | |
return <K extends keyof U>(key:K)=>{ | |
return this.options.mapFieldProps( | |
form ? form[key] != undefined ? form[key] : undefined : undefined , | |
this.getFormOnChangeFunction(key,form,updateForm,FieldType.property), | |
errorMap[key] as string|undefined | |
) | |
} | |
} | |
private makeFormMapField<U>(form:Partial<U>|null,updateForm:any,errorMap:ErrorMap<any>){ | |
return <K extends keyof U>(key:K)=>{ | |
const onChange = this.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:this.makeFormPropertyField<U[K]>(value,onChange,error), | |
array:this.makeFormArrayField<U[K]>(value,onChange,error), | |
map:this.makeFormMapField<U[K]>(value,onChange,error) | |
} | |
} | |
} | |
private makeFormArrayField<U>(parent:Partial<U>|null,updateParent:any,errorMap:ErrorMap<any>){ | |
return <K extends keyof U>(key:K)=>{ | |
const onChange = this.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 = this.getFormOnChangeFunction(i,value,onChange,FieldType.arrayItem) | |
return { | |
value:v, | |
onChange:(v:any)=>{ | |
const clone = value.slice() | |
clone[i] = v | |
updateParent({ | |
...parent, | |
[key]:clone | |
}) | |
}, | |
field:this.makeFormPropertyField<ItemOf<U[K]>>(v,onArrayItemChange,error && error[i] || emptyMap), | |
fieldMap:this.makeFormMapField<ItemOf<U[K]>>(v,onArrayItemChange,error && error[i] || emptyMap), | |
fieldArray:this.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)) | |
}, | |
insert: (newItem:any, index:number)=>{ | |
const clone = value.slice() | |
clone.splice(index,0,newItem) | |
onChange(clone) | |
}, | |
onChange: (newItems:any[])=>{ | |
onChange(newItems) | |
} | |
} | |
} | |
} | |
public 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 =( this.makeFormPropertyField(form,updateForm,form && form[ERROR_DETAIL] || emptyMap)) as (k:keyof T)=>P | |
const fieldArray = (this.makeFormArrayField(form,updateForm,form && form[ERROR_DETAIL] || emptyMap) as any) | |
const fieldMap = ( this.makeFormMapField(form,updateForm,form && form[ERROR_DETAIL] || emptyMap) as any) | |
return { | |
value: form as Partial<T> | null, //hide symbols | |
onChange: updateForm as (v:Partial<T>)=>void, | |
hasError, | |
field, | |
fieldArray, | |
fieldMap, | |
setError(err:any){ | |
setForm(form=>({ | |
...form, | |
[ERROR_DETAIL]:err, | |
[HAS_ERROR]:true | |
}) as any) | |
} | |
} as HookFormInstance<T,P> | |
} | |
} | |
const emptyArray = [] as any[] | |
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