12 Commits

Author SHA1 Message Date
040e3a0ae1 style: fix linting 2023-04-02 22:10:52 +02:00
5bb73df804 fix: rename value to message in HandleUseFormCallback return type
BREAKING CHANGE: Migrate your onSubmit handlers to return a `message` instead of `value`
2023-04-02 22:08:32 +02:00
69f12002c7 build(deps): update latest
BREAKING CHANGE: peerDependencies: `react@>=18.2.0`
2023-04-02 21:52:34 +02:00
85eb53d60c chore: fix vercel build error for example 2023-01-10 21:53:53 +01:00
45c072f2bd style: fix linting 2023-01-10 21:27:25 +01:00
cdff824ca5 fix: update dependencies to latest 2023-01-10 21:23:32 +01:00
54ef5ceea1 ci: fix timeout 2022-11-08 11:40:39 +01:00
48d4fb6f75 build(deps): bump Next.js to v13 2022-11-08 11:28:57 +01:00
1683474fa6 chore: remove usage of styled-jsx 2022-10-03 21:23:17 +02:00
a37453a115 style(example): fix linting 2022-09-21 09:38:57 +02:00
fcc2b2ea77 fix(types): improve Schema type for useForm 2022-09-21 09:33:09 +02:00
d213893d5d ci: avoid running twice (develop and master branch) [skip-ci] 2022-08-26 23:58:19 +02:00
35 changed files with 6922 additions and 22148 deletions

View File

@ -1,5 +1,4 @@
node_modules node_modules
build build
dist dist
.parcel-cache
example example

View File

@ -2,7 +2,7 @@ name: 'Build'
on: on:
push: push:
branches: [master, develop] branches: [develop]
pull_request: pull_request:
branches: [master, develop] branches: [master, develop]
@ -10,10 +10,10 @@ jobs:
build: build:
runs-on: 'ubuntu-latest' runs-on: 'ubuntu-latest'
steps: steps:
- uses: 'actions/checkout@v3.0.0' - uses: 'actions/checkout@v3.5.0'
- name: 'Use Node.js' - name: 'Use Node.js'
uses: 'actions/setup-node@v3.1.0' uses: 'actions/setup-node@v3.6.0'
with: with:
node-version: 'lts/*' node-version: 'lts/*'
cache: 'npm' cache: 'npm'

View File

@ -2,7 +2,7 @@ name: 'Lint'
on: on:
push: push:
branches: [master, develop] branches: [develop]
pull_request: pull_request:
branches: [master, develop] branches: [master, develop]
@ -10,10 +10,10 @@ jobs:
lint: lint:
runs-on: 'ubuntu-latest' runs-on: 'ubuntu-latest'
steps: steps:
- uses: 'actions/checkout@v3.0.0' - uses: 'actions/checkout@v3.5.0'
- name: 'Use Node.js' - name: 'Use Node.js'
uses: 'actions/setup-node@v3.1.0' uses: 'actions/setup-node@v3.6.0'
with: with:
node-version: 'lts/*' node-version: 'lts/*'
cache: 'npm' cache: 'npm'
@ -24,5 +24,5 @@ jobs:
- run: 'npm run lint:commit -- --to "${{ github.sha }}"' - run: 'npm run lint:commit -- --to "${{ github.sha }}"'
- run: 'npm run lint:editorconfig' - run: 'npm run lint:editorconfig'
- run: 'npm run lint:markdown' - run: 'npm run lint:markdown'
- run: 'npm run lint:typescript' - run: 'npm run lint:eslint'
- run: 'npm run lint:prettier' - run: 'npm run lint:prettier'

View File

@ -8,10 +8,10 @@ jobs:
build: build:
runs-on: 'ubuntu-latest' runs-on: 'ubuntu-latest'
steps: steps:
- uses: 'actions/checkout@v3.0.0' - uses: 'actions/checkout@v3.5.0'
- name: 'Use Node.js' - name: 'Use Node.js'
uses: 'actions/setup-node@v3.1.0' uses: 'actions/setup-node@v3.6.0'
with: with:
node-version: 'lts/*' node-version: 'lts/*'
cache: 'npm' cache: 'npm'

