import { reactive , computed , ref , Ref , watch , } from 'vue' ; import { RuleItem } from 'async-validator' ; import DeepValidator from './deepValidator' ; import { isAllUnmounted , get , set , toPathString , getDOMNode , FieldNode , } from './utils' ; export type ValidateMode = 'change' | 'focusout' | 'submit' ; export type FormOptions < Values extends object > = { defaultValues ? : Values ; shouldUnregister ? : boolean ; validateMode ? : ValidateMode ; } ; export type FieldOptions = { rule ? : RuleItem ; } ; export type Error = { message : string ; field : string ; } ; export type Errors = { [ field ] : Error [ ] | undefined ; } ; export const useForm = < T extends object > ( options = { } ) => { const { defaultValues = { } as T , shouldUnregister = true , validateMode = 'change' , } = options ; const validator = DeepValidator ( { } ) ; const fieldsRef = ref < { [ key ] : Set < Ref < FieldNode > > ; } > ( { } ) ; const fieldValues = reactive ( defaultValues ) as any ; const errors = reactive ( { } as Errors ) ; const clearErrors = ( ) => { Object . keys ( errors ) . forEach ( ( key ) => { delete errors [ key ] ; } ) ; } ; const setErrors = ( newErrors ) => { clearErrors ( ) ; Object . keys ( newErrors ) . forEach ( ( key ) => { errors [ key ] = newErrors [ key ] ; } ) ; } ; const getFieldValues = ( ) => Object . keys ( fieldsRef . value ) . reduce ( ( acc , path ) => { const value = get ( fieldValues , path ) ; if ( ! shouldUnregister ) { set ( acc , path , value ) ; return acc ; } if ( ! isAllUnmounted ( fieldsRef . value [ path ] ) ) { set ( acc , path , value ) ; return acc ; } return acc ; } , { } as Partial < T > ) ; const validateFields = async ( ) => { try { const noErrors = await validator . validate ( getFieldValues ( ) ) ; clearErrors ( ) ; return noErrors ; } catch ( error ) { setErrors ( error ) ; return error ; } } ; const validateField = async ( path ) => { try { const noError = await validator . validateField ( path , get ( fieldValues , path ) ) ; delete errors [ path ] ; return noError ; } catch ( error ) { errors [ path ] = error ; return error ; } } ; const useField = ( path , options = { } ) => { const pathStr = toPathString ( path ) ; const fieldRef = ref < FieldNode > ( null ) ; const { rule } = options ; const validateWithoutError = async ( ) => { await validateField ( pathStr ) ; } ; if ( rule ) { validator . registerRule ( pathStr , rule ) ; } const value = computed ( { get : ( ) => get ( fieldValues , pathStr ) , set : ( newValue ) => { set ( fieldValues , pathStr , newValue ) ; } , } ) ; const listener = ref ( ( e ) => { validateWithoutError ( ) ; } ) ; const getRef = ( nodeRef ) : void => { const domNode = getDOMNode ( nodeRef ) ; if ( domNode !== null ) { if ( validateMode === 'focusout' ) { domNode . addEventListener ( 'focusout' , listener . value ) ; } } else { const prevDomNode = getDOMNode ( fieldRef . value ) ; if ( prevDomNode !== null ) { if ( validateMode === 'focusout' ) { prevDomNode . removeEventListener ( 'focusout' , listener . value ) ; } } } fieldRef . value = nodeRef ; const nodeSet = fieldsRef . value [ pathStr ] || new Set ( ) ; nodeSet . add ( fieldRef ) ; fieldsRef . value [ pathStr ] = nodeSet ; if ( shouldUnregister && isAllUnmounted ( nodeSet ) ) { validator . removeRule ( pathStr ) ; } } ; watch ( value , async ( ) => { if ( validateMode === 'change' ) { validateWithoutError ( ) ; } } ) ; return reactive ( { ref : getRef , value , error : computed ( ( ) => errors [ pathStr ] ) , validate : ( ) => validateField ( pathStr ) , } ) ; } ; const handleSubmit = ( onSubmit ) => async ( e ? ) => { if ( e ) { e . preventDefault ( ) ; } const error = await validateFields ( ) ; if ( ! error ) { onSubmit ( getFieldValues ( ) ) ; } } ; return reactive ( { values : fieldValues as T , useField , get : ( path , defaultValue ? ) => get ( fieldValues , path , defaultValue ) , set : ( path , value ) => set ( fieldValues , path , value ) , getFieldValues , validateFields , validateField , errors , handleSubmit , } ) ; } ;