Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

examples(RHF): support for default{Value,Checked} & useFieldArray #134

Merged
merged 4 commits into from
Oct 30, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
141 changes: 131 additions & 10 deletions examples/react-hook-form/pages/form/[index].tsx
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -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()))
AkifumiSato marked this conversation as resolved.
Show resolved Hide resolved
}, [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>
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

imo: RHFのuseFieldArrayはimportして使うと思うので、↓みたくしてimprot { useFieldArraySync }して使うというのは微妙ですかね?
その方がhooksっぽいかなって気もしたものの、自信ないのでご意見いただきたいです🙇‍♂️

const { ... } = useFieldArraySync({
  onChangeForm,
  props,
})

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

最初はまさにそのつもりだったのですが、将来onChangeForm()以外を渡さないといけなくなる可能性を考えると最初からクロージャにしちゃった方がいいだろうと思いました
別案としてはuseFormSync()からラップしたcontrolを返してそれをuseFieldArraySync()に渡すというのも考えたのですが、ラップするオブジェクトが増え続けるのもどうかかと思いクロージャ案を選びました
useFormSync()するコンポーネントとuseFieldArraySync() するコンポーネントが分かれる場合はcontrolをラップするのが一番使い勝手がいい気はします
他の2案ではcontrolに加えてuseFieldArraySynconChangeFormのどちらかも一緒に子コンポーネントに渡さないといけないので

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

これの結論が出るまでマージ保留にします

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

将来onChangeForm()以外を渡さないといけなくなる可能性を考えると最初からクロージャにしちゃった方がいいだろうと思いました

確かにですね...
方針納得しました!

useFormSync()するコンポーネントとuseFieldArraySync() するコンポーネントが分かれる場合はcontrolをラップするのが一番使い勝手がいい気はします

一応知っておきたいんですが、ラップするというのは↓みたいなイメージであってますか?

const { control } = useFormSync(...)
// controlはRHFのcontrol + onChangeForm

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

そのイメージです

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

方針納得しました!

とのことなのでこれでマージします!

): 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,
}
}