View File

@ -2,7 +2,7 @@ name: 'Test'
on: on:
push: push:
branches: [master, develop] branches: [develop]
pull_request: pull_request:
branches: [master, develop] branches: [master, develop]
@ -10,10 +10,10 @@ jobs:
test: test:
runs-on: 'ubuntu-latest' runs-on: 'ubuntu-latest'
steps: steps:
- uses: 'actions/checkout@v3.0.0' - uses: 'actions/checkout@v3.5.0'
- name: 'Use Node.js' - name: 'Use Node.js'
uses: 'actions/setup-node@v3.1.0' uses: 'actions/setup-node@v3.6.0'
with: with:
node-version: 'lts/*' node-version: 'lts/*'
cache: 'npm' cache: 'npm'
@ -27,10 +27,10 @@ jobs:
test-e2e: test-e2e:
runs-on: 'ubuntu-latest' runs-on: 'ubuntu-latest'
steps: steps:
- uses: 'actions/checkout@v3.0.0' - uses: 'actions/checkout@v3.5.0'
- name: 'Use Node.js' - name: 'Use Node.js'
uses: 'actions/setup-node@v3.1.0' uses: 'actions/setup-node@v3.6.0'
with: with:
node-version: 'lts/*' node-version: 'lts/*'
cache: 'npm' cache: 'npm'

1
.gitignore vendored
View File

@ -5,6 +5,7 @@ node_modules
# production # production
build build
dist dist
.next
# testing # testing
coverage coverage

View File

@ -1,11 +1,5 @@
{ {
"config": {
"default": true,
"MD013": false,
"MD024": false,
"MD033": false,
"MD041": false
},
"globs": ["**/*.{md,mdx}"], "globs": ["**/*.{md,mdx}"],
"ignores": ["**/node_modules"] "ignores": ["**/node_modules"],
"customRules": ["markdownlint-rule-relative-links"]
} }

7
.markdownlint.json Normal file
View File

@ -0,0 +1,7 @@
{
"default": true,
"relative-links": true,
"extends": "markdownlint/style/prettier",
"MD033": false,
"MD041": false
}

View File

@ -34,7 +34,7 @@ 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._ _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'
@ -89,13 +89,13 @@ export const Example = () => {
formData, formData,
formElement formElement
) => { ) => {
console.log(formData) // { inputName: 'value of the input validated' } console.log(formData) // { inputName: 'value of the input validated and type-safe' }
formElement.reset() formElement.reset()
// The return can be either `null` or an object with a global message of type `'error' | 'success'`. // The return can be either `null` or an object with a global message of type `'error' | 'success'`.
return { return {
type: 'success', type: 'success',
value: 'Success: Form submitted' message: 'Success: Form submitted'
} }
} }

View File

@ -1,4 +1,5 @@
import { Form, HandleUseFormCallback, useForm } from 'react-component-form' import { Form, useForm } from 'react-component-form'
import type { HandleUseFormCallback } from 'react-component-form'
import useTranslation from 'next-translate/useTranslation' import useTranslation from 'next-translate/useTranslation'
import { Input } from './design/Input' import { Input } from './design/Input'
@ -22,12 +23,12 @@ export const FormExample: React.FC = () => {
formData, formData,
formElement formElement
) => { ) => {
await simulateServerRequest(2000) await simulateServerRequest(2_000)
console.log('onSubmit:', formData) console.log('onSubmit:', formData)
formElement.reset() formElement.reset()
return { return {
type: 'success', type: 'success',
value: 'common:success-message' message: 'common:success-message'
} }
} }

View File

