フォーム

1import React, { useState } from 'react';
2import { useForm } from 'react-hook-form';
3import Button from '../components/button';
4
5type FormData = {
6  title: string;
7  body: string;
8  gender: 'male' | 'female';
9  skills: {
10    [keyName in SkillId]: boolean;
11  };
12};
13
14const Form = () => {
15  const {
16    register,
17    handleSubmit,
18    control,
19    reset,
20    watch,
21    formState: { errors, isSubmitting },
22  } = useForm<FormData>();
23
24  const [result, setResult] = useState<FormData>();
25
26  const submit = (data: FormData) => {
27    return new Promise((resolve) =>
28      setTimeout(() => {
29        resolve(true);
30        setResult(data);
31        reset(undefined, {
32          keepValues: true,
33        });
34      }, 1000)
35    );
36  };
37
38  return (
39    <div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
40      <form onSubmit={handleSubmit(submit)} className="space-y-6">
41        <div>
42          <label htmlFor="title" className="block mb-2">
43            タイトル
44          </label>
45          <input
46            id="title"
47            autoComplete="off"
48            type="text"
49            className="bg-transparent rounded border"
50            {...register('title', {
51              required: '必須入力です',
52              maxLength: {
53                value: 80,
54                message: '最大80文字です',
55              },
56            })}
57          />
58          <p className="text-sm opacity-50">
59            {watch('title')?.length || 0} / 80
60          </p>
61          {errors.title && (
62            <p className="text-red-500 text-sm">{errors.title?.message}</p>
63          )}
64        </div>
65
66        <div>
67          <label className="block mb-2" htmlFor="body">
68            本文
69          </label>
70          <textarea
71            id="body"
72            className="bg-transparent rounded border w-80 max-w-full"
73            {...register('body', {
74              required: '必須入力です',
75              maxLength: {
76                value: 400,
77                message: '最大400文字です',
78              },
79            })}
80          />
81          <p className="text-sm opacity-50">
82            {watch('body')?.length || 0} / 400
83          </p>
84          {errors.body && (
85            <p className="text-red-500 text-sm">{errors.body?.message}</p>
86          )}
87        </div>
88
89        <div>
90          <h2 className="mb-2">性別</h2>
91          <div className="flex space-x-4">
92            {Genders.map((gender) => (
93              <div className="flex gap-2 items-center" key={gender.id}>
94                <input
95                  id={gender.id}
96                  type="radio"
97                  value={gender.id}
98                  {...register('gender', {
99                    required: 'どちらか選択してください',
100                  })}
101                />
102                <label htmlFor={gender.id}>{gender.label}</label>
103              </div>
104            ))}
105          </div>
106          {errors.gender && (
107            <p className="text-red-500 text-sm">{errors.gender?.message}</p>
108          )}
109        </div>
110
111        <div>
112          <h2 className="mb-2">スキル</h2>
113          <div className="flex space-x-4">
114            {Skills.map((skill) => (
115              <div className="flex gap-2 items-center" key={skill.id}>
116                <input
117                  id={skill.id}
118                  type="checkbox"
119                  className="rounded"
120                  {...register(`skills.${skill.id}`)}
121                />
122                <label htmlFor={skill.id}>{skill.label}</label>
123              </div>
124            ))}
125          </div>
126        </div>
127
128        <div></div>
129
130        <div className="flex space-x-4">
131          <button
132            type="button"
133            className="opacity-40"
134            onClick={() => {
135              reset();
136              setResult(undefined);
137            }}
138          >
139            リセット
140          </button>
141          <Button disabled={isSubmitting}>送信{isSubmitting && '中'}</Button>
142        </div>
143      </form>
144      <div>
145        <h2>結果</h2>
146        {result && (
147          <pre className="border rounded p-4 border-gray-700 mt-2">
148            {JSON.stringify(result, null, 2)}
149          </pre>
150        )}
151      </div>
152    </div>
153  );
154};
155
156export default Form;
157
158const Genders = [
159  {
160    id: 'male',
161    label: '男性',
162  },
163  {
164    id: 'female',
165    label: '女性',
166  },
167];
168
169const Skills = [
170  {
171    id: 'html',
172    label: 'HTML',
173  },
174  {
175    id: 'javascript',
176    label: 'JavaScript',
177  },
178  {
179    id: 'css',
180    label: 'CSS',
181  },
182  {
183    id: 'nextjs',
184    label: 'Next.js',
185  },
186  {
187    id: 'firebase',
188    label: 'Firebase',
189  },
190] as const;
191
192const SkillIds = Skills.map((item) => item.id);
193
194type SkillId = typeof SkillIds[number];

使用ライブラリ

参考サイト