├── .changeset ├── README.md └── config.json ├── .eslintignore ├── .eslintrc.cjs ├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.yml │ └── config.yml ├── version.sh └── workflows │ ├── preview.yml │ ├── release.yml │ ├── tests.yml │ └── validate.yml ├── .gitignore ├── .husky ├── .gitignore └── pre-commit ├── .npmrc ├── .prettierignore ├── .prettierrc.json ├── LICENSE ├── README.md ├── docs ├── README.md ├── accessibility.md ├── api │ ├── react │ │ ├── FormProvider.md │ │ ├── FormStateInput.md │ │ ├── getCollectionProps.md │ │ ├── getFieldsetProps.md │ │ ├── getFormProps.md │ │ ├── getInputProps.md │ │ ├── getSelectProps.md │ │ ├── getTextareaProps.md │ │ ├── useField.md │ │ ├── useForm.md │ │ ├── useFormMetadata.md │ │ └── useInputControl.md │ ├── valibot │ │ ├── coerceFormValue.md │ │ ├── conformValibotMessage.md │ │ ├── getValibotConstraint.md │ │ └── parseWithValibot.md │ ├── validitystate.md │ ├── yup │ │ ├── getYupConstraint.md │ │ └── parseWithYup.md │ └── zod │ │ ├── coerceFormValue.md │ │ ├── conformZodMessage.md │ │ ├── getZodConstraint.md │ │ └── parseWithZod.md ├── checkbox-and-radio-group.md ├── complex-structures.md ├── file-upload.md ├── installation.md ├── integration │ ├── nextjs.md │ ├── remix.md │ └── ui-libraries.md ├── intent-button.md ├── ja │ ├── README.md │ ├── accessibility.md │ ├── api │ │ ├── react │ │ │ ├── FormProvider.md │ │ │ ├── FormStateInput.md │ │ │ ├── getCollectionProps.md │ │ │ ├── getFieldsetProps.md │ │ │ ├── getFormProps.md │ │ │ ├── getInputProps.md │ │ │ ├── getSelectProps.md │ │ │ ├── getTextareaProps.md │ │ │ ├── useField.md │ │ │ ├── useForm.md │ │ │ ├── useFormMetadata.md │ │ │ └── useInputControl.md │ │ ├── valibot │ │ │ ├── coerceFormValue.md │ │ │ ├── conformValibotMessage.md │ │ │ ├── getValibotConstraint.md │ │ │ └── parseWithValibot.md │ │ ├── validitystate.md │ │ ├── yup │ │ │ ├── getYupConstraint.md │ │ │ └── parseWithYup.md │ │ └── zod │ │ │ ├── coerceFormValue.md │ │ │ ├── conformZodMessage.md │ │ │ ├── getZodConstraint.md │ │ │ └── parseWithZod.md │ ├── checkbox-and-radio-group.md │ ├── complex-structures.md │ ├── file-upload.md │ ├── installation.md │ ├── integration │ │ ├── nextjs.md │ │ ├── remix.md │ │ └── ui-libraries.md │ ├── intent-button.md │ ├── overview.md │ ├── tutorial.md │ ├── upgrading-v1.md │ └── validation.md ├── overview.md ├── tutorial.md ├── upgrading-v1.md └── validation.md ├── examples ├── chakra-ui │ ├── .gitignore │ ├── README.md │ ├── package.json │ ├── public │ │ └── index.html │ ├── src │ │ ├── App.tsx │ │ ├── index.css │ │ ├── index.tsx │ │ └── react-app-env.d.ts │ └── tsconfig.json ├── headless-ui │ ├── .gitignore │ ├── README.md │ ├── package.json │ ├── postcss.config.js │ ├── public │ │ └── index.html │ ├── sandbox.config.json │ ├── src │ │ ├── App.tsx │ │ ├── index.css │ │ ├── index.tsx │ │ └── react-app-env.d.ts │ ├── tailwind.config.js │ └── tsconfig.json ├── material-ui │ ├── .gitignore │ ├── README.md │ ├── package.json │ ├── public │ │ └── index.html │ ├── src │ │ ├── App.tsx │ │ ├── index.css │ │ ├── index.tsx │ │ └── react-app-env.d.ts │ └── tsconfig.json ├── nextjs │ ├── .eslintrc.json │ ├── .gitignore │ ├── README.md │ ├── app │ │ ├── actions.ts │ │ ├── favicon.ico │ │ ├── form.tsx │ │ ├── globals.css │ │ ├── layout.tsx │ │ ├── login │ │ │ └── page.tsx │ │ ├── page.tsx │ │ ├── schema.ts │ │ ├── signup │ │ │ └── page.tsx │ │ └── todos │ │ │ └── page.tsx │ ├── next.config.js │ ├── package.json │ ├── public │ │ ├── next.svg │ │ └── vercel.svg │ └── tsconfig.json ├── radix-ui │ ├── .eslintrc.cjs │ ├── .gitignore │ ├── README.md │ ├── index.html │ ├── package.json │ ├── postcss.config.js │ ├── public │ │ └── vite.svg │ ├── src │ │ ├── App.tsx │ │ ├── index.css │ │ ├── main.tsx │ │ ├── ui │ │ │ ├── Checkbox.tsx │ │ │ ├── RadioGroup.tsx │ │ │ ├── Select.tsx │ │ │ ├── Slider.tsx │ │ │ ├── Switch.tsx │ │ │ └── ToggleGroup.tsx │ │ └── vite-env.d.ts │ ├── tailwind.config.js │ ├── tsconfig.json │ ├── tsconfig.node.json │ └── vite.config.ts ├── react-router │ ├── README.md │ ├── index.html │ ├── package.json │ ├── src │ │ ├── App.tsx │ │ ├── _index.tsx │ │ ├── index.css │ │ ├── login-fetcher.tsx │ │ ├── login.tsx │ │ ├── main.tsx │ │ ├── signup.tsx │ │ ├── todos.tsx │ │ └── vite-env.d.ts │ ├── tsconfig.json │ └── vite.config.ts ├── remix │ ├── .eslintrc │ ├── .gitignore │ ├── .stackblitzrc │ ├── README.md │ ├── app │ │ ├── root.tsx │ │ ├── routes │ │ │ ├── _index.tsx │ │ │ ├── login-fetcher.tsx │ │ │ ├── login.tsx │ │ │ ├── signup.tsx │ │ │ └── todos.tsx │ │ └── styles.css │ ├── package.json │ ├── remix.config.js │ ├── remix.env.d.ts │ ├── sandbox.config.json │ └── tsconfig.json └── shadcn-ui │ ├── .eslintrc.cjs │ ├── .gitignore │ ├── README.md │ ├── components.json │ ├── index.html │ ├── package.json │ ├── postcss.config.js │ ├── public │ └── vite.svg │ ├── src │ ├── App.tsx │ ├── assets │ │ └── react.svg │ ├── components │ │ ├── Field.tsx │ │ ├── conform │ │ │ ├── Checkbox.tsx │ │ │ ├── CheckboxGroup.tsx │ │ │ ├── CountryPicker.tsx │ │ │ ├── DatePicker.tsx │ │ │ ├── Input.tsx │ │ │ ├── InputOTP.tsx │ │ │ ├── RadioGroup.tsx │ │ │ ├── Select.tsx │ │ │ ├── Slider.tsx │ │ │ ├── Switch.tsx │ │ │ ├── Textarea.tsx │ │ │ └── ToggleGroup.tsx │ │ └── ui │ │ │ ├── button.tsx │ │ │ ├── calendar.tsx │ │ │ ├── checkbox.tsx │ │ │ ├── command.tsx │ │ │ ├── dialog.tsx │ │ │ ├── input-otp.tsx │ │ │ ├── input.tsx │ │ │ ├── label.tsx │ │ │ ├── popover.tsx │ │ │ ├── radio-group.tsx │ │ │ ├── select.tsx │ │ │ ├── slider.tsx │ │ │ ├── switch.tsx │ │ │ ├── textarea.tsx │ │ │ ├── toggle-group.tsx │ │ │ └── toggle.tsx │ ├── index.css │ ├── lib │ │ └── utils.ts │ ├── main.tsx │ └── vite-env.d.ts │ ├── tailwind.config.js │ ├── tsconfig.json │ ├── tsconfig.node.json │ └── vite.config.ts ├── guide ├── .gitignore ├── .node-version ├── README.md ├── app │ ├── components.tsx │ ├── entry.client.tsx │ ├── entry.server.tsx │ ├── layout.tsx │ ├── markdoc.tsx │ ├── root.tsx │ ├── routes │ │ ├── $.tsx │ │ ├── _index.tsx │ │ └── examples.$name.tsx │ ├── styles.css │ └── util.ts ├── package.json ├── public │ ├── _headers │ ├── _routes.json │ └── fonts │ │ ├── ubuntu-v20-latin-300.woff │ │ ├── ubuntu-v20-latin-300.woff2 │ │ ├── ubuntu-v20-latin-500.woff │ │ ├── ubuntu-v20-latin-500.woff2 │ │ ├── ubuntu-v20-latin-700.woff │ │ ├── ubuntu-v20-latin-700.woff2 │ │ ├── ubuntu-v20-latin-regular.woff │ │ └── ubuntu-v20-latin-regular.woff2 ├── remix.config.js ├── remix.env.d.ts ├── server.ts ├── tailwind.config.cjs └── tsconfig.json ├── package.json ├── packages ├── conform-dom │ ├── .gitignore │ ├── dom.ts │ ├── form.ts │ ├── formdata.ts │ ├── index.ts │ ├── package.json │ ├── rollup.config.js │ ├── submission.ts │ ├── tsconfig.build.json │ ├── tsconfig.json │ └── util.ts ├── conform-react │ ├── .gitignore │ ├── context.tsx │ ├── helpers.ts │ ├── hooks.ts │ ├── index.ts │ ├── integrations.ts │ ├── package.json │ ├── rollup.config.js │ ├── tsconfig.build.json │ └── tsconfig.json ├── conform-valibot │ ├── .gitignore │ ├── coercion.ts │ ├── constraint.ts │ ├── index.ts │ ├── package.json │ ├── parse.ts │ ├── rollup.config.js │ ├── tests │ │ ├── coercion.test.ts │ │ ├── coercion │ │ │ ├── method │ │ │ │ ├── config.test.ts │ │ │ │ └── fallback.test.ts │ │ │ └── schema │ │ │ │ ├── any.test.ts │ │ │ │ ├── array.test.ts │ │ │ │ ├── bigint.test.ts │ │ │ │ ├── blob.test.ts │ │ │ │ ├── boolean.test.ts │ │ │ │ ├── date.test.ts │ │ │ │ ├── enum.test.ts │ │ │ │ ├── exactOptional.test.ts │ │ │ │ ├── exactOptionalAsync.test.ts │ │ │ │ ├── file.test.ts │ │ │ │ ├── intersect.test.ts │ │ │ │ ├── intersectAsync.test.ts │ │ │ │ ├── literal.test.ts │ │ │ │ ├── looseObject.test.ts │ │ │ │ ├── looseObjectAsync.test.ts │ │ │ │ ├── nonNullish.test.ts │ │ │ │ ├── nonNullishAsync.test.ts │ │ │ │ ├── nonOptional.test.ts │ │ │ │ ├── nonOptionalAsync.test.ts │ │ │ │ ├── nullable.test.ts │ │ │ │ ├── nullableAsync.test.ts │ │ │ │ ├── nullish.test.ts │ │ │ │ ├── nullishAsync.test.ts │ │ │ │ ├── number.test.ts │ │ │ │ ├── object.test.ts │ │ │ │ ├── objectAsync.test.ts │ │ │ │ ├── objectWithRest.test.ts │ │ │ │ ├── objectWithRestAsync.test.ts │ │ │ │ ├── optional.test.ts │ │ │ │ ├── optionalAsync.test.ts │ │ │ │ ├── picklist.test.ts │ │ │ │ ├── strictObject.test.ts │ │ │ │ ├── strictObjectAsync.test.ts │ │ │ │ ├── string.test.ts │ │ │ │ ├── tuple.test.ts │ │ │ │ ├── tupleAsync.test.ts │ │ │ │ ├── tupleWithRest.test.ts │ │ │ │ ├── tupleWithRestAsync.test.ts │ │ │ │ ├── undefined.test.ts │ │ │ │ ├── undefinedable.test.ts │ │ │ │ ├── undefinedableAsync.test.ts │ │ │ │ ├── union.test.ts │ │ │ │ ├── variant.test.ts │ │ │ │ ├── variantAsync.test.ts │ │ │ │ ├── wrap.test.ts │ │ │ │ └── wrapAsync.test.ts │ │ ├── constraint.test.ts │ │ ├── helpers │ │ │ ├── FormData.ts │ │ │ └── valibot.ts │ │ └── parse.test.ts │ ├── tsconfig.build.json │ └── tsconfig.json ├── conform-validitystate │ ├── .gitignore │ ├── index.ts │ ├── package.json │ ├── rollup.config.js │ ├── tsconfig.build.json │ └── tsconfig.json ├── conform-yup │ ├── .gitignore │ ├── constraint.ts │ ├── index.ts │ ├── package.json │ ├── rollup.config.js │ ├── tsconfig.build.json │ └── tsconfig.json └── conform-zod │ ├── .gitignore │ ├── package.json │ ├── rollup.config.js │ ├── tests │ └── helpers │ │ ├── FromData.ts │ │ └── zod.ts │ ├── tsconfig.build.json │ ├── tsconfig.json │ ├── v3 │ ├── coercion.ts │ ├── constraint.ts │ ├── index.ts │ ├── parse.ts │ └── tests │ │ ├── coercion │ │ ├── coercion.spec.ts │ │ └── schema │ │ │ ├── array.spec.ts │ │ │ ├── bigint.spec.ts │ │ │ ├── boolean.spec.ts │ │ │ ├── brand.spec.ts │ │ │ ├── catch.spec.ts │ │ │ ├── date.spec.ts │ │ │ ├── default.spec.ts │ │ │ ├── discriminatedUnion.spec.ts │ │ │ ├── file.spec.ts │ │ │ ├── lazy.spec.ts │ │ │ ├── literal.spec.ts │ │ │ ├── number.spec.ts │ │ │ ├── object.spec.ts │ │ │ ├── optional.spec.ts │ │ │ ├── preprocess.spec.ts │ │ │ └── string.spec.ts │ │ ├── constraint.spec.ts │ │ └── parse.spec.ts │ └── v4 │ ├── coercion.ts │ ├── constraint.ts │ ├── index.ts │ ├── parse.ts │ └── tests │ ├── coercion │ ├── coercion.spec.ts │ └── schema │ │ ├── array.spec.ts │ │ ├── bigint.spec.ts │ │ ├── boolean.spec.ts │ │ ├── brand.spec.ts │ │ ├── catch.spec.ts │ │ ├── date.spec.ts │ │ ├── default.spec.ts │ │ ├── discriminatedUnion.spec.ts │ │ ├── file.spec.ts │ │ ├── lazy.spec.ts │ │ ├── literal.spec.ts │ │ ├── number.spec.ts │ │ ├── object.spec.ts │ │ ├── optional.spec.ts │ │ ├── preprocess.spec.ts │ │ └── string.spec.ts │ ├── constraint.spec.ts │ └── parse.spec.ts ├── playground ├── .gitignore ├── README.md ├── app │ ├── components.tsx │ ├── root.tsx │ ├── routes │ │ ├── _index.tsx │ │ ├── async-validation.tsx │ │ ├── collection.tsx │ │ ├── custom-inputs.tsx │ │ ├── dom-value.tsx │ │ ├── file-upload.tsx │ │ ├── form-attributes.tsx │ │ ├── form-control.tsx │ │ ├── input-attributes.tsx │ │ ├── input-event.tsx │ │ ├── metadata.tsx │ │ ├── nested-list.tsx │ │ ├── parse-with-yup.tsx │ │ ├── recursive-list.tsx │ │ ├── reset-default-value.tsx │ │ ├── simple-list.tsx │ │ ├── subscription.tsx │ │ ├── typing.tsx │ │ ├── validate-constraint.tsx │ │ ├── validation-flow.tsx │ │ └── validitystate.tsx │ └── tailwind.css ├── package.json ├── postcss.config.js ├── public │ └── favicon.ico ├── remix.config.js ├── remix.env.d.ts ├── tailwind.config.ts └── tsconfig.json ├── playwright.config.ts ├── pnpm-lock.yaml ├── pnpm-workspace.yaml ├── tests ├── conform-dom.spec.ts ├── conform-react.spec.ts ├── conform-yup.spec.ts ├── helpers.ts └── integrations │ ├── async-validation.spec.ts │ ├── collection.spec.ts │ ├── conform-validitystate.spec.ts │ ├── custom-inputs.spec.ts │ ├── dom-value.spec.ts │ ├── file-upload.spec.ts │ ├── form-attributes.spec.ts │ ├── form-control.spec.ts │ ├── helpers.ts │ ├── input-attributes.spec.ts │ ├── input-event.spec.ts │ ├── metadata.spec.ts │ ├── nested-list.spec.ts │ ├── parse-with-yup.spec.ts │ ├── recursive-list.spec.ts │ ├── reset-default-value.spec.ts │ ├── simple-list.spec.ts │ ├── subscription.spec.ts │ ├── validate-constraint.spec.ts │ └── validation-flow.spec.ts ├── tsconfig.json └── vitest.workspace.mts /.changeset/README.md: -------------------------------------------------------------------------------- 1 | # Changesets 2 | 3 | Hello and welcome! This folder has been automatically generated by `@changesets/cli`, a build tool that works 4 | with multi-package repos, or single-package repos to help you version and publish your code. You can 5 | find the full documentation for it [in our repository](https://github.com/changesets/changesets) 6 | 7 | We have a quick list of common questions to get you started engaging with this project in 8 | [our documentation](https://github.com/changesets/changesets/blob/main/docs/common-questions.md) 9 | -------------------------------------------------------------------------------- /.changeset/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://unpkg.com/@changesets/config@3.0.1/schema.json", 3 | "changelog": "@changesets/cli/changelog", 4 | "commit": false, 5 | "fixed": [ 6 | [ 7 | "@conform-to/dom", 8 | "@conform-to/react", 9 | "@conform-to/valibot", 10 | "@conform-to/yup", 11 | "@conform-to/zod" 12 | ] 13 | ], 14 | "linked": [], 15 | "access": "public", 16 | "baseBranch": "main", 17 | "updateInternalDependencies": "patch", 18 | "ignore": ["@conform-example/*"] 19 | } 20 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | *.log 3 | logs 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | 8 | # Diagnostic reports (https://nodejs.org/api/report.html) 9 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 10 | 11 | # Runtime data 12 | *.pid 13 | *.pid.lock 14 | *.seed 15 | pids 16 | 17 | # Anything built 18 | build/ 19 | 20 | # tsc 21 | *.tsbuildinfo 22 | 23 | # Dependency directories 24 | node_modules/ 25 | 26 | # Optional npm cache directory 27 | .npm 28 | 29 | # Optional eslint cache 30 | .eslintcache 31 | 32 | # Output of 'npm pack' 33 | *.tgz 34 | /playwright-report/ 35 | /playwright/.cache/ 36 | /test-results/ 37 | 38 | # DS Store 39 | .DS_Store 40 | 41 | # Project files 42 | README.md 43 | pnpm-lock.yaml 44 | 45 | # Build files 46 | .cache/ 47 | .wrangler/ 48 | !rollup.config.js 49 | /packages/**/*.js 50 | /packages/**/*.mjs 51 | /guide/functions/ 52 | 53 | # Others 54 | /examples/ 55 | /packages/conform-validitystate/ 56 | 57 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | blank_issues_enabled: false 2 | contact_links: 3 | - name: 🤔 Feature Requests & Questions 4 | url: https://github.com/edmundhung/conform/discussions 5 | about: You can ask questions and share your ideas here. 6 | - name: 💿 Remix support 7 | url: https://rmx.as/discord 8 | about: Join the Remix discord channel if you need help integrating with Remix / React Router. 9 | - name: 🌟 EpicStack support 10 | url: https://kcd.im/discord 11 | about: Join the KCD Discord channel if you need help with the EpicStack setup. 12 | -------------------------------------------------------------------------------- /.github/version.sh: -------------------------------------------------------------------------------- 1 | # Find the current version of the @conform-to/dom package 2 | VERSION=$(node -p "require('./packages/conform-dom/package.json').version") 3 | 4 | # Create a tmp file with the version replaced 5 | sed "s/^Version [0-9]*\.[0-9]*\.[0-9]*/Version ${VERSION}/" ./README.md > ./README.tmp 6 | 7 | # Replace the original file with the updated file 8 | mv ./README.tmp ./README.md 9 | 10 | -------------------------------------------------------------------------------- /.github/workflows/preview.yml: -------------------------------------------------------------------------------- 1 | name: Preview 2 | on: 3 | push: 4 | branches: [main] 5 | pull_request: 6 | branches: [main] 7 | 8 | jobs: 9 | release: 10 | name: Release 11 | runs-on: ubuntu-latest 12 | steps: 13 | - name: Checkout repo 14 | uses: actions/checkout@v4 15 | - name: Enable Corepack 16 | run: | 17 | npm i -g corepack@latest 18 | corepack enable 19 | - name: Setup node 20 | uses: actions/setup-node@v4 21 | with: 22 | node-version: 20 23 | cache: 'pnpm' 24 | - name: Install dependencies 25 | run: pnpm i 26 | - name: Publish to pkg.pr.new 27 | run: pnpx pkg-pr-new publish --compact './packages/*' --template './examples/*' 28 | 29 | validate: 30 | name: Validate Preview 31 | needs: [release] 32 | uses: edmundhung/conform/.github/workflows/validate.yml@main 33 | with: 34 | preview: ${{ github.event.pull_request.number || github.sha }} 35 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | on: 3 | push: 4 | branches: 5 | - main 6 | concurrency: ${{ github.workflow }}-${{ github.ref }} 7 | jobs: 8 | release: 9 | name: Release 10 | runs-on: ubuntu-latest 11 | steps: 12 | - name: Checkout repo 13 | uses: actions/checkout@v4 14 | - name: Enable Corepack 15 | run: | 16 | npm i -g corepack@latest 17 | corepack enable 18 | - name: Setup node 19 | uses: actions/setup-node@v4 20 | with: 21 | node-version: 20 22 | cache: 'pnpm' 23 | - name: Install dependencies 24 | run: pnpm i 25 | - name: Creating .npmrc 26 | run: | 27 | cat << EOF > "$HOME/.npmrc" 28 | //registry.npmjs.org/:_authToken=$NPM_TOKEN 29 | EOF 30 | env: 31 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }} 32 | - name: Create Release Pull Request or Publish to npm 33 | id: changesets 34 | uses: changesets/action@v1 35 | with: 36 | title: 'release: bump packages version' 37 | commit: 'release: bump packages version' 38 | version: pnpm run version 39 | publish: pnpm publish -r 40 | createGithubReleases: false 41 | env: 42 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 43 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }} 44 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | *.log 3 | logs 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | 8 | # Diagnostic reports (https://nodejs.org/api/report.html) 9 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 10 | 11 | # Runtime data 12 | *.pid 13 | *.pid.lock 14 | *.seed 15 | pids 16 | 17 | # Anything built 18 | build/ 19 | 20 | # tsc 21 | *.tsbuildinfo 22 | 23 | # Dependency directories 24 | node_modules/ 25 | 26 | # Optional npm cache directory 27 | .npm 28 | 29 | # Optional eslint cache 30 | .eslintcache 31 | 32 | # Output of 'npm pack' 33 | *.tgz 34 | /playwright-report/ 35 | /playwright/.cache/ 36 | /test-results/ 37 | 38 | # DS Store 39 | .DS_Store 40 | 41 | # Changeset generated files 42 | CHANGELOG.md 43 | -------------------------------------------------------------------------------- /.husky/.gitignore: -------------------------------------------------------------------------------- 1 | _ 2 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . "$(dirname "$0")/_/husky.sh" 3 | 4 | npx lint-staged 5 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | link-workspace-packages=true 2 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | *.log 3 | logs 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | 8 | # Diagnostic reports (https://nodejs.org/api/report.html) 9 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 10 | 11 | # Runtime data 12 | *.pid 13 | *.pid.lock 14 | *.seed 15 | pids 16 | 17 | # Anything built 18 | build/ 19 | 20 | # tsc 21 | *.tsbuildinfo 22 | 23 | # Dependency directories 24 | node_modules/ 25 | 26 | # Optional npm cache directory 27 | .npm 28 | 29 | # Optional eslint cache 30 | .eslintcache 31 | 32 | # Output of 'npm pack' 33 | *.tgz 34 | /playwright-report/ 35 | /playwright/.cache/ 36 | /test-results/ 37 | 38 | # DS Store 39 | .DS_Store 40 | 41 | # Project files 42 | README.md 43 | pnpm-lock.yaml 44 | 45 | # Build files 46 | .cache/ 47 | .wrangler/ 48 | !rollup.config.js 49 | /packages/**/*.js 50 | /packages/**/*.mjs 51 | /guide/functions/ 52 | -------------------------------------------------------------------------------- /.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "useTabs": true, 4 | "trailingComma": "all", 5 | "overrides": [ 6 | { 7 | "files": ["*.md"], 8 | "options": { 9 | "useTabs": false, 10 | "tabWidth": 2 11 | } 12 | } 13 | ] 14 | } 15 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Edmund Hung 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ``` 2 | ███████╗ ██████╗ ███╗ ██╗ ████████╗ ██████╗ ███████╗ ███╗ ███╗ 3 | ██╔═════╝ ██╔═══██╗ ████╗ ██║ ██╔═════╝ ██╔═══██╗ ██╔═══██╗ ████████║ 4 | ██║ ██║ ██║ ██╔██╗██║ ███████╗ ██║ ██║ ███████╔╝ ██╔██╔██║ 5 | ██║ ██║ ██║ ██║╚████║ ██╔════╝ ██║ ██║ ██╔═══██╗ ██║╚═╝██║ 6 | ╚███████╗ ╚██████╔╝ ██║ ╚███║ ██║ ╚██████╔╝ ██║ ██║ ██║ ██║ 7 | ╚══════╝ ╚═════╝ ╚═╝ ╚══╝ ╚═╝ ╚═════╝ ╚═╝ ╚═╝ ╚═╝ ╚═╝ 8 | ``` 9 | 10 | Version 1.6.1 / License MIT / Copyright (c) 2024 Edmund Hung 11 | 12 | A type-safe form validation library utilizing web fundamentals to progressively enhance HTML Forms with full support for server frameworks like Remix and Next.js. 13 | 14 | # Getting Started 15 | 16 | Check out the overview and tutorial at our website https://conform.guide 17 | 18 | # Features 19 | 20 | - Progressive enhancement first APIs 21 | - Type-safe field inference 22 | - Fine-grained subscription 23 | - Built-in accessibility helpers 24 | - Automatic type coercion with Zod 25 | 26 | # Documentation 27 | 28 | - Validation: https://conform.guide/validation 29 | - Nested object and Array: https://conform.guide/complex-structures 30 | - UI Integrations: https://conform.guide/integration/ui-libraries 31 | - Intent button: https://conform.guide/intent-button 32 | - Accessibility Guide: https://conform.guide/accessibility 33 | 34 | # Support 35 | 36 | To report a bug, please open an issue on the repository at https://github.com/edmundhung/conform. For feature requests and questions, you can post them in the Discussions section. 37 | -------------------------------------------------------------------------------- /docs/api/react/FormStateInput.md: -------------------------------------------------------------------------------- 1 | # FormStateInput 2 | 3 | A React component that renders a hidden input to persist the form state in case document reload. 4 | 5 | ```tsx 6 | import { FormProvider, FormStateInput, useForm } from '@conform-to/react'; 7 | 8 | export default function SomeParent() { 9 | const [form, fields] = useForm(); 10 | 11 | return ( 12 | 13 | 14 | 15 | ); 16 | } 17 | ``` 18 | 19 | ## Props 20 | 21 | This component does not accept any props. 22 | 23 | ## Tips 24 | 25 | ### You need this only if you are looking for full progressive enhancement 26 | 27 | Some of the form state will be lost if the document is reloaded. For example, Conform will only shows the error of the validated fields. But this information will be lost if you are submitting the form with a non-submit intent, such as inserting a new field to the list. By rendering the FormStateInput, Conform will be able to restore the form state and make sure the errors from all validated fields are still displaying. 28 | -------------------------------------------------------------------------------- /docs/api/react/getFieldsetProps.md: -------------------------------------------------------------------------------- 1 | # getFieldsetProps 2 | 3 | A helper that returns all the props required to make a fieldset element accessible. 4 | 5 | ```tsx 6 | const props = getFieldsetProps(meta, options); 7 | ``` 8 | 9 | ## Example 10 | 11 | ```tsx 12 | import { useForm, getFieldsetProps } from '@conform-to/react'; 13 | 14 | function Example() { 15 | const [form, fields] = useForm(); 16 | 17 | return
; 18 | } 19 | ``` 20 | 21 | ## Options 22 | 23 | ### `ariaAttributes` 24 | 25 | Decide whether to include `aria-invalid` and `aria-describedby` in the result props. Default to **true**. 26 | 27 | ### `ariaInvalid` 28 | 29 | Decide whether the aria attributes should be based on `meta.errors` or `meta.allErrors`. Default to **errors**. 30 | 31 | ### `ariaDescribedBy` 32 | 33 | Append additional **id** to the `aria-describedby` attribute. You can pass `meta.descriptionId` from the field metadata. 34 | 35 | ## Tips 36 | 37 | ### The helper is optional 38 | 39 | The helper is just a convenience function to help reducing boilerplate and make it more readable. You can always use the field metadata directly to set the props of your fieldset element. 40 | 41 | ```tsx 42 | // Before 43 | function Example() { 44 | return ( 45 |
52 | ); 53 | } 54 | 55 | // After 56 | function Example() { 57 | return
; 58 | } 59 | ``` 60 | 61 | ### Make your own helper 62 | 63 | The helper is designed for the native fieldset elements. If you need to use a custom component, you can always make your own helpers. 64 | -------------------------------------------------------------------------------- /docs/api/react/getFormProps.md: -------------------------------------------------------------------------------- 1 | # getFormProps 2 | 3 | A helper that returns all the props required to make a form element accessible. 4 | 5 | ```tsx 6 | const props = getFormProps(form, options); 7 | ``` 8 | 9 | ## Example 10 | 11 | ```tsx 12 | import { useForm, getFormProps } from '@conform-to/react'; 13 | 14 | function Example() { 15 | const [form, fields] = useForm(); 16 | 17 | return
; 18 | } 19 | ``` 20 | 21 | ## Options 22 | 23 | ### `ariaAttributes` 24 | 25 | Decide whether to include `aria-invalid` and `aria-describedby` in the result props. Default to **true**. 26 | 27 | ### `ariaInvalid` 28 | 29 | Decide whether the aria attributes should be based on `meta.errors` or `meta.allErrors`. Default to **errors**. 30 | 31 | ### `ariaDescribedBy` 32 | 33 | Append additional **id** to the `aria-describedby` attribute. You can pass `meta.descriptionId` from the field metadata. 34 | 35 | ## Tips 36 | 37 | ### The helper is optional 38 | 39 | The helper is just a convenience function to help reducing boilerplate and make it more readable. You can always use the form metadata directly to set the props of your form element. 40 | 41 | ```tsx 42 | // Before 43 | function Example() { 44 | return ( 45 | 52 | ); 53 | } 54 | 55 | // After 56 | function Example() { 57 | return ; 58 | } 59 | ``` 60 | 61 | ### Make your own helper 62 | 63 | The helper is designed for the native form elements. If you need to use a custom component, you can always make your own helpers. 64 | -------------------------------------------------------------------------------- /docs/api/valibot/getValibotConstraint.md: -------------------------------------------------------------------------------- 1 | # getValibotConstraint 2 | 3 | A helper that returns an object containing the validation attributes for each field by introspecting the valibot schema. 4 | 5 | ```tsx 6 | const constraint = getValibotConstraint(schema); 7 | ``` 8 | 9 | ## Parameters 10 | 11 | ### `schema` 12 | 13 | The valibot schema to be introspected. 14 | 15 | ## Example 16 | 17 | ```tsx 18 | import { getValibotConstraint } from '@conform-to/valibot'; 19 | import { useForm } from '@conform-to/react'; 20 | import { object, pipe, string, minLength, optional } from 'valibot'; 21 | 22 | const schema = object({ 23 | title: pipe(string(), minLength(5), maxLength(20)), 24 | description: optional(pipe(string(), minLength(100), maxLength(1000))), 25 | }); 26 | 27 | function Example() { 28 | const [form, fields] = useForm({ 29 | constraint: getValibotConstraint(schema), 30 | }); 31 | 32 | // ... 33 | } 34 | ``` 35 | -------------------------------------------------------------------------------- /docs/api/yup/getYupConstraint.md: -------------------------------------------------------------------------------- 1 | # getYupConstraint 2 | 3 | A helper that returns an object containing the validation attributes for each field by introspecting the yup schema. 4 | 5 | ```tsx 6 | const constraint = getYupConstraint(schema); 7 | ``` 8 | 9 | ## Parameters 10 | 11 | ### `schema` 12 | 13 | The yup schema to be introspected. 14 | 15 | ## Example 16 | 17 | ```tsx 18 | import { getYupConstraint } from '@conform-to/yup'; 19 | import { useForm } from '@conform-to/react'; 20 | import * as yup from 'yup'; 21 | 22 | const schema = yup.object({ 23 | title: yup.string().required().min(5).max(20), 24 | description: yup.string().optional().min(100).max(1000), 25 | }); 26 | 27 | function Example() { 28 | const [form, fields] = useForm({ 29 | constraint: getYupConstraint(schema), 30 | }); 31 | 32 | // ... 33 | } 34 | ``` 35 | -------------------------------------------------------------------------------- /docs/api/yup/parseWithYup.md: -------------------------------------------------------------------------------- 1 | # parseWithYup 2 | 3 | A helper that returns an overview of the submission by parsing the form data with the provided yup schema. 4 | 5 | ```tsx 6 | const submission = parseWithYup(payload, options); 7 | ``` 8 | 9 | ## Parameters 10 | 11 | ### `payload` 12 | 13 | It could be either the **FormData** or **URLSearchParams** object depending on how the form is submitted. 14 | 15 | ### `options` 16 | 17 | #### `schema` 18 | 19 | Either a yup schema or a function that returns a yup schema. 20 | 21 | #### `async` 22 | 23 | Set it to **true** if you want to parse the form data with **validate** method from the yup schema instead of **validateSync**. 24 | 25 | ## Example 26 | 27 | ```tsx 28 | import { parseWithYup } from '@conform-to/yup'; 29 | import { useForm } from '@conform-to/react'; 30 | import * as yup from 'yup'; 31 | 32 | const schema = yup.object({ 33 | email: yup.string().email(), 34 | password: yup.string(), 35 | }); 36 | 37 | function Example() { 38 | const [form, fields] = useForm({ 39 | onValidate({ formData }) { 40 | return parseWithYup(formData, { schema }); 41 | }, 42 | }); 43 | 44 | // ... 45 | } 46 | ``` 47 | -------------------------------------------------------------------------------- /docs/api/zod/getZodConstraint.md: -------------------------------------------------------------------------------- 1 | # getZodConstraint 2 | 3 | A helper that returns an object containing the validation attributes for each field by introspecting the zod schema. 4 | 5 | ```tsx 6 | const constraint = getZodConstraint(schema); 7 | ``` 8 | 9 | ## Parameters 10 | 11 | ### `schema` 12 | 13 | The zod schema to be introspected. 14 | 15 | ## Example 16 | 17 | ```tsx 18 | import { getZodConstraint } from '@conform-to/zod'; // Or, if you use zod/v4 or zod/v4-mini, import `@conform-to/zod/v4`. 19 | import { useForm } from '@conform-to/react'; 20 | import { z } from 'zod'; 21 | 22 | const schema = z.object({ 23 | title: z.string().min(5).max(20), 24 | description: z.string().min(100).max(1000).optional(), 25 | }); 26 | 27 | function Example() { 28 | const [form, fields] = useForm({ 29 | constraint: getZodConstraint(schema), 30 | }); 31 | 32 | // ... 33 | } 34 | ``` 35 | -------------------------------------------------------------------------------- /docs/ja/README.md: -------------------------------------------------------------------------------- 1 | # Documentation 2 | 3 | - はじめに 4 | - [概要](./overview.md) 5 | - [チュートリアル](./tutorial.md) 6 | - [v1 へのアップグレード](./upgrading-v1.md) 7 | - ガイド 8 | - [バリデーション](./validation.md) 9 | - [ネストされたオブジェクトと配列](./complex-structures.md) 10 | - [インテントボタン](./intent-button.md) 11 | - [チェックボックスとラジオグループ](./checkbox-and-radio-group.md) 12 | - [ファイルのアップロード](./file-upload.md) 13 | - [アクセシビリティ](./accessibility.md) 14 | - インテグレーション 15 | - [UI ライブラリ](./integration/ui-libraries.md) 16 | - [Remix](./integration/remix.md) 17 | - [Next.js](./integration/nextjs.md) 18 | - API リファレンス 19 | - @conform-to/react 20 | - [useForm](./api/react/useForm.md) 21 | - [useField](./api/react/useField.md) 22 | - [useFormMetadata](./api/react/useFormMetadata.md) 23 | - [useInputControl](./api/react/useInputControl.md) 24 | - [FormProvider](./api/react/FormProvider.md) 25 | - [FormStateInput](./api/react/FormStateInput.md) 26 | - [getFormProps](./api/react/getFormProps.md) 27 | - [getFieldsetProps](./api/react/getFieldsetProps.md) 28 | - [getInputProps](./api/react/getInputProps.md) 29 | - [getSelectProps](./api/react/getSelectProps.md) 30 | - [getTextareaProps](./api/react/getTextareaProps.md) 31 | - [getCollectionProps](./api/react/getCollectionProps.md) 32 | - @conform-to/yup 33 | - [parseWithYup](./api/yup/parseWithYup.md) 34 | - [getYupConstraint](./api/yup/getYupConstraint.md) 35 | - @conform-to/zod 36 | - [parseWithZod](./api/zod/parseWithZod.md) 37 | - [coerceFormValue](./api/zod/coerceFormValue.md) 38 | - [getZodConstraint](./api/zod/getZodConstraint.md) 39 | - [conformZodMessage](./api/zod/conformZodMessage.md) 40 | - @conform-to/valibot 41 | - [parseWithZod](./api/valibot/parseWithZod.md) 42 | - [coerceFormValue](./api/valibot/coerceFormValue.md) 43 | - [getZodConstraint](./api/valibot/getZodConstraint.md) 44 | - [conformZodMessage](./api/valibot/conformZodMessage.md) 45 | -------------------------------------------------------------------------------- /docs/ja/api/react/FormStateInput.md: -------------------------------------------------------------------------------- 1 | # FormStateInput 2 | 3 | ドキュメントの再読み込みが発生した場合にフォームの状態を維持するために、非表示の入力をレンダリングする React コンポーネントです。 4 | 5 | ```tsx 6 | import { FormProvider, FormStateInput, useForm } from '@conform-to/react'; 7 | 8 | export default function SomeParent() { 9 | const [form, fields] = useForm(); 10 | 11 | return ( 12 | 13 | 14 | 15 | ); 16 | } 17 | ``` 18 | 19 | ## プロパティ 20 | 21 | このコンポーネントはプロパティを受け入れません。 22 | 23 | ## Tips 24 | 25 | ### 完全なプログレッシブエンハンスメントを求めている場合にのみ、これが必要です。 26 | 27 | ドキュメントが再読み込みされると、フォームの状態の一部が失われます。例えば、 Conform は検証されたフィールドのエラーのみを表示しますが、新しいフィールドをリストに挿入するなど、サブミット以外の意図でフォームを送信している場合、この情報は失われます。 FormStateInput をレンダリングすることで、 Conform はフォームの状態を復元し、検証されたすべてのフィールドのエラーが引き続き表示されることを保証できます。 28 | -------------------------------------------------------------------------------- /docs/ja/api/react/getFieldsetProps.md: -------------------------------------------------------------------------------- 1 | # getFieldsetProps 2 | 3 | フィールドセット要素をアクセシブルにするために必要なすべてのプロパティを返すヘルパーです。 4 | 5 | ```tsx 6 | const props = getFieldsetProps(meta, options); 7 | ``` 8 | 9 | ## 例 10 | 11 | ```tsx 12 | import { useForm, getFieldsetProps } from '@conform-to/react'; 13 | 14 | function Example() { 15 | const [form, fields] = useForm(); 16 | 17 | return
; 18 | } 19 | ``` 20 | 21 | ## オプション 22 | 23 | ### `ariaAttributes` 24 | 25 | 結果のプロパティに `aria-invalid` と `aria-describedby` を含めるかどうかを決定します。デフォルトは **true** です。 26 | 27 | ### `ariaInvalid` 28 | 29 | ARIA 属性が `meta.errors` または `meta.allErrors` に基づくべきかどうかを決定します。デフォルトは **errors** です。 30 | 31 | ### `ariaDescribedBy` 32 | 33 | `aria-describedby` 属性に追加の **id** を付加します。フィールドメタデータから `meta.descriptionId` を渡すことができます。 34 | 35 | ## Tips 36 | 37 | ### ヘルパーは任意です 38 | 39 | このヘルパーは、定型文を減らし、読みやすくするための便利な機能です。入力要素のプロパティを設定するために、常にフィールドのメタデータを直接使用することもできます。 40 | 41 | ```tsx 42 | // Before 43 | function Example() { 44 | return ( 45 |
52 | ); 53 | } 54 | 55 | // After 56 | function Example() { 57 | return
; 58 | } 59 | ``` 60 | 61 | ### 自分のヘルパーを作る 62 | 63 | このヘルパーは、ネイティブの入力要素用に設計されています。カスタムコンポーネントを使用する必要がある場合は、自分自身のヘルパーを作成することができます。 64 | -------------------------------------------------------------------------------- /docs/ja/api/react/getFormProps.md: -------------------------------------------------------------------------------- 1 | # getFormProps 2 | 3 | フォーム要素をアクセシブルにするために必要なすべてのプロパティを返すヘルパーです。 4 | 5 | ```tsx 6 | const props = getFormProps(form, options); 7 | ``` 8 | 9 | ## 例 10 | 11 | ```tsx 12 | import { useForm, getFormProps } from '@conform-to/react'; 13 | 14 | function Example() { 15 | const [form, fields] = useForm(); 16 | 17 | return ; 18 | } 19 | ``` 20 | 21 | ## オプション 22 | 23 | ### `ariaAttributes` 24 | 25 | 結果のプロパティに `aria-invalid` と `aria-describedby` を含めるかどうかを決定します。デフォルトは **true** です。 26 | 27 | ### `ariaInvalid` 28 | 29 | aria 属性が `meta.errors` または `meta.allErrors` に基づくべきかを決定します。デフォルトは **errors** です。 30 | 31 | ### `ariaDescribedBy` 32 | 33 | 追加の **id** を `aria-describedby` 属性に追加します。フィールドのメタデータから `meta.descriptionId` を渡すことができます。 34 | 35 | ## Tips 36 | 37 | ### ヘルパーは任意です 38 | 39 | ヘルパーは、定型文を減らし、読みやすくするための便利な関数にすぎません。フォーム要素のプロパティを設定するには、いつでもフォームのメタデータを直接使うことができます。 40 | 41 | ```tsx 42 | // Before 43 | function Example() { 44 | return ( 45 | 52 | ); 53 | } 54 | 55 | // After 56 | function Example() { 57 | return ; 58 | } 59 | ``` 60 | 61 | ### 自分のヘルパーを作る 62 | 63 | ヘルパーはネイティブのフォーム要素のために設計されています。カスタムコンポーネントを使う必要がある場合、いつでも独自のヘルパーを作ることができます。 64 | -------------------------------------------------------------------------------- /docs/ja/api/react/useFormMetadata.md: -------------------------------------------------------------------------------- 1 | # useFormMetadata 2 | 3 | [FormProvider](./FormProvider.md) に設定されたコンテキストを登録することで、フォームのメタデータを返す React フックです。 4 | 5 | ```tsx 6 | const form = useFormMetadata(formId); 7 | ``` 8 | 9 | ## パラメータ 10 | 11 | ### `formId` 12 | 13 | フォーム要素に設定される id 属性です。 14 | 15 | ## 戻り値 16 | 17 | ### `form` 18 | 19 | フォームメタデータです。これは、 [useForm](./useForm.md) フックによって返されるオブジェクトと同じです。 20 | 21 | ## Tips 22 | 23 | ### `FormId` 型を用いたより良い型推論 24 | 25 | フォームメタデータの型推論を改善するために、 `string` の代わりに `FormId` 型を使用できます。 26 | 27 | ```tsx 28 | import { type FormId, useFormMetadata } from '@conform-to/react'; 29 | 30 | type ExampleComponentProps = { 31 | formId: FormId; 32 | }; 33 | 34 | function ExampleComponent({ formId }: ExampleComponentProps) { 35 | // これで `form.errors` と `form.getFieldset()` の結果の型を認識する。 36 | const form = useFormMetadata(formId); 37 | 38 | return
{/* ... */}
; 39 | } 40 | ``` 41 | 42 | コンポーネントをレンダリングする際には、 Conform によって提供されたフォーム ID を使用します。例えば、 `form.id` や `fields.fieldName.formId` は、既に `FormId` として型付けされています。これにより、 TypeScript は型が互換性があるかをチェックし、互換性がない場合に警告を出すことができます。 `string` を渡すこともできますが、型チェックの能力は失われます。 43 | 44 | ```tsx 45 | import { useForm } from '@conform-to/react'; 46 | 47 | function Example() { 48 | const [form, fields] = useForm(); 49 | 50 | return ( 51 | <> 52 | 53 | 54 | 55 | ); 56 | } 57 | ``` 58 | 59 | しかし、 `FormId` 型をより具体的にするほど、コンポーネントの再利用が難しくなります。コンポーネントが `Schema` や `FormError` のジェネリクスを使用しない場合は、それを `string` としてシンプルに保つこともできます。 60 | 61 | ```ts 62 | type ExampleComponentProps = { 63 | // スキーマやフォームエラーの型を気にしない場合 64 | formId: string; 65 | // フォームメタデータから特定のフィールドにアクセスしている場合 66 | formId: FormId<{ fieldName: string }>; 67 | // カスタムエラータイプを持っている場合 68 | formId: FormId, CustomFormError>; 69 | }; 70 | ``` 71 | -------------------------------------------------------------------------------- /docs/ja/api/valibot/getValibotConstraint.md: -------------------------------------------------------------------------------- 1 | # getValibotConstraint 2 | 3 | Valibot スキーマを内省することにより、各フィールドの検証属性を含むオブジェクトを返すヘルパーです。 4 | 5 | ```tsx 6 | const constraint = getValibotConstraint(schema); 7 | ``` 8 | 9 | ## Parameters 10 | 11 | ### `schema` 12 | 13 | イントロスペクトされる valibot スキーマ。 14 | 15 | ## Example 16 | 17 | ```tsx 18 | import { getValibotConstraint } from '@conform-to/valibot'; 19 | import { useForm } from '@conform-to/react'; 20 | import { object, pipe, string, minLength, optional } from 'valibot'; 21 | 22 | const schema = object({ 23 | title: pipe(string(), minLength(5), maxLength(20)), 24 | description: optional(pipe(string(), minLength(100), maxLength(1000))), 25 | }); 26 | 27 | function Example() { 28 | const [form, fields] = useForm({ 29 | constraint: getValibotConstraint(schema), 30 | }); 31 | 32 | // ... 33 | } 34 | ``` 35 | -------------------------------------------------------------------------------- /docs/ja/api/yup/getYupConstraint.md: -------------------------------------------------------------------------------- 1 | # getYupConstraint 2 | 3 | Yup スキーマをイントロスペクトすることで、各フィールドの検証属性を含むオブジェクトを返すヘルパーです。 4 | 5 | ```tsx 6 | const constraint = getYupConstraint(schema); 7 | ``` 8 | 9 | ## パラメータ 10 | 11 | ### `schema` 12 | 13 | イントロスペクトされるべき Yup スキーマです。 14 | 15 | ## 例 16 | 17 | ```tsx 18 | import { getYupConstraint } from '@conform-to/yup'; 19 | import { useForm } from '@conform-to/react'; 20 | import * as yup from 'yup'; 21 | 22 | const schema = yup.object({ 23 | title: yup.string().required().min(5).max(20), 24 | description: yup.string().optional().min(100).max(1000), 25 | }); 26 | 27 | function Example() { 28 | const [form, fields] = useForm({ 29 | constraint: getYupConstraint(schema), 30 | }); 31 | 32 | // ... 33 | } 34 | ``` 35 | -------------------------------------------------------------------------------- /docs/ja/api/yup/parseWithYup.md: -------------------------------------------------------------------------------- 1 | # parseWithYup 2 | 3 | 指定された yup スキーマを使用してフォームデータを解析し、送信内容の概要を返すヘルパーです。 4 | 5 | ```tsx 6 | const submission = parseWithYup(payload, options); 7 | ``` 8 | 9 | ## パラメータ 10 | 11 | ### `payload` 12 | 13 | フォームの送信方法に応じて、 **FormData** オブジェクトまたは **URLSearchParams** オブジェクトのいずれかになります。 14 | 15 | ### `options` 16 | 17 | #### `schema` 18 | 19 | Yup スキーマ、または Yup スキーマを返す関数のいずれかです。 20 | 21 | #### `async` 22 | 23 | **validateSync** の代わりに yup スキーマから **validate** メソッドを使用してフォームデータを解析したい場合は、 **true** に設定してください。 24 | 25 | ## 例 26 | 27 | ```tsx 28 | import { parseWithYup } from '@conform-to/yup'; 29 | import { useForm } from '@conform-to/react'; 30 | import * as yup from 'yup'; 31 | 32 | const schema = yup.object({ 33 | email: yup.string().email(), 34 | password: yup.string(), 35 | }); 36 | 37 | function Example() { 38 | const [form, fields] = useForm({ 39 | onValidate({ formData }) { 40 | return parseWithYup(formData, { schema }); 41 | }, 42 | }); 43 | 44 | // ... 45 | } 46 | ``` 47 | -------------------------------------------------------------------------------- /docs/ja/api/zod/getZodConstraint.md: -------------------------------------------------------------------------------- 1 | # getZodConstraint 2 | 3 | Zod スキーマを内省することにより、各フィールドの検証属性を含むオブジェクトを返すヘルパーです。 4 | 5 | ```tsx 6 | const constraint = getZodConstraint(schema); 7 | ``` 8 | 9 | ## パラメータ 10 | 11 | ### `schema` 12 | 13 | イントロスペクトされる zod スキーマ。 14 | 15 | ## 例 16 | 17 | ```tsx 18 | import { getZodConstraint } from '@conform-to/zod'; // もしくは、zod/v4かzod/v4-miniを使用する場合は `@conform-to/zod/v4` をインポートします。 19 | import { useForm } from '@conform-to/react'; 20 | import { z } from 'zod'; 21 | 22 | const schema = z.object({ 23 | title: z.string().min(5).max(20), 24 | description: z.string().min(100).max(1000).optional(), 25 | }); 26 | 27 | function Example() { 28 | const [form, fields] = useForm({ 29 | constraint: getZodConstraint(schema), 30 | }); 31 | 32 | // ... 33 | } 34 | ``` 35 | -------------------------------------------------------------------------------- /examples/chakra-ui/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # production 12 | /build 13 | 14 | # misc 15 | .DS_Store 16 | .env.local 17 | .env.development.local 18 | .env.test.local 19 | .env.production.local 20 | 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | -------------------------------------------------------------------------------- /examples/chakra-ui/README.md: -------------------------------------------------------------------------------- 1 | # Chakra UI Integration 2 | 3 | This example shows you how to integrate [chakra-ui](https://chakra-ui.com/docs/components) forms components with Conform. 4 | 5 | ## Compatibility 6 | 7 | > Based on @chakra-ui/react@2.4.2 8 | 9 | **Native support** 10 | 11 | - Input 12 | - Select 13 | - Textarea 14 | - Checkbox 15 | - Switch 16 | - Radio 17 | - Editable 18 | 19 | **Integration required** 20 | 21 | - NumberInput 22 | - PinInput 23 | - Slider 24 | 25 | 26 | 27 | Try it out on [Codesandbox](https://codesandbox.io/s/github/edmundhung/conform/tree/main/examples/chakra-ui). 28 | 29 | 30 | -------------------------------------------------------------------------------- /examples/chakra-ui/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@conform-example/chakra-ui", 3 | "private": true, 4 | "dependencies": { 5 | "@chakra-ui/react": "^2.4.2", 6 | "@conform-to/react": "1.6.1", 7 | "@emotion/react": "^11.10.5", 8 | "@emotion/styled": "^11.10.5", 9 | "framer-motion": "^7.6.19", 10 | "react": "^18.2.0", 11 | "react-dom": "^18.2.0", 12 | "react-scripts": "5.0.1" 13 | }, 14 | "devDependencies": { 15 | "@types/node": "^16.18.14", 16 | "@types/react": "^18.0.28", 17 | "@types/react-dom": "^18.0.11", 18 | "typescript": "^4.9.5" 19 | }, 20 | "scripts": { 21 | "start": "react-scripts start", 22 | "build": "react-scripts build", 23 | "test": "react-scripts test", 24 | "eject": "react-scripts eject" 25 | }, 26 | "eslintConfig": { 27 | "extends": [ 28 | "react-app" 29 | ] 30 | }, 31 | "browserslist": { 32 | "production": [ 33 | ">0.2%", 34 | "not dead", 35 | "not op_mini all" 36 | ], 37 | "development": [ 38 | "last 1 chrome version", 39 | "last 1 firefox version", 40 | "last 1 safari version" 41 | ] 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /examples/chakra-ui/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 16 | Chakra UI - Conform Example 17 | 18 | 19 | 20 |
21 | 31 | 32 | 33 | -------------------------------------------------------------------------------- /examples/chakra-ui/src/index.css: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/edmundhung/conform/2c07a7f91dd56cc75ddb213b8ea321fea6fee142/examples/chakra-ui/src/index.css -------------------------------------------------------------------------------- /examples/chakra-ui/src/index.tsx: -------------------------------------------------------------------------------- 1 | import { ChakraProvider } from '@chakra-ui/react'; 2 | import ReactDOM from 'react-dom/client'; 3 | import App from './App'; 4 | import './index.css'; 5 | 6 | const root = ReactDOM.createRoot( 7 | document.getElementById('root') as HTMLElement, 8 | ); 9 | 10 | root.render( 11 | 12 | 13 | , 14 | ); 15 | -------------------------------------------------------------------------------- /examples/chakra-ui/src/react-app-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /examples/chakra-ui/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": ["dom", "dom.iterable", "esnext"], 5 | "allowJs": true, 6 | "skipLibCheck": true, 7 | "esModuleInterop": true, 8 | "allowSyntheticDefaultImports": true, 9 | "strict": true, 10 | "forceConsistentCasingInFileNames": true, 11 | "noFallthroughCasesInSwitch": true, 12 | "module": "esnext", 13 | "moduleResolution": "node", 14 | "resolveJsonModule": true, 15 | "isolatedModules": true, 16 | "noEmit": true, 17 | "jsx": "react-jsx" 18 | }, 19 | "include": ["src"] 20 | } 21 | -------------------------------------------------------------------------------- /examples/headless-ui/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # production 12 | /build 13 | 14 | # misc 15 | .DS_Store 16 | .env.local 17 | .env.development.local 18 | .env.test.local 19 | .env.production.local 20 | 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | -------------------------------------------------------------------------------- /examples/headless-ui/README.md: -------------------------------------------------------------------------------- 1 | # Headless UI Integration 2 | 3 | [Headless UI](https://headlessui.com) is a set of completely unstyled, fully accessible UI components for React, designed to integrate beautifully with Tailwind CSS. In this guide, we will show how to integrate its input components with Conform. 4 | 5 | ## Compatibility 6 | 7 | > Based on @headless-ui/react@1.7.4 8 | 9 | **Integration required** 10 | 11 | - ListBox 12 | - Combobox 13 | - Switch 14 | - RadioGroup 15 | 16 | ## Demo 17 | 18 | 19 | 20 | Try it out on [Codesandbox](https://codesandbox.io/s/github/edmundhung/conform/tree/main/examples/headless-ui?file=/src/App.tsx). 21 | 22 | 23 | -------------------------------------------------------------------------------- /examples/headless-ui/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@conform-example/headless-ui", 3 | "private": true, 4 | "dependencies": { 5 | "@conform-to/react": "1.6.1", 6 | "@headlessui/react": "^1.7.13", 7 | "@heroicons/react": "^2.0.18", 8 | "react": "^18.2.0", 9 | "react-dom": "^18.2.0", 10 | "react-scripts": "5.0.1" 11 | }, 12 | "devDependencies": { 13 | "@headlessui/tailwindcss": "^0.1.2", 14 | "@tailwindcss/forms": "^0.5.3", 15 | "@types/node": "^16.18.14", 16 | "@types/react": "^18.0.28", 17 | "@types/react-dom": "^18.0.11", 18 | "autoprefixer": "^10.4.13", 19 | "postcss": "^8.4.21", 20 | "tailwindcss": "^3.2.7", 21 | "typescript": "^4.9.5" 22 | }, 23 | "scripts": { 24 | "start": "react-scripts start", 25 | "build": "react-scripts build", 26 | "test": "react-scripts test", 27 | "eject": "react-scripts eject" 28 | }, 29 | "eslintConfig": { 30 | "extends": [ 31 | "react-app" 32 | ] 33 | }, 34 | "browserslist": { 35 | "production": [ 36 | ">0.2%", 37 | "not dead", 38 | "not op_mini all" 39 | ], 40 | "development": [ 41 | "last 1 chrome version", 42 | "last 1 firefox version", 43 | "last 1 safari version" 44 | ] 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /examples/headless-ui/postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | }; 7 | -------------------------------------------------------------------------------- /examples/headless-ui/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 16 | Headless UI - Conform Example 17 | 18 | 19 | 20 |
21 | 31 | 32 | 33 | -------------------------------------------------------------------------------- /examples/headless-ui/sandbox.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "template": "node", 3 | "container": { 4 | "port": 3000, 5 | "node": "16" 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /examples/headless-ui/src/index.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | -------------------------------------------------------------------------------- /examples/headless-ui/src/index.tsx: -------------------------------------------------------------------------------- 1 | import ReactDOM from 'react-dom/client'; 2 | import App from './App'; 3 | import './index.css'; 4 | 5 | const root = ReactDOM.createRoot( 6 | document.getElementById('root') as HTMLElement, 7 | ); 8 | 9 | root.render(); 10 | -------------------------------------------------------------------------------- /examples/headless-ui/src/react-app-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /examples/headless-ui/tailwind.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('tailwindcss').Config} */ 2 | module.exports = { 3 | content: ['./src/**/*.{js,jsx,ts,tsx}'], 4 | theme: { 5 | extend: {}, 6 | }, 7 | plugins: [require('@tailwindcss/forms'), require('@headlessui/tailwindcss')], 8 | }; 9 | -------------------------------------------------------------------------------- /examples/headless-ui/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": ["dom", "dom.iterable", "esnext"], 5 | "allowJs": true, 6 | "skipLibCheck": true, 7 | "esModuleInterop": true, 8 | "allowSyntheticDefaultImports": true, 9 | "strict": true, 10 | "forceConsistentCasingInFileNames": true, 11 | "noFallthroughCasesInSwitch": true, 12 | "module": "esnext", 13 | "moduleResolution": "node", 14 | "resolveJsonModule": true, 15 | "isolatedModules": true, 16 | "noEmit": true, 17 | "jsx": "react-jsx" 18 | }, 19 | "include": ["src"] 20 | } 21 | -------------------------------------------------------------------------------- /examples/material-ui/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # production 12 | /build 13 | 14 | # misc 15 | .DS_Store 16 | .env.local 17 | .env.development.local 18 | .env.test.local 19 | .env.production.local 20 | 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | -------------------------------------------------------------------------------- /examples/material-ui/README.md: -------------------------------------------------------------------------------- 1 | # Material UI Integration 2 | 3 | [Material UI](https://mui.com/material-ui) is a comprehensive library of components based on Google's Material Design system. In this guide, we will show how to integrate its **Inputs** components with Conform. 4 | 5 | ## Compatibility 6 | 7 | > Based on @mui/material@5.10 8 | 9 | **Native support** 10 | 11 | - Text Field (default) 12 | - Text Field (multiline) 13 | - NativeSelect 14 | - Checkbox 15 | - Radio Group 16 | - Switch 17 | 18 | **Integration required** 19 | 20 | - Autocomplete 21 | - Select 22 | - Rating 23 | - Slider 24 | 25 | ## Demo 26 | 27 | 28 | 29 | Try it out on [Codesandbox](https://codesandbox.io/s/github/edmundhung/conform/tree/main/examples/material-ui). 30 | 31 | 32 | -------------------------------------------------------------------------------- /examples/material-ui/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@conform-example/material-ui", 3 | "private": true, 4 | "dependencies": { 5 | "@conform-to/react": "1.6.1", 6 | "@emotion/react": "^11.10.0", 7 | "@emotion/styled": "^11.10.0", 8 | "@mui/material": "^5.10.2", 9 | "react": "^18.2.0", 10 | "react-dom": "^18.2.0", 11 | "react-scripts": "5.0.1" 12 | }, 13 | "devDependencies": { 14 | "@types/node": "^16.18.14", 15 | "@types/react": "^18.0.28", 16 | "@types/react-dom": "^18.0.11", 17 | "typescript": "^4.9.5" 18 | }, 19 | "scripts": { 20 | "start": "react-scripts start", 21 | "build": "react-scripts build", 22 | "test": "react-scripts test", 23 | "eject": "react-scripts eject" 24 | }, 25 | "eslintConfig": { 26 | "extends": [ 27 | "react-app" 28 | ] 29 | }, 30 | "browserslist": { 31 | "production": [ 32 | ">0.2%", 33 | "not dead", 34 | "not op_mini all" 35 | ], 36 | "development": [ 37 | "last 1 chrome version", 38 | "last 1 firefox version", 39 | "last 1 safari version" 40 | ] 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /examples/material-ui/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 16 | Material UI - Conform Example 17 | 18 | 19 | 20 |
21 | 31 | 32 | 33 | -------------------------------------------------------------------------------- /examples/material-ui/src/index.css: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/edmundhung/conform/2c07a7f91dd56cc75ddb213b8ea321fea6fee142/examples/material-ui/src/index.css -------------------------------------------------------------------------------- /examples/material-ui/src/index.tsx: -------------------------------------------------------------------------------- 1 | import ReactDOM from 'react-dom/client'; 2 | import App from './App'; 3 | import './index.css'; 4 | 5 | const root = ReactDOM.createRoot( 6 | document.getElementById('root') as HTMLElement, 7 | ); 8 | 9 | root.render(); 10 | -------------------------------------------------------------------------------- /examples/material-ui/src/react-app-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /examples/material-ui/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": ["dom", "dom.iterable", "esnext"], 5 | "allowJs": true, 6 | "skipLibCheck": true, 7 | "esModuleInterop": true, 8 | "allowSyntheticDefaultImports": true, 9 | "strict": true, 10 | "forceConsistentCasingInFileNames": true, 11 | "noFallthroughCasesInSwitch": true, 12 | "module": "esnext", 13 | "moduleResolution": "node", 14 | "resolveJsonModule": true, 15 | "isolatedModules": true, 16 | "noEmit": true, 17 | "jsx": "react-jsx" 18 | }, 19 | "include": ["src"] 20 | } 21 | -------------------------------------------------------------------------------- /examples/nextjs/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "next/core-web-vitals" 3 | } 4 | -------------------------------------------------------------------------------- /examples/nextjs/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | .yarn/install-state.gz 8 | 9 | # testing 10 | /coverage 11 | 12 | # next.js 13 | /.next/ 14 | /out/ 15 | 16 | # production 17 | /build 18 | 19 | # misc 20 | .DS_Store 21 | *.pem 22 | 23 | # debug 24 | npm-debug.log* 25 | yarn-debug.log* 26 | yarn-error.log* 27 | 28 | # local env files 29 | .env*.local 30 | 31 | # vercel 32 | .vercel 33 | 34 | # typescript 35 | *.tsbuildinfo 36 | next-env.d.ts 37 | -------------------------------------------------------------------------------- /examples/nextjs/README.md: -------------------------------------------------------------------------------- 1 | # NextJS Example 2 | 3 | > Please fork the sandbox if it is stuck in the _Initializing Sandbox Container_ stage 4 | 5 | This example demonstrates some of the features of Conform including **client validation**, **nested list**, and **async validation with zod** using NextJS. 6 | 7 | 8 | 9 | Try it out on [Codesandbox](https://codesandbox.io/s/github/edmundhung/conform/tree/main/examples/nextjs). 10 | 11 | 12 | -------------------------------------------------------------------------------- /examples/nextjs/app/actions.ts: -------------------------------------------------------------------------------- 1 | 'use server'; 2 | 3 | import { redirect } from 'next/navigation'; 4 | import { parseWithZod } from '@conform-to/zod'; 5 | import { todosSchema, loginSchema, createSignupSchema } from '@/app/schema'; 6 | 7 | export async function login(prevState: unknown, formData: FormData) { 8 | const submission = parseWithZod(formData, { 9 | schema: loginSchema, 10 | }); 11 | 12 | if (submission.status !== 'success') { 13 | return submission.reply(); 14 | } 15 | 16 | redirect(`/?value=${JSON.stringify(submission.value)}`); 17 | } 18 | 19 | export async function createTodos(prevState: unknown, formData: FormData) { 20 | const submission = parseWithZod(formData, { 21 | schema: todosSchema, 22 | }); 23 | 24 | if (submission.status !== 'success') { 25 | return submission.reply(); 26 | } 27 | 28 | redirect(`/?value=${JSON.stringify(submission.value)}`); 29 | } 30 | 31 | export async function signup(prevState: unknown, formData: FormData) { 32 | const submission = await parseWithZod(formData, { 33 | schema: (control) => 34 | // create a zod schema base on the control 35 | createSignupSchema(control, { 36 | isUsernameUnique(username) { 37 | return new Promise((resolve) => { 38 | setTimeout(() => { 39 | resolve(username !== 'admin'); 40 | }, Math.random() * 300); 41 | }); 42 | }, 43 | }), 44 | async: true, 45 | }); 46 | 47 | if (submission.status !== 'success') { 48 | return submission.reply(); 49 | } 50 | 51 | redirect(`/?value=${JSON.stringify(submission.value)}`); 52 | } 53 | -------------------------------------------------------------------------------- /examples/nextjs/app/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/edmundhung/conform/2c07a7f91dd56cc75ddb213b8ea321fea6fee142/examples/nextjs/app/favicon.ico -------------------------------------------------------------------------------- /examples/nextjs/app/globals.css: -------------------------------------------------------------------------------- 1 | main { 2 | max-width: 600px; 3 | margin: 0 auto; 4 | } 5 | 6 | hr { 7 | margin: 30px 0; 8 | color: gainsboro; 9 | } 10 | 11 | label { 12 | display: block; 13 | } 14 | 15 | .form-error { 16 | color: red; 17 | } 18 | 19 | input.error { 20 | outline: red; 21 | color: red; 22 | border: 1px solid red; 23 | } 24 | 25 | input.error + div { 26 | color: red; 27 | } 28 | -------------------------------------------------------------------------------- /examples/nextjs/app/layout.tsx: -------------------------------------------------------------------------------- 1 | import type { Metadata } from 'next'; 2 | import Link from 'next/link'; 3 | 4 | // These styles apply to every route in the application 5 | import './globals.css'; 6 | 7 | export const metadata: Metadata = { 8 | title: 'NextJS - Conform Example', 9 | }; 10 | 11 | export default function RootLayout({ 12 | children, 13 | }: { 14 | children: React.ReactNode; 15 | }) { 16 | return ( 17 | 18 | 19 |
20 |

NextJS Example

21 | 22 |

23 | This example demonstrates some of the features of Conform including{' '} 24 | client validation, nested list, 25 | and async validation with zod. 26 |

27 | 28 |
    29 |
  • 30 | Login 31 |
  • 32 |
  • 33 | Todo list 34 |
  • 35 |
  • 36 | Signup 37 |
  • 38 |
39 | 40 |
41 | 42 | {children} 43 |
44 | 45 | 46 | ); 47 | } 48 | -------------------------------------------------------------------------------- /examples/nextjs/app/login/page.tsx: -------------------------------------------------------------------------------- 1 | import { LoginForm } from '@/app/form'; 2 | 3 | export default function Login() { 4 | return ; 5 | } 6 | -------------------------------------------------------------------------------- /examples/nextjs/app/page.tsx: -------------------------------------------------------------------------------- 1 | export default function Index({ 2 | searchParams, 3 | }: { 4 | searchParams: { [key: string]: string | string[] | undefined }; 5 | }) { 6 | const value = searchParams['value']; 7 | 8 | if (!value) { 9 | return null; 10 | } 11 | 12 | return ( 13 |
14 | Submitted the following value: 15 |
{JSON.stringify(JSON.parse(value.toString()), null, 2)}
16 |
17 | ); 18 | } 19 | -------------------------------------------------------------------------------- /examples/nextjs/app/signup/page.tsx: -------------------------------------------------------------------------------- 1 | import { SignupForm } from '@/app/form'; 2 | 3 | export default function Signup() { 4 | return ; 5 | } 6 | -------------------------------------------------------------------------------- /examples/nextjs/app/todos/page.tsx: -------------------------------------------------------------------------------- 1 | import { TodoForm } from '@/app/form'; 2 | 3 | export default function Todos() { 4 | return ; 5 | } 6 | -------------------------------------------------------------------------------- /examples/nextjs/next.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('next').NextConfig} */ 2 | const nextConfig = {}; 3 | 4 | module.exports = nextConfig; 5 | -------------------------------------------------------------------------------- /examples/nextjs/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@conform-example/nextjs", 3 | "private": true, 4 | "license": "MIT", 5 | "scripts": { 6 | "dev": "next dev", 7 | "build": "next build", 8 | "start": "next start", 9 | "lint": "next lint" 10 | }, 11 | "dependencies": { 12 | "@conform-to/react": "1.6.1", 13 | "@conform-to/zod": "1.6.1", 14 | "next": "14.0.4", 15 | "react": "^18.2.0", 16 | "react-dom": "^18.2.0", 17 | "zod": "^3.25.30" 18 | }, 19 | "devDependencies": { 20 | "@types/node": "^20", 21 | "@types/react": "^18", 22 | "@types/react-dom": "^18", 23 | "eslint": "^8", 24 | "eslint-config-next": "14.0.4", 25 | "typescript": "^5" 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /examples/nextjs/public/next.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /examples/nextjs/public/vercel.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /examples/nextjs/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": ["dom", "dom.iterable", "esnext"], 5 | "allowJs": true, 6 | "skipLibCheck": true, 7 | "strict": true, 8 | "noEmit": true, 9 | "esModuleInterop": true, 10 | "module": "esnext", 11 | "moduleResolution": "bundler", 12 | "resolveJsonModule": true, 13 | "isolatedModules": true, 14 | "jsx": "preserve", 15 | "incremental": true, 16 | "plugins": [ 17 | { 18 | "name": "next" 19 | } 20 | ], 21 | "paths": { 22 | "@/*": ["./*"] 23 | } 24 | }, 25 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], 26 | "exclude": ["node_modules"] 27 | } 28 | -------------------------------------------------------------------------------- /examples/radix-ui/.eslintrc.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | env: { browser: true, es2020: true }, 4 | extends: [ 5 | 'eslint:recommended', 6 | 'plugin:@typescript-eslint/recommended', 7 | 'plugin:react-hooks/recommended', 8 | ], 9 | ignorePatterns: ['dist', '.eslintrc.cjs'], 10 | parser: '@typescript-eslint/parser', 11 | }; 12 | -------------------------------------------------------------------------------- /examples/radix-ui/.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | dist 12 | dist-ssr 13 | *.local 14 | 15 | # Editor directories and files 16 | .vscode/* 17 | !.vscode/extensions.json 18 | .idea 19 | .DS_Store 20 | *.suo 21 | *.ntvs* 22 | *.njsproj 23 | *.sln 24 | *.sw? 25 | -------------------------------------------------------------------------------- /examples/radix-ui/README.md: -------------------------------------------------------------------------------- 1 | # Radix UI Integration 2 | 3 | [Radix UI](https://www.radix-ui.com/) Radix UI is a headless UI library, offering flexible, unstyled primitives for creating customizable and accessible components, allowing developers to manage the visual layer independently. 4 | This example we leverage [Vite](https://vitejs.dev/) and [Tailwind CSS](https://tailwindcss.com/) 5 | 6 | ## Required packages 7 | 8 | - @radix-ui/react-checkbox 9 | - @radix-ui/react-icons 10 | - @radix-ui/react-radio-group 11 | - @radix-ui/react-select 12 | - @radix-ui/react-slider 13 | - @radix-ui/react-switch 14 | - @radix-ui/react-toggle-group 15 | 16 | **Integration required** 17 | 18 | - Checkbox 19 | - Radio group 20 | - Select 21 | - Slider 22 | - Switch 23 | - Toggle group 24 | 25 | ## Demo 26 | 27 | 28 | 29 | Try it out on [Codesandbox](https://codesandbox.io/s/github/edmundhung/conform/tree/main/examples/radix-ui). 30 | 31 | 32 | -------------------------------------------------------------------------------- /examples/radix-ui/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Conform 💖 Radix-ui 8 | 9 | 10 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /examples/radix-ui/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@conform-example/radix-ui", 3 | "private": true, 4 | "version": "0.0.0", 5 | "type": "module", 6 | "scripts": { 7 | "dev": "vite", 8 | "build": "tsc && vite build", 9 | "lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0", 10 | "preview": "vite preview" 11 | }, 12 | "dependencies": { 13 | "@conform-to/react": "1.6.1", 14 | "@conform-to/zod": "1.6.1", 15 | "@radix-ui/react-checkbox": "^1.0.4", 16 | "@radix-ui/react-icons": "^1.3.0", 17 | "@radix-ui/react-radio-group": "^1.1.3", 18 | "@radix-ui/react-select": "^2.0.0", 19 | "@radix-ui/react-slider": "^1.1.2", 20 | "@radix-ui/react-switch": "^1.0.3", 21 | "@radix-ui/react-toggle-group": "^1.0.4", 22 | "clsx": "^2.1.0", 23 | "react": "^18.2.0", 24 | "react-dom": "^18.2.0", 25 | "zod": "^3.25.30" 26 | }, 27 | "devDependencies": { 28 | "@types/react": "^18.2.43", 29 | "@types/react-dom": "^18.2.17", 30 | "@typescript-eslint/eslint-plugin": "^7.11.0", 31 | "@typescript-eslint/parser": "^7.11.0", 32 | "@vitejs/plugin-react-swc": "^3.6.0", 33 | "autoprefixer": "^10.4.17", 34 | "eslint": "^8.57.0", 35 | "eslint-plugin-react-hooks": "^4.6.2", 36 | "postcss": "^8.4.33", 37 | "tailwindcss": "^3.4.1", 38 | "typescript": "^5.2.2", 39 | "vite": "^5.0.8" 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /examples/radix-ui/postcss.config.js: -------------------------------------------------------------------------------- 1 | export default { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | }; 7 | -------------------------------------------------------------------------------- /examples/radix-ui/public/vite.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /examples/radix-ui/src/index.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | -------------------------------------------------------------------------------- /examples/radix-ui/src/main.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom/client'; 3 | import { App } from './App.tsx'; 4 | import './index.css'; 5 | 6 | ReactDOM.createRoot(document.getElementById('root')!).render( 7 | 8 | 9 | , 10 | ); 11 | -------------------------------------------------------------------------------- /examples/radix-ui/src/ui/Checkbox.tsx: -------------------------------------------------------------------------------- 1 | import { type FieldMetadata, useInputControl } from '@conform-to/react'; 2 | import * as RadixCheckbox from '@radix-ui/react-checkbox'; 3 | import { CheckIcon } from '@radix-ui/react-icons'; 4 | import { type ElementRef, useRef } from 'react'; 5 | 6 | export function Checkbox({ 7 | meta, 8 | }: { 9 | meta: FieldMetadata; 10 | }) { 11 | const checkboxRef = useRef>(null); 12 | const control = useInputControl(meta); 13 | 14 | return ( 15 | <> 16 | checkboxRef.current?.focus()} 22 | /> 23 | { 28 | control.change(checked ? 'on' : ''); 29 | }} 30 | onBlur={control.blur} 31 | className="hover:bg-amber-50 flex size-5 appearance-none items-center justify-center rounded-md bg-white outline-none border focus:ring-amber-500 focus:ring-2" 32 | > 33 | 34 | 35 | 36 | 37 | 38 | ); 39 | } 40 | -------------------------------------------------------------------------------- /examples/radix-ui/src/ui/RadioGroup.tsx: -------------------------------------------------------------------------------- 1 | import { type FieldMetadata, useInputControl } from '@conform-to/react'; 2 | import * as RadixRadioGroup from '@radix-ui/react-radio-group'; 3 | import clsx from 'clsx'; 4 | import { type ElementRef, useRef } from 'react'; 5 | 6 | export function RadioGroup({ 7 | meta, 8 | items, 9 | }: { 10 | meta: FieldMetadata; 11 | items: Array<{ value: string; label: string }>; 12 | }) { 13 | const radioGroupRef = useRef>(null); 14 | const control = useInputControl(meta); 15 | return ( 16 | <> 17 | { 23 | radioGroupRef.current?.focus(); 24 | }} 25 | /> 26 | 33 | {items.map((item) => { 34 | return ( 35 |
36 | 46 | 47 | 48 | 49 |
50 | ); 51 | })} 52 |
53 | 54 | ); 55 | } 56 | -------------------------------------------------------------------------------- /examples/radix-ui/src/ui/Slider.tsx: -------------------------------------------------------------------------------- 1 | import { FieldMetadata, useInputControl } from '@conform-to/react'; 2 | import * as RadixSlider from '@radix-ui/react-slider'; 3 | import { ElementRef, useRef } from 'react'; 4 | 5 | export function Slider({ 6 | meta, 7 | max = 100, 8 | }: { 9 | meta: FieldMetadata; 10 | ariaLabel?: string; 11 | max?: number; 12 | }) { 13 | const thumbRef = useRef>(null); 14 | const control = useInputControl(meta); 15 | 16 | return ( 17 |
18 | { 24 | thumbRef.current?.focus(); 25 | }} 26 | /> 27 | { 33 | control.change(value[0].toString()); 34 | }} 35 | onBlur={control.blur} 36 | step={1} 37 | > 38 | 39 | 40 | 41 | 45 | 46 |
{control.value}
47 |
48 | ); 49 | } 50 | -------------------------------------------------------------------------------- /examples/radix-ui/src/ui/Switch.tsx: -------------------------------------------------------------------------------- 1 | import { type FieldMetadata, useInputControl } from '@conform-to/react'; 2 | import * as RadixSwitch from '@radix-ui/react-switch'; 3 | import { type ElementRef, useRef } from 'react'; 4 | 5 | export function Switch({ meta }: { meta: FieldMetadata }) { 6 | const switchRef = useRef>(null); 7 | const control = useInputControl(meta); 8 | 9 | return ( 10 | <> 11 | { 17 | switchRef.current?.focus(); 18 | }} 19 | /> 20 | { 25 | control.change(checked ? 'on' : ''); 26 | }} 27 | onBlur={control.blur} 28 | className="w-[42px] h-[25px] bg-amber-700/30 rounded-full relative focus:ring-2 focus:ring-amber-500 data-[state=checked]:bg-amber-700 outline-none cursor-default" 29 | > 30 | 31 | 32 | 33 | ); 34 | } 35 | -------------------------------------------------------------------------------- /examples/radix-ui/src/ui/ToggleGroup.tsx: -------------------------------------------------------------------------------- 1 | import { type FieldMetadata, useInputControl } from '@conform-to/react'; 2 | import * as RadixToggleGroup from '@radix-ui/react-toggle-group'; 3 | import { type ElementRef, useRef } from 'react'; 4 | 5 | export function ToggleGroup({ 6 | meta, 7 | items, 8 | }: { 9 | meta: FieldMetadata; 10 | items: Array<{ label: string; value: string }>; 11 | }) { 12 | const toggleGroupRef = useRef>(null); 13 | const control = useInputControl(meta); 14 | 15 | return ( 16 | <> 17 | { 23 | toggleGroupRef.current?.focus(); 24 | }} 25 | /> 26 | 36 | {items.map((item) => ( 37 | 43 | {item.label} 44 | 45 | ))} 46 | 47 | 48 | ); 49 | } 50 | -------------------------------------------------------------------------------- /examples/radix-ui/src/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /examples/radix-ui/tailwind.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('tailwindcss').Config} */ 2 | export default { 3 | content: ['./index.html', './src/**/*.{js,ts,jsx,tsx}'], 4 | theme: { 5 | extend: {}, 6 | }, 7 | plugins: [], 8 | }; 9 | -------------------------------------------------------------------------------- /examples/radix-ui/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2020", 4 | "useDefineForClassFields": true, 5 | "lib": ["ES2020", "DOM", "DOM.Iterable"], 6 | "module": "ESNext", 7 | "skipLibCheck": true, 8 | 9 | /* Bundler mode */ 10 | "moduleResolution": "bundler", 11 | "allowImportingTsExtensions": true, 12 | "resolveJsonModule": true, 13 | "isolatedModules": true, 14 | "noEmit": true, 15 | "jsx": "react-jsx", 16 | 17 | /* Linting */ 18 | "strict": true, 19 | "noUnusedLocals": true, 20 | "noUnusedParameters": true, 21 | "noFallthroughCasesInSwitch": true 22 | }, 23 | "include": ["src"], 24 | "references": [{ "path": "./tsconfig.node.json" }] 25 | } 26 | -------------------------------------------------------------------------------- /examples/radix-ui/tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "composite": true, 4 | "skipLibCheck": true, 5 | "module": "ESNext", 6 | "moduleResolution": "bundler", 7 | "allowSyntheticDefaultImports": true 8 | }, 9 | "include": ["vite.config.ts"] 10 | } 11 | -------------------------------------------------------------------------------- /examples/radix-ui/vite.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite'; 2 | import react from '@vitejs/plugin-react-swc'; 3 | 4 | // https://vitejs.dev/config/ 5 | export default defineConfig({ 6 | plugins: [react()], 7 | }); 8 | -------------------------------------------------------------------------------- /examples/react-router/README.md: -------------------------------------------------------------------------------- 1 | # React Router Example 2 | 3 | > Please fork the sandbox if it is stuck in the _Initializing Sandbox Container_ stage 4 | 5 | This example demonstrates some of the features of Conform including **client validation**, **nested list**, and **async validation with zod** using React Router 6 | 7 | 8 | 9 | Try it out on [Codesandbox](https://codesandbox.io/s/github/edmundhung/conform/tree/main/examples/react-router?file=/app/routes/index.tsx). 10 | 11 | 12 | -------------------------------------------------------------------------------- /examples/react-router/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Conform - React Router Example 7 | 8 | 9 |
10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /examples/react-router/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@conform-example/react-router", 3 | "private": true, 4 | "scripts": { 5 | "dev": "vite", 6 | "build": "tsc && vite build", 7 | "serve": "vite preview" 8 | }, 9 | "dependencies": { 10 | "@conform-to/react": "1.6.1", 11 | "@conform-to/zod": "1.6.1", 12 | "react": "^18.2.0", 13 | "react-dom": "^18.2.0", 14 | "react-router-dom": "^6.15.0", 15 | "zod": "^3.25.30" 16 | }, 17 | "devDependencies": { 18 | "@rollup/plugin-replace": "^5.0.2", 19 | "@types/node": "18.x", 20 | "@types/react": "^18.0.27", 21 | "@types/react-dom": "^18.0.10", 22 | "@vitejs/plugin-react": "^3.0.1", 23 | "typescript": "^4.9.5", 24 | "vite": "^4.0.4" 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /examples/react-router/src/App.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | createBrowserRouter, 3 | createRoutesFromElements, 4 | Route, 5 | RouterProvider, 6 | Link, 7 | Outlet, 8 | } from 'react-router-dom'; 9 | 10 | const router = createBrowserRouter( 11 | createRoutesFromElements( 12 | 13 | import('./_index')} /> 14 | import('./login')} /> 15 | import('./login-fetcher')} /> 16 | import('./todos')} /> 17 | import('./signup')} /> 18 | , 19 | ), 20 | ); 21 | 22 | function Example() { 23 | return ( 24 |
25 |

