From a39844410c9e5247777dbe8e8650bbd5966c5cc9 Mon Sep 17 00:00:00 2001 From: Li Xin Date: Wed, 23 Apr 2025 21:16:58 +0800 Subject: [PATCH] chore: add `form` --- web/package.json | 4 +- web/pnpm-lock.yaml | 33 ++++++- web/src/components/ui/form.tsx | 167 +++++++++++++++++++++++++++++++++ 3 files changed, 202 insertions(+), 2 deletions(-) create mode 100644 web/src/components/ui/form.tsx diff --git a/web/package.json b/web/package.json index d8bdb40..92065a3 100644 --- a/web/package.json +++ b/web/package.json @@ -18,6 +18,7 @@ }, "dependencies": { "@ant-design/icons": "^6.0.0", + "@hookform/resolvers": "^5.0.1", "@radix-ui/react-dialog": "^1.1.10", "@radix-ui/react-dropdown-menu": "^2.1.11", "@radix-ui/react-label": "^2.1.4", @@ -41,6 +42,7 @@ "next-themes": "^0.4.6", "react": "^19.0.0", "react-dom": "^19.0.0", + "react-hook-form": "^7.56.1", "react-markdown": "^10.1.0", "react-syntax-highlighter": "^15.6.1", "rehype-katex": "^7.0.1", @@ -50,7 +52,7 @@ "tw-animate-css": "^1.2.5", "unist-util-visit": "^5.0.0", "use-stick-to-bottom": "^1.1.0", - "zod": "^3.24.2", + "zod": "^3.24.3", "zustand": "^5.0.3" }, "devDependencies": { diff --git a/web/pnpm-lock.yaml b/web/pnpm-lock.yaml index a888803..7a891e5 100644 --- a/web/pnpm-lock.yaml +++ b/web/pnpm-lock.yaml @@ -11,6 +11,9 @@ importers: '@ant-design/icons': specifier: ^6.0.0 version: 6.0.0(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@hookform/resolvers': + specifier: ^5.0.1 + version: 5.0.1(react-hook-form@7.56.1(react@19.1.0)) '@radix-ui/react-dialog': specifier: ^1.1.10 version: 1.1.10(@types/react-dom@19.1.1(@types/react@19.1.2))(@types/react@19.1.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) @@ -80,6 +83,9 @@ importers: react-dom: specifier: ^19.0.0 version: 19.1.0(react@19.1.0) + react-hook-form: + specifier: ^7.56.1 + version: 7.56.1(react@19.1.0) react-markdown: specifier: ^10.1.0 version: 10.1.0(@types/react@19.1.2)(react@19.1.0) @@ -108,7 +114,7 @@ importers: specifier: ^1.1.0 version: 1.1.0(react@19.1.0) zod: - specifier: ^3.24.2 + specifier: ^3.24.3 version: 3.24.3 zustand: specifier: ^5.0.3 @@ -256,6 +262,11 @@ packages: '@floating-ui/utils@0.2.9': resolution: {integrity: sha512-MDWhGtE+eHw5JW7lq4qhc5yRLS11ERl1c7Z6Xd0a58DozHES6EnNNwUWbMiG4J9Cgj053Bhk8zvlhFYKVhULwg==} + '@hookform/resolvers@5.0.1': + resolution: {integrity: sha512-u/+Jp83luQNx9AdyW2fIPGY6Y7NG68eN2ZW8FOJYL+M0i4s49+refdJdOp/A9n9HFQtQs3HIDHQvX3ZET2o7YA==} + peerDependencies: + react-hook-form: ^7.55.0 + '@humanfs/core@0.19.1': resolution: {integrity: sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==} engines: {node: '>=18.18.0'} @@ -969,6 +980,9 @@ packages: '@rushstack/eslint-patch@1.11.0': resolution: {integrity: sha512-zxnHvoMQVqewTJr/W4pKjF0bMGiKJv1WX7bSrkl46Hg0QjESbzBROWK0Wg4RphzSOS5Jiy7eFimmM3UgMrMZbQ==} + '@standard-schema/utils@0.3.0': + resolution: {integrity: sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g==} + '@swc/counter@0.1.3': resolution: {integrity: sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ==} @@ -2802,6 +2816,12 @@ packages: peerDependencies: react: ^19.1.0 + react-hook-form@7.56.1: + resolution: {integrity: sha512-qWAVokhSpshhcEuQDSANHx3jiAEFzu2HAaaQIzi/r9FNPm1ioAvuJSD4EuZzWd7Al7nTRKcKPnBKO7sRn+zavQ==} + engines: {node: '>=18.0.0'} + peerDependencies: + react: ^16.8.0 || ^17 || ^18 || ^19 + react-is@16.13.1: resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==} @@ -3435,6 +3455,11 @@ snapshots: '@floating-ui/utils@0.2.9': {} + '@hookform/resolvers@5.0.1(react-hook-form@7.56.1(react@19.1.0))': + dependencies: + '@standard-schema/utils': 0.3.0 + react-hook-form: 7.56.1(react@19.1.0) + '@humanfs/core@0.19.1': {} '@humanfs/node@0.16.6': @@ -4048,6 +4073,8 @@ snapshots: '@rushstack/eslint-patch@1.11.0': {} + '@standard-schema/utils@0.3.0': {} + '@swc/counter@0.1.3': {} '@swc/helpers@0.5.15': @@ -6215,6 +6242,10 @@ snapshots: react: 19.1.0 scheduler: 0.26.0 + react-hook-form@7.56.1(react@19.1.0): + dependencies: + react: 19.1.0 + react-is@16.13.1: {} react-is@18.3.1: {} diff --git a/web/src/components/ui/form.tsx b/web/src/components/ui/form.tsx new file mode 100644 index 0000000..c792ecc --- /dev/null +++ b/web/src/components/ui/form.tsx @@ -0,0 +1,167 @@ +"use client" + +import * as React from "react" +import * as LabelPrimitive from "@radix-ui/react-label" +import { Slot } from "@radix-ui/react-slot" +import { + Controller, + FormProvider, + useFormContext, + useFormState, + type ControllerProps, + type FieldPath, + type FieldValues, +} from "react-hook-form" + +import { cn } from "~/lib/utils" +import { Label } from "~/components/ui/label" + +const Form = FormProvider + +type FormFieldContextValue< + TFieldValues extends FieldValues = FieldValues, + TName extends FieldPath = FieldPath, +> = { + name: TName +} + +const FormFieldContext = React.createContext( + {} as FormFieldContextValue +) + +const FormField = < + TFieldValues extends FieldValues = FieldValues, + TName extends FieldPath = FieldPath, +>({ + ...props +}: ControllerProps) => { + return ( + + + + ) +} + +const useFormField = () => { + const fieldContext = React.useContext(FormFieldContext) + const itemContext = React.useContext(FormItemContext) + const { getFieldState } = useFormContext() + const formState = useFormState({ name: fieldContext.name }) + const fieldState = getFieldState(fieldContext.name, formState) + + if (!fieldContext) { + throw new Error("useFormField should be used within ") + } + + const { id } = itemContext + + return { + id, + name: fieldContext.name, + formItemId: `${id}-form-item`, + formDescriptionId: `${id}-form-item-description`, + formMessageId: `${id}-form-item-message`, + ...fieldState, + } +} + +type FormItemContextValue = { + id: string +} + +const FormItemContext = React.createContext( + {} as FormItemContextValue +) + +function FormItem({ className, ...props }: React.ComponentProps<"div">) { + const id = React.useId() + + return ( + +
+ + ) +} + +function FormLabel({ + className, + ...props +}: React.ComponentProps) { + const { error, formItemId } = useFormField() + + return ( +