Skip to content

Commit

Permalink
examples(RHF): support for default{Value,Checked} & useFieldArray (#134)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
koichik committed Oct 30, 2022
1 parent 7930a74 commit 677dbe4
Show file tree
Hide file tree
Showing 2 changed files with 268 additions and 17 deletions.
141 changes: 131 additions & 10 deletions 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<FormState>({
Expand All @@ -21,18 +28,47 @@ const formState = initializableAtom<FormState>({
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<FormState> = (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>(formState(props))
const { fields, append, prepend, remove, swap, move, insert } =
useFieldArraySync({
control,
name: 'items',
})

const router = useRouter()
const onSubmit: SubmitHandler<FormState> = async (data) => {
console.log('submit data', data)
Expand All @@ -53,18 +89,103 @@ const Form: NextPage = () => {
<dl className={styles.formList}>
<dt>name</dt>
<dd>
<input type="text" {...register('name')} />
<input type="text" {...registerWithDefaultValue('name')} />
</dd>
<dt>comment</dt>
<dd>
<input type="text" {...register('comment')} />
<input type="text" {...registerWithDefaultValue('comment')} />
</dd>
<dt>time</dt>
<dd>
<input type="text" {...registerWithDefaultValue('time')} />
</dd>
<dt>items</dt>
<dd>
<button type="button" onClick={() => prepend(createNewItem())}>
Prepend
</button>
<ul>
{fields.map((field, index) => (
<li key={field.id}>
<span>{index} </span>
<input
type="radio"
{...registerWithDefaultChecked(
'selectedRadio',
field.radio
)}
/>
<input
type="checkbox"
{...registerWithDefaultChecked(
'selectedCheckbox',
field.checkbox
)}
/>
<input
{...registerWithDefaultValue(
`items.${index}.value` as const
)}
/>
<button
type="button"
onClick={() => insert(index, createNewItem())}
>
Insert before
</button>
<button
type="button"
onClick={() => insert(index + 1, createNewItem())}
>
Insert after
</button>
<button
type="button"
disabled={index >= fields.length - 1}
onClick={() => swap(index, index + 1)}
>
Swap to next
</button>
<button type="button" onClick={() => move(index, 0)}>
Move to first
</button>
<button type="button" onClick={() => remove(index)}>
Remove
</button>
</li>
))}
</ul>
<button type="button" onClick={() => append(createNewItem())}>
Append
</button>
</dd>
</dl>
<button>submit</button>
<button>Submit</button>
<button type="button" onClick={() => reset()}>
Reset
</button>
</form>
</main>
</div>
)
}

export default Form

export const getServerSideProps: GetServerSideProps<FormState> = 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' },
],
},
}
}
144 changes: 137 additions & 7 deletions 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 {
Expand All @@ -13,10 +30,28 @@ import {
useSetRecoilState,
} from 'recoil'

type RegisterWithDefaultChecked<
TFieldValues extends FieldValues,
TContext = any
> = <TFieldName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>>(
name: TFieldName,
value: TFieldValues[TFieldName] extends Readonly<Array<infer E>>
? E
: TFieldValues[TFieldName],
options?: RegisterOptions<TFieldValues, TFieldName>
) => UseFormRegisterReturn<TFieldName>

type UseFormSyncReturn<
TFieldValues extends FieldValues,
TContext = any
> = UseFormReturn<TFieldValues, TContext> & { onChangeForm: () => void }
> = UseFormReturn<TFieldValues, TContext> & {
registerWithDefaultValue: UseFormRegister<TFieldValues>
registerWithDefaultChecked: RegisterWithDefaultChecked<TFieldValues, TContext>
onChangeForm: () => void
useFieldArraySync: (
props: UseFieldArrayProps<TFieldValues>
) => UseFieldArrayReturn<TFieldValues>
}

export function useFormSync<TFieldValues extends FieldValues, TContext = any>(
formState: RecoilState<TFieldValues>,
Expand All @@ -36,7 +71,14 @@ export function useFormSync<TFieldValues extends FieldValues, TContext = any>(
const defaultValuesRef = useRef<TFieldValues>()
defaultValuesRef.current ??= getDefaultValues()

const getDefaultValue = (name: string) => {
return name
.split('.')
.reduce((value, segment) => value?.[segment], defaultValuesRef.current!)
}

const {
register,
getValues,
reset: resetForm,
...rest
Expand All @@ -47,16 +89,104 @@ export function useFormSync<TFieldValues extends FieldValues, TContext = any>(
defaultValues: defaultValuesRef.current as DeepPartial<TFieldValues>,
})

const setFormValues = useSetRecoilState(formState)
const onChangeForm = useCallback(() => {
setFormValues(getValues())
}, [setFormValues, getValues])
const registerWithDefaultValue: UseFormRegister<TFieldValues> = 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<TFieldValues>
): UseFieldArrayReturn<TFieldValues> => {
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<TFieldValues> = (value, options) => {
origin.prepend(value, options)
onChangeForm()
}
const append: UseFieldArrayAppend<TFieldValues> = (value, options) => {
origin.append(value, options)
onChangeForm()
}
const remove: UseFieldArrayRemove = (index) => {
origin.remove(index)
onChangeForm()
}
const insert: UseFieldArrayInsert<TFieldValues> = (
index,
value,
options
) => {
origin.insert(index, value, options)
onChangeForm()
}
const update: UseFieldArrayUpdate<TFieldValues> = (index, value) => {
origin.update(index, value)
onChangeForm()
}
const replace: UseFieldArrayReplace<TFieldValues> = (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,
}
}

0 comments on commit 677dbe4

Please sign in to comment.