React Router Example

26 | 27 |

28 | This example demonstrates some of the features of Conform including{' '} 29 | client validation, nested list, and{' '} 30 | async validation with zod. 31 |

32 | 33 |
    34 |
  • 35 | Login ( 36 | with useFetcher) 37 |
  • 38 |
  • 39 | Todo list 40 |
  • 41 |
  • 42 | Signup 43 |
  • 44 |
45 | 46 |
47 | 48 | 49 |
50 | ); 51 | } 52 | 53 | export default function App() { 54 | return ; 55 | } 56 | -------------------------------------------------------------------------------- /examples/react-router/src/_index.tsx: -------------------------------------------------------------------------------- 1 | import type { LoaderFunctionArgs } from 'react-router-dom'; 2 | import { useLoaderData, json } from 'react-router-dom'; 3 | 4 | export function loader({ request }: LoaderFunctionArgs) { 5 | const url = new URL(request.url); 6 | const value = url.searchParams.get('value'); 7 | 8 | return json({ 9 | value: value ? JSON.parse(value) : undefined, 10 | }); 11 | } 12 | 13 | export function Component() { 14 | const { value } = useLoaderData() as any; 15 | 16 | if (!value) { 17 | return null; 18 | } 19 | 20 | return ( 21 |
22 | Submitted the following value: 23 |
{JSON.stringify(value, null, 2)}
24 |
25 | ); 26 | } 27 | -------------------------------------------------------------------------------- /examples/react-router/src/index.css: -------------------------------------------------------------------------------- 1 | main { 2 | max-width: 600px; 3 | margin: 0 auto; 4 | } 5 | 6 | hr { 7 | margin: 30px 0; 8 | color: gainsboro; 9 | } 10 | 11 | label { 12 | display: block; 13 | } 14 | 15 | .form-error { 16 | color: red; 17 | } 18 | 19 | input.error { 20 | outline: red; 21 | color: red; 22 | border: 1px solid red; 23 | } 24 | 25 | input.error + div { 26 | color: red; 27 | } 28 | -------------------------------------------------------------------------------- /examples/react-router/src/login-fetcher.tsx: -------------------------------------------------------------------------------- 1 | import { getFormProps, getInputProps, useForm } from '@conform-to/react'; 2 | import { parseWithZod } from '@conform-to/zod'; 3 | import type { ActionFunctionArgs } from 'react-router-dom'; 4 | import { useFetcher, json, redirect } from 'react-router-dom'; 5 | import { z } from 'zod'; 6 | 7 | const schema = z.object({ 8 | email: z.string().email(), 9 | password: z.string(), 10 | remember: z.boolean().optional(), 11 | }); 12 | 13 | export async function action({ request }: ActionFunctionArgs) { 14 | const formData = await request.formData(); 15 | const submission = parseWithZod(formData, { schema }); 16 | 17 | if (submission.status !== 'success') { 18 | return json(submission.reply()); 19 | } 20 | 21 | return redirect(`/?value=${JSON.stringify(submission.value)}`); 22 | } 23 | 24 | export function Component() { 25 | const fetcher = useFetcher(); 26 | const [form, fields] = useForm({ 27 | lastResult: fetcher.data, 28 | onValidate({ formData }) { 29 | return parseWithZod(formData, { schema }); 30 | }, 31 | shouldRevalidate: 'onBlur', 32 | }); 33 | 34 | return ( 35 | 36 |
37 | 38 | 42 |
{fields.email.errors}
43 |
44 |
45 | 46 | 50 |
{fields.password.errors}
51 |
52 | 58 |
59 | 60 |
61 | ); 62 | } 63 | -------------------------------------------------------------------------------- /examples/react-router/src/login.tsx: -------------------------------------------------------------------------------- 1 | import { getFormProps, getInputProps, useForm } from '@conform-to/react'; 2 | import { parseWithZod } from '@conform-to/zod'; 3 | import type { ActionFunctionArgs } from 'react-router-dom'; 4 | import { Form, useActionData, json, redirect } from 'react-router-dom'; 5 | import { z } from 'zod'; 6 | 7 | const schema = z.object({ 8 | email: z.string().email(), 9 | password: z.string(), 10 | remember: z.boolean().optional(), 11 | }); 12 | 13 | export async function action({ request }: ActionFunctionArgs) { 14 | const formData = await request.formData(); 15 | const submission = parseWithZod(formData, { schema }); 16 | 17 | if (submission.status !== 'success') { 18 | return json(submission.reply()); 19 | } 20 | 21 | return redirect(`/?value=${JSON.stringify(submission.value)}`); 22 | } 23 | 24 | export function Component() { 25 | const lastResult = useActionData() as any; 26 | const [form, fields] = useForm({ 27 | lastResult, 28 | onValidate({ formData }) { 29 | return parseWithZod(formData, { schema }); 30 | }, 31 | shouldRevalidate: 'onBlur', 32 | }); 33 | 34 | return ( 35 | 36 |
37 | 38 | 42 |
{fields.email.errors}
43 |
44 |
45 | 46 | 50 |
{fields.password.errors}
51 |
52 | 58 |
59 | 60 | 61 | ); 62 | } 63 | -------------------------------------------------------------------------------- /examples/react-router/src/main.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom/client'; 3 | 4 | import './index.css'; 5 | import App from './App'; 6 | 7 | ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render( 8 | 9 | 10 | , 11 | ); 12 | -------------------------------------------------------------------------------- /examples/react-router/src/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /examples/react-router/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "baseUrl": ".", 4 | "target": "ESNext", 5 | "lib": ["DOM", "DOM.Iterable", "ESNext"], 6 | "allowJs": false, 7 | "skipLibCheck": true, 8 | "esModuleInterop": false, 9 | "allowSyntheticDefaultImports": true, 10 | "strict": true, 11 | "forceConsistentCasingInFileNames": true, 12 | "module": "ESNext", 13 | "moduleResolution": "Node", 14 | "resolveJsonModule": true, 15 | "isolatedModules": true, 16 | "noEmit": true, 17 | "jsx": "react-jsx" 18 | }, 19 | "include": ["./src"] 20 | } 21 | -------------------------------------------------------------------------------- /examples/react-router/vite.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite'; 2 | import react from '@vitejs/plugin-react'; 3 | import rollupReplace from '@rollup/plugin-replace'; 4 | 5 | export default defineConfig({ 6 | plugins: [ 7 | rollupReplace({ 8 | preventAssignment: true, 9 | values: { 10 | __DEV__: JSON.stringify(true), 11 | 'process.env.NODE_ENV': JSON.stringify('development'), 12 | }, 13 | }), 14 | react(), 15 | ], 16 | }); 17 | -------------------------------------------------------------------------------- /examples/remix/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | // "@remix-run/eslint-config", 4 | "@remix-run/eslint-config/node" 5 | ], 6 | "ignorePatterns": ["build/**", "public/build/**"] 7 | } 8 | -------------------------------------------------------------------------------- /examples/remix/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | 3 | /.cache 4 | /build 5 | /public/build 6 | .env 7 | -------------------------------------------------------------------------------- /examples/remix/.stackblitzrc: -------------------------------------------------------------------------------- 1 | { 2 | "installDependencies": true, 3 | "startCommand": "npm run dev", 4 | "env": { 5 | "ENABLE_CJS_IMPORTS": true 6 | } 7 | } -------------------------------------------------------------------------------- /examples/remix/README.md: -------------------------------------------------------------------------------- 1 | # Remix Example 2 | 3 | > Please fork the sandbox if it is stuck in the _Initializing Sandbox Container_ stage 4 | 5 | This example demonstrates some of the features of Conform including **client validation**, **nested list**, and **async validation with zod** using Remix. 6 | 7 | 8 | 9 | Try it out on [Codesandbox](https://codesandbox.io/s/github/edmundhung/conform/tree/main/examples/remix?file=/app/root.tsx). 10 | 11 | 12 | -------------------------------------------------------------------------------- /examples/remix/app/root.tsx: -------------------------------------------------------------------------------- 1 | import type { V2_MetaFunction, LinksFunction } from '@remix-run/node'; 2 | import { 3 | Link, 4 | Links, 5 | LiveReload, 6 | Meta, 7 | Outlet, 8 | Scripts, 9 | ScrollRestoration, 10 | } from '@remix-run/react'; 11 | import stylesUrl from '~/styles.css'; 12 | 13 | export const links: LinksFunction = () => { 14 | return [{ rel: 'stylesheet', href: stylesUrl }]; 15 | }; 16 | 17 | export const meta: V2_MetaFunction = () => [ 18 | { 19 | charset: 'utf-8', 20 | title: 'Remix - Conform Example', 21 | viewport: 'width=device-width,initial-scale=1', 22 | }, 23 | ]; 24 | 25 | export default function App() { 26 | return ( 27 | 28 | 29 | 30 | 31 | 32 | 33 |
34 |

