-
-
Save suyanhanx/9ecc8b2d287fbdaefdbbbc2ef6178cb4 to your computer and use it in GitHub Desktop.
react hook form
This file contains 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" | |
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