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];