diff --git a/docs/components/example/checkbox-react-hook-form.tsx b/docs/components/example/checkbox-react-hook-form.tsx new file mode 100644 index 000000000..9b774c591 --- /dev/null +++ b/docs/components/example/checkbox-react-hook-form.tsx @@ -0,0 +1,66 @@ +"use client"; + +import { useController, useForm } from "react-hook-form"; +import { Checkbox } from "seed-design/ui/checkbox"; +import { ActionButton } from "seed-design/ui/action-button"; +import { useCallback, type FormEvent } from "react"; + +const POSSIBLE_FRUIT_VALUES = ["apple", "melon", "mango"] as const; + +type FormValues = Record<(typeof POSSIBLE_FRUIT_VALUES)[number], boolean>; + +export default function CheckboxReactHookForm() { + const { handleSubmit, reset, setValue, control } = useForm({ + defaultValues: { + apple: false, + melon: true, + mango: false, + }, + }); + + const onValid = useCallback((data: FormValues) => { + window.alert(JSON.stringify(data, null, 2)); + }, []); + + const onReset = useCallback( + (event: FormEvent) => { + event.preventDefault(); + reset(); + }, + [reset], + ); + + return ( +
+
+ {POSSIBLE_FRUIT_VALUES.map((name) => { + const { + field: { value, ...restProps }, + fieldState: { invalid }, + } = useController({ name, control }); + + return ( + + ); + })} +
+
+ + 제출 + + setValue("mango", true)}> + mango 선택 + + + 초기화 + +
+
+ ); +} diff --git a/docs/components/example/multiline-text-field-form.tsx b/docs/components/example/multiline-text-field-form.tsx new file mode 100644 index 000000000..15b6d08ab --- /dev/null +++ b/docs/components/example/multiline-text-field-form.tsx @@ -0,0 +1,117 @@ +"use client"; + +import { ActionButton } from "seed-design/ui/action-button"; +import { TextField, TextFieldTextarea } from "seed-design/ui/text-field"; +import { useState, useCallback, type FormEvent } from "react"; + +interface FormValues { + bio: string; + address: string; +} + +type FieldErrors = Record; + +export default function MultilineTextFieldForm() { + const [formValues, setFormValues] = useState({ + bio: "", + address: "", + }); + + const [fieldErrors, setFieldStates] = useState({ + bio: null, + address: null, + }); + + const validateForm = useCallback((): boolean => { + let isValid = true; + + const newFieldErrors: FieldErrors = { + bio: null, + address: null, + }; + + // Name validation + if (!formValues.bio) { + newFieldErrors.bio = "필수 입력 항목입니다"; + isValid = false; + } + + if (!formValues.address.startsWith("대한민국")) { + newFieldErrors.address = "대한민국으로 시작해주세요"; + isValid = false; + } + + if (!formValues.address) { + newFieldErrors.address = "필수 입력 항목입니다"; + isValid = false; + } + + setFieldStates(newFieldErrors); + + return isValid; + }, [formValues]); + + const handleSubmit = useCallback( + (event: FormEvent) => { + event.preventDefault(); + + if (validateForm()) { + window.alert(JSON.stringify(formValues, null, 2)); + } + }, + [formValues, validateForm], + ); + + const handleReset = useCallback((event: FormEvent) => { + event.preventDefault(); + + setFormValues({ bio: "", address: "" }); + setFieldStates({ bio: null, address: null }); + }, []); + + const handleNameChange = (value: string) => { + setFormValues((prev) => ({ ...prev, bio: value })); + setFieldStates((prev) => ({ ...prev, name: null })); + }; + + const handleAddressChange = (value: string) => { + setFormValues((prev) => ({ ...prev, address: value })); + setFieldStates((prev) => ({ ...prev, address: null })); + }; + + return ( +
+ handleNameChange(value)} + {...(fieldErrors.bio && { invalid: true, errorMessage: fieldErrors.bio })} + > + + + handleAddressChange(slicedValue)} + {...(fieldErrors.address && { invalid: true, errorMessage: fieldErrors.address })} + > + + +
+ + 제출 + + + 초기화 + +
+
+ ); +} diff --git a/docs/components/example/multiline-text-field-preview.tsx b/docs/components/example/multiline-text-field-preview.tsx index d50c12a2b..3a0f6709e 100644 --- a/docs/components/example/multiline-text-field-preview.tsx +++ b/docs/components/example/multiline-text-field-preview.tsx @@ -4,14 +4,9 @@ import { useState } from "react"; import { TextField, TextFieldTextarea } from "seed-design/ui/text-field"; export default function MultilineTextFieldPreview() { - const [value, setValue] = useState(""); - return ( -
- setValue(value)}> - - -

현재 값: {value}

-
+ + + ); } diff --git a/docs/components/example/multiline-text-field-react-hook-form.tsx b/docs/components/example/multiline-text-field-react-hook-form.tsx index 86734108e..254f406e3 100644 --- a/docs/components/example/multiline-text-field-react-hook-form.tsx +++ b/docs/components/example/multiline-text-field-react-hook-form.tsx @@ -1,9 +1,9 @@ "use client"; import { ActionButton } from "seed-design/ui/action-button"; -import { IconHouseLine } from "@daangn/react-monochrome-icon"; -import { useForm } from "react-hook-form"; +import { useController, useForm } from "react-hook-form"; import { TextField, TextFieldTextarea } from "seed-design/ui/text-field"; +import { useCallback, type FormEvent, type KeyboardEvent } from "react"; interface FormValues { bio: string; @@ -11,56 +11,90 @@ interface FormValues { } export default function MultilineTextFieldReactHookForm() { + const { handleSubmit, reset, control } = useForm({ + defaultValues: { + bio: "", + address: "", + }, + shouldFocusError: true, + }); + + const { field: bioField, fieldState: bioFieldState } = useController({ + name: "bio", + control, + rules: { + required: "필수 입력 항목입니다", + }, + }); const { - register, - handleSubmit, - clearErrors, - formState: { errors }, - } = useForm(); + field: { onChange: addressOnChange, ...addressField }, + fieldState: addressFieldState, + } = useController({ + name: "address", + control, + rules: { + required: "필수 입력 항목입니다", + pattern: { value: /^대한민국/, message: "대한민국으로 시작해주세요" }, + }, + }); - const onValid = (data: FormValues) => { + const onValid = useCallback((data: FormValues) => { window.alert(JSON.stringify(data, null, 2)); - }; + }, []); + + const onReset = useCallback( + (event: FormEvent) => { + event.preventDefault(); + reset(); + }, + [reset], + ); + + const onMetaReturn = useCallback( + (event: KeyboardEvent) => { + if (event.key === "Enter" && (event.metaKey || event.ctrlKey)) { + event.preventDefault(); + handleSubmit(onValid)(); + } + }, + [handleSubmit, onValid], + ); return ( -
+ - + addressOnChange(slicedValue)} required + {...addressField} > - +
제출 - clearErrors(["bio", "address"])} - variant="neutralWeak" - > + 초기화
diff --git a/docs/components/example/select-box-check-preview.tsx b/docs/components/example/select-box-check-preview.tsx index 6fa40b652..f2b43f941 100644 --- a/docs/components/example/select-box-check-preview.tsx +++ b/docs/components/example/select-box-check-preview.tsx @@ -3,12 +3,9 @@ import { SelectBoxCheck, SelectBoxCheckGroup } from "seed-design/ui/select-box"; export default function SelectBoxCheckPreview() { return ( - - - + + + ); } diff --git a/docs/components/example/select-box-check-react-hook-form.tsx b/docs/components/example/select-box-check-react-hook-form.tsx new file mode 100644 index 000000000..a18251f03 --- /dev/null +++ b/docs/components/example/select-box-check-react-hook-form.tsx @@ -0,0 +1,66 @@ +"use client"; + +import { useForm, useController } from "react-hook-form"; +import { SelectBoxCheck, SelectBoxCheckGroup } from "seed-design/ui/select-box"; +import { ActionButton } from "seed-design/ui/action-button"; +import { useCallback, type FormEvent } from "react"; + +const POSSIBLE_FRUIT_VALUES = ["apple", "melon", "mango"] as const; + +type FormValues = Record<(typeof POSSIBLE_FRUIT_VALUES)[number], boolean>; + +export default function SelectBoxCheckReactHookForm() { + const { handleSubmit, reset, setValue, control } = useForm({ + defaultValues: { + apple: false, + melon: true, + mango: false, + }, + }); + + const onValid = useCallback((data: FormValues) => { + window.alert(JSON.stringify(data, null, 2)); + }, []); + + const onReset = useCallback( + (event: FormEvent) => { + event.preventDefault(); + reset(); + }, + [reset], + ); + + return ( + + + {POSSIBLE_FRUIT_VALUES.map((name) => { + const { + field: { value, ...restProps }, + fieldState: { invalid }, + } = useController({ name, control }); + + return ( + + ); + })} + +
+ + 제출 + + setValue("mango", true)}> + mango 선택 + + + 초기화 + +
+ + ); +} diff --git a/docs/components/example/select-box-radio-preview.tsx b/docs/components/example/select-box-radio-preview.tsx index 258207b11..9f4288dd5 100644 --- a/docs/components/example/select-box-radio-preview.tsx +++ b/docs/components/example/select-box-radio-preview.tsx @@ -1,15 +1,17 @@ +"use client"; + import { SelectBoxRadioGroup, SelectBoxRadio } from "seed-design/ui/select-box"; export default function SelectBoxRadioPreview() { return ( - - + + - + ); } diff --git a/docs/components/example/select-box-radio-react-hook-form.tsx b/docs/components/example/select-box-radio-react-hook-form.tsx new file mode 100644 index 000000000..a9a218e90 --- /dev/null +++ b/docs/components/example/select-box-radio-react-hook-form.tsx @@ -0,0 +1,60 @@ +"use client"; + +import { useForm, useController } from "react-hook-form"; +import { SelectBoxRadio, SelectBoxRadioGroup } from "seed-design/ui/select-box"; +import { ActionButton } from "seed-design/ui/action-button"; +import { useCallback, type FormEvent } from "react"; + +const POSSIBLE_FRUIT_VALUES = ["apple", "melon", "mango"] as const; + +interface FormValues { + fruit: (typeof POSSIBLE_FRUIT_VALUES)[number]; +} + +export default function SelectBoxRadioReactHookForm() { + const { handleSubmit, reset, setValue, control } = useForm({ + defaultValues: { + fruit: "melon", + }, + }); + const { field } = useController({ name: "fruit", control }); + + const onValid = useCallback((data: FormValues) => { + window.alert(JSON.stringify(data, null, 2)); + }, []); + + const onReset = useCallback( + (event: FormEvent) => { + event.preventDefault(); + reset(); + }, + [reset], + ); + + return ( +
+
+ + {POSSIBLE_FRUIT_VALUES.map((value) => ( + + ))} + +
+ + 제출 + + setValue("fruit", "mango")} + > + mango 선택 + + + 초기화 + +
+
+
+ ); +} diff --git a/docs/components/example/text-field-form.tsx b/docs/components/example/text-field-form.tsx new file mode 100644 index 000000000..bd2993558 --- /dev/null +++ b/docs/components/example/text-field-form.tsx @@ -0,0 +1,117 @@ +"use client"; + +import { ActionButton } from "seed-design/ui/action-button"; +import { TextField, TextFieldInput } from "seed-design/ui/text-field"; +import { useState, useCallback, type FormEvent } from "react"; + +interface FormValues { + name: string; + address: string; +} + +type FieldErrors = Record; + +export default function TextFieldForm() { + const [formValues, setFormValues] = useState({ + name: "", + address: "", + }); + + const [fieldErrors, setFieldStates] = useState({ + name: null, + address: null, + }); + + const validateForm = useCallback((): boolean => { + let isValid = true; + + const newFieldErrors: FieldErrors = { + name: null, + address: null, + }; + + // Name validation + if (!formValues.name) { + newFieldErrors.name = "필수 입력 항목입니다"; + isValid = false; + } + + if (!formValues.address.startsWith("대한민국")) { + newFieldErrors.address = "대한민국으로 시작해주세요"; + isValid = false; + } + + if (!formValues.address) { + newFieldErrors.address = "필수 입력 항목입니다"; + isValid = false; + } + + setFieldStates(newFieldErrors); + + return isValid; + }, [formValues]); + + const handleSubmit = useCallback( + (event: FormEvent) => { + event.preventDefault(); + + if (validateForm()) { + window.alert(JSON.stringify(formValues, null, 2)); + } + }, + [formValues, validateForm], + ); + + const handleReset = useCallback((event: FormEvent) => { + event.preventDefault(); + + setFormValues({ name: "", address: "" }); + setFieldStates({ name: null, address: null }); + }, []); + + const handleNameChange = (value: string) => { + setFormValues((prev) => ({ ...prev, name: value })); + setFieldStates((prev) => ({ ...prev, name: null })); + }; + + const handleAddressChange = (value: string) => { + setFormValues((prev) => ({ ...prev, address: value })); + setFieldStates((prev) => ({ ...prev, address: null })); + }; + + return ( +
+ handleNameChange(value)} + {...(fieldErrors.name && { invalid: true, errorMessage: fieldErrors.name })} + > + + + handleAddressChange(slicedValue)} + {...(fieldErrors.address && { invalid: true, errorMessage: fieldErrors.address })} + > + + +
+ + 제출 + + + 초기화 + +
+
+ ); +} diff --git a/docs/components/example/text-field-preview.tsx b/docs/components/example/text-field-preview.tsx index 8b755204b..9af488f0e 100644 --- a/docs/components/example/text-field-preview.tsx +++ b/docs/components/example/text-field-preview.tsx @@ -4,14 +4,9 @@ import { useState } from "react"; import { TextField, TextFieldInput } from "seed-design/ui/text-field"; export default function TextFieldPreview() { - const [value, setValue] = useState(""); - return ( -
- setValue(value)}> - - -

현재 값: {value}

-
+ + + ); } diff --git a/docs/components/example/text-field-react-hook-form.tsx b/docs/components/example/text-field-react-hook-form.tsx index 4bf97e194..e3024a69c 100644 --- a/docs/components/example/text-field-react-hook-form.tsx +++ b/docs/components/example/text-field-react-hook-form.tsx @@ -1,9 +1,9 @@ "use client"; import { ActionButton } from "seed-design/ui/action-button"; -import { IconHouseLine } from "@daangn/react-monochrome-icon"; -import { useForm } from "react-hook-form"; +import { useController, useForm } from "react-hook-form"; import { TextField, TextFieldInput } from "seed-design/ui/text-field"; +import { useCallback, type FormEvent } from "react"; interface FormValues { name: string; @@ -11,57 +11,79 @@ interface FormValues { } export default function TextFieldReactHookForm() { + const { handleSubmit, reset, control } = useForm({ + defaultValues: { + name: "", + address: "", + }, + }); + + const { field: nameField, fieldState: nameFieldState } = useController({ + name: "name", + control, + rules: { + required: "필수 입력 항목입니다", + }, + }); const { - register, - handleSubmit, - clearErrors, - formState: { errors }, - } = useForm(); + field: { onChange: addressOnChange, ...addressField }, + fieldState: addressFieldState, + } = useController({ + name: "address", + control, + rules: { + required: "필수 입력 항목입니다", + pattern: { value: /^대한민국/, message: "대한민국으로 시작해주세요" }, + }, + }); - const onValid = (data: FormValues) => { + const onValid = useCallback((data: FormValues) => { window.alert(JSON.stringify(data, null, 2)); - }; + }, []); + + const onReset = useCallback( + (event: FormEvent) => { + event.preventDefault(); + reset(); + }, + [reset], + ); return ( -
+ addressOnChange(slicedValue)} required - indicator="(필수)" - prefixIcon={} + {...addressField} > - +
제출 - clearErrors(["name", "address"])} - variant="neutralWeak" - > + 초기화
diff --git a/docs/content/docs/react/components/checkbox.mdx b/docs/content/docs/react/components/checkbox.mdx index dd3258164..cf1b0feed 100644 --- a/docs/content/docs/react/components/checkbox.mdx +++ b/docs/content/docs/react/components/checkbox.mdx @@ -12,6 +12,8 @@ title: Checkbox +## 예제 + ### Size @@ -35,3 +37,9 @@ title: Checkbox ### Disabled + +### Use Cases + +#### React Hook Form + + diff --git a/docs/content/docs/react/components/select-boxes/select-box-check.mdx b/docs/content/docs/react/components/select-boxes/select-box-check.mdx index cff5cedae..c516065ad 100644 --- a/docs/content/docs/react/components/select-boxes/select-box-check.mdx +++ b/docs/content/docs/react/components/select-boxes/select-box-check.mdx @@ -20,3 +20,9 @@ title: Select Box Check path="./registry/ui/select-box.tsx" name="SelectBoxCheckProps" /> + +## 예제 + +### React Hook Form + + diff --git a/docs/content/docs/react/components/select-boxes/select-box-radio.mdx b/docs/content/docs/react/components/select-boxes/select-box-radio.mdx index 86b41395e..3cff34c9c 100644 --- a/docs/content/docs/react/components/select-boxes/select-box-radio.mdx +++ b/docs/content/docs/react/components/select-boxes/select-box-radio.mdx @@ -23,3 +23,9 @@ title: Select Box Radio path="./registry/ui/select-box.tsx" name="SelectBoxRadioProps" /> + +## 예제 + +### React Hook Form + + diff --git a/docs/content/docs/react/components/text-fields/multiline-text-field.mdx b/docs/content/docs/react/components/text-fields/multiline-text-field.mdx index 7bd4b8b3c..ff04c63a2 100644 --- a/docs/content/docs/react/components/text-fields/multiline-text-field.mdx +++ b/docs/content/docs/react/components/text-fields/multiline-text-field.mdx @@ -78,6 +78,10 @@ description: 사용자가 입력할 수 있는 텍스트를 받는 컴포넌트 ### Use Cases +#### Form + + + #### React Hook Form diff --git a/docs/content/docs/react/components/text-fields/text-field.mdx b/docs/content/docs/react/components/text-fields/text-field.mdx index 8e52fb490..7f6003a5d 100644 --- a/docs/content/docs/react/components/text-fields/text-field.mdx +++ b/docs/content/docs/react/components/text-fields/text-field.mdx @@ -76,6 +76,10 @@ description: 사용자가 입력할 수 있는 텍스트를 받는 컴포넌트 ### Use Cases +#### Form + + + #### React Hook Form diff --git a/docs/stories/Avatar.stories.tsx b/docs/stories/Avatar.stories.tsx index c5370e5a2..8d93d4d51 100644 --- a/docs/stories/Avatar.stories.tsx +++ b/docs/stories/Avatar.stories.tsx @@ -2,7 +2,7 @@ import type { Meta, StoryObj } from "@storybook/react"; import { Avatar } from "seed-design/ui/avatar"; -import { IdentityPlaceholder } from "@/registry/ui/identity-placeholder"; +import { IdentityPlaceholder } from "seed-design/ui/identity-placeholder"; import { avatarVariantMap } from "@seed-design/recipe/avatar"; import { SeedThemeDecorator } from "./components/decorator"; import { VariantTable } from "./components/variant-table"; diff --git a/docs/stories/SelectBoxRadio.stories.tsx b/docs/stories/SelectBoxRadio.stories.tsx index f1751dbe4..02ab8270d 100644 --- a/docs/stories/SelectBoxRadio.stories.tsx +++ b/docs/stories/SelectBoxRadio.stories.tsx @@ -1,6 +1,6 @@ import type { Meta, StoryObj } from "@storybook/react"; -import { SelectBoxRadio, SelectBoxRadioGroup } from "@/registry/ui/select-box"; +import { SelectBoxRadio, SelectBoxRadioGroup } from "seed-design/ui/select-box"; import { selectBoxGroupVariantMap } from "@seed-design/recipe/selectBoxGroup"; import { SeedThemeDecorator } from "./components/decorator";