feat: add form validation
This commit is contained in:
		
							
								
								
									
										52
									
								
								README.md
									
									
									
									
									
								
							
							
						
						
									
										52
									
								
								README.md
									
									
									
									
									
								
							| @@ -22,7 +22,9 @@ | ||||
|  | ||||
| **react-component-form** is a lightweight form component for [React.js](https://reactjs.org/), it allows you to get the inputs values without state thanks to `onChange` or `onSubmit` props. | ||||
|  | ||||
| Demo : [https://divlo.github.io/react-component-form/](https://divlo.github.io/react-component-form/). | ||||
| There is also a [React Hooks](https://reactjs.org/docs/hooks-intro.html) to be used in combination with the `<Form />` component to validate the data with [Ajv JSON schema validator](https://ajv.js.org/), see [advanced usage](#%EF%B8%8F-advanced-usage). | ||||
|  | ||||
| Demo: [https://divlo.github.io/react-component-form/](https://divlo.github.io/react-component-form/). | ||||
|  | ||||
| ## 💾 Install | ||||
|  | ||||
| @@ -32,9 +34,12 @@ npm install --save react-component-form | ||||
|  | ||||
| ## ⚙️ Usage | ||||
|  | ||||
| _Note : The examples use TypeScript, but obviously you can use JavaScript. Be aware that `HandleForm` is the type definition for the `onChange` and `onSubmit` props._ | ||||
|  | ||||
| ```tsx | ||||
| import React from 'react' | ||||
| import { Form, HandleForm } from 'react-component-form' | ||||
| import { Form } from 'react-component-form' | ||||
| import type { HandleForm } from 'react-component-form' | ||||
|  | ||||
| const Example = () => { | ||||
|   const handleSubmit: HandleForm = (formData, formElement) => { | ||||
| @@ -51,15 +56,52 @@ const Example = () => { | ||||
| } | ||||
| ``` | ||||
|  | ||||
| _Note : The example use TypeScript, but obviously you can use JavaScript. Be aware that `HandleForm` is the type definition for the `onChange` and `onSubmit` props._ | ||||
|  | ||||
| Basically you have access to the same props of the HTML `form` tag in React, but the onSubmit and the onChange props are differents. | ||||
|  | ||||
| Instead to get the `event` param you get `formData` and `formElement` params : | ||||
| Instead to get the `event` param you get `formData` and `formElement` parameters: | ||||
|  | ||||
| - `formData`: It's an object where the keys are the name of your inputs and the current value. Behind the scene, it uses the [FormData](https://developer.mozilla.org/docs/Web/API/FormData) constructor. | ||||
| - `formElement`: It's the actual HTML form element in the DOM so for example you can access the `.reset()` method on a [HTMLFormElement](https://developer.mozilla.org/docs/Web/API/HTMLFormElement). | ||||
|  | ||||
| ## ⚙️ Advanced Usage | ||||
|  | ||||
| This example shows how to use the `<Form />` component with `useForm` hook to validate the data with [Ajv JSON schema validator](https://ajv.js.org/). | ||||
|  | ||||
| You can see a more detailled example in the [./example](./example) folder. | ||||
|  | ||||
| ```tsx | ||||
| import React from 'react' | ||||
| import { Form, useForm } from 'react-component-form' | ||||
| import type { HandleSubmitCallback } from 'react-component-form' | ||||
|  | ||||
| const schema = { | ||||
|   inputName: { | ||||
|     type: 'string', | ||||
|     required: true, | ||||
|     minLength: 3, | ||||
|     maxLength: 10 | ||||
|   } | ||||
| } | ||||
|  | ||||
| const Example = () => { | ||||
|   const { errors, handleSubmit } = useForm(schema) | ||||
|  | ||||
|   const onSubmit: HandleSubmitCallback<typeof schema> = (formData, formElement) => { | ||||
|     console.log(formData) // { inputName: 'value of the input' } | ||||
|     formElement.reset() | ||||
|   } | ||||
|  | ||||
|   return ( | ||||
|     <Form onSubmit={handleSubmit(onSubmit)}> | ||||
|       <input type='text' name='inputName' /> | ||||
|       {errors.inputName != null && <p>{errors.inputName[0].message}</p>} | ||||
|  | ||||
|       <button type='submit'>Submit</button> | ||||
|     </Form> | ||||
|   ) | ||||
| } | ||||
| ``` | ||||
|  | ||||
| ## 💡 Contributing | ||||
|  | ||||
| Anyone can help to improve the project, submit a Feature Request, a bug report or | ||||
|   | ||||
							
								
								
									
										8
									
								
								example/package-lock.json
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										8
									
								
								example/package-lock.json
									
									
									
										generated
									
									
									
								
							| @@ -21,6 +21,11 @@ | ||||
|     "..": { | ||||
|       "version": "0.0.0-development", | ||||
|       "license": "MIT", | ||||
|       "dependencies": { | ||||
|         "@sinclair/typebox": "0.24.28", | ||||
|         "ajv": "8.11.0", | ||||
|         "ajv-formats": "2.1.1" | ||||
|       }, | ||||
|       "devDependencies": { | ||||
|         "@commitlint/cli": "17.0.3", | ||||
|         "@commitlint/config-conventional": "17.0.3", | ||||
| @@ -4356,12 +4361,15 @@ | ||||
|       "requires": { | ||||
|         "@commitlint/cli": "17.0.3", | ||||
|         "@commitlint/config-conventional": "17.0.3", | ||||
|         "@sinclair/typebox": "0.24.28", | ||||
|         "@testing-library/react": "13.3.0", | ||||
|         "@types/jest": "28.1.8", | ||||
|         "@types/react": "18.0.17", | ||||
|         "@types/react-dom": "18.0.6", | ||||
|         "@typescript-eslint/eslint-plugin": "5.35.1", | ||||
|         "@typescript-eslint/parser": "5.35.1", | ||||
|         "ajv": "8.11.0", | ||||
|         "ajv-formats": "2.1.1", | ||||
|         "editorconfig-checker": "4.0.2", | ||||
|         "esbuild": "0.15.5", | ||||
|         "esbuild-jest": "0.5.0", | ||||
|   | ||||
							
								
								
									
										59
									
								
								package-lock.json
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										59
									
								
								package-lock.json
									
									
									
										generated
									
									
									
								
							| @@ -8,6 +8,11 @@ | ||||
|       "name": "react-component-form", | ||||
|       "version": "0.0.0-development", | ||||
|       "license": "MIT", | ||||
|       "dependencies": { | ||||
|         "@sinclair/typebox": "0.24.28", | ||||
|         "ajv": "8.11.0", | ||||
|         "ajv-formats": "2.1.1" | ||||
|       }, | ||||
|       "devDependencies": { | ||||
|         "@commitlint/cli": "17.0.3", | ||||
|         "@commitlint/config-conventional": "17.0.3", | ||||
| @@ -2602,8 +2607,7 @@ | ||||
|     "node_modules/@sinclair/typebox": { | ||||
|       "version": "0.24.28", | ||||
|       "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.24.28.tgz", | ||||
|       "integrity": "sha512-dgJd3HLOkLmz4Bw50eZx/zJwtBq65nms3N9VBYu5LTjJ883oBFkTyXRlCB/ZGGwqYpJJHA5zW2Ibhl5ngITfow==", | ||||
|       "dev": true | ||||
|       "integrity": "sha512-dgJd3HLOkLmz4Bw50eZx/zJwtBq65nms3N9VBYu5LTjJ883oBFkTyXRlCB/ZGGwqYpJJHA5zW2Ibhl5ngITfow==" | ||||
|     }, | ||||
|     "node_modules/@sinonjs/commons": { | ||||
|       "version": "1.8.3", | ||||
| @@ -3217,7 +3221,6 @@ | ||||
|       "version": "8.11.0", | ||||
|       "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.11.0.tgz", | ||||
|       "integrity": "sha512-wGgprdCvMalC0BztXvitD2hC04YffAvtsUn93JbGXYLAtCUO4xd17mCCZQxUOItiBwZvJScWo8NIvQMQ71rdpg==", | ||||
|       "dev": true, | ||||
|       "dependencies": { | ||||
|         "fast-deep-equal": "^3.1.1", | ||||
|         "json-schema-traverse": "^1.0.0", | ||||
| @@ -3229,6 +3232,22 @@ | ||||
|         "url": "https://github.com/sponsors/epoberezkin" | ||||
|       } | ||||
|     }, | ||||
|     "node_modules/ajv-formats": { | ||||
|       "version": "2.1.1", | ||||
|       "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-2.1.1.tgz", | ||||
|       "integrity": "sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==", | ||||
|       "dependencies": { | ||||
|         "ajv": "^8.0.0" | ||||
|       }, | ||||
|       "peerDependencies": { | ||||
|         "ajv": "^8.0.0" | ||||
|       }, | ||||
|       "peerDependenciesMeta": { | ||||
|         "ajv": { | ||||
|           "optional": true | ||||
|         } | ||||
|       } | ||||
|     }, | ||||
|     "node_modules/ansi-escapes": { | ||||
|       "version": "4.3.2", | ||||
|       "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz", | ||||
| @@ -6122,8 +6141,7 @@ | ||||
|     "node_modules/fast-deep-equal": { | ||||
|       "version": "3.1.3", | ||||
|       "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", | ||||
|       "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", | ||||
|       "dev": true | ||||
|       "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==" | ||||
|     }, | ||||
|     "node_modules/fast-diff": { | ||||
|       "version": "1.2.0", | ||||
| @@ -9897,8 +9915,7 @@ | ||||
|     "node_modules/json-schema-traverse": { | ||||
|       "version": "1.0.0", | ||||
|       "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", | ||||
|       "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", | ||||
|       "dev": true | ||||
|       "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==" | ||||
|     }, | ||||
|     "node_modules/json-stable-stringify-without-jsonify": { | ||||
|       "version": "1.0.1", | ||||
| @@ -14081,7 +14098,6 @@ | ||||
|       "version": "2.1.1", | ||||
|       "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz", | ||||
|       "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==", | ||||
|       "dev": true, | ||||
|       "engines": { | ||||
|         "node": ">=6" | ||||
|       } | ||||
| @@ -14462,7 +14478,6 @@ | ||||
|       "version": "2.0.2", | ||||
|       "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", | ||||
|       "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", | ||||
|       "dev": true, | ||||
|       "engines": { | ||||
|         "node": ">=0.10.0" | ||||
|       } | ||||
| @@ -16621,7 +16636,6 @@ | ||||
|       "version": "4.4.1", | ||||
|       "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", | ||||
|       "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", | ||||
|       "dev": true, | ||||
|       "dependencies": { | ||||
|         "punycode": "^2.1.0" | ||||
|       } | ||||
| @@ -19041,8 +19055,7 @@ | ||||
|     "@sinclair/typebox": { | ||||
|       "version": "0.24.28", | ||||
|       "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.24.28.tgz", | ||||
|       "integrity": "sha512-dgJd3HLOkLmz4Bw50eZx/zJwtBq65nms3N9VBYu5LTjJ883oBFkTyXRlCB/ZGGwqYpJJHA5zW2Ibhl5ngITfow==", | ||||
|       "dev": true | ||||
|       "integrity": "sha512-dgJd3HLOkLmz4Bw50eZx/zJwtBq65nms3N9VBYu5LTjJ883oBFkTyXRlCB/ZGGwqYpJJHA5zW2Ibhl5ngITfow==" | ||||
|     }, | ||||
|     "@sinonjs/commons": { | ||||
|       "version": "1.8.3", | ||||
| @@ -19523,7 +19536,6 @@ | ||||
|       "version": "8.11.0", | ||||
|       "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.11.0.tgz", | ||||
|       "integrity": "sha512-wGgprdCvMalC0BztXvitD2hC04YffAvtsUn93JbGXYLAtCUO4xd17mCCZQxUOItiBwZvJScWo8NIvQMQ71rdpg==", | ||||
|       "dev": true, | ||||
|       "requires": { | ||||
|         "fast-deep-equal": "^3.1.1", | ||||
|         "json-schema-traverse": "^1.0.0", | ||||
| @@ -19531,6 +19543,14 @@ | ||||
|         "uri-js": "^4.2.2" | ||||
|       } | ||||
|     }, | ||||
|     "ajv-formats": { | ||||
|       "version": "2.1.1", | ||||
|       "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-2.1.1.tgz", | ||||
|       "integrity": "sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==", | ||||
|       "requires": { | ||||
|         "ajv": "^8.0.0" | ||||
|       } | ||||
|     }, | ||||
|     "ansi-escapes": { | ||||
|       "version": "4.3.2", | ||||
|       "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz", | ||||
| @@ -21631,8 +21651,7 @@ | ||||
|     "fast-deep-equal": { | ||||
|       "version": "3.1.3", | ||||
|       "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", | ||||
|       "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", | ||||
|       "dev": true | ||||
|       "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==" | ||||
|     }, | ||||
|     "fast-diff": { | ||||
|       "version": "1.2.0", | ||||
| @@ -24585,8 +24604,7 @@ | ||||
|     "json-schema-traverse": { | ||||
|       "version": "1.0.0", | ||||
|       "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", | ||||
|       "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", | ||||
|       "dev": true | ||||
|       "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==" | ||||
|     }, | ||||
|     "json-stable-stringify-without-jsonify": { | ||||
|       "version": "1.0.1", | ||||
| @@ -27565,8 +27583,7 @@ | ||||
|     "punycode": { | ||||
|       "version": "2.1.1", | ||||
|       "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz", | ||||
|       "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==", | ||||
|       "dev": true | ||||
|       "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==" | ||||
|     }, | ||||
|     "q": { | ||||
|       "version": "1.5.1", | ||||
| @@ -27855,8 +27872,7 @@ | ||||
|     "require-from-string": { | ||||
|       "version": "2.0.2", | ||||
|       "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", | ||||
|       "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", | ||||
|       "dev": true | ||||
|       "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==" | ||||
|     }, | ||||
|     "requires-port": { | ||||
|       "version": "1.0.0", | ||||
| @@ -29543,7 +29559,6 @@ | ||||
|       "version": "4.4.1", | ||||
|       "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", | ||||
|       "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", | ||||
|       "dev": true, | ||||
|       "requires": { | ||||
|         "punycode": "^2.1.0" | ||||
|       } | ||||
|   | ||||
| @@ -36,6 +36,11 @@ | ||||
|   "peerDependencies": { | ||||
|     "react": ">=16" | ||||
|   }, | ||||
|   "dependencies": { | ||||
|     "@sinclair/typebox": "0.24.28", | ||||
|     "ajv": "8.11.0", | ||||
|     "ajv-formats": "2.1.1" | ||||
|   }, | ||||
|   "devDependencies": { | ||||
|     "@commitlint/cli": "17.0.3", | ||||
|     "@commitlint/config-conventional": "17.0.3", | ||||
| @@ -46,6 +51,8 @@ | ||||
|     "@typescript-eslint/eslint-plugin": "5.35.1", | ||||
|     "@typescript-eslint/parser": "5.35.1", | ||||
|     "editorconfig-checker": "4.0.2", | ||||
|     "esbuild": "0.15.5", | ||||
|     "esbuild-jest": "0.5.0", | ||||
|     "eslint": "8.22.0", | ||||
|     "eslint-config-conventions": "3.0.0", | ||||
|     "eslint-config-prettier": "8.5.0", | ||||
| @@ -53,8 +60,6 @@ | ||||
|     "eslint-plugin-prettier": "4.2.1", | ||||
|     "eslint-plugin-promise": "6.0.1", | ||||
|     "eslint-plugin-unicorn": "43.0.2", | ||||
|     "esbuild": "0.15.5", | ||||
|     "esbuild-jest": "0.5.0", | ||||
|     "jest": "29.0.0", | ||||
|     "jest-environment-jsdom": "29.0.0", | ||||
|     "markdownlint-cli2": "0.5.1", | ||||
|   | ||||
| @@ -9,7 +9,7 @@ export type HandleForm = ( | ||||
|   formElement: HTMLFormElement | ||||
| ) => void | Promise<void> | ||||
| 
 | ||||
| export interface ReactFormProps | ||||
| interface ReactFormProps | ||||
|   extends Omit<React.HTMLProps<HTMLFormElement>, 'onSubmit' | 'onChange'> {} | ||||
| 
 | ||||
| export interface FormProps extends ReactFormProps { | ||||
							
								
								
									
										15
									
								
								src/hooks/useFetchState.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										15
									
								
								src/hooks/useFetchState.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,15 @@ | ||||
| import { useState } from 'react' | ||||
|  | ||||
| export const fetchState = ['idle', 'loading', 'error', 'success'] as const | ||||
|  | ||||
| export type FetchState = typeof fetchState[number] | ||||
|  | ||||
| export const useFetchState = ( | ||||
|   initialFetchState: FetchState = 'idle' | ||||
| ): [ | ||||
|   fetchState: FetchState, | ||||
|   setFetchState: React.Dispatch<React.SetStateAction<FetchState>> | ||||
| ] => { | ||||
|   const [fetchState, setFetchState] = useState<FetchState>(initialFetchState) | ||||
|   return [fetchState, setFetchState] | ||||
| } | ||||
							
								
								
									
										144
									
								
								src/hooks/useForm.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										144
									
								
								src/hooks/useForm.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,144 @@ | ||||
| import { useMemo, useState } from 'react' | ||||
| import { Static, TObject, TProperties, Type } from '@sinclair/typebox' | ||||
| import type { ErrorObject } from 'ajv' | ||||
|  | ||||
| import type { HandleForm } from '../components/Form' | ||||
| import { FetchState, useFetchState } from './useFetchState' | ||||
| import { ajv } from '../utils/ajv' | ||||
| import { handleCheckboxBoolean } from '../utils/handleCheckboxBoolean' | ||||
| import { handleOptionalEmptyStringToNull } from '../utils/handleOptionalEmptyStringToNull' | ||||
|  | ||||
| export type Error = ErrorObject | ||||
|  | ||||
| export type ErrorsObject<K extends TProperties> = { | ||||
|   [key in keyof Partial<K>]: Error[] | undefined | ||||
| } | ||||
|  | ||||
| export type HandleSubmitCallback<K extends TProperties> = ( | ||||
|   formData: Static<TObject<K>>, | ||||
|   formElement: HTMLFormElement | ||||
| ) => Promise<Message<K> | null> | ||||
|  | ||||
| export type HandleSubmit<K extends TProperties> = ( | ||||
|   callback: HandleSubmitCallback<K> | ||||
| ) => HandleForm | ||||
|  | ||||
| export interface GlobalMessage { | ||||
|   type: 'error' | 'success' | ||||
|   value?: string | ||||
|   properties?: undefined | ||||
| } | ||||
|  | ||||
| export interface PropertiesMessage<K extends TProperties> { | ||||
|   type: 'error' | ||||
|   value?: string | ||||
|   properties: { [key in keyof Partial<K>]: string } | ||||
| } | ||||
|  | ||||
| export type Message<K extends TProperties> = | ||||
|   | GlobalMessage | ||||
|   | PropertiesMessage<K> | ||||
|  | ||||
| export interface UseFormResult<K extends TProperties> { | ||||
|   handleSubmit: HandleSubmit<K> | ||||
|  | ||||
|   readonly fetchState: FetchState | ||||
|   setFetchState: React.Dispatch<React.SetStateAction<FetchState>> | ||||
|  | ||||
|   /** | ||||
|    * Global message of the form (not specific to a property). | ||||
|    */ | ||||
|   readonly message: string | null | ||||
|   setMessage: React.Dispatch<React.SetStateAction<string | null>> | ||||
|  | ||||
|   /** | ||||
|    * Errors for each property. | ||||
|    * | ||||
|    * The array will always have at least one element (never empty) in case of errors. | ||||
|    * | ||||
|    * `undefined` means no errors. | ||||
|    */ | ||||
|   readonly errors: ErrorsObject<K> | ||||
| } | ||||
|  | ||||
| export const useForm = <K extends TProperties>( | ||||
|   validationSchema: K | ||||
| ): UseFormResult<typeof validationSchema> => { | ||||
|   const validationSchemaObject = useMemo(() => { | ||||
|     return Type.Object(validationSchema) | ||||
|   }, [validationSchema]) | ||||
|  | ||||
|   const [fetchState, setFetchState] = useFetchState() | ||||
|   const [message, setMessage] = useState<string | null>(null) | ||||
|   const [errors, setErrors] = useState<ErrorsObject<typeof validationSchema>>( | ||||
|     {} as any | ||||
|   ) | ||||
|  | ||||
|   const validate = useMemo(() => { | ||||
|     return ajv.compile(validationSchemaObject) | ||||
|   }, [validationSchemaObject]) | ||||
|  | ||||
|   const handleSubmit: HandleSubmit<typeof validationSchema> = (callback) => { | ||||
|     return async (formData, formElement) => { | ||||
|       setErrors({} as any) | ||||
|       setMessage(null) | ||||
|       formData = handleOptionalEmptyStringToNull( | ||||
|         formData, | ||||
|         validationSchemaObject.required | ||||
|       ) | ||||
|       formData = handleCheckboxBoolean(formData, validationSchemaObject) | ||||
|       const isValid = validate(formData) | ||||
|       if (!isValid) { | ||||
|         setFetchState('error') | ||||
|         const errors: ErrorsObject<typeof validationSchema> = {} as any | ||||
|         for (const property in validationSchemaObject.properties) { | ||||
|           const errorsForProperty = validate.errors?.filter((error) => { | ||||
|             return error.instancePath === `/${property}` | ||||
|           }) | ||||
|           errors[property as keyof typeof validationSchema] = | ||||
|             errorsForProperty != null && errorsForProperty.length > 0 | ||||
|               ? errorsForProperty | ||||
|               : undefined | ||||
|         } | ||||
|         setErrors(errors) | ||||
|       } else { | ||||
|         setErrors({} as any) | ||||
|         setFetchState('loading') | ||||
|         const message = await callback( | ||||
|           formData as Static<TObject<typeof validationSchema>>, | ||||
|           formElement | ||||
|         ) | ||||
|         if (message != null) { | ||||
|           const { value = null, type, properties } = message | ||||
|           setMessage(value) | ||||
|           setFetchState(type) | ||||
|           if (type === 'error') { | ||||
|             const propertiesErrors: ErrorsObject<typeof validationSchema> = | ||||
|               {} as any | ||||
|             for (const property in properties) { | ||||
|               propertiesErrors[property] = [ | ||||
|                 { | ||||
|                   keyword: 'message', | ||||
|                   message: properties[property], | ||||
|                   instancePath: `/${property}`, | ||||
|                   schemaPath: `#/properties/${property}/message`, | ||||
|                   params: {} | ||||
|                 } | ||||
|               ] | ||||
|             } | ||||
|             setErrors(propertiesErrors) | ||||
|           } | ||||
|         } | ||||
|       } | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   return { | ||||
|     handleSubmit, | ||||
|     fetchState, | ||||
|     setFetchState, | ||||
|     message, | ||||
|     setMessage, | ||||
|     errors | ||||
|   } | ||||
| } | ||||
							
								
								
									
										4
									
								
								src/index.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										4
									
								
								src/index.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,4 @@ | ||||
| export * from './components/Form' | ||||
| export * from './hooks/useFetchState' | ||||
| export * from './hooks/useForm' | ||||
| export * from './utils/ajv' | ||||
							
								
								
									
										24
									
								
								src/utils/ajv.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										24
									
								
								src/utils/ajv.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,24 @@ | ||||
| import addFormats from 'ajv-formats' | ||||
| import Ajv from 'ajv' | ||||
|  | ||||
| export const ajv = addFormats( | ||||
|   new Ajv({ | ||||
|     allErrors: true | ||||
|   }), | ||||
|   [ | ||||
|     'date-time', | ||||
|     'time', | ||||
|     'date', | ||||
|     'email', | ||||
|     'hostname', | ||||
|     'ipv4', | ||||
|     'ipv6', | ||||
|     'uri', | ||||
|     'uri-reference', | ||||
|     'uuid', | ||||
|     'uri-template', | ||||
|     'json-pointer', | ||||
|     'relative-json-pointer', | ||||
|     'regex' | ||||
|   ] | ||||
| ) | ||||
							
								
								
									
										25
									
								
								src/utils/handleCheckboxBoolean.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										25
									
								
								src/utils/handleCheckboxBoolean.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,25 @@ | ||||
| import type { TObject } from '@sinclair/typebox' | ||||
|  | ||||
| import type { ObjectAny } from './types' | ||||
|  | ||||
| export const handleCheckboxBoolean = ( | ||||
|   object: ObjectAny, | ||||
|   validateSchemaObject: TObject<ObjectAny> | ||||
| ): ObjectAny => { | ||||
|   const booleanProperties: string[] = [] | ||||
|   for (const property in validateSchemaObject.properties) { | ||||
|     const rule = validateSchemaObject.properties[property] | ||||
|     if (rule.type === 'boolean') { | ||||
|       booleanProperties.push(property) | ||||
|     } | ||||
|   } | ||||
|   for (const booleanProperty of booleanProperties) { | ||||
|     if (object[booleanProperty] == null) { | ||||
|       object[booleanProperty] = | ||||
|         validateSchemaObject.properties[booleanProperty].default | ||||
|     } else { | ||||
|       object[booleanProperty] = object[booleanProperty] === 'on' | ||||
|     } | ||||
|   } | ||||
|   return object | ||||
| } | ||||
							
								
								
									
										17
									
								
								src/utils/handleOptionalEmptyStringToNull.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										17
									
								
								src/utils/handleOptionalEmptyStringToNull.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,17 @@ | ||||
| export const handleOptionalEmptyStringToNull = <K>( | ||||
|   object: K, | ||||
|   required: string[] = [] | ||||
| ): K => { | ||||
|   return Object.fromEntries( | ||||
|     Object.entries(object).map(([key, value]) => { | ||||
|       if ( | ||||
|         typeof value === 'string' && | ||||
|         value.length === 0 && | ||||
|         !required.includes(key) | ||||
|       ) { | ||||
|         return [key, null] | ||||
|       } | ||||
|       return [key, value] | ||||
|     }) | ||||
|   ) as K | ||||
| } | ||||
							
								
								
									
										3
									
								
								src/utils/types.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								src/utils/types.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,3 @@ | ||||
| export interface ObjectAny { | ||||
|   [key: string]: any | ||||
| } | ||||
| @@ -1,7 +1,7 @@ | ||||
| import { defineConfig } from 'tsup' | ||||
|  | ||||
| export default defineConfig({ | ||||
|   entry: ['src/index.tsx'], | ||||
|   entry: ['src/index.ts'], | ||||
|   sourcemap: true, | ||||
|   clean: true, | ||||
|   platform: 'browser', | ||||
|   | ||||
		Reference in New Issue
	
	Block a user