15 Commits

Author SHA1 Message Date
da5d46835d perf: enable tree-shaking 2023-05-13 18:24:44 +02:00
ef5635380c feat: add npm package provenance
Ref: https://github.blog/2023-04-19-introducing-npm-package-provenance/
2023-05-13 17:02:20 +02:00
882416cb49 build(deps): update latest 2023-05-13 17:00:31 +02:00
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
39 changed files with 7961 additions and 22295 deletions

View File

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

View File

@ -1,6 +1,6 @@
<!-- Please first discuss the change you wish to make via issue before making a change. It might avoid a waste of your time. --> <!-- Please first discuss the change you wish to make via issue before making a change. It might avoid a waste of your time. -->
## What changes this PR introduce? # What changes this PR introduce?
## List any relevant issue numbers ## List any relevant issue numbers

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,19 +10,19 @@ jobs:
build: build:
runs-on: 'ubuntu-latest' runs-on: 'ubuntu-latest'
steps: steps:
- uses: 'actions/checkout@v3.0.0' - uses: 'actions/checkout@v3.5.2'
- name: 'Use Node.js' - name: 'Setup 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'
- name: 'Install' - name: 'Install dependencies'
run: 'npm install' run: 'npm clean-install'
- name: 'Build Package' - name: 'Build Package'
run: 'npm run build' run: 'npm run build'
- name: 'Build Example' - name: 'Build Example'
run: 'cd example && npm install && npm run build' run: 'cd example && npm clean-install && npm run build'

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,19 +10,19 @@ jobs:
lint: lint:
runs-on: 'ubuntu-latest' runs-on: 'ubuntu-latest'
steps: steps:
- uses: 'actions/checkout@v3.0.0' - uses: 'actions/checkout@v3.5.2'
- name: 'Use Node.js' - name: 'Setup 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'
- name: 'Install' - name: 'Install dependencies'
run: 'npm install' run: 'npm clean-install'
- 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

@ -7,21 +7,29 @@ on:
jobs: jobs:
build: build:
runs-on: 'ubuntu-latest' runs-on: 'ubuntu-latest'
permissions:
contents: 'write'
issues: 'write'
pull-requests: 'write'
id-token: 'write'
steps: steps:
- uses: 'actions/checkout@v3.0.0' - uses: 'actions/checkout@v3.5.2'
- name: 'Use Node.js' - name: 'Setup 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'
- name: 'Install' - name: 'Install dependencies'
run: 'npm install' run: 'npm clean-install'
- name: 'Build Package' - name: 'Build Package'
run: 'npm run build' run: 'npm run build'
- name: 'Verify the integrity of provenance attestations and registry signatures for installed dependencies'
run: 'npm audit signatures'
- name: 'Release' - name: 'Release'
run: 'npm run release' run: 'npm run release'
env: env:

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,16 +10,16 @@ jobs:
test: test:
runs-on: 'ubuntu-latest' runs-on: 'ubuntu-latest'
steps: steps:
- uses: 'actions/checkout@v3.0.0' - uses: 'actions/checkout@v3.5.2'
- name: 'Use Node.js' - name: 'Setup 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'
- name: 'Install' - name: 'Install dependencies'
run: 'npm install' run: 'npm clean-install'
- name: 'Test' - name: 'Test'
run: 'npm run test' run: 'npm run test'
@ -27,22 +27,22 @@ 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.2'
- name: 'Use Node.js' - name: 'Setup 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'
- name: 'Install' - name: 'Install dependencies'
run: 'npm install' run: 'npm clean-install'
- name: 'Build Package' - name: 'Build Package'
run: 'npm run build' run: 'npm run build'
- name: 'Build Example' - name: 'Build Example'
run: 'cd example && npm install && npm run build' run: 'cd example && npm clean-install && npm run build'
- name: 'End To End (e2e) Test Example' - name: 'End To End (e2e) Test Example'
run: 'cd example && npm run test:e2e' run: 'cd example && npm run test:e2e'

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,11 @@
{ {
"config": { "config": {
"extends": "markdownlint/style/prettier",
"relative-links": true,
"default": true, "default": true,
"MD013": false, "MD033": false
"MD024": false,
"MD033": false,
"MD041": false
}, },
"globs": ["**/*.{md,mdx}"], "globs": ["**/*.{md,mdx}"],
"ignores": ["**/node_modules"] "ignores": ["**/node_modules"],
"customRules": ["markdownlint-rule-relative-links"]
} }

1
.npmrc
View File

@ -1 +1,2 @@
save-exact=true save-exact=true
provenance=true

View File

