fielder
Version:
A field-first form library for React and React Native
179 lines (157 loc) • 4.77 kB
text/typescript
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,
]
);
};