Skip to content

Instantly share code, notes, and snippets.

@buhichan
Last active July 13, 2020 06:18
Show Gist options
  • Save buhichan/ebe328496037f848be99649815feb5bf to your computer and use it in GitHub Desktop.
Save buhichan/ebe328496037f848be99649815feb5bf to your computer and use it in GitHub Desktop.
react hook form
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