@ -13,7 +13,9 @@ export const Language: React.FC = () => {
const languageClickRef = useRef<HTMLDivElement | null>(null) const languageClickRef = useRef<HTMLDivElement | null>(null)
const handleHiddenMenu = useCallback(() => { const handleHiddenMenu = useCallback(() => {
setHiddenMenu((oldHiddenMenu) => !oldHiddenMenu) setHiddenMenu((oldHiddenMenu) => {
return !oldHiddenMenu
})
}, []) }, [])
useEffect(() => { useEffect(() => {
@ -64,7 +66,9 @@ export const Language: React.FC = () => {
<li <li
key={index} key={index}
className='flex h-12 w-full items-center justify-center pl-2 hover:bg-[#4f545c] hover:bg-opacity-20' className='flex h-12 w-full items-center justify-center pl-2 hover:bg-[#4f545c] hover:bg-opacity-20'
onClick={async () => await handleLanguage(language)} onClick={async () => {
await handleLanguage(language)
}}
> >
<LanguageFlag language={language} /> <LanguageFlag language={language} />
</li> </li>

View File

@ -1,4 +1,5 @@
import { useEffect, useState } from 'react' import { useEffect, useState } from 'react'
import classNames from 'clsx'
import { useTheme } from 'next-themes' import { useTheme } from 'next-themes'
export const SwitchTheme: React.FC = () => { export const SwitchTheme: React.FC = () => {
@ -18,109 +19,60 @@ export const SwitchTheme: React.FC = () => {
} }
return ( return (
<>
<div <div
className='flex items-center' className='flex items-center'
data-cy='switch-theme-click' data-cy='switch-theme-click'
onClick={handleClick} onClick={handleClick}
> >
<div className='toggle-theme-button relative inline-block cursor-pointer bg-transparent'> <div className='relative inline-block cursor-pointer touch-pan-x select-none border-0 bg-transparent p-0'>
<div className='toggle-track'> <div className='h-[24px] w-[50px] rounded-[30px] bg-[#4d4d4d] p-0 text-white transition-all duration-200 ease-in-out'>
<div <div
data-cy='switch-theme-dark' data-cy='switch-theme-dark'
className='toggle-track-check absolute' className={classNames(
'absolute top-0 bottom-0 left-[8px] mt-auto mb-auto h-[10px] w-[14px] leading-[0] transition-opacity duration-[250ms] ease-in-out',
{
'opacity-100': theme === 'dark',
'opacity-0': theme === 'light'
}
)}
> >
<span className='toggle_Dark relative flex items-center justify-center'> <span className='relative flex h-[10px] w-[10px] items-center justify-center'>
🌜 🌜
</span> </span>
</div> </div>
<div <div
data-cy='switch-theme-light' data-cy='switch-theme-light'
className='toggle-track-x absolute' className={classNames(
'absolute right-[10px] top-0 bottom-0 mt-auto mb-auto h-[10px] w-[10px] leading-[0]',
{
'opacity-100': theme === 'light',
'opacity-0': theme === 'dark'
}
)}
> >
<span className='toggle_Light relative flex items-center justify-center'> <span className='relative flex h-[10px] w-[10px] items-center justify-center'>
🌞 🌞
</span> </span>
</div> </div>
</div> </div>
<div className='toggle-thumb absolute' /> <div
className={classNames(
'absolute top-[1px] box-border h-[22px] w-[22px] rounded-[50%] bg-[#fafafa] text-white transition-all duration-[250ms] ease-in-out',
{
'left-[27px]': theme === 'dark',
'left-0': theme === 'light'
}
)}
style={{ border: '1px solid #4d4d4d' }}
/>
<input <input
data-cy='switch-theme-input' data-cy='switch-theme-input'
type='checkbox' type='checkbox'
aria-label='Dark mode toggle' aria-label='Dark mode toggle'
className='toggle-screenreader-only absolute overflow-hidden' className='absolute m-[-1px] h-[1px] w-[1px] overflow-hidden border-0 p-0'
defaultChecked defaultChecked
/> />
</div> </div>
</div> </div>
<style jsx>
{`
.toggle-theme-button {
touch-action: pan-x;
border: 0;
padding: 0;
user-select: none;
}
.toggle-track {
width: 50px;
height: 24px;
padding: 0;
border-radius: 30px;
background-color: #4d4d4d;
transition: all 0.2s ease;
color: #fff;
}
.toggle-track-check {
width: 14px;
height: 10px;
top: 0;
bottom: 0;
margin-top: auto;
margin-bottom: auto;
line-height: 0;
left: 8px;
opacity: ${theme === 'dark' ? 1 : 0};
transition: opacity 0.25s ease;
}
.toggle-track-x {
width: 10px;
height: 10px;
top: 0;
bottom: 0;
margin-top: auto;
margin-bottom: auto;
line-height: 0;
right: 10px;
opacity: ${theme === 'dark' ? 0 : 1};
}
.toggle_Dark,
.toggle_Light {
height: 10px;
width: 10px;
}
.toggle-thumb {
left: ${theme === 'dark' ? '27px' : '0px'};
width: 22px;
height: 22px;
border: 1px solid #4d4d4d;
border-radius: 50%;
background-color: #fafafa;
box-sizing: border-box;
transition: all 0.25s ease;
top: 1px;
color: #fff;
}
.toggle-screenreader-only {
border: 0;
clip: rect(0 0 0 0);
height: 1px;
margin: -1px;
padding: 0;
width: 1px;
}
`}
</style>
</>
) )
} }

View File

@ -1,81 +0,0 @@
export interface LoaderProps {
width?: number
height?: number
className?: string
}
export const Loader: React.FC<LoaderProps> = (props) => {
const { width = 50, height = 50 } = props
return (
<div className={props.className}>
<div data-cy='progress-spinner' className='progress-spinner'>
<svg className='progress-spinner-svg' viewBox='25 25 50 50'>
<circle
className='progress-spinner-circle'
cx='50'
cy='50'
r='20'
fill='none'
strokeWidth='2'
strokeMiterlimit='10'
/>
</svg>
</div>
<style jsx>
{`
.progress-spinner {
position: relative;
margin: 0 auto;
width: ${width}px;
height: ${height}px;
}
.progress-spinner::before {
content: '';
display: block;
padding-top: 100%;
}
.progress-spinner-svg {
animation: progress-spinner-rotate 2s linear infinite;
height: 100%;
transform-origin: center center;
width: 100%;
position: absolute;
top: 0;
bottom: 0;
left: 0;
right: 0;
margin: auto;
}
.progress-spinner-circle {
stroke-dasharray: 89, 200;
stroke-dashoffset: 0;
stroke: #27b05e;
animation: progress-spinner-dash 1.5s ease-in-out infinite;
stroke-linecap: round;
}
@keyframes progress-spinner-rotate {
100% {
transform: rotate(360deg);
}
}
@keyframes progress-spinner-dash {
0% {
stroke-dasharray: 1, 200;
stroke-dashoffset: 0;
}
50% {
stroke-dasharray: 89, 200;
stroke-dashoffset: -35px;
}
100% {
stroke-dasharray: 89, 200;
stroke-dashoffset: -124px;
}
}
`}
</style>
</div>
)
}

View File

@ -0,0 +1,39 @@
@keyframes progressSpinnerRotate {
100% {
transform: rotate(360deg);
}
}
@keyframes progressSpinnerDash {
0% {
stroke-dasharray: 1, 200;
stroke-dashoffset: 0;
}
50% {
stroke-dasharray: 89, 200;
stroke-dashoffset: -35px;
}
100% {
stroke-dasharray: 89, 200;
stroke-dashoffset: -124px;
}
}
.progressSpinnerSvg {
animation: progressSpinnerRotate 2s linear infinite;
height: 100%;
transform-origin: center center;
width: 100%;
position: absolute;
top: 0;
bottom: 0;
left: 0;
right: 0;
margin: auto;
}
.progressSpinnerCircle {
stroke-dasharray: 89, 200;
stroke-dashoffset: 0;
stroke: #27b05e;
animation: progressSpinnerDash 1.5s ease-in-out infinite;
stroke-linecap: round;
}

View File

@ -0,0 +1,33 @@
import styles from './Loader.module.css'
export interface LoaderProps {
width?: number
height?: number
className?: string
}
export const Loader: React.FC<LoaderProps> = (props) => {
const { width = 50, height = 50 } = props
return (
<div className={props.className}>
<div
data-cy='progress-spinner'
className='relative my-0 mx-auto before:content-none before:block before:pt-[100%]'
style={{ width: `${width}px`, height: `${height}px` }}
>
<svg className={styles['progressSpinnerSvg']} viewBox='25 25 50 50'>
<circle
className={styles['progressSpinnerCircle']}
cx='50'
cy='50'
r='20'
fill='none'
strokeWidth='2'
strokeMiterlimit='10'
/>
</svg>
</div>
</div>
)
}

View File

@ -0,0 +1 @@
export * from './Loader'

View File

@ -5,9 +5,8 @@ export default defineConfig({
video: false, video: false,
downloadsFolder: undefined, downloadsFolder: undefined,
screenshotOnRunFailure: false, screenshotOnRunFailure: false,
e2e: { e2e: {
baseUrl: 'http://localhost:3000', baseUrl: 'http://127.0.0.1:3000',
supportFile: false supportFile: false
} }
}) })

View File

@ -3,7 +3,7 @@ describe('Form', () => {
cy.visit('/') cy.visit('/')
}) })
it('suceeds, reset input values and display the global success message', () => { it('succeeds, reset input values and display the global success message', () => {
cy.get('[data-cy=input-name]').type('John') cy.get('[data-cy=input-name]').type('John')
cy.get('[data-cy=input-email]').type('john@john.com') cy.get('[data-cy=input-email]').type('john@john.com')
cy.get('#error-name').should('not.exist') cy.get('#error-name').should('not.exist')

View File

@ -13,7 +13,7 @@ const getErrorTranslationKey = (error: Error): string => {
return 'common:required' return 'common:required'
} }
if (error.keyword === 'format') { if (error.keyword === 'format') {
if (error.params.format === 'email') { if (error.params['format'] === 'email') {
return 'common:invalid-email' return 'common:invalid-email'
} }
return 'common:invalid' return 'common:invalid'
@ -32,7 +32,7 @@ export const useFormTranslation = () => {
if (error != null) { if (error != null) {
return t(getErrorTranslationKey(error)).replace( return t(getErrorTranslationKey(error)).replace(
'{expected}', '{expected}',
error?.params?.limit error?.params?.['limit']
) )
} }
return undefined return undefined

View File

@ -1,4 +1,5 @@
import { Static, Type } from '@sinclair/typebox' import type { Static } from '@sinclair/typebox'
import { Type } from '@sinclair/typebox'
export const userSchema = { export const userSchema = {
name: Type.String({ minLength: 3, maxLength: 10 }), name: Type.String({ minLength: 3, maxLength: 10 }),

View File

@ -1,4 +1,4 @@
const nextTranslate = require('next-translate') const nextTranslate = require('next-translate-plugin')
/** @type {import('next').NextConfig} */ /** @type {import('next').NextConfig} */
const nextConfig = { const nextConfig = {

5925
example/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -7,30 +7,32 @@
"build": "next build", "build": "next build",
"start": "next start", "start": "next start",
"lint": "next lint", "lint": "next lint",
"test:e2e": "start-server-and-test \"start\" \"http://localhost:3000\" \"cypress run\"", "test:e2e": "start-server-and-test \"start\" \"http://127.0.0.1:3000\" \"cypress run\"",
"test:dev": "start-server-and-test \"dev\" \"http://localhost:3000\" \"cypress open\"" "test:dev": "start-server-and-test \"dev\" \"http://127.0.0.1:3000\" \"cypress open\""
}, },
"dependencies": { "dependencies": {
"@sinclair/typebox": "0.24.28", "@sinclair/typebox": "0.26.8",
"clsx": "1.2.1", "clsx": "1.2.1",
"next": "12.2.5", "next": "13.2.4",
"next-themes": "0.2.0", "next-themes": "0.2.1",
"next-translate": "1.5.0", "next-translate": "2.0.4",
"react": "18.2.0", "react": "18.2.0",
"react-component-form": "file:..", "react-component-form": "file:..",
"react-dom": "18.2.0" "react-dom": "18.2.0"
}, },
"devDependencies": { "devDependencies": {
"@types/node": "18.7.13", "@tsconfig/strictest": "2.0.0",
"@types/react": "18.0.17", "@types/node": "18.15.11",
"@types/react-dom": "18.0.6", "@types/react": "18.0.32",
"autoprefixer": "10.4.8", "@types/react-dom": "18.0.11",
"cypress": "10.6.0", "autoprefixer": "10.4.14",
"eslint": "8.22.0", "cypress": "12.9.0",
"eslint-config-next": "12.2.5", "eslint": "8.37.0",
"postcss": "8.4.16", "eslint-config-next": "13.2.4",
"start-server-and-test": "1.14.0", "next-translate-plugin": "2.0.4",
"tailwindcss": "3.1.8", "postcss": "8.4.21",
"typescript": "4.8.2" "start-server-and-test": "2.0.0",
"tailwindcss": "3.3.1",
"typescript": "5.0.3"
} }
} }

View File

@ -1,9 +1,9 @@
import type { AppProps } from 'next/app' import type { AppType } from 'next/app'
import { ThemeProvider } from 'next-themes' import { ThemeProvider } from 'next-themes'
import '../styles/globals.css' import '../styles/globals.css'
const MyApp = ({ Component, pageProps }: AppProps): JSX.Element => { const MyApp: AppType = ({ Component, pageProps }) => {
return ( return (
<ThemeProvider attribute='class' defaultTheme='dark'> <ThemeProvider attribute='class' defaultTheme='dark'>
<Component {...pageProps} /> <Component {...pageProps} />

View File

@ -1,5 +1,5 @@
/** @type {import('tailwindcss').Config} */ /** @type {import('tailwindcss').Config} */
module.exports = { const tailwindConfig = {
content: [ content: [
'./pages/**/*.{js,ts,jsx,tsx}', './pages/**/*.{js,ts,jsx,tsx}',
'./components/**/*.{js,ts,jsx,tsx}' './components/**/*.{js,ts,jsx,tsx}'
@ -16,3 +16,5 @@ module.exports = {
}, },
plugins: [] plugins: []
} }
module.exports = tailwindConfig

View File

@ -1,20 +1,19 @@
{ {
"extends": "@tsconfig/strictest/tsconfig.json",
"compilerOptions": { "compilerOptions": {
"target": "ESNext", "target": "ESNext",
"module": "ESNext", "module": "ESNext",
"lib": ["dom", "dom.iterable", "esnext"], "lib": ["dom", "dom.iterable", "ESNext"],
"allowJs": true, "allowJs": true,
"skipLibCheck": true,
"strict": true,
"types": ["cypress"], "types": ["cypress"],
"forceConsistentCasingInFileNames": true,
"noEmit": true, "noEmit": true,
"esModuleInterop": true,
"moduleResolution": "node", "moduleResolution": "node",
"resolveJsonModule": true, "resolveJsonModule": true,
"isolatedModules": true,
"jsx": "preserve", "jsx": "preserve",
"incremental": true "incremental": true,
"exactOptionalPropertyTypes": false,
"verbatimModuleSyntax": false,
"isolatedModules": true
}, },
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"], "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"],
"exclude": ["node_modules"] "exclude": ["node_modules"]

22593
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -29,45 +29,47 @@
"lint:commit": "commitlint", "lint:commit": "commitlint",
"lint:editorconfig": "editorconfig-checker", "lint:editorconfig": "editorconfig-checker",
"lint:markdown": "markdownlint-cli2", "lint:markdown": "markdownlint-cli2",
"lint:typescript": "eslint \"**/*.{js,jsx,ts,tsx}\"", "lint:eslint": "eslint \".\"",
"lint:prettier": "prettier \".\" --check --ignore-path \".gitignore\"", "lint:prettier": "prettier \".\" --check --ignore-path \".gitignore\"",
"release": "semantic-release" "release": "semantic-release"
}, },
"peerDependencies": { "peerDependencies": {
"react": ">=16" "react": ">=18.2.0"
}, },
"dependencies": { "dependencies": {
"@sinclair/typebox": "0.24.28", "@sinclair/typebox": "0.26.8",
"ajv": "8.11.0", "ajv": "8.12.0",
"ajv-formats": "2.1.1" "ajv-formats": "2.1.1"
}, },
"devDependencies": { "devDependencies": {
"@commitlint/cli": "17.0.3", "@commitlint/cli": "17.5.1",
"@commitlint/config-conventional": "17.0.3", "@commitlint/config-conventional": "17.4.4",
"@testing-library/react": "13.3.0", "@testing-library/react": "14.0.0",
"@types/jest": "28.1.8", "@tsconfig/strictest": "2.0.0",
"@types/react": "18.0.17", "@types/jest": "29.5.0",
"@types/react-dom": "18.0.6", "@types/react": "18.0.32",
"@typescript-eslint/eslint-plugin": "5.35.1", "@types/react-dom": "18.0.11",
"@typescript-eslint/parser": "5.35.1", "@typescript-eslint/eslint-plugin": "5.57.0",
"editorconfig-checker": "4.0.2", "@typescript-eslint/parser": "5.57.0",
"esbuild": "0.15.5", "editorconfig-checker": "5.0.1",
"esbuild": "0.17.15",
"esbuild-jest": "0.5.0", "esbuild-jest": "0.5.0",
"eslint": "8.22.0", "eslint": "8.37.0",
"eslint-config-conventions": "3.0.0", "eslint-config-conventions": "8.0.0",
"eslint-config-prettier": "8.5.0", "eslint-config-prettier": "8.8.0",
"eslint-plugin-import": "2.26.0", "eslint-plugin-import": "2.27.5",
"eslint-plugin-prettier": "4.2.1", "eslint-plugin-prettier": "4.2.1",
"eslint-plugin-promise": "6.0.1", "eslint-plugin-promise": "6.1.1",
"eslint-plugin-unicorn": "43.0.2", "eslint-plugin-unicorn": "46.0.0",
"jest": "29.0.0", "jest": "29.5.0",
"jest-environment-jsdom": "29.0.0", "jest-environment-jsdom": "29.5.0",
"markdownlint-cli2": "0.5.1", "markdownlint-cli2": "0.6.0",
"prettier": "2.7.1", "markdownlint-rule-relative-links": "1.1.2",
"prettier": "2.8.7",
"react": "18.2.0", "react": "18.2.0",
"react-dom": "18.2.0", "react-dom": "18.2.0",
"semantic-release": "19.0.5", "semantic-release": "21.0.1",
"tsup": "6.2.2", "tsup": "6.7.0",
"typescript": "4.7.4" "typescript": "5.0.3"
} }
} }

View File

@ -1,7 +1,8 @@
import React from 'react' import React from 'react'
import { render, cleanup, fireEvent } from '@testing-library/react' import { render, cleanup, fireEvent } from '@testing-library/react'
import { Form, HandleForm } from '..' import type { HandleForm } from '..'
import { Form } from '..'
afterEach(cleanup) afterEach(cleanup)

View File

@ -2,7 +2,7 @@ import { useState } from 'react'
export const fetchState = ['idle', 'loading', 'error', 'success'] as const export const fetchState = ['idle', 'loading', 'error', 'success'] as const
export type FetchState = typeof fetchState[number] export type FetchState = (typeof fetchState)[number]
export const useFetchState = ( export const useFetchState = (
initialFetchState: FetchState = 'idle' initialFetchState: FetchState = 'idle'

View File

@ -1,14 +1,18 @@
import { useMemo, useState } from 'react' import { useMemo, useState } from 'react'
import { SchemaOptions, Static, TObject, Type } from '@sinclair/typebox' import type { Static, TObject } from '@sinclair/typebox'
import { Type } from '@sinclair/typebox'
import type { ErrorObject } from 'ajv' import type { ErrorObject } from 'ajv'
import type { HandleForm } from '../components/Form' import type { HandleForm } from '../components/Form'
import { FetchState, useFetchState } from './useFetchState' import type { FetchState } from './useFetchState'
import { useFetchState } from './useFetchState'
import { ajv } from '../utils/ajv' import { ajv } from '../utils/ajv'
import { handleCheckboxBoolean } from '../utils/handleCheckboxBoolean' import { handleCheckboxBoolean } from '../utils/handleCheckboxBoolean'
import { handleOptionalEmptyStringToNull } from '../utils/handleOptionalEmptyStringToNull' import { handleOptionalEmptyStringToNull } from '../utils/handleOptionalEmptyStringToNull'
export type Schema = SchemaOptions export interface Schema {
[property: string | symbol]: any
}
export type Error = ErrorObject export type Error = ErrorObject
@ -34,13 +38,13 @@ export type HandleUseForm<K extends Schema> = (
export interface GlobalMessage { export interface GlobalMessage {
type: 'error' | 'success' type: 'error' | 'success'
value?: string message?: string
properties?: undefined properties?: undefined
} }
export interface PropertiesMessage<K extends Schema> { export interface PropertiesMessage<K extends Schema> {
type: 'error' type: 'error'
value?: string message?: string
properties: { [key in keyof Partial<K>]: string } properties: { [key in keyof Partial<K>]: string }
} }
@ -61,7 +65,7 @@ export interface UseFormResult<K extends Schema> {
/** /**
* Global message of the form (not specific to a property). * Global message of the form (not specific to a property).
*/ */
readonly message?: string readonly message: string | undefined
setMessage: React.Dispatch<React.SetStateAction<string | undefined>> setMessage: React.Dispatch<React.SetStateAction<string | undefined>>
/** /**
@ -125,8 +129,8 @@ export const useForm = <K extends Schema>(
formElement formElement
) )
if (message != null) { if (message != null) {
const { value, type, properties } = message const { message: messageValue, type, properties } = message
setMessage(value) setMessage(messageValue)
setFetchState(type) setFetchState(type)
if (type === 'error') { if (type === 'error') {
const propertiesErrors: ErrorsObject<typeof validationSchema> = const propertiesErrors: ErrorsObject<typeof validationSchema> =

View File

@ -1,11 +1,11 @@
import type { TObject } from '@sinclair/typebox' import type { TObject } from '@sinclair/typebox'
import type { ObjectAny } from './types' import type { Schema } from '../hooks/useForm'
export const handleCheckboxBoolean = ( export const handleCheckboxBoolean = (
object: ObjectAny, object: Schema,
validateSchemaObject: TObject<ObjectAny> validateSchemaObject: TObject<Schema>
): ObjectAny => { ): Schema => {
const booleanProperties: string[] = [] const booleanProperties: string[] = []
for (const property in validateSchemaObject.properties) { for (const property in validateSchemaObject.properties) {
const rule = validateSchemaObject.properties[property] const rule = validateSchemaObject.properties[property]

View File

@ -1,4 +1,6 @@
export const handleOptionalEmptyStringToNull = <K>( import type { Schema } from '../hooks/useForm'
export const handleOptionalEmptyStringToNull = <K extends Schema>(
object: K, object: K,
required: string[] = [] required: string[] = []
): K => { ): K => {

View File

@ -1,3 +0,0 @@
export interface ObjectAny {
[key: string]: any
}

View File

@ -1,4 +1,5 @@
{ {
"extends": "@tsconfig/strictest/tsconfig.json",
"include": ["src", "types"], "include": ["src", "types"],
"compilerOptions": { "compilerOptions": {
"module": "ESNext", "module": "ESNext",
@ -7,16 +8,8 @@
"declaration": true, "declaration": true,
"sourceMap": true, "sourceMap": true,
"rootDir": "./src", "rootDir": "./src",
"strict": true,
"noImplicitReturns": true,
"noFallthroughCasesInSwitch": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"moduleResolution": "node", "moduleResolution": "node",
"jsx": "react", "jsx": "react",
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"noEmit": true "noEmit": true
} }
} }