├── .all-contributorsrc ├── .eslintrc.cjs ├── .github ├── dependabot.yml └── workflows │ └── tests.yml ├── .gitignore ├── .prettierrc.cjs ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── README_zh-CN.md ├── __tests__ ├── Form.test.tsx ├── TestForm.tsx └── setup.ts ├── package.json ├── pnpm-lock.yaml ├── src ├── FormItem.tsx └── index.ts ├── tea.yaml ├── tsconfig.json ├── tsconfig.node.json ├── tsup.config.ts └── vitest.config.ts /.all-contributorsrc: -------------------------------------------------------------------------------- 1 | { 2 | "files": [ 3 | "README.md" 4 | ], 5 | "imageSize": 100, 6 | "commit": false, 7 | "commitType": "docs", 8 | "commitConvention": "angular", 9 | "contributors": [ 10 | { 11 | "login": "jsun969", 12 | "name": "Yeyang (Justin) Sun", 13 | "avatar_url": "https://avatars.githubusercontent.com/u/29330847?v=4", 14 | "profile": "http://jsun.lol", 15 | "contributions": [ 16 | "code", 17 | "ideas", 18 | "maintenance", 19 | "doc" 20 | ] 21 | }, 22 | { 23 | "login": "jakub-szewczyk", 24 | "name": "Jakub Szewczyk", 25 | "avatar_url": "https://avatars.githubusercontent.com/u/134627903?v=4", 26 | "profile": "https://github.com/jakub-szewczyk", 27 | "contributions": [ 28 | "code" 29 | ] 30 | }, 31 | { 32 | "login": "dmuharemagic", 33 | "name": "Dino Muharemagić", 34 | "avatar_url": "https://avatars.githubusercontent.com/u/2150642?v=4", 35 | "profile": "https://github.com/dmuharemagic", 36 | "contributions": [ 37 | "code", 38 | "bug" 39 | ] 40 | }, 41 | { 42 | "login": "avegatolber", 43 | "name": "avegatolber", 44 | "avatar_url": "https://avatars.githubusercontent.com/u/159487029?v=4", 45 | "profile": "https://github.com/avegatolber", 46 | "contributions": [ 47 | "code", 48 | "bug" 49 | ] 50 | }, 51 | { 52 | "login": "ahmedrowaihi", 53 | "name": "Ahmed Rowaihi", 54 | "avatar_url": "https://avatars.githubusercontent.com/u/67356781?v=4", 55 | "profile": "http://dev.ahmedrowaihi.lol", 56 | "contributions": [ 57 | "code" 58 | ] 59 | }, 60 | { 61 | "login": "yorman2401", 62 | "name": "Yorman Rodriguez", 63 | "avatar_url": "https://avatars.githubusercontent.com/u/66335054?v=4", 64 | "profile": "https://github.com/yorman2401", 65 | "contributions": [ 66 | "bug" 67 | ] 68 | }, 69 | { 70 | "login": "snndmnsz", 71 | "name": "Sinan", 72 | "avatar_url": "https://avatars.githubusercontent.com/u/42818330?v=4", 73 | "profile": "http://snndmnsz.com", 74 | "contributions": [ 75 | "doc" 76 | ] 77 | }, 78 | { 79 | "login": "nagamejun", 80 | "name": "nagamejun", 81 | "avatar_url": "https://avatars.githubusercontent.com/u/18486040?v=4", 82 | "profile": "https://github.com/nagamejun", 83 | "contributions": [ 84 | "bug", 85 | "code" 86 | ] 87 | }, 88 | { 89 | "login": "Anav11", 90 | "name": "Andrey", 91 | "avatar_url": "https://avatars.githubusercontent.com/u/43610000?v=4", 92 | "profile": "https://github.com/Anav11", 93 | "contributions": [ 94 | "bug" 95 | ] 96 | } 97 | ], 98 | "contributorsPerLine": 7, 99 | "skipCi": true, 100 | "repoType": "github", 101 | "repoHost": "https://github.com", 102 | "projectName": "react-hook-form-antd", 103 | "projectOwner": "jsun969" 104 | } 105 | -------------------------------------------------------------------------------- /.eslintrc.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | parser: '@typescript-eslint/parser', 3 | parserOptions: { 4 | project: ['./tsconfig.json', './tsconfig.node.json'], 5 | }, 6 | plugins: ['@typescript-eslint'], 7 | extends: [ 8 | 'plugin:prettier/recommended', 9 | 'plugin:@typescript-eslint/recommended', 10 | 'plugin:react-hooks/recommended', 11 | 'plugin:react/jsx-runtime', 12 | ], 13 | ignorePatterns: ['*.cjs'], 14 | rules: { 15 | '@typescript-eslint/explicit-function-return-type': 'off', 16 | '@typescript-eslint/explicit-module-boundary-types': 'off', 17 | '@typescript-eslint/no-non-null-assertion': 'off', 18 | '@typescript-eslint/no-unused-vars': ['warn', { ignoreRestSiblings: true }], 19 | '@typescript-eslint/consistent-type-imports': 'warn', 20 | 'no-console': 'warn', 21 | }, 22 | }; 23 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # To get started with Dependabot version updates, you'll need to specify which 2 | # package ecosystems to update and where the package manifests are located. 3 | # Please see the documentation for all configuration options: 4 | # https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates 5 | 6 | version: 2 7 | updates: 8 | - package-ecosystem: "npm" 9 | directory: "/" 10 | versioning-strategy: "increase" 11 | groups: 12 | deps: 13 | patterns: 14 | - "*" 15 | schedule: 16 | interval: "monthly" 17 | -------------------------------------------------------------------------------- /.github/workflows/tests.yml: -------------------------------------------------------------------------------- 1 | name: Unit tests 2 | on: 3 | push: 4 | env: 5 | PNPM_VERSION: 9 6 | jobs: 7 | build: 8 | runs-on: ubuntu-20.04 9 | strategy: 10 | matrix: 11 | node-version: [20] 12 | steps: 13 | - uses: actions/checkout@v3 14 | - uses: pnpm/action-setup@v2 15 | with: 16 | version: ${{ env.PNPM_VERSION }} 17 | - name: Use Node.js ${{ matrix.node-version }} 18 | uses: actions/setup-node@v3 19 | with: 20 | node-version: ${{ matrix.node-version }} 21 | cache: 'pnpm' 22 | - name: Install dependencies 23 | run: pnpm install --no-frozen-lockfile 24 | - name: Run tests 25 | run: pnpm run test 26 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | dist/ 3 | 4 | .vitest-preview/ -------------------------------------------------------------------------------- /.prettierrc.cjs: -------------------------------------------------------------------------------- 1 | /** @type {import('prettier').Config & import('@trivago/prettier-plugin-sort-imports').PluginConfig} */ 2 | module.exports = { 3 | singleQuote: true, 4 | trailingComma: 'all', 5 | useTabs: true, 6 | plugins: ['@trivago/prettier-plugin-sort-imports'], 7 | importOrder: ['', '^[./]'], 8 | importOrderSeparation: true, 9 | }; 10 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | We as members, contributors, and leaders pledge to make participation in our 6 | community a harassment-free experience for everyone, regardless of age, body 7 | size, visible or invisible disability, ethnicity, sex characteristics, gender 8 | identity and expression, level of experience, education, socio-economic status, 9 | nationality, personal appearance, race, religion, or sexual identity 10 | and orientation. 11 | 12 | We pledge to act and interact in ways that contribute to an open, welcoming, 13 | diverse, inclusive, and healthy community. 14 | 15 | ## Our Standards 16 | 17 | Examples of behavior that contributes to a positive environment for our 18 | community include: 19 | 20 | - Demonstrating empathy and kindness toward other people 21 | - Being respectful of differing opinions, viewpoints, and experiences 22 | - Giving and gracefully accepting constructive feedback 23 | - Accepting responsibility and apologizing to those affected by our mistakes, 24 | and learning from the experience 25 | - Focusing on what is best not just for us as individuals, but for the 26 | overall community 27 | 28 | Examples of unacceptable behavior include: 29 | 30 | - The use of sexualized language or imagery, and sexual attention or 31 | advances of any kind 32 | - Trolling, insulting or derogatory comments, and personal or political attacks 33 | - Public or private harassment 34 | - Publishing others' private information, such as a physical or email 35 | address, without their explicit permission 36 | - Other conduct which could reasonably be considered inappropriate in a 37 | professional setting 38 | 39 | ## Enforcement Responsibilities 40 | 41 | Community leaders are responsible for clarifying and enforcing our standards of 42 | acceptable behavior and will take appropriate and fair corrective action in 43 | response to any behavior that they deem inappropriate, threatening, offensive, 44 | or harmful. 45 | 46 | Community leaders have the right and responsibility to remove, edit, or reject 47 | comments, commits, code, wiki edits, issues, and other contributions that are 48 | not aligned to this Code of Conduct, and will communicate reasons for moderation 49 | decisions when appropriate. 50 | 51 | ## Scope 52 | 53 | This Code of Conduct applies within all community spaces, and also applies when 54 | an individual is officially representing the community in public spaces. 55 | Examples of representing our community include using an official e-mail address, 56 | posting via an official social media account, or acting as an appointed 57 | representative at an online or offline event. 58 | 59 | ## Enforcement 60 | 61 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 62 | reported to the community leaders responsible for enforcement at 63 | i@jsun.lol. 64 | All complaints will be reviewed and investigated promptly and fairly. 65 | 66 | All community leaders are obligated to respect the privacy and security of the 67 | reporter of any incident. 68 | 69 | ## Enforcement Guidelines 70 | 71 | Community leaders will follow these Community Impact Guidelines in determining 72 | the consequences for any action they deem in violation of this Code of Conduct: 73 | 74 | ### 1. Correction 75 | 76 | **Community Impact**: Use of inappropriate language or other behavior deemed 77 | unprofessional or unwelcome in the community. 78 | 79 | **Consequence**: A private, written warning from community leaders, providing 80 | clarity around the nature of the violation and an explanation of why the 81 | behavior was inappropriate. A public apology may be requested. 82 | 83 | ### 2. Warning 84 | 85 | **Community Impact**: A violation through a single incident or series 86 | of actions. 87 | 88 | **Consequence**: A warning with consequences for continued behavior. No 89 | interaction with the people involved, including unsolicited interaction with 90 | those enforcing the Code of Conduct, for a specified period of time. This 91 | includes avoiding interactions in community spaces as well as external channels 92 | like social media. Violating these terms may lead to a temporary or 93 | permanent ban. 94 | 95 | ### 3. Temporary Ban 96 | 97 | **Community Impact**: A serious violation of community standards, including 98 | sustained inappropriate behavior. 99 | 100 | **Consequence**: A temporary ban from any sort of interaction or public 101 | communication with the community for a specified period of time. No public or 102 | private interaction with the people involved, including unsolicited interaction 103 | with those enforcing the Code of Conduct, is allowed during this period. 104 | Violating these terms may lead to a permanent ban. 105 | 106 | ### 4. Permanent Ban 107 | 108 | **Community Impact**: Demonstrating a pattern of violation of community 109 | standards, including sustained inappropriate behavior, harassment of an 110 | individual, or aggression toward or disparagement of classes of individuals. 111 | 112 | **Consequence**: A permanent ban from any sort of public interaction within 113 | the community. 114 | 115 | ## Attribution 116 | 117 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], 118 | version 2.0, available at 119 | https://www.contributor-covenant.org/version/2/0/code_of_conduct.html. 120 | 121 | Community Impact Guidelines were inspired by [Mozilla's code of conduct 122 | enforcement ladder](https://github.com/mozilla/diversity). 123 | 124 | [homepage]: https://www.contributor-covenant.org 125 | 126 | For answers to common questions about this code of conduct, see the FAQ at 127 | https://www.contributor-covenant.org/faq. Translations are available at 128 | https://www.contributor-covenant.org/translations. 129 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to `React Hook Form Antd` 2 | 3 | As the creators and maintainers of this project, we want to ensure that `react-hook-form-antd` lives and continues to grow and evolve. We would like to encourage everyone to help and support this library by contributing. 4 | 5 | ## Code contributions 6 | 7 | Here is a quick guide to doing code contributions to the library. 8 | 9 | 1. Fork and clone the repo to your local machine. 10 | 11 | ```bash 12 | git clone https://github.com/YOUR_GITHUB_USERNAME/react-hook-form-antd.git 13 | ``` 14 | 15 | 2. Create a new branch from `main` with a meaningful name for a new feature or an issue you want to work on. 16 | 17 | ```bash 18 | git checkout -b your-meaningful-branch-name 19 | ``` 20 | 21 | 3. Install packages by running. 22 | 23 | ```bash 24 | pnpm install 25 | ``` 26 | 27 | 4. Test your code. 28 | 29 | ```bash 30 | pnpm run test 31 | ``` 32 | 33 | 5. Ensure your code lints without errors. 34 | 35 | ``` 36 | pnpm run lint 37 | ``` 38 | 39 | 6. Ensure build passes. 40 | 41 | ```bash 42 | pnpm run build 43 | ``` 44 | 45 | 7. Push your branch. 46 | 47 | ```bash 48 | git push -u origin your-meaningful-branch-name 49 | ``` 50 | 51 | 8. Submit a pull request to the upstream react-hook-form-antd repository. 52 | 53 | 9. Choose a descriptive title and describe your changes briefly. 54 | 55 | ## Coding style 56 | 57 | Please follow the coding style of the project. React Hook Form Antd uses eslint and prettier. If possible, enable their respective plugins in your editor to get real-time feedback. The linting can be run manually with the following command: `pnpm run format` and `pnpm run lint` 58 | 59 | ## License 60 | 61 | By contributing your code to the react-hook-form-antd GitHub repository, you agree to license your contribution under the MIT license. 62 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023-present Yeyang (Justin) Sun 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 | # 📋 React Hook Form Antd 🐜 4 | 5 | Master your Ant Design form with React Hook Form! 6 | 7 | [![version](https://img.shields.io/npm/v/react-hook-form-antd?style=for-the-badge)](https://www.npmjs.com/package/react-hook-form-antd) 8 | [![license](https://img.shields.io/npm/l/react-hook-form-antd?style=for-the-badge)](https://github.com/jsun969/react-hook-form-antd/blob/main/LICENSE) 9 | [![size](https://img.shields.io/bundlephobia/minzip/react-hook-form-antd?style=for-the-badge)](https://bundlephobia.com/result?p=react-hook-form-antd) 10 | [![downloads](https://img.shields.io/npm/dw/react-hook-form-antd?style=for-the-badge)](https://www.npmjs.com/package/react-hook-form-antd) 11 | 12 | English | [简体中文](./README_zh-CN.md) 13 | 14 |
15 | 16 | ## 📜 Requirement 17 | 18 | - [react-hook-form](https://github.com/react-hook-form/react-hook-form) `^7` 19 | - [antd](https://github.com/ant-design/ant-design) `^5` 20 | 21 | ## 🕶 Example 22 | 23 | [EXAMPLE](https://codesandbox.io/s/react-hook-form-antd-example-6s0i3z?file=/src/App.tsx) 24 | 25 | ## 📦 Installation 26 | 27 | ```bash 28 | npm install react-hook-form-antd 29 | ``` 30 | 31 | ## 🎯 Quickstart 32 | 33 | You may have an original antd form like below 34 | 35 |
36 | Show code 37 | 38 | ```tsx 39 |
40 | 48 | 49 | 50 | 55 | 56 | 57 | 58 | Remember me 59 | 60 | 61 | 64 | 65 |
66 | ``` 67 | 68 |
69 | 70 | Check the [EXAMPLE](https://codesandbox.io/s/react-hook-form-antd-example-6s0i3z?file=/src/App.tsx) for this form after using `react-hook-form-antd`! 71 | 72 | All you need to do: 73 | 74 | 1. Use `useForm` from `react-hook-form` and get `control` 75 | 2. Use `FormItem` from `react-hook-form-antd` instead of `Form.Item` 76 | - Pass `control` to all `FormItem` (Field names can be inferred by `control` 😎) 77 | - Remove `rules` and use [react hook form resolver](https://github.com/react-hook-form/resolvers) instead (You can use schema from any validation libraries 🤩) 78 | - Use `handleSubmit` in `onFinish` 79 | 3. Enjoy! 🎉 80 | 81 | ## 🕹 API 82 | 83 | ### 🔗 `FormItem` 84 | 85 | > Ant Design `Form.Item` [API](https://ant.design/components/form#formitem) 86 | 87 | A component instead of `Form.Item` in antd. It has inherited all props from `Form.Item` except `rules` `validateStatus` (If you need rules, please use [react hook form resolver](https://github.com/react-hook-form/resolvers) instead) 88 | 89 | Added and modified props: 90 | 91 | | Prop | Type | Description | 92 | | --------- | ------- | --------------------------------------------------------------------------------- | 93 | | `control` | Control | [control object](https://react-hook-form.com/api/useform/control/) from `useForm` | 94 | | `name` | string | form field name | 95 | 96 | ## 🚧 Known Issues 97 | 98 | #### TypeError: elm.focus is not a function 99 | 100 | When using an upload component, set `shouldFocusError: false` in your `useForm` configuration. This will prevent React Hook Form from trying to focus on the error, effectively resolving the issue. 101 | 102 | ## 👥 Contributors 103 | 104 | Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/docs/en/emoji-key)): 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 |
Yeyang (Justin) Sun
Yeyang (Justin) Sun

💻 🤔 🚧 📖
Jakub Szewczyk
Jakub Szewczyk

💻
Dino Muharemagić
Dino Muharemagić

💻 🐛
avegatolber
avegatolber

💻 🐛
Ahmed Rowaihi
Ahmed Rowaihi

💻
Yorman Rodriguez
Yorman Rodriguez

🐛
Sinan
Sinan

📖
nagamejun
nagamejun

🐛 💻
Andrey
Andrey

🐛
126 | 127 | 128 | 129 | 130 | 131 | 132 | This project follows the [all-contributors](https://github.com/all-contributors/all-contributors) specification. Contributions of any kind welcome! 133 | -------------------------------------------------------------------------------- /README_zh-CN.md: -------------------------------------------------------------------------------- 1 |
2 | 3 | # 📋🐜 React Hook Form Antd 4 | 5 | 用 React Hook Form 拿捏你的 Ant Design 表单! 6 | 7 | [![version](https://img.shields.io/npm/v/react-hook-form-antd?style=for-the-badge)](https://www.npmjs.com/package/react-hook-form-antd) 8 | [![license](https://img.shields.io/npm/l/react-hook-form-antd?style=for-the-badge)](https://github.com/jsun969/react-hook-form-antd/blob/main/LICENSE) 9 | [![size](https://img.shields.io/bundlephobia/minzip/react-hook-form-antd?style=for-the-badge)](https://bundlephobia.com/result?p=react-hook-form-antd) 10 | [![downloads](https://img.shields.io/npm/dw/react-hook-form-antd?style=for-the-badge)](https://www.npmjs.com/package/react-hook-form-antd) 11 | 12 | [English](./README.md) | 简体中文 13 | 14 |
15 | 16 | ## 📜 依赖 17 | 18 | - [react-hook-form](https://github.com/react-hook-form/react-hook-form) `^7` 19 | - [antd](https://github.com/ant-design/ant-design) `^5` 20 | 21 | ## 🕶 示例 22 | 23 | [示例](https://codesandbox.io/s/react-hook-form-antd-example-6s0i3z?file=/src/App.tsx) 24 | 25 | ## 📦 安装 26 | 27 | ```bash 28 | npm install react-hook-form-antd 29 | ``` 30 | 31 | ## 🎯 快速上手 32 | 33 | 你可能原本有一个如下的 antd 表单 34 | 35 |
36 | 显示代码 37 | 38 | ```tsx 39 |
40 | 48 | 49 | 50 | 55 | 56 | 57 | 58 | Remember me 59 | 60 | 61 | 64 | 65 |
66 | ``` 67 | 68 |
69 | 70 | 查看这个表单在使用 `react-hook-form-antd` 后的[示例](https://codesandbox.io/s/react-hook-form-antd-example-6s0i3z?file=/src/App.tsx) 71 | 72 | 你只需要: 73 | 74 | 1. 从 `react-hook-form` 中使用 `useForm` 并获取 `control` 75 | 2. 从 `react-hook-form-antd` 中使用 `FormItem` 代替 `Form.Item` 76 | - 将 `control` 传递给所有的 `FormItem` (字段名能从 `control` 中推断 😎) 77 | - 移除 `rules` 并使用 [react hook form resolver](https://github.com/react-hook-form/resolvers) 替代 (你可以使用任何验证库的 schema 以验证表单 🤩) 78 | - 在 `onFinish` 中使用 `handleSubmit` 79 | 3. 搞定! 🎉 80 | 81 | ## 🕹 API 82 | 83 | ### 🔗 `FormItem` 84 | 85 | > Ant Design `Form.Item` [API](https://ant.design/components/form#formitem) 86 | 87 | 一个用于替代 antd 中的 `Form.Item` 的组件。它已经继承了 `Form.Item` 中除了 `rules` `validateStatus` 之外的所有参数(如果你需要 rules,请使用 [react hook form resolver](https://github.com/react-hook-form/resolvers) 替代) 88 | 89 | 新增和修改的参数: 90 | 91 | | 参数 | 类型 | 描述 | 92 | | --------- | ------- | ---------------------------------------------------------------------------------- | 93 | | `control` | Control | 来自 `useForm` 的 [control 对象](https://react-hook-form.com/api/useform/control/) | 94 | | `name` | string | 表单字段名 | 95 | -------------------------------------------------------------------------------- /__tests__/Form.test.tsx: -------------------------------------------------------------------------------- 1 | import { cleanup, render, screen } from '@testing-library/react'; 2 | import userEvent from '@testing-library/user-event'; 3 | import type { Mock } from 'vitest'; 4 | import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; 5 | 6 | import type { TestFormData } from './TestForm'; 7 | import TestForm from './TestForm'; 8 | 9 | const clickSubmit = async () => { 10 | await userEvent.click(screen.getByText('Submit')); 11 | }; 12 | let submitFn: Mock<[data: TestFormData], TestFormData>; 13 | 14 | describe('Form', () => { 15 | beforeEach(() => { 16 | // Restore mock function manually 17 | submitFn = vi.fn((data: TestFormData) => data); 18 | render(); 19 | }); 20 | afterEach(() => { 21 | // FIXME: Restore all mocks will make mock function undefined 22 | // vi.restoreAllMocks(); 23 | cleanup(); 24 | }); 25 | it('should show error message when password is empty', async () => { 26 | await clickSubmit(); 27 | expect(screen.getByTestId('password-field')).toHaveTextContent( 28 | 'Password is required', 29 | ); 30 | expect(submitFn).not.toBeCalled(); 31 | }); 32 | it('should show error message when username is too long', async () => { 33 | await userEvent.type( 34 | screen.getByLabelText('Username'), 35 | 'looooooooooooooooong', 36 | ); 37 | await clickSubmit(); 38 | expect(screen.getByTestId('username-field')).toHaveTextContent( 39 | 'Username should be less than 15 characters', 40 | ); 41 | expect(submitFn).not.toBeCalled(); 42 | }); 43 | it('should submit when validation passed', async () => { 44 | await userEvent.type(screen.getByLabelText('Password'), 'password'); 45 | await clickSubmit(); 46 | expect(submitFn).toBeCalledWith({ 47 | username: 'jsun969', 48 | password: 'password', 49 | remember: true, 50 | }); 51 | }); 52 | it('should reset when click reset button', async () => { 53 | await userEvent.type(screen.getByLabelText('Password'), 'password'); 54 | const passwordInputBeforeReset = screen.getByLabelText( 55 | 'Password', 56 | ) as HTMLInputElement; 57 | expect(passwordInputBeforeReset.value).toBe('password'); 58 | await userEvent.click(screen.getByText('Reset')); 59 | const passwordInput = screen.getByLabelText('Password') as HTMLInputElement; 60 | expect(passwordInput.value).toBe(''); 61 | }); 62 | }); 63 | -------------------------------------------------------------------------------- /__tests__/TestForm.tsx: -------------------------------------------------------------------------------- 1 | import { zodResolver } from '@hookform/resolvers/zod'; 2 | import { Button, Checkbox, Form, Input } from 'antd'; 3 | import { useForm } from 'react-hook-form'; 4 | import z from 'zod'; 5 | 6 | import { FormItem } from '../src'; 7 | 8 | const schema = z.object({ 9 | username: z 10 | .string() 11 | .min(1, { message: 'Username is required' }) 12 | .max(15, { message: 'Username should be less than 15 characters' }), 13 | password: z.string().min(1, { message: 'Password is required' }), 14 | remember: z.boolean(), 15 | }); 16 | export type TestFormData = z.infer; 17 | 18 | const TestForm = ({ submitFn }: { submitFn: (data: TestFormData) => void }) => { 19 | const { control, watch, handleSubmit, reset } = useForm({ 20 | defaultValues: { username: 'jsun969', password: '', remember: true }, 21 | resolver: zodResolver(schema), 22 | }); 23 | 24 | return ( 25 |
{ 28 | submitFn?.(data); 29 | })} 30 | > 31 | 38 | 39 | 40 | 47 | 48 | 49 | 50 | Remember me 51 | 52 | 53 | 56 | 59 | 60 |
61 | ); 62 | }; 63 | export default TestForm; 64 | -------------------------------------------------------------------------------- /__tests__/setup.ts: -------------------------------------------------------------------------------- 1 | import '@testing-library/jest-dom/vitest'; 2 | import { vi } from 'vitest'; 3 | 4 | Object.defineProperty(window, 'matchMedia', { 5 | writable: true, 6 | value: vi.fn().mockImplementation((query) => ({ 7 | matches: false, 8 | media: query, 9 | onchange: null, 10 | addListener: vi.fn(), // deprecated 11 | removeListener: vi.fn(), // deprecated 12 | addEventListener: vi.fn(), 13 | removeEventListener: vi.fn(), 14 | dispatchEvent: vi.fn(), 15 | })), 16 | }); 17 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-hook-form-antd", 3 | "version": "1.1.3", 4 | "description": "Master your And Design form with React Hook Form!", 5 | "type": "module", 6 | "main": "./dist/index.js", 7 | "exports": { 8 | "require": { 9 | "types": "./dist/index.d.cts", 10 | "default": "./dist/index.cjs" 11 | }, 12 | "import": { 13 | "types": "./dist/index.d.ts", 14 | "default": "./dist/index.js" 15 | } 16 | }, 17 | "files": [ 18 | "dist", 19 | "LICENSE", 20 | "README.md" 21 | ], 22 | "sideEffects": false, 23 | "scripts": { 24 | "prepare": "simple-git-hooks", 25 | "prepublishOnly": "pnpm run build", 26 | "format": "prettier --write \"**/*.{js,ts,tsx,md}\"", 27 | "lint": "eslint \"**/*.{js,ts,tsx}\" --fix", 28 | "dev": "tsup --watch", 29 | "build": "tsup", 30 | "test": "vitest", 31 | "test:preview": "vitest-preview" 32 | }, 33 | "keywords": [ 34 | "react", 35 | "form", 36 | "react-hook-form", 37 | "ant-design" 38 | ], 39 | "repository": "github:jsun969/react-hook-form-antd", 40 | "author": "Yeyang (Justin) Sun", 41 | "license": "MIT", 42 | "devDependencies": { 43 | "@hookform/resolvers": "^3.3.4", 44 | "@testing-library/jest-dom": "^6.4.2", 45 | "@testing-library/react": "^14.2.1", 46 | "@testing-library/user-event": "^14.5.2", 47 | "@trivago/prettier-plugin-sort-imports": "^4.3.0", 48 | "@types/react": "^18.2.61", 49 | "@types/react-dom": "^18.2.19", 50 | "@typescript-eslint/eslint-plugin": "^7.1.0", 51 | "@typescript-eslint/parser": "^7.1.0", 52 | "antd": "^5.14.2", 53 | "eslint": "^8.57.0", 54 | "eslint-config-prettier": "^9.1.0", 55 | "eslint-plugin-prettier": "^5.1.3", 56 | "eslint-plugin-react": "^7.32.2", 57 | "eslint-plugin-react-hooks": "^4.6.0", 58 | "jsdom": "^24.0.0", 59 | "lint-staged": "^15.2.2", 60 | "prettier": "^3.2.5", 61 | "react": "^18.2.0", 62 | "react-dom": "^18.2.0", 63 | "react-hook-form": "^7.51.0", 64 | "simple-git-hooks": "^2.9.0", 65 | "tsup": "^8.0.2", 66 | "typescript": "^5.3.3", 67 | "vitest": "^1.3.1", 68 | "vitest-preview": "^0.0.1", 69 | "zod": "^3.22.4" 70 | }, 71 | "peerDependencies": { 72 | "antd": "^5", 73 | "react": "^16.8.0 || ^17 || ^18", 74 | "react-dom": "^16.8.0 || ^17 || ^18", 75 | "react-hook-form": "^7" 76 | }, 77 | "lint-staged": { 78 | "*.{js,ts,tsx,css,md}": [ 79 | "prettier --write" 80 | ] 81 | }, 82 | "simple-git-hooks": { 83 | "pre-commit": "npx lint-staged" 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /src/FormItem.tsx: -------------------------------------------------------------------------------- 1 | import { Form as AntdForm } from 'antd'; 2 | import { Children, cloneElement, isValidElement, useEffect } from 'react'; 3 | import type { Control, FieldPath, FieldValues } from 'react-hook-form'; 4 | import { useController } from 'react-hook-form'; 5 | 6 | type AntdFormItemProps = React.ComponentProps; 7 | 8 | export type FormItemProps = { 9 | children: React.ReactNode; 10 | control: Control; 11 | name: FieldPath; 12 | disabled?: boolean; 13 | overrideFieldOnChange?: (...values: any[]) => void; 14 | } & Omit; 15 | 16 | // TODO: Support `onBlur` `ref` `reset` 17 | export const FormItem = ({ 18 | children, 19 | control, 20 | name, 21 | disabled, 22 | help, 23 | valuePropName, 24 | overrideFieldOnChange, 25 | ...props 26 | }: FormItemProps) => { 27 | const { field, fieldState } = useController({ name, control, disabled }); 28 | const form = AntdForm.useFormInstance(); 29 | 30 | useEffect(() => { 31 | form.setFieldValue(name, field.value); 32 | }, [field.value, form, name]); 33 | 34 | return ( 35 | 43 | {Children.map( 44 | children, 45 | (child) => 46 | isValidElement(child) && 47 | cloneElement(child, { 48 | ...field, 49 | //@ts-expect-error onChange type safe is not necessary here 50 | onChange: (...params) => { 51 | child.props.onChange && child.props.onChange(...params); 52 | overrideFieldOnChange 53 | ? overrideFieldOnChange(...params) 54 | : field.onChange(...params); 55 | }, 56 | onBlur: () => { 57 | child.props.onBlur && child.props.onBlur(); 58 | field.onBlur(); 59 | }, 60 | ...(valuePropName && { 61 | [valuePropName]: field.value, 62 | }), 63 | }), 64 | )} 65 | 66 | ); 67 | }; 68 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './FormItem'; 2 | -------------------------------------------------------------------------------- /tea.yaml: -------------------------------------------------------------------------------- 1 | # https://tea.xyz/what-is-this-file 2 | --- 3 | version: 1.0.0 4 | codeOwners: 5 | - '0xA0b36305f2b1d037054644cccc0b5D7325F3F973' 6 | quorum: 1 7 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "sourceMap": true, 4 | "target": "ES6", 5 | "module": "ESNext", 6 | "moduleResolution": "Node", 7 | "jsx": "react-jsx", 8 | "skipLibCheck": true, 9 | "noEmit": true, 10 | "allowSyntheticDefaultImports": true, 11 | "lib": ["DOM", "DOM.Iterable", "ESNext"], 12 | "strict": true, 13 | "forceConsistentCasingInFileNames": true, 14 | "resolveJsonModule": true 15 | }, 16 | "include": ["src", "__tests__"] 17 | } 18 | -------------------------------------------------------------------------------- /tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "composite": true, 4 | "module": "ESNext", 5 | "moduleResolution": "Node", 6 | "allowSyntheticDefaultImports": true 7 | }, 8 | "include": ["tsup.config.ts", "vitest.config.ts"] 9 | } 10 | -------------------------------------------------------------------------------- /tsup.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'tsup'; 2 | 3 | export default defineConfig({ 4 | entry: ['src/index.ts'], 5 | format: ['esm', 'cjs'], 6 | platform: 'browser', 7 | dts: true, 8 | splitting: false, 9 | clean: true, 10 | sourcemap: true, 11 | }); 12 | -------------------------------------------------------------------------------- /vitest.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vitest/config'; 2 | 3 | export default defineConfig({ 4 | test: { 5 | environment: 'jsdom', 6 | setupFiles: ['./__tests__/setup.ts'], 7 | css: true, 8 | }, 9 | }); 10 | --------------------------------------------------------------------------------