Remix Example

35 | 36 |

37 | This example demonstrates some of the features of Conform including{' '} 38 | client validation, nested list, 39 | and async validation with zod. 40 |

41 | 42 |
    43 |
  • 44 | Login ( 45 | with useFetcher) 46 |
  • 47 |
  • 48 | Todo list 49 |
  • 50 |
  • 51 | Signup 52 |
  • 53 |
54 | 55 |
56 | 57 | 58 |
59 | 60 | 61 | 62 | 63 | 64 | ); 65 | } 66 | -------------------------------------------------------------------------------- /examples/remix/app/routes/_index.tsx: -------------------------------------------------------------------------------- 1 | import type { LoaderArgs } from '@remix-run/node'; 2 | import { json } from '@remix-run/node'; 3 | import { useLoaderData } from '@remix-run/react'; 4 | 5 | export function loader({ request }: LoaderArgs) { 6 | const url = new URL(request.url); 7 | const value = url.searchParams.get('value'); 8 | 9 | return json({ 10 | value: value ? JSON.parse(value) : undefined, 11 | }); 12 | } 13 | 14 | export default function Index() { 15 | const { value } = useLoaderData(); 16 | 17 | if (!value) { 18 | return null; 19 | } 20 | 21 | return ( 22 |
23 | Submitted the following value: 24 |
{JSON.stringify(value, null, 2)}
25 |
26 | ); 27 | } 28 | -------------------------------------------------------------------------------- /examples/remix/app/styles.css: -------------------------------------------------------------------------------- 1 | main { 2 | max-width: 600px; 3 | margin: 0 auto; 4 | } 5 | 6 | hr { 7 | margin: 30px 0; 8 | color: gainsboro; 9 | } 10 | 11 | label { 12 | display: block; 13 | } 14 | 15 | .form-error { 16 | color: red; 17 | } 18 | 19 | input.error { 20 | outline: red; 21 | color: red; 22 | border: 1px solid red; 23 | } 24 | 25 | input.error + div { 26 | color: red; 27 | } 28 | -------------------------------------------------------------------------------- /examples/remix/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@conform-example/remix", 3 | "private": true, 4 | "license": "MIT", 5 | "sideEffects": false, 6 | "scripts": { 7 | "build": "remix build", 8 | "dev": "remix dev", 9 | "start": "remix-serve build" 10 | }, 11 | "dependencies": { 12 | "@conform-to/react": "1.6.1", 13 | "@conform-to/zod": "1.6.1", 14 | "@remix-run/node": "^1.19.3", 15 | "@remix-run/react": "^1.19.3", 16 | "@remix-run/serve": "^1.19.3", 17 | "clsx": "^1.2.0", 18 | "isbot": "^3", 19 | "react": "^18.2.0", 20 | "react-dom": "^18.2.0", 21 | "zod": "^3.25.30" 22 | }, 23 | "devDependencies": { 24 | "@remix-run/dev": "^1.19.3", 25 | "@remix-run/eslint-config": "^1.19.3", 26 | "@types/react": "^18.0.28", 27 | "@types/react-dom": "^18.0.11", 28 | "cross-env": "^7.0.3", 29 | "eslint": "^8.57.0", 30 | "typescript": "^4.9.5" 31 | }, 32 | "engines": { 33 | "node": "20.x" 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /examples/remix/remix.config.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @type {import('@remix-run/dev').AppConfig} 3 | */ 4 | module.exports = { 5 | ignoredRouteFiles: ['.*'], 6 | future: { 7 | v2_dev: true, 8 | v2_errorBoundary: true, 9 | v2_headers: true, 10 | v2_meta: true, 11 | v2_normalizeFormMethod: true, 12 | v2_routeConvention: true, 13 | }, 14 | }; 15 | -------------------------------------------------------------------------------- /examples/remix/remix.env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | -------------------------------------------------------------------------------- /examples/remix/sandbox.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "hardReloadOnChange": true, 3 | "template": "node", 4 | "container": { 5 | "port": 3000, 6 | "node": "16" 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /examples/remix/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "include": ["remix.env.d.ts", "**/*.ts", "**/*.tsx"], 3 | "compilerOptions": { 4 | "lib": ["DOM", "DOM.Iterable", "ES2019"], 5 | "isolatedModules": true, 6 | "esModuleInterop": true, 7 | "jsx": "react-jsx", 8 | "moduleResolution": "node", 9 | "resolveJsonModule": true, 10 | "target": "ES2019", 11 | "strict": true, 12 | "baseUrl": ".", 13 | "paths": { 14 | "~/*": ["./app/*"] 15 | }, 16 | "noEmit": true, 17 | "allowJs": true, 18 | "forceConsistentCasingInFileNames": true 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /examples/shadcn-ui/.eslintrc.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | env: { browser: true, es2020: true }, 4 | extends: [ 5 | 'eslint:recommended', 6 | 'plugin:@typescript-eslint/recommended', 7 | 'plugin:react-hooks/recommended', 8 | ], 9 | ignorePatterns: ['dist', '.eslintrc.cjs'], 10 | parser: '@typescript-eslint/parser', 11 | }; 12 | -------------------------------------------------------------------------------- /examples/shadcn-ui/.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | dist 12 | dist-ssr 13 | *.local 14 | 15 | # Editor directories and files 16 | .vscode/* 17 | !.vscode/extensions.json 18 | .idea 19 | .DS_Store 20 | *.suo 21 | *.ntvs* 22 | *.njsproj 23 | *.sln 24 | *.sw? 25 | -------------------------------------------------------------------------------- /examples/shadcn-ui/README.md: -------------------------------------------------------------------------------- 1 | # Shadcn UI Integration 2 | 3 | [Shadcn UI](https://ui.shadcn.com/) 4 | Shadcn UI is a comprehensive component library built with React. It provides a wide range of pre-built components that can be easily integrated into your projects. The library is designed to be simple to use, allowing you to add components to your project either by copy/pasting them directly or using the provided CLI. 5 | 6 | ## Installation 7 | 8 | To install a component, you can simply copy and paste the component code into your project. Alternatively, you can use the Shadcn UI CLI to automatically add components to your project. By default, the CLI will place the components into the `src/components/ui` folder. 9 | 10 | ## Conform Forms integration 11 | 12 | This example includes a set of components in a separate folder `src/components/conform` that extend the shadcn components. By using these components, you can quickly and easily build complex forms with full validation and error handling. 13 | 14 | ## Additional infos 15 | 16 | This example we leverage [Vite](https://vitejs.dev/) and [Tailwind CSS](https://tailwindcss.com/) 17 | 18 | **Components** 19 | 20 | - Checkbox 21 | - Checkbox group 22 | - Combobox 23 | - Date picker 24 | - Radio group 25 | - Select 26 | - Slider 27 | - Switch 28 | - Textarea 29 | - Toggle group 30 | 31 | ## Demo 32 | 33 | 34 | 35 | Try it out on [Codesandbox](https://codesandbox.io/s/github/edmundhung/conform/tree/main/examples/shadcn-ui). 36 | 37 | 38 | -------------------------------------------------------------------------------- /examples/shadcn-ui/components.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://ui.shadcn.com/schema.json", 3 | "style": "default", 4 | "rsc": false, 5 | "tsx": true, 6 | "tailwind": { 7 | "config": "tailwind.config.js", 8 | "css": "src/index.css", 9 | "baseColor": "stone", 10 | "cssVariables": false, 11 | "prefix": "" 12 | }, 13 | "aliases": { 14 | "components": "@/components", 15 | "utils": "@/lib/utils" 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /examples/shadcn-ui/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Vite + React + TS 8 | 9 | 10 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /examples/shadcn-ui/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@conform-example/shadcn-ui", 3 | "private": true, 4 | "version": "0.0.0", 5 | "type": "module", 6 | "scripts": { 7 | "dev": "vite", 8 | "build": "tsc && vite build", 9 | "lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0", 10 | "preview": "vite preview" 11 | }, 12 | "dependencies": { 13 | "@conform-to/react": "1.6.1", 14 | "@conform-to/zod": "1.6.1", 15 | "@radix-ui/react-checkbox": "^1.0.4", 16 | "@radix-ui/react-dialog": "^1.0.5", 17 | "@radix-ui/react-label": "^2.0.2", 18 | "@radix-ui/react-popover": "^1.0.7", 19 | "@radix-ui/react-radio-group": "^1.1.3", 20 | "@radix-ui/react-select": "^2.0.0", 21 | "@radix-ui/react-slider": "^1.1.2", 22 | "@radix-ui/react-slot": "^1.0.2", 23 | "@radix-ui/react-switch": "^1.0.3", 24 | "@radix-ui/react-toggle": "^1.0.3", 25 | "@radix-ui/react-toggle-group": "^1.0.4", 26 | "class-variance-authority": "^0.7.0", 27 | "clsx": "^2.1.0", 28 | "cmdk": "^0.2.1", 29 | "date-fns": "^3.3.1", 30 | "input-otp": "^1.2.2", 31 | "lucide-react": "^0.338.0", 32 | "react": "^18.2.0", 33 | "react-day-picker": "^8.10.0", 34 | "react-dom": "^18.2.0", 35 | "tailwind-merge": "^2.2.1", 36 | "tailwindcss-animate": "^1.0.7", 37 | "zod": "^3.25.30" 38 | }, 39 | "devDependencies": { 40 | "@types/node": "^20.11.20", 41 | "@types/react": "^18.2.56", 42 | "@types/react-dom": "^18.2.19", 43 | "@typescript-eslint/eslint-plugin": "^7.11.0", 44 | "@typescript-eslint/parser": "^7.11.0", 45 | "@vitejs/plugin-react": "^4.2.1", 46 | "autoprefixer": "^10.4.17", 47 | "eslint": "^8.57.0", 48 | "eslint-plugin-react-hooks": "^4.6.2", 49 | "postcss": "^8.4.35", 50 | "tailwindcss": "^3.4.1", 51 | "tailwindcss-animatecss": "^3.0.5", 52 | "typescript": "^5.2.2", 53 | "vite": "^5.1.4" 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /examples/shadcn-ui/postcss.config.js: -------------------------------------------------------------------------------- 1 | export default { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | }; 7 | -------------------------------------------------------------------------------- /examples/shadcn-ui/public/vite.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /examples/shadcn-ui/src/components/Field.tsx: -------------------------------------------------------------------------------- 1 | export const Field = ({ children }: { children: React.ReactNode }) => { 2 | return
{children}
; 3 | }; 4 | 5 | export const FieldError = ({ children }: { children: React.ReactNode }) => { 6 | return
{children}
; 7 | }; 8 | -------------------------------------------------------------------------------- /examples/shadcn-ui/src/components/conform/Checkbox.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | unstable_useControl as useControl, 3 | type FieldMetadata, 4 | } from '@conform-to/react'; 5 | import { useRef, type ElementRef } from 'react'; 6 | import { Checkbox } from '@/components/ui/checkbox'; 7 | 8 | export function CheckboxConform({ 9 | meta, 10 | }: { 11 | meta: FieldMetadata; 12 | }) { 13 | const checkboxRef = useRef>(null); 14 | const control = useControl(meta); 15 | 16 | return ( 17 | <> 18 | checkboxRef.current?.focus()} 26 | /> 27 | { 32 | control.change(checked ? 'on' : ''); 33 | }} 34 | onBlur={control.blur} 35 | className="focus:ring-stone-950 focus:ring-2 focus:ring-offset-2" 36 | /> 37 | 38 | ); 39 | } 40 | -------------------------------------------------------------------------------- /examples/shadcn-ui/src/components/conform/CheckboxGroup.tsx: -------------------------------------------------------------------------------- 1 | import { Checkbox } from '@/components/ui/checkbox'; 2 | import { 3 | unstable_Control as Control, 4 | type FieldMetadata, 5 | } from '@conform-to/react'; 6 | 7 | export function CheckboxGroupConform({ 8 | meta, 9 | items, 10 | }: { 11 | meta: FieldMetadata; 12 | items: Array<{ name: string; value: string }>; 13 | }) { 14 | const initialValue = 15 | typeof meta.initialValue === 'string' 16 | ? [meta.initialValue] 17 | : meta.initialValue ?? []; 18 | 19 | return ( 20 | <> 21 | {items.map((item) => ( 22 | v == item.value) 27 | ? [item.value] 28 | : '', 29 | }} 30 | render={(control) => ( 31 |
{ 34 | control.register(element?.querySelector('input')); 35 | }} 36 | > 37 | 44 | control.change(value.valueOf() ? item.value : '') 45 | } 46 | onBlur={control.blur} 47 | className="focus:ring-stone-950 focus:ring-2 focus:ring-offset-2" 48 | /> 49 | 50 |
51 | )} 52 | /> 53 | ))} 54 | 55 | ); 56 | } 57 | -------------------------------------------------------------------------------- /examples/shadcn-ui/src/components/conform/Input.tsx: -------------------------------------------------------------------------------- 1 | import { FieldMetadata, getInputProps } from '@conform-to/react'; 2 | import { Input } from '../ui/input'; 3 | import { ComponentProps } from 'react'; 4 | 5 | export const InputConform = ({ 6 | meta, 7 | type, 8 | ...props 9 | }: { 10 | meta: FieldMetadata; 11 | type: Parameters[1]['type']; 12 | } & ComponentProps) => { 13 | return ( 14 | 18 | ); 19 | }; 20 | -------------------------------------------------------------------------------- /examples/shadcn-ui/src/components/conform/InputOTP.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | type FieldMetadata, 3 | unstable_useControl as useControl, 4 | } from '@conform-to/react'; 5 | import { REGEXP_ONLY_DIGITS_AND_CHARS } from 'input-otp'; 6 | import { type ElementRef, useRef } from 'react'; 7 | import { InputOTP, InputOTPGroup, InputOTPSlot } from '../ui/input-otp'; 8 | 9 | export function InputOTPConform({ 10 | meta, 11 | length = 6, 12 | pattern = REGEXP_ONLY_DIGITS_AND_CHARS, 13 | }: { 14 | meta: FieldMetadata; 15 | length: number; 16 | pattern?: string; 17 | }) { 18 | const inputOTPRef = useRef>(null); 19 | const control = useControl(meta); 20 | 21 | return ( 22 | <> 23 | { 30 | inputOTPRef.current?.focus(); 31 | }} 32 | /> 33 | 41 | 42 | {new Array(length).fill(0).map((_, index) => ( 43 | 44 | ))} 45 | 46 | 47 | 48 | ); 49 | } 50 | -------------------------------------------------------------------------------- /examples/shadcn-ui/src/components/conform/RadioGroup.tsx: -------------------------------------------------------------------------------- 1 | import { RadioGroup, RadioGroupItem } from '@/components/ui/radio-group'; 2 | import { 3 | FieldMetadata, 4 | unstable_useControl as useControl, 5 | } from '@conform-to/react'; 6 | import { ElementRef, useRef } from 'react'; 7 | 8 | export function RadioGroupConform({ 9 | meta, 10 | items, 11 | }: { 12 | meta: FieldMetadata; 13 | items: Array<{ value: string; label: string }>; 14 | }) { 15 | const radioGroupRef = useRef>(null); 16 | const control = useControl(meta); 17 | 18 | return ( 19 | <> 20 | { 27 | radioGroupRef.current?.focus(); 28 | }} 29 | /> 30 | 37 | {items.map((item) => { 38 | return ( 39 |
40 | 44 | 45 |
46 | ); 47 | })} 48 |
49 | 50 | ); 51 | } 52 | -------------------------------------------------------------------------------- /examples/shadcn-ui/src/components/conform/Select.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | unstable_useControl as useControl, 3 | type FieldMetadata, 4 | } from '@conform-to/react'; 5 | import { useRef, type ElementRef, ComponentProps } from 'react'; 6 | import { 7 | SelectTrigger, 8 | Select, 9 | SelectValue, 10 | SelectContent, 11 | SelectItem, 12 | } from '@/components/ui/select'; 13 | 14 | export const SelectConform = ({ 15 | meta, 16 | items, 17 | placeholder, 18 | ...props 19 | }: { 20 | meta: FieldMetadata; 21 | items: Array<{ name: string; value: string }>; 22 | placeholder: string; 23 | } & ComponentProps) => { 24 | const selectRef = useRef>(null); 25 | const control = useControl(meta); 26 | 27 | return ( 28 | <> 29 | 45 | 46 | 69 | 70 | ); 71 | }; 72 | -------------------------------------------------------------------------------- /examples/shadcn-ui/src/components/conform/Slider.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | FieldMetadata, 3 | unstable_useControl as useControl, 4 | } from '@conform-to/react'; 5 | import { ComponentProps, ElementRef, useRef } from 'react'; 6 | import { Slider } from '@/components/ui/slider'; 7 | 8 | export function SliderConform({ 9 | meta, 10 | ...props 11 | }: { 12 | meta: FieldMetadata; 13 | ariaLabel?: string; 14 | } & ComponentProps) { 15 | const sliderRef = useRef>(null); 16 | const control = useControl(meta); 17 | 18 | return ( 19 | <> 20 | { 27 | const sliderSpan = 28 | sliderRef.current?.querySelector('[role="slider"]'); 29 | if (sliderSpan instanceof HTMLElement) { 30 | sliderSpan.focus(); 31 | } 32 | }} 33 | /> 34 |
35 | { 41 | control.change(value[0].toString()); 42 | }} 43 | onBlur={control.blur} 44 | className="w-[280px]" 45 | /> 46 |
{control.value}
47 |
48 | 49 | ); 50 | } 51 | -------------------------------------------------------------------------------- /examples/shadcn-ui/src/components/conform/Switch.tsx: -------------------------------------------------------------------------------- 1 | import { Switch } from '@/components/ui/switch'; 2 | import { 3 | unstable_useControl as useControl, 4 | type FieldMetadata, 5 | } from '@conform-to/react'; 6 | import { useRef, type ElementRef } from 'react'; 7 | 8 | export function SwitchConform({ meta }: { meta: FieldMetadata }) { 9 | const switchRef = useRef>(null); 10 | const control = useControl(meta); 11 | 12 | return ( 13 | <> 14 | { 21 | switchRef.current?.focus(); 22 | }} 23 | /> 24 | { 28 | control.change(checked ? 'on' : ''); 29 | }} 30 | onBlur={control.blur} 31 | className="focus:ring-stone-950 focus:ring-2 focus:ring-offset-2" 32 | > 33 | 34 | ); 35 | } 36 | -------------------------------------------------------------------------------- /examples/shadcn-ui/src/components/conform/Textarea.tsx: -------------------------------------------------------------------------------- 1 | import { FieldMetadata, getTextareaProps } from '@conform-to/react'; 2 | import { Textarea } from '@/components/ui/textarea'; 3 | import { ComponentProps } from 'react'; 4 | 5 | export const TextareaConform = ({ 6 | meta, 7 | ...props 8 | }: { 9 | meta: FieldMetadata; 10 | } & ComponentProps) => { 11 | return