├── .changeset └── config.json ├── .eslintignore ├── .eslintrc.js ├── .github └── workflows │ ├── main.yml │ └── release.yml ├── .gitignore ├── .npmrc ├── .prettierrc.js ├── .vscode └── settings.json ├── LICENSE ├── README.md ├── jest.config.js ├── package.json ├── rollup.config.js ├── rollup ├── createRollupConfig.js ├── pascalcase.js ├── safePackageName.js └── writeCjsEntryFile.js ├── src ├── __snapshots__ │ └── useTypedController.test.tsx.snap ├── index.ts ├── logic │ ├── formatName.test.ts │ └── formatName.ts ├── types.ts ├── useTypedController.test.tsx └── useTypedController.tsx ├── tsconfig.json └── yarn.lock /.changeset/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://unpkg.com/@changesets/config@1.3.0/schema.json", 3 | "changelog": [ 4 | "@changesets/changelog-github", 5 | { "repo": "react-hook-form/strictly-typed" } 6 | ], 7 | "commit": false, 8 | "linked": [], 9 | "access": "public", 10 | "baseBranch": "master", 11 | "ignore": [] 12 | } 13 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | /node_modules 2 | /dist 3 | /esm 4 | /coverage 5 | !.*.js 6 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | parser: '@typescript-eslint/parser', 3 | plugins: ['@typescript-eslint', 'react-hooks'], 4 | extends: [ 5 | 'plugin:react/recommended', 6 | 'plugin:@typescript-eslint/recommended', 7 | 'prettier/@typescript-eslint', 8 | 'plugin:prettier/recommended', 9 | ], 10 | parser: '@typescript-eslint/parser', 11 | parserOptions: { 12 | ecmaVersion: 2020, 13 | sourceType: 'module', 14 | ecmaFeatures: { 15 | jsx: true, 16 | }, 17 | }, 18 | rules: { 19 | '@typescript-eslint/no-explicit-any': 'off', 20 | '@typescript-eslint/ban-types': 'off', 21 | '@typescript-eslint/explicit-module-boundary-types': 'off', 22 | 'react-hooks/rules-of-hooks': 'error', 23 | 'react-hooks/exhaustive-deps': 'warn', 24 | }, 25 | settings: { 26 | react: { 27 | version: 'detect', 28 | }, 29 | }, 30 | }; 31 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | on: 3 | push: 4 | branches: 5 | - master 6 | pull_request: 7 | branches: 8 | - master 9 | paths-ignore: 10 | - ".gitignore" 11 | - ".npmignore" 12 | - "*.md" 13 | jobs: 14 | build: 15 | runs-on: ubuntu-latest 16 | strategy: 17 | matrix: 18 | node-version: [12.x] 19 | steps: 20 | - uses: actions/checkout@v2 21 | - name: Use Node.js ${{ matrix.node-version }} 22 | uses: actions/setup-node@v1 23 | with: 24 | node-version: ${{ matrix.node-version }} 25 | - name: Install dependencies 26 | run: yarn install --frozen-lockfile 27 | - name: Lint 28 | run: | 29 | npm run lint:types 30 | npm run lint 31 | - name: Test 32 | run: npm test 33 | env: 34 | CI: true 35 | - name: Build 36 | run: npm run build 37 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | 8 | jobs: 9 | release: 10 | runs-on: ubuntu-latest 11 | 12 | steps: 13 | - uses: actions/checkout@v2 14 | with: 15 | fetch-depth: 0 16 | 17 | - name: Use Node.js 12.x 18 | uses: actions/setup-node@v1 19 | with: 20 | version: 12.x 21 | 22 | - name: Install Dependencies 23 | run: npm i 24 | 25 | - name: Create Release Pull Request or Publish to npm 26 | uses: changesets/action@master 27 | with: 28 | publish: npm run release 29 | env: 30 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 31 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }} 32 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ### https://raw.github.com/github/gitignore/6c87d249af5f2b3f8ab65ae0a2648682ee4e8a2d/Global/macOS.gitignore 2 | 3 | # General 4 | .DS_Store 5 | .AppleDouble 6 | .LSOverride 7 | .idea 8 | 9 | # Icon must end with two \r 10 | Icon 11 | 12 | 13 | # Thumbnails 14 | ._* 15 | 16 | # Files that might appear in the root of a volume 17 | .DocumentRevisions-V100 18 | .fseventsd 19 | .Spotlight-V100 20 | .TemporaryItems 21 | .Trashes 22 | .VolumeIcon.icns 23 | .com.apple.timemachine.donotpresent 24 | 25 | # Directories potentially created on remote AFP share 26 | .AppleDB 27 | .AppleDesktop 28 | Network Trash Folder 29 | Temporary Items 30 | .apdisk 31 | 32 | 33 | ### https://raw.github.com/github/gitignore/6c87d249af5f2b3f8ab65ae0a2648682ee4e8a2d/Global/Windows.gitignore 34 | 35 | # Windows thumbnail cache files 36 | Thumbs.db 37 | Thumbs.db:encryptable 38 | ehthumbs.db 39 | ehthumbs_vista.db 40 | 41 | # Dump file 42 | *.stackdump 43 | 44 | # Folder config file 45 | [Dd]esktop.ini 46 | 47 | # Recycle Bin used on file shares 48 | $RECYCLE.BIN/ 49 | 50 | # Windows Installer files 51 | *.cab 52 | *.msi 53 | *.msix 54 | *.msm 55 | *.msp 56 | 57 | # Windows shortcuts 58 | *.lnk 59 | 60 | 61 | ### https://raw.github.com/github/gitignore/6c87d249af5f2b3f8ab65ae0a2648682ee4e8a2d/Global/Linux.gitignore 62 | 63 | *~ 64 | 65 | # temporary files which can be created if a process still has a handle open of a deleted file 66 | .fuse_hidden* 67 | 68 | # KDE directory preferences 69 | .directory 70 | 71 | # Linux trash folder which might appear on any partition or disk 72 | .Trash-* 73 | 74 | # .nfs files are created when an open file is removed but is still being accessed 75 | .nfs* 76 | 77 | 78 | ### https://raw.github.com/github/gitignore/6c87d249af5f2b3f8ab65ae0a2648682ee4e8a2d/Global/VisualStudioCode.gitignore 79 | 80 | .vscode/* 81 | !.vscode/settings.json 82 | !.vscode/tasks.json 83 | !.vscode/launch.json 84 | !.vscode/extensions.json 85 | *.code-workspace 86 | 87 | 88 | ### https://raw.github.com/github/gitignore/6c87d249af5f2b3f8ab65ae0a2648682ee4e8a2d/Node.gitignore 89 | 90 | # Logs 91 | logs 92 | *.log 93 | npm-debug.log* 94 | yarn-debug.log* 95 | yarn-error.log* 96 | lerna-debug.log* 97 | 98 | # Diagnostic reports (https://nodejs.org/api/report.html) 99 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 100 | 101 | # Runtime data 102 | pids 103 | *.pid 104 | *.seed 105 | *.pid.lock 106 | 107 | # Directory for instrumented libs generated by jscoverage/JSCover 108 | lib-cov 109 | 110 | # Coverage directory used by tools like istanbul 111 | coverage 112 | *.lcov 113 | 114 | # nyc test coverage 115 | .nyc_output 116 | 117 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 118 | .grunt 119 | 120 | # Bower dependency directory (https://bower.io/) 121 | bower_components 122 | 123 | # node-waf configuration 124 | .lock-wscript 125 | 126 | # Compiled binary addons (https://nodejs.org/api/addons.html) 127 | build/Release 128 | 129 | # Dependency directories 130 | node_modules/ 131 | jspm_packages/ 132 | 133 | # TypeScript v1 declaration files 134 | typings/ 135 | 136 | # TypeScript cache 137 | *.tsbuildinfo 138 | 139 | # Optional npm cache directory 140 | .npm 141 | 142 | # Optional eslint cache 143 | .eslintcache 144 | 145 | # Microbundle cache 146 | .rpt2_cache/ 147 | .rts2_cache_cjs/ 148 | .rts2_cache_es/ 149 | .rts2_cache_umd/ 150 | 151 | # Optional REPL history 152 | .node_repl_history 153 | 154 | # Output of 'npm pack' 155 | *.tgz 156 | 157 | # Yarn Integrity file 158 | .yarn-integrity 159 | 160 | # dotenv environment variables file 161 | .env 162 | .env.test 163 | 164 | # parcel-bundler cache (https://parceljs.org/) 165 | .cache 166 | 167 | # Next.js build output 168 | .next 169 | 170 | # Nuxt.js build / generate output 171 | .nuxt 172 | dist 173 | 174 | # Gatsby files 175 | .cache/ 176 | # Comment in the public line in if your project uses Gatsby and not Next.js 177 | # https://nextjs.org/blog/next-9-1#public-directory-support 178 | # public 179 | 180 | # vuepress build output 181 | .vuepress/dist 182 | 183 | # Serverless directories 184 | .serverless/ 185 | 186 | # FuseBox cache 187 | .fusebox/ 188 | 189 | # DynamoDB Local files 190 | .dynamodb/ 191 | 192 | # TernJS port file 193 | .tern-port 194 | 195 | # Stores VSCode versions used for testing VSCode extensions 196 | .vscode-test 197 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | package-lock=false -------------------------------------------------------------------------------- /.prettierrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | singleQuote: true, 3 | trailingComma: 'all', 4 | }; 5 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.codeActionsOnSave": { 3 | "source.fixAll.eslint": true 4 | }, 5 | "typescript.tsdk": "node_modules/typescript/lib" 6 | } 7 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Kotaro Sugawara 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. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
2 |

