入力フィールドの数や項目を動的に変化させたい場合、React Hook FormというライブラリのuseFieldArrayというフックを実装することで実現可能です。なお、UIライブラリとしてはMUI v.5を利用します。
フィールドの追加・削除
まずは、シンプルにフィールドの追加と削除ができるフォームを作って、ついで登録ボタンで入力データを確認しましょう。
<App.tsx> import { useFieldArray, useForm } from 'react-hook-form' import { Button, Container, Stack, TextField } from '@mui/material' export default function App() { // フィールドの初期値となる配列を設定 const { register, handleSubmit, control } = useForm({ defaultValues: { register: [{ name: "" }] } }) // 一意のnameを設定 // append, removeでそれぞれ追加, 削除が可能 const { fields, append, remove } = useFieldArray({ control, name: "register", }) const submit = (data: any) => { console.log(data) } return ( <Container maxWidth="sm" sx={{ pt: 5 }}> <form onSubmit={handleSubmit(submit)}> <Stack spacing={3}> // fieldsをmapメソッドで展開 {fields.map((field, index) => ( <Stack direction={"row"} spacing={3}> <TextField label="Name" key={field.id} {...register(`register.${index}.name`)} fullWidth /> // 削除ボタン // 引数: 削除する要素のインデックス <Button type="button" color="primary" variant="contained" onClick={() => remove(index)} disabled={index === 0 ? true : false} > Delete </Button> </Stack> ))} // 追加ボタン // 引数: 初期値を設定した追加要素のオブジェクト <Button type="button" color="primary" variant="contained" onClick={() => append({ name: "" })} > Add </Button> <Button type="submit" color="secondary" variant="contained" > Register </Button> </Stack> </form> </Container> ) }
ブラウザで動作を確認
初期表示
追加ボタンと削除ボタンで、それぞれテキストフィールドの追加したり削除できます。
登録ボタンをクリックして、送信されたデータを確認すると、ちゃんと配列形式で取得できていますね。
実用的なフォームにアップデート
- 有/無のラジオボタンで、有を選択するとフォームを表示。
- フォームの数は、追加・削除ボタンで操作可能。
- 各フォームの合計金額を表示。
- バリデーションを実装。
- UIはMUIを使用。
上記の仕様を満たすようなより実用的なフォームにアップデートしてみましょう。
<App.tsx> import { useState } from 'react' import { useFieldArray, useForm } from 'react-hook-form' import { Button, Container, FormControl, FormControlLabel, Paper, Radio, RadioGroup, Stack, TextField } from '@mui/material' import IconButton from '@mui/material/IconButton' import DeleteIcon from '@mui/icons-material/Delete'; import { TotalPrice } from './Calculate'; export type formType = { isExists: string, items: { item: string quantity: number price: number }[] } export default function App() { const [isExists, setIsExists] = useState(false) const { register, handleSubmit, reset, control, formState: { errors } } = useForm<formType>({ defaultValues: { items: [{ item: "", quantity: 0, price: 0 }] } }) const { fields, append, remove } = useFieldArray({ control, name: "items", }) // ラジオボタン操作時の処理 const changeIsExists = (value: string) => { if (value === "false") { setIsExists(false) reset() } else { setIsExists(true) } } const submit = (data: formType) => { console.log(data) } return ( <Container maxWidth="sm" sx={{ pt: 5 }}> <Paper sx={{ p: 5 }}> <form onSubmit={handleSubmit(submit)}> <Stack spacing={3}> <FormControl> <RadioGroup row value={isExists} onChange={(event) => changeIsExists(event.target.value)}> <FormControlLabel value="false" control={<Radio {...register("isExists")} />} label="No" /> <FormControlLabel value="true" control={<Radio {...register("isExists")} />} label="Yes" /> </RadioGroup> </FormControl> {isExists && <> { fields.map((field, index) => ( <Stack direction={"row"} spacing={3} key={field.id}> <TextField label="Item" {...register(`items.${index}.item`, { required: "error" })} fullWidth error={Boolean(errors.items?.[index]?.item)} /> <TextField type="number" label="Quantity" {...register(`items.${index}.quantity`, { min: 1 })} fullWidth error={Boolean(errors.items?.[index]?.quantity)} helperText={errors.items?.[index]?.quantity?.message} /> <TextField type="number" label="Price" {...register(`items.${index}.price`, { min: 1 })} fullWidth error={Boolean(errors.items?.[index]?.price)} helperText={errors.items?.[index]?.price?.message} /> <IconButton onClick={() => remove(index)} disabled={fields.length < 2 ? true : false} > <DeleteIcon /> </IconButton> </Stack> )) } <TotalPrice control={control} /> < Button type="button" color="primary" variant="contained" onClick={() => append({ item: "", quantity: 0, price: 0 })} > Add </Button> </> } <Button type="submit" color="secondary" variant="contained" > Register </Button> </Stack> </form> </Paper> </Container > ) }
同階層に合計金額を計算して返すコンポーネントを置きます。
<Calculate.tsx> import { useWatch, Control } from "react-hook-form" import { formType } from "./App" import { Box, Typography } from '@mui/material' type Props = { control: Control<formType> } export const TotalPrice = ({ control }: Props) => { // useWatchで対象の値を監視 const formValues = useWatch({ control, name: "items", }) // 監視している値から合計を計算 const total = formValues.reduce( (total, { quantity, price }) => total + (quantity * price), 0 ) return ( <Box textAlign="right"> <Typography variant="h6">Total Price: {total}-</Typography> </Box> ) }
ブラウザで確認
こんな感じ。
以上、動的に変化するフォームの実装でした。