UNPKG

fielder

Version:

A field-first form library for React and React Native

179 lines (157 loc) 4.77 kB
import { useContext, useCallback, useMemo, useRef } from 'react'; import { useLayoutEffect } from './util'; import { FielderContext } from './context'; import { FormState, FieldState, FormSchemaType, ObjectValidation, ValidationFn, FormValue, } from './types'; export type UseFieldProps<T = any> = { /** Field name. */ readonly name: string; /** Field value. */ readonly value: T; /** Change event handler. */ readonly onChange: (e: { currentTarget: { value: any } } | T) => void; /** Blur event handler (sets blurred state). */ readonly onBlur: () => void; }; export type UseFieldMeta = { /** Field error. */ readonly error?: FieldState['error']; /** Field is currently valid. */ readonly isValid: FieldState['isValid']; /** Field is currently being validated (async). */ readonly isValidating: FieldState['isValidating']; /** Field has been blurred since mount. */ readonly hasBlurred: FieldState['hasBlurred']; /** Field has been changed since mount. */ readonly hasChanged: FieldState['hasChanged']; }; export type UseFieldArgs< S extends FormSchemaType = FormSchemaType, K extends keyof S = keyof S, V extends FormValue = S[K] > = { /** Unique identifier for field. */ readonly name: K; /** Starting value. */ readonly initialValue: V; /** Validation function (throws errors). */ readonly validate?: ObjectValidation<V, S> | ValidationFn<V, S>; /** Should destroy value when useField hook is unmounted. */ readonly destroyOnUnmount?: boolean; }; export type UseFieldResponse = readonly [UseFieldProps, UseFieldMeta]; export const useField = <T extends FormSchemaType = FormSchemaType>({ name, validate, initialValue, destroyOnUnmount = false, }: UseFieldArgs<T>): UseFieldResponse => { const destroyRef = useRef(destroyOnUnmount); const initialMount = useRef(true); const { fields, blurField, premountField, mountField, unmountField, setFieldValue, setFieldValidation, } = useContext<FormState<any>>(FielderContext); // Set unchanging initial values const initial = useMemo(() => ({ name, value: initialValue, validate }), []); // eslint-disable-line const field = useMemo(() => { if (initialMount.current) { // Simulate mounting without committing to state return premountField({ name: initial.name, initialValue: initial.value, validate: initial.validate, }); } return fields[initial.name]; }, [initial.name, initial.value, initial.validate, premountField, fields]); useLayoutEffect(() => { mountField({ name: initial.name, initialValue: initial.value, validate: initial.validate, }); }, [initial.name, initial.validate, initial.value, mountField]); useLayoutEffect( () => () => { initialMount.current = false; }, [] ); useMemo(() => (destroyRef.current = destroyOnUnmount), [destroyOnUnmount]); useLayoutEffect( () => () => { unmountField({ name: initial.name, destroy: destroyRef.current }); }, [unmountField, initial.name] ); /** Update field state on validation config change. */ useLayoutEffect(() => { if (initialMount.current) { initialMount.current = false; return; } setFieldValidation({ name: initial.name, validation: validate }); }, [validate, initial.name, setFieldValidation]); const onBlur = useCallback(() => blurField({ name: initial.name }), [ initial.name, blurField, ]); const onChange = useCallback<UseFieldProps['onChange']>( (e) => { // If initial value is boolean, // toggle value on change (i.e. checkbox) if (typeof initial.value === 'boolean') { return setFieldValue({ name: initial.name, value: (v: boolean) => !v, }); } const value = typeof e === 'object' && 'currentTarget' in e ? e.currentTarget.value : e; return setFieldValue({ name: initial.name, value: (previousValue: any) => { if (!Array.isArray(previousValue)) { return value; } return previousValue.includes(value) ? previousValue.filter((v) => v !== value) : [...previousValue, value]; }, }); }, [initial.name, initial.value, setFieldValue] ); const { value, error, isValid, isValidating, hasChanged, hasBlurred } = field; return useMemo( () => [ { name: initial.name, value, onBlur, onChange }, { error, isValid, isValidating, hasBlurred, hasChanged }, ], [ initial.name, value, onBlur, onChange, error, isValid, isValidating, hasChanged, hasBlurred, ] ); };