Skip to content

Commit 4fbdcaf

Browse files
authored
[#98] ✨ shared 컴포넌트 Form 개발 (#118)
* [#98] 🌱 Initialize Form component creation * [#98] ✨ Add onChange handler to update controlled component state * [#98] ✨ Add onChange handler to update controlled component state and Change button tag to span * [#98] ✨ Implement Form component (TagInput not implemented yet) * [#98] ♻️ Make TagInput props exportable * [#98] ✨ Update options type and add TagInput component to Form * [#98] ✨ Add Storybook for Form component (WIP, to be updated after authentication page development) * [#98] ✨ add type export to input components and integrate TextInput, PasswordInput into Form * [#98] ✨ integrate TextInput and PasswordInput into Form * [#98] 📦 Add es-hangul library * [#98] ✨ implement form validation rules * [#98] ✨ Add validation and textarea field * [#98] ♻️ Update error message for introduce field * [#98] ✨ Add rules to Tag * [#98] 🐛 integrate state with Tag and correct typo in NAME_RULES * [#98] ✨ Add email validation logic and Update variable names in form
1 parent 21530c8 commit 4fbdcaf

File tree

10 files changed

+476
-9
lines changed

10 files changed

+476
-9
lines changed

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
"dependencies": {
33
"@tanstack/react-query": "^5.59.19",
44
"clsx": "^2.1.1",
5+
"es-hangul": "^2.2.4",
56
"ky": "^1.7.2",
67
"next": "^15.0.2",
78
"react": "^18.3.1",

pnpm-lock.yaml

Lines changed: 8 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/components/common/input/CheckboxInput.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,7 @@ export const CheckboxInput = ({
6666
<input
6767
type='checkbox'
6868
checked={checked}
69+
onChange={onChange}
6970
disabled={disabled}
7071
{...props}
7172
className='hidden'

src/components/common/input/RadioInput.tsx

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,8 @@ import clsx from 'clsx'
22

33
import { handleKeyDown } from '@/utils/handleKeyDown'
44

5-
interface RadioInputProps extends React.InputHTMLAttributes<HTMLInputElement> {
5+
export interface RadioInputProps
6+
extends React.InputHTMLAttributes<HTMLInputElement> {
67
label: string
78
}
89

@@ -22,12 +23,18 @@ export const RadioInput = ({
2223

2324
return (
2425
<label className={labelClass}>
25-
<input type='radio' checked={checked} disabled={disabled} {...props} />
26-
<button
26+
<input
27+
type='radio'
28+
checked={checked}
29+
onChange={onChange}
30+
disabled={disabled}
31+
{...props}
32+
/>
33+
<span
2734
role='radio'
2835
tabIndex={0}
2936
aria-checked={checked}
30-
aria-label={'radio button'}
37+
aria-label={'radio input'}
3138
onKeyDown={e =>
3239
handleKeyDown(
3340
e,

src/components/common/input/TagInput.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ interface TagProps {
1111
label: string
1212
}
1313

14-
interface TagInputProps extends Omit<TextInputProps, 'endAdornment'> {
14+
export interface TagInputProps extends Omit<TextInputProps, 'endAdornment'> {
1515
name: string
1616
}
1717

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,13 @@
1-
import { CheckboxInput } from './CheckboxInput'
1+
import { CheckboxInput, CheckboxInputProps } from './CheckboxInput'
22
import { PasswordInput } from './PasswordInput'
3-
import { RadioInput } from './RadioInput'
4-
import { TagInput } from './TagInput'
5-
import { TextInput } from './TextInput'
3+
import { RadioInput, RadioInputProps } from './RadioInput'
4+
import { TagInput, TagInputProps } from './TagInput'
5+
import { TextInput, TextInputProps } from './TextInput'
66

77
export { CheckboxInput, PasswordInput, RadioInput, TagInput, TextInput }
8+
export type {
9+
CheckboxInputProps,
10+
RadioInputProps,
11+
TagInputProps,
12+
TextInputProps,
13+
}
Lines changed: 255 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,255 @@
1+
import {
2+
Controller,
3+
FieldValues,
4+
FormProvider,
5+
UseFormReturn,
6+
useFormContext,
7+
} from 'react-hook-form'
8+
9+
import {
10+
FormField,
11+
PASSWORD_CONFIRM_RULES,
12+
VALIDATION_RULES,
13+
} from '@/constants/formValidation'
14+
import clsx from 'clsx'
15+
16+
import {
17+
CheckboxInput,
18+
CheckboxInputProps,
19+
PasswordInput,
20+
RadioInput,
21+
RadioInputProps,
22+
TagInput,
23+
TagInputProps,
24+
TextInput,
25+
TextInputProps,
26+
} from '@/components/common/input'
27+
import { TextArea } from '@/components/common/textarea'
28+
import { TextAreaProps } from '@/components/common/textarea/TextArea'
29+
30+
interface FormProps<TFieldValues extends FieldValues>
31+
extends React.FormHTMLAttributes<HTMLFormElement> {
32+
methods: UseFormReturn<TFieldValues>
33+
children: React.ReactNode
34+
}
35+
36+
export const Form = <TFieldValues extends FieldValues>({
37+
methods,
38+
children,
39+
...props
40+
}: FormProps<TFieldValues>): JSX.Element => {
41+
return (
42+
<FormProvider {...methods}>
43+
<form {...props}>{children}</form>
44+
</FormProvider>
45+
)
46+
}
47+
48+
const FormText = ({
49+
name,
50+
...props
51+
}: {
52+
name: FormField
53+
} & TextInputProps): JSX.Element => {
54+
const {
55+
register,
56+
formState: { errors, touchedFields },
57+
watch,
58+
} = useFormContext()
59+
const value = watch(name)
60+
const isError = touchedFields[name] && errors[name]
61+
const isSuccess = touchedFields[name] && !errors[name] && value
62+
63+
return (
64+
<>
65+
<TextInput
66+
{...register(name, VALIDATION_RULES[name])}
67+
error={Boolean(errors[name])}
68+
{...props}
69+
/>
70+
{isError && (
71+
<StatusMessage hasError={true}>
72+
{String(errors[name]?.message as string)}
73+
</StatusMessage>
74+
)}
75+
{/* TODO: 이미 가입된 이메일 확인하는 로직 추가하면서 수정 필요 */}
76+
{isSuccess && (
77+
<StatusMessage hasError={false}>
78+
가입 가능한 이메일입니다.
79+
</StatusMessage>
80+
)}
81+
</>
82+
)
83+
}
84+
85+
const FormPassword = ({
86+
name,
87+
...props
88+
}: {
89+
name: FormField
90+
rules?: Record<string, unknown>
91+
} & Omit<TextInputProps, 'type'>): JSX.Element => {
92+
const {
93+
register,
94+
formState: { errors },
95+
getValues,
96+
} = useFormContext()
97+
const registerOptions =
98+
name === 'passwordConfirmation'
99+
? PASSWORD_CONFIRM_RULES(getValues('password'))
100+
: VALIDATION_RULES[name]
101+
102+
return (
103+
<>
104+
<PasswordInput {...register(name, registerOptions)} {...props} />
105+
{errors[name]?.message && (
106+
<StatusMessage hasError={Boolean(errors[name])}>
107+
{errors[name].message as string}
108+
</StatusMessage>
109+
)}
110+
</>
111+
)
112+
}
113+
114+
const FormTextArea = ({
115+
name,
116+
...props
117+
}: { name: FormField } & TextAreaProps): JSX.Element => {
118+
const {
119+
register,
120+
formState: { errors },
121+
} = useFormContext()
122+
123+
return (
124+
<>
125+
<TextArea {...register(name, VALIDATION_RULES[name])} {...props} />
126+
{errors[name] && (
127+
<StatusMessage hasError={Boolean(errors[name])}>
128+
{String(errors[name].message)}
129+
</StatusMessage>
130+
)}
131+
</>
132+
)
133+
}
134+
135+
const FormCheckbox = ({
136+
name,
137+
rules,
138+
options,
139+
...props
140+
}: {
141+
name: string
142+
rules?: Record<string, unknown>
143+
options: { label: string; value: string }[]
144+
} & CheckboxInputProps): JSX.Element => {
145+
const { control } = useFormContext()
146+
return (
147+
<Controller
148+
name={name}
149+
control={control}
150+
rules={rules}
151+
render={({ field }) => (
152+
<>
153+
{options.map(option => (
154+
<CheckboxInput
155+
key={option.value}
156+
{...props}
157+
value={option.value}
158+
label={option.label}
159+
checked={(field.value || []).includes(option.value)}
160+
onChange={e => {
161+
const currentValue = field.value || []
162+
const newValue = e.target.checked
163+
? [...currentValue, option.value]
164+
: currentValue.filter((v: string) => v !== option.value)
165+
field.onChange(newValue)
166+
}}
167+
/>
168+
))}
169+
</>
170+
)}
171+
/>
172+
)
173+
}
174+
175+
const FormRadio = ({
176+
name,
177+
rules,
178+
options,
179+
...props
180+
}: {
181+
name: string
182+
rules?: Record<string, unknown>
183+
options: { label: string; value: string }[]
184+
} & RadioInputProps): JSX.Element => {
185+
const { control } = useFormContext()
186+
return (
187+
<Controller
188+
name={name}
189+
control={control}
190+
rules={rules}
191+
render={({ field }) => (
192+
<>
193+
{options.map(option => (
194+
<RadioInput
195+
key={option.value}
196+
{...props}
197+
label={option.label}
198+
value={option.value}
199+
checked={field.value === option.value}
200+
onChange={() => field.onChange(option.value)}
201+
/>
202+
))}
203+
</>
204+
)}
205+
/>
206+
)
207+
}
208+
209+
const FormTag = ({
210+
name,
211+
rules,
212+
...props
213+
}: {
214+
name: string
215+
rules?: Record<string, unknown>
216+
} & TagInputProps): JSX.Element => {
217+
const { control } = useFormContext()
218+
return (
219+
<Controller
220+
name={name}
221+
control={control}
222+
rules={rules}
223+
render={({ field }) => <TagInput {...field} {...props} />}
224+
/>
225+
)
226+
}
227+
228+
interface StatusMessageProps {
229+
className?: string
230+
children: string
231+
hasError: boolean
232+
}
233+
234+
const StatusMessage = ({
235+
className,
236+
children,
237+
hasError,
238+
}: StatusMessageProps): JSX.Element => {
239+
const baseClass = 'mt-4 text-caption1 font-medium'
240+
const statusClass = clsx({
241+
'text-semantic-negative': hasError,
242+
'text-semantic-positive': !hasError,
243+
})
244+
245+
return (
246+
<span className={clsx(baseClass, statusClass, className)}>{children}</span>
247+
)
248+
}
249+
250+
Form.Text = FormText
251+
Form.Password = FormPassword
252+
Form.TextArea = FormTextArea
253+
Form.Checkbox = FormCheckbox
254+
Form.Radio = FormRadio
255+
Form.TagInput = FormTag
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
import { Form } from './Form'
2+
3+
export { Form }

0 commit comments

Comments
 (0)