├── .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 | You need to enable JavaScript to run this app.
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 | You need to enable JavaScript to run this app.
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 | You need to enable JavaScript to run this app.
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 | {item.label}
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 |
Email
38 |
42 |
{fields.email.errors}
43 |
44 |
45 |
Password
46 |
50 |
{fields.password.errors}
51 |
52 |
53 |
54 | Remember me
55 |
56 |
57 |
58 |
59 | Login
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 |
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 | {item.name}
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 | {item.label}
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 | {
37 | selectRef.current?.focus();
38 | }}
39 | >
40 |
41 | {items.map((option) => (
42 |
43 | ))}
44 |
45 |
46 | {
51 | if (!open) {
52 | control.blur();
53 | }
54 | }}
55 | >
56 |
57 |
58 |
59 |
60 | {items.map((item) => {
61 | return (
62 |
63 | {item.name}
64 |
65 | );
66 | })}
67 |
68 |
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 ;
12 | };
13 |
--------------------------------------------------------------------------------
/examples/shadcn-ui/src/components/conform/ToggleGroup.tsx:
--------------------------------------------------------------------------------
1 | import {
2 | FieldMetadata,
3 | unstable_useControl as useControl,
4 | } from '@conform-to/react';
5 | import { ComponentProps, ElementRef, useRef } from 'react';
6 | import { ToggleGroup, ToggleGroupItem } from '@/components/ui/toggle-group';
7 |
8 | export const ToggleGroupConform = ({
9 | type = 'single',
10 | meta,
11 | items,
12 | ...props
13 | }: {
14 | items: Array<{ value: string; label: string }>;
15 | meta: FieldMetadata;
16 | } & Omit, 'defaultValue'>) => {
17 | const toggleGroupRef = useRef>(null);
18 | const control = useControl(meta);
19 |
20 | return (
21 | <>
22 | {type === 'single' ? {
29 | toggleGroupRef.current?.focus();
30 | }}
31 | /> : {
37 | toggleGroupRef.current?.focus();
38 | }}
39 | defaultValue={meta.initialValue}
40 | tabIndex={-1}
41 | >
42 | {items.map((item) => (
43 |
44 | {item.label}
45 |
46 | ))}
47 |
48 | }
49 |
50 | {
56 | props.onValueChange?.(value);
57 | control.change(value);
58 | }}
59 | >
60 | {items.map((item) => (
61 |
62 | {item.label}
63 |
64 | ))}
65 |
66 | >
67 | );
68 | };
69 |
--------------------------------------------------------------------------------
/examples/shadcn-ui/src/components/ui/checkbox.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 | import * as CheckboxPrimitive from '@radix-ui/react-checkbox';
3 | import { Check } from 'lucide-react';
4 |
5 | import { cn } from '@/lib/utils';
6 |
7 | const Checkbox = React.forwardRef<
8 | React.ElementRef,
9 | React.ComponentPropsWithoutRef
10 | >(({ className, ...props }, ref) => (
11 |
19 |
22 |
23 |
24 |
25 | ));
26 | Checkbox.displayName = CheckboxPrimitive.Root.displayName;
27 |
28 | export { Checkbox };
29 |
--------------------------------------------------------------------------------
/examples/shadcn-ui/src/components/ui/input.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 |
3 | import { cn } from '@/lib/utils';
4 |
5 | export interface InputProps
6 | extends React.InputHTMLAttributes {}
7 |
8 | const Input = React.forwardRef(
9 | ({ className, type, ...props }, ref) => {
10 | return (
11 |
20 | );
21 | },
22 | );
23 | Input.displayName = 'Input';
24 |
25 | export { Input };
26 |
--------------------------------------------------------------------------------
/examples/shadcn-ui/src/components/ui/label.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 | import * as LabelPrimitive from '@radix-ui/react-label';
3 | import { cva, type VariantProps } from 'class-variance-authority';
4 |
5 | import { cn } from '@/lib/utils';
6 |
7 | const labelVariants = cva(
8 | 'text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70',
9 | );
10 |
11 | const Label = React.forwardRef<
12 | React.ElementRef,
13 | React.ComponentPropsWithoutRef &
14 | VariantProps
15 | >(({ className, ...props }, ref) => (
16 |
21 | ));
22 | Label.displayName = LabelPrimitive.Root.displayName;
23 |
24 | export { Label };
25 |
--------------------------------------------------------------------------------
/examples/shadcn-ui/src/components/ui/popover.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 | import * as PopoverPrimitive from '@radix-ui/react-popover';
3 |
4 | import { cn } from '@/lib/utils';
5 |
6 | const Popover = PopoverPrimitive.Root;
7 |
8 | const PopoverTrigger = PopoverPrimitive.Trigger;
9 |
10 | const PopoverContent = React.forwardRef<
11 | React.ElementRef,
12 | React.ComponentPropsWithoutRef
13 | >(({ className, align = 'center', sideOffset = 4, ...props }, ref) => (
14 |
15 |
25 |
26 | ));
27 | PopoverContent.displayName = PopoverPrimitive.Content.displayName;
28 |
29 | export { Popover, PopoverTrigger, PopoverContent };
30 |
--------------------------------------------------------------------------------
/examples/shadcn-ui/src/components/ui/radio-group.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 | import * as RadioGroupPrimitive from '@radix-ui/react-radio-group';
3 | import { Circle } from 'lucide-react';
4 |
5 | import { cn } from '@/lib/utils';
6 |
7 | const RadioGroup = React.forwardRef<
8 | React.ElementRef,
9 | React.ComponentPropsWithoutRef
10 | >(({ className, ...props }, ref) => {
11 | return (
12 |
17 | );
18 | });
19 | RadioGroup.displayName = RadioGroupPrimitive.Root.displayName;
20 |
21 | const RadioGroupItem = React.forwardRef<
22 | React.ElementRef,
23 | React.ComponentPropsWithoutRef
24 | >(({ className, ...props }, ref) => {
25 | return (
26 |
34 |
35 |
36 |
37 |
38 | );
39 | });
40 | RadioGroupItem.displayName = RadioGroupPrimitive.Item.displayName;
41 |
42 | export { RadioGroup, RadioGroupItem };
43 |
--------------------------------------------------------------------------------
/examples/shadcn-ui/src/components/ui/slider.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 | import * as SliderPrimitive from '@radix-ui/react-slider';
3 |
4 | import { cn } from '@/lib/utils';
5 |
6 | const Slider = React.forwardRef<
7 | React.ElementRef,
8 | React.ComponentPropsWithoutRef
9 | >(({ className, ...props }, ref) => (
10 |
18 |
19 |
20 |
21 |
22 |
23 | ));
24 | Slider.displayName = SliderPrimitive.Root.displayName;
25 |
26 | export { Slider };
27 |
--------------------------------------------------------------------------------
/examples/shadcn-ui/src/components/ui/switch.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 | import * as SwitchPrimitives from '@radix-ui/react-switch';
3 |
4 | import { cn } from '@/lib/utils';
5 |
6 | const Switch = React.forwardRef<
7 | React.ElementRef,
8 | React.ComponentPropsWithoutRef
9 | >(({ className, ...props }, ref) => (
10 |
18 |
23 |
24 | ));
25 | Switch.displayName = SwitchPrimitives.Root.displayName;
26 |
27 | export { Switch };
28 |
--------------------------------------------------------------------------------
/examples/shadcn-ui/src/components/ui/textarea.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 |
3 | import { cn } from '@/lib/utils';
4 |
5 | export interface TextareaProps
6 | extends React.TextareaHTMLAttributes {}
7 |
8 | const Textarea = React.forwardRef(
9 | ({ className, ...props }, ref) => {
10 | return (
11 |
19 | );
20 | },
21 | );
22 | Textarea.displayName = 'Textarea';
23 |
24 | export { Textarea };
25 |
--------------------------------------------------------------------------------
/examples/shadcn-ui/src/components/ui/toggle.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 | import * as TogglePrimitive from '@radix-ui/react-toggle';
3 | import { cva, type VariantProps } from 'class-variance-authority';
4 |
5 | import { cn } from '@/lib/utils';
6 |
7 | const toggleVariants = cva(
8 | 'inline-flex items-center justify-center rounded-md text-sm font-medium ring-offset-white transition-colors hover:bg-stone-100 hover:text-stone-500 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-stone-950 focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 data-[state=on]:bg-stone-100 data-[state=on]:text-stone-900 dark:ring-offset-stone-950 dark:hover:bg-stone-800 dark:hover:text-stone-400 dark:focus-visible:ring-stone-300 dark:data-[state=on]:bg-stone-800 dark:data-[state=on]:text-stone-50',
9 | {
10 | variants: {
11 | variant: {
12 | default: 'bg-transparent',
13 | outline:
14 | 'border border-stone-200 bg-transparent hover:bg-stone-100 hover:text-stone-900 dark:border-stone-800 dark:hover:bg-stone-800 dark:hover:text-stone-50',
15 | },
16 | size: {
17 | default: 'h-10 px-3',
18 | sm: 'h-9 px-2.5',
19 | lg: 'h-11 px-5',
20 | },
21 | },
22 | defaultVariants: {
23 | variant: 'default',
24 | size: 'default',
25 | },
26 | },
27 | );
28 |
29 | const Toggle = React.forwardRef<
30 | React.ElementRef,
31 | React.ComponentPropsWithoutRef &
32 | VariantProps
33 | >(({ className, variant, size, ...props }, ref) => (
34 |
39 | ));
40 |
41 | Toggle.displayName = TogglePrimitive.Root.displayName;
42 |
43 | export { Toggle, toggleVariants };
44 |
--------------------------------------------------------------------------------
/examples/shadcn-ui/src/index.css:
--------------------------------------------------------------------------------
1 | @tailwind base;
2 | @tailwind components;
3 | @tailwind utilities;
4 |
--------------------------------------------------------------------------------
/examples/shadcn-ui/src/lib/utils.ts:
--------------------------------------------------------------------------------
1 | import { type ClassValue, clsx } from 'clsx';
2 | import { twMerge } from 'tailwind-merge';
3 |
4 | export function cn(...inputs: ClassValue[]) {
5 | return twMerge(clsx(inputs));
6 | }
7 |
--------------------------------------------------------------------------------
/examples/shadcn-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/shadcn-ui/src/vite-env.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 |
--------------------------------------------------------------------------------
/examples/shadcn-ui/tailwind.config.js:
--------------------------------------------------------------------------------
1 | import tailwindCssAnimate from 'tailwindcss-animatecss';
2 |
3 | /** @type {import('tailwindcss').Config} */
4 | export default {
5 | darkMode: ['class'],
6 | content: [
7 | './pages/**/*.{ts,tsx}',
8 | './components/**/*.{ts,tsx}',
9 | './app/**/*.{ts,tsx}',
10 | './src/**/*.{ts,tsx}',
11 | ],
12 | prefix: '',
13 | theme: {
14 | container: {
15 | center: true,
16 | padding: '2rem',
17 | screens: {
18 | '2xl': '1400px',
19 | },
20 | },
21 | extend: {
22 | keyframes: {
23 | 'accordion-down': {
24 | from: { height: '0' },
25 | to: { height: 'var(--radix-accordion-content-height)' },
26 | },
27 | 'accordion-up': {
28 | from: { height: 'var(--radix-accordion-content-height)' },
29 | to: { height: '0' },
30 | },
31 | 'caret-blink': {
32 | '0%,70%,100%': { opacity: '1' },
33 | '20%,50%': { opacity: '0' },
34 | },
35 | },
36 | animation: {
37 | 'accordion-down': 'accordion-down 0.2s ease-out',
38 | 'accordion-up': 'accordion-up 0.2s ease-out',
39 | 'caret-blink': 'caret-blink 1.25s ease-out infinite',
40 | },
41 | },
42 | },
43 | plugins: [tailwindCssAnimate],
44 | };
45 |
--------------------------------------------------------------------------------
/examples/shadcn-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 | "baseUrl": ".",
23 | "paths": {
24 | "@/*": ["./src/*"]
25 | }
26 | },
27 | "include": ["src"],
28 | "references": [{ "path": "./tsconfig.node.json" }]
29 | }
30 |
--------------------------------------------------------------------------------
/examples/shadcn-ui/tsconfig.node.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "composite": true,
4 | "skipLibCheck": true,
5 | "module": "ESNext",
6 | "moduleResolution": "bundler",
7 | "allowSyntheticDefaultImports": true,
8 | "strict": true
9 | },
10 | "include": ["vite.config.ts"]
11 | }
12 |
--------------------------------------------------------------------------------
/examples/shadcn-ui/vite.config.ts:
--------------------------------------------------------------------------------
1 | import path from 'path';
2 | import react from '@vitejs/plugin-react';
3 | import { defineConfig } from 'vite';
4 |
5 | export default defineConfig({
6 | plugins: [react()],
7 | resolve: {
8 | alias: {
9 | '@': path.resolve(__dirname, './src'),
10 | },
11 | },
12 | });
13 |
--------------------------------------------------------------------------------
/guide/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 |
3 | /.wrangler
4 | /.cache
5 | /functions/version.txt
6 | /functions/\[\[path\]\].js
7 | /functions/\[\[path\]\].js.map
8 | /functions/metafile.*
9 | /public/build
10 | /public/docs
11 | .env
12 |
--------------------------------------------------------------------------------
/guide/.node-version:
--------------------------------------------------------------------------------
1 | 20
2 |
--------------------------------------------------------------------------------
/guide/README.md:
--------------------------------------------------------------------------------
1 | # Conform Guide
2 |
--------------------------------------------------------------------------------
/guide/app/entry.client.tsx:
--------------------------------------------------------------------------------
1 | import { RemixBrowser } from '@remix-run/react';
2 | import { startTransition, StrictMode } from 'react';
3 | import { hydrateRoot } from 'react-dom/client';
4 |
5 | function hydrate() {
6 | startTransition(() => {
7 | hydrateRoot(
8 | document,
9 |
10 |
11 | ,
12 | );
13 | });
14 | }
15 |
16 | if (typeof requestIdleCallback === 'function') {
17 | requestIdleCallback(hydrate);
18 | } else {
19 | // Safari doesn't support requestIdleCallback
20 | // https://caniuse.com/requestidlecallback
21 | setTimeout(hydrate, 1);
22 | }
23 |
--------------------------------------------------------------------------------
/guide/app/entry.server.tsx:
--------------------------------------------------------------------------------
1 | import type { EntryContext } from '@remix-run/cloudflare';
2 | import { RemixServer } from '@remix-run/react';
3 | import { renderToString } from 'react-dom/server';
4 |
5 | export default function handleRequest(
6 | request: Request,
7 | responseStatusCode: number,
8 | responseHeaders: Headers,
9 | remixContext: EntryContext,
10 | ) {
11 | const markup = renderToString(
12 | ,
13 | );
14 |
15 | responseHeaders.set('Content-Type', 'text/html');
16 |
17 | return new Response('' + markup, {
18 | status: responseStatusCode,
19 | headers: responseHeaders,
20 | });
21 | }
22 |
--------------------------------------------------------------------------------
/guide/app/routes/$.tsx:
--------------------------------------------------------------------------------
1 | import {
2 | type LoaderFunctionArgs,
3 | type HeadersFunction,
4 | type MetaFunction,
5 | json,
6 | } from '@remix-run/cloudflare';
7 | import { useLoaderData } from '@remix-run/react';
8 | import { collectHeadings, parse } from '~/markdoc';
9 | import { Markdown } from '~/components';
10 | import { getDocPath, formatTitle, getFileContent } from '~/util';
11 |
12 | export const headers: HeadersFunction = ({ loaderHeaders }) => {
13 | return loaderHeaders;
14 | };
15 |
16 | export const meta: MetaFunction = ({ data }) => {
17 | return [
18 | {
19 | title: formatTitle(data?.toc.title),
20 | },
21 | ];
22 | };
23 |
24 | export async function loader({ params, context }: LoaderFunctionArgs) {
25 | const file = `${getDocPath(context)}/${params['*']}.md`;
26 | const readme = await getFileContent(context, file);
27 | const content = parse(readme);
28 |
29 | return json(
30 | {
31 | file,
32 | content,
33 | toc: collectHeadings(content),
34 | },
35 | {
36 | headers: {
37 | 'Cache-Control': 'public, max-age=60',
38 | },
39 | },
40 | );
41 | }
42 |
43 | export default function Page() {
44 | const { content } = useLoaderData();
45 |
46 | return ;
47 | }
48 |
--------------------------------------------------------------------------------
/guide/app/routes/_index.tsx:
--------------------------------------------------------------------------------
1 | import {
2 | type LoaderFunctionArgs,
3 | type HeadersFunction,
4 | type MetaFunction,
5 | json,
6 | } from '@remix-run/cloudflare';
7 | import { useLoaderData } from '@remix-run/react';
8 | import { collectHeadings, parse } from '~/markdoc';
9 | import { Markdown } from '~/components';
10 | import { getDocPath, formatTitle, getFileContent } from '~/util';
11 |
12 | export const headers: HeadersFunction = ({ loaderHeaders }) => {
13 | return loaderHeaders;
14 | };
15 |
16 | export const meta: MetaFunction = ({ data }) => {
17 | return [
18 | {
19 | title: formatTitle(data?.toc.title),
20 | },
21 | ];
22 | };
23 |
24 | export async function loader({ context }: LoaderFunctionArgs) {
25 | const file = `${getDocPath(context)}/overview.md`;
26 | const readme = await getFileContent(context, file);
27 | const content = parse(readme);
28 |
29 | return json(
30 | {
31 | file,
32 | content,
33 | toc: collectHeadings(content),
34 | },
35 | {
36 | headers: {
37 | 'Cache-Control': 'public, max-age=60',
38 | },
39 | },
40 | );
41 | }
42 |
43 | export default function Index() {
44 | const { content } = useLoaderData();
45 |
46 | return ;
47 | }
48 |
--------------------------------------------------------------------------------
/guide/app/routes/examples.$name.tsx:
--------------------------------------------------------------------------------
1 | import { LoaderFunctionArgs, redirect } from '@remix-run/cloudflare';
2 | import { getMetadata } from '~/util';
3 |
4 | export async function loader({ params, context }: LoaderFunctionArgs) {
5 | const { owner, repo, ref } = getMetadata(context);
6 |
7 | return redirect(
8 | `https://github.com/${owner}/${repo}/tree/${ref}/examples/${params.name}`,
9 | );
10 | }
11 |
--------------------------------------------------------------------------------
/guide/app/styles.css:
--------------------------------------------------------------------------------
1 | @tailwind base;
2 | @tailwind components;
3 | @tailwind utilities;
4 |
5 | :root {
6 | color-scheme: light dark;
7 | }
8 |
9 | @layer base {
10 | /* ubuntu-300 - latin */
11 | @font-face {
12 | font-family: 'Ubuntu';
13 | font-style: normal;
14 | font-weight: 300;
15 | src:
16 | local(''),
17 | url('/fonts/ubuntu-v20-latin-300.woff2') format('woff2'),
18 | /* Chrome 26+, Opera 23+, Firefox 39+ */
19 | url('/fonts/ubuntu-v20-latin-300.woff') format('woff'); /* Chrome 6+, Firefox 3.6+, IE 9+, Safari 5.1+ */
20 | }
21 |
22 | /* ubuntu-regular - latin */
23 | @font-face {
24 | font-family: 'Ubuntu';
25 | font-style: normal;
26 | font-weight: 400;
27 | src:
28 | local(''),
29 | url('/fonts/ubuntu-v20-latin-regular.woff2') format('woff2'),
30 | /* Chrome 26+, Opera 23+, Firefox 39+ */
31 | url('/fonts/ubuntu-v20-latin-regular.woff') format('woff'); /* Chrome 6+, Firefox 3.6+, IE 9+, Safari 5.1+ */
32 | }
33 |
34 | /* ubuntu-500 - latin */
35 | @font-face {
36 | font-family: 'Ubuntu';
37 | font-style: normal;
38 | font-weight: 500;
39 | src:
40 | local(''),
41 | url('/fonts/ubuntu-v20-latin-500.woff2') format('woff2'),
42 | /* Chrome 26+, Opera 23+, Firefox 39+ */
43 | url('/fonts/ubuntu-v20-latin-500.woff') format('woff'); /* Chrome 6+, Firefox 3.6+, IE 9+, Safari 5.1+ */
44 | }
45 |
46 | /* ubuntu-700 - latin */
47 | @font-face {
48 | font-family: 'Ubuntu';
49 | font-style: normal;
50 | font-weight: 700;
51 | src:
52 | local(''),
53 | url('/fonts/ubuntu-v20-latin-700.woff2') format('woff2'),
54 | /* Chrome 26+, Opera 23+, Firefox 39+ */
55 | url('/fonts/ubuntu-v20-latin-700.woff') format('woff'); /* Chrome 6+, Firefox 3.6+, IE 9+, Safari 5.1+ */
56 | }
57 | }
58 |
--------------------------------------------------------------------------------
/guide/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "private": true,
3 | "sideEffects": false,
4 | "name": "conform-guide",
5 | "type": "module",
6 | "scripts": {
7 | "build": "remix build",
8 | "ci": "sed -i -e \"s/CF_PAGES_BRANCH:\\s'main'/CF_PAGES_BRANCH: '$CF_PAGES_BRANCH'/g\" ./server.ts",
9 | "dev": "remix dev --manual -c \"npm run start\"",
10 | "predev": "remix build",
11 | "start": "wrangler pages dev public --port 3000 --kv CACHE -b ENVIRONMENT=development -b CF_PAGES_BRANCH=$(git branch --show-current)"
12 | },
13 | "dependencies": {
14 | "@markdoc/markdoc": "^0.4.0",
15 | "@remix-run/cloudflare": "^2.5.1",
16 | "@remix-run/cloudflare-pages": "^2.5.1",
17 | "@remix-run/react": "^2.5.1",
18 | "cross-env": "^7.0.3",
19 | "isbot": "^3",
20 | "react": "^18.2.0",
21 | "react-dom": "^18.2.0",
22 | "react-syntax-highlighter": "^15.5.0"
23 | },
24 | "devDependencies": {
25 | "@cloudflare/workers-types": "^4.20240419.0",
26 | "@octokit/types": "^12.4.0",
27 | "@remix-run/dev": "^2.5.1",
28 | "@tailwindcss/forms": "^0.5.7",
29 | "@tailwindcss/typography": "^0.5.10",
30 | "@types/react": "^18.2.46",
31 | "@types/react-dom": "^18.2.18",
32 | "@types/react-syntax-highlighter": "^15.5.11",
33 | "tailwindcss": "^3.4.0",
34 | "typescript": "^5.3.3",
35 | "wrangler": "^3.28.2"
36 | },
37 | "engines": {
38 | "node": "20.x"
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/guide/public/_headers:
--------------------------------------------------------------------------------
1 | /build/*
2 | Cache-Control: public, max-age=31536000, immutable
3 |
--------------------------------------------------------------------------------
/guide/public/_routes.json:
--------------------------------------------------------------------------------
1 | {
2 | "version": 1,
3 | "include": ["/*"],
4 | "exclude": ["/build/*", "/fonts/*", "/favicon.ico"]
5 | }
6 |
--------------------------------------------------------------------------------
/guide/public/fonts/ubuntu-v20-latin-300.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/edmundhung/conform/2c07a7f91dd56cc75ddb213b8ea321fea6fee142/guide/public/fonts/ubuntu-v20-latin-300.woff
--------------------------------------------------------------------------------
/guide/public/fonts/ubuntu-v20-latin-300.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/edmundhung/conform/2c07a7f91dd56cc75ddb213b8ea321fea6fee142/guide/public/fonts/ubuntu-v20-latin-300.woff2
--------------------------------------------------------------------------------
/guide/public/fonts/ubuntu-v20-latin-500.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/edmundhung/conform/2c07a7f91dd56cc75ddb213b8ea321fea6fee142/guide/public/fonts/ubuntu-v20-latin-500.woff
--------------------------------------------------------------------------------
/guide/public/fonts/ubuntu-v20-latin-500.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/edmundhung/conform/2c07a7f91dd56cc75ddb213b8ea321fea6fee142/guide/public/fonts/ubuntu-v20-latin-500.woff2
--------------------------------------------------------------------------------
/guide/public/fonts/ubuntu-v20-latin-700.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/edmundhung/conform/2c07a7f91dd56cc75ddb213b8ea321fea6fee142/guide/public/fonts/ubuntu-v20-latin-700.woff
--------------------------------------------------------------------------------
/guide/public/fonts/ubuntu-v20-latin-700.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/edmundhung/conform/2c07a7f91dd56cc75ddb213b8ea321fea6fee142/guide/public/fonts/ubuntu-v20-latin-700.woff2
--------------------------------------------------------------------------------
/guide/public/fonts/ubuntu-v20-latin-regular.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/edmundhung/conform/2c07a7f91dd56cc75ddb213b8ea321fea6fee142/guide/public/fonts/ubuntu-v20-latin-regular.woff
--------------------------------------------------------------------------------
/guide/public/fonts/ubuntu-v20-latin-regular.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/edmundhung/conform/2c07a7f91dd56cc75ddb213b8ea321fea6fee142/guide/public/fonts/ubuntu-v20-latin-regular.woff2
--------------------------------------------------------------------------------
/guide/remix.config.js:
--------------------------------------------------------------------------------
1 | /** @type {import('@remix-run/dev').AppConfig} */
2 | export default {
3 | ignoredRouteFiles: ['**/.*'],
4 | server: './server.ts',
5 | serverBuildPath: 'functions/[[path]].js',
6 | serverConditions: ['workerd', 'worker', 'browser'],
7 | serverDependenciesToBundle: 'all',
8 | serverMainFields: ['browser', 'module', 'main'],
9 | serverMinify: true,
10 | serverModuleFormat: 'esm',
11 | serverPlatform: 'neutral',
12 | // appDirectory: "app",
13 | // assetsBuildDirectory: "public/build",
14 | // publicPath: "/build/",
15 | tailwind: true,
16 | future: {
17 | v3_relativeSplatPath: true,
18 | },
19 | };
20 |
--------------------------------------------------------------------------------
/guide/remix.env.d.ts:
--------------------------------------------------------------------------------
1 | import '@remix-run/dev';
2 | import '@remix-run/cloudflare';
3 | import '@cloudflare/workers-types';
4 |
5 | interface Env {
6 | ENVIRONMENT?: 'development';
7 | LANGUAGE?: string;
8 | GITHUB_ACCESS_TOKEN?: string;
9 | CF_PAGES_BRANCH?: string;
10 | CACHE: KVNamespace;
11 | }
12 |
13 | declare module '@remix-run/cloudflare' {
14 | export interface AppLoadContext {
15 | env: Env;
16 | waitUntil(promise: Promise): void;
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/guide/server.ts:
--------------------------------------------------------------------------------
1 | import { logDevReady } from '@remix-run/cloudflare';
2 | import { createPagesFunctionHandler } from '@remix-run/cloudflare-pages';
3 | import * as build from '@remix-run/dev/server-build';
4 | import { getLanguageCode } from '~/util';
5 |
6 | if (process.env.NODE_ENV === 'development') {
7 | logDevReady(build);
8 | }
9 |
10 | export const onRequest = createPagesFunctionHandler({
11 | build,
12 | getLoadContext: (context) => ({
13 | env: {
14 | CF_PAGES_BRANCH: 'main',
15 | LANGUAGE: getLanguageCode(context.request.url),
16 | ...context.env,
17 | },
18 | waitUntil(promise: Promise) {
19 | context.waitUntil(promise);
20 | },
21 | }),
22 | mode: process.env.NODE_ENV,
23 | });
24 |
--------------------------------------------------------------------------------
/guide/tailwind.config.cjs:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | content: ['./app/**/*.tsx', './app/**/*.ts'],
3 | plugins: [require('@tailwindcss/typography'), require('@tailwindcss/forms')],
4 | };
5 |
--------------------------------------------------------------------------------
/guide/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "include": ["remix.env.d.ts", "**/*.ts", "**/*.tsx"],
3 | "compilerOptions": {
4 | "lib": ["DOM", "DOM.Iterable", "ES2022"],
5 | "isolatedModules": true,
6 | "esModuleInterop": true,
7 | "jsx": "react-jsx",
8 | "moduleResolution": "Bundler",
9 | "resolveJsonModule": true,
10 | "target": "ES2022",
11 | "strict": true,
12 | "allowJs": true,
13 | "forceConsistentCasingInFileNames": true,
14 | "baseUrl": ".",
15 | "paths": {
16 | "~/*": ["./app/*"]
17 | },
18 | "skipLibCheck": true,
19 |
20 | // Remix takes care of building everything in `remix build`.
21 | "noEmit": true
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/packages/conform-dom/.gitignore:
--------------------------------------------------------------------------------
1 | dist
2 | LICENSE
3 | README.md
4 |
--------------------------------------------------------------------------------
/packages/conform-dom/index.ts:
--------------------------------------------------------------------------------
1 | export {
2 | type Combine,
3 | type Constraint,
4 | type ControlButtonProps,
5 | type FormId,
6 | type FieldName,
7 | type DefaultValue,
8 | type FormValue,
9 | type FormOptions,
10 | type FormState,
11 | type FormContext,
12 | type SubscriptionSubject,
13 | type SubscriptionScope,
14 | createFormContext as unstable_createFormContext,
15 | updateFieldValue as unstable_updateFieldValue,
16 | } from './form';
17 | export { type FieldElement, isFieldElement } from './dom';
18 | export {
19 | type Submission,
20 | type SubmissionResult,
21 | type Intent,
22 | INTENT,
23 | STATE,
24 | serializeIntent,
25 | parse,
26 | } from './submission';
27 | export { getPaths, formatPaths, isPrefix } from './formdata';
28 |
--------------------------------------------------------------------------------
/packages/conform-dom/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@conform-to/dom",
3 | "description": "A set of opinionated helpers built on top of the Constraint Validation API",
4 | "homepage": "https://conform.guide",
5 | "license": "MIT",
6 | "version": "1.6.1",
7 | "main": "./dist/index.js",
8 | "module": "./dist/index.mjs",
9 | "types": "./dist/index.d.ts",
10 | "exports": {
11 | ".": {
12 | "types": "./dist/index.d.ts",
13 | "module": "./dist/index.mjs",
14 | "import": "./dist/index.mjs",
15 | "require": "./dist/index.js",
16 | "default": "./dist/index.mjs"
17 | }
18 | },
19 | "files": [
20 | "./dist/**/*.{js,mjs}",
21 | "./dist/**/*.d.ts"
22 | ],
23 | "scripts": {
24 | "build:js": "rollup -c",
25 | "build:ts": "tsc --project ./tsconfig.build.json",
26 | "build": "pnpm run \"/^build:.*/\"",
27 | "dev:js": "pnpm run build:js --watch",
28 | "dev:ts": "pnpm run build:ts --watch",
29 | "dev": "pnpm run \"/^dev:.*/\"",
30 | "typecheck": "tsc",
31 | "prepare": "pnpm run build"
32 | },
33 | "repository": {
34 | "type": "git",
35 | "url": "https://github.com/edmundhung/conform",
36 | "directory": "packages/conform-dom"
37 | },
38 | "author": {
39 | "name": "Edmund Hung",
40 | "email": "me@edmund.dev",
41 | "url": "https://edmund.dev"
42 | },
43 | "bugs": {
44 | "url": "https://github.com/edmundhung/conform/issues"
45 | },
46 | "keywords": [
47 | "constraint-validation",
48 | "form",
49 | "form-validation",
50 | "html",
51 | "progressive-enhancement",
52 | "validation",
53 | "dom"
54 | ],
55 | "sideEffects": false,
56 | "devDependencies": {
57 | "@babel/core": "^7.17.8",
58 | "@babel/preset-env": "^7.20.2",
59 | "@babel/preset-typescript": "^7.20.2",
60 | "@rollup/plugin-babel": "^5.3.1",
61 | "@rollup/plugin-node-resolve": "^13.3.0",
62 | "rollup-plugin-copy": "^3.4.0",
63 | "rollup": "^2.79.1"
64 | }
65 | }
66 |
--------------------------------------------------------------------------------
/packages/conform-dom/tsconfig.build.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "./tsconfig",
3 | "compilerOptions": {
4 | "outDir": "./dist",
5 | "emitDeclarationOnly": true,
6 | "noEmit": false
7 | },
8 | "exclude": ["**/tests", "dist"]
9 | }
10 |
--------------------------------------------------------------------------------
/packages/conform-dom/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "lib": ["ES2022", "DOM", "DOM.Iterable"],
4 | "target": "ES2022",
5 | "module": "ES2022",
6 | "moduleResolution": "Bundler",
7 | "allowSyntheticDefaultImports": true,
8 | "noUncheckedIndexedAccess": true,
9 | "strict": true,
10 | "declaration": true,
11 | "declarationMap": true,
12 | "composite": true,
13 | "noEmit": true,
14 | "skipLibCheck": true
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/packages/conform-dom/util.ts:
--------------------------------------------------------------------------------
1 | export function invariant(
2 | expectedCondition: boolean,
3 | message: string,
4 | ): asserts expectedCondition {
5 | if (!expectedCondition) {
6 | throw new Error(message);
7 | }
8 | }
9 |
10 | export function generateId(): string {
11 | return (Date.now() * Math.random()).toString(36);
12 | }
13 |
14 | export function clone(data: Data): Data {
15 | return JSON.parse(JSON.stringify(data));
16 | }
17 |
--------------------------------------------------------------------------------
/packages/conform-react/.gitignore:
--------------------------------------------------------------------------------
1 | dist
2 | LICENSE
3 | README.md
4 |
--------------------------------------------------------------------------------
/packages/conform-react/index.ts:
--------------------------------------------------------------------------------
1 | export {
2 | type Submission,
3 | type SubmissionResult,
4 | type DefaultValue,
5 | type Intent,
6 | type FormId,
7 | type FieldName,
8 | parse,
9 | } from '@conform-to/dom';
10 | export {
11 | type FieldMetadata,
12 | type FormMetadata,
13 | FormProvider,
14 | FormStateInput,
15 | } from './context';
16 | export { useForm, useFormMetadata, useField } from './hooks';
17 | export {
18 | Control as unstable_Control,
19 | useControl as unstable_useControl,
20 | useInputControl,
21 | } from './integrations';
22 | export {
23 | getFormProps,
24 | getFieldsetProps,
25 | getInputProps,
26 | getSelectProps,
27 | getTextareaProps,
28 | getCollectionProps,
29 | } from './helpers';
30 |
--------------------------------------------------------------------------------
/packages/conform-react/tsconfig.build.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "./tsconfig",
3 | "compilerOptions": {
4 | "outDir": "./dist",
5 | "emitDeclarationOnly": true,
6 | "noEmit": false
7 | },
8 | "exclude": ["**/tests", "dist"]
9 | }
10 |
--------------------------------------------------------------------------------
/packages/conform-react/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "lib": ["ES2022", "DOM", "DOM.Iterable"],
4 | "target": "ES2022",
5 | "module": "ES2022",
6 | "moduleResolution": "Bundler",
7 | "jsx": "react-jsx",
8 | "allowSyntheticDefaultImports": true,
9 | "noUncheckedIndexedAccess": true,
10 | "strict": true,
11 | "declaration": true,
12 | "declarationMap": true,
13 | "composite": true,
14 | "noEmit": true,
15 | "skipLibCheck": true
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/packages/conform-valibot/.gitignore:
--------------------------------------------------------------------------------
1 | dist
2 | LICENSE
3 | README.md
4 |
--------------------------------------------------------------------------------
/packages/conform-valibot/index.ts:
--------------------------------------------------------------------------------
1 | export { getValibotConstraint } from './constraint';
2 | export { conformValibotMessage, parseWithValibot } from './parse';
3 | export {
4 | coerceFormValue as unstable_coerceFormValue,
5 | type CoercionFunction,
6 | } from './coercion';
7 |
--------------------------------------------------------------------------------
/packages/conform-valibot/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@conform-to/valibot",
3 | "description": "Conform helpers for integrating with Valibot",
4 | "homepage": "https://conform.guide",
5 | "license": "MIT",
6 | "version": "1.6.1",
7 | "main": "./dist/index.js",
8 | "module": "./dist/index.mjs",
9 | "types": "./dist/index.d.ts",
10 | "exports": {
11 | ".": {
12 | "types": "./dist/index.d.ts",
13 | "module": "./dist/index.mjs",
14 | "import": "./dist/index.mjs",
15 | "require": "./dist/index.js",
16 | "default": "./dist/index.mjs"
17 | }
18 | },
19 | "files": [
20 | "./dist/**/*.{js,mjs}",
21 | "./dist/**/*.d.ts"
22 | ],
23 | "scripts": {
24 | "build:js": "rollup -c",
25 | "build:ts": "tsc --project ./tsconfig.build.json",
26 | "build": "pnpm run \"/^build:.*/\"",
27 | "dev:js": "pnpm run build:js --watch",
28 | "dev:ts": "pnpm run build:ts --watch",
29 | "dev": "pnpm run \"/^dev:.*/\"",
30 | "test": "vitest",
31 | "typecheck": "tsc",
32 | "prepare": "pnpm run build"
33 | },
34 | "repository": {
35 | "type": "git",
36 | "url": "https://github.com/edmundhung/conform",
37 | "directory": "packages/conform-valibot"
38 | },
39 | "bugs": {
40 | "url": "https://github.com/edmundhung/conform/issues"
41 | },
42 | "dependencies": {
43 | "@conform-to/dom": "workspace:*"
44 | },
45 | "peerDependencies": {
46 | "valibot": ">= 0.32.0"
47 | },
48 | "devDependencies": {
49 | "@babel/core": "^7.17.8",
50 | "@babel/preset-env": "^7.20.2",
51 | "@babel/preset-typescript": "^7.20.2",
52 | "@rollup/plugin-babel": "^5.3.1",
53 | "@rollup/plugin-node-resolve": "^13.3.0",
54 | "rollup-plugin-copy": "^3.4.0",
55 | "rollup": "^2.79.1",
56 | "valibot": "^1.0.0"
57 | },
58 | "keywords": [
59 | "constraint-validation",
60 | "form",
61 | "form-validation",
62 | "html",
63 | "progressive-enhancement",
64 | "validation",
65 | "valibot"
66 | ],
67 | "sideEffects": false
68 | }
69 |
--------------------------------------------------------------------------------
/packages/conform-valibot/tests/coercion/method/config.test.ts:
--------------------------------------------------------------------------------
1 | import { config, empty, object, pipe, string } from 'valibot';
2 | import { describe, expect, test } from 'vitest';
3 | import { parseWithValibot } from '../../../parse';
4 | import { createFormData } from '../../helpers/FormData';
5 |
6 | describe('config', () => {
7 | test('should fail with message', () => {
8 | const schema = object({
9 | key: config(pipe(string(), empty()), { message: 'wrong value' }),
10 | });
11 | const input = createFormData('key', 'value');
12 | const output = parseWithValibot(input, { schema });
13 | expect(output).toMatchObject({
14 | status: 'error',
15 | error: { key: ['wrong value'] },
16 | });
17 | });
18 | });
19 |
--------------------------------------------------------------------------------
/packages/conform-valibot/tests/coercion/method/fallback.test.ts:
--------------------------------------------------------------------------------
1 | import { empty, fallback, object, pipe, string } from 'valibot';
2 | import { describe, expect, test } from 'vitest';
3 | import { parseWithValibot } from '../../../parse';
4 | import { createFormData } from '../../helpers/FormData';
5 |
6 | describe('fallback', () => {
7 | test('should pass with fallback', () => {
8 | const schema = object({
9 | key: fallback(pipe(string(), empty()), 'fallback value'),
10 | });
11 | const input = createFormData('key', 'value');
12 | const output = parseWithValibot(input, { schema });
13 | expect(output).toMatchObject({
14 | status: 'success',
15 | value: { key: 'fallback value' },
16 | });
17 | });
18 | });
19 |
--------------------------------------------------------------------------------
/packages/conform-valibot/tests/coercion/schema/any.test.ts:
--------------------------------------------------------------------------------
1 | import { any, object } from 'valibot';
2 | import { describe, expect, test } from 'vitest';
3 | import { parseWithValibot } from '../../../parse';
4 | import { createFormData } from '../../helpers/FormData';
5 |
6 | describe('any', () => {
7 | test('should pass any values', () => {
8 | const schema = object({ item: any() });
9 | const input1 = createFormData('item', 'hello');
10 | const output1 = parseWithValibot(input1, { schema });
11 | expect(output1).toMatchObject({
12 | status: 'success',
13 | value: { item: 'hello' },
14 | });
15 | const input2 = createFormData('item', '1');
16 | input2.append('item', '2');
17 | const output2 = parseWithValibot(input2, { schema });
18 | expect(output2).toMatchObject({
19 | status: 'success',
20 | value: { item: ['1', '2'] },
21 | });
22 | });
23 | });
24 |
--------------------------------------------------------------------------------
/packages/conform-valibot/tests/coercion/schema/bigint.test.ts:
--------------------------------------------------------------------------------
1 | import { bigint, object } from 'valibot';
2 | import { describe, expect, test } from 'vitest';
3 | import { parseWithValibot } from '../../../parse';
4 | import { createFormData } from '../../helpers/FormData';
5 |
6 | describe('bigint', () => {
7 | test('should pass only bigint', () => {
8 | const schema = object({ id: bigint() });
9 | const output = parseWithValibot(createFormData('id', '20'), { schema });
10 |
11 | expect(output).toMatchObject({ status: 'success', value: { id: 20n } });
12 | expect(
13 | parseWithValibot(createFormData('id', ''), { schema }),
14 | ).toMatchObject({
15 | error: { id: expect.anything() },
16 | });
17 | expect(
18 | parseWithValibot(createFormData('id', 'non bigint'), { schema }),
19 | ).toMatchObject({
20 | error: { id: expect.anything() },
21 | });
22 | });
23 | });
24 |
--------------------------------------------------------------------------------
/packages/conform-valibot/tests/coercion/schema/blob.test.ts:
--------------------------------------------------------------------------------
1 | import { blob, mimeType, object, pipe } from 'valibot';
2 | import { describe, expect, test } from 'vitest';
3 | import { parseWithValibot } from '../../../parse';
4 | import { createFormData } from '../../helpers/FormData';
5 |
6 | describe('blob', () => {
7 | test('should pass only blob', () => {
8 | const schema = object({ file: blob() });
9 |
10 | const output = parseWithValibot(createFormData('file', new Blob(['foo'])), {
11 | schema,
12 | });
13 |
14 | expect(output).toMatchObject({
15 | status: 'success',
16 | });
17 | expect(
18 | parseWithValibot(createFormData('name', ''), { schema }),
19 | ).toMatchObject({
20 | error: { file: expect.anything() },
21 | });
22 | });
23 |
24 | test('should pass blob with pipe', () => {
25 | const schema = object({
26 | file: pipe(blob(), mimeType(['image/jpeg', 'image/png'])),
27 | });
28 |
29 | const output = parseWithValibot(
30 | createFormData('file', new Blob(['foo'], { type: 'image/jpeg' })),
31 | { schema },
32 | );
33 |
34 | expect(output).toMatchObject({
35 | status: 'success',
36 | });
37 | expect(
38 | parseWithValibot(
39 | createFormData('file', new Blob(['foo'], { type: 'image/gif' })),
40 | { schema },
41 | ),
42 | ).toMatchObject({
43 | error: {
44 | file: expect.anything(),
45 | },
46 | });
47 | });
48 | });
49 |
--------------------------------------------------------------------------------
/packages/conform-valibot/tests/coercion/schema/boolean.test.ts:
--------------------------------------------------------------------------------
1 | import { boolean, object } from 'valibot';
2 | import { describe, expect, test } from 'vitest';
3 | import { parseWithValibot } from '../../../parse';
4 | import { createFormData } from '../../helpers/FormData';
5 |
6 | describe('boolean', () => {
7 | test('should pass only booleans', () => {
8 | const schema = object({ check: boolean() });
9 | const input1 = createFormData('check', 'on');
10 | const output1 = parseWithValibot(input1, { schema });
11 | expect(output1).toMatchObject({
12 | status: 'success',
13 | value: { check: true },
14 | });
15 | expect(
16 | parseWithValibot(createFormData('check', ''), { schema }),
17 | ).toMatchObject({
18 | error: {
19 | check: expect.anything(),
20 | },
21 | });
22 | });
23 | });
24 |
--------------------------------------------------------------------------------
/packages/conform-valibot/tests/coercion/schema/date.test.ts:
--------------------------------------------------------------------------------
1 | import { date, object } from 'valibot';
2 | import { describe, expect, test } from 'vitest';
3 | import { parseWithValibot } from '../../../parse';
4 | import { createFormData } from '../../helpers/FormData';
5 |
6 | describe('date', () => {
7 | test('should pass only dates', () => {
8 | const schema = object({ birthday: date() });
9 | const output = parseWithValibot(createFormData('birthday', '2023-11-19'), {
10 | schema,
11 | });
12 | expect(output).toMatchObject({
13 | status: 'success',
14 | value: { birthday: new Date('2023-11-19') },
15 | });
16 |
17 | expect(
18 | parseWithValibot(createFormData('birthday', 'non date'), { schema }),
19 | ).toMatchObject({
20 | error: {
21 | birthday: expect.anything(),
22 | },
23 | });
24 | });
25 | });
26 |
--------------------------------------------------------------------------------
/packages/conform-valibot/tests/coercion/schema/enum.test.ts:
--------------------------------------------------------------------------------
1 | import { enum_, object } from 'valibot';
2 | import { describe, expect, test } from 'vitest';
3 | import { parseWithValibot } from '../../../parse';
4 | import { createFormData } from '../../helpers/FormData';
5 |
6 | enum Direction {
7 | Left = 'left',
8 | Right = 'right',
9 | }
10 |
11 | describe('enum_', () => {
12 | test('should pass only enum values', () => {
13 | const schema = object({ item: enum_(Direction) });
14 |
15 | const formData1 = createFormData('item', Direction.Left);
16 | const output1 = parseWithValibot(formData1, { schema });
17 | expect(output1).toMatchObject({
18 | status: 'success',
19 | value: { item: Direction.Left },
20 | });
21 |
22 | const formData2 = createFormData('item', Direction.Right);
23 | const output2 = parseWithValibot(formData2, { schema });
24 | expect(output2).toMatchObject({
25 | status: 'success',
26 | value: { item: Direction.Right },
27 | });
28 |
29 | const formData3 = createFormData('item', 'value_3');
30 | const output3 = parseWithValibot(formData3, { schema });
31 | expect(output3).toMatchObject({
32 | error: {
33 | item: expect.anything(),
34 | },
35 | });
36 | });
37 | });
38 |
--------------------------------------------------------------------------------
/packages/conform-valibot/tests/coercion/schema/exactOptional.test.ts:
--------------------------------------------------------------------------------
1 | import { exactOptional, number, object, string } from 'valibot';
2 | import { describe, expect, test } from 'vitest';
3 | import { parseWithValibot } from '../../../parse';
4 | import { createFormData } from '../../helpers/FormData';
5 |
6 | describe('exactOptional', () => {
7 | test('should pass only exactOptional', () => {
8 | const schema = object({ name: string(), age: exactOptional(number()) });
9 | const formData1 = createFormData('name', 'Jane');
10 | expect(parseWithValibot(formData1, { schema })).toMatchObject({
11 | status: 'success',
12 | value: { name: 'Jane' },
13 | });
14 |
15 | formData1.append('age', '20');
16 | expect(parseWithValibot(formData1, { schema })).toMatchObject({
17 | status: 'success',
18 | value: { name: 'Jane', age: 20 },
19 | });
20 |
21 | const formData2 = createFormData('name', 'Jane');
22 | formData2.append('age', 'abc');
23 | expect(parseWithValibot(formData2, { schema })).toMatchObject({
24 | error: { age: expect.anything() },
25 | });
26 | });
27 | });
28 |
--------------------------------------------------------------------------------
/packages/conform-valibot/tests/coercion/schema/exactOptionalAsync.test.ts:
--------------------------------------------------------------------------------
1 | import { exactOptionalAsync, number, objectAsync, string } from 'valibot';
2 | import { describe, expect, test } from 'vitest';
3 | import { parseWithValibot } from '../../../parse';
4 | import { createFormData } from '../../helpers/FormData';
5 |
6 | describe('exactOptionalAsync', () => {
7 | test('should pass only exactOptional', async () => {
8 | const schema = objectAsync({
9 | name: string(),
10 | age: exactOptionalAsync(number()),
11 | });
12 | const formData1 = createFormData('name', 'Jane');
13 | expect(await parseWithValibot(formData1, { schema })).toMatchObject({
14 | status: 'success',
15 | value: { name: 'Jane' },
16 | });
17 |
18 | formData1.append('age', '20');
19 | expect(await parseWithValibot(formData1, { schema })).toMatchObject({
20 | status: 'success',
21 | value: { name: 'Jane', age: 20 },
22 | });
23 |
24 | const formData2 = createFormData('name', 'Jane');
25 | formData2.append('age', 'abc');
26 | expect(await parseWithValibot(formData2, { schema })).toMatchObject({
27 | error: { age: expect.anything() },
28 | });
29 | });
30 | });
31 |
--------------------------------------------------------------------------------
/packages/conform-valibot/tests/coercion/schema/file.test.ts:
--------------------------------------------------------------------------------
1 | import { file, mimeType, object, pipe } from 'valibot';
2 | import { describe, expect, test } from 'vitest';
3 | import { parseWithValibot } from '../../../parse';
4 | import { createFormData } from '../../helpers/FormData';
5 |
6 | describe('file', () => {
7 | test('should pass only file', () => {
8 | const schema = object({ file: file() });
9 |
10 | const output = parseWithValibot(
11 | createFormData('file', new File(['foo'], 'foo.pn')),
12 | {
13 | schema,
14 | },
15 | );
16 |
17 | expect(output).toMatchObject({
18 | status: 'success',
19 | });
20 | expect(
21 | parseWithValibot(createFormData('name', ''), { schema }),
22 | ).toMatchObject({
23 | error: { file: expect.anything() },
24 | });
25 | });
26 |
27 | test('should pass file with pipe', () => {
28 | const schema = object({
29 | file: pipe(file(), mimeType(['image/jpeg', 'image/png'])),
30 | });
31 |
32 | const output = parseWithValibot(
33 | createFormData(
34 | 'file',
35 | new File(['foo'], 'foo.jpeg', { type: 'image/jpeg' }),
36 | ),
37 | { schema },
38 | );
39 |
40 | expect(output).toMatchObject({
41 | status: 'success',
42 | });
43 | expect(
44 | parseWithValibot(
45 | createFormData(
46 | 'file',
47 | new File(['foo'], 'foo.gif', { type: 'image/gif' }),
48 | ),
49 | { schema },
50 | ),
51 | ).toMatchObject({
52 | error: {
53 | file: expect.anything(),
54 | },
55 | });
56 | });
57 | });
58 |
--------------------------------------------------------------------------------
/packages/conform-valibot/tests/coercion/schema/number.test.ts:
--------------------------------------------------------------------------------
1 | import { number, object } from 'valibot';
2 | import { describe, expect, test } from 'vitest';
3 | import { parseWithValibot } from '../../../parse';
4 | import { createFormData } from '../../helpers/FormData';
5 |
6 | describe('number', () => {
7 | test('should pass only numbers', () => {
8 | const schema = object({ age: number() });
9 | const output = parseWithValibot(createFormData('age', '20'), { schema });
10 |
11 | expect(output).toMatchObject({ status: 'success', value: { age: 20 } });
12 | expect(
13 | parseWithValibot(createFormData('age', ''), { schema }),
14 | ).toMatchObject({
15 | error: { age: expect.anything() },
16 | });
17 | expect(
18 | parseWithValibot(createFormData('age', 'non number'), { schema }),
19 | ).toMatchObject({
20 | error: { age: expect.anything() },
21 | });
22 | });
23 | });
24 |
--------------------------------------------------------------------------------
/packages/conform-valibot/tests/coercion/schema/picklist.test.ts:
--------------------------------------------------------------------------------
1 | import { object, picklist } from 'valibot';
2 | import { describe, expect, test } from 'vitest';
3 | import { parseWithValibot } from '../../../parse';
4 | import { createFormData } from '../../helpers/FormData';
5 |
6 | describe('picklist', () => {
7 | test('should pass only picklist values', () => {
8 | const schema = object({ list: picklist(['value_1', 'value_2']) });
9 |
10 | const formData1 = createFormData('list', 'value_1');
11 | const output1 = parseWithValibot(formData1, { schema });
12 | expect(output1).toMatchObject({
13 | status: 'success',
14 | value: { list: 'value_1' },
15 | });
16 |
17 | const formData2 = createFormData('list', 'value_2');
18 | const output2 = parseWithValibot(formData2, { schema });
19 | expect(output2).toMatchObject({
20 | status: 'success',
21 | value: { list: 'value_2' },
22 | });
23 |
24 | const formData3 = createFormData('list', 'value_3');
25 | const output3 = parseWithValibot(formData3, { schema });
26 | expect(output3).toMatchObject({
27 | error: {
28 | list: expect.anything(),
29 | },
30 | });
31 | });
32 | });
33 |
--------------------------------------------------------------------------------
/packages/conform-valibot/tests/coercion/schema/string.test.ts:
--------------------------------------------------------------------------------
1 | import { object, string } from 'valibot';
2 | import { describe, expect, test } from 'vitest';
3 | import { parseWithValibot } from '../../../parse';
4 | import { createFormData } from '../../helpers/FormData';
5 |
6 | describe('string', () => {
7 | test('should pass only strings', () => {
8 | const schema = object({ name: string() });
9 |
10 | const output = parseWithValibot(createFormData('name', 'Jane'), { schema });
11 |
12 | expect(output).toMatchObject({
13 | status: 'success',
14 | value: { name: 'Jane' },
15 | });
16 | expect(
17 | parseWithValibot(createFormData('name', ''), { schema }),
18 | ).toMatchObject({
19 | error: { name: expect.anything() },
20 | });
21 | });
22 | });
23 |
--------------------------------------------------------------------------------
/packages/conform-valibot/tests/coercion/schema/undefined.test.ts:
--------------------------------------------------------------------------------
1 | import { object, string, undefined_ } from 'valibot';
2 | import { describe, expect, test } from 'vitest';
3 | import { parseWithValibot } from '../../../parse';
4 | import { createFormData } from '../../helpers/FormData';
5 |
6 | describe('undefined', () => {
7 | test('should pass only undefined', () => {
8 | const schema = object({ name: string(), age: undefined_() });
9 | const formData1 = createFormData('name', 'Jane');
10 | formData1.append('age', '');
11 | expect(parseWithValibot(formData1, { schema })).toMatchObject({
12 | status: 'success',
13 | value: { name: 'Jane', age: undefined },
14 | });
15 |
16 | const formData2 = createFormData('name', 'Jane');
17 | expect(parseWithValibot(formData2, { schema })).toMatchObject({
18 | error: { age: expect.anything() },
19 | });
20 |
21 | formData2.append('age', '20');
22 | expect(parseWithValibot(formData2, { schema })).toMatchObject({
23 | error: { age: expect.anything() },
24 | });
25 | });
26 | });
27 |
--------------------------------------------------------------------------------
/packages/conform-valibot/tests/coercion/schema/undefinedable.test.ts:
--------------------------------------------------------------------------------
1 | import { number, object, string, undefinedable } from 'valibot';
2 | import { describe, expect, test } from 'vitest';
3 | import { parseWithValibot } from '../../../parse';
4 | import { createFormData } from '../../helpers/FormData';
5 |
6 | describe('undefinedable', () => {
7 | test('should pass only undefinedable', () => {
8 | const schema = object({ name: string(), age: undefinedable(number()) });
9 | const formData1 = createFormData('name', 'Jane');
10 | formData1.append('age', '');
11 | expect(parseWithValibot(formData1, { schema })).toMatchObject({
12 | status: 'success',
13 | value: { name: 'Jane', age: undefined },
14 | });
15 |
16 | const formData2 = createFormData('name', 'Jane');
17 | expect(parseWithValibot(formData2, { schema })).toMatchObject({
18 | error: { age: expect.anything() },
19 | });
20 |
21 | formData2.append('age', '20');
22 | expect(parseWithValibot(formData2, { schema })).toMatchObject({
23 | status: 'success',
24 | value: { name: 'Jane', age: 20 },
25 | });
26 | });
27 | });
28 |
--------------------------------------------------------------------------------
/packages/conform-valibot/tests/coercion/schema/undefinedableAsync.test.ts:
--------------------------------------------------------------------------------
1 | import { number, objectAsync, string, undefinedableAsync } from 'valibot';
2 | import { describe, expect, test } from 'vitest';
3 | import { parseWithValibot } from '../../../parse';
4 | import { createFormData } from '../../helpers/FormData';
5 |
6 | describe('undefinedableAsync', () => {
7 | test('should pass only undefinedable', async () => {
8 | const schema = objectAsync({
9 | name: string(),
10 | age: undefinedableAsync(number()),
11 | });
12 | const formData1 = createFormData('name', 'Jane');
13 | formData1.append('age', '');
14 | expect(await parseWithValibot(formData1, { schema })).toMatchObject({
15 | status: 'success',
16 | value: { name: 'Jane', age: undefined },
17 | });
18 |
19 | const formData2 = createFormData('name', 'Jane');
20 | expect(await parseWithValibot(formData2, { schema })).toMatchObject({
21 | error: { age: expect.anything() },
22 | });
23 |
24 | formData2.append('age', '20');
25 | expect(await parseWithValibot(formData2, { schema })).toMatchObject({
26 | status: 'success',
27 | value: { name: 'Jane', age: 20 },
28 | });
29 | });
30 | });
31 |
--------------------------------------------------------------------------------
/packages/conform-valibot/tests/helpers/FormData.ts:
--------------------------------------------------------------------------------
1 | export function createFormData(key: string, value: string | Blob) {
2 | const formData = new FormData();
3 | formData.append(key, value);
4 | return formData;
5 | }
6 |
--------------------------------------------------------------------------------
/packages/conform-valibot/tests/helpers/valibot.ts:
--------------------------------------------------------------------------------
1 | import type {
2 | GenericSchema,
3 | GenericSchemaAsync,
4 | SafeParseResult,
5 | } from 'valibot';
6 | import { formatPaths } from '@conform-to/dom';
7 |
8 | export function getResult(
9 | result: SafeParseResult,
10 | ):
11 | | { success: false; error: Record }
12 | | { success: true; data: Output } {
13 | if (result.success) {
14 | return { success: true, data: result.output as Output };
15 | }
16 |
17 | const error: Record = {};
18 |
19 | for (const issue of result.issues) {
20 | const name = formatPaths(
21 | issue.path?.map((d) => d.key as string | number) ?? [],
22 | );
23 |
24 | error[name] ??= [];
25 | error[name].push(issue.message);
26 | }
27 |
28 | return { success: false, error };
29 | }
30 |
--------------------------------------------------------------------------------
/packages/conform-valibot/tsconfig.build.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "./tsconfig",
3 | "compilerOptions": {
4 | "outDir": "./dist",
5 | "emitDeclarationOnly": true,
6 | "noEmit": false
7 | },
8 | "exclude": ["**/tests", "dist"]
9 | }
10 |
--------------------------------------------------------------------------------
/packages/conform-valibot/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "lib": ["ES2022", "DOM", "DOM.Iterable"],
4 | "target": "ES2022",
5 | "module": "ES2022",
6 | "moduleResolution": "Bundler",
7 | "allowSyntheticDefaultImports": true,
8 | "noUncheckedIndexedAccess": true,
9 | "strict": true,
10 | "declaration": true,
11 | "declarationMap": true,
12 | "composite": true,
13 | "noEmit": true,
14 | "skipLibCheck": true
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/packages/conform-validitystate/.gitignore:
--------------------------------------------------------------------------------
1 | dist
2 | LICENSE
3 | README.md
4 |
--------------------------------------------------------------------------------
/packages/conform-validitystate/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@conform-to/validitystate",
3 | "description": "Validate on the server using the same rules as the browser",
4 | "homepage": "https://conform.guide",
5 | "license": "MIT",
6 | "version": "0.2.0",
7 | "main": "./dist/index.js",
8 | "module": "./dist/index.mjs",
9 | "types": "./dist/index.d.ts",
10 | "exports": {
11 | ".": {
12 | "types": "./dist/index.d.ts",
13 | "module": "./dist/index.mjs",
14 | "import": "./dist/index.mjs",
15 | "require": "./dist/index.js",
16 | "default": "./dist/index.mjs"
17 | }
18 | },
19 | "files": [
20 | "./dist/**/*.{js,mjs}",
21 | "./dist/**/*.d.ts"
22 | ],
23 | "scripts": {
24 | "build:js": "rollup -c",
25 | "build:ts": "tsc --project ./tsconfig.build.json",
26 | "build": "pnpm run \"/^build:.*/\"",
27 | "dev:js": "pnpm run build:js --watch",
28 | "dev:ts": "pnpm run build:ts --watch",
29 | "dev": "pnpm run \"/^dev:.*/\"",
30 | "typecheck": "tsc",
31 | "prepare": "pnpm run build"
32 | },
33 | "repository": {
34 | "type": "git",
35 | "url": "https://github.com/edmundhung/conform",
36 | "directory": "packages/conform-validitystate"
37 | },
38 | "devDependencies": {
39 | "@babel/core": "^7.17.8",
40 | "@babel/preset-env": "^7.20.2",
41 | "@babel/preset-typescript": "^7.20.2",
42 | "@rollup/plugin-babel": "^5.3.1",
43 | "@rollup/plugin-node-resolve": "^13.3.0",
44 | "rollup-plugin-copy": "^3.4.0",
45 | "rollup": "^2.79.1"
46 | },
47 | "author": {
48 | "name": "Edmund Hung",
49 | "email": "me@edmund.dev",
50 | "url": "https://edmund.dev"
51 | },
52 | "bugs": {
53 | "url": "https://github.com/edmundhung/conform/issues"
54 | },
55 | "keywords": [
56 | "constraint-validation",
57 | "form",
58 | "form-validation",
59 | "progressive-enhancement",
60 | "validation",
61 | "validitystate",
62 | "dom"
63 | ],
64 | "sideEffects": false
65 | }
66 |
--------------------------------------------------------------------------------
/packages/conform-validitystate/tsconfig.build.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "./tsconfig",
3 | "compilerOptions": {
4 | "outDir": "./dist",
5 | "emitDeclarationOnly": true,
6 | "noEmit": false
7 | },
8 | "exclude": ["**/tests", "dist"]
9 | }
10 |
--------------------------------------------------------------------------------
/packages/conform-validitystate/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "lib": ["ES2022", "DOM", "DOM.Iterable"],
4 | "target": "ES2022",
5 | "module": "ES2022",
6 | "moduleResolution": "Bundler",
7 | "allowSyntheticDefaultImports": true,
8 | "strict": true,
9 | "declaration": true,
10 | "declarationMap": true,
11 | "composite": true,
12 | "noEmit": true,
13 | "skipLibCheck": true
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/packages/conform-yup/.gitignore:
--------------------------------------------------------------------------------
1 | dist
2 | LICENSE
3 | README.md
4 |
--------------------------------------------------------------------------------
/packages/conform-yup/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@conform-to/yup",
3 | "description": "Conform helpers for integrating with yup",
4 | "homepage": "https://conform.guide",
5 | "license": "MIT",
6 | "version": "1.6.1",
7 | "main": "./dist/index.js",
8 | "module": "./dist/index.mjs",
9 | "types": "./dist/index.d.ts",
10 | "exports": {
11 | ".": {
12 | "types": "./dist/index.d.ts",
13 | "module": "./dist/index.mjs",
14 | "import": "./dist/index.mjs",
15 | "require": "./dist/index.js",
16 | "default": "./dist/index.mjs"
17 | }
18 | },
19 | "files": [
20 | "./dist/**/*.{js,mjs}",
21 | "./dist/**/*.d.ts"
22 | ],
23 | "scripts": {
24 | "build:js": "rollup -c",
25 | "build:ts": "tsc --project ./tsconfig.build.json",
26 | "build": "pnpm run \"/^build:.*/\"",
27 | "dev:js": "pnpm run build:js --watch",
28 | "dev:ts": "pnpm run build:ts --watch",
29 | "dev": "pnpm run \"/^dev:.*/\"",
30 | "typecheck": "tsc",
31 | "prepare": "pnpm run build"
32 | },
33 | "repository": {
34 | "type": "git",
35 | "url": "https://github.com/edmundhung/conform",
36 | "directory": "packages/conform-yup"
37 | },
38 | "bugs": {
39 | "url": "https://github.com/edmundhung/conform/issues"
40 | },
41 | "dependencies": {
42 | "@conform-to/dom": "workspace:*"
43 | },
44 | "peerDependencies": {
45 | "yup": ">=0.32.0"
46 | },
47 | "devDependencies": {
48 | "@babel/core": "^7.17.8",
49 | "@babel/preset-env": "^7.20.2",
50 | "@babel/preset-typescript": "^7.20.2",
51 | "@rollup/plugin-babel": "^5.3.1",
52 | "@rollup/plugin-node-resolve": "^13.3.0",
53 | "rollup-plugin-copy": "^3.4.0",
54 | "rollup": "^2.79.1",
55 | "yup": "^0.32.11"
56 | },
57 | "keywords": [
58 | "constraint-validation",
59 | "form",
60 | "form-validation",
61 | "html",
62 | "progressive-enhancement",
63 | "validation",
64 | "yup"
65 | ],
66 | "sideEffects": false
67 | }
68 |
--------------------------------------------------------------------------------
/packages/conform-yup/tsconfig.build.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "./tsconfig",
3 | "compilerOptions": {
4 | "outDir": "./dist",
5 | "emitDeclarationOnly": true,
6 | "noEmit": false
7 | },
8 | "exclude": ["**/tests", "dist"]
9 | }
10 |
--------------------------------------------------------------------------------
/packages/conform-yup/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "lib": ["ES2022", "DOM", "DOM.Iterable"],
4 | "target": "ES2022",
5 | "module": "ES2022",
6 | "moduleResolution": "Bundler",
7 | "allowSyntheticDefaultImports": true,
8 | "noUncheckedIndexedAccess": true,
9 | "strict": true,
10 | "declaration": true,
11 | "declarationMap": true,
12 | "composite": true,
13 | "noEmit": true,
14 | "skipLibCheck": true
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/packages/conform-zod/.gitignore:
--------------------------------------------------------------------------------
1 | dist
2 | LICENSE
3 | README.md
4 |
--------------------------------------------------------------------------------
/packages/conform-zod/tests/helpers/FromData.ts:
--------------------------------------------------------------------------------
1 | export function createFormData(
2 | entries: Array<[string, FormDataEntryValue]>,
3 | ): FormData {
4 | const formData = new FormData();
5 |
6 | for (const [name, value] of entries) {
7 | formData.append(name, value);
8 | }
9 |
10 | return formData;
11 | }
12 |
--------------------------------------------------------------------------------
/packages/conform-zod/tests/helpers/zod.ts:
--------------------------------------------------------------------------------
1 | import type { util } from 'zod/v4/core';
2 | import { formatPaths } from '@conform-to/dom';
3 | import { SafeParseReturnType } from 'zod';
4 |
5 | export function getResult(
6 | result: util.SafeParseResult | SafeParseReturnType,
7 | ):
8 | | { success: false; error: Record }
9 | | { success: true; data: Output } {
10 | if (result.success) {
11 | return { success: true, data: result.data };
12 | }
13 |
14 | const error: Record = {};
15 |
16 | for (const issue of result.error.issues) {
17 | const name = formatPaths(
18 | issue.path.map((path) => {
19 | if (typeof path === 'symbol') {
20 | throw new Error(
21 | '@conform-to/zod does not support symbol paths. Please use a string or number instead.',
22 | );
23 | }
24 | return path;
25 | }),
26 | );
27 |
28 | error[name] ??= [];
29 | error[name].push(issue.message);
30 | }
31 |
32 | return { success: false, error };
33 | }
34 |
--------------------------------------------------------------------------------
/packages/conform-zod/tsconfig.build.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "./tsconfig",
3 | "compilerOptions": {
4 | "outDir": "./dist",
5 | "emitDeclarationOnly": true,
6 | "noEmit": false
7 | },
8 | "exclude": ["**/tests", "dist"]
9 | }
10 |
--------------------------------------------------------------------------------
/packages/conform-zod/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "lib": ["ES2022", "DOM", "DOM.Iterable"],
4 | "target": "ES2022",
5 | "module": "ES2022",
6 | "moduleResolution": "Bundler",
7 | "allowSyntheticDefaultImports": true,
8 | "noUncheckedIndexedAccess": true,
9 | "strict": true,
10 | "declaration": true,
11 | "declarationMap": true,
12 | "composite": true,
13 | "noEmit": true,
14 | "skipLibCheck": true
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/packages/conform-zod/v3/index.ts:
--------------------------------------------------------------------------------
1 | export { getZodConstraint } from './constraint';
2 | export { parseWithZod, conformZodMessage } from './parse';
3 | export { coerceFormValue as unstable_coerceFormValue } from './coercion';
4 |
--------------------------------------------------------------------------------
/packages/conform-zod/v3/tests/coercion/schema/bigint.spec.ts:
--------------------------------------------------------------------------------
1 | import { describe, test, expect } from 'vitest';
2 | import { coerceFormValue } from '../../../coercion';
3 | import { z } from 'zod';
4 | import { getResult } from '../../../../tests/helpers/zod';
5 |
6 | describe('coercion', () => {
7 | describe('z.bigint', () => {
8 | test('should pass bigint', () => {
9 | const schema = z
10 | .bigint({ required_error: 'required', invalid_type_error: 'invalid' })
11 | .min(1n, 'min')
12 | .max(10n, 'max')
13 | .multipleOf(2n, 'step');
14 | const file = new File([], '');
15 |
16 | expect(getResult(coerceFormValue(schema).safeParse(''))).toEqual({
17 | success: false,
18 | error: {
19 | '': ['required'],
20 | },
21 | });
22 | expect(getResult(coerceFormValue(schema).safeParse('abc'))).toEqual({
23 | success: false,
24 | error: {
25 | '': ['invalid'],
26 | },
27 | });
28 | expect(getResult(coerceFormValue(schema).safeParse(file))).toEqual({
29 | success: false,
30 | error: {
31 | '': ['invalid'],
32 | },
33 | });
34 | expect(getResult(coerceFormValue(schema).safeParse(' '))).toEqual({
35 | success: false,
36 | error: {
37 | '': ['invalid'],
38 | },
39 | });
40 | expect(getResult(coerceFormValue(schema).safeParse('5'))).toEqual({
41 | success: false,
42 | error: {
43 | '': ['step'],
44 | },
45 | });
46 | expect(getResult(coerceFormValue(schema).safeParse('4'))).toEqual({
47 | success: true,
48 | data: 4n,
49 | });
50 | });
51 | });
52 | });
53 |
--------------------------------------------------------------------------------
/packages/conform-zod/v3/tests/coercion/schema/boolean.spec.ts:
--------------------------------------------------------------------------------
1 | import { describe, test, expect } from 'vitest';
2 | import { coerceFormValue } from '../../../coercion';
3 | import { z } from 'zod';
4 | import { getResult } from '../../../../tests/helpers/zod';
5 |
6 | describe('coercion', () => {
7 | describe('z.boolean', () => {
8 | test('should pass boolean', () => {
9 | const schema = z.boolean({
10 | required_error: 'required',
11 | invalid_type_error: 'invalid',
12 | });
13 | const file = new File([], '');
14 |
15 | expect(getResult(coerceFormValue(schema).safeParse(''))).toEqual({
16 | success: false,
17 | error: {
18 | '': ['required'],
19 | },
20 | });
21 | expect(getResult(coerceFormValue(schema).safeParse(file))).toEqual({
22 | success: false,
23 | error: {
24 | '': ['invalid'],
25 | },
26 | });
27 | expect(getResult(coerceFormValue(schema).safeParse('true'))).toEqual({
28 | success: false,
29 | error: {
30 | '': ['invalid'],
31 | },
32 | });
33 | expect(getResult(coerceFormValue(schema).safeParse('on'))).toEqual({
34 | success: true,
35 | data: true,
36 | });
37 | });
38 | });
39 | });
40 |
--------------------------------------------------------------------------------
/packages/conform-zod/v3/tests/coercion/schema/brand.spec.ts:
--------------------------------------------------------------------------------
1 | import { describe, test, expect } from 'vitest';
2 | import { coerceFormValue } from '../../../coercion';
3 | import { z } from 'zod';
4 | import { getResult } from '../../../../tests/helpers/zod';
5 |
6 | describe('coercion', () => {
7 | describe('z.brand', () => {
8 | test('should pass brand', () => {
9 | const schema = z
10 | .object({
11 | a: z.string().brand(),
12 | b: z.number().brand(),
13 | c: z.boolean().brand(),
14 | d: z.date().brand(),
15 | e: z.bigint().brand(),
16 | f: z.instanceof(File).brand(),
17 | g: z.string().optional().brand(),
18 | h: z.string().brand().optional(),
19 | })
20 | .brand();
21 | const defaultFile = new File(['hello', 'world'], 'example.txt');
22 |
23 | expect(
24 | getResult(
25 | coerceFormValue(schema).safeParse({
26 | a: '',
27 | b: '',
28 | c: '',
29 | d: '',
30 | e: '',
31 | f: '',
32 | g: '',
33 | h: '',
34 | }),
35 | ),
36 | ).toEqual({
37 | success: false,
38 | error: {
39 | a: ['Required'],
40 | b: ['Required'],
41 | c: ['Required'],
42 | d: ['Required'],
43 | e: ['Required'],
44 | f: ['Input not instance of File'],
45 | },
46 | });
47 | expect(
48 | getResult(
49 | coerceFormValue(schema).safeParse({
50 | a: 'hello world',
51 | b: '42',
52 | c: 'on',
53 | d: '1970-01-01',
54 | e: '0x1fffffffffffff',
55 | f: defaultFile,
56 | g: '',
57 | h: '',
58 | }),
59 | ),
60 | ).toEqual({
61 | success: true,
62 | data: {
63 | a: 'hello world',
64 | b: 42,
65 | c: true,
66 | d: new Date('1970-01-01'),
67 | e: BigInt('0x1fffffffffffff'),
68 | f: defaultFile,
69 | g: undefined,
70 | h: undefined,
71 | },
72 | });
73 | });
74 | });
75 | });
76 |
--------------------------------------------------------------------------------
/packages/conform-zod/v3/tests/coercion/schema/catch.spec.ts:
--------------------------------------------------------------------------------
1 | import { describe, test, expect } from 'vitest';
2 | import { coerceFormValue } from '../../../coercion';
3 | import { z } from 'zod';
4 | import { getResult } from '../../../../tests/helpers/zod';
5 |
6 | describe('coercion', () => {
7 | describe('z.catch', () => {
8 | test('should pass catch', () => {
9 | const defaultFile = new File(['hello', 'world'], 'example.txt');
10 | const userFile = new File(['foo', 'bar'], 'foobar.txt');
11 | const defaultDate = new Date(0);
12 | const userDate = new Date(1);
13 | const schema = z.object({
14 | a: z.string().catch('text'),
15 | b: z.number().catch(123),
16 | c: z.boolean().catch(true),
17 | d: z.date().catch(defaultDate),
18 | e: z.instanceof(File).catch(defaultFile),
19 | f: z.array(z.string()).min(1).catch(['foo', 'bar']),
20 | });
21 | const emptyFile = new File([], '');
22 |
23 | expect(
24 | getResult(
25 | coerceFormValue(schema).safeParse({
26 | a: '',
27 | b: '',
28 | c: '',
29 | d: '',
30 | e: emptyFile,
31 | f: '',
32 | }),
33 | ),
34 | ).toEqual({
35 | success: true,
36 | data: {
37 | a: 'text',
38 | b: 123,
39 | c: true,
40 | d: defaultDate,
41 | e: defaultFile,
42 | f: ['foo', 'bar'],
43 | },
44 | });
45 |
46 | expect(
47 | getResult(
48 | coerceFormValue(schema).safeParse({
49 | a: 'othertext',
50 | b: '456',
51 | c: 'on',
52 | d: userDate.toISOString(),
53 | e: userFile,
54 | f: ['hello', 'world'],
55 | }),
56 | ),
57 | ).toEqual({
58 | success: true,
59 | data: {
60 | a: 'othertext',
61 | b: 456,
62 | c: true,
63 | d: userDate,
64 | e: userFile,
65 | f: ['hello', 'world'],
66 | },
67 | });
68 | });
69 | });
70 | });
71 |
--------------------------------------------------------------------------------
/packages/conform-zod/v3/tests/coercion/schema/date.spec.ts:
--------------------------------------------------------------------------------
1 | import { describe, test, expect } from 'vitest';
2 | import { coerceFormValue } from '../../../coercion';
3 | import { z } from 'zod';
4 | import { getResult } from '../../../../tests/helpers/zod';
5 |
6 | describe('coercion', () => {
7 | describe('z.date', () => {
8 | test('should pass date', () => {
9 | const schema = z
10 | .date({
11 | required_error: 'required',
12 | invalid_type_error: 'invalid',
13 | })
14 | .min(new Date(1), 'min')
15 | .max(new Date(10), 'max');
16 | const file = new File([], '');
17 |
18 | expect(getResult(coerceFormValue(schema).safeParse(''))).toEqual({
19 | success: false,
20 | error: {
21 | '': ['required'],
22 | },
23 | });
24 | expect(getResult(coerceFormValue(schema).safeParse('abc'))).toEqual({
25 | success: false,
26 | error: {
27 | '': ['invalid'],
28 | },
29 | });
30 | expect(getResult(coerceFormValue(schema).safeParse(file))).toEqual({
31 | success: false,
32 | error: {
33 | '': ['invalid'],
34 | },
35 | });
36 | expect(getResult(coerceFormValue(schema).safeParse(' '))).toEqual({
37 | success: false,
38 | error: {
39 | '': ['invalid'],
40 | },
41 | });
42 | expect(
43 | getResult(coerceFormValue(schema).safeParse(new Date(0).toISOString())),
44 | ).toEqual({
45 | success: false,
46 | error: {
47 | '': ['min'],
48 | },
49 | });
50 | expect(
51 | getResult(coerceFormValue(schema).safeParse(new Date(5).toISOString())),
52 | ).toEqual({
53 | success: true,
54 | data: new Date(5),
55 | });
56 | });
57 | });
58 | });
59 |
--------------------------------------------------------------------------------
/packages/conform-zod/v3/tests/coercion/schema/discriminatedUnion.spec.ts:
--------------------------------------------------------------------------------
1 | import { describe, test, expect } from 'vitest';
2 | import { coerceFormValue } from '../../../coercion';
3 | import { z } from 'zod';
4 | import { getResult } from '../../../../tests/helpers/zod';
5 |
6 | describe('coercion', () => {
7 | describe('z.discriminatedUnion', () => {
8 | test('should pass discriminatedUnion', () => {
9 | const schema = z.discriminatedUnion('type', [
10 | z.object({
11 | type: z.literal('a'),
12 | number: z.number(),
13 | }),
14 | z.object({
15 | type: z.literal('b'),
16 | boolean: z.boolean(),
17 | }),
18 | ]);
19 |
20 | expect(
21 | getResult(
22 | coerceFormValue(schema).safeParse({
23 | type: 'a',
24 | number: '1',
25 | }),
26 | ),
27 | ).toEqual({
28 | success: true,
29 | data: {
30 | type: 'a',
31 | number: 1,
32 | },
33 | });
34 | expect(
35 | getResult(
36 | coerceFormValue(schema).safeParse({
37 | type: 'b',
38 | boolean: 'on',
39 | }),
40 | ),
41 | ).toEqual({
42 | success: true,
43 | data: {
44 | type: 'b',
45 | boolean: true,
46 | },
47 | });
48 |
49 | expect(getResult(coerceFormValue(schema).safeParse({}))).toEqual({
50 | success: false,
51 | error: {
52 | type: ["Invalid discriminator value. Expected 'a' | 'b'"],
53 | },
54 | });
55 |
56 | const nestedSchema = z.object({
57 | nest: schema,
58 | });
59 | expect(getResult(coerceFormValue(nestedSchema).safeParse({}))).toEqual({
60 | success: false,
61 | error: {
62 | 'nest.type': ["Invalid discriminator value. Expected 'a' | 'b'"],
63 | },
64 | });
65 | });
66 | });
67 | });
68 |
--------------------------------------------------------------------------------
/packages/conform-zod/v3/tests/coercion/schema/file.spec.ts:
--------------------------------------------------------------------------------
1 | import { describe, test, expect } from 'vitest';
2 | import { coerceFormValue } from '../../../coercion';
3 | import { z } from 'zod';
4 | import { getResult } from '../../../../tests/helpers/zod';
5 |
6 | describe('coercion', () => {
7 | describe('z.instanceof(file)', () => {
8 | test('should pass file', () => {
9 | const schema = z.instanceof(File, { message: 'required' });
10 | const emptyFile = new File([], '');
11 | const txtFile = new File(['hello', 'world'], 'example.txt');
12 |
13 | expect(getResult(coerceFormValue(schema).safeParse(''))).toEqual({
14 | success: false,
15 | error: {
16 | '': ['required'],
17 | },
18 | });
19 | expect(
20 | getResult(coerceFormValue(schema).safeParse('helloworld')),
21 | ).toEqual({
22 | success: false,
23 | error: {
24 | '': ['required'],
25 | },
26 | });
27 | expect(getResult(coerceFormValue(schema).safeParse(emptyFile))).toEqual({
28 | success: false,
29 | error: {
30 | '': ['required'],
31 | },
32 | });
33 | expect(getResult(coerceFormValue(schema).safeParse(txtFile))).toEqual({
34 | success: true,
35 | data: txtFile,
36 | });
37 | });
38 | });
39 | });
40 |
--------------------------------------------------------------------------------
/packages/conform-zod/v3/tests/coercion/schema/number.spec.ts:
--------------------------------------------------------------------------------
1 | import { describe, test, expect } from 'vitest';
2 | import { coerceFormValue } from '../../../coercion';
3 | import { z } from 'zod';
4 | import { getResult } from '../../../../tests/helpers/zod';
5 |
6 | describe('coercion', () => {
7 | describe('z.number', () => {
8 | test('should pass numbers', () => {
9 | const schema = z
10 | .number({ required_error: 'required', invalid_type_error: 'invalid' })
11 | .min(1, 'min')
12 | .max(10, 'max')
13 | .step(2, 'step');
14 | const file = new File([], '');
15 |
16 | expect(getResult(coerceFormValue(schema).safeParse(''))).toEqual({
17 | success: false,
18 | error: {
19 | '': ['required'],
20 | },
21 | });
22 | expect(getResult(coerceFormValue(schema).safeParse('abc'))).toEqual({
23 | success: false,
24 | error: {
25 | '': ['invalid'],
26 | },
27 | });
28 | expect(getResult(coerceFormValue(schema).safeParse(file))).toEqual({
29 | success: false,
30 | error: {
31 | '': ['invalid'],
32 | },
33 | });
34 | expect(getResult(coerceFormValue(schema).safeParse('5'))).toEqual({
35 | success: false,
36 | error: {
37 | '': ['step'],
38 | },
39 | });
40 | expect(getResult(coerceFormValue(schema).safeParse(' '))).toEqual({
41 | success: false,
42 | error: {
43 | '': ['invalid'],
44 | },
45 | });
46 | expect(getResult(coerceFormValue(schema).safeParse('6'))).toEqual({
47 | success: true,
48 | data: 6,
49 | });
50 | });
51 | });
52 | });
53 |
--------------------------------------------------------------------------------
/packages/conform-zod/v3/tests/coercion/schema/object.spec.ts:
--------------------------------------------------------------------------------
1 | import { describe, test, expect } from 'vitest';
2 | import { coerceFormValue } from '../../../coercion';
3 | import { z } from 'zod';
4 | import { getResult } from '../../../../tests/helpers/zod';
5 |
6 | describe('coercion', () => {
7 | describe('z.object', () => {
8 | test('should pass object', () => {
9 | const schema = z.object({
10 | a: z.object({
11 | text: z.string({
12 | required_error: 'required',
13 | }),
14 | flag: z
15 | .boolean({
16 | required_error: 'required',
17 | })
18 | .optional(),
19 | }),
20 | b: z
21 | .object({
22 | text: z.string({
23 | required_error: 'required',
24 | }),
25 | flag: z.boolean({
26 | required_error: 'required',
27 | }),
28 | })
29 | .optional(),
30 | });
31 |
32 | expect(getResult(coerceFormValue(schema).safeParse({}))).toEqual({
33 | success: false,
34 | error: {
35 | 'a.text': ['required'],
36 | },
37 | });
38 | expect(
39 | getResult(
40 | coerceFormValue(schema).safeParse({
41 | b: {
42 | text: '',
43 | },
44 | }),
45 | ),
46 | ).toEqual({
47 | success: false,
48 | error: {
49 | 'a.text': ['required'],
50 | 'b.text': ['required'],
51 | 'b.flag': ['required'],
52 | },
53 | });
54 | expect(
55 | getResult(
56 | coerceFormValue(schema).safeParse({
57 | a: {
58 | text: 'foo',
59 | },
60 | b: {
61 | text: 'bar',
62 | flag: 'on',
63 | },
64 | }),
65 | ),
66 | ).toEqual({
67 | success: true,
68 | data: {
69 | a: {
70 | text: 'foo',
71 | },
72 | b: {
73 | text: 'bar',
74 | flag: true,
75 | },
76 | },
77 | });
78 | });
79 | });
80 | });
81 |
--------------------------------------------------------------------------------
/packages/conform-zod/v3/tests/coercion/schema/optional.spec.ts:
--------------------------------------------------------------------------------
1 | import { vi, describe, test, expect } from 'vitest';
2 | import { coerceFormValue } from '../../../coercion';
3 | import { z } from 'zod';
4 | import { getResult } from '../../../../tests/helpers/zod';
5 |
6 | describe('coercion', () => {
7 | describe('z.optional', () => {
8 | test('should pass optional', () => {
9 | const schema = z.object({
10 | a: z.string().optional(),
11 | b: z.number().optional(),
12 | c: z.boolean().optional(),
13 | d: z.date().optional(),
14 | e: z.instanceof(File).optional(),
15 | f: z.array(z.string().optional()),
16 | g: z.array(z.string()).optional(),
17 | });
18 | const emptyFile = new File([], '');
19 |
20 | expect(getResult(coerceFormValue(schema).safeParse({}))).toEqual({
21 | success: true,
22 | data: {
23 | a: undefined,
24 | b: undefined,
25 | c: undefined,
26 | d: undefined,
27 | e: undefined,
28 | f: [],
29 | g: undefined,
30 | },
31 | });
32 | expect(
33 | getResult(
34 | coerceFormValue(schema).safeParse({
35 | a: '',
36 | b: '',
37 | c: '',
38 | d: '',
39 | e: emptyFile,
40 | f: '',
41 | g: '',
42 | }),
43 | ),
44 | ).toEqual({
45 | success: true,
46 | data: {
47 | a: undefined,
48 | b: undefined,
49 | c: undefined,
50 | d: undefined,
51 | e: undefined,
52 | f: [],
53 | g: undefined,
54 | },
55 | });
56 |
57 | // To test if File is not defined in certain environment
58 | vi.stubGlobal('File', undefined);
59 |
60 | expect(() =>
61 | getResult(coerceFormValue(schema).safeParse({})),
62 | ).not.toThrow();
63 | });
64 | });
65 | });
66 |
--------------------------------------------------------------------------------
/packages/conform-zod/v3/tests/coercion/schema/preprocess.spec.ts:
--------------------------------------------------------------------------------
1 | import { describe, test, expect } from 'vitest';
2 | import { coerceFormValue } from '../../../coercion';
3 | import { z } from 'zod';
4 | import { getResult } from '../../../../tests/helpers/zod';
5 |
6 | describe('coercion', () => {
7 | describe('z.preprocess', () => {
8 | test('should pass preprocess', () => {
9 | const schemaWithNoPreprocess = z.number({
10 | invalid_type_error: 'invalid',
11 | });
12 | const schemaWithCustomPreprocess = z.preprocess(
13 | (value) => {
14 | if (typeof value !== 'string') {
15 | return value;
16 | } else if (value === '') {
17 | return undefined;
18 | } else {
19 | return value.replace(/,/g, '');
20 | }
21 | },
22 | z.number({ invalid_type_error: 'invalid' }),
23 | );
24 |
25 | expect(
26 | getResult(coerceFormValue(schemaWithNoPreprocess).safeParse('1,234.5')),
27 | ).toEqual({
28 | success: false,
29 | error: {
30 | '': ['invalid'],
31 | },
32 | });
33 | expect(
34 | getResult(
35 | coerceFormValue(schemaWithCustomPreprocess).safeParse('1,234.5'),
36 | ),
37 | ).toEqual({
38 | success: true,
39 | data: 1234.5,
40 | });
41 | });
42 | });
43 | });
44 |
--------------------------------------------------------------------------------
/packages/conform-zod/v3/tests/coercion/schema/string.spec.ts:
--------------------------------------------------------------------------------
1 | import { describe, test, expect } from 'vitest';
2 | import { coerceFormValue } from '../../../coercion';
3 | import { z } from 'zod';
4 | import { getResult } from '../../../../tests/helpers/zod';
5 |
6 | describe('coercion', () => {
7 | describe('z.string', () => {
8 | test('should pass strings', () => {
9 | const schema = z
10 | .string({ required_error: 'required', invalid_type_error: 'invalid' })
11 | .min(10, 'min')
12 | .max(100, 'max')
13 | .regex(/^[A-Z]{1,100}$/, 'regex')
14 | .refine((value) => value !== 'error', 'refine');
15 | const file = new File([], '');
16 |
17 | expect(getResult(coerceFormValue(schema).safeParse(''))).toEqual({
18 | success: false,
19 | error: {
20 | '': ['required'],
21 | },
22 | });
23 | expect(getResult(coerceFormValue(schema).safeParse(file))).toEqual({
24 | success: false,
25 | error: {
26 | '': ['invalid'],
27 | },
28 | });
29 | expect(getResult(coerceFormValue(schema).safeParse('error'))).toEqual({
30 | success: false,
31 | error: {
32 | '': ['min', 'regex', 'refine'],
33 | },
34 | });
35 | expect(
36 | getResult(coerceFormValue(schema).safeParse('ABCDEFGHIJ')),
37 | ).toEqual({
38 | success: true,
39 | data: 'ABCDEFGHIJ',
40 | });
41 | });
42 | });
43 | });
44 |
--------------------------------------------------------------------------------
/packages/conform-zod/v4/index.ts:
--------------------------------------------------------------------------------
1 | export { getZodConstraint } from './constraint';
2 | export { parseWithZod, conformZodMessage } from './parse';
3 | export { coerceFormValue as unstable_coerceFormValue } from './coercion';
4 |
--------------------------------------------------------------------------------
/playground/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 |
3 | /.cache
4 | /build
5 | /public/build
6 | /app/styles.css
7 | .env
8 |
--------------------------------------------------------------------------------
/playground/README.md:
--------------------------------------------------------------------------------
1 | # Conform Playground
2 |
--------------------------------------------------------------------------------
/playground/app/root.tsx:
--------------------------------------------------------------------------------
1 | import type { MetaFunction, LinksFunction } from '@remix-run/node';
2 | import {
3 | Links,
4 | LiveReload,
5 | Meta,
6 | Outlet,
7 | Scripts,
8 | ScrollRestoration,
9 | } from '@remix-run/react';
10 | import stylesheet from '~/tailwind.css';
11 |
12 | export const links: LinksFunction = () => {
13 | return [{ rel: 'stylesheet', href: stylesheet }];
14 | };
15 |
16 | export const meta: MetaFunction = () => [
17 | {
18 | charset: 'utf-8',
19 | title: 'Conform Playground',
20 | viewport: 'width=device-width,initial-scale=1',
21 | },
22 | ];
23 |
24 | export default function App() {
25 | return (
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 | );
39 | }
40 |
--------------------------------------------------------------------------------
/playground/app/routes/_index.tsx:
--------------------------------------------------------------------------------
1 | import { getFormProps, getInputProps, useForm } from '@conform-to/react';
2 | import { parseWithZod } from '@conform-to/zod';
3 | import type { ActionFunctionArgs, LoaderFunctionArgs } from '@remix-run/node';
4 | import { json } from '@remix-run/node';
5 | import { Form, useActionData, useLoaderData } from '@remix-run/react';
6 | import { z } from 'zod';
7 | import { Playground, Field } from '~/components';
8 |
9 | const schema = z.object({
10 | name: z.string({ required_error: 'Name is required' }),
11 | });
12 |
13 | export async function loader({ request }: LoaderFunctionArgs) {
14 | const url = new URL(request.url);
15 |
16 | return {
17 | noClientValidate: url.searchParams.get('noClientValidate') === 'yes',
18 | };
19 | }
20 |
21 | export async function action({ request }: ActionFunctionArgs) {
22 | const formData = await request.formData();
23 | const submission = parseWithZod(formData, { schema });
24 |
25 | return json(
26 | submission.reply({
27 | fieldErrors: {
28 | name: ['Something went wrong'],
29 | },
30 | }),
31 | );
32 | }
33 |
34 | export default function Example() {
35 | const { noClientValidate } = useLoaderData();
36 | const lastResult = useActionData();
37 | const [form, fields] = useForm({
38 | lastResult,
39 | shouldValidate: 'onBlur',
40 | onValidate: !noClientValidate
41 | ? ({ formData }) => parseWithZod(formData, { schema })
42 | : undefined,
43 | });
44 |
45 | return (
46 |
53 | );
54 | }
55 |
--------------------------------------------------------------------------------
/playground/app/tailwind.css:
--------------------------------------------------------------------------------
1 | @tailwind base;
2 | @tailwind components;
3 | @tailwind utilities;
4 |
5 | @layer components {
6 | input:not([type='checkbox'], [type='radio']),
7 | select,
8 | select[multiple],
9 | textarea {
10 | @apply mt-1 focus:ring-indigo-500 focus:border-indigo-500;
11 | @apply block w-full shadow-sm lg:text-sm border-gray-300 rounded-md;
12 | }
13 |
14 | input[type='checkbox'],
15 | input[type='radio'] {
16 | @apply h-4 w-4 text-indigo-600 focus:ring-indigo-500 border-gray-300 rounded;
17 | }
18 |
19 | section + section {
20 | @apply border-t border-gray-200;
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/playground/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@conform-example/playground",
3 | "private": true,
4 | "sideEffects": false,
5 | "scripts": {
6 | "start": "pnpm run build && remix-serve build/index.js",
7 | "build": "remix build",
8 | "dev": "remix dev",
9 | "typecheck": "tsc"
10 | },
11 | "dependencies": {
12 | "@conform-to/dom": "workspace:*",
13 | "@conform-to/react": "workspace:*",
14 | "@conform-to/validitystate": "workspace:*",
15 | "@conform-to/yup": "workspace:*",
16 | "@conform-to/zod": "workspace:*",
17 | "@headlessui/react": "^1.7.19",
18 | "@headlessui/tailwindcss": "^0.2.0",
19 | "@heroicons/react": "^2.1.3",
20 | "@radix-ui/react-checkbox": "^1.0.4",
21 | "@remix-run/node": "^2.9.1",
22 | "@remix-run/react": "^2.9.1",
23 | "@remix-run/serve": "^2.9.1",
24 | "isbot": "^5.1.5",
25 | "react": "^18.2.0",
26 | "react-dom": "^18.2.0",
27 | "yup": "^0.32.11",
28 | "zod": "^3.25.30"
29 | },
30 | "devDependencies": {
31 | "@remix-run/dev": "^2.9.1",
32 | "@tailwindcss/forms": "^0.5.2",
33 | "@types/react": "^18.3.1",
34 | "@types/react-dom": "^18.3.0",
35 | "autoprefixer": "^10.4.19",
36 | "cross-env": "^7.0.3",
37 | "postcss": "^8.4.38",
38 | "tailwindcss": "^3.4.3",
39 | "typescript": "^5.4.5"
40 | },
41 | "engines": {
42 | "node": "20.x"
43 | },
44 | "type": "module"
45 | }
46 |
--------------------------------------------------------------------------------
/playground/postcss.config.js:
--------------------------------------------------------------------------------
1 | export default {
2 | plugins: {
3 | tailwindcss: {},
4 | autoprefixer: {},
5 | },
6 | };
7 |
--------------------------------------------------------------------------------
/playground/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/edmundhung/conform/2c07a7f91dd56cc75ddb213b8ea321fea6fee142/playground/public/favicon.ico
--------------------------------------------------------------------------------
/playground/remix.config.js:
--------------------------------------------------------------------------------
1 | /** @type {import('@remix-run/dev').AppConfig} */
2 | export default {
3 | ignoredRouteFiles: ['**/.*'],
4 | };
5 |
--------------------------------------------------------------------------------
/playground/remix.env.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 | ///
3 |
--------------------------------------------------------------------------------
/playground/tailwind.config.ts:
--------------------------------------------------------------------------------
1 | import type { Config } from 'tailwindcss';
2 | import form from '@tailwindcss/forms';
3 |
4 | export default {
5 | content: ['./app/**/*.{js,jsx,ts,tsx}'],
6 | theme: {
7 | extend: {},
8 | },
9 | plugins: [form],
10 | } satisfies Config;
11 |
--------------------------------------------------------------------------------
/playground/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "include": ["remix.env.d.ts", "**/*.ts", "**/*.tsx"],
3 | "compilerOptions": {
4 | "lib": ["DOM", "DOM.Iterable", "ES2019"],
5 | "isolatedModules": true,
6 | "skipLibCheck": true,
7 | "esModuleInterop": true,
8 | "jsx": "react-jsx",
9 | "moduleResolution": "node",
10 | "resolveJsonModule": true,
11 | "target": "ES2019",
12 | "strict": true,
13 | "allowJs": false,
14 | "allowSyntheticDefaultImports": true,
15 | "noUncheckedIndexedAccess": true,
16 | "forceConsistentCasingInFileNames": true,
17 | "baseUrl": ".",
18 | "paths": {
19 | "~/*": ["./app/*"]
20 | },
21 |
22 | // Remix takes care of building everything in `remix build`.
23 | "noEmit": true
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/playwright.config.ts:
--------------------------------------------------------------------------------
1 | import { defineConfig, devices } from '@playwright/test';
2 |
3 | /**
4 | * See https://playwright.dev/docs/test-configuration.
5 | */
6 | export default defineConfig({
7 | testDir: './tests/integrations',
8 | /* Run tests in files in parallel */
9 | fullyParallel: true,
10 | /* Fail the build on CI if you accidentally left test.only in the source code. */
11 | forbidOnly: !!process.env.CI,
12 | /* Retry on CI only */
13 | retries: process.env.CI ? 2 : 0,
14 | /* Opt out of parallel tests on CI. */
15 | workers: process.env.CI ? 1 : undefined,
16 | /* Reporter to use. See https://playwright.dev/docs/test-reporters */
17 | reporter: process.env.CI ? 'github' : 'html',
18 | /* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */
19 | use: {
20 | /* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */
21 | trace: 'on-first-retry',
22 | /* Take screenshot on testrun failure. */
23 | screenshot: 'only-on-failure',
24 | },
25 |
26 | /* Configure projects for major browsers */
27 | projects: [
28 | {
29 | name: 'chromium',
30 | use: devices['Desktop Chrome'],
31 | },
32 |
33 | {
34 | name: 'firefox',
35 | use: devices['Desktop Firefox'],
36 | },
37 |
38 | {
39 | name: 'webkit',
40 | use: devices['Desktop Safari'],
41 | },
42 | ],
43 |
44 | /* Run your local dev server before starting the tests */
45 | webServer: {
46 | command: 'pnpm --filter=./playground start',
47 | port: process.env.PORT ? Number(process.env.PORT) : 3000,
48 | reuseExistingServer: !process.env.CI,
49 | stderr: 'pipe',
50 | stdout: 'pipe',
51 | },
52 | });
53 |
--------------------------------------------------------------------------------
/pnpm-workspace.yaml:
--------------------------------------------------------------------------------
1 | packages:
2 | - 'packages/*'
3 | - 'examples/*'
4 | - 'guide'
5 | - 'playground'
6 |
--------------------------------------------------------------------------------
/tests/helpers.ts:
--------------------------------------------------------------------------------
1 | export function createFormData(
2 | entries: Array<[string, FormDataEntryValue]>,
3 | ): FormData {
4 | const formData = new FormData();
5 |
6 | for (const [name, value] of entries) {
7 | formData.append(name, value);
8 | }
9 |
10 | return formData;
11 | }
12 |
--------------------------------------------------------------------------------
/tests/integrations/dom-value.spec.ts:
--------------------------------------------------------------------------------
1 | import { type Page, type Locator, test, expect } from '@playwright/test';
2 | import { getPlayground } from './helpers';
3 |
4 | function getFieldset(form: Locator) {
5 | return {
6 | messageType: form.getByLabel('message', { exact: true }),
7 | titleType: form.getByLabel('title', { exact: true }),
8 | message: form.getByLabel('Message', { exact: true }),
9 | title: form.getByLabel('Title', { exact: true }),
10 | };
11 | }
12 |
13 | async function runTest(page: Page) {
14 | const playground = getPlayground(page);
15 | const fieldset = getFieldset(playground.container);
16 |
17 | await expect.poll(playground.result).toEqual({
18 | value: {
19 | type: 'message',
20 | message: 'Hello',
21 | },
22 | });
23 |
24 | await fieldset.message.fill('Test');
25 | await expect.poll(playground.result).toEqual({
26 | value: {
27 | type: 'message',
28 | message: 'Test',
29 | },
30 | });
31 |
32 | await fieldset.titleType.click();
33 | await expect.poll(playground.result).toEqual({
34 | value: {
35 | type: 'title',
36 | },
37 | });
38 |
39 | await fieldset.title.pressSequentially('foobar');
40 | await expect.poll(playground.result).toEqual({
41 | value: {
42 | type: 'title',
43 | title: 'foobar',
44 | },
45 | });
46 |
47 | await fieldset.messageType.click();
48 | await expect.poll(playground.result).toEqual({
49 | value: {
50 | type: 'message',
51 | message: 'Hello',
52 | },
53 | });
54 | }
55 |
56 | test.describe('With JS', () => {
57 | test('Client Validation', async ({ page }) => {
58 | await page.goto('/dom-value');
59 | await runTest(page);
60 | });
61 |
62 | test('Server Validation', async ({ page }) => {
63 | await page.goto('/dom-value?noClientValidate=yes');
64 | await runTest(page);
65 | });
66 | });
67 |
--------------------------------------------------------------------------------
/tests/integrations/helpers.ts:
--------------------------------------------------------------------------------
1 | import type { Locator, Page } from '@playwright/test';
2 |
3 | export function getPlayground(page: Page) {
4 | const container = page.locator('body');
5 | const submission = container.locator('pre');
6 |
7 | return {
8 | container,
9 | form: container.locator('form'),
10 | submit: container.locator('footer button[type="submit"]'),
11 | reset: container.locator('footer button[type="reset"]'),
12 | submission,
13 | result: () => submission.innerText().then(JSON.parse),
14 | error: container.locator('main p'),
15 | };
16 | }
17 |
18 | export async function selectAll(locator: Locator) {
19 | switch (process.platform) {
20 | case 'darwin':
21 | await locator.press('Meta+a');
22 | break;
23 | default:
24 | await locator.press('Control+a');
25 | }
26 | }
27 |
28 | export async function cut(locator: Locator) {
29 | switch (process.platform) {
30 | case 'darwin':
31 | await locator.press('Meta+x');
32 | break;
33 | default:
34 | await locator.press('Control+x');
35 | }
36 | }
37 |
38 | export async function paste(locator: Locator) {
39 | switch (process.platform) {
40 | case 'darwin':
41 | await locator.press('Meta+v');
42 | break;
43 | default:
44 | await locator.press('Control+v');
45 | }
46 | }
47 |
--------------------------------------------------------------------------------
/tests/integrations/recursive-list.spec.ts:
--------------------------------------------------------------------------------
1 | import { type Page, type Locator, test, expect } from '@playwright/test';
2 | import { getPlayground } from './helpers';
3 |
4 | function getFieldset(form: Locator) {
5 | return {
6 | name: form.getByLabel('Name'),
7 | add: form.getByRole('button', { name: 'Add' }),
8 | delete: form.getByRole('button', { name: 'Delete' }),
9 | };
10 | }
11 |
12 | async function runTest(page: Page) {
13 | const playground = getPlayground(page);
14 | const fieldset = getFieldset(playground.container);
15 |
16 | await expect(fieldset.name).toHaveCount(1);
17 |
18 | await fieldset.add.click();
19 | await expect(fieldset.name).toHaveCount(3);
20 |
21 | await fieldset.name.nth(0).fill('First');
22 | await fieldset.name.nth(1).fill('Second');
23 | await fieldset.name.nth(2).fill('Third');
24 |
25 | // Fix #493: To test if the value are persisted after adding a new field on a nested list
26 | await fieldset.add.nth(1).click();
27 | await expect(fieldset.name.nth(0)).toHaveValue('First');
28 | await expect(fieldset.name.nth(1)).toHaveValue('Second');
29 | await expect(fieldset.name.nth(2)).toHaveValue('Third');
30 | }
31 |
32 | test.describe('With JS', () => {
33 | test('Client Validation', async ({ page }) => {
34 | await page.goto('/recursive-list');
35 | await runTest(page);
36 | });
37 |
38 | test('Server Validation', async ({ page }) => {
39 | await page.goto('/recursive-list?noClientValidate=yes');
40 | await runTest(page);
41 | });
42 | });
43 |
44 | test.describe('No JS', () => {
45 | test.use({ javaScriptEnabled: false });
46 |
47 | test('Validation', async ({ page }) => {
48 | await page.goto('/recursive-list');
49 | await runTest(page);
50 | });
51 | });
52 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "references": [
3 | { "path": "./packages/conform-dom" },
4 | { "path": "./packages/conform-yup" },
5 | { "path": "./packages/conform-zod" },
6 | { "path": "./packages/conform-react" },
7 | { "path": "./packages/conform-validitystate" },
8 | { "path": "./playground" }
9 | ],
10 | "files": []
11 | }
12 |
--------------------------------------------------------------------------------
/vitest.workspace.mts:
--------------------------------------------------------------------------------
1 | import { defineWorkspace } from 'vitest/config';
2 |
3 | export default defineWorkspace([
4 | {
5 | test: {
6 | name: 'browser',
7 | browser: {
8 | enabled: true,
9 | provider: 'playwright',
10 | name: 'chromium',
11 | },
12 | include: ['tests/*.spec.ts'],
13 | },
14 | },
15 | {
16 | test: {
17 | name: 'node',
18 | include: [
19 | 'tests/conform-yup.spec.ts',
20 | ],
21 | environment: 'node',
22 | },
23 | },
24 | 'packages/conform-zod',
25 | 'packages/conform-valibot',
26 | ]);
27 |
--------------------------------------------------------------------------------