From c51c95fd39756e0b84c4f6209112d08a8449b063 Mon Sep 17 00:00:00 2001 From: imollov Date: Wed, 22 Nov 2023 15:52:02 +0200 Subject: [PATCH] wip: refactor forms with react-hook-form and zod --- package.json | 6 +- pnpm-lock.yaml | 110 +++++++++--- src/components/balance.tsx | 66 +++++-- src/components/send-transaction-prepared.tsx | 2 - src/components/ui/form.tsx | 176 +++++++++++++++++++ src/components/ui/label.tsx | 26 +++ 6 files changed, 341 insertions(+), 45 deletions(-) create mode 100644 src/components/ui/form.tsx create mode 100644 src/components/ui/label.tsx diff --git a/package.json b/package.json index c4557b8..d921314 100644 --- a/package.json +++ b/package.json @@ -11,10 +11,12 @@ "prettier:write": "prettier --write ." }, "dependencies": { + "@hookform/resolvers": "^3.3.2", "@radix-ui/react-alert-dialog": "^1.0.5", "@radix-ui/react-collapsible": "^1.0.3", "@radix-ui/react-dialog": "1.0.4", "@radix-ui/react-dropdown-menu": "^2.0.6", + "@radix-ui/react-label": "^2.0.2", "@radix-ui/react-scroll-area": "^1.0.5", "@radix-ui/react-slot": "^1.0.2", "@rainbow-me/rainbowkit": "^1.3.0", @@ -26,11 +28,13 @@ "pino-pretty": "^10.2.3", "react": "^18.2.0", "react-dom": "^18.2.0", + "react-hook-form": "^7.48.2", "react-wrap-balancer": "^1.1.0", "tailwind-merge": "^2.0.0", "tailwindcss-animate": "^1.0.7", "viem": "~1.19.5", - "wagmi": "^1.4.7" + "wagmi": "^1.4.7", + "zod": "^3.22.4" }, "devDependencies": { "@types/node": "^20.9.3", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index b7d5b8c..a5eb995 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1,6 +1,9 @@ lockfileVersion: '6.0' dependencies: + '@hookform/resolvers': + specifier: ^3.3.2 + version: 3.3.2(react-hook-form@7.48.2) '@radix-ui/react-alert-dialog': specifier: ^1.0.5 version: 1.0.5(@types/react-dom@18.2.16)(@types/react@18.2.38)(react-dom@18.2.0)(react@18.2.0) @@ -13,6 +16,9 @@ dependencies: '@radix-ui/react-dropdown-menu': specifier: ^2.0.6 version: 2.0.6(@types/react-dom@18.2.16)(@types/react@18.2.38)(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-label': + specifier: ^2.0.2 + version: 2.0.2(@types/react-dom@18.2.16)(@types/react@18.2.38)(react-dom@18.2.0)(react@18.2.0) '@radix-ui/react-scroll-area': specifier: ^1.0.5 version: 1.0.5(@types/react-dom@18.2.16)(@types/react@18.2.38)(react-dom@18.2.0)(react@18.2.0) @@ -46,6 +52,9 @@ dependencies: react-dom: specifier: ^18.2.0 version: 18.2.0(react@18.2.0) + react-hook-form: + specifier: ^7.48.2 + version: 7.48.2(react@18.2.0) react-wrap-balancer: specifier: ^1.1.0 version: 1.1.0(react@18.2.0) @@ -57,10 +66,13 @@ dependencies: version: 1.0.7(tailwindcss@3.3.5) viem: specifier: ~1.19.5 - version: 1.19.5(typescript@5.3.2) + version: 1.19.5(typescript@5.3.2)(zod@3.22.4) wagmi: specifier: ^1.4.7 - version: 1.4.7(@types/react@18.2.38)(react-dom@18.2.0)(react@18.2.0)(typescript@5.3.2)(viem@1.19.5) + version: 1.4.7(@types/react@18.2.38)(react-dom@18.2.0)(react@18.2.0)(typescript@5.3.2)(viem@1.19.5)(zod@3.22.4) + zod: + specifier: ^3.22.4 + version: 3.22.4 devDependencies: '@types/node': @@ -218,6 +230,14 @@ packages: react-dom: 18.2.0(react@18.2.0) dev: false + /@hookform/resolvers@3.3.2(react-hook-form@7.48.2): + resolution: {integrity: sha512-Tw+GGPnBp+5DOsSg4ek3LCPgkBOuOgS5DsDV7qsWNH9LZc433kgsWICjlsh2J9p04H2K66hsXPPb9qn9ILdUtA==} + peerDependencies: + react-hook-form: ^7.0.0 + dependencies: + react-hook-form: 7.48.2(react@18.2.0) + dev: false + /@humanwhocodes/config-array@0.11.13: resolution: {integrity: sha512-JSBDMiDKSzQVngfRjOdFXgFfklaXI4K9nLF49Auh21lmBWRLIK3+xTErTWD4KU54pb6coM6ESE7Awz/FNU3zgQ==} engines: {node: '>=10.10.0'} @@ -850,6 +870,27 @@ packages: react: 18.2.0 dev: false + /@radix-ui/react-label@2.0.2(@types/react-dom@18.2.16)(@types/react@18.2.38)(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-N5ehvlM7qoTLx7nWPodsPYPgMzA5WM8zZChQg8nyFJKnDO5WHdba1vv5/H6IO5LtJMfD2Q3wh1qHFGNtK0w3bQ==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 + react-dom: ^16.8 || ^17.0 || ^18.0 + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + dependencies: + '@babel/runtime': 7.23.2 + '@radix-ui/react-primitive': 1.0.3(@types/react-dom@18.2.16)(@types/react@18.2.38)(react-dom@18.2.0)(react@18.2.0) + '@types/react': 18.2.38 + '@types/react-dom': 18.2.16 + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + dev: false + /@radix-ui/react-menu@2.0.6(@types/react-dom@18.2.16)(@types/react@18.2.38)(react-dom@18.2.0)(react@18.2.0): resolution: {integrity: sha512-BVkFLS+bUC8HcImkRKPSiVumA1VPOOEC5WBMiT+QAVsPzW1FJzI9KnqgGxVDPBcql5xXrHkD3JOVoXWEXD8SYA==} peerDependencies: @@ -1189,8 +1230,8 @@ packages: react-dom: 18.2.0(react@18.2.0) react-remove-scroll: 2.5.4(@types/react@18.2.38)(react@18.2.0) ua-parser-js: 1.0.37 - viem: 1.19.5(typescript@5.3.2) - wagmi: 1.4.7(@types/react@18.2.38)(react-dom@18.2.0)(react@18.2.0)(typescript@5.3.2)(viem@1.19.5) + viem: 1.19.5(typescript@5.3.2)(zod@3.22.4) + wagmi: 1.4.7(@types/react@18.2.38)(react-dom@18.2.0)(react@18.2.0)(typescript@5.3.2)(viem@1.19.5)(zod@3.22.4) transitivePeerDependencies: - '@types/react' dev: false @@ -1199,10 +1240,10 @@ packages: resolution: {integrity: sha512-6i/8UoL0P5y4leBIGzvkZdS85RDMG9y1ihZzmTZQ5LdHUYmZ7pKFoj8X0236s3lusPs1Fa5HTQUpwI+UfTcmeA==} dev: true - /@safe-global/safe-apps-provider@0.17.1(typescript@5.3.2): + /@safe-global/safe-apps-provider@0.17.1(typescript@5.3.2)(zod@3.22.4): resolution: {integrity: sha512-lYfRqrbbK1aKU1/UGkYWc/X7PgySYcumXKc5FB2uuwAs2Ghj8uETuW5BrwPqyjBknRxutFbTv+gth/JzjxAhdQ==} dependencies: - '@safe-global/safe-apps-sdk': 8.0.0(typescript@5.3.2) + '@safe-global/safe-apps-sdk': 8.0.0(typescript@5.3.2)(zod@3.22.4) events: 3.3.0 transitivePeerDependencies: - bufferutil @@ -1212,11 +1253,11 @@ packages: - zod dev: false - /@safe-global/safe-apps-sdk@8.0.0(typescript@5.3.2): + /@safe-global/safe-apps-sdk@8.0.0(typescript@5.3.2)(zod@3.22.4): resolution: {integrity: sha512-gYw0ki/EAuV1oSyMxpqandHjnthZjYYy+YWpTAzf8BqfXM3ItcZLpjxfg+3+mXW8HIO+3jw6T9iiqEXsqHaMMw==} dependencies: '@safe-global/safe-gateway-typescript-sdk': 3.7.3 - viem: 1.19.5(typescript@5.3.2) + viem: 1.19.5(typescript@5.3.2)(zod@3.22.4) transitivePeerDependencies: - bufferutil - encoding @@ -1225,11 +1266,11 @@ packages: - zod dev: false - /@safe-global/safe-apps-sdk@8.1.0(typescript@5.3.2): + /@safe-global/safe-apps-sdk@8.1.0(typescript@5.3.2)(zod@3.22.4): resolution: {integrity: sha512-XJbEPuaVc7b9n23MqlF6c+ToYIS3f7P2Sel8f3cSBQ9WORE4xrSuvhMpK9fDSFqJ7by/brc+rmJR/5HViRr0/w==} dependencies: '@safe-global/safe-gateway-typescript-sdk': 3.7.3 - viem: 1.19.5(typescript@5.3.2) + viem: 1.19.5(typescript@5.3.2)(zod@3.22.4) transitivePeerDependencies: - bufferutil - encoding @@ -1624,7 +1665,7 @@ packages: '@vanilla-extract/css': 1.9.1 dev: false - /@wagmi/connectors@3.1.5(@types/react@18.2.38)(react@18.2.0)(typescript@5.3.2)(viem@1.19.5): + /@wagmi/connectors@3.1.5(@types/react@18.2.38)(react@18.2.0)(typescript@5.3.2)(viem@1.19.5)(zod@3.22.4): resolution: {integrity: sha512-aE4rWZbivqWa9HqjiLDPtwROH2b1Az+lBVMeZ3o/aFxGNGNEkdrSAMOUG15/UFy3VnN6HqGOtTobOBZ10JhfNQ==} peerDependencies: typescript: '>=5.0.4' @@ -1635,16 +1676,16 @@ packages: dependencies: '@coinbase/wallet-sdk': 3.6.6 '@ledgerhq/connect-kit-loader': 1.1.2 - '@safe-global/safe-apps-provider': 0.17.1(typescript@5.3.2) - '@safe-global/safe-apps-sdk': 8.1.0(typescript@5.3.2) + '@safe-global/safe-apps-provider': 0.17.1(typescript@5.3.2)(zod@3.22.4) + '@safe-global/safe-apps-sdk': 8.1.0(typescript@5.3.2)(zod@3.22.4) '@walletconnect/ethereum-provider': 2.10.2(@walletconnect/modal@2.6.2) '@walletconnect/legacy-provider': 2.0.0 '@walletconnect/modal': 2.6.2(@types/react@18.2.38)(react@18.2.0) '@walletconnect/utils': 2.10.2 - abitype: 0.8.7(typescript@5.3.2) + abitype: 0.8.7(typescript@5.3.2)(zod@3.22.4) eventemitter3: 4.0.7 typescript: 5.3.2 - viem: 1.19.5(typescript@5.3.2) + viem: 1.19.5(typescript@5.3.2)(zod@3.22.4) transitivePeerDependencies: - '@react-native-async-storage/async-storage' - '@types/react' @@ -1657,7 +1698,7 @@ packages: - zod dev: false - /@wagmi/core@1.4.7(@types/react@18.2.38)(react@18.2.0)(typescript@5.3.2)(viem@1.19.5): + /@wagmi/core@1.4.7(@types/react@18.2.38)(react@18.2.0)(typescript@5.3.2)(viem@1.19.5)(zod@3.22.4): resolution: {integrity: sha512-PiOIGni8ArQoPmuDylHX38zMt2nPnTYRIluIqiduKyGCM61X/tf10a0rafUMOOphDPudZu1TacNDhCSeoh/LEA==} peerDependencies: typescript: '>=5.0.4' @@ -1666,11 +1707,11 @@ packages: typescript: optional: true dependencies: - '@wagmi/connectors': 3.1.5(@types/react@18.2.38)(react@18.2.0)(typescript@5.3.2)(viem@1.19.5) - abitype: 0.8.7(typescript@5.3.2) + '@wagmi/connectors': 3.1.5(@types/react@18.2.38)(react@18.2.0)(typescript@5.3.2)(viem@1.19.5)(zod@3.22.4) + abitype: 0.8.7(typescript@5.3.2)(zod@3.22.4) eventemitter3: 4.0.7 typescript: 5.3.2 - viem: 1.19.5(typescript@5.3.2) + viem: 1.19.5(typescript@5.3.2)(zod@3.22.4) zustand: 4.3.8(react@18.2.0) transitivePeerDependencies: - '@react-native-async-storage/async-storage' @@ -2080,7 +2121,7 @@ packages: through: 2.3.8 dev: false - /abitype@0.8.7(typescript@5.3.2): + /abitype@0.8.7(typescript@5.3.2)(zod@3.22.4): resolution: {integrity: sha512-wQ7hV8Yg/yKmGyFpqrNZufCxbszDe5es4AZGYPBitocfSqXtjrTG9JMWFcc4N30ukl2ve48aBTwt7NJxVQdU3w==} peerDependencies: typescript: '>=5.0.4' @@ -2090,9 +2131,10 @@ packages: optional: true dependencies: typescript: 5.3.2 + zod: 3.22.4 dev: false - /abitype@0.9.8(typescript@5.3.2): + /abitype@0.9.8(typescript@5.3.2)(zod@3.22.4): resolution: {integrity: sha512-puLifILdm+8sjyss4S+fsUN09obiT1g2YW6CtcQF+QDzxR0euzgEB29MZujC6zMk2a6SVmtttq1fc6+YFA7WYQ==} peerDependencies: typescript: '>=5.0.4' @@ -2104,6 +2146,7 @@ packages: optional: true dependencies: typescript: 5.3.2 + zod: 3.22.4 dev: false /abort-controller@3.0.0: @@ -4551,6 +4594,15 @@ packages: scheduler: 0.23.0 dev: false + /react-hook-form@7.48.2(react@18.2.0): + resolution: {integrity: sha512-H0T2InFQb1hX7qKtDIZmvpU1Xfn/bdahWBN1fH19gSe4bBEqTfmlr7H3XWTaVtiK4/tpPaI1F3355GPMZYge+A==} + engines: {node: '>=12.22.0'} + peerDependencies: + react: ^16.8.0 || ^17 || ^18 + dependencies: + react: 18.2.0 + dev: false + /react-is@16.13.1: resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==} dev: true @@ -5351,7 +5403,7 @@ packages: use-sync-external-store: 1.2.0(react@18.2.0) dev: false - /viem@1.19.5(typescript@5.3.2): + /viem@1.19.5(typescript@5.3.2)(zod@3.22.4): resolution: {integrity: sha512-lcWP9ErivUKVRpnDgI6mly27iWibJN1jZW2yUGuwU2CjfVHs7DJCyA5ityL+n/B+Oss4bllzQkWtJiBAHqaX2g==} peerDependencies: typescript: '>=5.0.4' @@ -5364,7 +5416,7 @@ packages: '@noble/hashes': 1.3.2 '@scure/bip32': 1.3.2 '@scure/bip39': 1.2.1 - abitype: 0.9.8(typescript@5.3.2) + abitype: 0.9.8(typescript@5.3.2)(zod@3.22.4) isows: 1.0.3(ws@8.13.0) typescript: 5.3.2 ws: 8.13.0(bufferutil@4.0.8)(utf-8-validate@5.0.10) @@ -5374,7 +5426,7 @@ packages: - zod dev: false - /wagmi@1.4.7(@types/react@18.2.38)(react-dom@18.2.0)(react@18.2.0)(typescript@5.3.2)(viem@1.19.5): + /wagmi@1.4.7(@types/react@18.2.38)(react-dom@18.2.0)(react@18.2.0)(typescript@5.3.2)(viem@1.19.5)(zod@3.22.4): resolution: {integrity: sha512-/k8gA9S6RnwU6Qroxs630jAFvRIx+DSKpCP1owgAEGWc7D2bAJHljwRSCRTGENz48HyJ4V3R7KYV1yImxPvM3A==} peerDependencies: react: '>=17.0.0' @@ -5387,12 +5439,12 @@ packages: '@tanstack/query-sync-storage-persister': 4.29.7 '@tanstack/react-query': 4.29.7(react-dom@18.2.0)(react@18.2.0) '@tanstack/react-query-persist-client': 4.29.7(@tanstack/react-query@4.29.7) - '@wagmi/core': 1.4.7(@types/react@18.2.38)(react@18.2.0)(typescript@5.3.2)(viem@1.19.5) - abitype: 0.8.7(typescript@5.3.2) + '@wagmi/core': 1.4.7(@types/react@18.2.38)(react@18.2.0)(typescript@5.3.2)(viem@1.19.5)(zod@3.22.4) + abitype: 0.8.7(typescript@5.3.2)(zod@3.22.4) react: 18.2.0 typescript: 5.3.2 use-sync-external-store: 1.2.0(react@18.2.0) - viem: 1.19.5(typescript@5.3.2) + viem: 1.19.5(typescript@5.3.2)(zod@3.22.4) transitivePeerDependencies: - '@react-native-async-storage/async-storage' - '@types/react' @@ -5572,6 +5624,10 @@ packages: engines: {node: '>=10'} dev: true + /zod@3.22.4: + resolution: {integrity: sha512-iC+8Io04lddc+mVqQ9AZ7OQ2MrUKGN+oIQyq1vemgt46jwCwLfhq7/pwnBnNXXXZb8VTVLKwp9EDkx+ryxIWmg==} + dev: false + /zustand@4.3.8(react@18.2.0): resolution: {integrity: sha512-4h28KCkHg5ii/wcFFJ5Fp+k1J3gJoasaIbppdgZFO4BPJnsNxL0mQXBSFgOgAdCdBj35aDTPvdAJReTMntFPGg==} engines: {node: '>=12.7.0'} diff --git a/src/components/balance.tsx b/src/components/balance.tsx index 20d1ca9..af0ecc3 100644 --- a/src/components/balance.tsx +++ b/src/components/balance.tsx @@ -1,11 +1,28 @@ 'use client' import { useState } from 'react' -import type { Address } from 'wagmi' import { useAccount, useBalance } from 'wagmi' +import { useForm } from 'react-hook-form' +import * as z from 'zod' +import { zodResolver } from '@hookform/resolvers/zod' +import { isAddress } from 'viem' +import type { Address } from 'wagmi' import { Button } from '@/components/ui/button' import { Input } from '@/components/ui/input' +import { + Form, + FormControl, + FormField, + FormItem, + FormMessage, +} from '@/components/ui/form' + +const isValidAddress = (value: unknown) => isAddress(value as string) + +const formSchema = z.object({ + address: z.custom(isValidAddress, 'Invalid Address'), +}) export function AccountBalance() { const { address } = useAccount() @@ -30,23 +47,42 @@ export function FindBalance() { address: address as Address, }) - const [value, setValue] = useState('') + const form = useForm>({ + resolver: zodResolver(formSchema), + defaultValues: { + address: '', + }, + }) + + function onSubmit(values: z.infer) { + if (values.address === address) { + refetch() + } else { + setAddress(values.address) + } + } return ( <> -
- setValue(e.target.value)} - placeholder="Wallet address" - value={value} - /> - -
+
+ + ( + + + + + + + )} + /> + + +
{data?.formatted}
) diff --git a/src/components/send-transaction-prepared.tsx b/src/components/send-transaction-prepared.tsx index 849ec92..6fadf98 100644 --- a/src/components/send-transaction-prepared.tsx +++ b/src/components/send-transaction-prepared.tsx @@ -34,8 +34,6 @@ export function SendTransactionPrepared() { isSuccess, } = useWaitForTransaction({ hash: data?.hash }) - console.log(isError, error?.message) - return ( <>
= 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, formState } = useFormContext() + + 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, +) + +const FormItem = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => { + const id = React.useId() + + return ( + +
+ + ) +}) +FormItem.displayName = 'FormItem' + +const FormLabel = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => { + const { error, formItemId } = useFormField() + + return ( +