@ -60,7 +60,7 @@ representative at an online or offline event.
Instances of abusive, harassing, or otherwise unacceptable behavior may be Instances of abusive, harassing, or otherwise unacceptable behavior may be
reported to the community leaders responsible for enforcement at reported to the community leaders responsible for enforcement at
contact@divlo.fr. <contact@divlo.fr>.
All complaints will be reviewed and investigated promptly and fairly. All complaints will be reviewed and investigated promptly and fairly.
All community leaders are obligated to respect the privacy and security of the All community leaders are obligated to respect the privacy and security of the

View File

@ -6,6 +6,10 @@ Thanks a lot for your interest in contributing to **react-component-form**! 🎉
**react-component-form** has adopted the [Contributor Covenant](https://www.contributor-covenant.org/) as its Code of Conduct, and we expect project participants to adhere to it. Please read [the full text](./CODE_OF_CONDUCT.md) so that you can understand what actions will and will not be tolerated. **react-component-form** has adopted the [Contributor Covenant](https://www.contributor-covenant.org/) as its Code of Conduct, and we expect project participants to adhere to it. Please read [the full text](./CODE_OF_CONDUCT.md) so that you can understand what actions will and will not be tolerated.
## Open Development
All work on **react-component-form** happens directly on this repository. Both core team members and external contributors send pull requests which go through the same review process.
## Types of contributions ## Types of contributions
- Reporting a bug. - Reporting a bug.
@ -25,28 +29,4 @@ If you're adding new features to **react-component-form**, please include tests.
## Commits ## Commits
The commit message guidelines respect The commit message guidelines adheres to [Conventional Commits](https://www.conventionalcommits.org/) and [Semantic Versioning](https://semver.org/) for releases.
[@commitlint/config-conventional](https://github.com/conventional-changelog/commitlint/tree/master/%40commitlint/config-conventional)
and [Semantic Versioning](https://semver.org/) for releases.
### Types
Types define which kind of changes you made to the project.
| Types | Description |
| -------- | ------------------------------------------------------------------------------------------------------------ |
| feat | A new feature. |
| fix | A bug fix. |
| docs | Documentation only changes. |
| style | Changes that do not affect the meaning of the code (white-space, formatting, missing semi-colons, etc). |
| refactor | A code change that neither fixes a bug nor adds a feature. |
| perf | A code change that improves performance. |
| test | Adding missing tests or correcting existing tests. |
| build | Changes that affect the build system or external dependencies (example scopes: gulp, broccoli, npm). |
| ci | Changes to our CI configuration files and scripts (example scopes: Travis, Circle, BrowserStack, SauceLabs). |
| chore | Other changes that don't modify src or test files. |
| revert | Reverts a previous commit. |
### Scopes
Scopes define what part of the code changed.

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,7 @@
import { Form, HandleUseFormCallback, useForm } from 'react-component-form' 'use client'
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'
@ -7,7 +10,7 @@ import { useFormTranslation } from '../hooks/useFormTranslation'
import { userSchema } from '../models/User' import { userSchema } from '../models/User'
import { FormState } from './design/FormState' import { FormState } from './design/FormState'
const simulateServerRequest = async (ms: number): Promise<void> => { const fakeServerRequest = async (ms: number): Promise<void> => {
return await new Promise((resolve) => { return await new Promise((resolve) => {
setTimeout(resolve, ms) setTimeout(resolve, ms)
}) })
@ -22,12 +25,12 @@ export const FormExample: React.FC = () => {
formData, formData,
formElement formElement
) => { ) => {
await simulateServerRequest(2000) await fakeServerRequest(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='relative inline-block cursor-pointer touch-pan-x select-none border-0 bg-transparent p-0'>
<div className='toggle-theme-button relative inline-block cursor-pointer bg-transparent'> <div className='h-[24px] w-[50px] rounded-[30px] bg-[#4d4d4d] p-0 text-white transition-all duration-200 ease-in-out'>
<div className='toggle-track'> <div
<div data-cy='switch-theme-dark'
data-cy='switch-theme-dark' className={classNames(
className='toggle-track-check absolute' 'absolute top-0 bottom-0 left-[8px] mt-auto mb-auto h-[10px] w-[14px] leading-[0] transition-opacity duration-[250ms] ease-in-out',
> {
<span className='toggle_Dark relative flex items-center justify-center'> 'opacity-100': theme === 'dark',
🌜 'opacity-0': theme === 'light'
</span> }
</div> )}
<div >
data-cy='switch-theme-light' <span className='relative flex h-[10px] w-[10px] items-center justify-center'>
className='toggle-track-x absolute' 🌜
> </span>
<span className='toggle_Light relative flex items-center justify-center'> </div>
🌞 <div
</span> data-cy='switch-theme-light'
</div> 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='relative flex h-[10px] w-[10px] items-center justify-center'>
🌞
</span>
</div> </div>
<div className='toggle-thumb absolute' />
<input
data-cy='switch-theme-input'
type='checkbox'
aria-label='Dark mode toggle'
className='toggle-screenreader-only absolute overflow-hidden'
defaultChecked
/>
</div> </div>
<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
data-cy='switch-theme-input'
type='checkbox'
aria-label='Dark mode toggle'
className='absolute m-[-1px] h-[1px] w-[1px] overflow-hidden border-0 p-0'
defaultChecked
/>
</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 = {

6387
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.28.10",
"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.5",
"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.1",
"@types/react": "18.0.17", "@types/node": "20.1.4",
"@types/react-dom": "18.0.6", "@types/react": "18.2.6",
"autoprefixer": "10.4.8", "@types/react-dom": "18.2.4",
"cypress": "10.6.0", "autoprefixer": "10.4.14",
"eslint": "8.22.0", "cypress": "12.12.0",
"eslint-config-next": "12.2.5", "eslint": "8.40.0",
"postcss": "8.4.16", "eslint-config-next": "13.2.4",
"start-server-and-test": "1.14.0", "next-translate-plugin": "2.0.5",
"tailwindcss": "3.1.8", "postcss": "8.4.23",
"typescript": "4.8.2" "start-server-and-test": "2.0.0",
"tailwindcss": "3.3.2",
"typescript": "5.0.4"
} }
} }

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"]

23222
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -18,56 +18,62 @@
"url": "https://github.com/Divlo/react-component-form/issues" "url": "https://github.com/Divlo/react-component-form/issues"
}, },
"homepage": "https://react-component-form.vercel.app/", "homepage": "https://react-component-form.vercel.app/",
"main": "dist/index.js", "main": "build/index.js",
"types": "dist/index.d.ts", "types": "build/index.d.ts",
"files": [ "files": [
"dist" "build"
], ],
"publishConfig": {
"access": "public",
"provenance": true
},
"scripts": { "scripts": {
"build": "tsup", "build": "tsup",
"test": "jest", "test": "jest",
"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.28.10",
"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.6.3",
"@commitlint/config-conventional": "17.0.3", "@commitlint/config-conventional": "17.6.3",
"@testing-library/react": "13.3.0", "@testing-library/react": "14.0.0",
"@types/jest": "28.1.8", "@tsconfig/strictest": "2.0.1",
"@types/react": "18.0.17", "@types/jest": "29.5.1",
"@types/react-dom": "18.0.6", "@types/react": "18.2.6",
"@typescript-eslint/eslint-plugin": "5.35.1", "@types/react-dom": "18.2.4",
"@typescript-eslint/parser": "5.35.1", "@typescript-eslint/eslint-plugin": "5.59.5",
"editorconfig-checker": "4.0.2", "@typescript-eslint/parser": "5.59.5",
"esbuild": "0.15.5", "editorconfig-checker": "5.0.1",
"esbuild": "0.17.19",
"esbuild-jest": "0.5.0", "esbuild-jest": "0.5.0",
"eslint": "8.22.0", "eslint": "8.40.0",
"eslint-config-conventions": "3.0.0", "eslint-config-conventions": "9.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": "47.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.7.1",
"prettier": "2.7.1", "markdownlint-rule-relative-links": "1.2.0",
"prettier": "2.8.8",
"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.2",
"tsup": "6.2.2", "tsup": "6.7.0",
"typescript": "4.7.4" "typescript": "5.0.4"
} }
} }

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)
@ -28,14 +29,14 @@ describe('<Form />', () => {
const text = 'some random text' const text = 'some random text'
fireEvent.change(inputForm, { target: { value: text } }) fireEvent.change(inputForm, { target: { value: text } })
expect(formData.inputName).toEqual(text) expect(formData['inputName']).toEqual(text)
expect(formElement instanceof HTMLFormElement).toBeTruthy() expect(formElement instanceof HTMLFormElement).toBeTruthy()
formData = {} formData = {}
formElement = null formElement = null
fireEvent.click(buttonSubmit) fireEvent.click(buttonSubmit)
expect(Object.keys(formData).length).toEqual(1) expect(Object.keys(formData).length).toEqual(1)
expect(formData.inputName).toEqual(text) expect(formData['inputName']).toEqual(text)
expect(formElement instanceof HTMLFormElement).toBeTruthy() expect(formElement instanceof HTMLFormElement).toBeTruthy()
}) })
}) })

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
} }
} }

View File

@ -1,12 +1,13 @@
import { defineConfig } from 'tsup' import { defineConfig } from 'tsup'
export default defineConfig({ export default defineConfig({
entry: ['src/index.ts'], entry: ['src/**/*.{ts,tsx}', '!src/**/*.test.{ts,tsx}'],
sourcemap: true, sourcemap: false,
clean: true, clean: true,
platform: 'browser', platform: 'browser',
target: 'esnext', target: 'esnext',
format: ['esm'], format: ['esm'],
minify: true, minify: false,
outDir: 'build',
dts: true dts: true
}) })