3 | 4 | React Hook Form Logo - React hook custom hook for form validation 5 | 6 |

7 |
8 | 9 |

Performant, flexible and extensible forms with easy to use validation.

10 | 11 |
12 | 13 | [![npm downloads](https://img.shields.io/npm/dm/@hookform/strictly-typed.svg?style=for-the-badge)](https://www.npmjs.com/package/@hookform/strictly-typed) 14 | [![npm](https://img.shields.io/npm/dt/@hookform/strictly-typed.svg?style=for-the-badge)](https://www.npmjs.com/package/@hookform/strictly-typed) 15 | [![npm](https://img.shields.io/bundlephobia/minzip/@hookform/strictly-typed?style=for-the-badge)](https://bundlephobia.com/result?p=@hookform/strictly-typed) 16 | 17 |
18 | 19 | ## Goal 20 | 21 | React Hook Form strictly typed custom hooks. 22 | 23 | ## Install 24 | 25 | ``` 26 | $ npm install @hookform/strictly-typed 27 | ``` 28 | 29 | ## Quickstart 30 | 31 | ```tsx 32 | import { useTypedController } from '@hookform/strictly-typed'; 33 | import { useForm } from 'react-hook-form'; 34 | import { TextField, Checkbox } from '@material-ui/core'; 35 | 36 | type FormValues = { 37 | flat: string; 38 | nested: { 39 | object: { test: string }; 40 | array: { test: boolean }[]; 41 | }; 42 | }; 43 | 44 | export default function App() { 45 | const { control, handleSubmit } = useForm(); 46 | const TypedController = useTypedController({ control }); 47 | 48 | const onSubmit = handleSubmit((data) => console.log(data)); 49 | 50 | return ( 51 |
52 | } 56 | /> 57 | 58 | 64 | 65 | } 69 | /> 70 | 71 | {/* ❌: Type '"notExists"' is not assignable to type 'DeepPath'. */} 72 | 73 | 74 | {/* ❌: Type 'number' is not assignable to type 'string | undefined'. */} 75 | 80 | 81 | {/* ❌: Type 'true' is not assignable to type 'string | undefined'. */} 82 | 83 | 84 | 85 | 86 | ); 87 | } 88 | ``` 89 | 90 | [![Edit React Hook Form - useTypedController](https://codesandbox.io/static/img/play-codesandbox.svg)](https://codesandbox.io/s/react-hook-form-usetypedcontroller-23qv1?fontsize=14&hidenavigation=1&theme=dark) 91 | 92 | ## Name Reference 93 | 94 | | Field Path | Field Name | 95 | | :------------------ | :----------- | 96 | | `foo` | `foo` | 97 | | `['foo', 'bar']` | `foo.bar` | 98 | | `['foo', 0]` | `foo[0]` | 99 | | `['foo', '0']` | `foo.0` | 100 | | `['foo', 1]` | `foo[1]` | 101 | | `['foo', 0, 'bar']` | `foo[0].bar` | 102 | | `['foo']` | `foo` | 103 | | `['foo', 'bar']` | `foo.bar` | 104 | | `['foo', 'bar', 0]` | `foo.bar[0]` | 105 | 106 | ## API 107 | 108 | - useTypedController 109 | 110 | | Name | Type | Required | 111 | | :-------- | :------- | :------- | 112 | | `control` | `Object` | | 113 | 114 | - TypedController 115 | 116 | | Name | Type | Required | 117 | | :------------- | :-------------------------------------------- | :------- | 118 | | `name` | `string \| [string, ...(string \| number)[]]` | ✓ | 119 | | `as` | `'input' \| 'select' \| 'textarea'` | | 120 | | `render` | `Function` | | 121 | | `defaultValue` | `DeepPathValue` | | 122 | | `rules` | `Object` | | 123 | | `onFocus` | `() => void` | | 124 | 125 | ## Backers 126 | 127 | Thanks goes to all our backers! [[Become a backer](https://opencollective.com/react-hook-form#backer)]. 128 | 129 | 130 | 131 | 132 | 133 | ## Organizations 134 | 135 | Thanks goes to these wonderful organizations! [[Contribute](https://opencollective.com/react-hook-form/contribute)]. 136 | 137 | 138 | 139 | 140 | 141 | ## Contributors 142 | 143 | Thanks goes to these wonderful people! [[Become a contributor](CONTRIBUTING.md)]. 144 | 145 | 146 | 147 | 148 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | preset: 'ts-jest', 3 | clearMocks: true, 4 | testMatch: ['/src/**/*.test.(ts|tsx)'], 5 | }; 6 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@hookform/strictly-typed", 3 | "version": "0.0.4", 4 | "description": "React Hook Form strictly typed custom hooks.", 5 | "main": "dist/index.js", 6 | "module": "dist/index.esm.js", 7 | "umd:main": "dist/index.umd.production.min.js", 8 | "unpkg": "dist/index.umd.production.min.js", 9 | "jsdelivr": "dist/index.umd.production.min.js", 10 | "jsnext:main": "dist/index.esm.js", 11 | "source": "src/index.ts", 12 | "types": "dist/index.d.ts", 13 | "sideEffects": false, 14 | "files": [ 15 | "dist" 16 | ], 17 | "publishConfig": { 18 | "access": "public" 19 | }, 20 | "scripts": { 21 | "clean": "rimraf dist", 22 | "prebuild": "npm run clean", 23 | "build": "node rollup/writeCjsEntryFile.js && rollup -c", 24 | "lint": "eslint '**/*.{js,ts,tsx}'", 25 | "lint:fix": "npm run lint -- --fix", 26 | "lint:types": "tsc --noEmit", 27 | "test": "jest", 28 | "test:watch": "jest --watch", 29 | "test:coverage": "jest --coverage", 30 | "postversion": "git push && git push origin v$npm_package_version", 31 | "prepublishOnly": "npm run lint && npm run lint:types && npm test && npm run build", 32 | "changeset": "changeset", 33 | "release": "changeset publish" 34 | }, 35 | "keywords": [ 36 | "react", 37 | "react-component", 38 | "hooks", 39 | "react-hooks", 40 | "form", 41 | "forms", 42 | "form-validation", 43 | "validation", 44 | "hookform", 45 | "react-hook-form", 46 | "typescript", 47 | "typesafe" 48 | ], 49 | "repository": { 50 | "type": "git", 51 | "url": "git+https://github.com/react-hook-form/strictly-typed.git" 52 | }, 53 | "author": "Kotaro Sugawara ", 54 | "license": "MIT", 55 | "bugs": { 56 | "url": "https://github.com/react-hook-form/strictly-typed/issues" 57 | }, 58 | "homepage": "https://react-hook-form.com", 59 | "devDependencies": { 60 | "@changesets/changelog-github": "^0.2.7", 61 | "@changesets/cli": "^2.10.2", 62 | "@rollup/plugin-commonjs": "^13.0.0", 63 | "@rollup/plugin-json": "^4.1.0", 64 | "@rollup/plugin-node-resolve": "^8.0.1", 65 | "@rollup/plugin-replace": "^2.3.3", 66 | "@testing-library/react": "^10.3.0", 67 | "@testing-library/react-hooks": "^3.3.0", 68 | "@types/jest": "^26.0.0", 69 | "@types/react": "^16.9.38", 70 | "@types/react-dom": "^16.9.8", 71 | "@typescript-eslint/eslint-plugin": "^3.3.0", 72 | "@typescript-eslint/parser": "^3.3.0", 73 | "eslint": "^7.2.0", 74 | "eslint-config-prettier": "^6.11.0", 75 | "eslint-plugin-prettier": "^3.1.4", 76 | "eslint-plugin-react": "^7.20.0", 77 | "eslint-plugin-react-hooks": "^4.0.4", 78 | "husky": "^4.2.5", 79 | "jest": "^26.0.1", 80 | "lint-staged": "^10.2.11", 81 | "prettier": "^2.0.5", 82 | "react": "^16.13.1", 83 | "react-dom": "^16.13.1", 84 | "react-hook-form": "^6.7.0", 85 | "react-test-renderer": "^16.13.1", 86 | "rimraf": "^3.0.2", 87 | "rollup": "^2.17.0", 88 | "rollup-plugin-peer-deps-external": "^2.2.2", 89 | "rollup-plugin-sourcemaps": "^0.6.2", 90 | "rollup-plugin-terser": "^6.1.0", 91 | "rollup-plugin-typescript2": "^0.27.0", 92 | "semantic-release": "^17.0.7", 93 | "ts-jest": "^26.1.0", 94 | "typescript": "^3.9.5" 95 | }, 96 | "peerDependencies": { 97 | "react": ">=16.8.0", 98 | "react-dom": ">=16.8.0", 99 | "react-hook-form": ">=6.6.0", 100 | "typescript": ">=3.0.1" 101 | }, 102 | "husky": { 103 | "hooks": { 104 | "pre-commit": "npm run lint:types && lint-staged" 105 | } 106 | }, 107 | "lint-staged": { 108 | "*.{js,ts,tsx}": [ 109 | "npm run lint:fix" 110 | ], 111 | "*.{md,json,yml}": [ 112 | "prettier --write" 113 | ] 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | import { createRollupConfig } from './rollup/createRollupConfig'; 2 | import pkg from './package.json'; 3 | 4 | const name = 'index'; 5 | const umdName = 'ReactHookFormStrictTyped'; 6 | const options = [ 7 | { 8 | name, 9 | umdName, 10 | format: 'cjs', 11 | env: 'development', 12 | input: pkg.source, 13 | }, 14 | { 15 | name, 16 | umdName, 17 | format: 'cjs', 18 | env: 'production', 19 | input: pkg.source, 20 | }, 21 | { 22 | name, 23 | umdName, 24 | format: 'esm', 25 | input: pkg.source, 26 | }, 27 | { 28 | name, 29 | umdName, 30 | format: 'umd', 31 | env: 'development', 32 | input: pkg.source, 33 | }, 34 | { 35 | name, 36 | umdName, 37 | format: 'umd', 38 | env: 'production', 39 | input: pkg.source, 40 | }, 41 | ]; 42 | 43 | export default options.map((option) => createRollupConfig(option)); 44 | -------------------------------------------------------------------------------- /rollup/createRollupConfig.js: -------------------------------------------------------------------------------- 1 | import ts from 'typescript'; 2 | import path from 'path'; 3 | import external from 'rollup-plugin-peer-deps-external'; 4 | import json from '@rollup/plugin-json'; 5 | import replace from '@rollup/plugin-replace'; 6 | import typescript from 'rollup-plugin-typescript2'; 7 | import resolve from '@rollup/plugin-node-resolve'; 8 | import commonjs from '@rollup/plugin-commonjs'; 9 | import sourcemaps from 'rollup-plugin-sourcemaps'; 10 | import { terser } from 'rollup-plugin-terser'; 11 | import { safePackageName } from './safePackageName'; 12 | import { pascalcase } from './pascalcase'; 13 | import pkg from '../package.json'; 14 | 15 | export function createRollupConfig(options) { 16 | const name = options.name || safePackageName(pkg.name); 17 | const umdName = options.umdName || pascalcase(safePackageName(pkg.name)); 18 | const shouldMinify = options.minify || options.env === 'production'; 19 | const tsconfigPath = options.tsconfig || 'tsconfig.json'; 20 | const tsconfigJSON = ts.readConfigFile(tsconfigPath, ts.sys.readFile).config; 21 | const tsCompilerOptions = ts.parseJsonConfigFileContent( 22 | tsconfigJSON, 23 | ts.sys, 24 | './', 25 | ).options; 26 | 27 | const outputName = [ 28 | path.join(tsCompilerOptions.outDir, name), 29 | options.formatName || options.format, 30 | options.env, 31 | shouldMinify ? 'min' : '', 32 | 'js', 33 | ] 34 | .filter(Boolean) 35 | .join('.'); 36 | 37 | return { 38 | input: options.input, 39 | output: { 40 | file: outputName, 41 | format: options.format, 42 | name: umdName, 43 | sourcemap: true, 44 | globals: { 45 | react: 'React', 46 | 'react-dom': 'ReactDOM', 47 | 'react-hook-form': 'ReactHookForm', 48 | }, 49 | exports: 'named', 50 | }, 51 | plugins: [ 52 | external({ 53 | includeDependencies: options.format !== 'umd', 54 | }), 55 | json(), 56 | typescript({ 57 | tsconfig: options.tsconfig, 58 | clean: true, 59 | }), 60 | resolve(), 61 | options.format === 'umd' && 62 | commonjs({ 63 | include: /\/node_modules\//, 64 | }), 65 | options.env !== undefined && 66 | replace({ 67 | 'process.env.NODE_ENV': JSON.stringify(options.env), 68 | }), 69 | sourcemaps(), 70 | shouldMinify && 71 | terser({ 72 | output: { comments: false }, 73 | compress: { 74 | drop_console: true, 75 | }, 76 | }), 77 | options.plugins, 78 | ], 79 | }; 80 | } 81 | -------------------------------------------------------------------------------- /rollup/pascalcase.js: -------------------------------------------------------------------------------- 1 | const titlecase = (input) => input[0].toLocaleUpperCase() + input.slice(1); 2 | 3 | export const pascalcase = (value) => { 4 | if (value === null || value === void 0) { 5 | return ''; 6 | } 7 | if (typeof value.toString !== 'function') { 8 | return ''; 9 | } 10 | 11 | let input = value.toString().trim(); 12 | if (input === '') { 13 | return ''; 14 | } 15 | if (input.length === 1) { 16 | return input.toLocaleUpperCase(); 17 | } 18 | 19 | let match = input.match(/[a-zA-Z0-9]+/g); 20 | if (match) { 21 | return match.map((m) => titlecase(m)).join(''); 22 | } 23 | 24 | return input; 25 | }; 26 | -------------------------------------------------------------------------------- /rollup/safePackageName.js: -------------------------------------------------------------------------------- 1 | export const safePackageName = (name) => 2 | name 3 | .toLowerCase() 4 | .replace(/(^@.*\/)|((^[^a-zA-Z]+)|[^\w.-])|([^a-zA-Z0-9]+$)/g, ''); 5 | -------------------------------------------------------------------------------- /rollup/writeCjsEntryFile.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-var-requires */ 2 | const ts = require('typescript'); 3 | const fs = require('fs-extra'); 4 | const path = require('path'); 5 | const pkg = require('../package.json'); 6 | 7 | function writeCjsEntryFile( 8 | name = pkg.name, 9 | formatName = 'cjs', 10 | tsconfig = 'tsconfig.json', 11 | ) { 12 | const baseLine = `module.exports = require('./${name}`; 13 | const contents = ` 14 | 'use strict' 15 | 16 | if (process.env.NODE_ENV === 'production') { 17 | ${baseLine}.${formatName}.production.min.js') 18 | } else { 19 | ${baseLine}.${formatName}.development.js') 20 | } 21 | `; 22 | 23 | const tsconfigJSON = ts.readConfigFile(tsconfig, ts.sys.readFile).config; 24 | const tsCompilerOptions = ts.parseJsonConfigFileContent( 25 | tsconfigJSON, 26 | ts.sys, 27 | './', 28 | ).options; 29 | 30 | const filename = 31 | formatName === 'cjs' 32 | ? [name, 'js'].join('.') 33 | : [name, formatName, 'js'].join('.'); 34 | 35 | return fs.outputFile(path.join(tsCompilerOptions.outDir, filename), contents); 36 | } 37 | 38 | writeCjsEntryFile('index'); 39 | -------------------------------------------------------------------------------- /src/__snapshots__/useTypedController.test.tsx.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`useTypedField should render correctly when as is input and name is string 1`] = ` 4 | 5 | 9 | 10 | `; 11 | 12 | exports[`useTypedField should render correctly when as is textarea and name is array 1`] = ` 13 | 14 | 19 | 20 | `; 21 | 22 | exports[`useTypedField should render correctly when name is array and render is input 1`] = ` 23 | 24 | 28 | 29 | `; 30 | 31 | exports[`useTypedField should render correctly when name is string and render is textarea 1`] = ` 32 | 33 | 38 | 39 | `; 40 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './useTypedController'; 2 | export * from './types'; 3 | -------------------------------------------------------------------------------- /src/logic/formatName.test.ts: -------------------------------------------------------------------------------- 1 | import { formatName } from './formatName'; 2 | 3 | describe('formatName', () => { 4 | const tests: [string, string | [string, ...(string | number)[]], string][] = [ 5 | ['should format correctly when name is string', 'test', 'test'], 6 | [ 7 | 'should combine array to string like object if name is array when contain one item', 8 | ['test1'], 9 | 'test1', 10 | ], 11 | [ 12 | 'should combine array to string like object if name is array', 13 | ['test1', 'test2', 'test3'], 14 | 'test1.test2.test3', 15 | ], 16 | [ 17 | 'should combine array to string like array that contain object, if name is array when contain one item', 18 | ['test1', 0], 19 | 'test1[0]', 20 | ], 21 | [ 22 | 'should combine array to string like array that contain object, if name is array', 23 | ['test1', 0, 'test2', 1, 'test3'], 24 | 'test1[0].test2[1].test3', 25 | ], 26 | ]; 27 | 28 | test.each(tests)('%s', (_, input, result) => { 29 | expect(formatName(input)).toBe(result); 30 | }); 31 | }); 32 | -------------------------------------------------------------------------------- /src/logic/formatName.ts: -------------------------------------------------------------------------------- 1 | const isNumber = (value: unknown): value is number => typeof value === 'number'; 2 | 3 | export const formatName = ( 4 | name: string | [string, ...(string | number)[]], 5 | ): string => { 6 | if (Array.isArray(name)) { 7 | return name 8 | .reduce((accumulator: string[], currentValue) => { 9 | if (isNumber(currentValue)) { 10 | const lastIndex = accumulator.length - 1; 11 | accumulator[lastIndex] = `${accumulator[lastIndex]}[${currentValue}]`; 12 | return accumulator; 13 | } 14 | return [...accumulator, currentValue]; 15 | }, []) 16 | .join('.'); 17 | } 18 | return name; 19 | }; 20 | -------------------------------------------------------------------------------- /src/types.ts: -------------------------------------------------------------------------------- 1 | import { 2 | FieldValues, 3 | Control, 4 | Message, 5 | ValidationRule, 6 | ValidateResult, 7 | } from 'react-hook-form'; 8 | 9 | export type ArrayWithLength = { [K in N]: any }; 10 | 11 | export interface DeepPathArray 12 | extends ReadonlyArray { 13 | ['0']: keyof TValues; 14 | ['1']?: TPath extends { 15 | ['0']: infer K0; 16 | } 17 | ? K0 extends keyof TValues 18 | ? keyof TValues[K0] 19 | : never 20 | : never; 21 | ['2']?: TPath extends { 22 | ['0']: infer K0; 23 | ['1']: infer K1; 24 | } 25 | ? K0 extends keyof TValues 26 | ? K1 extends keyof TValues[K0] 27 | ? keyof TValues[K0][K1] 28 | : never 29 | : never 30 | : never; 31 | ['3']?: TPath extends { 32 | ['0']: infer K0; 33 | ['1']: infer K1; 34 | ['2']: infer K2; 35 | } 36 | ? K0 extends keyof TValues 37 | ? K1 extends keyof TValues[K0] 38 | ? K2 extends keyof TValues[K0][K1] 39 | ? keyof TValues[K0][K1][K2] 40 | : never 41 | : never 42 | : never 43 | : never; 44 | ['4']?: TPath extends { 45 | ['0']: infer K0; 46 | ['1']: infer K1; 47 | ['2']: infer K2; 48 | ['3']: infer K3; 49 | } 50 | ? K0 extends keyof TValues 51 | ? K1 extends keyof TValues[K0] 52 | ? K2 extends keyof TValues[K0][K1] 53 | ? K3 extends keyof TValues[K0][K1][K2] 54 | ? keyof TValues[K0][K1][K2][K3] 55 | : never 56 | : never 57 | : never 58 | : never 59 | : never; 60 | ['5']?: TPath extends { 61 | ['0']: infer K0; 62 | ['1']: infer K1; 63 | ['2']: infer K2; 64 | ['3']: infer K3; 65 | ['4']: infer K4; 66 | } 67 | ? K0 extends keyof TValues 68 | ? K1 extends keyof TValues[K0] 69 | ? K2 extends keyof TValues[K0][K1] 70 | ? K3 extends keyof TValues[K0][K1][K2] 71 | ? K4 extends keyof TValues[K0][K1][K2][K3] 72 | ? keyof TValues[K0][K1][K2][K3][K4] 73 | : never 74 | : never 75 | : never 76 | : never 77 | : never 78 | : never; 79 | } 80 | 81 | export type DeepPathArrayValue< 82 | TValues extends FieldValues, 83 | TPath extends DeepPathArray 84 | > = TPath extends ArrayWithLength<0 | 1 | 2 | 3 | 4 | 5 | 6> 85 | ? any 86 | : TPath extends ArrayWithLength<0 | 1 | 2 | 3 | 4 | 5> 87 | ? TValues[TPath[0]][TPath[1]][TPath[2]][TPath[3]][TPath[4]][TPath[5]] 88 | : TPath extends ArrayWithLength<0 | 1 | 2 | 3 | 4> 89 | ? TValues[TPath[0]][TPath[1]][TPath[2]][TPath[3]][TPath[4]] 90 | : TPath extends ArrayWithLength<0 | 1 | 2 | 3> 91 | ? TValues[TPath[0]][TPath[1]][TPath[2]][TPath[3]] 92 | : TPath extends ArrayWithLength<0 | 1 | 2> 93 | ? TValues[TPath[0]][TPath[1]][TPath[2]] 94 | : TPath extends ArrayWithLength<0 | 1> 95 | ? TValues[TPath[0]][TPath[1]] 96 | : TPath extends ArrayWithLength<0> 97 | ? TValues[TPath[0]] 98 | : never; 99 | 100 | export type DeepPath = 101 | | DeepPathArray 102 | | keyof TValues; 103 | 104 | export type DeepPathValue< 105 | TValues extends FieldValues, 106 | TPath extends DeepPath 107 | > = TPath extends DeepPathArray 108 | ? DeepPathArrayValue 109 | : TPath extends keyof TValues 110 | ? TValues[TPath] 111 | : any; 112 | 113 | export type Options = { 114 | control?: TControl; 115 | }; 116 | 117 | export type Validate = ( 118 | data: TFieldValue, 119 | ) => ValidateResult | Promise; 120 | 121 | export type ValidationRules = Partial<{ 122 | required: Message | ValidationRule; 123 | min: ValidationRule; 124 | max: ValidationRule; 125 | maxLength: ValidationRule; 126 | minLength: ValidationRule; 127 | pattern: ValidationRule; 128 | validate: Validate | Record>; 129 | }>; 130 | 131 | export type Assign = TValues & 132 | Omit; 133 | 134 | export type ControllerProps< 135 | TFieldValues extends FieldValues, 136 | TFieldName extends DeepPath, 137 | TAs extends 'input' | 'select' | 'textarea' 138 | > = Assign< 139 | { 140 | name: TFieldName; 141 | as?: TAs; 142 | rules?: ValidationRules>; 143 | onFocus?: () => void; 144 | defaultValue?: DeepPathValue; 145 | render?: (props: { 146 | onChange: (...event: any[]) => void; 147 | onBlur: () => void; 148 | value: DeepPathValue; 149 | }) => React.ReactElement; 150 | }, 151 | JSX.IntrinsicElements[TAs] 152 | >; 153 | -------------------------------------------------------------------------------- /src/useTypedController.test.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Control } from 'react-hook-form'; 3 | import { render } from '@testing-library/react'; 4 | import { renderHook } from '@testing-library/react-hooks'; 5 | import { useTypedController } from './useTypedController'; 6 | 7 | export const reconfigureControl = ( 8 | controlOverrides: Partial = {}, 9 | ): Control => ({ 10 | unmountFieldsStateRef: { 11 | current: {}, 12 | }, 13 | defaultValuesRef: { 14 | current: {}, 15 | }, 16 | isWatchAllRef: { 17 | current: false, 18 | }, 19 | validFieldsRef: { 20 | current: new Set(), 21 | }, 22 | fieldsWithValidationRef: { 23 | current: new Set(), 24 | }, 25 | fieldArrayDefaultValues: { 26 | current: {}, 27 | }, 28 | watchFieldsRef: { 29 | current: new Set(), 30 | }, 31 | watchFieldsHookRef: { 32 | current: {}, 33 | }, 34 | watchFieldsHookRenderRef: { 35 | current: {}, 36 | }, 37 | watchInternal: jest.fn(), 38 | validateResolver: jest.fn(), 39 | setValue: jest.fn(), 40 | getValues: jest.fn(), 41 | register: jest.fn(), 42 | unregister: jest.fn(), 43 | trigger: jest.fn(), 44 | removeFieldEventListener: jest.fn(), 45 | mode: { 46 | isOnSubmit: false, 47 | isOnBlur: false, 48 | isOnChange: false, 49 | isOnTouch: false, 50 | isOnAll: false, 51 | }, 52 | reValidateMode: { 53 | isReValidateOnBlur: false, 54 | isReValidateOnChange: false, 55 | }, 56 | formStateRef: { 57 | current: { 58 | errors: {}, 59 | isDirty: false, 60 | isSubmitted: false, 61 | dirtyFields: {}, 62 | submitCount: 0, 63 | touched: {}, 64 | isSubmitting: false, 65 | isValid: false, 66 | }, 67 | }, 68 | fieldsRef: { 69 | current: {}, 70 | }, 71 | resetFieldArrayFunctionRef: { 72 | current: {}, 73 | }, 74 | fieldArrayNamesRef: { 75 | current: new Set(), 76 | }, 77 | // eslint-disable-next-line @typescript-eslint/no-empty-function 78 | updateFormState: () => {}, 79 | readFormStateRef: { 80 | current: { 81 | isDirty: true, 82 | errors: true, 83 | isSubmitted: false, 84 | submitCount: false, 85 | touched: false, 86 | isSubmitting: false, 87 | isValid: false, 88 | dirtyFields: false, 89 | }, 90 | }, 91 | // eslint-disable-next-line @typescript-eslint/no-empty-function 92 | renderWatchedInputs: () => {}, 93 | ...controlOverrides, 94 | }); 95 | 96 | type FormValues = { 97 | flat: string; 98 | nested: { 99 | object: { test: string }; 100 | array: { test: string }[]; 101 | }; 102 | }; 103 | 104 | describe('useTypedField', () => { 105 | it('should render correctly when as is input and name is string', () => { 106 | const control = reconfigureControl(); 107 | const { result } = renderHook(() => 108 | useTypedController({ 109 | control, 110 | }), 111 | ); 112 | const TypedController = result.current; 113 | 114 | const { asFragment } = render( 115 | , 116 | ); 117 | 118 | expect(asFragment()).toMatchSnapshot(); 119 | }); 120 | 121 | it('should render correctly when name is string and render is textarea', () => { 122 | const control = reconfigureControl(); 123 | const { result } = renderHook(() => 124 | useTypedController({ 125 | control, 126 | }), 127 | ); 128 | const TypedController = result.current; 129 | 130 | const { asFragment } = render( 131 |