feat: add form validation
This commit is contained in:
parent
c9bb631073
commit
17656c149a
50
README.md
50
README.md
@ -22,6 +22,8 @@
|
|||||||
|
|
||||||
**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.
|
**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.
|
||||||
|
|
||||||
|
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/).
|
Demo: [https://divlo.github.io/react-component-form/](https://divlo.github.io/react-component-form/).
|
||||||
|
|
||||||
## 💾 Install
|
## 💾 Install
|
||||||
@ -32,9 +34,12 @@ npm install --save react-component-form
|
|||||||
|
|
||||||
## ⚙️ Usage
|
## ⚙️ 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
|
```tsx
|
||||||
import React from 'react'
|
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 Example = () => {
|
||||||
const handleSubmit: HandleForm = (formData, formElement) => {
|
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.
|
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.
|
- `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).
|
- `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
|
## 💡 Contributing
|
||||||
|
|
||||||
Anyone can help to improve the project, submit a Feature Request, a bug report or
|
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",
|
"version": "0.0.0-development",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@sinclair/typebox": "0.24.28",
|
||||||
|
"ajv": "8.11.0",
|
||||||
|
"ajv-formats": "2.1.1"
|
||||||
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@commitlint/cli": "17.0.3",
|
"@commitlint/cli": "17.0.3",
|
||||||
"@commitlint/config-conventional": "17.0.3",
|
"@commitlint/config-conventional": "17.0.3",
|
||||||
@ -4356,12 +4361,15 @@
|
|||||||
"requires": {
|
"requires": {
|
||||||
"@commitlint/cli": "17.0.3",
|
"@commitlint/cli": "17.0.3",
|
||||||
"@commitlint/config-conventional": "17.0.3",
|
"@commitlint/config-conventional": "17.0.3",
|
||||||
|
"@sinclair/typebox": "0.24.28",
|
||||||
"@testing-library/react": "13.3.0",
|
"@testing-library/react": "13.3.0",
|
||||||
"@types/jest": "28.1.8",
|
"@types/jest": "28.1.8",
|
||||||
"@types/react": "18.0.17",
|
"@types/react": "18.0.17",
|
||||||
"@types/react-dom": "18.0.6",
|
"@types/react-dom": "18.0.6",
|
||||||
"@typescript-eslint/eslint-plugin": "5.35.1",
|
"@typescript-eslint/eslint-plugin": "5.35.1",
|
||||||
"@typescript-eslint/parser": "5.35.1",
|
"@typescript-eslint/parser": "5.35.1",
|
||||||
|
"ajv": "8.11.0",
|
||||||
|
"ajv-formats": "2.1.1",
|
||||||
"editorconfig-checker": "4.0.2",
|
"editorconfig-checker": "4.0.2",
|
||||||
"esbuild": "0.15.5",
|
"esbuild": "0.15.5",
|
||||||
"esbuild-jest": "0.5.0",
|
"esbuild-jest": "0.5.0",
|
||||||
|
59
package-lock.json
generated
59
package-lock.json
generated
@ -8,6 +8,11 @@
|
|||||||
"name": "react-component-form",
|
"name": "react-component-form",
|
||||||
"version": "0.0.0-development",
|
"version": "0.0.0-development",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@sinclair/typebox": "0.24.28",
|
||||||
|
"ajv": "8.11.0",
|
||||||
|
"ajv-formats": "2.1.1"
|
||||||
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@commitlint/cli": "17.0.3",
|
"@commitlint/cli": "17.0.3",
|
||||||
"@commitlint/config-conventional": "17.0.3",
|
"@commitlint/config-conventional": "17.0.3",
|
||||||
@ -2602,8 +2607,7 @@
|
|||||||
"node_modules/@sinclair/typebox": {
|
"node_modules/@sinclair/typebox": {
|
||||||
"version": "0.24.28",
|
"version": "0.24.28",
|
||||||
"resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.24.28.tgz",
|
"resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.24.28.tgz",
|
||||||
"integrity": "sha512-dgJd3HLOkLmz4Bw50eZx/zJwtBq65nms3N9VBYu5LTjJ883oBFkTyXRlCB/ZGGwqYpJJHA5zW2Ibhl5ngITfow==",
|
"integrity": "sha512-dgJd3HLOkLmz4Bw50eZx/zJwtBq65nms3N9VBYu5LTjJ883oBFkTyXRlCB/ZGGwqYpJJHA5zW2Ibhl5ngITfow=="
|
||||||
"dev": true
|
|
||||||
},
|
},
|
||||||
"node_modules/@sinonjs/commons": {
|
"node_modules/@sinonjs/commons": {
|
||||||
"version": "1.8.3",
|
"version": "1.8.3",
|
||||||
@ -3217,7 +3221,6 @@
|
|||||||
"version": "8.11.0",
|
"version": "8.11.0",
|
||||||
"resolved": "https://registry.npmjs.org/ajv/-/ajv-8.11.0.tgz",
|
"resolved": "https://registry.npmjs.org/ajv/-/ajv-8.11.0.tgz",
|
||||||
"integrity": "sha512-wGgprdCvMalC0BztXvitD2hC04YffAvtsUn93JbGXYLAtCUO4xd17mCCZQxUOItiBwZvJScWo8NIvQMQ71rdpg==",
|
"integrity": "sha512-wGgprdCvMalC0BztXvitD2hC04YffAvtsUn93JbGXYLAtCUO4xd17mCCZQxUOItiBwZvJScWo8NIvQMQ71rdpg==",
|
||||||
"dev": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"fast-deep-equal": "^3.1.1",
|
"fast-deep-equal": "^3.1.1",
|
||||||
"json-schema-traverse": "^1.0.0",
|
"json-schema-traverse": "^1.0.0",
|
||||||
@ -3229,6 +3232,22 @@
|
|||||||
"url": "https://github.com/sponsors/epoberezkin"
|
"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": {
|
"node_modules/ansi-escapes": {
|
||||||
"version": "4.3.2",
|
"version": "4.3.2",
|
||||||
"resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz",
|
"resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz",
|
||||||
@ -6122,8 +6141,7 @@
|
|||||||
"node_modules/fast-deep-equal": {
|
"node_modules/fast-deep-equal": {
|
||||||
"version": "3.1.3",
|
"version": "3.1.3",
|
||||||
"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
|
"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
|
||||||
"integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==",
|
"integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="
|
||||||
"dev": true
|
|
||||||
},
|
},
|
||||||
"node_modules/fast-diff": {
|
"node_modules/fast-diff": {
|
||||||
"version": "1.2.0",
|
"version": "1.2.0",
|
||||||
@ -9897,8 +9915,7 @@
|
|||||||
"node_modules/json-schema-traverse": {
|
"node_modules/json-schema-traverse": {
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz",
|
||||||
"integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==",
|
"integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="
|
||||||
"dev": true
|
|
||||||
},
|
},
|
||||||
"node_modules/json-stable-stringify-without-jsonify": {
|
"node_modules/json-stable-stringify-without-jsonify": {
|
||||||
"version": "1.0.1",
|
"version": "1.0.1",
|
||||||
@ -14081,7 +14098,6 @@
|
|||||||
"version": "2.1.1",
|
"version": "2.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz",
|
||||||
"integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==",
|
"integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==",
|
||||||
"dev": true,
|
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=6"
|
"node": ">=6"
|
||||||
}
|
}
|
||||||
@ -14462,7 +14478,6 @@
|
|||||||
"version": "2.0.2",
|
"version": "2.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz",
|
"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==",
|
"integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==",
|
||||||
"dev": true,
|
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=0.10.0"
|
"node": ">=0.10.0"
|
||||||
}
|
}
|
||||||
@ -16621,7 +16636,6 @@
|
|||||||
"version": "4.4.1",
|
"version": "4.4.1",
|
||||||
"resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz",
|
"resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz",
|
||||||
"integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==",
|
"integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==",
|
||||||
"dev": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"punycode": "^2.1.0"
|
"punycode": "^2.1.0"
|
||||||
}
|
}
|
||||||
@ -19041,8 +19055,7 @@
|
|||||||
"@sinclair/typebox": {
|
"@sinclair/typebox": {
|
||||||
"version": "0.24.28",
|
"version": "0.24.28",
|
||||||
"resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.24.28.tgz",
|
"resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.24.28.tgz",
|
||||||
"integrity": "sha512-dgJd3HLOkLmz4Bw50eZx/zJwtBq65nms3N9VBYu5LTjJ883oBFkTyXRlCB/ZGGwqYpJJHA5zW2Ibhl5ngITfow==",
|
"integrity": "sha512-dgJd3HLOkLmz4Bw50eZx/zJwtBq65nms3N9VBYu5LTjJ883oBFkTyXRlCB/ZGGwqYpJJHA5zW2Ibhl5ngITfow=="
|
||||||
"dev": true
|
|
||||||
},
|
},
|
||||||
"@sinonjs/commons": {
|
"@sinonjs/commons": {
|
||||||
"version": "1.8.3",
|
"version": "1.8.3",
|
||||||
@ -19523,7 +19536,6 @@
|
|||||||
"version": "8.11.0",
|
"version": "8.11.0",
|
||||||
"resolved": "https://registry.npmjs.org/ajv/-/ajv-8.11.0.tgz",
|
"resolved": "https://registry.npmjs.org/ajv/-/ajv-8.11.0.tgz",
|
||||||
"integrity": "sha512-wGgprdCvMalC0BztXvitD2hC04YffAvtsUn93JbGXYLAtCUO4xd17mCCZQxUOItiBwZvJScWo8NIvQMQ71rdpg==",
|
"integrity": "sha512-wGgprdCvMalC0BztXvitD2hC04YffAvtsUn93JbGXYLAtCUO4xd17mCCZQxUOItiBwZvJScWo8NIvQMQ71rdpg==",
|
||||||
"dev": true,
|
|
||||||
"requires": {
|
"requires": {
|
||||||
"fast-deep-equal": "^3.1.1",
|
"fast-deep-equal": "^3.1.1",
|
||||||
"json-schema-traverse": "^1.0.0",
|
"json-schema-traverse": "^1.0.0",
|
||||||
@ -19531,6 +19543,14 @@
|
|||||||
"uri-js": "^4.2.2"
|
"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": {
|
"ansi-escapes": {
|
||||||
"version": "4.3.2",
|
"version": "4.3.2",
|
||||||
"resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz",
|
"resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz",
|
||||||
@ -21631,8 +21651,7 @@
|
|||||||
"fast-deep-equal": {
|
"fast-deep-equal": {
|
||||||
"version": "3.1.3",
|
"version": "3.1.3",
|
||||||
"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
|
"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
|
||||||
"integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==",
|
"integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="
|
||||||
"dev": true
|
|
||||||
},
|
},
|
||||||
"fast-diff": {
|
"fast-diff": {
|
||||||
"version": "1.2.0",
|
"version": "1.2.0",
|
||||||
@ -24585,8 +24604,7 @@
|
|||||||
"json-schema-traverse": {
|
"json-schema-traverse": {
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz",
|
||||||
"integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==",
|
"integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="
|
||||||
"dev": true
|
|
||||||
},
|
},
|
||||||
"json-stable-stringify-without-jsonify": {
|
"json-stable-stringify-without-jsonify": {
|
||||||
"version": "1.0.1",
|
"version": "1.0.1",
|
||||||
@ -27565,8 +27583,7 @@
|
|||||||
"punycode": {
|
"punycode": {
|
||||||
"version": "2.1.1",
|
"version": "2.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz",
|
||||||
"integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==",
|
"integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A=="
|
||||||
"dev": true
|
|
||||||
},
|
},
|
||||||
"q": {
|
"q": {
|
||||||
"version": "1.5.1",
|
"version": "1.5.1",
|
||||||
@ -27855,8 +27872,7 @@
|
|||||||
"require-from-string": {
|
"require-from-string": {
|
||||||
"version": "2.0.2",
|
"version": "2.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz",
|
"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==",
|
"integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw=="
|
||||||
"dev": true
|
|
||||||
},
|
},
|
||||||
"requires-port": {
|
"requires-port": {
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
@ -29543,7 +29559,6 @@
|
|||||||
"version": "4.4.1",
|
"version": "4.4.1",
|
||||||
"resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz",
|
"resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz",
|
||||||
"integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==",
|
"integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==",
|
||||||
"dev": true,
|
|
||||||
"requires": {
|
"requires": {
|
||||||
"punycode": "^2.1.0"
|
"punycode": "^2.1.0"
|
||||||
}
|
}
|
||||||
|
@ -36,6 +36,11 @@
|
|||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"react": ">=16"
|
"react": ">=16"
|
||||||
},
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@sinclair/typebox": "0.24.28",
|
||||||
|
"ajv": "8.11.0",
|
||||||
|
"ajv-formats": "2.1.1"
|
||||||
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@commitlint/cli": "17.0.3",
|
"@commitlint/cli": "17.0.3",
|
||||||
"@commitlint/config-conventional": "17.0.3",
|
"@commitlint/config-conventional": "17.0.3",
|
||||||
@ -46,6 +51,8 @@
|
|||||||
"@typescript-eslint/eslint-plugin": "5.35.1",
|
"@typescript-eslint/eslint-plugin": "5.35.1",
|
||||||
"@typescript-eslint/parser": "5.35.1",
|
"@typescript-eslint/parser": "5.35.1",
|
||||||
"editorconfig-checker": "4.0.2",
|
"editorconfig-checker": "4.0.2",
|
||||||
|
"esbuild": "0.15.5",
|
||||||
|
"esbuild-jest": "0.5.0",
|
||||||
"eslint": "8.22.0",
|
"eslint": "8.22.0",
|
||||||
"eslint-config-conventions": "3.0.0",
|
"eslint-config-conventions": "3.0.0",
|
||||||
"eslint-config-prettier": "8.5.0",
|
"eslint-config-prettier": "8.5.0",
|
||||||
@ -53,8 +60,6 @@
|
|||||||
"eslint-plugin-prettier": "4.2.1",
|
"eslint-plugin-prettier": "4.2.1",
|
||||||
"eslint-plugin-promise": "6.0.1",
|
"eslint-plugin-promise": "6.0.1",
|
||||||
"eslint-plugin-unicorn": "43.0.2",
|
"eslint-plugin-unicorn": "43.0.2",
|
||||||
"esbuild": "0.15.5",
|
|
||||||
"esbuild-jest": "0.5.0",
|
|
||||||
"jest": "29.0.0",
|
"jest": "29.0.0",
|
||||||
"jest-environment-jsdom": "29.0.0",
|
"jest-environment-jsdom": "29.0.0",
|
||||||
"markdownlint-cli2": "0.5.1",
|
"markdownlint-cli2": "0.5.1",
|
||||||
|
@ -9,7 +9,7 @@ export type HandleForm = (
|
|||||||
formElement: HTMLFormElement
|
formElement: HTMLFormElement
|
||||||
) => void | Promise<void>
|
) => void | Promise<void>
|
||||||
|
|
||||||
export interface ReactFormProps
|
interface ReactFormProps
|
||||||
extends Omit<React.HTMLProps<HTMLFormElement>, 'onSubmit' | 'onChange'> {}
|
extends Omit<React.HTMLProps<HTMLFormElement>, 'onSubmit' | 'onChange'> {}
|
||||||
|
|
||||||
export interface FormProps extends ReactFormProps {
|
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'
|
import { defineConfig } from 'tsup'
|
||||||
|
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
entry: ['src/index.tsx'],
|
entry: ['src/index.ts'],
|
||||||
sourcemap: true,
|
sourcemap: true,
|
||||||
clean: true,
|
clean: true,
|
||||||
platform: 'browser',
|
platform: 'browser',
|
||||||
|
Reference in New Issue
Block a user