Compare commits
55 Commits
Author | SHA1 | Date | |
---|---|---|---|
f21e98175f | |||
141abe01b3 | |||
d9af7f415c | |||
4a975e431a | |||
8168a18ede | |||
a49e844c70 | |||
1224ece116 | |||
41b4472870 | |||
6cb0c3fb1b | |||
81290836f3 | |||
|
da5d46835d | ||
|
ef5635380c | ||
|
882416cb49 | ||
|
040e3a0ae1 | ||
|
5bb73df804 | ||
|
69f12002c7 | ||
|
85eb53d60c | ||
|
45c072f2bd | ||
|
cdff824ca5 | ||
|
54ef5ceea1 | ||
|
48d4fb6f75 | ||
|
1683474fa6 | ||
|
a37453a115 | ||
|
fcc2b2ea77 | ||
|
d213893d5d | ||
|
1125103c45 | ||
|
ce884c354d | ||
|
0819304e1e | ||
|
50d724eb6a | ||
|
c979bab553 | ||
|
52081972e9 | ||
|
5c49f94b53 | ||
|
01419426a3 | ||
|
676a70b1a9 | ||
|
37b4b9b990 | ||
|
7e3ef0f492 | ||
|
c0034d5af6 | ||
|
694d31e68d | ||
|
a2edafdc22 | ||
|
17656c149a | ||
|
c9bb631073 | ||
|
8cbe5c3bf2 | ||
|
686b5643b3 | ||
|
ec4929d7d8 | ||
|
079d3f9d50 | ||
|
d1f5430374 | ||
|
fd166db46c | ||
|
c90742b200 | ||
|
7ac46825ee | ||
|
e3c4c53f23 | ||
|
c133ba5a20 | ||
|
efc06a9639 | ||
|
f475f18b29 | ||
|
f720068fcc | ||
|
d4a3fc4573 |
1
.commitlintrc.json
Normal file
1
.commitlintrc.json
Normal file
@ -0,0 +1 @@
|
||||
{ "extends": ["@commitlint/config-conventional"] }
|
@ -1,9 +1,11 @@
|
||||
# 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
|
||||
|
@ -1,5 +0,0 @@
|
||||
build/
|
||||
dist/
|
||||
node_modules/
|
||||
.snapshots/
|
||||
*.min.js
|
36
.eslintrc
36
.eslintrc
@ -1,36 +0,0 @@
|
||||
{
|
||||
"parser": "@typescript-eslint/parser",
|
||||
"extends": [
|
||||
"standard",
|
||||
"standard-react",
|
||||
"plugin:prettier/recommended",
|
||||
"prettier/standard",
|
||||
"prettier/react",
|
||||
"plugin:@typescript-eslint/eslint-recommended"
|
||||
],
|
||||
"env": {
|
||||
"node": true
|
||||
},
|
||||
"parserOptions": {
|
||||
"ecmaVersion": 2020,
|
||||
"ecmaFeatures": {
|
||||
"legacyDecorators": true,
|
||||
"jsx": true
|
||||
}
|
||||
},
|
||||
"settings": {
|
||||
"react": {
|
||||
"version": "16"
|
||||
}
|
||||
},
|
||||
"rules": {
|
||||
"space-before-function-paren": 0,
|
||||
"react/prop-types": 0,
|
||||
"react/jsx-handler-names": 0,
|
||||
"react/jsx-fragments": 0,
|
||||
"react/no-unused-prop-types": 0,
|
||||
"import/export": 0,
|
||||
"no-use-before-define": "off",
|
||||
"@typescript-eslint/no-use-before-define": "off"
|
||||
}
|
||||
}
|
16
.eslintrc.json
Normal file
16
.eslintrc.json
Normal file
@ -0,0 +1,16 @@
|
||||
{
|
||||
"extends": ["conventions", "prettier"],
|
||||
"ignorePatterns": ["tsup.config.js", "example"],
|
||||
"plugins": ["prettier", "import", "unicorn"],
|
||||
"parser": "@typescript-eslint/parser",
|
||||
"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
|
59
.github/CONTRIBUTING.md
vendored
59
.github/CONTRIBUTING.md
vendored
@ -1,59 +0,0 @@
|
||||
# Contributing
|
||||
|
||||
Thanks a lot for your interest in contributing to **react-component-form**! 🎉
|
||||
|
||||
## Types of contributions
|
||||
|
||||
- Reporting a bug.
|
||||
- Suggest a new feature idea.
|
||||
- Correct spelling errors, improvements or additions to documentation files (README, CONTRIBUTING...).
|
||||
- Improve structure/format/performance/refactor/tests of the code.
|
||||
|
||||
## Pull Requests
|
||||
|
||||
- **Please first discuss** the change you wish to make via [issue](https://github.com/Divlo/react-component-form/issues) before making a change. It might avoid a waste of your time.
|
||||
|
||||
- Ensure your code respect [JavaScript Standard Style](https://standardjs.com/).
|
||||
|
||||
- Make sure your code passes the tests.
|
||||
|
||||
If you're adding new features to **react-component-form**, please include tests.
|
||||
|
||||
## Commits
|
||||
|
||||
The commit message guidelines respect [@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.
|
||||
|
||||
There are 2 principal scopes in the project :
|
||||
|
||||
- lib
|
||||
- example
|
||||
|
||||
### Examples
|
||||
|
||||
```sh
|
||||
git commit -m "feat(lib): add Form component"
|
||||
git commit -m "docs(readme): update installation process"
|
||||
git commit -m "fix(example): remove bugs"
|
||||
```
|
7
.github/ISSUE_TEMPLATE/BUG.md
vendored
7
.github/ISSUE_TEMPLATE/BUG.md
vendored
@ -1,7 +1,8 @@
|
||||
---
|
||||
name: '🐛 Bug Report'
|
||||
about: 'Report an unexpected problem or unintended behavior.'
|
||||
labels: 'bug'
|
||||
name: "🐛 Bug Report"
|
||||
about: "Report an unexpected problem or unintended behavior."
|
||||
title: "[Bug]"
|
||||
labels: "bug"
|
||||
---
|
||||
|
||||
<!--
|
||||
|
24
.github/ISSUE_TEMPLATE/DOCUMENTATION.md
vendored
24
.github/ISSUE_TEMPLATE/DOCUMENTATION.md
vendored
@ -1,20 +1,18 @@
|
||||
---
|
||||
name: '📜 Documentation'
|
||||
about: 'Correct spelling errors, improvements or additions to documentation files (README, CONTRIBUTING...).'
|
||||
labels: 'documentation'
|
||||
name: "📜 Documentation"
|
||||
about: "Correct spelling errors, improvements or additions to documentation files (README, CONTRIBUTING...)."
|
||||
title: "[Documentation]"
|
||||
labels: "documentation"
|
||||
---
|
||||
|
||||
<!--
|
||||
Please make sure your issue has not already been fixed.
|
||||
<!-- Please make sure your issue has not already been fixed. -->
|
||||
|
||||
Please place an x (no spaces - [x]) in all [ ] that apply.
|
||||
-->
|
||||
## Documentation
|
||||
|
||||
### Documentation :
|
||||
<!-- Please uncomment the type of documentation problem this issue address -->
|
||||
|
||||
- [ ] Is Missing
|
||||
- [ ] Is Confusing
|
||||
- [ ] Has Typo errors
|
||||
- [ ] Not Sure?
|
||||
<!-- Documentation is Missing -->
|
||||
<!-- Documentation is Confusing -->
|
||||
<!-- Documentation has Typo errors -->
|
||||
|
||||
### Proposal
|
||||
## Proposal
|
||||
|
13
.github/ISSUE_TEMPLATE/FEATURE_REQUEST.md
vendored
13
.github/ISSUE_TEMPLATE/FEATURE_REQUEST.md
vendored
@ -1,19 +1,20 @@
|
||||
---
|
||||
name: '✨ Feature Request'
|
||||
about: 'Suggest a new feature idea.'
|
||||
labels: 'feature request'
|
||||
name: "✨ Feature Request"
|
||||
about: "Suggest a new feature idea."
|
||||
title: "[Feature]"
|
||||
labels: "feature request"
|
||||
---
|
||||
|
||||
<!-- Please make sure your issue has not already been fixed. -->
|
||||
|
||||
### Description
|
||||
## Description
|
||||
|
||||
<!-- A clear and concise description of the problem or missing capability... -->
|
||||
|
||||
### Describe the solution you'd like
|
||||
## Describe the solution you'd like
|
||||
|
||||
<!-- If you have a solution in mind, please describe it. -->
|
||||
|
||||
### Describe alternatives you've considered
|
||||
## Describe alternatives you've considered
|
||||
|
||||
<!-- Have you considered any alternative solutions or workarounds? -->
|
||||
|
27
.github/ISSUE_TEMPLATE/IMPROVEMENT.md
vendored
27
.github/ISSUE_TEMPLATE/IMPROVEMENT.md
vendored
@ -1,21 +1,20 @@
|
||||
---
|
||||
name: '🔧 Improvement'
|
||||
about: 'Improve structure/format/performance/refactor/tests of the code.'
|
||||
labels: 'improvement'
|
||||
name: "🔧 Improvement"
|
||||
about: "Improve structure/format/performance/refactor/tests of the code."
|
||||
title: "[Improvement]"
|
||||
labels: "improvement"
|
||||
---
|
||||
|
||||
<!--.
|
||||
Please make sure your issue has not already been fixed.
|
||||
<!-- Please make sure your issue has not already been fixed. -->
|
||||
|
||||
Please place an x (no spaces - [x]) in all [ ] that apply.
|
||||
-->
|
||||
## Type of Improvement
|
||||
|
||||
### Type of Improvement :
|
||||
<!-- Please uncomment the type of improvements this issue address -->
|
||||
|
||||
- [ ] Files and Folders Structure
|
||||
- [ ] Performance
|
||||
- [ ] Refactoring code
|
||||
- [ ] Tests
|
||||
- [ ] Not Sure?
|
||||
<!-- Files and Folders Structure -->
|
||||
<!-- Performance -->
|
||||
<!-- Refactoring code -->
|
||||
<!-- Tests -->
|
||||
<!-- Not Sure? -->
|
||||
|
||||
### Proposal
|
||||
## Proposal
|
||||
|
7
.github/ISSUE_TEMPLATE/QUESTION.md
vendored
7
.github/ISSUE_TEMPLATE/QUESTION.md
vendored
@ -1,7 +1,8 @@
|
||||
---
|
||||
name: '🙋 Question'
|
||||
about: 'Further information is requested.'
|
||||
labels: 'question'
|
||||
name: "🙋 Question"
|
||||
about: "Further information is requested."
|
||||
title: "[Question]"
|
||||
labels: "question"
|
||||
---
|
||||
|
||||
### Question
|
||||
|
30
.github/PULL_REQUEST_TEMPLATE.md
vendored
30
.github/PULL_REQUEST_TEMPLATE.md
vendored
@ -1,29 +1,7 @@
|
||||
<!--
|
||||
<!-- Please first discuss the change you wish to make via issue before making a change. It might avoid a waste of your time. -->
|
||||
|
||||
Thanks a lot for your interest in contributing to react-component-form! 🎉
|
||||
# What changes this PR introduce?
|
||||
|
||||
Please first discuss the change you wish to make via issue before making a change. It might avoid a waste of your time.
|
||||
## List any relevant issue numbers
|
||||
|
||||
Before submitting your contribution, please take a moment to review this document:
|
||||
https://github.com/Divlo/react-component-form/blob/master/.github/CONTRIBUTING.md
|
||||
|
||||
Please place an x (no spaces - [x]) in all [ ] that apply.
|
||||
|
||||
-->
|
||||
|
||||
### What type of change does this PR introduce?
|
||||
|
||||
- [ ] Bugfix
|
||||
- [ ] Feature
|
||||
- [ ] Refactor
|
||||
- [ ] Documentation
|
||||
- [ ] Not Sure?
|
||||
|
||||
### Does this PR introduce breaking changes?
|
||||
|
||||
- [ ] Yes
|
||||
- [ ] No
|
||||
|
||||
### List any relevant issue numbers:
|
||||
|
||||
### Description:
|
||||
## Is there anything you'd like reviewers to focus on?
|
||||
|
28
.github/workflows/build.yml
vendored
Normal file
28
.github/workflows/build.yml
vendored
Normal file
@ -0,0 +1,28 @@
|
||||
name: "Build"
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [develop]
|
||||
pull_request:
|
||||
branches: [master, develop]
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: "ubuntu-latest"
|
||||
steps:
|
||||
- uses: "actions/checkout@v4.1.1"
|
||||
|
||||
- name: "Setup Node.js"
|
||||
uses: "actions/setup-node@v4.0.1"
|
||||
with:
|
||||
node-version: "lts/*"
|
||||
cache: "npm"
|
||||
|
||||
- name: "Install dependencies"
|
||||
run: "npm clean-install"
|
||||
|
||||
- name: "Build Package"
|
||||
run: "npm run build"
|
||||
|
||||
- name: "Build Example"
|
||||
run: "cd example && npm clean-install && npm run build"
|
16
.github/workflows/commitlint.yml
vendored
16
.github/workflows/commitlint.yml
vendored
@ -1,16 +0,0 @@
|
||||
name: 'Lint Commit Messages'
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [master]
|
||||
pull_request:
|
||||
branches: [master]
|
||||
|
||||
jobs:
|
||||
commitlint:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- uses: wagoid/commitlint-github-action@v2
|
28
.github/workflows/lint.yml
vendored
Normal file
28
.github/workflows/lint.yml
vendored
Normal file
@ -0,0 +1,28 @@
|
||||
name: "Lint"
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [develop]
|
||||
pull_request:
|
||||
branches: [master, develop]
|
||||
|
||||
jobs:
|
||||
lint:
|
||||
runs-on: "ubuntu-latest"
|
||||
steps:
|
||||
- uses: "actions/checkout@v4.1.1"
|
||||
|
||||
- name: "Setup Node.js"
|
||||
uses: "actions/setup-node@v4.0.1"
|
||||
with:
|
||||
node-version: "lts/*"
|
||||
cache: "npm"
|
||||
|
||||
- name: "Install dependencies"
|
||||
run: "npm clean-install"
|
||||
|
||||
- run: 'npm run lint:commit -- --to "${{ github.sha }}"'
|
||||
- run: "npm run lint:editorconfig"
|
||||
- run: "npm run lint:markdown"
|
||||
- run: "npm run lint:eslint"
|
||||
- run: "npm run lint:prettier"
|
33
.github/workflows/node.js.yml
vendored
33
.github/workflows/node.js.yml
vendored
@ -1,33 +0,0 @@
|
||||
name: Node.js CI
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [master]
|
||||
pull_request:
|
||||
branches: [master]
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
strategy:
|
||||
matrix:
|
||||
node-version: [12.x, 14.x]
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: 'Cache dependencies'
|
||||
uses: actions/cache@v2
|
||||
with:
|
||||
path: '**/node_modules'
|
||||
key: ${{ runner.os }}-${{ hashFiles('**/package-lock.json') }}
|
||||
- name: Use Node.js ${{ matrix.node-version }}
|
||||
uses: actions/setup-node@v2.1.2
|
||||
with:
|
||||
node-version: ${{ matrix.node-version }}
|
||||
|
||||
- name: 'Install dependencies'
|
||||
run: npm ci
|
||||
|
||||
- name: 'Run the tests'
|
||||
run: npm test
|
30
.github/workflows/npm-publish.yml
vendored
30
.github/workflows/npm-publish.yml
vendored
@ -1,30 +0,0 @@
|
||||
# For more information see: https://help.github.com/actions/language-and-framework-guides/publishing-nodejs-packages
|
||||
|
||||
name: 'Node.js Package'
|
||||
|
||||
on:
|
||||
release:
|
||||
types: [created]
|
||||
|
||||
jobs:
|
||||
publish-npm:
|
||||
runs-on: 'ubuntu-latest'
|
||||
steps:
|
||||
- uses: 'actions/checkout@v2'
|
||||
|
||||
- name: 'Cache dependencies'
|
||||
uses: 'actions/cache@v2'
|
||||
with:
|
||||
path: '**/node_modules'
|
||||
key: ${{ runner.os }}-${{ hashFiles('**/package.json') }}
|
||||
|
||||
- uses: 'actions/setup-node@v2.1.2'
|
||||
with:
|
||||
node-version: 14
|
||||
registry-url: 'https://registry.npmjs.org/'
|
||||
|
||||
- run: 'npm install'
|
||||
- run: 'npm run build'
|
||||
- run: 'npm publish'
|
||||
env:
|
||||
NODE_AUTH_TOKEN: ${{secrets.NPM_TOKEN}}
|
37
.github/workflows/release.yml
vendored
Normal file
37
.github/workflows/release.yml
vendored
Normal file
@ -0,0 +1,37 @@
|
||||
name: "Release"
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [master]
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: "ubuntu-latest"
|
||||
permissions:
|
||||
contents: "write"
|
||||
issues: "write"
|
||||
pull-requests: "write"
|
||||
id-token: "write"
|
||||
steps:
|
||||
- uses: "actions/checkout@v4.1.1"
|
||||
|
||||
- name: "Setup Node.js"
|
||||
uses: "actions/setup-node@v4.0.1"
|
||||
with:
|
||||
node-version: "lts/*"
|
||||
cache: "npm"
|
||||
|
||||
- name: "Install dependencies"
|
||||
run: "npm clean-install"
|
||||
|
||||
- name: "Build Package"
|
||||
run: "npm run build"
|
||||
|
||||
- name: "Verify the integrity of provenance attestations and registry signatures for installed dependencies"
|
||||
run: "npm audit signatures"
|
||||
|
||||
- name: "Release"
|
||||
run: "npm run release"
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
|
48
.github/workflows/test.yml
vendored
Normal file
48
.github/workflows/test.yml
vendored
Normal file
@ -0,0 +1,48 @@
|
||||
name: "Test"
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [develop]
|
||||
pull_request:
|
||||
branches: [master, develop]
|
||||
|
||||
jobs:
|
||||
test:
|
||||
runs-on: "ubuntu-latest"
|
||||
steps:
|
||||
- uses: "actions/checkout@v4.1.1"
|
||||
|
||||
- name: "Setup Node.js"
|
||||
uses: "actions/setup-node@v4.0.1"
|
||||
with:
|
||||
node-version: "lts/*"
|
||||
cache: "npm"
|
||||
|
||||
- name: "Install dependencies"
|
||||
run: "npm clean-install"
|
||||
|
||||
- name: "Test"
|
||||
run: "npm run test"
|
||||
|
||||
test-e2e:
|
||||
runs-on: "ubuntu-latest"
|
||||
steps:
|
||||
- uses: "actions/checkout@v4.1.1"
|
||||
|
||||
- name: "Setup Node.js"
|
||||
uses: "actions/setup-node@v4.0.1"
|
||||
with:
|
||||
node-version: "lts/*"
|
||||
cache: "npm"
|
||||
|
||||
- name: "Install dependencies"
|
||||
run: "npm clean-install"
|
||||
|
||||
- name: "Build Package"
|
||||
run: "npm run build"
|
||||
|
||||
- name: "Build Example"
|
||||
run: "cd example && npm clean-install && npm run build"
|
||||
|
||||
- name: "End To End (e2e) Test Example"
|
||||
run: "cd example && npm run test:e2e"
|
45
.gitignore
vendored
45
.gitignore
vendored
@ -1,21 +1,40 @@
|
||||
# See https://help.github.com/ignore-files/ for more about ignoring files.
|
||||
|
||||
# dependencies
|
||||
node_modules
|
||||
.npm
|
||||
|
||||
# builds
|
||||
# production
|
||||
build
|
||||
dist
|
||||
.rpt2_cache
|
||||
.next
|
||||
|
||||
# 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
|
||||
.env
|
||||
.env.local
|
||||
.env.development.local
|
||||
.env.test.local
|
||||
.env.production.local
|
||||
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
.parcel-cache
|
||||
.cache
|
||||
|
11
.markdownlint-cli2.jsonc
Normal file
11
.markdownlint-cli2.jsonc
Normal file
@ -0,0 +1,11 @@
|
||||
{
|
||||
"config": {
|
||||
"extends": "markdownlint/style/prettier",
|
||||
"default": true,
|
||||
"relative-links": true,
|
||||
"no-inline-html": false,
|
||||
},
|
||||
"globs": ["**/*.md"],
|
||||
"ignores": ["**/node_modules"],
|
||||
"customRules": ["markdownlint-rule-relative-links"],
|
||||
}
|
10
.prettierrc
10
.prettierrc
@ -1,10 +0,0 @@
|
||||
{
|
||||
"singleQuote": true,
|
||||
"jsxSingleQuote": true,
|
||||
"semi": false,
|
||||
"tabWidth": 2,
|
||||
"bracketSpacing": true,
|
||||
"jsxBracketSameLine": false,
|
||||
"arrowParens": "always",
|
||||
"trailingComma": "none"
|
||||
}
|
3
.prettierrc.json
Normal file
3
.prettierrc.json
Normal file
@ -0,0 +1,3 @@
|
||||
{
|
||||
"semi": false
|
||||
}
|
19
.releaserc.json
Normal file
19
.releaserc.json
Normal file
@ -0,0 +1,19 @@
|
||||
{
|
||||
"branches": ["master"],
|
||||
"plugins": [
|
||||
[
|
||||
"@semantic-release/commit-analyzer",
|
||||
{
|
||||
"preset": "conventionalcommits"
|
||||
}
|
||||
],
|
||||
[
|
||||
"@semantic-release/release-notes-generator",
|
||||
{
|
||||
"preset": "conventionalcommits"
|
||||
}
|
||||
],
|
||||
"@semantic-release/npm",
|
||||
"@semantic-release/github"
|
||||
]
|
||||
}
|
@ -60,7 +60,7 @@ representative at an online or offline event.
|
||||
|
||||
Instances of abusive, harassing, or otherwise unacceptable behavior may be
|
||||
reported to the community leaders responsible for enforcement at
|
||||
contact@divlo.fr.
|
||||
<contact@theoludwig.fr>.
|
||||
All complaints will be reviewed and investigated promptly and fairly.
|
||||
|
||||
All community leaders are obligated to respect the privacy and security of the
|
32
CONTRIBUTING.md
Normal file
32
CONTRIBUTING.md
Normal file
@ -0,0 +1,32 @@
|
||||
# 💡 Contributing
|
||||
|
||||
Thanks a lot for your interest in contributing to **react-component-form**! 🎉
|
||||
|
||||
## Code of Conduct
|
||||
|
||||
**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
|
||||
|
||||
- Reporting a bug.
|
||||
- Suggest a new feature idea.
|
||||
- Correct spelling errors, improvements or additions to documentation files.
|
||||
- Improve structure/format/performance/refactor/tests of the code.
|
||||
|
||||
## Pull Requests
|
||||
|
||||
- **Please first discuss** the change you wish to make via issues.
|
||||
|
||||
- Ensure your code respect linting.
|
||||
|
||||
- Make sure your **code passes the tests**.
|
||||
|
||||
If you're adding new features to **react-component-form**, please include tests.
|
||||
|
||||
## Commits
|
||||
|
||||
The commit message guidelines adheres to [Conventional Commits](https://www.conventionalcommits.org/) and [Semantic Versioning](https://semver.org/) for releases.
|
4
LICENSE
4
LICENSE
@ -1,6 +1,6 @@
|
||||
MIT License
|
||||
# MIT License
|
||||
|
||||
Copyright (c) Divlo
|
||||
Copyright (c) Théo LUDWIG <contact@theoludwig.fr>
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
|
121
README.md
121
README.md
@ -5,21 +5,30 @@
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<a href="https://github.com/Divlo/react-component-form/actions?query=workflow%3A%22Node.js+CI%22"><img src="https://github.com/Divlo/react-component-form/workflows/Node.js%20CI/badge.svg" alt="Node.js CI" /></a>
|
||||
<a href="https://standardjs.com"><img src="https://img.shields.io/badge/code_style-standard-brightgreen.svg" alt="JavaScript Style Guide"/></a>
|
||||
<a href="https://www.npmjs.com/package/react-component-form"><img src="https://img.shields.io/npm/v/react-component-form.svg" alt="npm version"></a>
|
||||
<strong>⚠️ This project is not maintained anymore, you can still use the code as you wish and fork it to maintain it yourself.</strong>
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<a href="./CONTRIBUTING.md"><img src="https://img.shields.io/badge/PRs-welcome-brightgreen.svg?style=flat" alt="CONTRIBUTING /></a>
|
||||
<a href="./LICENSE"><img src="https://img.shields.io/badge/licence-MIT-blue.svg" alt="Licence MIT"/></a>
|
||||
<a href="./CODE_OF_CONDUCT.md"><img src="https://img.shields.io/badge/Contributor%20Covenant-v2.0%20adopted-ff69b4.svg" alt="Contributor Covenant" /></a>
|
||||
<br />
|
||||
<a href="https://github.com/theoludwig/react-component-form/actions/workflows/build.yml"><img src="https://github.com/theoludwig/react-component-form/actions/workflows/build.yml/badge.svg?branch=develop" alt="Build" /></a>
|
||||
<a href="https://github.com/theoludwig/react-component-form/actions/workflows/lint.yml"><img src="https://github.com/theoludwig/react-component-form/actions/workflows/lint.yml/badge.svg?branch=develop" alt="Lint" /></a>
|
||||
<a href="https://github.com/theoludwig/react-component-form/actions/workflows/test.yml"><img src="https://github.com/theoludwig/react-component-form/actions/workflows/test.yml/badge.svg?branch=develop" alt="Test" /></a>
|
||||
<br />
|
||||
<a href="https://conventionalcommits.org"><img src="https://img.shields.io/badge/Conventional%20Commits-1.0.0-yellow.svg" alt="Conventional Commits" /></a>
|
||||
<a href="./.github/CODE_OF_CONDUCT.md"><img src="https://img.shields.io/badge/Contributor%20Covenant-v2.0%20adopted-ff69b4.svg" alt="Contributor Covenant" /></a>
|
||||
<a href="https://github.com/semantic-release/semantic-release"><img src="https://img.shields.io/badge/%20%20%F0%9F%93%A6%F0%9F%9A%80-semantic--release-e10079.svg" alt="semantic-release" /></a>
|
||||
<a href="https://www.npmjs.com/package/react-component-form"><img src="https://img.shields.io/npm/v/react-component-form.svg" alt="npm version"></a>
|
||||
</p>
|
||||
|
||||
## 📜 About
|
||||
|
||||
**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).
|
||||
|
||||
This project was bootstrapped with [create-react-library](https://www.npmjs.com/package/create-react-library).
|
||||
Example demo: [https://react-component-form.vercel.app/](https://react-component-form.vercel.app/).
|
||||
|
||||
## 💾 Install
|
||||
|
||||
@ -29,11 +38,14 @@ npm install --save react-component-form
|
||||
|
||||
## ⚙️ Usage
|
||||
|
||||
```tsx
|
||||
import React from 'react'
|
||||
import Form, { HandleForm } from 'react-component-form'
|
||||
_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._
|
||||
|
||||
const Example = () => {
|
||||
```tsx
|
||||
import React from "react"
|
||||
import { Form } from "react-component-form"
|
||||
import type { HandleForm } from "react-component-form"
|
||||
|
||||
export const Example = () => {
|
||||
const handleSubmit: HandleForm = (formData, formElement) => {
|
||||
console.log(formData) // { inputName: 'value of the input' }
|
||||
formElement.reset()
|
||||
@ -41,21 +53,96 @@ const Example = () => {
|
||||
|
||||
return (
|
||||
<Form onSubmit={handleSubmit}>
|
||||
<input type='text' name='inputName' />
|
||||
<button type='submit'>Submit</button>
|
||||
<input type="text" name="inputName" />
|
||||
<button type="submit">Submit</button>
|
||||
</Form>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
_Note : The example use TypeScript, but obviously you can use JavaScript. Be aware that `HandleForm` is the type definition for the `onChange` and `onSubmit` props._
|
||||
Basically you have access to the same props of the HTML `form` tag in React, but the `onSubmit` and the `onChange` props are differents.
|
||||
|
||||
Basically you have access to the same props of the HTML `form` tag in React, but the onSubmit and the onChange props are differents.
|
||||
Instead to get the `event` param you get `formData` and `formElement` parameters:
|
||||
|
||||
Instead to get the `event` param you get `formData` and `formElement` params :
|
||||
- `formData`: 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`: The 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).
|
||||
|
||||
- `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 { handleUseForm, errors, message } = useForm(schema)
|
||||
|
||||
const onSubmit: HandleUseFormCallback<typeof schema> = (
|
||||
formData,
|
||||
formElement,
|
||||
) => {
|
||||
console.log(formData) // { inputName: 'value of the input validated and type-safe' }
|
||||
formElement.reset()
|
||||
|
||||
// The return can be either `null` or an object with a global message of type `'error' | 'success'`.
|
||||
return {
|
||||
type: "success",
|
||||
message: "Success: Form submitted",
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Form onSubmit={handleUseForm(onSubmit)}>
|
||||
<input type="text" name="inputName" />
|
||||
{errors.inputName != null && <p>{errors.inputName[0].message}</p>}
|
||||
|
||||
<button type="submit">Submit</button>
|
||||
|
||||
{message != null && <p>{message}</p>}
|
||||
</Form>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
## API
|
||||
|
||||
### `useForm(schema)`
|
||||
|
||||
#### Parameters
|
||||
|
||||
- `schema`: The JSON schema to validate the data (recommended to use [@sinclair/typebox](https://www.npmjs.com/package/@sinclair/typebox)).
|
||||
|
||||
#### Returns
|
||||
|
||||
- `handleUseForm(onSubmit)`: Function to be used with the `onSubmit` or `onChange` prop of the `<Form />` component.
|
||||
- `fetchState = 'idle'`: The current state of the form (`'error' | 'success' | 'idle' | 'loading'`).
|
||||
- `setFetchState`: Function to update the `fetchState`.
|
||||
- `message`: Global message of the form (not specific to a property).
|
||||
- `setMessage`: Function to update the `message`.
|
||||
- `errors`: Object of errors:
|
||||
- Key: correspond to a property in the JSON Schema.
|
||||
- Value: array of [ajv `ErrorObject`](https://ajv.js.org/api.html#error-objects).
|
||||
The array will always have at least one element (never empty) in case of errors.
|
||||
If the value is `undefined`, it means there are no errors for this property.
|
||||
|
||||
## 💡 Contributing
|
||||
|
||||
Anyone can help to improve the project, submit a Feature Request, a bug report or
|
||||
even correct a simple spelling mistake.
|
||||
|
||||
The steps to contribute can be found in [CONTRIBUTING.md](./CONTRIBUTING.md).
|
||||
|
||||
## 📄 License
|
||||
|
||||
|
@ -1 +0,0 @@
|
||||
module.exports = { extends: ['@commitlint/config-conventional'] }
|
3
example/.eslintrc.json
Normal file
3
example/.eslintrc.json
Normal file
@ -0,0 +1,3 @@
|
||||
{
|
||||
"extends": "next/core-web-vitals"
|
||||
}
|
39
example/.gitignore
vendored
Normal file
39
example/.gitignore
vendored
Normal file
@ -0,0 +1,39 @@
|
||||
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
||||
|
||||
# dependencies
|
||||
/node_modules
|
||||
/.pnp
|
||||
.pnp.js
|
||||
|
||||
# testing
|
||||
/coverage
|
||||
cypress/screenshots
|
||||
cypress/videos
|
||||
cypress/downloads
|
||||
|
||||
# next.js
|
||||
/.next/
|
||||
/out/
|
||||
|
||||
# production
|
||||
/build
|
||||
|
||||
# misc
|
||||
.DS_Store
|
||||
*.pem
|
||||
|
||||
# debug
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
.pnpm-debug.log*
|
||||
|
||||
# local env files
|
||||
.env*.local
|
||||
|
||||
# vercel
|
||||
.vercel
|
||||
|
||||
# typescript
|
||||
*.tsbuildinfo
|
||||
next-env.d.ts
|
1
example/.npmrc
Normal file
1
example/.npmrc
Normal file
@ -0,0 +1 @@
|
||||
save-exact=true
|
@ -1,5 +1,7 @@
|
||||
This example was bootstrapped with [Create React App](https://github.com/facebook/create-react-app).
|
||||
# example
|
||||
|
||||
It is linked to the react-component-form package in the parent directory for development purposes.
|
||||
This is an example for using `react-component-form` inside a Next.js application with translations thanks to [next-translate](https://www.npmjs.com/package/next-translate).
|
||||
|
||||
You can run `npm install` and then `npm start` to test your package.
|
||||
The application shows how to use the `<Form />` component with the `useForm` hook to validate and submit a form with a `name` input and an `email` input.
|
||||
|
||||
The interesting code is in [./components/FormExample.tsx](./components/FormExample.tsx).
|
||||
|
29
example/components/About.tsx
Normal file
29
example/components/About.tsx
Normal file
@ -0,0 +1,29 @@
|
||||
import Translation from "next-translate/Trans"
|
||||
|
||||
import { Link } from "./design/Link"
|
||||
import { TextSpecial } from "./design/TextSpecial"
|
||||
|
||||
export const About: React.FC = () => {
|
||||
return (
|
||||
<section className="text-center mt-6">
|
||||
<h1 className="text-4xl">{"<Form />"}</h1>
|
||||
<h2 className="text-xl dark:text-gray-300 text-gray-600 mt-4">
|
||||
npm install --save{" "}
|
||||
<Link
|
||||
href="https://www.npmjs.com/package/react-component-form"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
react-component-form
|
||||
</Link>
|
||||
</h2>
|
||||
|
||||
<p className="max-w-lg mt-6 text-base" data-cy="main-description">
|
||||
<Translation
|
||||
i18nKey="common:about"
|
||||
components={[<TextSpecial key="special" />]}
|
||||
/>
|
||||
</p>
|
||||
</section>
|
||||
)
|
||||
}
|
72
example/components/FormExample.tsx
Normal file
72
example/components/FormExample.tsx
Normal file
@ -0,0 +1,72 @@
|
||||
"use client"
|
||||
|
||||
import { Form, useForm } from "react-component-form"
|
||||
import type { HandleUseFormCallback } from "react-component-form"
|
||||
import useTranslation from "next-translate/useTranslation"
|
||||
|
||||
import { Input } from "./design/Input"
|
||||
import { Button } from "./design/Button"
|
||||
import { useFormTranslation } from "../hooks/useFormTranslation"
|
||||
import { userSchema } from "../models/User"
|
||||
import { FormState } from "./design/FormState"
|
||||
|
||||
const fakeServerRequest = async (ms: number): Promise<void> => {
|
||||
return await new Promise((resolve) => {
|
||||
setTimeout(resolve, ms)
|
||||
})
|
||||
}
|
||||
|
||||
export const FormExample: React.FC = () => {
|
||||
const { handleUseForm, errors, fetchState, message } = useForm(userSchema)
|
||||
const { getFirstErrorTranslation } = useFormTranslation()
|
||||
const { t } = useTranslation()
|
||||
|
||||
const onSubmit: HandleUseFormCallback<typeof userSchema> = async (
|
||||
formData,
|
||||
formElement,
|
||||
) => {
|
||||
await fakeServerRequest(2_000)
|
||||
console.log("onSubmit:", formData)
|
||||
formElement.reset()
|
||||
return {
|
||||
type: "success",
|
||||
message: "common:success-message",
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<section>
|
||||
<Form
|
||||
className="mt-6 w-[90%] max-w-xs"
|
||||
noValidate
|
||||
onSubmit={handleUseForm(onSubmit)}
|
||||
>
|
||||
<Input
|
||||
type="text"
|
||||
placeholder={t("common:name")}
|
||||
name="name"
|
||||
label={t("common:name")}
|
||||
error={getFirstErrorTranslation(errors.name)}
|
||||
/>
|
||||
|
||||
<Input
|
||||
type="text"
|
||||
placeholder="Email"
|
||||
name="email"
|
||||
label="Email"
|
||||
error={getFirstErrorTranslation(errors.email)}
|
||||
/>
|
||||
|
||||
<Button className="mt-6 w-full" type="submit" data-cy="submit">
|
||||
Submit
|
||||
</Button>
|
||||
</Form>
|
||||
|
||||
<FormState
|
||||
id="message"
|
||||
state={fetchState}
|
||||
message={message != null ? t(message) : undefined}
|
||||
/>
|
||||
</section>
|
||||
)
|
||||
}
|
11
example/components/Header/Header.tsx
Normal file
11
example/components/Header/Header.tsx
Normal file
@ -0,0 +1,11 @@
|
||||
import { Language } from "./Language"
|
||||
import { SwitchTheme } from "./SwitchTheme"
|
||||
|
||||
export const Header: React.FC = () => {
|
||||
return (
|
||||
<header className="flex justify-center mt-6">
|
||||
<Language />
|
||||
<SwitchTheme />
|
||||
</header>
|
||||
)
|
||||
}
|
16
example/components/Header/Language/Arrow.tsx
Normal file
16
example/components/Header/Language/Arrow.tsx
Normal file
@ -0,0 +1,16 @@
|
||||
export const Arrow: React.FC = () => {
|
||||
return (
|
||||
<svg
|
||||
width="12"
|
||||
height="8"
|
||||
viewBox="0 0 12 8"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
className="fill-current text-black dark:text-white"
|
||||
d="M9.8024 0.292969L5.61855 4.58597L1.43469 0.292969L0.0566406 1.70697L5.61855 7.41397L11.1805 1.70697L9.8024 0.292969Z"
|
||||
/>
|
||||
</svg>
|
||||
)
|
||||
}
|
24
example/components/Header/Language/LanguageFlag.tsx
Normal file
24
example/components/Header/Language/LanguageFlag.tsx
Normal file
@ -0,0 +1,24 @@
|
||||
import Image from "next/image"
|
||||
|
||||
export interface LanguageFlagProps {
|
||||
language: string
|
||||
}
|
||||
|
||||
export const LanguageFlag: React.FC<LanguageFlagProps> = (props) => {
|
||||
const { language } = props
|
||||
|
||||
return (
|
||||
<>
|
||||
<Image
|
||||
quality={100}
|
||||
width={35}
|
||||
height={35}
|
||||
src={`/images/languages/${language}.svg`}
|
||||
alt={language}
|
||||
/>
|
||||
<p data-cy="language-flag-text" className="mx-2 text-base">
|
||||
{language.toUpperCase()}
|
||||
</p>
|
||||
</>
|
||||
)
|
||||
}
|
80
example/components/Header/Language/index.tsx
Normal file
80
example/components/Header/Language/index.tsx
Normal file
@ -0,0 +1,80 @@
|
||||
import { useCallback, useEffect, useState, useRef } from "react"
|
||||
import useTranslation from "next-translate/useTranslation"
|
||||
import setLanguage from "next-translate/setLanguage"
|
||||
import classNames from "clsx"
|
||||
|
||||
import i18n from "../../../i18n.json"
|
||||
import { Arrow } from "./Arrow"
|
||||
import { LanguageFlag } from "./LanguageFlag"
|
||||
|
||||
export const Language: React.FC = () => {
|
||||
const { lang: currentLanguage } = useTranslation()
|
||||
const [hiddenMenu, setHiddenMenu] = useState(true)
|
||||
const languageClickRef = useRef<HTMLDivElement | null>(null)
|
||||
|
||||
const handleHiddenMenu = useCallback(() => {
|
||||
setHiddenMenu((oldHiddenMenu) => {
|
||||
return !oldHiddenMenu
|
||||
})
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
const handleClickEvent = (event: MouseEvent): void => {
|
||||
if (languageClickRef.current == null || event.target == null) {
|
||||
return
|
||||
}
|
||||
if (!languageClickRef.current.contains(event.target as Node)) {
|
||||
setHiddenMenu(true)
|
||||
}
|
||||
}
|
||||
|
||||
window.document.addEventListener("click", handleClickEvent)
|
||||
|
||||
return () => {
|
||||
return window.removeEventListener("click", handleClickEvent)
|
||||
}
|
||||
}, [])
|
||||
|
||||
const handleLanguage = async (language: string): Promise<void> => {
|
||||
await setLanguage(language)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex cursor-pointer flex-col items-center justify-center">
|
||||
<div
|
||||
ref={languageClickRef}
|
||||
data-cy="language-click"
|
||||
className="mr-5 flex items-center"
|
||||
onClick={handleHiddenMenu}
|
||||
>
|
||||
<LanguageFlag language={currentLanguage} />
|
||||
<Arrow />
|
||||
</div>
|
||||
|
||||
<ul
|
||||
data-cy="languages-list"
|
||||
className={classNames(
|
||||
"absolute top-14 z-10 mt-3 mr-4 flex w-24 list-none flex-col items-center justify-center rounded-lg bg-white p-0 shadow-lightFlag dark:bg-black dark:shadow-darkFlag",
|
||||
{ hidden: hiddenMenu },
|
||||
)}
|
||||
>
|
||||
{i18n.locales.map((language, index) => {
|
||||
if (language === currentLanguage) {
|
||||
return null
|
||||
}
|
||||
return (
|
||||
<li
|
||||
key={index}
|
||||
className="flex h-12 w-full items-center justify-center pl-2 hover:bg-[#4f545c] hover:bg-opacity-20"
|
||||
onClick={async () => {
|
||||
await handleLanguage(language)
|
||||
}}
|
||||
>
|
||||
<LanguageFlag language={language} />
|
||||
</li>
|
||||
)
|
||||
})}
|
||||
</ul>
|
||||
</div>
|
||||
)
|
||||
}
|
78
example/components/Header/SwitchTheme.tsx
Normal file
78
example/components/Header/SwitchTheme.tsx
Normal file
@ -0,0 +1,78 @@
|
||||
import { useEffect, useState } from "react"
|
||||
import classNames from "clsx"
|
||||
import { useTheme } from "next-themes"
|
||||
|
||||
export const SwitchTheme: React.FC = () => {
|
||||
const [mounted, setMounted] = useState(false)
|
||||
const { theme, setTheme } = useTheme()
|
||||
|
||||
useEffect(() => {
|
||||
setMounted(true)
|
||||
}, [])
|
||||
|
||||
if (!mounted) {
|
||||
return null
|
||||
}
|
||||
|
||||
const handleClick = (): void => {
|
||||
setTheme(theme === "dark" ? "light" : "dark")
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className="flex items-center"
|
||||
data-cy="switch-theme-click"
|
||||
onClick={handleClick}
|
||||
>
|
||||
<div className="relative inline-block cursor-pointer touch-pan-x select-none border-0 bg-transparent p-0">
|
||||
<div className="h-[24px] w-[50px] rounded-[30px] bg-[#4d4d4d] p-0 text-white transition-all duration-200 ease-in-out">
|
||||
<div
|
||||
data-cy="switch-theme-dark"
|
||||
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="relative flex h-[10px] w-[10px] items-center justify-center">
|
||||
🌜
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
data-cy="switch-theme-light"
|
||||
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={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>
|
||||
)
|
||||
}
|
1
example/components/Header/index.ts
Normal file
1
example/components/Header/index.ts
Normal file
@ -0,0 +1 @@
|
||||
export * from "./Header"
|
19
example/components/design/Button.tsx
Normal file
19
example/components/design/Button.tsx
Normal file
@ -0,0 +1,19 @@
|
||||
import classNames from "clsx"
|
||||
|
||||
export interface ButtonProps extends React.ComponentPropsWithoutRef<"button"> {}
|
||||
|
||||
export const Button: React.FC<ButtonProps> = (props) => {
|
||||
const { children, className, ...rest } = props
|
||||
|
||||
return (
|
||||
<button
|
||||
className={classNames(
|
||||
"py-2 px-6 font-paragraph rounded-lg bg-transparent border hover:text-white dark:hover:text-black fill-current stroke-current transform transition-colors duration-300 ease-in-out focus:outline-none focus:text-white dark:focus:text-black border-green-800 dark:border-green-400 text-green-800 dark:text-green-400 hover:bg-green-800 focus:bg-green-800 dark:focus:bg-green-400 dark:hover:bg-green-400",
|
||||
className,
|
||||
)}
|
||||
{...rest}
|
||||
>
|
||||
{children}
|
||||
</button>
|
||||
)
|
||||
}
|
49
example/components/design/FormState.tsx
Normal file
49
example/components/design/FormState.tsx
Normal file
@ -0,0 +1,49 @@
|
||||
import classNames from "clsx"
|
||||
import useTranslation from "next-translate/useTranslation"
|
||||
import type { FetchState as FormStateType } from "react-component-form"
|
||||
|
||||
import { Loader } from "./Loader"
|
||||
|
||||
export interface FormStateProps extends React.ComponentPropsWithoutRef<"div"> {
|
||||
state: FormStateType
|
||||
message?: string
|
||||
id?: string
|
||||
}
|
||||
|
||||
export const FormState: React.FC<FormStateProps> = (props) => {
|
||||
const { state, message, id, ...rest } = props
|
||||
const { t } = useTranslation()
|
||||
|
||||
if (state === "loading") {
|
||||
return (
|
||||
<div data-cy="loader" className="mt-8 flex justify-center">
|
||||
<Loader />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
if (state === "idle" || message == null) {
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<div
|
||||
{...rest}
|
||||
className={classNames(
|
||||
props.className,
|
||||
"mt-6 flex max-w-xl items-center text-center font-medium",
|
||||
{
|
||||
"text-red-800 dark:text-red-400": state === "error",
|
||||
"text-green-800 dark:text-green-400": state === "success",
|
||||
},
|
||||
)}
|
||||
>
|
||||
<div className="inline bg-cover font-headline" />
|
||||
<span id={id} className="pl-2">
|
||||
<b>{t(`common:${state}`)}:</b> {message}
|
||||
</span>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
37
example/components/design/Input.tsx
Normal file
37
example/components/design/Input.tsx
Normal file
@ -0,0 +1,37 @@
|
||||
import classNames from "clsx"
|
||||
|
||||
import { FormState } from "./FormState"
|
||||
|
||||
export interface InputProps extends React.ComponentPropsWithRef<"input"> {
|
||||
label: string
|
||||
error?: string
|
||||
className?: string
|
||||
}
|
||||
|
||||
export const Input: React.FC<InputProps> = (props) => {
|
||||
const { label, name, className, error, ...rest } = props
|
||||
|
||||
return (
|
||||
<div className="flex flex-col">
|
||||
<div className={classNames("mt-6 mb-2 flex justify-between", className)}>
|
||||
<label className="pl-1" htmlFor={name}>
|
||||
{label}
|
||||
</label>
|
||||
</div>
|
||||
<div className="relative mt-0">
|
||||
<input
|
||||
className="h-11 w-full rounded-lg border border-transparent bg-[#f1f1f1] px-3 font-paragraph leading-10 text-[#2a2a2a] caret-green-600 focus:border focus:shadow-green focus:outline-none"
|
||||
{...rest}
|
||||
id={name}
|
||||
name={name}
|
||||
data-cy={`input-${name ?? "name"}`}
|
||||
/>
|
||||
<FormState
|
||||
id={`error-${name ?? "input"}`}
|
||||
state={error == null ? "idle" : "error"}
|
||||
message={error}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
19
example/components/design/Link.tsx
Normal file
19
example/components/design/Link.tsx
Normal file
@ -0,0 +1,19 @@
|
||||
import classNames from "clsx"
|
||||
|
||||
export interface LinkProps extends React.ComponentPropsWithoutRef<"a"> {}
|
||||
|
||||
export const Link: React.FC<LinkProps> = (props) => {
|
||||
const { children, className, ...rest } = props
|
||||
|
||||
return (
|
||||
<a
|
||||
className={classNames(
|
||||
"text-green-800 hover:underline dark:text-green-400",
|
||||
className,
|
||||
)}
|
||||
{...rest}
|
||||
>
|
||||
{children}
|
||||
</a>
|
||||
)
|
||||
}
|
39
example/components/design/Loader/Loader.module.css
Normal file
39
example/components/design/Loader/Loader.module.css
Normal 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;
|
||||
}
|
33
example/components/design/Loader/Loader.tsx
Normal file
33
example/components/design/Loader/Loader.tsx
Normal 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, className } = props
|
||||
|
||||
return (
|
||||
<div className={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>
|
||||
)
|
||||
}
|
1
example/components/design/Loader/index.ts
Normal file
1
example/components/design/Loader/index.ts
Normal file
@ -0,0 +1 @@
|
||||
export * from "./Loader"
|
17
example/components/design/TextSpecial.tsx
Normal file
17
example/components/design/TextSpecial.tsx
Normal file
@ -0,0 +1,17 @@
|
||||
import classNames from "clsx"
|
||||
|
||||
export interface TextSpecialProps
|
||||
extends React.ComponentPropsWithoutRef<"span"> {}
|
||||
|
||||
export const TextSpecial: React.FC<TextSpecialProps> = (props) => {
|
||||
const { children, className, ...rest } = props
|
||||
|
||||
return (
|
||||
<span
|
||||
className={classNames("text-green-800 dark:text-green-400", className)}
|
||||
{...rest}
|
||||
>
|
||||
{children}
|
||||
</span>
|
||||
)
|
||||
}
|
12
example/cypress.config.ts
Normal file
12
example/cypress.config.ts
Normal file
@ -0,0 +1,12 @@
|
||||
import { defineConfig } from "cypress"
|
||||
|
||||
export default defineConfig({
|
||||
fixturesFolder: false,
|
||||
video: false,
|
||||
downloadsFolder: undefined,
|
||||
screenshotOnRunFailure: false,
|
||||
e2e: {
|
||||
baseUrl: "http://127.0.0.1:3000",
|
||||
supportFile: false,
|
||||
},
|
||||
})
|
65
example/cypress/e2e/Form.cy.ts
Normal file
65
example/cypress/e2e/Form.cy.ts
Normal file
@ -0,0 +1,65 @@
|
||||
describe("Form", () => {
|
||||
beforeEach(() => {
|
||||
cy.visit("/")
|
||||
})
|
||||
|
||||
it("succeeds, reset input values and display the global success message", () => {
|
||||
cy.get("[data-cy=input-name]").type("John")
|
||||
cy.get("[data-cy=input-email]").type("john@john.com")
|
||||
cy.get("#error-name").should("not.exist")
|
||||
cy.get("#error-email").should("not.exist")
|
||||
cy.get("[data-cy=submit]").click()
|
||||
cy.get("[data-cy=input-name]").should("have.value", "")
|
||||
cy.get("[data-cy=input-email]").should("have.value", "")
|
||||
cy.get("#message").should(
|
||||
"have.text",
|
||||
"Success: The form has been submitted.",
|
||||
)
|
||||
})
|
||||
|
||||
it("fails with all inputs as required with error messages and update error messages when updating language (translation)", () => {
|
||||
const requiredErrorMessage = {
|
||||
en: "Error: Oops, this field is required 🙈.",
|
||||
fr: "Erreur: Oups, ce champ est obligatoire 🙈.",
|
||||
}
|
||||
cy.get("#error-name").should("not.exist")
|
||||
cy.get("#error-email").should("not.exist")
|
||||
cy.get("[data-cy=submit]").click()
|
||||
cy.get("#error-name").should("have.text", requiredErrorMessage.en)
|
||||
cy.get("#error-email").should("have.text", requiredErrorMessage.en)
|
||||
cy.get("[data-cy=language-click]").click()
|
||||
cy.get("[data-cy=languages-list] > li:first-child").contains("FR").click()
|
||||
cy.get("#error-name").should("have.text", requiredErrorMessage.fr)
|
||||
cy.get("#error-email").should("have.text", requiredErrorMessage.fr)
|
||||
})
|
||||
|
||||
it("fails with invalid name (less than 3 characters)", () => {
|
||||
cy.get("[data-cy=input-name]").type("a")
|
||||
cy.get("[data-cy=submit]").click()
|
||||
cy.get("#error-name").should(
|
||||
"have.text",
|
||||
"Error: The field must contain at least 3 characters.",
|
||||
)
|
||||
})
|
||||
|
||||
it("fails with invalid name (more than 10 characters)", () => {
|
||||
cy.get("[data-cy=input-name]").type("12345678910aaaa")
|
||||
cy.get("[data-cy=submit]").click()
|
||||
cy.get("#error-name").should(
|
||||
"have.text",
|
||||
"Error: The field must contain at most 10 characters.",
|
||||
)
|
||||
})
|
||||
|
||||
it("fails with wrong email format", () => {
|
||||
cy.get("#error-email").should("not.exist")
|
||||
cy.get("[data-cy=input-email]").type("test")
|
||||
cy.get("[data-cy=submit]").click()
|
||||
cy.get("#error-email").should(
|
||||
"have.text",
|
||||
"Error: Mmm… It seems that this email is not valid 🤔.",
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
export {}
|
49
example/cypress/e2e/Header.cy.ts
Normal file
49
example/cypress/e2e/Header.cy.ts
Normal file
@ -0,0 +1,49 @@
|
||||
describe("Header", () => {
|
||||
beforeEach(() => cy.visit("/"))
|
||||
|
||||
describe("Switch theme color (dark/light)", () => {
|
||||
it("should switch theme from `dark` (default) to `light`", () => {
|
||||
cy.get("[data-cy=switch-theme-dark]").should("be.visible")
|
||||
cy.get("[data-cy=switch-theme-light]").should("not.be.visible")
|
||||
cy.get("body").should(
|
||||
"not.have.css",
|
||||
"background-color",
|
||||
"rgb(255, 255, 255)",
|
||||
)
|
||||
|
||||
cy.get("[data-cy=switch-theme-click]").click()
|
||||
|
||||
cy.get("[data-cy=switch-theme-dark]").should("not.be.visible")
|
||||
cy.get("[data-cy=switch-theme-light]").should("be.visible")
|
||||
cy.get("body").should(
|
||||
"have.css",
|
||||
"background-color",
|
||||
"rgb(255, 255, 255)",
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe("Switch Language", () => {
|
||||
it("should switch language from EN (default) to FR", () => {
|
||||
cy.get("[data-cy=main-description]").contains("This is an example")
|
||||
cy.get("[data-cy=language-flag-text]").contains("EN")
|
||||
cy.get("[data-cy=languages-list]").should("not.be.visible")
|
||||
cy.get("[data-cy=language-click]").click()
|
||||
cy.get("[data-cy=languages-list]").should("be.visible")
|
||||
cy.get("[data-cy=languages-list] > li:first-child").contains("FR").click()
|
||||
cy.get("[data-cy=languages-list]").should("not.be.visible")
|
||||
cy.get("[data-cy=language-flag-text]").contains("FR")
|
||||
cy.get("[data-cy=main-description]").contains("Ceci est un exemple")
|
||||
})
|
||||
|
||||
it("should close the language list menu when clicking outside", () => {
|
||||
cy.get("[data-cy=languages-list]").should("not.be.visible")
|
||||
cy.get("[data-cy=language-click]").click()
|
||||
cy.get("[data-cy=languages-list]").should("be.visible")
|
||||
cy.get("[data-cy=main-description]").click()
|
||||
cy.get("[data-cy=languages-list]").should("not.be.visible")
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
export {}
|
51
example/hooks/useFormTranslation.ts
Normal file
51
example/hooks/useFormTranslation.ts
Normal file
@ -0,0 +1,51 @@
|
||||
import useTranslation from "next-translate/useTranslation"
|
||||
import type { Error } from "react-component-form"
|
||||
|
||||
const knownErrorKeywords = ["minLength", "maxLength", "format"]
|
||||
|
||||
const getErrorTranslationKey = (error: Error): string => {
|
||||
if (knownErrorKeywords.includes(error?.keyword)) {
|
||||
if (
|
||||
error.keyword === "minLength" &&
|
||||
typeof error.data === "string" &&
|
||||
error.data.length === 0
|
||||
) {
|
||||
return "common:required"
|
||||
}
|
||||
if (error.keyword === "format") {
|
||||
if (error.params["format"] === "email") {
|
||||
return "common:invalid-email"
|
||||
}
|
||||
return "common:invalid"
|
||||
}
|
||||
return `common:${error.keyword}`
|
||||
}
|
||||
return "common:invalid"
|
||||
}
|
||||
|
||||
export const useFormTranslation = () => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
const getErrorTranslation = (
|
||||
error: Error | undefined,
|
||||
): string | undefined => {
|
||||
if (error != null) {
|
||||
return t(getErrorTranslationKey(error)).replace(
|
||||
"{expected}",
|
||||
error?.params?.["limit"],
|
||||
)
|
||||
}
|
||||
return undefined
|
||||
}
|
||||
|
||||
const getFirstErrorTranslation = (
|
||||
errors: Error[] | undefined,
|
||||
): string | undefined => {
|
||||
if (errors != null) {
|
||||
return getErrorTranslation(errors[0])
|
||||
}
|
||||
return undefined
|
||||
}
|
||||
|
||||
return { getFirstErrorTranslation, getErrorTranslation }
|
||||
}
|
7
example/i18n.json
Normal file
7
example/i18n.json
Normal file
@ -0,0 +1,7 @@
|
||||
{
|
||||
"locales": ["en", "fr"],
|
||||
"defaultLocale": "en",
|
||||
"pages": {
|
||||
"*": ["common"]
|
||||
}
|
||||
}
|
15
example/locales/en/common.json
Normal file
15
example/locales/en/common.json
Normal file
@ -0,0 +1,15 @@
|
||||
{
|
||||
"about": "This is an example of using <0>`react-component-form`</0> inside a Next.js application. The application shows how to use the <0>`<Form />`</0> component with the <0>`useForm`</0> hook to validate and submit a form with a <0>`name`</0> and an <0>`email`</0> input.",
|
||||
"name": "Name",
|
||||
"error": "Error",
|
||||
"success": "Success",
|
||||
"success-message": "The form has been submitted.",
|
||||
"page-not-found": "This page could not be found.",
|
||||
"server-error": "Internal Server Error.",
|
||||
"return-to-home-page": "Return to the home page?",
|
||||
"required": "Oops, this field is required 🙈.",
|
||||
"minLength": "The field must contain at least {expected} characters.",
|
||||
"maxLength": "The field must contain at most {expected} characters.",
|
||||
"invalid-email": "Mmm… It seems that this email is not valid 🤔.",
|
||||
"invalid": "Invalid value."
|
||||
}
|
15
example/locales/fr/common.json
Normal file
15
example/locales/fr/common.json
Normal file
@ -0,0 +1,15 @@
|
||||
{
|
||||
"about": "Ceci est un exemple d'utilisation de <0>`react-component-form`</0> dans une application Next.js. L'application montre comment utiliser le composant <0>`<Form />`</0> avec le hook <0>`useForm`</0> hook pour valider et soumettre un formulaire avec un input `name` et `email`.",
|
||||
"name": "Nom",
|
||||
"error": "Erreur",
|
||||
"success": "Succès",
|
||||
"success-message": "Le formulaire a été envoyé.",
|
||||
"page-not-found": "Cette page est introuvable.",
|
||||
"server-error": "Erreur interne du serveur.",
|
||||
"return-to-home-page": "Revenir à la page d'accueil ?",
|
||||
"required": "Oups, ce champ est obligatoire 🙈.",
|
||||
"minLength": "Le champ doit contenir au moins {expected} caractères.",
|
||||
"maxLength": "Le champ doit contenir au plus {expected} caractères.",
|
||||
"invalid-email": "Mmm… Il semblerait que cet email ne soit pas valide 🤔.",
|
||||
"invalid": "Valeur invalide."
|
||||
}
|
11
example/models/User.ts
Normal file
11
example/models/User.ts
Normal file
@ -0,0 +1,11 @@
|
||||
import type { Static } from "@sinclair/typebox"
|
||||
import { Type } from "@sinclair/typebox"
|
||||
|
||||
export const userSchema = {
|
||||
name: Type.String({ minLength: 3, maxLength: 10 }),
|
||||
email: Type.String({ minLength: 1, maxLength: 254, format: "email" }),
|
||||
}
|
||||
|
||||
export const userObjectSchema = Type.Object(userSchema)
|
||||
|
||||
export type User = Static<typeof userObjectSchema>
|
8
example/next.config.js
Executable file
8
example/next.config.js
Executable file
@ -0,0 +1,8 @@
|
||||
const nextTranslate = require("next-translate-plugin")
|
||||
|
||||
/** @type {import('next').NextConfig} */
|
||||
const nextConfig = {
|
||||
reactStrictMode: true,
|
||||
}
|
||||
|
||||
module.exports = nextTranslate(nextConfig)
|
40067
example/package-lock.json
generated
40067
example/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -1,44 +1,38 @@
|
||||
{
|
||||
"name": "react-component-form-example",
|
||||
"homepage": ".",
|
||||
"version": "0.0.0",
|
||||
"name": "example",
|
||||
"version": "1.0.0",
|
||||
"private": true,
|
||||
"eslintConfig": {
|
||||
"extends": "react-app"
|
||||
},
|
||||
"browserslist": {
|
||||
"production": [
|
||||
">0.2%",
|
||||
"not dead",
|
||||
"not op_mini all"
|
||||
],
|
||||
"development": [
|
||||
"last 1 chrome version",
|
||||
"last 1 firefox version",
|
||||
"last 1 safari version"
|
||||
]
|
||||
},
|
||||
"scripts": {
|
||||
"start": "node ../node_modules/react-scripts/bin/react-scripts.js start",
|
||||
"build": "node ../node_modules/react-scripts/bin/react-scripts.js build",
|
||||
"test": "node ../node_modules/react-scripts/bin/react-scripts.js test",
|
||||
"eject": "node ../node_modules/react-scripts/bin/react-scripts.js eject"
|
||||
"dev": "next dev",
|
||||
"build": "next build",
|
||||
"start": "next start",
|
||||
"lint": "next lint",
|
||||
"test:e2e": "start-server-and-test \"start\" \"http://127.0.0.1:3000\" \"cypress run\"",
|
||||
"test:dev": "start-server-and-test \"dev\" \"http://127.0.0.1:3000\" \"cypress open\""
|
||||
},
|
||||
"dependencies": {
|
||||
"@testing-library/jest-dom": "file:../node_modules/@testing-library/jest-dom",
|
||||
"@testing-library/react": "file:../node_modules/@testing-library/react",
|
||||
"@testing-library/user-event": "file:../node_modules/@testing-library/user-event",
|
||||
"@types/jest": "file:../node_modules/@types/jest",
|
||||
"@types/node": "file:../node_modules/@types/node",
|
||||
"@types/react": "file:../node_modules/@types/react",
|
||||
"@types/react-dom": "file:../node_modules/@types/react-dom",
|
||||
"react": "file:../node_modules/react",
|
||||
"@sinclair/typebox": "0.32.13",
|
||||
"clsx": "2.1.0",
|
||||
"next": "13.2.4",
|
||||
"next-themes": "0.2.1",
|
||||
"next-translate": "2.0.5",
|
||||
"react": "18.2.0",
|
||||
"react-component-form": "file:..",
|
||||
"react-dom": "file:../node_modules/react-dom",
|
||||
"react-scripts": "file:../node_modules/react-scripts",
|
||||
"typescript": "file:../node_modules/typescript"
|
||||
"react-dom": "18.2.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/plugin-syntax-object-rest-spread": "^7.8.3"
|
||||
"@tsconfig/strictest": "2.0.2",
|
||||
"@types/node": "20.11.10",
|
||||
"@types/react": "18.2.48",
|
||||
"@types/react-dom": "18.2.18",
|
||||
"autoprefixer": "10.4.17",
|
||||
"cypress": "13.6.3",
|
||||
"eslint": "8.56.0",
|
||||
"eslint-config-next": "13.2.4",
|
||||
"next-translate-plugin": "2.0.5",
|
||||
"postcss": "8.4.33",
|
||||
"start-server-and-test": "2.0.3",
|
||||
"tailwindcss": "3.4.1",
|
||||
"typescript": "5.3.3"
|
||||
}
|
||||
}
|
||||
|
14
example/pages/_app.tsx
Normal file
14
example/pages/_app.tsx
Normal file
@ -0,0 +1,14 @@
|
||||
import type { AppType } from "next/app"
|
||||
import { ThemeProvider } from "next-themes"
|
||||
|
||||
import "../styles/globals.css"
|
||||
|
||||
const MyApp: AppType = ({ Component, pageProps }) => {
|
||||
return (
|
||||
<ThemeProvider attribute="class" defaultTheme="dark">
|
||||
<Component {...pageProps} />
|
||||
</ThemeProvider>
|
||||
)
|
||||
}
|
||||
|
||||
export default MyApp
|
15
example/pages/_document.tsx
Normal file
15
example/pages/_document.tsx
Normal file
@ -0,0 +1,15 @@
|
||||
import { Html, Head, Main, NextScript } from "next/document"
|
||||
|
||||
const Document: React.FC = () => {
|
||||
return (
|
||||
<Html>
|
||||
<Head />
|
||||
<body className="bg-white text-black dark:bg-black dark:text-white">
|
||||
<Main />
|
||||
<NextScript />
|
||||
</body>
|
||||
</Html>
|
||||
)
|
||||
}
|
||||
|
||||
export default Document
|
30
example/pages/index.tsx
Normal file
30
example/pages/index.tsx
Normal file
@ -0,0 +1,30 @@
|
||||
import type { GetStaticProps, NextPage } from "next"
|
||||
import Head from "next/head"
|
||||
|
||||
import { About } from "../components/About"
|
||||
import { FormExample } from "../components/FormExample"
|
||||
import { Header } from "../components/Header"
|
||||
|
||||
const Home: NextPage = () => {
|
||||
return (
|
||||
<>
|
||||
<Head>
|
||||
<title>react-component-form</title>
|
||||
<meta name="description" content="Manage React Forms with ease." />
|
||||
<link rel="icon" href="/favicon.ico" />
|
||||
</Head>
|
||||
|
||||
<Header />
|
||||
<main className="flex flex-col justify-center items-center mt-4">
|
||||
<About />
|
||||
<FormExample />
|
||||
</main>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export const getStaticProps: GetStaticProps = async () => {
|
||||
return { props: {} }
|
||||
}
|
||||
|
||||
export default Home
|
6
example/postcss.config.js
Normal file
6
example/postcss.config.js
Normal file
@ -0,0 +1,6 @@
|
||||
module.exports = {
|
||||
plugins: {
|
||||
tailwindcss: {},
|
||||
autoprefixer: {},
|
||||
},
|
||||
}
|
Binary file not shown.
Before Width: | Height: | Size: 3.8 KiB After Width: | Height: | Size: 25 KiB |
30
example/public/images/languages/en.svg
Normal file
30
example/public/images/languages/en.svg
Normal file
@ -0,0 +1,30 @@
|
||||
<svg width="60" height="60" viewBox="0 0 60 60" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M46 4.6C41.3 1.7 35.9 0 30 0V4.6H46Z" fill="#ED4C5C"/>
|
||||
<path d="M30 9.2H51.6C49.9 7.5 48 5.9 46 4.6H30V9.2Z" fill="white"/>
|
||||
<path d="M30 13.8H55.3C54.2 12.1 53 10.6 51.7 9.2H30V13.8Z" fill="#ED4C5C"/>
|
||||
<path d="M30 18.4H57.7C57 16.8 56.2 15.2 55.3 13.8H30V18.4Z" fill="white"/>
|
||||
<path d="M30 23H59.2C58.8 21.4 58.3 19.9 57.7 18.4H30V23Z" fill="#ED4C5C"/>
|
||||
<path d="M30 27.7H59.9C59.8 26.1 59.5 24.6 59.2 23.1H30V27.7Z" fill="white"/>
|
||||
<path d="M59.9 27.7H30V30H0C0 30.8 -9.68575e-08 31.5 0.0999999 32.3H59.9C60 31.5 60 30.8 60 30C60 29.2 60 28.4 59.9 27.7Z" fill="#ED4C5C"/>
|
||||
<path d="M0.800006 36.9H59.2C59.6 35.4 59.8 33.9 59.9 32.3H0.100006C0.200006 33.8 0.400006 35.4 0.800006 36.9Z" fill="white"/>
|
||||
<path d="M2.3 41.5H57.7C58.3 40 58.8 38.5 59.2 36.9H0.800003C1.2 38.5 1.7 40 2.3 41.5Z" fill="#ED4C5C"/>
|
||||
<path d="M4.7 46.1H55.3C56.2 44.6 57 43.1 57.7 41.5H2.3C3 43.1 3.8 44.6 4.7 46.1Z" fill="white"/>
|
||||
<path d="M8.3 50.7H51.7C53 49.3 54.3 47.7 55.3 46.1H4.7C5.7 47.8 7 49.3 8.3 50.7Z" fill="#ED4C5C"/>
|
||||
<path d="M13.9 55.3H46.1C48.2 54 50 52.4 51.7 50.7H8.3C10 52.5 11.9 54 13.9 55.3Z" fill="white"/>
|
||||
<path d="M30 60C35.9 60 41.4 58.3 46.1 55.3H13.9C18.6 58.3 24.1 60 30 60Z" fill="#ED4C5C"/>
|
||||
<path d="M14 4.6C11.9 5.9 10 7.5 8.3 9.2C6.9 10.6 5.7 12.2 4.7 13.8C3.8 15.3 2.9 16.8 2.3 18.4C1.7 19.9 1.2 21.4 0.8 23C0.4 24.5 0.2 26 0.0999999 27.6C-9.68575e-08 28.4 0 29.2 0 30H30V0C24.1 0 18.7 1.7 14 4.6Z" fill="#428BC1"/>
|
||||
<path d="M23 1L23.5 2.5H25L23.8 3.5L24.2 5L23 4.1L21.8 5L22.2 3.5L21 2.5H22.5L23 1Z" fill="white"/>
|
||||
<path d="M27 7L27.5 8.5H29L27.8 9.5L28.2 11L27 10.1L25.8 11L26.2 9.5L25 8.5H26.5L27 7Z" fill="white"/>
|
||||
<path d="M19 7L19.5 8.5H21L19.8 9.5L20.2 11L19 10.1L17.8 11L18.2 9.5L17 8.5H18.5L19 7Z" fill="white"/>
|
||||
<path d="M23 13L23.5 14.5H25L23.8 15.5L24.2 17L23 16.1L21.8 17L22.2 15.5L21 14.5H22.5L23 13Z" fill="white"/>
|
||||
<path d="M15 13L15.5 14.5H17L15.8 15.5L16.2 17L15 16.1L13.8 17L14.2 15.5L13 14.5H14.5L15 13Z" fill="white"/>
|
||||
<path d="M7 13L7.5 14.5H9L7.8 15.5L8.2 17L7 16.1L5.8 17L6.2 15.5L5 14.5H6.5L7 13Z" fill="white"/>
|
||||
<path d="M27 19L27.5 20.5H29L27.8 21.5L28.2 23L27 22.1L25.8 23L26.2 21.5L25 20.5H26.5L27 19Z" fill="white"/>
|
||||
<path d="M19 19L19.5 20.5H21L19.8 21.5L20.2 23L19 22.1L17.8 23L18.2 21.5L17 20.5H18.5L19 19Z" fill="white"/>
|
||||
<path d="M11 19L11.5 20.5H13L11.8 21.5L12.2 23L11 22.1L9.8 23L10.2 21.5L9 20.5H10.5L11 19Z" fill="white"/>
|
||||
<path d="M23 25L23.5 26.5H25L23.8 27.5L24.2 29L23 28.1L21.8 29L22.2 27.5L21 26.5H22.5L23 25Z" fill="white"/>
|
||||
<path d="M15 25L15.5 26.5H17L15.8 27.5L16.2 29L15 28.1L13.8 29L14.2 27.5L13 26.5H14.5L15 25Z" fill="white"/>
|
||||
<path d="M7 25L7.5 26.5H9L7.8 27.5L8.2 29L7 28.1L5.8 29L6.2 27.5L5 26.5H6.5L7 25Z" fill="white"/>
|
||||
<path d="M9.79999 11L11 10.1L12.2 11L11.7 9.5L12.9 8.5H11.4L11 7L10.5 8.5H9.09999L10.3 9.4L9.79999 11Z" fill="white"/>
|
||||
<path d="M1.79999 23L2.99999 22.1L4.19999 23L3.69999 21.5L4.89999 20.5H3.49999L2.99999 19L2.49999 20.5H1.49999C1.49999 20.6 1.39999 20.7 1.39999 20.8L2.19999 21.4L1.79999 23Z" fill="white"/>
|
||||
</svg>
|
After Width: | Height: | Size: 3.0 KiB |
12
example/public/images/languages/fr.svg
Normal file
12
example/public/images/languages/fr.svg
Normal file
@ -0,0 +1,12 @@
|
||||
<svg width="35" height="35" viewBox="0 0 35 35" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clip-path="url(#clip0)">
|
||||
<path d="M0 17.5C0 25.1417 4.9 31.6167 11.6667 34.0084V0.991699C4.9 3.38337 0 9.85837 0 17.5Z" fill="#428BC1"/>
|
||||
<path d="M35 17.5C35 9.85837 30.1584 3.38337 23.3334 0.991699V34.0084C30.1584 31.6167 35 25.1417 35 17.5Z" fill="#ED4C5C"/>
|
||||
<path d="M11.6666 34.0083C13.475 34.65 15.4583 35 17.5 35C19.5416 35 21.525 34.65 23.3333 34.0083V0.991667C21.525 0.35 19.6 0 17.5 0C15.4 0 13.475 0.35 11.6666 0.991667V34.0083Z" fill="white"/>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0">
|
||||
<rect width="35" height="35" fill="white"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
After Width: | Height: | Size: 659 B |
@ -1,48 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<link rel="shortcut icon" href="%PUBLIC_URL%/favicon.ico" />
|
||||
<meta
|
||||
name="viewport"
|
||||
content="width=device-width, initial-scale=1, shrink-to-fit=no"
|
||||
/>
|
||||
<meta name="theme-color" content="#000000" />
|
||||
|
||||
<!--
|
||||
manifest.json provides metadata used when your web app is added to the
|
||||
homescreen on Android. See https://developers.google.com/web/fundamentals/engage-and-retain/web-app-manifest/
|
||||
-->
|
||||
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
|
||||
|
||||
<!--
|
||||
Notice the use of %PUBLIC_URL% in the tags above.
|
||||
It will be replaced with the URL of the `public` folder during the build.
|
||||
Only files inside the `public` folder can be referenced from the HTML.
|
||||
|
||||
Unlike "/favicon.ico" or "favicon.ico", "%PUBLIC_URL%/favicon.ico" will
|
||||
work correctly both with client-side routing and a non-root public URL.
|
||||
Learn how to configure a non-root public URL by running `npm run build`.
|
||||
-->
|
||||
<title>react-component-form</title>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<noscript>
|
||||
You need to enable JavaScript to run this app.
|
||||
</noscript>
|
||||
|
||||
<div id="root"></div>
|
||||
|
||||
<!--
|
||||
This HTML file is a template.
|
||||
If you open it directly in the browser, you will see an empty page.
|
||||
|
||||
You can add webfonts, meta tags, or analytics to this file.
|
||||
The build step will place the bundled scripts into the <body> tag.
|
||||
|
||||
To begin the development, run `npm start` or `yarn start`.
|
||||
To create a production bundle, use `npm run build` or `yarn build`.
|
||||
-->
|
||||
</body>
|
||||
</html>
|
@ -1,15 +0,0 @@
|
||||
{
|
||||
"short_name": "react-component-form",
|
||||
"name": "react-component-form",
|
||||
"icons": [
|
||||
{
|
||||
"src": "favicon.ico",
|
||||
"sizes": "64x64 32x32 24x24 16x16",
|
||||
"type": "image/x-icon"
|
||||
}
|
||||
],
|
||||
"start_url": ".",
|
||||
"display": "standalone",
|
||||
"theme_color": "#000000",
|
||||
"background_color": "#ffffff"
|
||||
}
|
Binary file not shown.
Before Width: | Height: | Size: 83 KiB |
@ -1,112 +0,0 @@
|
||||
*,
|
||||
*::after,
|
||||
*::before {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
body {
|
||||
margin: 0;
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto,
|
||||
'Helvetica Neue', Arial, 'Noto Sans', sans-serif, 'Apple Color Emoji',
|
||||
'Segoe UI Emoji', 'Segoe UI Symbol', 'Noto Color Emoji';
|
||||
font-size: 1rem;
|
||||
font-weight: 400;
|
||||
line-height: 1.5;
|
||||
color: #212529;
|
||||
text-align: left;
|
||||
background-color: #fff;
|
||||
}
|
||||
h1,
|
||||
h2,
|
||||
h3,
|
||||
h4,
|
||||
h5,
|
||||
h6 {
|
||||
font-weight: 500;
|
||||
line-height: 1.2;
|
||||
}
|
||||
h2 {
|
||||
font-size: 2rem;
|
||||
}
|
||||
h4 {
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
h5 {
|
||||
font-size: 1.25rem;
|
||||
}
|
||||
|
||||
.container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
.result-container {
|
||||
border-top: 0.2px solid lightgray;
|
||||
border-bottom: 0.2px solid lightgray;
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
}
|
||||
.github-logo {
|
||||
position: fixed;
|
||||
top: 10px;
|
||||
right: 10px;
|
||||
}
|
||||
.title-install {
|
||||
color: gray;
|
||||
margin-bottom: 50px;
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
form {
|
||||
margin-bottom: 40px;
|
||||
}
|
||||
label {
|
||||
display: inline-block;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
.form-group {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
.form-control {
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: calc(1.5em + 0.75rem + 2px);
|
||||
padding: 0.375rem 0.75rem;
|
||||
font-size: 1rem;
|
||||
font-weight: 400;
|
||||
line-height: 1.5;
|
||||
color: #495057;
|
||||
background-color: #fff;
|
||||
background-clip: padding-box;
|
||||
border: 1px solid #ced4da;
|
||||
border-radius: 0.25rem;
|
||||
transition: border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out;
|
||||
}
|
||||
.btn {
|
||||
display: inline-block;
|
||||
font-weight: 400;
|
||||
color: #212529;
|
||||
text-align: center;
|
||||
vertical-align: middle;
|
||||
-webkit-user-select: none;
|
||||
user-select: none;
|
||||
background-color: transparent;
|
||||
border: 1px solid transparent;
|
||||
padding: 0.375rem 0.75rem;
|
||||
font-size: 1rem;
|
||||
line-height: 1.5;
|
||||
border-radius: 0.25rem;
|
||||
transition: color 0.15s ease-in-out, background-color 0.15s ease-in-out,
|
||||
border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out;
|
||||
cursor: pointer;
|
||||
}
|
||||
.btn-primary {
|
||||
color: #fff;
|
||||
background-color: #007bff;
|
||||
border-color: #007bff;
|
||||
}
|
||||
.btn-primary:hover {
|
||||
color: #fff;
|
||||
background-color: #0069d9;
|
||||
border-color: #0062cc;
|
||||
}
|
@ -1,64 +0,0 @@
|
||||
//eslint-disable-next-line
|
||||
import React from 'react'
|
||||
import ReactDOM from 'react-dom'
|
||||
import Form, { HandleForm } from 'react-component-form'
|
||||
|
||||
import GithubLogo from './github.jpg'
|
||||
import './index.css'
|
||||
|
||||
const App = () => {
|
||||
const handleSubmit: HandleForm = (formData, formElement) => {
|
||||
console.clear()
|
||||
console.log('onSubmit: ', formData)
|
||||
formElement.reset()
|
||||
}
|
||||
|
||||
const handleChange: HandleForm = (formData) => {
|
||||
console.log('onChange: ', formData)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className='container'>
|
||||
<h2>{'<Form />'}</h2>
|
||||
<h5 className='title-install'>npm i react-component-form</h5>
|
||||
|
||||
<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>
|
||||
)
|
||||
}
|
||||
|
||||
ReactDOM.render(<App />, document.getElementById('root'))
|
1
example/src/react-app-env.d.ts
vendored
1
example/src/react-app-env.d.ts
vendored
@ -1 +0,0 @@
|
||||
/// <reference types="react-scripts" />
|
3
example/styles/globals.css
Normal file
3
example/styles/globals.css
Normal file
@ -0,0 +1,3 @@
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
20
example/tailwind.config.js
Normal file
20
example/tailwind.config.js
Normal file
@ -0,0 +1,20 @@
|
||||
/** @type {import('tailwindcss').Config} */
|
||||
const tailwindConfig = {
|
||||
content: [
|
||||
"./pages/**/*.{js,ts,jsx,tsx}",
|
||||
"./components/**/*.{js,ts,jsx,tsx}",
|
||||
],
|
||||
darkMode: "class",
|
||||
theme: {
|
||||
extend: {
|
||||
colors: {
|
||||
black: "#212121",
|
||||
success: "#45C85A",
|
||||
error: "#C84545",
|
||||
},
|
||||
},
|
||||
},
|
||||
plugins: [],
|
||||
}
|
||||
|
||||
module.exports = tailwindConfig
|
@ -1,30 +1,21 @@
|
||||
{
|
||||
"extends": "@tsconfig/strictest/tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "dist",
|
||||
"module": "esnext",
|
||||
"lib": ["dom", "esnext", "DOM.Iterable"],
|
||||
"moduleResolution": "node",
|
||||
"jsx": "react",
|
||||
"sourceMap": true,
|
||||
"declaration": true,
|
||||
"esModuleInterop": true,
|
||||
"noImplicitReturns": true,
|
||||
"noImplicitThis": true,
|
||||
"noImplicitAny": true,
|
||||
"strictNullChecks": true,
|
||||
"suppressImplicitAnyIndexErrors": true,
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true,
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"target": "es5",
|
||||
"target": "ESNext",
|
||||
"module": "ESNext",
|
||||
"lib": ["dom", "dom.iterable", "ESNext"],
|
||||
"allowJs": true,
|
||||
"skipLibCheck": true,
|
||||
"strict": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"baseUrl": ".",
|
||||
"types": ["cypress"],
|
||||
"noEmit": true,
|
||||
"moduleResolution": "node",
|
||||
"resolveJsonModule": true,
|
||||
"jsx": "preserve",
|
||||
"incremental": true,
|
||||
"exactOptionalPropertyTypes": false,
|
||||
"verbatimModuleSyntax": false,
|
||||
"isolatedModules": true,
|
||||
"noEmit": true
|
||||
},
|
||||
"include": ["src"],
|
||||
"exclude": ["node_modules", "build"]
|
||||
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"],
|
||||
"exclude": ["node_modules"],
|
||||
}
|
||||
|
7
jest.config.json
Normal file
7
jest.config.json
Normal file
@ -0,0 +1,7 @@
|
||||
{
|
||||
"testEnvironment": "jsdom",
|
||||
"rootDir": "./src",
|
||||
"transform": {
|
||||
"^.+\\.tsx?$": "esbuild-jest"
|
||||
}
|
||||
}
|
37837
package-lock.json
generated
37837
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
123
package.json
123
package.json
@ -1,86 +1,79 @@
|
||||
{
|
||||
"name": "react-component-form",
|
||||
"version": "1.3.0",
|
||||
"version": "0.0.0-development",
|
||||
"public": true,
|
||||
"type": "module",
|
||||
"description": "Manage React Forms with ease.",
|
||||
"author": "Divlo <contact@divlo.fr>",
|
||||
"author": "Théo LUDWIG <contact@theoludwig.fr>",
|
||||
"license": "MIT",
|
||||
"files": [
|
||||
"dist"
|
||||
],
|
||||
"main": "dist/index.js",
|
||||
"module": "dist/index.modern.js",
|
||||
"types": "dist/index.d.ts",
|
||||
"source": "src/index.tsx",
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/Divlo/react-component-form.git"
|
||||
"url": "https://github.com/theoludwig/react-component-form.git"
|
||||
},
|
||||
"keywords": [
|
||||
"react-form",
|
||||
"react-component-form"
|
||||
],
|
||||
"bugs": {
|
||||
"url": "https://github.com/Divlo/react-component-form/issues"
|
||||
"url": "https://github.com/theoludwig/react-component-form/issues"
|
||||
},
|
||||
"homepage": "https://github.com/Divlo/react-component-form#readme",
|
||||
"husky": {
|
||||
"hooks": {
|
||||
"commit-msg": "commitlint -E HUSKY_GIT_PARAMS"
|
||||
}
|
||||
"homepage": "https://react-component-form.vercel.app/",
|
||||
"main": "build/index.js",
|
||||
"types": "build/index.d.ts",
|
||||
"files": [
|
||||
"build"
|
||||
],
|
||||
"publishConfig": {
|
||||
"access": "public",
|
||||
"provenance": true
|
||||
},
|
||||
"scripts": {
|
||||
"build": "microbundle-crl --no-compress --format modern,cjs",
|
||||
"start": "microbundle-crl watch --no-compress --format modern,cjs",
|
||||
"prepare": "run-s build",
|
||||
"test": "run-s test:unit test:lint test:build",
|
||||
"test:build": "run-s build",
|
||||
"test:lint": "eslint ./**/*.{js,jsx,ts,tsx}",
|
||||
"test:unit": "cross-env CI=1 react-scripts test --env=jsdom",
|
||||
"test:watch": "react-scripts test --env=jsdom",
|
||||
"predeploy": "cd example && npm install && npm run build",
|
||||
"deploy": "gh-pages -d example/build"
|
||||
"build": "tsup",
|
||||
"test": "jest",
|
||||
"lint:commit": "commitlint",
|
||||
"lint:editorconfig": "editorconfig-checker",
|
||||
"lint:markdown": "markdownlint-cli2",
|
||||
"lint:eslint": "eslint . --max-warnings 0 --report-unused-disable-directives --ignore-path .gitignore",
|
||||
"lint:prettier": "prettier . --check",
|
||||
"release": "semantic-release"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "*"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@commitlint/cli": "^11.0.0",
|
||||
"@commitlint/config-conventional": "^11.0.0",
|
||||
"@testing-library/jest-dom": "^5.11.6",
|
||||
"@testing-library/react": "^11.2.2",
|
||||
"@testing-library/user-event": "^12.6.0",
|
||||
"@types/jest": "^26.0.19",
|
||||
"@types/node": "^14.14.16",
|
||||
"@types/react": "^17.0.0",
|
||||
"@types/react-dom": "^17.0.0",
|
||||
"@typescript-eslint/eslint-plugin": "^4.0.1",
|
||||
"@typescript-eslint/parser": "^4.0.1",
|
||||
"babel-eslint": "^10.1.0",
|
||||
"cross-env": "^7.0.3",
|
||||
"eslint": "^6.6.0",
|
||||
"eslint-config-prettier": "^6.12.0",
|
||||
"eslint-config-standard": "^14.1.1",
|
||||
"eslint-config-standard-react": "^9.2.0",
|
||||
"eslint-plugin-import": "^2.22.1",
|
||||
"eslint-plugin-node": "^11.1.0",
|
||||
"eslint-plugin-prettier": "^3.3.0",
|
||||
"eslint-plugin-promise": "^4.2.1",
|
||||
"eslint-plugin-react": "^7.21.5",
|
||||
"eslint-plugin-standard": "^4.0.1",
|
||||
"gh-pages": "^3.1.0",
|
||||
"husky": "^4.3.6",
|
||||
"microbundle-crl": "^0.13.11",
|
||||
"npm-run-all": "^4.1.5",
|
||||
"prettier": "^2.2.1",
|
||||
"react": "^17.0.1",
|
||||
"react-dom": "^17.0.1",
|
||||
"react-scripts": "^3.4.3",
|
||||
"typescript": "^4.1.3"
|
||||
"react": ">=18.2.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"polyfill-object.fromentries": "^1.0.1"
|
||||
"@sinclair/typebox": "0.32.13",
|
||||
"ajv": "8.12.0",
|
||||
"ajv-formats": "2.1.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@commitlint/cli": "18.6.0",
|
||||
"@commitlint/config-conventional": "18.6.0",
|
||||
"@testing-library/react": "14.1.2",
|
||||
"@tsconfig/strictest": "2.0.2",
|
||||
"@types/jest": "29.5.11",
|
||||
"@types/react": "18.2.48",
|
||||
"@types/react-dom": "18.2.18",
|
||||
"@typescript-eslint/eslint-plugin": "6.20.0",
|
||||
"@typescript-eslint/parser": "6.20.0",
|
||||
"editorconfig-checker": "5.1.2",
|
||||
"esbuild": "0.20.0",
|
||||
"esbuild-jest": "0.5.0",
|
||||
"eslint": "8.56.0",
|
||||
"eslint-config-conventions": "13.1.0",
|
||||
"eslint-config-prettier": "9.1.0",
|
||||
"eslint-plugin-import": "2.29.1",
|
||||
"eslint-plugin-prettier": "5.1.3",
|
||||
"eslint-plugin-promise": "6.1.1",
|
||||
"eslint-plugin-unicorn": "50.0.1",
|
||||
"jest": "29.7.0",
|
||||
"jest-environment-jsdom": "29.7.0",
|
||||
"markdownlint-cli2": "0.12.1",
|
||||
"markdownlint-rule-relative-links": "2.2.0",
|
||||
"prettier": "3.2.4",
|
||||
"react": "18.2.0",
|
||||
"react-dom": "18.2.0",
|
||||
"semantic-release": "23.0.0",
|
||||
"tsup": "8.0.1",
|
||||
"typescript": "5.3.3"
|
||||
}
|
||||
}
|
||||
|
@ -1,11 +1,13 @@
|
||||
import React from 'react'
|
||||
import Form, { HandleForm } from '.'
|
||||
import { render, cleanup, fireEvent } from '@testing-library/react'
|
||||
import React from "react"
|
||||
import { render, cleanup, fireEvent } from "@testing-library/react"
|
||||
|
||||
import type { HandleForm } from ".."
|
||||
import { Form } from ".."
|
||||
|
||||
afterEach(cleanup)
|
||||
|
||||
describe('Form component', () => {
|
||||
it('should get the formData and formElement onSubmit and onChange', () => {
|
||||
describe("<Form />", () => {
|
||||
it("should get the formData and formElement onSubmit and onChange", () => {
|
||||
let formData: { [k: string]: any } = {}
|
||||
let formElement: any = null
|
||||
const handleSubmitChange: HandleForm = (data, element) => {
|
||||
@ -14,27 +16,27 @@ describe('Form component', () => {
|
||||
}
|
||||
const formComponent = render(
|
||||
<Form onSubmit={handleSubmitChange} onChange={handleSubmitChange}>
|
||||
<input data-testid='input-form' type='text' name='inputName' />
|
||||
<button data-testid='button-submit' type='submit'>
|
||||
<input data-testid="input-form" type="text" name="inputName" />
|
||||
<button data-testid="button-submit" type="submit">
|
||||
Submit
|
||||
</button>
|
||||
</Form>
|
||||
</Form>,
|
||||
)
|
||||
const inputForm = formComponent.getByTestId(
|
||||
'input-form'
|
||||
"input-form",
|
||||
) as HTMLInputElement
|
||||
const buttonSubmit = formComponent.getByTestId('button-submit')
|
||||
const text = 'some random text'
|
||||
const buttonSubmit = formComponent.getByTestId("button-submit")
|
||||
const text = "some random text"
|
||||
|
||||
fireEvent.change(inputForm, { target: { value: text } })
|
||||
expect(formData.inputName).toEqual(text)
|
||||
expect(formData["inputName"]).toEqual(text)
|
||||
expect(formElement instanceof HTMLFormElement).toBeTruthy()
|
||||
formData = {}
|
||||
formElement = null
|
||||
|
||||
fireEvent.click(buttonSubmit)
|
||||
expect(Object.keys(formData).length).toEqual(1)
|
||||
expect(formData.inputName).toEqual(text)
|
||||
expect(formData["inputName"]).toEqual(text)
|
||||
expect(formElement instanceof HTMLFormElement).toBeTruthy()
|
||||
})
|
||||
})
|
@ -1,29 +1,33 @@
|
||||
// eslint-disable-next-line
|
||||
import React, { useRef } from 'react'
|
||||
import 'polyfill-object.fromentries'
|
||||
import React, { useRef } from "react"
|
||||
|
||||
export interface FormDataObject {
|
||||
[key: string]: FormDataEntryValue
|
||||
}
|
||||
|
||||
/**
|
||||
* @param formData Object where the keys are the name of your inputs and the current value.
|
||||
* @param formElement The HTML form element in the DOM.
|
||||
*/
|
||||
export type HandleForm = (
|
||||
formData: FormDataObject,
|
||||
formElement: HTMLFormElement
|
||||
formElement: HTMLFormElement,
|
||||
) => void | Promise<void>
|
||||
|
||||
interface ReactFormProps
|
||||
extends Omit<React.HTMLProps<HTMLFormElement>, 'onSubmit' | 'onChange'> {}
|
||||
extends Omit<React.HTMLProps<HTMLFormElement>, "onSubmit" | "onChange"> {}
|
||||
|
||||
interface FormProps extends ReactFormProps {
|
||||
export interface FormProps extends ReactFormProps {
|
||||
onSubmit?: HandleForm
|
||||
onChange?: HandleForm
|
||||
}
|
||||
|
||||
const getFormDataObject = (formElement: HTMLFormElement): FormDataObject => {
|
||||
export const getFormDataObject = (
|
||||
formElement: HTMLFormElement,
|
||||
): FormDataObject => {
|
||||
return Object.fromEntries<FormDataEntryValue>(new FormData(formElement))
|
||||
}
|
||||
|
||||
const Form = (props: FormProps): JSX.Element => {
|
||||
export const Form: React.FC<FormProps> = (props) => {
|
||||
const { onSubmit, onChange, children, ...rest } = props
|
||||
const formRef = useRef<HTMLFormElement>(null)
|
||||
|
||||
@ -53,5 +57,3 @@ const Form = (props: FormProps): JSX.Element => {
|
||||
</form>
|
||||
)
|
||||
}
|
||||
|
||||
export default Form
|
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]
|
||||
}
|
166
src/hooks/useForm.ts
Normal file
166
src/hooks/useForm.ts
Normal file
@ -0,0 +1,166 @@
|
||||
import { useMemo, useState } from "react"
|
||||
import type { Static, TObject } from "@sinclair/typebox"
|
||||
import { Type } from "@sinclair/typebox"
|
||||
import type { ErrorObject } from "ajv"
|
||||
|
||||
import type { HandleForm } from "../components/Form"
|
||||
import type { FetchState } from "./useFetchState"
|
||||
import { useFetchState } from "./useFetchState"
|
||||
import { ajv } from "../utils/ajv"
|
||||
import { handleCheckboxBoolean } from "../utils/handleCheckboxBoolean"
|
||||
import { handleOptionalEmptyStringToNull } from "../utils/handleOptionalEmptyStringToNull"
|
||||
|
||||
export interface Schema {
|
||||
[property: string | symbol]: any
|
||||
}
|
||||
|
||||
export type Error = ErrorObject
|
||||
|
||||
export type ErrorsObject<K extends Schema> = {
|
||||
[key in keyof Partial<K>]: Error[] | undefined
|
||||
}
|
||||
|
||||
export type HandleUseFormCallbackResult<K extends Schema> = Message<K> | null
|
||||
|
||||
/**
|
||||
* @param formData Object where the keys are the name of your inputs and the current value.
|
||||
* @param formElement The HTML form element in the DOM.
|
||||
* @returns The return can be either `null` or an object with a global message of type `'error' | 'success'`.
|
||||
*/
|
||||
export type HandleUseFormCallback<K extends Schema> = (
|
||||
formData: Static<TObject<K>>,
|
||||
formElement: HTMLFormElement,
|
||||
) => Promise<HandleUseFormCallbackResult<K>> | HandleUseFormCallbackResult<K>
|
||||
|
||||
export type HandleUseForm<K extends Schema> = (
|
||||
callback?: HandleUseFormCallback<K>,
|
||||
) => HandleForm
|
||||
|
||||
export interface GlobalMessage {
|
||||
type: "error" | "success"
|
||||
message?: string
|
||||
properties?: undefined
|
||||
}
|
||||
|
||||
export interface PropertiesMessage<K extends Schema> {
|
||||
type: "error"
|
||||
message?: string
|
||||
properties: { [key in keyof Partial<K>]: string }
|
||||
}
|
||||
|
||||
export type Message<K extends Schema> = GlobalMessage | PropertiesMessage<K>
|
||||
|
||||
export interface UseFormResult<K extends Schema> {
|
||||
/**
|
||||
* Function to be used with the `onSubmit` or `onChange` prop of the `<Form />` component.
|
||||
*/
|
||||
handleUseForm: HandleUseForm<K>
|
||||
|
||||
/**
|
||||
* The current state of the form.
|
||||
*/
|
||||
readonly fetchState: FetchState
|
||||
setFetchState: React.Dispatch<React.SetStateAction<FetchState>>
|
||||
|
||||
/**
|
||||
* Global message of the form (not specific to a property).
|
||||
*/
|
||||
readonly message: string | undefined
|
||||
setMessage: React.Dispatch<React.SetStateAction<string | undefined>>
|
||||
|
||||
/**
|
||||
* Object of errors:
|
||||
* - Key: correspond to a property in the JSON Schema.
|
||||
* - Value: array of {@link ErrorObject}.
|
||||
*
|
||||
* The array will always have at least one element (never empty) in case of errors.
|
||||
*
|
||||
* If the value is `undefined`, it means there are no errors for this property.
|
||||
*/
|
||||
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 | undefined>(undefined)
|
||||
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(undefined)
|
||||
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 { message: messageValue, type, properties } = message
|
||||
setMessage(messageValue)
|
||||
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,
|
||||
}
|
||||
}
|
1
src/index.d.ts
vendored
1
src/index.d.ts
vendored
@ -1 +0,0 @@
|
||||
declare module 'polyfill-object.fromentries'
|
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"
|
1
src/react-app-env.d.ts
vendored
1
src/react-app-env.d.ts
vendored
@ -1 +0,0 @@
|
||||
/// <reference types="react-scripts" />
|
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 { Schema } from "../hooks/useForm"
|
||||
|
||||
export const handleCheckboxBoolean = (
|
||||
object: Schema,
|
||||
validateSchemaObject: TObject<Schema>,
|
||||
): Schema => {
|
||||
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
|
||||
}
|
19
src/utils/handleOptionalEmptyStringToNull.ts
Normal file
19
src/utils/handleOptionalEmptyStringToNull.ts
Normal file
@ -0,0 +1,19 @@
|
||||
import type { Schema } from "../hooks/useForm"
|
||||
|
||||
export const handleOptionalEmptyStringToNull = <K extends Schema>(
|
||||
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
|
||||
}
|
@ -1,41 +1,15 @@
|
||||
{
|
||||
"extends": "@tsconfig/strictest/tsconfig.json",
|
||||
"include": ["src", "types"],
|
||||
"compilerOptions": {
|
||||
"outDir": "dist",
|
||||
"module": "esnext",
|
||||
"target": "ES3",
|
||||
"lib": [
|
||||
"dom",
|
||||
"esnext",
|
||||
"DOM.Iterable"
|
||||
],
|
||||
"module": "ESNext",
|
||||
"lib": ["ESNext", "DOM", "DOM.Iterable"],
|
||||
"importHelpers": false,
|
||||
"declaration": true,
|
||||
"sourceMap": true,
|
||||
"rootDir": "./src",
|
||||
"moduleResolution": "node",
|
||||
"jsx": "react",
|
||||
"sourceMap": true,
|
||||
"declaration": true,
|
||||
"esModuleInterop": true,
|
||||
"noImplicitReturns": true,
|
||||
"noImplicitThis": true,
|
||||
"noImplicitAny": true,
|
||||
"strictNullChecks": true,
|
||||
"suppressImplicitAnyIndexErrors": true,
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true,
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"downlevelIteration": true,
|
||||
"allowJs": true,
|
||||
"skipLibCheck": true,
|
||||
"strict": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"noEmit": true
|
||||
"noEmit": true,
|
||||
},
|
||||
"include": [
|
||||
"src"
|
||||
],
|
||||
"exclude": [
|
||||
"node_modules",
|
||||
"dist",
|
||||
"example"
|
||||
]
|
||||
}
|
||||
|
@ -1,6 +0,0 @@
|
||||
{
|
||||
"extends": "./tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"module": "commonjs"
|
||||
}
|
||||
}
|
13
tsup.config.js
Normal file
13
tsup.config.js
Normal file
@ -0,0 +1,13 @@
|
||||
import { defineConfig } from "tsup"
|
||||
|
||||
export default defineConfig({
|
||||
entry: ["src/**/*.{ts,tsx}", "!src/**/*.test.{ts,tsx}"],
|
||||
sourcemap: false,
|
||||
clean: true,
|
||||
platform: "browser",
|
||||
target: "esnext",
|
||||
format: ["esm"],
|
||||
minify: false,
|
||||
outDir: "build",
|
||||
dts: true,
|
||||
})
|
Reference in New Issue
Block a user