├── .editorconfig ├── .eslintignore ├── .eslintrc.js ├── .github └── workflows │ ├── node.js.yml │ └── npm-publish.yml ├── .gitignore ├── .husky ├── pre-commit └── pre-push ├── .prettierignore ├── .stylelintrc.js ├── .vscode ├── extensions.json ├── launch.json └── settings.json ├── CHANGELOG.md ├── LICENSE ├── README.md ├── doc ├── email.html ├── example-password.png ├── input-required.png ├── input-type-email.png ├── react-native-android.png └── react-native-ios.png ├── examples ├── Bootstrap │ ├── .browserslistrc │ ├── App.jsx │ ├── App.scss │ ├── App.test.e2e.ts │ ├── babel.config.js │ ├── bootstrap-spinner.scss │ ├── index.html │ ├── package.json │ ├── playwright.config.ts │ └── webpack.config.js ├── ClubMembers │ ├── App.tsx │ ├── babel.config.js │ ├── index.html │ ├── package.json │ ├── style.css │ ├── tsconfig.json │ └── webpack.config.ts ├── HTML5ConstraintValidationAPI │ ├── App.tsx │ ├── babel.config.js │ ├── index.html │ ├── package.json │ ├── style.css │ ├── tsconfig.json │ └── webpack.config.ts ├── MaterialUI │ ├── App.tsx │ ├── babel.config.js │ ├── index.html │ ├── package.json │ ├── tsconfig.json │ └── webpack.config.ts ├── Password │ ├── App.test.e2e.ts │ ├── App.tsx │ ├── babel.config.js │ ├── index.html │ ├── package.json │ ├── playwright.config.ts │ ├── style.css │ ├── tsconfig.json │ └── webpack.config.ts ├── PasswordWithoutState │ ├── App.tsx │ ├── babel.config.js │ ├── index.html │ ├── package.json │ ├── style.css │ ├── tsconfig.json │ └── webpack.config.ts ├── PlainOldReact │ ├── App.tsx │ ├── babel.config.js │ ├── index.html │ ├── package.json │ ├── style.css │ ├── tsconfig.json │ └── webpack.config.ts ├── ReactNative │ ├── App.test.tsx │ ├── App.tsx │ ├── app.json │ ├── babel.config.js │ ├── index.js │ ├── jest.config.js │ ├── metro.config.js │ ├── package.json │ └── tsconfig.json ├── ServerSideRendering │ ├── App.tsx │ ├── babel.config.js │ ├── browser.tsx │ ├── package.json │ ├── server.tsx │ ├── style.css │ ├── tsconfig.json │ └── webpack.config.ts ├── SignUp │ ├── App.tsx │ ├── Gender.ts │ ├── Loader.tsx │ ├── babel.config.js │ ├── i18n.ts │ ├── i18next-parser.config.js │ ├── index.html │ ├── locales │ │ └── fr │ │ │ └── translation.json │ ├── package.json │ ├── spinner.css │ ├── style.css │ ├── tsconfig.json │ └── webpack.config.ts ├── WizardForm │ ├── App.tsx │ ├── Color.ts │ ├── Gender.ts │ ├── WizardForm.tsx │ ├── WizardFormStep1.tsx │ ├── WizardFormStep2.tsx │ ├── WizardFormStep3.tsx │ ├── babel.config.js │ ├── index.html │ ├── package.json │ ├── style.css │ ├── tsconfig.json │ └── webpack.config.ts └── index.html ├── lerna.json ├── package-lock.json ├── package.json ├── packages ├── react-form-with-constraints-bootstrap │ ├── README.md │ ├── jest.config.js │ ├── jest.setup.ts │ ├── package.json │ ├── rollup.config.js │ ├── scss │ │ └── bootstrap.scss │ ├── src │ │ ├── Bootstrap.test.tsx │ │ ├── Bootstrap.ts │ │ ├── FieldFeedbacksEnzymeFix.tsx │ │ ├── SignUp.tsx │ │ └── index.ts │ ├── tsconfig.dist.json │ └── tsconfig.json ├── react-form-with-constraints-material-ui │ ├── README.md │ ├── jest.config.js │ ├── jest.setup.ts │ ├── package.json │ ├── src │ │ ├── FieldFeedbacksEnzymeFix.tsx │ │ ├── Material.test.tsx │ │ ├── Material.tsx │ │ ├── SignUp.tsx │ │ └── index.ts │ ├── tsconfig.dist.json │ └── tsconfig.json ├── react-form-with-constraints-native │ ├── README.md │ ├── babel.config.js │ ├── jest.config.js │ ├── jest.setup.ts │ ├── package.json │ ├── src │ │ ├── FieldFeedbacksEnzymeFix.tsx │ │ ├── Native.test.tsx │ │ ├── Native.tsx │ │ ├── SignUp.tsx │ │ ├── TextInput.tsx │ │ └── index.ts │ ├── tsconfig.dist.json │ └── tsconfig.json ├── react-form-with-constraints-tools │ ├── README.md │ ├── jest.config.js │ ├── jest.setup.ts │ ├── package.json │ ├── rollup.config.js │ ├── src │ │ ├── DisplayFields.test.tsx │ │ ├── DisplayFields.tsx │ │ ├── SignUp.tsx │ │ └── index.ts │ ├── tsconfig.dist.json │ └── tsconfig.json └── react-form-with-constraints │ ├── jest.config.js │ ├── jest.setup.ts │ ├── package.json │ ├── rollup.config.js │ ├── src │ ├── Async.test.tsx │ ├── Async.tsx │ ├── Constructor.ts │ ├── EventEmitter.test.ts │ ├── EventEmitter.ts │ ├── Field.test.ts │ ├── Field.ts │ ├── FieldFeedback.test.tsx │ ├── FieldFeedback.tsx │ ├── FieldFeedbackType.ts │ ├── FieldFeedbackValidation.ts │ ├── FieldFeedbackWhenValid.test.tsx │ ├── FieldFeedbackWhenValid.tsx │ ├── FieldFeedbacks.test.tsx │ ├── FieldFeedbacks.tsx │ ├── FieldFeedbacksEnzymeFix.tsx │ ├── FieldsStore.test.ts │ ├── FieldsStore.ts │ ├── FormWithConstraints.test.tsx │ ├── FormWithConstraints.tsx │ ├── Input.test.tsx │ ├── Input.tsx │ ├── InputElement.test.ts │ ├── InputElement.ts │ ├── InputElementMock.ts │ ├── Nullable.ts │ ├── SignUp.tsx │ ├── assert.test.ts │ ├── assert.ts │ ├── checkUsernameAvailability.ts │ ├── clearArray.test.ts │ ├── clearArray.ts │ ├── deepForEach.test.tsx │ ├── deepForEach.ts │ ├── formatHTML.ts │ ├── index.ts │ ├── notUndefined.test.ts │ ├── notUndefined.ts │ ├── wait.ts │ ├── withFieldDidResetEventEmitter.ts │ ├── withFieldDidValidateEventEmitter.ts │ ├── withFieldWillValidateEventEmitter.ts │ └── withValidateFieldEventEmitter.ts │ ├── tsconfig.dist.json │ └── tsconfig.json ├── prettier.config.js ├── removeConsoleTransform.ts └── tsconfig.json /.editorconfig: -------------------------------------------------------------------------------- 1 | # https://editorconfig.org/ 2 | # Visual Studio Code needs an extension: `ext install EditorConfig` 3 | 4 | root = true 5 | 6 | [*] 7 | end_of_line = lf 8 | charset = utf-8 9 | trim_trailing_whitespace = true 10 | insert_final_newline = true 11 | indent_style = space 12 | indent_size = 2 13 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | build/ 3 | dist/ 4 | coverage/ 5 | -------------------------------------------------------------------------------- /.github/workflows/node.js.yml: -------------------------------------------------------------------------------- 1 | # https://github.com/actions/starter-workflows/blob/692c4c52607f67dd3ee34ad0b7c26066ae85bbae/ci/node.js.yml 2 | 3 | name: Node.js CI 4 | 5 | # https://help.github.com/en/actions/reference/workflow-syntax-for-github-actions#on 6 | on: push 7 | 8 | jobs: 9 | build: 10 | runs-on: ubuntu-latest 11 | 12 | strategy: 13 | matrix: 14 | node-version: [16.x, 18.x] 15 | 16 | steps: 17 | - uses: actions/checkout@v3 18 | - name: Use Node.js ${{matrix.node-version}} 19 | uses: actions/setup-node@v3 20 | with: 21 | node-version: ${{matrix.node-version}} 22 | cache: 'npm' 23 | - run: npm install 24 | - run: npx playwright install --with-deps 25 | - run: npm run build 26 | - run: npm run lint 27 | - run: npm run test:coverage 28 | - run: npm run test:e2e 29 | -------------------------------------------------------------------------------- /.github/workflows/npm-publish.yml: -------------------------------------------------------------------------------- 1 | # https://github.com/actions/starter-workflows/blob/692c4c52607f67dd3ee34ad0b7c26066ae85bbae/ci/npm-publish.yml 2 | 3 | name: Node.js Package 4 | 5 | on: 6 | release: 7 | types: [created] 8 | 9 | jobs: 10 | build: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - uses: actions/checkout@v3 14 | - uses: actions/setup-node@v3 15 | with: 16 | node-version: 16 17 | - run: npm install 18 | - run: npx playwright install --with-deps 19 | - run: npm run prepublishOnly 20 | - run: npm run lint 21 | - run: npm run test:coverage 22 | - run: npm run test:e2e 23 | 24 | publish-npm: 25 | needs: build 26 | runs-on: ubuntu-latest 27 | steps: 28 | - uses: actions/checkout@v3 29 | - uses: actions/setup-node@v3 30 | with: 31 | node-version: 16 32 | registry-url: https://registry.npmjs.org/ 33 | - run: npm install 34 | - run: npm run publish 35 | env: 36 | NODE_AUTH_TOKEN: ${{secrets.NPM_TOKEN}} 37 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Node.js 2 | node_modules/ 3 | 4 | # Generated files 5 | build/ 6 | dist/ 7 | coverage/ 8 | 9 | # React Native example 10 | examples/ReactNative/package-lock.json 11 | examples/ReactNative/.expo/ 12 | examples/ReactNative/ios/ 13 | examples/ReactNative/android/ 14 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | . "$(dirname -- "$0")/_/husky.sh" 3 | 4 | npm run precommit 5 | -------------------------------------------------------------------------------- /.husky/pre-push: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | . "$(dirname -- "$0")/_/husky.sh" 3 | 4 | npm run prepush 5 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | .git/ 2 | .husky/ 3 | node_modules/ 4 | build/ 5 | dist/ 6 | coverage/ 7 | .gitignore 8 | .prettierignore 9 | .eslintignore 10 | .editorconfig 11 | .browserslistrc 12 | *.png 13 | package-lock.json 14 | LICENSE 15 | 16 | # React Native example 17 | examples/ReactNative/.expo/ 18 | examples/ReactNative/ios/ 19 | examples/ReactNative/android/ 20 | -------------------------------------------------------------------------------- /.stylelintrc.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | 3 | /** @type {Partial} */ 4 | const config = { 5 | extends: [ 6 | // /!\ Order matters: the next one overrides rules from the previous one 7 | 8 | // Extends stylelint-config-standard-scss which extends stylelint-config-recommended-scss 9 | 'stylelint-config-twbs-bootstrap', 10 | 11 | 'stylelint-prettier/recommended' 12 | ], 13 | 14 | rules: {} 15 | }; 16 | 17 | module.exports = config; 18 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": [ 3 | "editorconfig.editorconfig", 4 | "esbenp.prettier-vscode", 5 | "dbaeumer.vscode-eslint", 6 | "stylelint.vscode-stylelint" 7 | ] 8 | } 9 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.2.0", 3 | "configurations": [ 4 | { 5 | "name": "Jest react-form-with-constraints", 6 | "type": "node", 7 | "request": "launch", 8 | "program": "${workspaceFolder}/packages/react-form-with-constraints/node_modules/.bin/jest", 9 | "args": ["--runInBand", "--no-cache"], 10 | "cwd": "${workspaceFolder}/packages/react-form-with-constraints", 11 | "runtimeArgs": [ 12 | "--nolazy" // [What does the Node.js `--nolazy` flag mean?](https://stackoverflow.com/q/21534565/990356) 13 | ], 14 | "env": { "NODE_ENV": "development" } 15 | }, 16 | { 17 | "name": "Jest react-form-with-constraints-bootstrap", 18 | "type": "node", 19 | "request": "launch", 20 | "program": "${workspaceFolder}/packages/react-form-with-constraints-bootstrap/node_modules/.bin/jest", 21 | "args": ["--runInBand", "--no-cache"], 22 | "cwd": "${workspaceFolder}/packages/react-form-with-constraints-bootstrap", 23 | "runtimeArgs": ["--nolazy"], 24 | "env": { "NODE_ENV": "development" } 25 | }, 26 | { 27 | "name": "Jest react-form-with-constraints-native", 28 | "type": "node", 29 | "request": "launch", 30 | "program": "${workspaceFolder}/packages/react-form-with-constraints-native/node_modules/.bin/jest", 31 | "args": ["--runInBand", "--no-cache"], 32 | "cwd": "${workspaceFolder}/packages/react-form-with-constraints-native", 33 | "runtimeArgs": ["--nolazy"], 34 | "env": { "NODE_ENV": "development" } 35 | }, 36 | { 37 | "name": "Jest react-form-with-constraints-tools", 38 | "type": "node", 39 | "request": "launch", 40 | "program": "${workspaceFolder}/packages/react-form-with-constraints-tools/node_modules/.bin/jest", 41 | "args": ["--runInBand", "--no-cache"], 42 | "cwd": "${workspaceFolder}/packages/react-form-with-constraints-tools", 43 | "runtimeArgs": ["--nolazy"], 44 | "env": { "NODE_ENV": "development" } 45 | }, 46 | { 47 | "name": "Jest react-form-with-constraints-material-ui", 48 | "type": "node", 49 | "request": "launch", 50 | "program": "${workspaceFolder}/packages/react-form-with-constraints-material-ui/node_modules/.bin/jest", 51 | "args": ["--runInBand", "--no-cache"], 52 | "cwd": "${workspaceFolder}/packages/react-form-with-constraints-material-ui", 53 | "runtimeArgs": ["--nolazy"], 54 | "env": { "NODE_ENV": "development" } 55 | } 56 | ] 57 | } 58 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | // See .editorconfig 3 | 4 | "editor.formatOnSave": true, 5 | // https://github.com/prettier/prettier-vscode 6 | "editor.defaultFormatter": "esbenp.prettier-vscode" 7 | } 8 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) Tanguy Krotoff 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /doc/email.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | HTML5 form validation basic example 8 | 9 | 10 | 11 |
12 | 13 | 14 | 15 |
16 | 17 | 18 | -------------------------------------------------------------------------------- /doc/example-password.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tkrotoff/react-form-with-constraints/0b1af608c35ee66dccd11196053dada3f3f63e5b/doc/example-password.png -------------------------------------------------------------------------------- /doc/input-required.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tkrotoff/react-form-with-constraints/0b1af608c35ee66dccd11196053dada3f3f63e5b/doc/input-required.png -------------------------------------------------------------------------------- /doc/input-type-email.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tkrotoff/react-form-with-constraints/0b1af608c35ee66dccd11196053dada3f3f63e5b/doc/input-type-email.png -------------------------------------------------------------------------------- /doc/react-native-android.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tkrotoff/react-form-with-constraints/0b1af608c35ee66dccd11196053dada3f3f63e5b/doc/react-native-android.png -------------------------------------------------------------------------------- /doc/react-native-ios.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tkrotoff/react-form-with-constraints/0b1af608c35ee66dccd11196053dada3f3f63e5b/doc/react-native-ios.png -------------------------------------------------------------------------------- /examples/Bootstrap/.browserslistrc: -------------------------------------------------------------------------------- 1 | >= 1% 2 | Explorer 11 3 | -------------------------------------------------------------------------------- /examples/Bootstrap/App.scss: -------------------------------------------------------------------------------- 1 | @import '~bootstrap/scss/bootstrap'; 2 | @import '~react-form-with-constraints-bootstrap/scss/bootstrap'; 3 | 4 | @import './bootstrap-spinner'; 5 | -------------------------------------------------------------------------------- /examples/Bootstrap/App.test.e2e.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable jest/no-standalone-expect */ 2 | 3 | import { expect, test } from '@playwright/test'; 4 | 5 | test.describe.configure({ mode: 'parallel' }); 6 | 7 | test.beforeEach(async ({ page }) => { 8 | await page.goto('http://localhost:8080'); 9 | }); 10 | 11 | test('john/123456/12345', async ({ page }) => { 12 | const username = page.locator('input[name=username]'); 13 | await username.fill('john'); 14 | await page.waitForSelector('input[name=username] ~ span[data-feedback].invalid-feedback'); 15 | const usernameFeedbacks = page.locator('input[name=username] ~ span[data-feedback]'); 16 | await expect(usernameFeedbacks).toHaveCount(1); 17 | expect(usernameFeedbacks.nth(0)).toHaveText('Username already taken, choose another'); 18 | 19 | const password = page.locator('input[name=password]'); 20 | await password.fill('123456'); 21 | await page.waitForSelector('input[name=password] ~ span[data-feedback]'); 22 | const passwordFeedbacks = page.locator('input[name=password] ~ span[data-feedback]'); 23 | await expect(passwordFeedbacks).toHaveCount(4); 24 | expect(passwordFeedbacks.nth(0)).toHaveText('Should contain small letters'); 25 | expect(passwordFeedbacks.nth(1)).toHaveText('Should contain capital letters'); 26 | expect(passwordFeedbacks.nth(2)).toHaveText('Should contain special characters'); 27 | expect(passwordFeedbacks.nth(3)).toHaveText('Looks good!'); 28 | 29 | const passwordConfirm = page.locator('input[name=passwordConfirm]'); 30 | await passwordConfirm.fill('12345'); 31 | const passwordConfirmFeedbacks = page.locator( 32 | 'input[name=passwordConfirm] ~ span[data-feedback]' 33 | ); 34 | await expect(passwordConfirmFeedbacks).toHaveCount(1); 35 | expect(passwordConfirmFeedbacks.nth(0)).toHaveText('Not the same password'); 36 | 37 | const signUp = page.locator("'Sign Up'"); 38 | await expect(signUp).toBeDisabled(); 39 | }); 40 | 41 | function indent(text: string, indentation: string) { 42 | return ( 43 | text 44 | // [Add a char to the start of each line in JavaScript using regular expression](https://stackoverflow.com/q/11939575) 45 | .replace(/^/gm, indentation) 46 | // [Trim trailing spaces before newlines in a single multi-line string in JavaScript](https://stackoverflow.com/q/5568797) 47 | .replace(/[^\S\n]+$/gm, '') 48 | ); 49 | } 50 | 51 | test('jimmy/12345/12345', async ({ page }) => { 52 | const username = page.locator('input[name=username]'); 53 | await username.fill('jimmy'); 54 | await page.waitForSelector('input[name=username] ~ span[data-feedback].valid-feedback'); 55 | const usernameFeedbacks = page.locator('input[name=username] ~ span[data-feedback]'); 56 | await expect(usernameFeedbacks).toHaveCount(2); 57 | expect(usernameFeedbacks.nth(0)).toHaveText('Username available'); 58 | expect(usernameFeedbacks.nth(1)).toHaveText('Looks good!'); 59 | 60 | const password = page.locator('input[name=password]'); 61 | await password.fill('12345'); 62 | await page.waitForSelector('input[name=password] ~ span[data-feedback]'); 63 | const passwordFeedbacks = page.locator('input[name=password] ~ span[data-feedback]'); 64 | await expect(passwordFeedbacks).toHaveCount(4); 65 | expect(passwordFeedbacks.nth(0)).toHaveText('Should contain small letters'); 66 | expect(passwordFeedbacks.nth(1)).toHaveText('Should contain capital letters'); 67 | expect(passwordFeedbacks.nth(2)).toHaveText('Should contain special characters'); 68 | expect(passwordFeedbacks.nth(3)).toHaveText('Looks good!'); 69 | 70 | const passwordConfirm = page.locator('input[name=passwordConfirm]'); 71 | await passwordConfirm.fill('12345'); 72 | const passwordConfirmFeedbacks = page.locator( 73 | 'input[name=passwordConfirm] ~ span[data-feedback]' 74 | ); 75 | await expect(passwordConfirmFeedbacks).toHaveCount(1); 76 | expect(passwordConfirmFeedbacks.nth(0)).toHaveText('Looks good!'); 77 | 78 | const signUp = page.locator("'Sign Up'"); 79 | await expect(signUp).toBeEnabled(); 80 | 81 | page.on('dialog', async dialog => { 82 | expect(indent(dialog.message(), ' ')).toEqual(`\ 83 | Valid form 84 | 85 | inputs = 86 | { 87 | "username": "jimmy", 88 | "password": "12345", 89 | "passwordConfirm": "12345" 90 | }`); 91 | await dialog.accept(); 92 | }); 93 | await signUp.click(); 94 | }); 95 | -------------------------------------------------------------------------------- /examples/Bootstrap/babel.config.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | 3 | module.exports = { 4 | presets: [ 5 | [ 6 | '@babel/preset-env', 7 | { 8 | useBuiltIns: 'entry', 9 | corejs: 3, 10 | debug: false 11 | } 12 | ], 13 | ['@babel/preset-react', { runtime: 'automatic' }] 14 | ] 15 | }; 16 | -------------------------------------------------------------------------------- /examples/Bootstrap/bootstrap-spinner.scss: -------------------------------------------------------------------------------- 1 | // Taken and adapted from https://github.com/tkrotoff/bootstrap-input-spinner/blob/v0.1.0/src/bootstrap4-input-spinner.scss 2 | 3 | .spinner-border, 4 | .spinner-grow { 5 | display: none; 6 | } 7 | 8 | @function calc-input-right-padding($right-element-width) { 9 | @return calc( 10 | #{$right-element-width} - #{$input-border-width} * 2 + #{$input-height-inner-quarter} * 2 11 | ); 12 | } 13 | 14 | @function calc-input-top-margin($right-element-width) { 15 | @return calc((#{$input-height} + #{$right-element-width}) / -2); 16 | } 17 | 18 | // stylelint-disable selector-no-qualifying-type 19 | 20 | input.is-pending { 21 | // Cannot use the same as the validation icons because .spinner-border is too big 22 | padding-right: calc-input-right-padding($spinner-width); 23 | 24 | &.is-pending-sm { 25 | // Same as the validation icons, this is possible because .spinner-border-sm is small enough 26 | //padding-right: calcInputRightPadding($spinner-width-sm); 27 | // https://github.com/twbs/bootstrap/blob/v4.3.1/scss/mixins/_forms.scss#L59 28 | padding-right: $input-height-inner; 29 | } 30 | 31 | + .spinner-border, 32 | + .spinner-grow { 33 | display: block; 34 | 35 | float: right; 36 | 37 | // Same as the validation icons 38 | // https://github.com/twbs/bootstrap/blob/v4.3.1/scss/mixins/_forms.scss#L62 39 | margin-right: $input-height-inner-quarter; 40 | 41 | // stylelint-disable-next-line order/properties-order 42 | margin-top: calc-input-top-margin($spinner-height); 43 | 44 | &.spinner-border-sm, 45 | &.spinner-grow-sm { 46 | margin-top: calc-input-top-margin($spinner-height-sm); 47 | } 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /examples/Bootstrap/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | react-form-with-constraints Bootstrap example 8 | 9 | 10 | 11 |
12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /examples/Bootstrap/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "bootstrap-example", 3 | "version": "0.19.1", 4 | "private": true, 5 | "description": "react-form-with-constraints Bootstrap example", 6 | "keywords": [ 7 | "react", 8 | "form", 9 | "validation", 10 | "react-form-with-constraints", 11 | "bootstrap" 12 | ], 13 | "main": "App.jsx", 14 | "scripts": { 15 | "clean": "rm -rf build", 16 | "start": "webpack serve --mode=development --host 0.0.0.0", 17 | "start:prod": "webpack serve --mode=production --compress --host 0.0.0.0", 18 | "build": "webpack --mode=development", 19 | "build:watch": "webpack --mode=development --watch", 20 | "build:prod": "webpack --mode=production", 21 | "test:e2e": "playwright test", 22 | "test:e2e:debug": "PWDEBUG=1 playwright test" 23 | }, 24 | "dependencies": { 25 | "bootstrap": "^5.2.0", 26 | "core-js": "^3.23.5", 27 | "lodash-es": "^4.17.21", 28 | "raf": "^3.4.1", 29 | "react": "^18.2.0", 30 | "react-dom": "^18.2.0", 31 | "react-form-with-constraints": "^0.19.1", 32 | "react-form-with-constraints-bootstrap": "^0.19.1", 33 | "react-form-with-constraints-tools": "^0.19.1" 34 | }, 35 | "devDependencies": { 36 | "@babel/core": "^7.18.9", 37 | "@babel/preset-env": "^7.18.9", 38 | "@babel/preset-react": "^7.18.6", 39 | "@playwright/test": "^1.23.4", 40 | "@types/react-dom": "^18.0.6", 41 | "babel-loader": "^8.2.5", 42 | "css-loader": "^6.7.1", 43 | "postcss": "^8.4.14", 44 | "postcss-loader": "^7.0.1", 45 | "postcss-preset-env": "^7.7.2", 46 | "sass": "^1.53.0", 47 | "sass-loader": "^13.0.2", 48 | "style-loader": "^3.3.1", 49 | "webpack": "^5.73.0", 50 | "webpack-cli": "^4.10.0", 51 | "webpack-dev-server": "^4.9.3" 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /examples/Bootstrap/playwright.config.ts: -------------------------------------------------------------------------------- 1 | import { devices, PlaywrightTestConfig } from '@playwright/test'; 2 | 3 | const config: PlaywrightTestConfig = { 4 | testMatch: /.*\.test\.e2e\.ts$/, 5 | 6 | use: { 7 | headless: true 8 | }, 9 | 10 | webServer: { 11 | command: 'npm run start', 12 | port: 8080 13 | }, 14 | 15 | projects: [ 16 | { 17 | name: 'Desktop Chrome', 18 | use: devices['Desktop Chrome'] 19 | }, 20 | { 21 | name: 'Desktop Firefox', 22 | use: devices['Desktop Firefox'] 23 | }, 24 | { 25 | name: 'Desktop Safari', 26 | use: devices['Desktop Safari'] 27 | } 28 | ] 29 | }; 30 | 31 | export default config; 32 | -------------------------------------------------------------------------------- /examples/Bootstrap/webpack.config.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | 3 | const path = require('node:path'); 4 | 5 | /** @type import('webpack').Configuration */ 6 | const config = { 7 | entry: './App.jsx', 8 | 9 | output: { 10 | path: path.resolve('build') 11 | }, 12 | 13 | resolve: { 14 | extensions: ['.js', '.jsx'] 15 | }, 16 | 17 | module: { 18 | rules: [ 19 | { 20 | test: /\.jsx?$/, 21 | // [Babel should not transpile core-js](https://github.com/zloirock/core-js/issues/514#issuecomment-476533317) 22 | exclude: /\/core-js/, 23 | loader: 'babel-loader' 24 | }, 25 | { test: /\.html$/, type: 'asset/resource', generator: { filename: '[name][ext]' } }, 26 | { 27 | test: /\.scss$/, 28 | use: [ 29 | 'style-loader', 30 | 'css-loader', 31 | { 32 | loader: 'postcss-loader', 33 | options: { postcssOptions: { plugins: [['postcss-preset-env']] } } 34 | }, 35 | 'sass-loader' 36 | ] 37 | } 38 | ] 39 | } 40 | }; 41 | 42 | module.exports = config; 43 | -------------------------------------------------------------------------------- /examples/ClubMembers/babel.config.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | 3 | module.exports = { 4 | presets: [['@babel/preset-react', { runtime: 'automatic' }], '@babel/preset-typescript'], 5 | plugins: [ 6 | // https://mobx.js.org/enabling-decorators.html 7 | ['@babel/plugin-proposal-decorators', { legacy: true }], 8 | ['@babel/plugin-proposal-class-properties', { loose: false }] 9 | ] 10 | }; 11 | -------------------------------------------------------------------------------- /examples/ClubMembers/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | react-form-with-constraints ClubMembers example 8 | 9 | 13 | 14 | 15 | 16 |
17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /examples/ClubMembers/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "club-members-example", 3 | "version": "0.19.1", 4 | "private": true, 5 | "description": "react-form-with-constraints ClubMembers example", 6 | "keywords": [ 7 | "react", 8 | "form", 9 | "validation", 10 | "react-form-with-constraints" 11 | ], 12 | "main": "App.tsx", 13 | "scripts": { 14 | "clean": "rm -rf build", 15 | "tsc": "tsc", 16 | "start": "webpack serve --mode=development --host 0.0.0.0", 17 | "build": "webpack --mode=development", 18 | "build:watch": "webpack --mode=development --watch" 19 | }, 20 | "dependencies": { 21 | "mobx": "^6.6.1", 22 | "mobx-react": "^7.5.2", 23 | "react": "^18.2.0", 24 | "react-dom": "^18.2.0", 25 | "react-form-with-constraints": "^0.19.1", 26 | "react-form-with-constraints-tools": "^0.19.1" 27 | }, 28 | "devDependencies": { 29 | "@babel/core": "^7.18.9", 30 | "@babel/plugin-proposal-class-properties": "^7.18.6", 31 | "@babel/plugin-proposal-decorators": "^7.18.9", 32 | "@babel/preset-react": "^7.18.6", 33 | "@babel/preset-typescript": "^7.18.6", 34 | "@types/node": "^18.0.6", 35 | "@types/react-dom": "^18.0.6", 36 | "babel-loader": "^8.2.5", 37 | "css-loader": "^6.7.1", 38 | "style-loader": "^3.3.1", 39 | "ts-node": "^10.9.1", 40 | "typescript": "^4.7.4", 41 | "webpack": "^5.73.0", 42 | "webpack-cli": "^4.10.0", 43 | "webpack-dev-server": "^4.9.3" 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /examples/ClubMembers/style.css: -------------------------------------------------------------------------------- 1 | .list-style-none { 2 | list-style-type: none; 3 | } 4 | 5 | /* 6 | * https://github.com/twbs/bootstrap/blob/v5.0.1/scss/mixins/_forms.scss#L26 7 | */ 8 | [data-feedback].error { 9 | margin-top: 0.25rem; /* $form-feedback-margin-top => $form-text-margin-top => .25rem */ 10 | font-size: 0.875em; /* $form-feedback-font-size => $form-text-font-size => $small-font-size => .875em */ 11 | color: #dc3545; /* $form-feedback-invalid-color => $danger => $red => #dc3545 */ 12 | } 13 | [data-feedback].warning { 14 | margin-top: 0.25rem; 15 | font-size: 0.875em; 16 | color: #ffc107; /* $warning => $yellow => #ffc107 */ 17 | } 18 | [data-feedback].info { 19 | margin-top: 0.25rem; 20 | font-size: 0.875em; 21 | color: #0dcaf0; /* $info => $cyan => #0dcaf0 */ 22 | } 23 | [data-feedback].when-valid { 24 | margin-top: 0.25rem; 25 | font-size: 0.875em; 26 | color: #198754; /* $form-feedback-valid-color => $success => $green => #198754 */ 27 | } 28 | -------------------------------------------------------------------------------- /examples/ClubMembers/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "noEmit": true, 4 | 5 | // Nullish coalescing operator (??) needs to be transpiled by TypeScript to support Node.js 12 6 | // https://stackoverflow.com/a/59787575 7 | "target": "es2019", 8 | 9 | "module": "commonjs", 10 | "moduleResolution": "node", 11 | "jsx": "preserve", 12 | "esModuleInterop": false, 13 | "resolveJsonModule": true, 14 | "isolatedModules": true, 15 | "experimentalDecorators": true, 16 | 17 | "strict": true, 18 | "noUnusedLocals": true, 19 | "noUnusedParameters": true, 20 | "noImplicitReturns": true, 21 | "noFallthroughCasesInSwitch": true, 22 | "forceConsistentCasingInFileNames": true, 23 | 24 | // FIXME 25 | // [error TS2300: Duplicate identifier 'require'](https://github.com/tkrotoff/react-form-with-constraints/issues/12) 26 | // [@types/react-native definitions for `global` and `require` conflict with @types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/issues/16825) 27 | "skipLibCheck": true 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /examples/ClubMembers/webpack.config.ts: -------------------------------------------------------------------------------- 1 | import * as path from 'node:path'; 2 | import { Configuration } from 'webpack'; 3 | 4 | const config: Configuration = { 5 | entry: './App.tsx', 6 | 7 | output: { 8 | path: path.resolve('build') 9 | }, 10 | 11 | resolve: { 12 | extensions: ['.js', '.ts', '.tsx'] 13 | }, 14 | 15 | module: { 16 | rules: [ 17 | { test: /\.tsx?$/, loader: 'babel-loader' }, 18 | { test: /\.css$/, use: ['style-loader', 'css-loader'] }, 19 | { test: /\.html$/, type: 'asset/resource', generator: { filename: '[name][ext]' } } 20 | ] 21 | } 22 | }; 23 | 24 | export default config; 25 | -------------------------------------------------------------------------------- /examples/HTML5ConstraintValidationAPI/App.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useRef, useState } from 'react'; 2 | import { createRoot } from 'react-dom/client'; 3 | 4 | import './index.html'; 5 | import './style.css'; 6 | 7 | // Inspired by [ReactJS Form Validation Approaches](https://moduscreate.com/reactjs-form-validation-approaches/) 8 | 9 | interface Errors { 10 | email: string[]; 11 | password: string[]; 12 | passwordConfirm: string[]; 13 | } 14 | 15 | function validateHTML5Field(field: HTMLInputElement) { 16 | const errors = []; 17 | if (field.validationMessage !== '') errors.push(field.validationMessage); 18 | return errors; 19 | } 20 | 21 | function validatePasswordConfirm(password: HTMLInputElement, passwordConfirm: HTMLInputElement) { 22 | const errors = []; 23 | if (passwordConfirm.value !== password.value) errors.push('Not the same password'); 24 | return errors; 25 | } 26 | 27 | function hasErrors(errors: Errors) { 28 | return errors.email.length > 0 || errors.password.length > 0 || errors.passwordConfirm.length > 0; 29 | } 30 | 31 | function Form() { 32 | const email = useRef(null); 33 | const password = useRef(null); 34 | const passwordConfirm = useRef(null); 35 | 36 | const [errors, setErrors] = useState({ 37 | email: [], 38 | password: [], 39 | passwordConfirm: [] 40 | }); 41 | 42 | const [isSubmitted, setIsSubmitted] = useState(false); 43 | useEffect(() => { 44 | if (isSubmitted) { 45 | if (!hasErrors(errors)) { 46 | alert('Valid form'); 47 | } else { 48 | alert('Invalid form'); 49 | } 50 | setIsSubmitted(false); 51 | } 52 | }, [isSubmitted, errors]); 53 | 54 | function handleEmailChange({ target }: React.ChangeEvent) { 55 | setErrors(prevState => { 56 | return { 57 | ...prevState, 58 | email: validateHTML5Field(target) 59 | }; 60 | }); 61 | } 62 | 63 | function handlePasswordChange({ target }: React.ChangeEvent) { 64 | setErrors(prevState => { 65 | return { 66 | ...prevState, 67 | password: validateHTML5Field(target), 68 | passwordConfirm: validatePasswordConfirm(target, passwordConfirm.current!) 69 | }; 70 | }); 71 | } 72 | 73 | function handlePasswordConfirmChange({ target }: React.ChangeEvent) { 74 | setErrors(prevState => { 75 | return { 76 | ...prevState, 77 | passwordConfirm: validatePasswordConfirm(password.current!, target) 78 | }; 79 | }); 80 | } 81 | 82 | function handleSubmit(e: React.FormEvent) { 83 | e.preventDefault(); 84 | 85 | setErrors(prevState => { 86 | return { 87 | ...prevState, 88 | email: validateHTML5Field(email.current!), 89 | password: validateHTML5Field(password.current!), 90 | passwordConfirm: validatePasswordConfirm(password.current!, passwordConfirm.current!) 91 | }; 92 | }); 93 | setIsSubmitted(true); 94 | } 95 | 96 | return ( 97 |
98 |
99 | 100 | 109 |
110 | {errors.email.map(error => ( 111 |
{error}
112 | ))} 113 |
114 |
115 | 116 |
117 | 118 | 127 |
128 | {errors.password.map(error => ( 129 |
{error}
130 | ))} 131 |
132 |
133 | 134 |
135 | 136 | 143 |
144 | {errors.passwordConfirm.map(error => ( 145 |
{error}
146 | ))} 147 |
148 |
149 | 150 | 151 |
152 | ); 153 | } 154 | 155 | const root = createRoot(document.getElementById('app')!); 156 | root.render(
); 157 | -------------------------------------------------------------------------------- /examples/HTML5ConstraintValidationAPI/babel.config.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | 3 | module.exports = { 4 | presets: [['@babel/preset-react', { runtime: 'automatic' }], '@babel/preset-typescript'] 5 | }; 6 | -------------------------------------------------------------------------------- /examples/HTML5ConstraintValidationAPI/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | React with HTML5 constraint validation API example 8 | 9 | 10 | 11 |
12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /examples/HTML5ConstraintValidationAPI/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "html5-constraint-validation-api-example", 3 | "version": "0.19.1", 4 | "private": true, 5 | "description": "React with HTML5 constraint validation API example", 6 | "keywords": [ 7 | "react", 8 | "html5", 9 | "form", 10 | "validation", 11 | "constraints", 12 | "constraint-validation-api" 13 | ], 14 | "main": "App.tsx", 15 | "scripts": { 16 | "clean": "rm -rf build", 17 | "tsc": "tsc", 18 | "start": "webpack serve --mode=development --host 0.0.0.0", 19 | "build": "webpack --mode=development", 20 | "build:watch": "webpack --mode=development --watch" 21 | }, 22 | "dependencies": { 23 | "react": "^18.2.0", 24 | "react-dom": "^18.2.0" 25 | }, 26 | "devDependencies": { 27 | "@babel/core": "^7.18.9", 28 | "@babel/preset-react": "^7.18.6", 29 | "@babel/preset-typescript": "^7.18.6", 30 | "@types/node": "^18.0.6", 31 | "@types/react-dom": "^18.0.6", 32 | "babel-loader": "^8.2.5", 33 | "css-loader": "^6.7.1", 34 | "style-loader": "^3.3.1", 35 | "ts-node": "^10.9.1", 36 | "typescript": "^4.7.4", 37 | "webpack": "^5.73.0", 38 | "webpack-cli": "^4.10.0", 39 | "webpack-dev-server": "^4.9.3" 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /examples/HTML5ConstraintValidationAPI/style.css: -------------------------------------------------------------------------------- 1 | /* 2 | Copy-pasted from PlainOldReact/style.css 3 | Cannot use a symlink nor "import '../PlainOldReact/style.css'" because of StackBlitz 4 | */ 5 | 6 | body { 7 | background-color: lightgrey; 8 | } 9 | 10 | form > div { 11 | margin-bottom: 10px; 12 | } 13 | 14 | form > div > label { 15 | display: block; 16 | } 17 | 18 | div.error { 19 | color: red; 20 | } 21 | -------------------------------------------------------------------------------- /examples/HTML5ConstraintValidationAPI/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "noEmit": true, 4 | 5 | // Nullish coalescing operator (??) needs to be transpiled by TypeScript to support Node.js 12 6 | // https://stackoverflow.com/a/59787575 7 | "target": "es2019", 8 | 9 | "module": "commonjs", 10 | "moduleResolution": "node", 11 | "jsx": "preserve", 12 | "esModuleInterop": false, 13 | "resolveJsonModule": true, 14 | "isolatedModules": true, 15 | 16 | "strict": true, 17 | "noUnusedLocals": true, 18 | "noUnusedParameters": true, 19 | "noImplicitReturns": true, 20 | "noFallthroughCasesInSwitch": true, 21 | "forceConsistentCasingInFileNames": true, 22 | 23 | // FIXME 24 | // [error TS2300: Duplicate identifier 'require'](https://github.com/tkrotoff/react-form-with-constraints/issues/12) 25 | // [@types/react-native definitions for `global` and `require` conflict with @types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/issues/16825) 26 | "skipLibCheck": true 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /examples/HTML5ConstraintValidationAPI/webpack.config.ts: -------------------------------------------------------------------------------- 1 | import * as path from 'node:path'; 2 | import { Configuration } from 'webpack'; 3 | 4 | const config: Configuration = { 5 | entry: './App.tsx', 6 | 7 | output: { 8 | path: path.resolve('build') 9 | }, 10 | 11 | resolve: { 12 | extensions: ['.js', '.ts', '.tsx'] 13 | }, 14 | 15 | module: { 16 | rules: [ 17 | { test: /\.tsx?$/, loader: 'babel-loader' }, 18 | { test: /\.css$/, use: ['style-loader', 'css-loader'] }, 19 | { test: /\.html$/, type: 'asset/resource', generator: { filename: '[name][ext]' } } 20 | ] 21 | } 22 | }; 23 | 24 | export default config; 25 | -------------------------------------------------------------------------------- /examples/MaterialUI/babel.config.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | 3 | module.exports = { 4 | presets: [['@babel/preset-react', { runtime: 'automatic' }], '@babel/preset-typescript'] 5 | }; 6 | -------------------------------------------------------------------------------- /examples/MaterialUI/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | react-form-with-constraints Material-UI example 8 | 9 | 10 | 11 | 25 | 26 | 27 | 28 |
29 | 30 | 31 | 32 | 33 | -------------------------------------------------------------------------------- /examples/MaterialUI/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "material-ui-example", 3 | "version": "0.19.1", 4 | "private": true, 5 | "description": "react-form-with-constraints Material-UI example", 6 | "keywords": [ 7 | "react", 8 | "form", 9 | "validation", 10 | "react-form-with-constraints", 11 | "material design", 12 | "material-ui" 13 | ], 14 | "main": "App.tsx", 15 | "scripts": { 16 | "clean": "rm -rf build", 17 | "tsc": "tsc", 18 | "start": "webpack serve --mode=development --host 0.0.0.0", 19 | "build": "webpack --mode=development", 20 | "build:prod": "webpack --mode=production", 21 | "build:watch": "webpack --mode=development --watch" 22 | }, 23 | "dependencies": { 24 | "@material-ui/core": "^4.12.4", 25 | "lodash-es": "^4.17.21", 26 | "react": "^16.14.0", 27 | "react-dom": "^16.14.0", 28 | "react-form-with-constraints": "^0.19.1", 29 | "react-form-with-constraints-material-ui": "^0.19.1", 30 | "react-form-with-constraints-tools": "^0.19.1" 31 | }, 32 | "devDependencies": { 33 | "@babel/core": "^7.18.9", 34 | "@babel/preset-react": "^7.18.6", 35 | "@babel/preset-typescript": "^7.18.6", 36 | "@types/lodash-es": "^4.17.6", 37 | "@types/node": "^18.0.6", 38 | "@types/react-dom": "^18.0.6", 39 | "babel-loader": "^8.2.5", 40 | "ts-node": "^10.9.1", 41 | "typescript": "^4.7.4", 42 | "webpack": "^5.73.0", 43 | "webpack-cli": "^4.10.0", 44 | "webpack-dev-server": "^4.9.3" 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /examples/MaterialUI/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "noEmit": true, 4 | 5 | // Nullish coalescing operator (??) needs to be transpiled by TypeScript to support Node.js 12 6 | // https://stackoverflow.com/a/59787575 7 | "target": "es2019", 8 | 9 | "module": "commonjs", 10 | "moduleResolution": "node", 11 | "jsx": "preserve", 12 | "esModuleInterop": false, 13 | "resolveJsonModule": true, 14 | "isolatedModules": true, 15 | 16 | "strict": true, 17 | "noUnusedLocals": true, 18 | "noUnusedParameters": true, 19 | "noImplicitReturns": true, 20 | "noFallthroughCasesInSwitch": true, 21 | "forceConsistentCasingInFileNames": true, 22 | 23 | // FIXME 24 | // [error TS2300: Duplicate identifier 'require'](https://github.com/tkrotoff/react-form-with-constraints/issues/12) 25 | // [@types/react-native definitions for `global` and `require` conflict with @types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/issues/16825) 26 | "skipLibCheck": true 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /examples/MaterialUI/webpack.config.ts: -------------------------------------------------------------------------------- 1 | import * as path from 'node:path'; 2 | import { Configuration } from 'webpack'; 3 | 4 | const config: Configuration = { 5 | entry: './App.tsx', 6 | 7 | output: { 8 | path: path.resolve('build') 9 | }, 10 | 11 | resolve: { 12 | extensions: ['.js', '.ts', '.tsx'] 13 | }, 14 | 15 | module: { 16 | rules: [ 17 | { test: /\.tsx?$/, loader: 'babel-loader' }, 18 | { test: /\.html$/, type: 'asset/resource', generator: { filename: '[name][ext]' } } 19 | ] 20 | } 21 | }; 22 | 23 | export default config; 24 | -------------------------------------------------------------------------------- /examples/Password/App.test.e2e.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable jest/no-standalone-expect */ 2 | 3 | import { expect, test } from '@playwright/test'; 4 | 5 | test.describe.configure({ mode: 'parallel' }); 6 | 7 | test.beforeEach(async ({ page }) => { 8 | await page.goto('http://localhost:8080'); 9 | }); 10 | 11 | test('john@beatles/123456/12345', async ({ page }) => { 12 | const email = page.locator('input[name=email]'); 13 | await email.fill('john@beatles'); 14 | await page.waitForSelector('input[name=email] ~ span[data-feedback]'); 15 | const emailFeedbacks = page.locator('input[name=email] ~ span[data-feedback]'); 16 | await expect(emailFeedbacks).toHaveCount(1); 17 | expect(emailFeedbacks.nth(0)).toHaveText('Looks good!'); 18 | 19 | const password = page.locator('input[name=password]'); 20 | await password.fill('123456'); 21 | await page.waitForSelector('input[name=password] ~ span[data-feedback].when-valid'); 22 | const passwordFeedbacks = page.locator('input[name=password] ~ span[data-feedback]'); 23 | expect(passwordFeedbacks).toHaveCount(5); 24 | expect(passwordFeedbacks.nth(0)).toHaveText('Should contain small letters'); 25 | expect(passwordFeedbacks.nth(1)).toHaveText('Should contain capital letters'); 26 | expect(passwordFeedbacks.nth(2)).toHaveText('Should contain special characters'); 27 | expect(passwordFeedbacks.nth(3)).toHaveText('This password is very common'); 28 | expect(passwordFeedbacks.nth(4)).toHaveText('Looks good!'); 29 | 30 | const passwordConfirm = page.locator('input[name=passwordConfirm]'); 31 | await passwordConfirm.fill('12345'); 32 | const passwordConfirmFeedbacks = page.locator( 33 | 'input[name=passwordConfirm] ~ span[data-feedback]' 34 | ); 35 | expect(passwordConfirmFeedbacks).toHaveCount(1); 36 | expect(passwordConfirmFeedbacks.nth(0)).toHaveText('Not the same password'); 37 | 38 | const signUp = page.locator("'Sign Up'"); 39 | await expect(signUp).toBeDisabled(); 40 | }); 41 | 42 | function indent(text: string, indentation: string) { 43 | return ( 44 | text 45 | // [Add a char to the start of each line in JavaScript using regular expression](https://stackoverflow.com/q/11939575) 46 | .replace(/^/gm, indentation) 47 | // [Trim trailing spaces before newlines in a single multi-line string in JavaScript](https://stackoverflow.com/q/5568797) 48 | .replace(/[^\S\n]+$/gm, '') 49 | ); 50 | } 51 | 52 | test('john@beatles/Tr0ub4dor&3/Tr0ub4dor&3', async ({ page }) => { 53 | const email = page.locator('input[name=email]'); 54 | await email.fill('john@beatles'); 55 | await page.waitForSelector('input[name=email] ~ span[data-feedback]'); 56 | const emailFeedbacks = page.locator('input[name=email] ~ span[data-feedback]'); 57 | expect(emailFeedbacks).toHaveCount(1); 58 | expect(emailFeedbacks.nth(0)).toHaveText('Looks good!'); 59 | 60 | const password = page.locator('input[name=password]'); 61 | await password.fill('Tr0ub4dor&3'); 62 | await page.waitForSelector('input[name=password] ~ span[data-feedback].when-valid'); 63 | const passwordFeedbacks = page.locator('input[name=password] ~ span[data-feedback]'); 64 | expect(passwordFeedbacks).toHaveCount(1); 65 | expect(passwordFeedbacks.nth(0)).toHaveText('Looks good!'); 66 | 67 | const passwordConfirm = page.locator('input[name=passwordConfirm]'); 68 | await passwordConfirm.click(); 69 | await passwordConfirm.fill('Tr0ub4dor&3'); 70 | const passwordConfirmFeedbacks = page.locator( 71 | 'input[name=passwordConfirm] ~ span[data-feedback]' 72 | ); 73 | expect(passwordConfirmFeedbacks).toHaveCount(0); 74 | 75 | const signUp = page.locator("'Sign Up'"); 76 | await expect(signUp).toBeEnabled(); 77 | 78 | page.on('dialog', async dialog => { 79 | expect(indent(dialog.message(), ' ')).toEqual(`\ 80 | Valid form 81 | 82 | inputs = 83 | { 84 | "email": "john@beatles", 85 | "password": "Tr0ub4dor&3", 86 | "passwordConfirm": "Tr0ub4dor&3" 87 | }`); 88 | await dialog.accept(); 89 | }); 90 | await signUp.click(); 91 | }); 92 | -------------------------------------------------------------------------------- /examples/Password/babel.config.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | 3 | module.exports = { 4 | presets: [['@babel/preset-react', { runtime: 'automatic' }], '@babel/preset-typescript'] 5 | }; 6 | -------------------------------------------------------------------------------- /examples/Password/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | react-form-with-constraints Password example 8 | 9 | 10 | 11 |
12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /examples/Password/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "password-example", 3 | "version": "0.19.1", 4 | "private": true, 5 | "description": "react-form-with-constraints Password example", 6 | "keywords": [ 7 | "react", 8 | "form", 9 | "validation", 10 | "react-form-with-constraints" 11 | ], 12 | "main": "App.tsx", 13 | "scripts": { 14 | "clean": "rm -rf build", 15 | "tsc": "tsc", 16 | "start": "webpack serve --mode=development --host 0.0.0.0", 17 | "build": "webpack --mode=development", 18 | "build:watch": "webpack --mode=development --watch", 19 | "test:e2e": "playwright test", 20 | "test:e2e:debug": "PWDEBUG=1 playwright test" 21 | }, 22 | "dependencies": { 23 | "react": "^18.2.0", 24 | "react-dom": "^18.2.0", 25 | "react-form-with-constraints": "^0.19.1", 26 | "react-form-with-constraints-tools": "^0.19.1" 27 | }, 28 | "devDependencies": { 29 | "@babel/core": "^7.18.9", 30 | "@babel/preset-react": "^7.18.6", 31 | "@babel/preset-typescript": "^7.18.6", 32 | "@playwright/test": "^1.23.4", 33 | "@types/circular-dependency-plugin": "^5.0.5", 34 | "@types/node": "^18.0.6", 35 | "@types/react-dom": "^18.0.6", 36 | "babel-loader": "^8.2.5", 37 | "circular-dependency-plugin": "^5.2.2", 38 | "css-loader": "^6.7.1", 39 | "style-loader": "^3.3.1", 40 | "ts-node": "^10.9.1", 41 | "typescript": "^4.7.4", 42 | "webpack": "^5.73.0", 43 | "webpack-cli": "^4.10.0", 44 | "webpack-dev-server": "^4.9.3" 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /examples/Password/playwright.config.ts: -------------------------------------------------------------------------------- 1 | import { devices, PlaywrightTestConfig } from '@playwright/test'; 2 | 3 | const config: PlaywrightTestConfig = { 4 | testMatch: /.*\.test\.e2e\.ts$/, 5 | 6 | use: { 7 | headless: true 8 | }, 9 | 10 | webServer: { 11 | command: 'npm run start', 12 | port: 8080 13 | }, 14 | 15 | projects: [ 16 | { 17 | name: 'Desktop Chrome', 18 | use: devices['Desktop Chrome'] 19 | }, 20 | { 21 | name: 'Desktop Firefox', 22 | use: devices['Desktop Firefox'] 23 | }, 24 | { 25 | name: 'Desktop Safari', 26 | use: devices['Desktop Safari'] 27 | } 28 | ] 29 | }; 30 | 31 | export default config; 32 | -------------------------------------------------------------------------------- /examples/Password/style.css: -------------------------------------------------------------------------------- 1 | body { 2 | background-color: lightgrey; 3 | } 4 | 5 | form > div { 6 | margin-bottom: 10px; 7 | } 8 | 9 | form > div > label { 10 | display: block; 11 | } 12 | 13 | [data-feedback].error { 14 | color: red; 15 | } 16 | [data-feedback].warning { 17 | color: orange; 18 | } 19 | [data-feedback].info { 20 | color: blue; 21 | } 22 | [data-feedback].when-valid { 23 | color: green; 24 | } 25 | -------------------------------------------------------------------------------- /examples/Password/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "noEmit": true, 4 | 5 | // Nullish coalescing operator (??) needs to be transpiled by TypeScript to support Node.js 12 6 | // https://stackoverflow.com/a/59787575 7 | "target": "es2019", 8 | 9 | "module": "commonjs", 10 | "moduleResolution": "node", 11 | "jsx": "preserve", 12 | "esModuleInterop": false, 13 | "resolveJsonModule": true, 14 | "isolatedModules": true, 15 | 16 | "strict": true, 17 | "noUnusedLocals": true, 18 | "noUnusedParameters": true, 19 | "noImplicitReturns": true, 20 | "noFallthroughCasesInSwitch": true, 21 | "forceConsistentCasingInFileNames": true, 22 | 23 | // FIXME 24 | // [error TS2300: Duplicate identifier 'require'](https://github.com/tkrotoff/react-form-with-constraints/issues/12) 25 | // [@types/react-native definitions for `global` and `require` conflict with @types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/issues/16825) 26 | "skipLibCheck": true 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /examples/Password/webpack.config.ts: -------------------------------------------------------------------------------- 1 | import * as CircularDependencyPlugin from 'circular-dependency-plugin'; 2 | import * as path from 'node:path'; 3 | import { Configuration } from 'webpack'; 4 | 5 | const config: Configuration = { 6 | entry: './App.tsx', 7 | 8 | output: { 9 | path: path.resolve('build') 10 | }, 11 | 12 | plugins: [ 13 | new CircularDependencyPlugin({ 14 | failOnError: true 15 | }) 16 | ], 17 | 18 | resolve: { 19 | extensions: ['.js', '.ts', '.tsx'] 20 | }, 21 | 22 | module: { 23 | rules: [ 24 | { test: /\.tsx?$/, loader: 'babel-loader' }, 25 | { test: /\.css$/, use: ['style-loader', 'css-loader'] }, 26 | { test: /\.html$/, type: 'asset/resource', generator: { filename: '[name][ext]' } } 27 | ] 28 | } 29 | }; 30 | 31 | export default config; 32 | -------------------------------------------------------------------------------- /examples/PasswordWithoutState/App.tsx: -------------------------------------------------------------------------------- 1 | import { useRef } from 'react'; 2 | import { createRoot } from 'react-dom/client'; 3 | import { FieldFeedback, FieldFeedbacks, FormWithConstraints } from 'react-form-with-constraints'; 4 | 5 | import './index.html'; 6 | import './style.css'; 7 | 8 | function Form() { 9 | const form = useRef(null); 10 | const password = useRef(null); 11 | 12 | async function handleChange({ target }: React.ChangeEvent) { 13 | await form.current!.validateFields(target); 14 | } 15 | 16 | async function handlePasswordChange({ target }: React.ChangeEvent) { 17 | await form.current!.validateFields(target, 'passwordConfirm'); 18 | } 19 | 20 | async function handleSubmit(e: React.FormEvent) { 21 | e.preventDefault(); 22 | 23 | await form.current!.validateFields(); 24 | if (form.current!.isValid()) { 25 | alert('Valid form'); 26 | } else { 27 | alert('Invalid form'); 28 | } 29 | } 30 | 31 | return ( 32 | 33 |
34 | 35 | 43 | 44 | Too short 45 | 46 | Looks good! 47 | 48 |
49 | 50 |
51 | 52 | 61 | 62 | 63 | Should be at least 5 characters long 64 | !/\d/.test(value)} warning> 65 | Should contain numbers 66 | 67 | !/[a-z]/.test(value)} warning> 68 | Should contain small letters 69 | 70 | !/[A-Z]/.test(value)} warning> 71 | Should contain capital letters 72 | 73 | !/\W/.test(value)} warning> 74 | Should contain special characters 75 | 76 | Looks good! 77 | 78 |
79 | 80 |
81 | 82 | 88 | 89 | value !== password.current!.value}> 90 | Not the same password 91 | 92 | 93 |
94 | 95 | 96 |
97 | ); 98 | } 99 | 100 | const root = createRoot(document.getElementById('app')!); 101 | root.render(); 102 | -------------------------------------------------------------------------------- /examples/PasswordWithoutState/babel.config.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | 3 | module.exports = { 4 | presets: [['@babel/preset-react', { runtime: 'automatic' }], '@babel/preset-typescript'] 5 | }; 6 | -------------------------------------------------------------------------------- /examples/PasswordWithoutState/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | react-form-with-constraints Password without state example 8 | 9 | 10 | 11 |
12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /examples/PasswordWithoutState/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "password-without-state-example", 3 | "version": "0.19.1", 4 | "private": true, 5 | "description": "react-form-with-constraints Password without state example", 6 | "keywords": [ 7 | "react", 8 | "form", 9 | "validation", 10 | "react-form-with-constraints" 11 | ], 12 | "main": "App.tsx", 13 | "scripts": { 14 | "clean": "rm -rf build", 15 | "tsc": "tsc", 16 | "start": "webpack serve --mode=development --host 0.0.0.0", 17 | "build": "webpack --mode=development", 18 | "build:watch": "webpack --mode=development --watch" 19 | }, 20 | "dependencies": { 21 | "react": "^18.2.0", 22 | "react-dom": "^18.2.0", 23 | "react-form-with-constraints": "^0.19.1" 24 | }, 25 | "devDependencies": { 26 | "@babel/core": "^7.18.9", 27 | "@babel/preset-react": "^7.18.6", 28 | "@babel/preset-typescript": "^7.18.6", 29 | "@types/node": "^18.0.6", 30 | "@types/react-dom": "^18.0.6", 31 | "babel-loader": "^8.2.5", 32 | "css-loader": "^6.7.1", 33 | "style-loader": "^3.3.1", 34 | "ts-node": "^10.9.1", 35 | "typescript": "^4.7.4", 36 | "webpack": "^5.73.0", 37 | "webpack-cli": "^4.10.0", 38 | "webpack-dev-server": "^4.9.3" 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /examples/PasswordWithoutState/style.css: -------------------------------------------------------------------------------- 1 | /* 2 | Copy-pasted from Password/style.css 3 | Cannot use a symlink nor "import '../Password/style.css'" because of StackBlitz 4 | */ 5 | 6 | body { 7 | background-color: lightgrey; 8 | } 9 | 10 | form > div { 11 | margin-bottom: 10px; 12 | } 13 | 14 | form > div > label { 15 | display: block; 16 | } 17 | 18 | [data-feedback].error { 19 | color: red; 20 | } 21 | [data-feedback].warning { 22 | color: orange; 23 | } 24 | [data-feedback].info { 25 | color: blue; 26 | } 27 | [data-feedback].when-valid { 28 | color: green; 29 | } 30 | -------------------------------------------------------------------------------- /examples/PasswordWithoutState/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "noEmit": true, 4 | 5 | // Nullish coalescing operator (??) needs to be transpiled by TypeScript to support Node.js 12 6 | // https://stackoverflow.com/a/59787575 7 | "target": "es2019", 8 | 9 | "module": "commonjs", 10 | "moduleResolution": "node", 11 | "jsx": "preserve", 12 | "esModuleInterop": false, 13 | "resolveJsonModule": true, 14 | "isolatedModules": true, 15 | 16 | "strict": true, 17 | "noUnusedLocals": true, 18 | "noUnusedParameters": true, 19 | "noImplicitReturns": true, 20 | "noFallthroughCasesInSwitch": true, 21 | "forceConsistentCasingInFileNames": true, 22 | 23 | // FIXME 24 | // [error TS2300: Duplicate identifier 'require'](https://github.com/tkrotoff/react-form-with-constraints/issues/12) 25 | // [@types/react-native definitions for `global` and `require` conflict with @types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/issues/16825) 26 | "skipLibCheck": true 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /examples/PasswordWithoutState/webpack.config.ts: -------------------------------------------------------------------------------- 1 | import * as path from 'node:path'; 2 | import { Configuration } from 'webpack'; 3 | 4 | const config: Configuration = { 5 | entry: './App.tsx', 6 | 7 | output: { 8 | path: path.resolve('build') 9 | }, 10 | 11 | resolve: { 12 | extensions: ['.js', '.ts', '.tsx'] 13 | }, 14 | 15 | module: { 16 | rules: [ 17 | { test: /\.tsx?$/, loader: 'babel-loader' }, 18 | { test: /\.css$/, use: ['style-loader', 'css-loader'] }, 19 | { test: /\.html$/, type: 'asset/resource', generator: { filename: '[name][ext]' } } 20 | ] 21 | } 22 | }; 23 | 24 | export default config; 25 | -------------------------------------------------------------------------------- /examples/PlainOldReact/App.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useRef, useState } from 'react'; 2 | import { createRoot } from 'react-dom/client'; 3 | 4 | import './index.html'; 5 | import './style.css'; 6 | 7 | // Inspired by 8 | // - [Validating a React form upon submit](https://goshakkk.name/submit-time-validation-react/) 9 | // - [How to do Simple Form Validation in #Reactjs](https://learnetto.com/blog/how-to-do-simple-form-validation-in-reactjs) 10 | 11 | interface Errors { 12 | email: string[]; 13 | password: string[]; 14 | passwordConfirm: string[]; 15 | } 16 | 17 | function validateEmail(email: string) { 18 | const errors = [] as string[]; 19 | if (email.length === 0) errors.push("Can't be empty"); 20 | if (!email.includes('@')) errors.push('Should contain @'); 21 | return errors; 22 | } 23 | 24 | function validatePassword(password: string) { 25 | const errors = [] as string[]; 26 | if (password.length === 0) errors.push("Can't be empty"); 27 | if (password.length < 5) errors.push('Should be at least 5 characters long'); 28 | return errors; 29 | } 30 | 31 | function validatePasswordConfirm(password: string, passwordConfirm: string) { 32 | const errors = [] as string[]; 33 | if (passwordConfirm !== password) errors.push('Not the same password'); 34 | return errors; 35 | } 36 | 37 | function hasErrors(errors: Errors) { 38 | return errors.email.length > 0 || errors.password.length > 0 || errors.passwordConfirm.length > 0; 39 | } 40 | 41 | function Form() { 42 | const email = useRef(null); 43 | const password = useRef(null); 44 | const passwordConfirm = useRef(null); 45 | 46 | const [errors, setErrors] = useState({ 47 | email: [], 48 | password: [], 49 | passwordConfirm: [] 50 | }); 51 | 52 | const [isSubmitted, setIsSubmitted] = useState(false); 53 | useEffect(() => { 54 | if (isSubmitted) { 55 | if (!hasErrors(errors)) { 56 | alert('Valid form'); 57 | } else { 58 | alert('Invalid form'); 59 | } 60 | setIsSubmitted(false); 61 | } 62 | }, [isSubmitted, errors]); 63 | 64 | function handleEmailChange({ target }: React.ChangeEvent) { 65 | const { value } = target; 66 | setErrors(prevState => { 67 | return { 68 | ...prevState, 69 | email: validateEmail(value) 70 | }; 71 | }); 72 | } 73 | 74 | function handlePasswordChange({ target }: React.ChangeEvent) { 75 | const { value } = target; 76 | setErrors(prevState => { 77 | return { 78 | ...prevState, 79 | password: validatePassword(value), 80 | passwordConfirm: validatePasswordConfirm(value, passwordConfirm.current!.value) 81 | }; 82 | }); 83 | } 84 | 85 | function handlePasswordConfirmChange({ target }: React.ChangeEvent) { 86 | const { value } = target; 87 | setErrors(prevState => { 88 | return { 89 | ...prevState, 90 | passwordConfirm: validatePasswordConfirm(password.current!.value, value) 91 | }; 92 | }); 93 | } 94 | 95 | function handleSubmit(e: React.FormEvent) { 96 | e.preventDefault(); 97 | 98 | setErrors(prevState => { 99 | return { 100 | ...prevState, 101 | email: validateEmail(email.current!.value), 102 | password: validatePassword(password.current!.value), 103 | passwordConfirm: validatePasswordConfirm( 104 | password.current!.value, 105 | passwordConfirm.current!.value 106 | ) 107 | }; 108 | }); 109 | setIsSubmitted(true); 110 | } 111 | 112 | return ( 113 | 114 |
115 | 116 | 117 |
118 | {errors.email.map(error => ( 119 |
{error}
120 | ))} 121 |
122 |
123 | 124 |
125 | 126 | 127 |
128 | {errors.password.map(error => ( 129 |
{error}
130 | ))} 131 |
132 |
133 | 134 |
135 | 136 | 142 |
143 | {errors.passwordConfirm.map(error => ( 144 |
{error}
145 | ))} 146 |
147 |
148 | 149 | 150 | 151 | ); 152 | } 153 | 154 | const root = createRoot(document.getElementById('app')!); 155 | root.render(
); 156 | -------------------------------------------------------------------------------- /examples/PlainOldReact/babel.config.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | 3 | module.exports = { 4 | presets: [['@babel/preset-react', { runtime: 'automatic' }], '@babel/preset-typescript'] 5 | }; 6 | -------------------------------------------------------------------------------- /examples/PlainOldReact/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Plain old React form validation example 8 | 9 | 10 | 11 |
12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /examples/PlainOldReact/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "plain-old-react-example", 3 | "version": "0.19.1", 4 | "private": true, 5 | "description": "Plain old React form validation example", 6 | "keywords": [ 7 | "react", 8 | "form", 9 | "validation" 10 | ], 11 | "main": "App.tsx", 12 | "scripts": { 13 | "clean": "rm -rf build", 14 | "tsc": "tsc", 15 | "start": "webpack serve --mode=development --host 0.0.0.0", 16 | "build": "webpack --mode=development", 17 | "build:watch": "webpack --mode=development --watch" 18 | }, 19 | "dependencies": { 20 | "react": "^18.2.0", 21 | "react-dom": "^18.2.0" 22 | }, 23 | "devDependencies": { 24 | "@babel/core": "^7.18.9", 25 | "@babel/preset-react": "^7.18.6", 26 | "@babel/preset-typescript": "^7.18.6", 27 | "@types/node": "^18.0.6", 28 | "@types/react-dom": "^18.0.6", 29 | "babel-loader": "^8.2.5", 30 | "css-loader": "^6.7.1", 31 | "style-loader": "^3.3.1", 32 | "ts-node": "^10.9.1", 33 | "typescript": "^4.7.4", 34 | "webpack": "^5.73.0", 35 | "webpack-cli": "^4.10.0", 36 | "webpack-dev-server": "^4.9.3" 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /examples/PlainOldReact/style.css: -------------------------------------------------------------------------------- 1 | body { 2 | background-color: lightgrey; 3 | } 4 | 5 | form > div { 6 | margin-bottom: 10px; 7 | } 8 | 9 | form > div > label { 10 | display: block; 11 | } 12 | 13 | div.error { 14 | color: red; 15 | } 16 | -------------------------------------------------------------------------------- /examples/PlainOldReact/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "noEmit": true, 4 | 5 | // Nullish coalescing operator (??) needs to be transpiled by TypeScript to support Node.js 12 6 | // https://stackoverflow.com/a/59787575 7 | "target": "es2019", 8 | 9 | "module": "commonjs", 10 | "moduleResolution": "node", 11 | "jsx": "preserve", 12 | "esModuleInterop": false, 13 | "resolveJsonModule": true, 14 | "isolatedModules": true, 15 | 16 | "strict": true, 17 | "noUnusedLocals": true, 18 | "noUnusedParameters": true, 19 | "noImplicitReturns": true, 20 | "noFallthroughCasesInSwitch": true, 21 | "forceConsistentCasingInFileNames": true, 22 | 23 | // FIXME 24 | // [error TS2300: Duplicate identifier 'require'](https://github.com/tkrotoff/react-form-with-constraints/issues/12) 25 | // [@types/react-native definitions for `global` and `require` conflict with @types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/issues/16825) 26 | "skipLibCheck": true 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /examples/PlainOldReact/webpack.config.ts: -------------------------------------------------------------------------------- 1 | import * as path from 'node:path'; 2 | import { Configuration } from 'webpack'; 3 | 4 | const config: Configuration = { 5 | entry: './App.tsx', 6 | 7 | output: { 8 | path: path.resolve('build') 9 | }, 10 | 11 | resolve: { 12 | extensions: ['.js', '.ts', '.tsx'] 13 | }, 14 | 15 | module: { 16 | rules: [ 17 | { test: /\.tsx?$/, loader: 'babel-loader' }, 18 | { test: /\.css$/, use: ['style-loader', 'css-loader'] }, 19 | { test: /\.html$/, type: 'asset/resource', generator: { filename: '[name][ext]' } } 20 | ] 21 | } 22 | }; 23 | 24 | export default config; 25 | -------------------------------------------------------------------------------- /examples/ReactNative/App.test.tsx: -------------------------------------------------------------------------------- 1 | import * as renderer from 'react-test-renderer'; 2 | 3 | import App from './App'; 4 | 5 | it('renders correctly', () => { 6 | renderer.create(); 7 | }); 8 | -------------------------------------------------------------------------------- /examples/ReactNative/app.json: -------------------------------------------------------------------------------- 1 | { 2 | "expo": { 3 | "name": "React Native example", 4 | "slug": "react-native-example", 5 | "ios": { 6 | "bundleIdentifier": "react-native-example" 7 | }, 8 | "android": { 9 | "package": "com.example" 10 | } 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /examples/ReactNative/babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = api => { 2 | api.cache(true); 3 | return { 4 | presets: ['babel-preset-expo'] 5 | }; 6 | }; 7 | -------------------------------------------------------------------------------- /examples/ReactNative/index.js: -------------------------------------------------------------------------------- 1 | import { registerRootComponent } from 'expo'; 2 | 3 | import App from './App'; 4 | 5 | // registerRootComponent calls AppRegistry.registerComponent('main', () => App); 6 | // It also ensures that whether you load the app in Expo Go or in a native build, 7 | // the environment is set up appropriately 8 | registerRootComponent(App); 9 | -------------------------------------------------------------------------------- /examples/ReactNative/jest.config.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | 3 | /** @type import('@jest/types').Config.InitialOptions */ 4 | const config = { 5 | preset: 'jest-expo', 6 | 7 | // https://metareal.blog/en/post/2021/01/16/setup-jest-for-expo-typescript-project/ 8 | // https://docs.expo.dev/guides/testing-with-jest/#jest-configuration 9 | transformIgnorePatterns: [ 10 | 'node_modules/(?!((jest-)?react-native|@react-native(-community)?)|expo(nent)?|@expo(nent)?/.*|@expo-google-fonts/.*|react-navigation|@react-navigation/.*|@unimodules/.*|unimodules|sentry-expo|native-base|react-native-svg|lodash-es)' 11 | ] 12 | }; 13 | 14 | module.exports = config; 15 | -------------------------------------------------------------------------------- /examples/ReactNative/metro.config.js: -------------------------------------------------------------------------------- 1 | // Learn more https://docs.expo.io/guides/customizing-metro 2 | const { getDefaultConfig } = require('expo/metro-config'); 3 | 4 | module.exports = getDefaultConfig(__dirname); 5 | -------------------------------------------------------------------------------- /examples/ReactNative/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-native-example", 3 | "version": "1.0.0", 4 | "private": true, 5 | "description": "react-form-with-constraints React Native example", 6 | "keywords": [ 7 | "react", 8 | "form", 9 | "validation", 10 | "react-form-with-constraints", 11 | "react-native" 12 | ], 13 | "main": "index.js", 14 | "scripts": { 15 | "clean:all": "rm -rf package-lock.json node_modules .expo ios android", 16 | "tsc": "tsc", 17 | "start": "expo start --dev-client", 18 | "android": "expo run:android", 19 | "ios": "expo run:ios", 20 | "test": "jest --verbose --detectOpenHandles" 21 | }, 22 | "dependencies": { 23 | "expo": "^45.0.6", 24 | "lodash-es": "^4.17.21", 25 | "react": "17.0.2", 26 | "react-form-with-constraints": "../../packages/react-form-with-constraints", 27 | "react-form-with-constraints-native": "../../packages/react-form-with-constraints-native", 28 | "react-form-with-constraints-tools": "../../packages/react-form-with-constraints-tools", 29 | "react-native": "0.68.2" 30 | }, 31 | "devDependencies": { 32 | "@babel/core": "^7.18.9", 33 | "@types/jest": "^28.1.6", 34 | "@types/lodash-es": "^4.17.6", 35 | "@types/react-native": "^0.69.3", 36 | "@types/react-test-renderer": "^18.0.0", 37 | "expo-cli": "^5.5.1", 38 | "jest": "^27.5.1", 39 | "jest-expo": "^45.0.1", 40 | "react-test-renderer": "17.0.2", 41 | "typescript": "^4.7.4" 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /examples/ReactNative/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "noEmit": true, 4 | 5 | "target": "es2015", 6 | 7 | "module": "commonjs", 8 | "moduleResolution": "node", 9 | "jsx": "react-native", 10 | "esModuleInterop": false, 11 | "resolveJsonModule": true, 12 | "isolatedModules": true, 13 | 14 | "strict": true, 15 | "noUnusedLocals": true, 16 | "noUnusedParameters": true, 17 | "noImplicitReturns": true, 18 | "noFallthroughCasesInSwitch": true, 19 | "forceConsistentCasingInFileNames": true, 20 | 21 | // FIXME 22 | // [error TS2300: Duplicate identifier 'require'](https://github.com/tkrotoff/react-form-with-constraints/issues/12) 23 | // [@types/react-native definitions for `global` and `require` conflict with @types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/issues/16825) 24 | "skipLibCheck": true 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /examples/ServerSideRendering/babel.config.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | 3 | module.exports = { 4 | presets: [['@babel/preset-react', { runtime: 'automatic' }], '@babel/preset-typescript'] 5 | }; 6 | -------------------------------------------------------------------------------- /examples/ServerSideRendering/browser.tsx: -------------------------------------------------------------------------------- 1 | import { hydrateRoot } from 'react-dom/client'; 2 | 3 | import { App } from './App'; 4 | 5 | hydrateRoot(document.getElementById('app')!, ); 6 | -------------------------------------------------------------------------------- /examples/ServerSideRendering/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "server-side-rendering-example", 3 | "version": "0.19.1", 4 | "private": true, 5 | "description": "react-form-with-constraints server-side rendering example", 6 | "keywords": [ 7 | "react", 8 | "form", 9 | "validation", 10 | "react-form-with-constraints", 11 | "server-side" 12 | ], 13 | "main": "App.tsx", 14 | "scripts": { 15 | "clean": "rm -rf build", 16 | "tsc": "tsc", 17 | "build": "webpack --mode=development", 18 | "start": "npm run clean && npm run build && cd build && node server" 19 | }, 20 | "dependencies": { 21 | "express": "^4.18.1", 22 | "react": "^18.2.0", 23 | "react-dom": "^18.2.0", 24 | "react-form-with-constraints": "^0.19.1", 25 | "react-form-with-constraints-tools": "^0.19.1" 26 | }, 27 | "devDependencies": { 28 | "@babel/core": "^7.18.9", 29 | "@babel/preset-react": "^7.18.6", 30 | "@babel/preset-typescript": "^7.18.6", 31 | "@types/express": "^4.17.13", 32 | "@types/node": "^18.0.6", 33 | "@types/react-dom": "^18.0.6", 34 | "babel-loader": "^8.2.5", 35 | "express": "^4.18.1", 36 | "ts-node": "^10.9.1", 37 | "typescript": "^4.7.4", 38 | "webpack": "^5.73.0", 39 | "webpack-cli": "^4.10.0" 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /examples/ServerSideRendering/server.tsx: -------------------------------------------------------------------------------- 1 | import * as express from 'express'; 2 | import { renderToString } from 'react-dom/server'; 3 | 4 | import { App } from './App'; 5 | import './style.css'; 6 | 7 | const server = express(); 8 | 9 | server.use(express.static('.')); 10 | 11 | server.get('/', (_req, res) => { 12 | res.send(` 13 | 14 | 15 | 16 | react-form-with-constraints server-side rendering example 17 | 18 | 19 | 20 |
${renderToString()}
21 | 22 | 23 | 24 | `); 25 | }); 26 | 27 | server.listen(8080); 28 | -------------------------------------------------------------------------------- /examples/ServerSideRendering/style.css: -------------------------------------------------------------------------------- 1 | /* 2 | Copy-pasted from Password/style.css 3 | Cannot use a symlink nor "import '../Password/style.css'" because of StackBlitz 4 | */ 5 | 6 | body { 7 | background-color: lightgrey; 8 | } 9 | 10 | form > div { 11 | margin-bottom: 10px; 12 | } 13 | 14 | form > div > label { 15 | display: block; 16 | } 17 | 18 | [data-feedback].error { 19 | color: red; 20 | } 21 | [data-feedback].warning { 22 | color: orange; 23 | } 24 | [data-feedback].info { 25 | color: blue; 26 | } 27 | [data-feedback].when-valid { 28 | color: green; 29 | } 30 | -------------------------------------------------------------------------------- /examples/ServerSideRendering/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "noEmit": true, 4 | 5 | // Nullish coalescing operator (??) needs to be transpiled by TypeScript to support Node.js 12 6 | // https://stackoverflow.com/a/59787575 7 | "target": "es2019", 8 | 9 | "module": "commonjs", 10 | "moduleResolution": "node", 11 | "jsx": "preserve", 12 | "esModuleInterop": false, 13 | "resolveJsonModule": true, 14 | "isolatedModules": true, 15 | 16 | "strict": true, 17 | "noUnusedLocals": true, 18 | "noUnusedParameters": true, 19 | "noImplicitReturns": true, 20 | "noFallthroughCasesInSwitch": true, 21 | "forceConsistentCasingInFileNames": true, 22 | 23 | // FIXME 24 | // [error TS2300: Duplicate identifier 'require'](https://github.com/tkrotoff/react-form-with-constraints/issues/12) 25 | // [@types/react-native definitions for `global` and `require` conflict with @types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/issues/16825) 26 | "skipLibCheck": true 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /examples/ServerSideRendering/webpack.config.ts: -------------------------------------------------------------------------------- 1 | import * as path from 'node:path'; 2 | import { Configuration } from 'webpack'; 3 | 4 | const output = { 5 | path: path.resolve('build') 6 | }; 7 | 8 | const extensions = ['.js', '.jsx', '.ts', '.tsx']; 9 | 10 | const babelLoaderRule = { test: /\.tsx?$/, loader: 'babel-loader' }; 11 | 12 | const config: Configuration[] = [ 13 | { 14 | entry: { server: './server.tsx' }, 15 | 16 | // [How can I use webpack with express?](https://stackoverflow.com/a/31655760/990356) 17 | target: 'node', 18 | 19 | output, 20 | resolve: { extensions }, 21 | module: { 22 | rules: [ 23 | babelLoaderRule, 24 | { test: /\.(html|css)$/, type: 'asset/resource', generator: { filename: '[name][ext]' } } 25 | ] 26 | } 27 | }, 28 | 29 | { 30 | entry: { browser: './browser.tsx' }, 31 | output, 32 | resolve: { extensions }, 33 | module: { 34 | rules: [babelLoaderRule] 35 | } 36 | } 37 | ]; 38 | 39 | export default config; 40 | -------------------------------------------------------------------------------- /examples/SignUp/Gender.ts: -------------------------------------------------------------------------------- 1 | /* 2 | Copy-pasted from WizardForm/Gender.ts 3 | Cannot use a symlink nor "import '../WizardForm/Gender'" because of StackBlitz 4 | */ 5 | 6 | export enum Gender { 7 | Male = 'male', 8 | Female = 'female' 9 | } 10 | -------------------------------------------------------------------------------- /examples/SignUp/Loader.tsx: -------------------------------------------------------------------------------- 1 | import './spinner.css'; 2 | 3 | // https://forums.meteor.com/t/any-recommendations-for-a-spinner-to-use-with-react-apps/22510/4 4 | // https://github.com/lukehaas/css-loaders 5 | export function Loader() { 6 | return
Loading...
; 7 | } 8 | -------------------------------------------------------------------------------- /examples/SignUp/babel.config.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | 3 | module.exports = { 4 | presets: [['@babel/preset-react', { runtime: 'automatic' }], '@babel/preset-typescript'], 5 | plugins: ['@babel/plugin-proposal-class-properties'] 6 | }; 7 | -------------------------------------------------------------------------------- /examples/SignUp/i18n.ts: -------------------------------------------------------------------------------- 1 | import i18n from 'i18next'; 2 | import LanguageDetector from 'i18next-browser-languagedetector'; 3 | import { initReactI18next } from 'react-i18next'; 4 | 5 | import * as fr from './locales/fr/translation.json'; 6 | 7 | export default i18n 8 | .use(LanguageDetector) 9 | .use(initReactI18next) 10 | .init({ 11 | debug: false, 12 | 13 | fallbackLng: 'en', 14 | 15 | detection: { 16 | caches: [] 17 | }, 18 | 19 | // Allow keys to be phrases having `:`, `.` 20 | nsSeparator: false, 21 | keySeparator: false, 22 | 23 | interpolation: { 24 | escapeValue: false // Not needed with React 25 | }, 26 | 27 | resources: { 28 | fr: { translation: fr } 29 | } 30 | }); 31 | -------------------------------------------------------------------------------- /examples/SignUp/i18next-parser.config.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | 3 | module.exports = { 4 | locales: ['fr'], 5 | 6 | // https://github.com/i18next/i18next-parser/issues/129 7 | namespaceSeparator: '__NS', 8 | keySeparator: '__KS' 9 | }; 10 | -------------------------------------------------------------------------------- /examples/SignUp/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | SignUp example 8 | 9 | 10 | 11 |
12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /examples/SignUp/locales/fr/translation.json: -------------------------------------------------------------------------------- 1 | { 2 | "Red": "Rouge", 3 | "Orange": "Orange", 4 | "Yellow": "Jaune", 5 | "Green": "Vert", 6 | "Blue": "Bleu", 7 | "Indigo": "Indigo", 8 | "Violet": "Violet", 9 | "Language:": "Langue :", 10 | "English": "Anglais", 11 | "French": "Français", 12 | "Note: each field is debounced with a 1s delay": "Note : chaque champ à un \"debounce\" de 1s", 13 | "First Name": "Prénom", 14 | "Too short": "Trop court", 15 | "Looks good!": "OK !", 16 | "Last Name": "Nom de famille", 17 | "Username <1>(already taken: john, paul, george, ringo)": "Nom d'utilisateur <1>(déjà pris : john, paul, george, ringo)", 18 | "Username available": "Nom d'utilisateur disponible", 19 | "Username already taken, choose another": "Nom d'utilisateur déjà pris, choisissez-en un autre", 20 | "Email": "Email", 21 | "Gender": "Genre", 22 | "Male": "Masculin", 23 | "Female": "Féminin", 24 | "Age": "Age", 25 | "Sorry, you must be at least 18 years old": "Désolé, vous devez avoir au moins 18 ans", 26 | "Hmm, you seem a bit young...": "Humm, vous êtes un peu jeune...", 27 | "Phone number": "Numéro de téléphone", 28 | "Invalid phone number, must be 10 digits": "Numéro de téléphone invalide, doit contenir 10 chiffres", 29 | "Favorite Color": "Couleur préférée", 30 | "Select a color...": "Selectionnez une couleur", 31 | "Employed": "Salarié", 32 | "Notes": "Notes", 33 | "Do you have a website?": "Avez-vous un site web ?", 34 | "Website": "Site web", 35 | "Password": "Mot de passe", 36 | "Should be at least 5 characters long": "Doit être au moins d'une longueur de 5 caractères", 37 | "Should contain numbers": "Doit contenir des chiffres", 38 | "Should contain small letters": "Doit contenir des lettres minuscules", 39 | "Should contain capital letters": "Doit contenir des lettres majuscules", 40 | "Should contain special characters": "Doit contenir des caractères spéciaux", 41 | "Confirm Password": "Confirmer le mot de passe", 42 | "Not the same password": "Pas le même mot de passe", 43 | "Sign Up": "S'inscrire", 44 | "Reset": "Réinitialiser" 45 | } 46 | -------------------------------------------------------------------------------- /examples/SignUp/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "sign-up-example", 3 | "version": "0.19.1", 4 | "private": true, 5 | "description": "react-form-with-constraints SignUp example", 6 | "keywords": [ 7 | "react", 8 | "form", 9 | "validation", 10 | "react-form-with-constraints" 11 | ], 12 | "main": "App.tsx", 13 | "scripts": { 14 | "clean": "rm -rf build", 15 | "tsc": "tsc", 16 | "start": "webpack serve --mode=development --host 0.0.0.0", 17 | "build": "webpack --mode=development", 18 | "build:watch": "webpack --mode=development --watch", 19 | "i18next": "i18next '**/*.{jsx,tsx}'" 20 | }, 21 | "dependencies": { 22 | "i18next": "^21.8.14", 23 | "i18next-browser-languagedetector": "^6.1.4", 24 | "lodash-es": "^4.17.21", 25 | "react": "^18.2.0", 26 | "react-dom": "^18.2.0", 27 | "react-form-with-constraints": "^0.19.1", 28 | "react-form-with-constraints-tools": "^0.19.1", 29 | "react-i18next": "^11.18.1" 30 | }, 31 | "devDependencies": { 32 | "@babel/core": "^7.18.9", 33 | "@babel/plugin-proposal-class-properties": "^7.18.6", 34 | "@babel/preset-react": "^7.18.6", 35 | "@babel/preset-typescript": "^7.18.6", 36 | "@types/lodash-es": "^4.17.6", 37 | "@types/node": "^18.0.6", 38 | "@types/react-dom": "^18.0.6", 39 | "babel-loader": "^8.2.5", 40 | "css-loader": "^6.7.1", 41 | "i18next-parser": "^6.5.0", 42 | "style-loader": "^3.3.1", 43 | "ts-node": "^10.9.1", 44 | "typescript": "^4.7.4", 45 | "webpack": "^5.73.0", 46 | "webpack-cli": "^4.10.0", 47 | "webpack-dev-server": "^4.9.3" 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /examples/SignUp/spinner.css: -------------------------------------------------------------------------------- 1 | /* Taken from https://github.com/lukehaas/css-loaders */ 2 | 3 | .loader, 4 | .loader:before, 5 | .loader:after { 6 | border-radius: 50%; 7 | width: 2.5em; 8 | height: 2.5em; 9 | -webkit-animation-fill-mode: both; 10 | animation-fill-mode: both; 11 | -webkit-animation: load7 1.8s infinite ease-in-out; 12 | animation: load7 1.8s infinite ease-in-out; 13 | } 14 | .loader { 15 | color: blue; /*color: #ffffff;*/ 16 | font-size: 2px; /*font-size: 10px;*/ 17 | margin-left: 9px; /*margin: 80px auto;*/ 18 | position: relative; 19 | text-indent: -9999em; 20 | -webkit-transform: translateZ(0); 21 | -ms-transform: translateZ(0); 22 | transform: translateZ(0); 23 | -webkit-animation-delay: -0.16s; 24 | animation-delay: -0.16s; 25 | } 26 | .loader:before, 27 | .loader:after { 28 | content: ''; 29 | position: absolute; 30 | top: 0; 31 | } 32 | .loader:before { 33 | left: -3.5em; 34 | -webkit-animation-delay: -0.32s; 35 | animation-delay: -0.32s; 36 | } 37 | .loader:after { 38 | left: 3.5em; 39 | } 40 | @-webkit-keyframes load7 { 41 | 0%, 42 | 80%, 43 | 100% { 44 | box-shadow: 0 2.5em 0 -1.3em; 45 | } 46 | 40% { 47 | box-shadow: 0 2.5em 0 0; 48 | } 49 | } 50 | @keyframes load7 { 51 | 0%, 52 | 80%, 53 | 100% { 54 | box-shadow: 0 2.5em 0 -1.3em; 55 | } 56 | 40% { 57 | box-shadow: 0 2.5em 0 0; 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /examples/SignUp/style.css: -------------------------------------------------------------------------------- 1 | /* 2 | Copy-pasted from Password/style.css 3 | Cannot use a symlink nor "import '../Password/style.css'" because of StackBlitz 4 | */ 5 | 6 | body { 7 | background-color: lightgrey; 8 | } 9 | 10 | form > div { 11 | margin-bottom: 10px; 12 | } 13 | 14 | form > div > label { 15 | display: block; 16 | } 17 | 18 | [data-feedback].error { 19 | color: red; 20 | } 21 | [data-feedback].warning { 22 | color: orange; 23 | } 24 | [data-feedback].info { 25 | color: blue; 26 | } 27 | [data-feedback].when-valid { 28 | color: green; 29 | } 30 | -------------------------------------------------------------------------------- /examples/SignUp/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "noEmit": true, 4 | 5 | // Nullish coalescing operator (??) needs to be transpiled by TypeScript to support Node.js 12 6 | // https://stackoverflow.com/a/59787575 7 | "target": "es2019", 8 | 9 | "module": "commonjs", 10 | "moduleResolution": "node", 11 | "jsx": "preserve", 12 | "esModuleInterop": false, 13 | "resolveJsonModule": true, 14 | "isolatedModules": true, 15 | 16 | "strict": true, 17 | "noUnusedLocals": true, 18 | "noUnusedParameters": true, 19 | "noImplicitReturns": true, 20 | "noFallthroughCasesInSwitch": true, 21 | "forceConsistentCasingInFileNames": true, 22 | 23 | // FIXME 24 | // [error TS2300: Duplicate identifier 'require'](https://github.com/tkrotoff/react-form-with-constraints/issues/12) 25 | // [@types/react-native definitions for `global` and `require` conflict with @types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/issues/16825) 26 | "skipLibCheck": true 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /examples/SignUp/webpack.config.ts: -------------------------------------------------------------------------------- 1 | import * as path from 'node:path'; 2 | import { Configuration } from 'webpack'; 3 | 4 | const config: Configuration = { 5 | entry: './App.tsx', 6 | 7 | output: { 8 | path: path.resolve('build') 9 | }, 10 | 11 | resolve: { 12 | extensions: ['.js', '.ts', '.tsx'] 13 | }, 14 | 15 | module: { 16 | rules: [ 17 | { test: /\.tsx?$/, loader: 'babel-loader' }, 18 | { test: /\.css$/, use: ['style-loader', 'css-loader'] }, 19 | { test: /\.html$/, type: 'asset/resource', generator: { filename: '[name][ext]' } } 20 | ] 21 | } 22 | }; 23 | 24 | export default config; 25 | -------------------------------------------------------------------------------- /examples/WizardForm/App.tsx: -------------------------------------------------------------------------------- 1 | import { createRoot } from 'react-dom/client'; 2 | 3 | import { WizardForm } from './WizardForm'; 4 | import './index.html'; 5 | import './style.css'; 6 | 7 | function App() { 8 | return ( 9 | <> 10 |

11 | Inspired by{' '} 12 | Redux Form - Wizard Form Example 13 |

14 | 15 | 16 | ); 17 | } 18 | 19 | const root = createRoot(document.getElementById('app')!); 20 | root.render(); 21 | -------------------------------------------------------------------------------- /examples/WizardForm/Color.ts: -------------------------------------------------------------------------------- 1 | export enum Color { 2 | Red = 'Red', 3 | Orange = 'Orange', 4 | Yellow = 'Yellow', 5 | Green = 'Green', 6 | Blue = 'Blue', 7 | Indigo = 'Indigo', 8 | Violet = 'Violet' 9 | } 10 | 11 | export const colorKeys = Object.keys(Color) as (keyof typeof Color)[]; 12 | -------------------------------------------------------------------------------- /examples/WizardForm/Gender.ts: -------------------------------------------------------------------------------- 1 | export enum Gender { 2 | Male = 'male', 3 | Female = 'female' 4 | } 5 | -------------------------------------------------------------------------------- /examples/WizardForm/WizardForm.tsx: -------------------------------------------------------------------------------- 1 | import { Component } from 'react'; 2 | 3 | import { Color } from './Color'; 4 | import { WizardFormStep1 } from './WizardFormStep1'; 5 | import { WizardFormStep2 } from './WizardFormStep2'; 6 | import { WizardFormStep3 } from './WizardFormStep3'; 7 | 8 | interface Props {} 9 | 10 | interface State { 11 | step: number; 12 | 13 | firstName: string; 14 | lastName: string; 15 | email: string; 16 | favoriteColor: '' | Color; 17 | } 18 | 19 | export class WizardForm extends Component { 20 | state: State = { 21 | step: 1, 22 | 23 | firstName: '', 24 | lastName: '', 25 | email: '', 26 | favoriteColor: '' 27 | }; 28 | 29 | handleChange = (target: HTMLInputElement | HTMLSelectElement | HTMLTextAreaElement) => { 30 | const value = target.type === 'checkbox' ? (target as HTMLInputElement).checked : target.value; 31 | 32 | // FIXME [Computed property key names should not be widened](https://github.com/Microsoft/TypeScript/issues/13948) 33 | // @ts-ignore 34 | this.setState({ 35 | [target.name as keyof State]: value 36 | }); 37 | }; 38 | 39 | handleSubmit = () => { 40 | alert(`Form submitted\n\nthis.state =\n${JSON.stringify(this.state, null, 2)}`); 41 | }; 42 | 43 | nextStep = () => { 44 | this.setState(prevState => ({ step: prevState.step + 1 })); 45 | }; 46 | 47 | previousStep = () => { 48 | this.setState(prevState => ({ step: prevState.step - 1 })); 49 | }; 50 | 51 | render() { 52 | const { step } = this.state; 53 | 54 | return ( 55 | <> 56 | {step === 1 && ( 57 | 58 | )} 59 | {step === 2 && ( 60 | 66 | )} 67 | {step === 3 && ( 68 | 74 | )} 75 | 76 |
77 |
this.state = {JSON.stringify(this.state, null, 2)}
78 |
79 | 80 | ); 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /examples/WizardForm/WizardFormStep1.tsx: -------------------------------------------------------------------------------- 1 | import { useRef } from 'react'; 2 | import { FieldFeedback, FieldFeedbacks, FormWithConstraints } from 'react-form-with-constraints'; 3 | 4 | interface Props { 5 | firstName: string; 6 | lastName: string; 7 | onChange: (input: HTMLInputElement) => void; 8 | nextPage: () => void; 9 | } 10 | 11 | export function WizardFormStep1({ firstName, lastName, onChange, nextPage }: Props) { 12 | const form = useRef(null); 13 | 14 | async function handleChange({ target }: React.ChangeEvent) { 15 | onChange(target); 16 | 17 | await form.current!.validateFields(target); 18 | } 19 | 20 | async function handleSubmit(e: React.FormEvent) { 21 | e.preventDefault(); 22 | 23 | await form.current!.validateForm(); 24 | if (form.current!.isValid()) { 25 | nextPage(); 26 | } 27 | } 28 | 29 | return ( 30 | 31 |
32 | 33 | 41 | 42 | Too short 43 | 44 | 45 |
46 | 47 |
48 | 49 | 57 | 58 | Too short 59 | 60 | 61 |
62 | 63 | 64 |
65 | ); 66 | } 67 | -------------------------------------------------------------------------------- /examples/WizardForm/WizardFormStep2.tsx: -------------------------------------------------------------------------------- 1 | import { useRef } from 'react'; 2 | import { FieldFeedback, FieldFeedbacks, FormWithConstraints } from 'react-form-with-constraints'; 3 | 4 | import { Gender } from './Gender'; 5 | 6 | interface Props { 7 | email: string; 8 | gender?: Gender; 9 | previousPage: () => void; 10 | onChange: (input: HTMLInputElement) => void; 11 | nextPage: () => void; 12 | } 13 | 14 | export function WizardFormStep2({ email, gender, previousPage, onChange, nextPage }: Props) { 15 | const form = useRef(null); 16 | 17 | async function handleChange({ target }: React.ChangeEvent) { 18 | onChange(target); 19 | 20 | await form.current!.validateFields(target); 21 | } 22 | 23 | async function handleSubmit(e: React.FormEvent) { 24 | e.preventDefault(); 25 | 26 | await form.current!.validateForm(); 27 | if (form.current!.isValid()) { 28 | nextPage(); 29 | } 30 | } 31 | 32 | return ( 33 | 34 |
35 | 36 | 44 | 45 | 46 | 47 |
48 |
49 | 50 | 61 | 71 | 72 | 73 | 74 |
75 | 78 |   79 | 80 |
81 | ); 82 | } 83 | -------------------------------------------------------------------------------- /examples/WizardForm/WizardFormStep3.tsx: -------------------------------------------------------------------------------- 1 | import { useRef } from 'react'; 2 | import { FieldFeedback, FieldFeedbacks, FormWithConstraints } from 'react-form-with-constraints'; 3 | 4 | import { Color, colorKeys } from './Color'; 5 | 6 | interface Props { 7 | favoriteColor: '' | Color; 8 | employed?: boolean; 9 | notes?: string; 10 | previousPage: () => void; 11 | onChange: (input: HTMLInputElement | HTMLSelectElement | HTMLTextAreaElement) => void; 12 | onSubmit: () => void; 13 | } 14 | 15 | export function WizardFormStep3({ 16 | favoriteColor, 17 | employed, 18 | notes, 19 | previousPage, 20 | onChange, 21 | onSubmit 22 | }: Props) { 23 | const form = useRef(null); 24 | 25 | async function handleChange({ 26 | target 27 | }: React.ChangeEvent) { 28 | onChange(target); 29 | 30 | await form.current!.validateFields(target); 31 | } 32 | 33 | async function handleSubmit(e: React.FormEvent) { 34 | e.preventDefault(); 35 | 36 | await form.current!.validateForm(); 37 | if (form.current!.isValid()) { 38 | onSubmit(); 39 | } 40 | } 41 | 42 | return ( 43 | 44 |
45 | 59 | 60 | 61 | 62 |
63 |
64 | 73 |
74 |
75 | 76 |