From 677dbe469184f6880ce18c961fa37e0434734972 Mon Sep 17 00:00:00 2001 From: Koichi Kobayashi Date: Sun, 30 Oct 2022 23:40:44 +0900 Subject: [PATCH] examples(RHF): support for default{Value,Checked} & useFieldArray (#134) * examples(RHF): support for defaultValue & useFieldArray * examples(RHF): support for defaultChecked * examples(RHF): fix array fields to reset properly * examples(RHF): fix the sample page for proper behavior --- .../react-hook-form/pages/form/[index].tsx | 141 +++++++++++++++-- .../react-hook-form/src/hooks/useFormSync.ts | 144 +++++++++++++++++- 2 files changed, 268 insertions(+), 17 deletions(-) diff --git a/examples/react-hook-form/pages/form/[index].tsx b/examples/react-hook-form/pages/form/[index].tsx index 2125bba3..f0730e88 100755 --- a/examples/react-hook-form/pages/form/[index].tsx +++ b/examples/react-hook-form/pages/form/[index].tsx @@ -1,17 +1,24 @@ -import { object, string } from '@recoiljs/refine' +import type { GetServerSideProps, NextPage } from 'next' import Head from 'next/head' import { useRouter } from 'next/router' import { SubmitHandler } from 'react-hook-form/dist/types/form' import { syncEffect } from 'recoil-sync' +import { array, object, string } from '@recoiljs/refine' import { initializableAtom } from 'recoil-sync-next' import { useFormSync } from '../../src/hooks/useFormSync' import styles from '../../styles/form.module.css' -import type { NextPage } from 'next' - type FormState = { name: string comment: string + time: string + selectedRadio: string + selectedCheckbox: readonly string[] + items: readonly { + value: string + radio: string + checkbox: string + }[] } const formState = initializableAtom({ @@ -21,18 +28,47 @@ const formState = initializableAtom({ refine: object({ name: string(), comment: string(), + time: string(), + selectedRadio: string(), + selectedCheckbox: array(string()), + items: array( + object({ + value: string(), + radio: string(), + checkbox: string(), + }) + ), }), }), ], }) -const Form: NextPage = () => { +let count = 1 +const createNewItem = (): FormState['items'][number] => ({ + value: '', + radio: `${count}`, + checkbox: `${count++}`, +}) + +const Form: NextPage = (props) => { // check render console.log('Form: re render') - const { register, onChangeForm, handleSubmit } = useFormSync( - formState({ name: 'a', comment: 'b' }) - ) + const { + control, + registerWithDefaultValue, + registerWithDefaultChecked, + onChangeForm, + handleSubmit, + reset, + useFieldArraySync, + } = useFormSync(formState(props)) + const { fields, append, prepend, remove, swap, move, insert } = + useFieldArraySync({ + control, + name: 'items', + }) + const router = useRouter() const onSubmit: SubmitHandler = async (data) => { console.log('submit data', data) @@ -53,14 +89,81 @@ const Form: NextPage = () => {
name
- +
comment
- + +
+
time
+
+ +
+
items
+
+ +
    + {fields.map((field, index) => ( +
  • + {index} + + + + + + + + +
  • + ))} +
+
- + + @@ -68,3 +171,21 @@ const Form: NextPage = () => { } export default Form + +export const getServerSideProps: GetServerSideProps = async ({ + params, +}) => { + return { + props: { + name: 'a', + comment: 'b', + time: new Date().toLocaleTimeString(), + selectedRadio: 'A', + selectedCheckbox: ['A'], + items: [ + { value: '', radio: 'A', checkbox: 'A' }, + { value: '', radio: 'B', checkbox: 'B' }, + ], + }, + } +} diff --git a/examples/react-hook-form/src/hooks/useFormSync.ts b/examples/react-hook-form/src/hooks/useFormSync.ts index f5737214..1b479074 100644 --- a/examples/react-hook-form/src/hooks/useFormSync.ts +++ b/examples/react-hook-form/src/hooks/useFormSync.ts @@ -1,9 +1,26 @@ import { useCallback, useRef } from 'react' import { DeepPartial, + FieldPath, FieldValues, + InternalFieldName, + RegisterOptions, + Resolver, + useFieldArray, + UseFieldArrayAppend, + UseFieldArrayInsert, + UseFieldArrayMove, + UseFieldArrayPrepend, + UseFieldArrayProps, + UseFieldArrayRemove, + UseFieldArrayReplace, + UseFieldArrayReturn, + UseFieldArraySwap, + UseFieldArrayUpdate, useForm, UseFormProps, + UseFormRegister, + UseFormRegisterReturn, UseFormReturn, } from 'react-hook-form' import { @@ -13,10 +30,28 @@ import { useSetRecoilState, } from 'recoil' +type RegisterWithDefaultChecked< + TFieldValues extends FieldValues, + TContext = any +> = = FieldPath>( + name: TFieldName, + value: TFieldValues[TFieldName] extends Readonly> + ? E + : TFieldValues[TFieldName], + options?: RegisterOptions +) => UseFormRegisterReturn + type UseFormSyncReturn< TFieldValues extends FieldValues, TContext = any -> = UseFormReturn & { onChangeForm: () => void } +> = UseFormReturn & { + registerWithDefaultValue: UseFormRegister + registerWithDefaultChecked: RegisterWithDefaultChecked + onChangeForm: () => void + useFieldArraySync: ( + props: UseFieldArrayProps + ) => UseFieldArrayReturn +} export function useFormSync( formState: RecoilState, @@ -36,7 +71,14 @@ export function useFormSync( const defaultValuesRef = useRef() defaultValuesRef.current ??= getDefaultValues() + const getDefaultValue = (name: string) => { + return name + .split('.') + .reduce((value, segment) => value?.[segment], defaultValuesRef.current!) + } + const { + register, getValues, reset: resetForm, ...rest @@ -47,16 +89,104 @@ export function useFormSync( defaultValues: defaultValuesRef.current as DeepPartial, }) - const setFormValues = useSetRecoilState(formState) - const onChangeForm = useCallback(() => { - setFormValues(getValues()) - }, [setFormValues, getValues]) + const registerWithDefaultValue: UseFormRegister = useCallback( + (name, options) => { + const defaultValue = getDefaultValue(name) ?? null + return { ...register(name, options), defaultValue } + }, + [register] + ) + + const registerWithDefaultChecked: RegisterWithDefaultChecked< + TFieldValues, + TContext + > = useCallback( + (name, value, options) => { + const defaultValue = getDefaultValue(name) + const defaultChecked = + defaultValue == null + ? false + : Array.isArray(defaultValue) + ? defaultValue.includes(value) + : defaultValue === value + return { ...register(name, options), value, defaultChecked } + }, + [register] + ) const resetState = useResetRecoilState(formState) const reset = useCallback(() => { resetState() - resetForm(getDefaultValues()) + resetForm(structuredClone(getDefaultValues())) }, [getDefaultValues, resetForm, resetState]) - return { ...rest, getValues, reset, onChangeForm } + const setFormValues = useSetRecoilState(formState) + const onChangeForm = useCallback(() => { + setFormValues(structuredClone(getValues())) + }, [setFormValues, getValues]) + + const useFieldArraySync = ( + props: UseFieldArrayProps + ): UseFieldArrayReturn => { + const origin = useFieldArray(props) + const swap: UseFieldArraySwap = (indexA, indexB) => { + origin.swap(indexA, indexB) + onChangeForm() + } + const move: UseFieldArrayMove = (indexA, indexB) => { + origin.move(indexA, indexB) + onChangeForm() + } + const prepend: UseFieldArrayPrepend = (value, options) => { + origin.prepend(value, options) + onChangeForm() + } + const append: UseFieldArrayAppend = (value, options) => { + origin.append(value, options) + onChangeForm() + } + const remove: UseFieldArrayRemove = (index) => { + origin.remove(index) + onChangeForm() + } + const insert: UseFieldArrayInsert = ( + index, + value, + options + ) => { + origin.insert(index, value, options) + onChangeForm() + } + const update: UseFieldArrayUpdate = (index, value) => { + origin.update(index, value) + onChangeForm() + } + const replace: UseFieldArrayReplace = (value) => { + origin.replace(value) + onChangeForm() + } + + return { + swap, + move, + prepend, + append, + remove, + insert, + update, + replace, + fields: origin.fields, + } + } + + return { + ...rest, + register, + registerWithDefaultValue, + registerWithDefaultChecked, + getValues, + reset, + onChangeForm, + useFieldArraySync, + } }