import expr from 'property-expr' ; import useUpdatedRef from '@restart/hooks/useUpdatedRef' ; import React , { useCallback , useMemo , useContext , useRef , useState , useLayoutEffect , SyntheticEvent , } from 'react' ; import updateIn from './utils/updateIn' ; import { MapToValue } from './useBinding' ; export type Mapper < TOut , TIn = any > = ( input : TIn ) => TOut ; type BindingValue = Record < PropertyKey , any > | unknown [ ] ; export const formGetter = ( path , model ) => path ? expr . getter ( path , true ) ( model || { } ) : model ; export function formSetter < TValue extends BindingValue > ( path , value , fieldValue ) { return updateIn ( value , path , fieldValue ) ; } type BindingContextValue = { getValue < T > ( path ) : T ; updateBindingValue < T > ( path , args ) : void ; updateFormValue : ( nextFormValue : any ) => void ; formValue : any ; } ; export const BindingContext = React . createContext < BindingContextValue > ( { getValue ( ) { } , updateBindingValue ( ) { } , } as any ) ; BindingContext . displayName = 'ReactFormalValueContext' ; export const useBindingContext = ( ) => { return useContext ( BindingContext ) ; } ; type Setter < TValue extends BindingValue > = ( path : string , value : TValue | undefined , fieldValue : unknown ) => TValue ; type Props < TValue extends BindingValue > = { formValue ? : TValue ; onChange ( value , paths ) : void ; getSchemaForPath : ( path : string , value : TValue ) => any ; getter ? : ( path : string , value ? : TValue ) => any ; setter ? : ( path : string , value : TValue | undefined , fieldValue : unknown , defaultSetter : Setter < TValue > ) => TValue ; } ; const isEvent = ( e ) : e is SyntheticEvent => typeof e == 'object' && e != null && 'target' in e ; function parseValueFromEvent ( target , fieldValue , fieldSchema ? ) { const { type , value , checked , options , multiple , files } = target ; if ( type === 'file' ) return multiple ? files : files && files [ 0 ] ; if ( multiple ) { const innerType = fieldSchema ?. _subType ?. _type ; return Array . from ( options ) . filter ( ( opt ) => opt . selected ) . map ( ( { value : option } ) => innerType == 'number' ? parseFloat ( option ) : option ) ; } if ( /number|range/ . test ( type ) ) { let parsed = parseFloat ( value ) ; return isNaN ( parsed ) ? null : parsed ; } if ( type === 'checkbox' ) { const isArray = Array . isArray ( fieldValue ) ; const isBool = ! isArray && ( fieldSchema as any ) ?. _type === 'boolean' ; if ( isBool ) return checked ; const nextValue = isArray ? [ ... fieldValue ] : [ ] ; const idx = nextValue . indexOf ( value ) ; if ( checked ) { if ( idx === - 1 ) nextValue . push ( value ) ; } else nextValue . splice ( idx , 1 ) ; return nextValue ; } return value ; } function useFormBindingContext < TValue extends BindingValue > ( { formValue , onChange , setter = formSetter , getter = formGetter , getSchemaForPath , } ) { let formValueRef = useUpdatedRef ( formValue ) ; let pendingChangeRef = useRef ( false ) ; let [ pendingChange , setPendingChange ] = useState < [ TValue , string [ ] ] > ( [ formValue ! , [ ] , ] ) ; useLayoutEffect ( ( ) => { const [ nextFormValue , paths ] = pendingChange ; if ( pendingChangeRef . current ) { pendingChangeRef . current = false ; onChange ( nextFormValue , paths ) ; } } ) ; const updateBindingValue = useCallback ( ( mapValue , args ) => { setPendingChange ( ( pendingState ) => { let [ nextModel , paths ] = pendingState ; if ( ! pendingChangeRef . current ) { pendingChangeRef . current = true ; nextModel = formValueRef . current ! ; paths = [ ] ; } Object . keys ( mapValue ) . forEach ( ( key ) => { let field = mapValue [ key ] ; let value ; if ( typeof field === 'function' ) value = field ( ... args ) ; else if ( field === '.' || field == null || args [ 0 ] == null ) value = args [ 0 ] ; else { value = expr . getter ( field , true ) ( args [ 0 ] ) ; } if ( paths . indexOf ( key ) === - 1 ) paths . push ( key ) ; if ( isEvent ( value ) ) value = parseValueFromEvent ( value . target as any , formGetter ( key , nextModel ) , getSchemaForPath ( key , nextModel ) ) ; nextModel = setter ! ( key , nextModel , value , formSetter ) ; } ) ; return [ nextModel , paths ] ; } ) ; } , [ formValueRef , getSchemaForPath , setter ] ) ; const getValue = useCallback ( ( pathOrAccessor ) => typeof pathOrAccessor === 'function' ? pathOrAccessor ( formValue , getter ) : getter ( pathOrAccessor , formValue ) , [ getter , formValue ] ) ; return useMemo ( ( ) => ( { getValue , updateBindingValue , updateFormValue : ( nextFormValue ) => { setPendingChange ( ( ) => { pendingChangeRef . current = true ; return [ nextFormValue , [ ] ] ; } ) ; } , formValue , } ) , [ getValue , updateBindingValue , setPendingChange , formValue ] ) ; } export default useFormBindingContext ;