Compare commits
11 Commits
v2.0.0
...
v3.1.0-bet
Author | SHA1 | Date | |
---|---|---|---|
37b4b9b990 | |||
7e3ef0f492 | |||
c0034d5af6 | |||
694d31e68d | |||
a2edafdc22 | |||
17656c149a | |||
c9bb631073 | |||
8cbe5c3bf2 | |||
686b5643b3 | |||
ec4929d7d8 | |||
079d3f9d50 |
@ -1,9 +1,11 @@
|
||||
# For more information see: https://editorconfig.org/
|
||||
|
||||
root = true
|
||||
|
||||
[*]
|
||||
charset = utf-8
|
||||
indent_style = space
|
||||
indent_size = 2
|
||||
end_of_line = lf
|
||||
insert_final_newline = true
|
||||
charset = utf-8
|
||||
trim_trailing_whitespace = true
|
||||
insert_final_newline = true
|
||||
|
5
.eslintignore
Normal file
5
.eslintignore
Normal file
@ -0,0 +1,5 @@
|
||||
node_modules
|
||||
build
|
||||
dist
|
||||
.parcel-cache
|
||||
example
|
14
.eslintrc.json
Normal file
14
.eslintrc.json
Normal file
@ -0,0 +1,14 @@
|
||||
{
|
||||
"extends": ["conventions", "prettier"],
|
||||
"plugins": ["prettier", "import", "unicorn"],
|
||||
"parserOptions": {
|
||||
"project": "./tsconfig.json"
|
||||
},
|
||||
"env": {
|
||||
"jest": true,
|
||||
"browser": true
|
||||
},
|
||||
"rules": {
|
||||
"prettier/prettier": "error"
|
||||
}
|
||||
}
|
1
.gitattributes
vendored
Normal file
1
.gitattributes
vendored
Normal file
@ -0,0 +1 @@
|
||||
* text=auto eol=lf
|
17
.github/workflows/build.yml
vendored
17
.github/workflows/build.yml
vendored
@ -2,28 +2,27 @@ name: 'Build'
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [master]
|
||||
branches: [master, develop]
|
||||
pull_request:
|
||||
branches: [master]
|
||||
branches: [master, develop]
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: 'ubuntu-latest'
|
||||
steps:
|
||||
- uses: 'actions/checkout@v2'
|
||||
- uses: 'actions/checkout@v3.0.0'
|
||||
|
||||
- name: 'Use Node.js'
|
||||
uses: 'actions/setup-node@v2.1.5'
|
||||
uses: 'actions/setup-node@v3.1.0'
|
||||
with:
|
||||
node-version: '14.x'
|
||||
|
||||
- run: 'npm install --global npm@7'
|
||||
node-version: 'lts/*'
|
||||
cache: 'npm'
|
||||
|
||||
- name: 'Install'
|
||||
run: 'npm ci'
|
||||
run: 'npm install'
|
||||
|
||||
- name: 'Build Package'
|
||||
run: 'npm run build'
|
||||
|
||||
- name: 'Build Example'
|
||||
run: 'cd example && npm ci && npm run build'
|
||||
run: 'cd example && npm install && npm run build'
|
||||
|
16
.github/workflows/lint.yml
vendored
16
.github/workflows/lint.yml
vendored
@ -2,27 +2,27 @@ name: 'Lint'
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [master]
|
||||
branches: [master, develop]
|
||||
pull_request:
|
||||
branches: [master]
|
||||
branches: [master, develop]
|
||||
|
||||
jobs:
|
||||
lint:
|
||||
runs-on: 'ubuntu-latest'
|
||||
steps:
|
||||
- uses: 'actions/checkout@v2'
|
||||
- uses: 'actions/checkout@v3.0.0'
|
||||
|
||||
- name: 'Use Node.js'
|
||||
uses: 'actions/setup-node@v2.1.5'
|
||||
uses: 'actions/setup-node@v3.1.0'
|
||||
with:
|
||||
node-version: '14.x'
|
||||
|
||||
- run: 'npm install --global npm@7'
|
||||
node-version: 'lts/*'
|
||||
cache: 'npm'
|
||||
|
||||
- name: 'Install'
|
||||
run: 'npm ci'
|
||||
run: 'npm install'
|
||||
|
||||
- run: 'npm run lint:commit -- --to "${{ github.sha }}"'
|
||||
- run: 'npm run lint:editorconfig'
|
||||
- run: 'npm run lint:markdown'
|
||||
- run: 'npm run lint:typescript'
|
||||
- run: 'npm run lint:prettier'
|
||||
|
22
.github/workflows/release.yml
vendored
22
.github/workflows/release.yml
vendored
@ -2,36 +2,26 @@ name: 'Release'
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [master]
|
||||
branches: [master, develop]
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: 'ubuntu-latest'
|
||||
steps:
|
||||
- uses: 'actions/checkout@v2'
|
||||
- uses: 'actions/checkout@v3.0.0'
|
||||
|
||||
- name: 'Use Node.js'
|
||||
uses: 'actions/setup-node@v2.1.5'
|
||||
uses: 'actions/setup-node@v3.1.0'
|
||||
with:
|
||||
node-version: '14.x'
|
||||
|
||||
- run: 'npm install --global npm@7'
|
||||
node-version: 'lts/*'
|
||||
cache: 'npm'
|
||||
|
||||
- name: 'Install'
|
||||
run: 'npm ci'
|
||||
run: 'npm install'
|
||||
|
||||
- name: 'Build Package'
|
||||
run: 'npm run build'
|
||||
|
||||
- name: 'Build Example'
|
||||
run: 'cd example && npm ci && npm run build'
|
||||
|
||||
- name: 'Deploy Example'
|
||||
uses: 'JamesIves/github-pages-deploy-action@4.1.4'
|
||||
with:
|
||||
branch: 'gh-pages'
|
||||
folder: 'example/dist'
|
||||
|
||||
- name: 'Release'
|
||||
run: 'npm run release'
|
||||
env:
|
||||
|
15
.github/workflows/test.yml
vendored
15
.github/workflows/test.yml
vendored
@ -2,25 +2,24 @@ name: 'Test'
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [master]
|
||||
branches: [master, develop]
|
||||
pull_request:
|
||||
branches: [master]
|
||||
branches: [master, develop]
|
||||
|
||||
jobs:
|
||||
test:
|
||||
runs-on: 'ubuntu-latest'
|
||||
steps:
|
||||
- uses: 'actions/checkout@v2'
|
||||
- uses: 'actions/checkout@v3.0.0'
|
||||
|
||||
- name: 'Use Node.js'
|
||||
uses: 'actions/setup-node@v2.1.5'
|
||||
uses: 'actions/setup-node@v3.1.0'
|
||||
with:
|
||||
node-version: '14.x'
|
||||
|
||||
- run: 'npm install --global npm@7'
|
||||
node-version: 'lts/*'
|
||||
cache: 'npm'
|
||||
|
||||
- name: 'Install'
|
||||
run: 'npm ci'
|
||||
run: 'npm install'
|
||||
|
||||
- name: 'Test'
|
||||
run: 'npm run test'
|
||||
|
39
.gitignore
vendored
39
.gitignore
vendored
@ -1,6 +1,39 @@
|
||||
*.log
|
||||
.DS_Store
|
||||
# dependencies
|
||||
node_modules
|
||||
.cache
|
||||
.npm
|
||||
|
||||
# production
|
||||
build
|
||||
dist
|
||||
|
||||
# testing
|
||||
coverage
|
||||
.nyc_output
|
||||
|
||||
# envs
|
||||
.env
|
||||
.env.production
|
||||
|
||||
# debug
|
||||
npm-debug.log*
|
||||
|
||||
# IDEs and editors
|
||||
/.idea
|
||||
.project
|
||||
.classpath
|
||||
.c9/
|
||||
*.launch
|
||||
.settings/
|
||||
*.sublime-workspace
|
||||
|
||||
# IDE - VSCode
|
||||
.vscode/*
|
||||
!.vscode/settings.json
|
||||
!.vscode/tasks.json
|
||||
!.vscode/launch.json
|
||||
!.vscode/extensions.json
|
||||
|
||||
# misc
|
||||
.DS_Store
|
||||
.parcel-cache
|
||||
.cache
|
||||
|
11
.markdownlint-cli2.jsonc
Normal file
11
.markdownlint-cli2.jsonc
Normal file
@ -0,0 +1,11 @@
|
||||
{
|
||||
"config": {
|
||||
"default": true,
|
||||
"MD013": false,
|
||||
"MD024": false,
|
||||
"MD033": false,
|
||||
"MD041": false
|
||||
},
|
||||
"globs": ["**/*.{md,mdx}"],
|
||||
"ignores": ["**/node_modules"]
|
||||
}
|
@ -1,6 +0,0 @@
|
||||
{
|
||||
"default": true,
|
||||
"MD013": false,
|
||||
"MD033": false,
|
||||
"MD041": false
|
||||
}
|
@ -1,6 +1,6 @@
|
||||
{
|
||||
"semi": false,
|
||||
"singleQuote": true,
|
||||
"jsxSingleQuote": true,
|
||||
"semi": false,
|
||||
"trailingComma": "none"
|
||||
}
|
||||
|
@ -1,5 +1,8 @@
|
||||
{
|
||||
"branches": ["master"],
|
||||
"branches": [
|
||||
"master",
|
||||
{ "name": "develop", "prerelease": "beta", "channel": "beta" }
|
||||
],
|
||||
"plugins": [
|
||||
[
|
||||
"@semantic-release/commit-analyzer",
|
||||
|
@ -17,7 +17,7 @@ Thanks a lot for your interest in contributing to **react-component-form**! 🎉
|
||||
|
||||
- **Please first discuss** the change you wish to make via issues.
|
||||
|
||||
- Ensure your code respect the linter.
|
||||
- Ensure your code respect linting.
|
||||
|
||||
- Make sure your **code passes the tests**.
|
||||
|
||||
|
57
README.md
57
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,11 +34,14 @@ 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 = () => {
|
||||
export const Example = () => {
|
||||
const handleSubmit: HandleForm = (formData, formElement) => {
|
||||
console.log(formData) // { inputName: 'value of the input' }
|
||||
formElement.reset()
|
||||
@ -51,15 +56,55 @@ 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 { HandleUseFormCallback } from 'react-component-form'
|
||||
|
||||
const schema = {
|
||||
inputName: {
|
||||
type: 'string',
|
||||
minLength: 3,
|
||||
maxLength: 20
|
||||
}
|
||||
}
|
||||
|
||||
export const Example = () => {
|
||||
const { errors, handleUseForm } = useForm(schema)
|
||||
|
||||
const onSubmit: HandleUseFormCallback<typeof schema> = (
|
||||
formData,
|
||||
formElement
|
||||
) => {
|
||||
console.log(formData) // { inputName: 'value of the input validated' }
|
||||
formElement.reset()
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<Form onSubmit={handleUseForm(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
|
||||
|
@ -1,3 +0,0 @@
|
||||
node_modules
|
||||
.cache
|
||||
dist
|
2
example/globals.d.ts
vendored
2
example/globals.d.ts
vendored
@ -1 +1 @@
|
||||
declare module "*.jpg"
|
||||
declare module '*.jpg'
|
||||
|
@ -3,12 +3,11 @@
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<meta http-equiv="X-UA-Compatible" content="ie=edge" />
|
||||
<link rel="shortcut icon" href="./github.jpg" type="image/jpg">
|
||||
<link rel="shortcut icon" href="./github.jpg" type="image/jpg" />
|
||||
<title>react-component-form</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script src="./index.tsx"></script>
|
||||
<script type="module" src="./index.tsx"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
@ -1,63 +1,38 @@
|
||||
import * as React from 'react'
|
||||
import * as ReactDOM from 'react-dom'
|
||||
import { Form, HandleForm } from '../.'
|
||||
import { createRoot } from 'react-dom/client'
|
||||
import React from 'react'
|
||||
import { Form, useForm } from 'react-component-form'
|
||||
import type { HandleUseFormCallback } from 'react-component-form'
|
||||
|
||||
import './index.css'
|
||||
import GitHubLogo from 'url:./github.jpg'
|
||||
|
||||
const App = () => {
|
||||
const handleSubmit: HandleForm = (formData, formElement) => {
|
||||
console.clear()
|
||||
console.log('onSubmit: ', formData)
|
||||
formElement.reset()
|
||||
const schema = {
|
||||
inputName: {
|
||||
type: 'string',
|
||||
minLength: 3,
|
||||
maxLength: 20
|
||||
}
|
||||
}
|
||||
|
||||
const handleChange: HandleForm = (formData) => {
|
||||
console.log('onChange: ', formData)
|
||||
export const Example = () => {
|
||||
const { errors, handleUseForm } = useForm(schema)
|
||||
|
||||
const onSubmit: HandleUseFormCallback<typeof schema> = (
|
||||
formData,
|
||||
formElement
|
||||
) => {
|
||||
console.log(formData) // { inputName: 'value of the input validated' }
|
||||
formElement.reset()
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<div className='container'>
|
||||
<h2>{'<Form />'}</h2>
|
||||
<h5 className='title-install'>npm install --save react-component-form</h5>
|
||||
<Form onSubmit={handleUseForm(onSubmit)}>
|
||||
<input type='text' name='inputName' />
|
||||
{errors.inputName != null && <p>{errors.inputName[0].message}</p>}
|
||||
|
||||
<Form onSubmit={handleSubmit} onChange={handleChange}>
|
||||
<div className='form-group'>
|
||||
<label htmlFor='name'>Name :</label>
|
||||
<input
|
||||
className='form-control'
|
||||
type='text'
|
||||
name='name'
|
||||
id='name'
|
||||
placeholder='name'
|
||||
/>
|
||||
</div>
|
||||
|
||||
<button type='submit' className='btn btn-primary'>
|
||||
Submit
|
||||
</button>
|
||||
</Form>
|
||||
|
||||
<div className='result-container'>
|
||||
<h4>
|
||||
Try the form and Inspect the console{' '}
|
||||
<span role='img' aria-label='smiley'>
|
||||
😃
|
||||
</span>
|
||||
</h4>
|
||||
</div>
|
||||
|
||||
<div className='github-logo'>
|
||||
<a
|
||||
target='_blank'
|
||||
rel='noopener noreferrer'
|
||||
href='https://github.com/Divlo/react-component-form'
|
||||
>
|
||||
<img width='30px' alt='github' src={GitHubLogo} />
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<button type='submit'>Submit</button>
|
||||
</Form>
|
||||
)
|
||||
}
|
||||
|
||||
ReactDOM.render(<App />, document.getElementById('root'))
|
||||
const container = document.getElementById('root') as HTMLElement
|
||||
const root = createRoot(container)
|
||||
root.render(<Example />)
|
||||
|
27560
example/package-lock.json
generated
27560
example/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -1,20 +1,22 @@
|
||||
{
|
||||
"name": "example",
|
||||
"version": "1.0.0",
|
||||
"license": "MIT",
|
||||
"type": "module",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"start": "parcel index.html",
|
||||
"build": "parcel build index.html --public-url '/react-component-form/'"
|
||||
"build": "parcel build index.html --public-url \"/react-component-form/\""
|
||||
},
|
||||
"dependencies": {
|
||||
"react": "file:../node_modules/react",
|
||||
"react-component-form": "file:..",
|
||||
"react-dom": "file:../node_modules/react-dom"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@parcel/transformer-image": "2.0.0-beta.2",
|
||||
"@types/react": "17.0.11",
|
||||
"@types/react-dom": "17.0.7",
|
||||
"parcel": "2.0.0-beta.2",
|
||||
"typescript": "4.3.3"
|
||||
"@parcel/transformer-image": "2.7.0",
|
||||
"@types/react": "18.0.17",
|
||||
"@types/react-dom": "18.0.6",
|
||||
"parcel": "2.7.0",
|
||||
"process": "^0.11.10",
|
||||
"typescript": "4.7.4"
|
||||
}
|
||||
}
|
||||
|
@ -1,18 +1,12 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"allowSyntheticDefaultImports": false,
|
||||
"target": "es5",
|
||||
"module": "commonjs",
|
||||
"module": "ESNext",
|
||||
"lib": ["ESNext", "DOM", "DOM.Iterable"],
|
||||
"target": "ESNext",
|
||||
"jsx": "react",
|
||||
"moduleResolution": "node",
|
||||
"noImplicitAny": false,
|
||||
"noUnusedLocals": false,
|
||||
"noUnusedParameters": false,
|
||||
"removeComments": true,
|
||||
"strictNullChecks": true,
|
||||
"preserveConstEnums": true,
|
||||
"sourceMap": true,
|
||||
"lib": ["es2015", "es2016", "dom"],
|
||||
"types": ["node"]
|
||||
"strict": true,
|
||||
"esModuleInterop": true
|
||||
}
|
||||
}
|
||||
|
7
jest.config.json
Normal file
7
jest.config.json
Normal file
@ -0,0 +1,7 @@
|
||||
{
|
||||
"testEnvironment": "jsdom",
|
||||
"rootDir": "./src",
|
||||
"transform": {
|
||||
"^.+\\.tsx?$": "esbuild-jest"
|
||||
}
|
||||
}
|
29560
package-lock.json
generated
29560
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
64
package.json
64
package.json
@ -1,12 +1,14 @@
|
||||
{
|
||||
"name": "react-component-form",
|
||||
"version": "0.0.0-development",
|
||||
"public": true,
|
||||
"type": "module",
|
||||
"description": "Manage React Forms with ease.",
|
||||
"author": "Divlo <contact@divlo.fr>",
|
||||
"license": "MIT",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/Divlo/react-component-form.git"
|
||||
"url": "https://github.com/Divlo/react-component-form.git"
|
||||
},
|
||||
"keywords": [
|
||||
"react-form",
|
||||
@ -15,41 +17,57 @@
|
||||
"bugs": {
|
||||
"url": "https://github.com/Divlo/react-component-form/issues"
|
||||
},
|
||||
"homepage": "https://github.com/Divlo/react-component-form#readme",
|
||||
"module": "dist/react-component-form.esm.js",
|
||||
"homepage": "https://github.com/Divlo/react-component-form",
|
||||
"main": "dist/index.js",
|
||||
"typings": "dist/index.d.ts",
|
||||
"types": "dist/index.d.ts",
|
||||
"files": [
|
||||
"dist"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
"scripts": {
|
||||
"start": "tsdx watch",
|
||||
"build": "tsdx build",
|
||||
"test": "tsdx test --passWithNoTests",
|
||||
"build": "tsup",
|
||||
"test": "jest",
|
||||
"lint:commit": "commitlint",
|
||||
"lint:editorconfig": "editorconfig-checker",
|
||||
"lint:markdown": "markdownlint '**/*.md' --dot --ignore node_modules",
|
||||
"lint:typescript": "tsdx lint",
|
||||
"lint:markdown": "markdownlint-cli2",
|
||||
"lint:typescript": "eslint \"**/*.{js,jsx,ts,tsx}\"",
|
||||
"lint:prettier": "prettier \".\" --check --ignore-path \".gitignore\"",
|
||||
"release": "semantic-release"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": ">=16"
|
||||
},
|
||||
"dependencies": {
|
||||
"@sinclair/typebox": "0.24.28",
|
||||
"ajv": "8.11.0",
|
||||
"ajv-formats": "2.1.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@commitlint/cli": "12.1.4",
|
||||
"@commitlint/config-conventional": "12.1.4",
|
||||
"@testing-library/react": "11.2.7",
|
||||
"@types/react": "17.0.11",
|
||||
"@types/react-dom": "17.0.7",
|
||||
"@commitlint/cli": "17.0.3",
|
||||
"@commitlint/config-conventional": "17.0.3",
|
||||
"@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",
|
||||
"editorconfig-checker": "4.0.2",
|
||||
"markdownlint-cli": "0.27.1",
|
||||
"react": "17.0.2",
|
||||
"react-dom": "17.0.2",
|
||||
"semantic-release": "17.4.4",
|
||||
"tsdx": "0.14.1",
|
||||
"typescript": "4.3.3"
|
||||
"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",
|
||||
"eslint-plugin-import": "2.26.0",
|
||||
"eslint-plugin-prettier": "4.2.1",
|
||||
"eslint-plugin-promise": "6.0.1",
|
||||
"eslint-plugin-unicorn": "43.0.2",
|
||||
"jest": "29.0.0",
|
||||
"jest-environment-jsdom": "29.0.0",
|
||||
"markdownlint-cli2": "0.5.1",
|
||||
"prettier": "2.7.1",
|
||||
"react": "18.2.0",
|
||||
"react-dom": "18.2.0",
|
||||
"semantic-release": "19.0.5",
|
||||
"tsup": "6.2.2",
|
||||
"typescript": "4.7.4"
|
||||
}
|
||||
}
|
||||
|
@ -1,7 +1,7 @@
|
||||
import React from 'react'
|
||||
import { render, cleanup, fireEvent } from '@testing-library/react'
|
||||
|
||||
import { Form, HandleForm } from '../src'
|
||||
import { Form, HandleForm } from '..'
|
||||
|
||||
afterEach(cleanup)
|
||||
|
@ -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 {
|
||||
@ -17,11 +17,13 @@ export interface FormProps extends ReactFormProps {
|
||||
onChange?: HandleForm
|
||||
}
|
||||
|
||||
export const getFormDataObject = (formElement: HTMLFormElement): FormDataObject => {
|
||||
export const getFormDataObject = (
|
||||
formElement: HTMLFormElement
|
||||
): FormDataObject => {
|
||||
return Object.fromEntries<FormDataEntryValue>(new FormData(formElement))
|
||||
}
|
||||
|
||||
export const Form = (props: FormProps): JSX.Element => {
|
||||
export const Form: React.FC<FormProps> = (props) => {
|
||||
const { onSubmit, onChange, children, ...rest } = props
|
||||
const formRef = useRef<HTMLFormElement>(null)
|
||||
|
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]
|
||||
}
|
147
src/hooks/useForm.ts
Normal file
147
src/hooks/useForm.ts
Normal file
@ -0,0 +1,147 @@
|
||||
import { useMemo, useState } from 'react'
|
||||
import { SchemaOptions, Static, TObject, 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 Schema = SchemaOptions
|
||||
|
||||
export type Error = ErrorObject
|
||||
|
||||
export type ErrorsObject<K extends Schema> = {
|
||||
[key in keyof Partial<K>]: Error[] | undefined
|
||||
}
|
||||
|
||||
export type HandleUseFormCallback<K extends Schema> = (
|
||||
formData: Static<TObject<K>>,
|
||||
formElement: HTMLFormElement
|
||||
) => Promise<Message<K> | null> | Message<K> | null
|
||||
|
||||
export type HandleUseForm<K extends Schema> = (
|
||||
callback?: HandleUseFormCallback<K>
|
||||
) => HandleForm
|
||||
|
||||
export interface GlobalMessage {
|
||||
type: 'error' | 'success'
|
||||
value?: string
|
||||
properties?: undefined
|
||||
}
|
||||
|
||||
export interface PropertiesMessage<K extends Schema> {
|
||||
type: 'error'
|
||||
value?: string
|
||||
properties: { [key in keyof Partial<K>]: string }
|
||||
}
|
||||
|
||||
export type Message<K extends Schema> = GlobalMessage | PropertiesMessage<K>
|
||||
|
||||
export interface UseFormResult<K extends Schema> {
|
||||
handleUseForm: HandleUseForm<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 Schema>(
|
||||
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 handleUseForm: HandleUseForm<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)
|
||||
if (callback != null) {
|
||||
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: {},
|
||||
data: formData[property]
|
||||
}
|
||||
]
|
||||
}
|
||||
setErrors(propertiesErrors)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
handleUseForm,
|
||||
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'
|
25
src/utils/ajv.ts
Normal file
25
src/utils/ajv.ts
Normal file
@ -0,0 +1,25 @@
|
||||
import addFormats from 'ajv-formats'
|
||||
import Ajv from 'ajv'
|
||||
|
||||
export const ajv = addFormats(
|
||||
new Ajv({
|
||||
allErrors: true,
|
||||
verbose: 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
|
||||
}
|
@ -17,6 +17,6 @@
|
||||
"esModuleInterop": true,
|
||||
"skipLibCheck": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"noEmit": true,
|
||||
"noEmit": true
|
||||
}
|
||||
}
|
||||
|
12
tsup.config.js
Normal file
12
tsup.config.js
Normal file
@ -0,0 +1,12 @@
|
||||
import { defineConfig } from 'tsup'
|
||||
|
||||
export default defineConfig({
|
||||
entry: ['src/index.ts'],
|
||||
sourcemap: true,
|
||||
clean: true,
|
||||
platform: 'browser',
|
||||
target: 'esnext',
|
||||
format: ['esm'],
|
||||
minify: true,
|
||||
dts: true
|
||||
})
|
Reference in New Issue
Block a user