├── .actrc ├── .gitattributes ├── .github └── workflows │ ├── build.yml │ ├── codeql.yml │ ├── npm-publish.yml │ └── publish-examples.yml ├── .gitignore ├── .prettierrc ├── CONTRIBUTING.md ├── LICENSE.txt ├── README.md ├── docs └── index.html ├── images ├── custom-field.png ├── grid-layout.png ├── mui-simple.png └── simple-form.png ├── packages ├── examples-antd │ ├── .gitignore │ ├── index.html │ ├── package.json │ ├── public │ │ └── vite.svg │ ├── src │ │ ├── App.css │ │ ├── App.tsx │ │ ├── CodeViewer.tsx │ │ ├── Sider.tsx │ │ ├── assets │ │ │ └── react.svg │ │ ├── examples │ │ │ ├── AsyncDataSource.tsx │ │ │ ├── Basic.tsx │ │ │ ├── ComplexLayout.tsx │ │ │ ├── Coordinated.tsx │ │ │ ├── CustomComponent.tsx │ │ │ ├── DynamicFields.tsx │ │ │ ├── FieldCondition.tsx │ │ │ ├── FormInModal.tsx │ │ │ ├── FormList.tsx │ │ │ ├── FormListManual.tsx │ │ │ ├── Mixed.tsx │ │ │ ├── MultipleColumns.tsx │ │ │ ├── MultipleSections.tsx │ │ │ ├── Simple.tsx │ │ │ ├── SingleField.tsx │ │ │ ├── Validation.tsx │ │ │ ├── ViewEdit.tsx │ │ │ ├── ViewMode.tsx │ │ │ └── Wizard.tsx │ │ ├── index.css │ │ ├── main.tsx │ │ ├── useHash.ts │ │ └── vite-env.d.ts │ ├── tsconfig.json │ ├── tsconfig.node.json │ └── vite.config.ts ├── examples-formik │ ├── .eslintrc.cjs │ ├── .gitignore │ ├── README.md │ ├── index.html │ ├── package.json │ ├── public │ │ └── vite.svg │ ├── src │ │ ├── App.css │ │ ├── App.tsx │ │ ├── CodeViewer.tsx │ │ ├── assets │ │ │ └── react.svg │ │ ├── examples │ │ │ ├── AsyncDataSource.tsx │ │ │ ├── Basic.tsx │ │ │ ├── ComplexLayout.tsx │ │ │ ├── Coordinated.tsx │ │ │ ├── CustomComponent.tsx │ │ │ ├── DynamicFields.tsx │ │ │ ├── FieldCondition.tsx │ │ │ ├── FormInModal.tsx │ │ │ ├── FormList.tsx │ │ │ ├── Mixed.tsx │ │ │ ├── MultipleColumns.tsx │ │ │ ├── MultipleSections.tsx │ │ │ ├── Simple.tsx │ │ │ ├── Validation.tsx │ │ │ ├── ViewEdit.tsx │ │ │ ├── ViewMode.tsx │ │ │ └── Wizard.tsx │ │ ├── index.css │ │ ├── main.tsx │ │ ├── useHash.ts │ │ └── vite-env.d.ts │ ├── tsconfig.json │ ├── tsconfig.node.json │ └── vite.config.ts └── nice-form-react │ ├── .prettierrc │ ├── README.md │ ├── babel.config.js │ ├── jest.config.js │ ├── package.json │ ├── public │ └── vite.svg │ ├── src │ ├── FormField.tsx │ ├── FormLayout.tsx │ ├── NiceForm.tsx │ ├── adapters │ │ ├── antdAdapter.tsx │ │ ├── formikAdapter.tsx │ │ └── formikMuiAdapter.tsx │ ├── config.tsx │ ├── index.ts │ ├── types.ts │ └── utils.ts │ ├── tests │ ├── __mocks__ │ │ ├── fileMock.js │ │ ├── matchMedia.js │ │ └── styleMock.js │ ├── antd │ │ ├── AsyncDataSource.test.tsx │ │ ├── Basic.test.tsx │ │ ├── ComplexLayout.test.tsx │ │ ├── Coordinated.test.tsx │ │ ├── CustomComponent.test.tsx │ │ ├── DynamicFields.test.tsx │ │ ├── FieldCondition.test.tsx │ │ ├── FormInModal.test.tsx │ │ ├── FormList.test.tsx │ │ ├── FormListManual.test.tsx │ │ ├── Mixed.test.tsx │ │ ├── MultipleColumns.test.tsx │ │ ├── Simple.test.tsx │ │ ├── SingleField.test.tsx │ │ ├── Validation.test.tsx │ │ ├── ViewEdit.test.tsx │ │ ├── ViewMode.test.tsx │ │ └── Wizard.test.tsx │ ├── formik │ │ ├── AsyncDataSource.test.tsx │ │ ├── Basic.test.tsx │ │ ├── ComplexLayout.test.tsx │ │ ├── Coordinated.test.tsx │ │ ├── CustomComponent.test.tsx │ │ ├── DynamicFields.test.tsx │ │ ├── FieldCondition.test.tsx │ │ ├── FormInModal.test.tsx │ │ ├── FormList.test.tsx │ │ ├── Mixed.test.tsx │ │ ├── MultipleColumns.test.tsx │ │ ├── MultipleSections.test.tsx │ │ ├── Simple.test.tsx │ │ ├── Validation.test.tsx │ │ ├── ViewEdit.test.tsx │ │ ├── ViewMode.test.tsx │ │ └── Wizard.test.tsx │ └── setupAfterEnv.ts │ ├── tsconfig.json │ ├── tsconfig.node.json │ └── typedoc.json ├── pnpm-lock.yaml └── pnpm-workspace.yaml /.actrc: -------------------------------------------------------------------------------- 1 | --container-architecture=linux/amd64 2 | --pull=false -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | * linguist-vendored 2 | packages/nice-form-react/src/** linguist-vendored=false -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: build 2 | 3 | # Controls when the workflow will run 4 | on: 5 | push: 6 | branches: 7 | - main 8 | paths: 9 | - 'packages/nice-form-react/src/**' 10 | - '.github/workflows/build.yml' 11 | 12 | # Allows you to run this workflow manually from the Actions tab 13 | workflow_dispatch: 14 | 15 | jobs: 16 | build: 17 | runs-on: ubuntu-latest 18 | 19 | steps: 20 | # Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it 21 | - uses: actions/checkout@v4 22 | - uses: actions/setup-node@v4 23 | with: 24 | node-version: 18.13.0 25 | 26 | - uses: pnpm/action-setup@v3 27 | with: 28 | version: 8 29 | 30 | # Install dependencies 31 | - name: Install dependencies 32 | run: pnpm i 33 | 34 | - name: Unit tests 35 | run: | 36 | cd packages/nice-form-react 37 | pnpm test 38 | 39 | - name: Coveralls 40 | uses: coverallsapp/github-action@v2 41 | 42 | - name: Build 43 | run: | 44 | cd packages/nice-form-react 45 | pnpm build 46 | -------------------------------------------------------------------------------- /.github/workflows/codeql.yml: -------------------------------------------------------------------------------- 1 | # For most projects, this workflow file will not need changing; you simply need 2 | # to commit it to your repository. 3 | # 4 | # You may wish to alter this file to override the set of languages analyzed, 5 | # or to provide custom queries or build logic. 6 | # 7 | # ******** NOTE ******** 8 | # We have attempted to detect the languages in your repository. Please check 9 | # the `language` matrix defined below to confirm you have the correct set of 10 | # supported CodeQL languages. 11 | # 12 | name: "CodeQL" 13 | 14 | on: 15 | push: 16 | branches: [ "main" ] 17 | pull_request: 18 | branches: [ "main" ] 19 | schedule: 20 | - cron: '39 16 * * 0' 21 | 22 | jobs: 23 | analyze: 24 | name: Analyze 25 | # Runner size impacts CodeQL analysis time. To learn more, please see: 26 | # - https://gh.io/recommended-hardware-resources-for-running-codeql 27 | # - https://gh.io/supported-runners-and-hardware-resources 28 | # - https://gh.io/using-larger-runners 29 | # Consider using larger runners for possible analysis time improvements. 30 | runs-on: ${{ (matrix.language == 'swift' && 'macos-latest') || 'ubuntu-latest' }} 31 | timeout-minutes: ${{ (matrix.language == 'swift' && 120) || 360 }} 32 | permissions: 33 | # required for all workflows 34 | security-events: write 35 | 36 | # only required for workflows in private repositories 37 | actions: read 38 | contents: read 39 | 40 | strategy: 41 | fail-fast: false 42 | matrix: 43 | language: [ 'javascript-typescript' ] 44 | # CodeQL supports [ 'c-cpp', 'csharp', 'go', 'java-kotlin', 'javascript-typescript', 'python', 'ruby', 'swift' ] 45 | # Use only 'java-kotlin' to analyze code written in Java, Kotlin or both 46 | # Use only 'javascript-typescript' to analyze code written in JavaScript, TypeScript or both 47 | # Learn more about CodeQL language support at https://aka.ms/codeql-docs/language-support 48 | 49 | steps: 50 | - name: Checkout repository 51 | uses: actions/checkout@v4 52 | 53 | # Initializes the CodeQL tools for scanning. 54 | - name: Initialize CodeQL 55 | uses: github/codeql-action/init@v3 56 | with: 57 | languages: ${{ matrix.language }} 58 | # If you wish to specify custom queries, you can do so here or in a config file. 59 | # By default, queries listed here will override any specified in a config file. 60 | # Prefix the list here with "+" to use these queries and those in the config file. 61 | 62 | # For more details on CodeQL's query packs, refer to: https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs 63 | # queries: security-extended,security-and-quality 64 | 65 | 66 | # Autobuild attempts to build any compiled languages (C/C++, C#, Go, Java, or Swift). 67 | # If this step fails, then you should remove it and run the build manually (see below) 68 | - name: Autobuild 69 | uses: github/codeql-action/autobuild@v3 70 | 71 | # ℹ️ Command-line programs to run using the OS shell. 72 | # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun 73 | 74 | # If the Autobuild fails above, remove it and uncomment the following three lines. 75 | # modify them (or add more) to build your code if your project, please refer to the EXAMPLE below for guidance. 76 | 77 | # - run: | 78 | # echo "Run, Build Application using script" 79 | # ./location_of_script_within_repo/buildscript.sh 80 | 81 | - name: Perform CodeQL Analysis 82 | uses: github/codeql-action/analyze@v3 83 | with: 84 | category: "/language:${{matrix.language}}" 85 | -------------------------------------------------------------------------------- /.github/workflows/npm-publish.yml: -------------------------------------------------------------------------------- 1 | name: npm-publish 2 | 3 | # Controls when the workflow will run 4 | on: 5 | release: 6 | types: [published] 7 | 8 | # Allows you to run this workflow manually from the Actions tab 9 | workflow_dispatch: 10 | 11 | jobs: 12 | build: 13 | runs-on: ubuntu-latest 14 | 15 | steps: 16 | # Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it 17 | - uses: actions/checkout@v4 18 | - uses: actions/setup-node@v4 19 | with: 20 | node-version: 18.13.0 21 | registry-url: 'https://registry.npmjs.org' 22 | 23 | - uses: pnpm/action-setup@v3 24 | with: 25 | version: 8 26 | 27 | # Install dependencies 28 | - name: Install dependencies 29 | run: pnpm i 30 | 31 | - name: Unit tests 32 | run: | 33 | cd packages/nice-form-react 34 | pnpm test 35 | 36 | - name: Build 37 | run: | 38 | cd packages/nice-form-react 39 | pnpm build 40 | 41 | - name: Publish 42 | env: 43 | NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} 44 | run: | 45 | cd packages/nice-form-react 46 | npm publish --access public 47 | -------------------------------------------------------------------------------- /.github/workflows/publish-examples.yml: -------------------------------------------------------------------------------- 1 | # This is a basic workflow to help you get started with Actions 2 | 3 | name: publish-examples 4 | 5 | # Controls when the workflow will run 6 | on: 7 | # Triggers the workflow on push or pull request events but only for the "main" branch 8 | push: 9 | branches: 10 | - main 11 | paths: 12 | - 'packages/examples-**' 13 | - '.github/workflows/publish-examples.yaml' 14 | 15 | # Allows you to run this workflow manually from the Actions tab 16 | workflow_dispatch: 17 | 18 | permissions: 19 | contents: write 20 | # A workflow run is made up of one or more jobs that can run sequentially or in parallel 21 | jobs: 22 | # This workflow contains a single job called "build" 23 | build: 24 | # The type of runner that the job will run on 25 | runs-on: ubuntu-latest 26 | 27 | # Steps represent a sequence of tasks that will be executed as part of the job 28 | steps: 29 | # Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it 30 | - uses: actions/checkout@v4 31 | - uses: actions/setup-node@v4 32 | with: 33 | node-version: 18.13.0 34 | 35 | - uses: pnpm/action-setup@v3 36 | with: 37 | version: 8 38 | 39 | # Install dependencies 40 | - name: Install dependencies 41 | run: pnpm i 42 | 43 | # Build API docs and examples 44 | - name: Build examples 45 | run: | 46 | mkdir gh-pages 47 | cd packages/nice-form-react 48 | pnpm build 49 | pnpm gendoc 50 | cp -r ./docs ../../gh-pages/docs 51 | cd ../../ 52 | cd packages/examples-antd 53 | pnpm build 54 | cd ../../packages/examples-formik 55 | pnpm build 56 | cd ../../ 57 | cp ./docs/index.html ./gh-pages/index.html 58 | 59 | - name: Deploy to gh-pages 60 | uses: peaceiris/actions-gh-pages@v3 61 | with: 62 | github_token: ${{ secrets.GITHUB_TOKEN }} 63 | publish_dir: ./gh-pages 64 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | gh-pages 11 | coverage 12 | node_modules 13 | dist 14 | dist-ssr 15 | *.local 16 | /packages/nice-form-react/lib 17 | 18 | # Editor directories and files 19 | .vscode/* 20 | !.vscode/extensions.json 21 | .idea 22 | .DS_Store 23 | *.suo 24 | *.ntvs* 25 | *.njsproj 26 | *.sln 27 | *.sw? 28 | ~ 29 | 30 | packages/nice-form-react/docs -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "trailingComma": "all", 4 | "arrowParens": "always", 5 | "printWidth": 100 6 | } 7 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing Guidelines 2 | Thanks for your interests in contributing to this project. Please read through this document before your contribution. 3 | 4 | ## Discussing 5 | You are encouraged to use the Github issues for any discussion related activities. Below are the included items but not 6 | limited to these: 7 | 8 | 1. Reporting bugs or issues 9 | 2. Proposing new features or improvements 10 | 3. Q & A 11 | 12 | ## Contributing 13 | Any changes to the repository are only accepted via [Pull Request](https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/proposing-changes-to-your-work-with-pull-requests/about-pull-requests): 14 | 15 | Steps to contribute via the pull request: 16 | 1. [Fork the repository](https://help.github.com/articles/fork-a-repo/) 17 | 2. Make the changes with the helpful information in [README](README.md) to the forked repository 18 | 3. Refer to [Dev Guide](README.md#dev-guide) about how to develop 19 | 4. Commit and push the changes to your remote forked repository 20 | 5. Send the pull request to the official repository and describe the details of the changes 21 | 6. Wait for the committer to merge the changes into **main** branch and take care of the following CI build in 22 | case of any failures 23 | 24 | ## License 25 | The project is under [MIT](https://opensource.org/license/mit/). 26 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | The MIT License 2 | 3 | Copyright (c) 2023 The eBay Platform Team 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 13 | all 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 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /docs/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Nice Form - meta based form rendering 8 | 9 | 10 | redirecting to examples... 11 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /images/custom-field.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eBay/nice-form-react/0a9498adacfa2b8c1daed308f4d401b7c3f17f5e/images/custom-field.png -------------------------------------------------------------------------------- /images/grid-layout.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eBay/nice-form-react/0a9498adacfa2b8c1daed308f4d401b7c3f17f5e/images/grid-layout.png -------------------------------------------------------------------------------- /images/mui-simple.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eBay/nice-form-react/0a9498adacfa2b8c1daed308f4d401b7c3f17f5e/images/mui-simple.png -------------------------------------------------------------------------------- /images/simple-form.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eBay/nice-form-react/0a9498adacfa2b8c1daed308f4d401b7c3f17f5e/images/simple-form.png -------------------------------------------------------------------------------- /packages/examples-antd/.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | dist 12 | dist-ssr 13 | *.local 14 | 15 | # Editor directories and files 16 | .vscode/* 17 | !.vscode/extensions.json 18 | .idea 19 | .DS_Store 20 | *.suo 21 | *.ntvs* 22 | *.njsproj 23 | *.sln 24 | *.sw? 25 | -------------------------------------------------------------------------------- /packages/examples-antd/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Nice Form Exmaples - Ant.Design 8 | 9 | 10 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /packages/examples-antd/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "example", 3 | "private": true, 4 | "version": "0.0.0", 5 | "type": "module", 6 | "scripts": { 7 | "dev": "vite", 8 | "build:tsc": "tsc && vite build", 9 | "build": "vite build --emptyOutDir", 10 | "preview": "vite preview" 11 | }, 12 | "dependencies": { 13 | "@ant-design/icons": "^5.2.6", 14 | "@ebay/nice-form-react": "^2.0.0", 15 | "antd": "^5.1.1", 16 | "dayjs": "^1.11.7", 17 | "react": "^18.2.0", 18 | "react-dom": "^18.2.0" 19 | }, 20 | "devDependencies": { 21 | "@types/prismjs": "^1.26.0", 22 | "@types/react": "^18.0.26", 23 | "@types/react-dom": "^18.0.9", 24 | "@vitejs/plugin-react": "^3.0.0", 25 | "prismjs": "1.29.0", 26 | "typescript": "^4.9.3", 27 | "vite": "^4.0.0" 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /packages/examples-antd/public/vite.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /packages/examples-antd/src/App.css: -------------------------------------------------------------------------------- 1 | .app { 2 | display: grid; 3 | grid-template-columns: 260px 1fr; 4 | min-height: 100vh; 5 | } 6 | 7 | .app .sider { 8 | background-color: #f7f7f7; 9 | font-size: 14px; 10 | } 11 | 12 | .app .sider h1 { 13 | position: relative; 14 | background-color: #f0f0f0; 15 | padding: 10px 0 10px 28px; 16 | } 17 | .app .sider h1 .header-name { 18 | display: block; 19 | font-size: 12px; 20 | color: #999; 21 | } 22 | 23 | .app .sider img.logo { 24 | position: absolute; 25 | right: 10px; 26 | top: 10px; 27 | width: 32px; 28 | } 29 | 30 | .app .sider h1 span.example-title { 31 | font-size: 20px; 32 | display: block; 33 | margin-top: 5px; 34 | color: #333; 35 | display: inline-block; 36 | } 37 | .app .sider h1 .ant-select { 38 | margin-top: 5px; 39 | color: #666; 40 | margin-left: 10px; 41 | } 42 | 43 | .app .sider ul, 44 | .app .sider li { 45 | margin: 0; 46 | padding: 0; 47 | list-style: none; 48 | } 49 | 50 | .app .sider li { 51 | line-height: 260%; 52 | } 53 | 54 | .app .sider li a { 55 | display: block; 56 | padding-left: 28px; 57 | } 58 | 59 | .app .sider li a.active { 60 | color: #f90; 61 | } 62 | 63 | .app .sider .social { 64 | padding-left: 28px; 65 | margin-top: 20px; 66 | line-height: 40px; 67 | } 68 | 69 | .app .lib-switch { 70 | font-size: 14px; 71 | display: block; 72 | padding: 5px; 73 | width: 90%; 74 | border: 1px solid #ccc; 75 | margin-bottom: 10px; 76 | background-color: transparent; 77 | } 78 | 79 | .app .example-container { 80 | padding: 40px; 81 | height: 100vh; 82 | overflow-x: auto; 83 | } 84 | 85 | .app .example-container h1 { 86 | border-bottom: 1px solid #eee; 87 | padding-bottom: 15px; 88 | margin-bottom: 15px; 89 | } 90 | 91 | .app .example-container .ant-form { 92 | width: 600px; 93 | } 94 | 95 | .example-description { 96 | color: #777; 97 | font-size: 14px; 98 | margin: 10px 0 0 0; 99 | line-height: 150%; 100 | } 101 | 102 | .code-container { 103 | margin-top: 40px; 104 | grid-column: 2 / span 1; 105 | } 106 | 107 | .nice-form-react-item-view-mode .ant-form-item-label label { 108 | font-weight: bold; 109 | } 110 | -------------------------------------------------------------------------------- /packages/examples-antd/src/CodeViewer.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect } from 'react'; 2 | import Prism from 'prismjs'; 3 | import 'prismjs/themes/prism-tomorrow.css'; 4 | import 'prismjs/plugins/line-numbers/prism-line-numbers.js'; 5 | import 'prismjs/plugins/line-numbers/prism-line-numbers.css'; 6 | import 'prismjs/plugins/toolbar/prism-toolbar.css'; 7 | import 'prismjs/plugins/toolbar/prism-toolbar.js'; 8 | import 'prismjs/plugins/copy-to-clipboard/prism-copy-to-clipboard.js'; 9 | import codeBasic from './examples/Basic.tsx?raw'; 10 | import codeDynamicFields from './examples/DynamicFields.js?raw'; 11 | import codeFieldCondition from './examples/FieldCondition.js?raw'; 12 | import codeAsyncDataSource from './examples/AsyncDataSource.js?raw'; 13 | import codeMultipleColumns from './examples/MultipleColumns.js?raw'; 14 | import codeComplexLayout from './examples/ComplexLayout.js?raw'; 15 | import codeMultipleSections from './examples/MultipleSections.js?raw'; 16 | import codeSingleField from './examples/SingleField.js?raw'; 17 | import codeValidation from './examples/Validation.js?raw'; 18 | import codeCoordinated from './examples/Coordinated.js?raw'; 19 | import codeFormInModal from './examples/FormInModal.js?raw'; 20 | import codeCustomComponent from './examples/CustomComponent.js?raw'; 21 | import codeViewEdit from './examples/ViewEdit.js?raw'; 22 | import codeMixed from './examples/Mixed.js?raw'; 23 | import codeWizard from './examples/Wizard.js?raw'; 24 | import codeSimple from './examples/Simple.js?raw'; 25 | import codeViewMode from './examples/ViewMode.js?raw'; 26 | import codeFormList from './examples/FormList.js?raw'; 27 | import codeFormListManual from './examples/FormListManual.tsx?raw'; 28 | 29 | type CodeMap = { 30 | [key: string]: string; 31 | }; 32 | 33 | const codeMap: CodeMap = { 34 | basic: codeBasic, 35 | 'view-edit': codeViewEdit, 36 | 'dynamic-fields': codeDynamicFields, 37 | 'field-condition': codeFieldCondition, 38 | 'async-data-source': codeAsyncDataSource, 39 | 'multiple-columns': codeMultipleColumns, 40 | 'complex-layout': codeComplexLayout, 41 | 'multiple-sections': codeMultipleSections, 42 | 'single-field': codeSingleField, 43 | validation: codeValidation, 44 | coordinated: codeCoordinated, 45 | 'form-in-modal': codeFormInModal, 46 | 'custom-component': codeCustomComponent, 47 | mixed: codeMixed, 48 | wizard: codeWizard, 49 | simple: codeSimple, 50 | 'view-mode': codeViewMode, 51 | 'form-list': codeFormList, 52 | 'form-list-manual': codeFormListManual, 53 | }; 54 | 55 | interface CodeViewerProps { 56 | code: string; 57 | } 58 | 59 | const CodeViewer: React.FC = ({ code }) => { 60 | useEffect(() => { 61 | Prism.highlightAll(); 62 | }, [code]); 63 | return ( 64 |
65 |       
66 |         {codeMap[code] || `// Error: code of "${code}" not found`}
67 |       
68 |     
69 | ); 70 | }; 71 | 72 | export default CodeViewer; 73 | -------------------------------------------------------------------------------- /packages/examples-antd/src/Sider.tsx: -------------------------------------------------------------------------------- 1 | function Sider() {} 2 | export default Sider; 3 | -------------------------------------------------------------------------------- /packages/examples-antd/src/assets/react.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /packages/examples-antd/src/examples/AsyncDataSource.tsx: -------------------------------------------------------------------------------- 1 | import { useCallback, useState, useEffect } from 'react'; 2 | import { Form, Button } from 'antd'; 3 | import NiceForm from '@ebay/nice-form-react'; 4 | import type { AntdNiceFormMeta } from '@ebay/nice-form-react/lib/esm/adapters/antdAdapter'; 5 | 6 | const MOCK_DATA: { 7 | [key: string]: string[]; 8 | } = { 9 | China: ['Beijing', 'Shanghai', 'Nanjing'], 10 | USA: ['New York', 'San Jose', 'Washton'], 11 | France: ['Paris', 'Marseille', 'Cannes'], 12 | }; 13 | 14 | // Mock fetch 15 | const fetchCities = (country: string): Promise => { 16 | return new Promise((resolve, reject) => { 17 | setTimeout(() => { 18 | if (MOCK_DATA[country]) resolve(MOCK_DATA[country]); 19 | else reject(new Error('Not found')); 20 | }, 1500); 21 | }); 22 | }; 23 | 24 | export default () => { 25 | const [form] = Form.useForm(); 26 | const [cities, setCities] = useState<{ [key: string]: string }>({}); 27 | const updateOnChange = NiceForm.useUpdateOnChange(['country']); 28 | const country = form.getFieldValue('country'); 29 | const loading = country && !cities[country]; 30 | 31 | const meta: AntdNiceFormMeta = { 32 | fields: [ 33 | { 34 | key: 'country', 35 | label: 'Country', 36 | widget: 'select', 37 | options: ['China', 'USA', 'France'], 38 | placeholder: 'Select country...', 39 | initialValue: 'China', 40 | widgetProps: { 41 | onChange: () => { 42 | // Clear city value when country is changed 43 | form.setFieldsValue({ city: undefined }); 44 | }, 45 | }, 46 | }, 47 | { 48 | key: 'city', 49 | label: 'City', 50 | widget: 'select', 51 | options: country ? cities[country] || [] : [], 52 | placeholder: loading ? 'Loading...' : 'Select city...', 53 | widgetProps: { loading }, 54 | disabled: loading || !country, 55 | }, 56 | ], 57 | }; 58 | 59 | const handleFinish = useCallback((values: any) => { 60 | console.log('Submit: ', values); 61 | }, []); 62 | 63 | useEffect(() => { 64 | if (country && !cities[country]) { 65 | fetchCities(country).then((arr) => { 66 | setCities((p) => ({ ...p, [country]: arr })); 67 | }); 68 | } 69 | }, [country, setCities, cities]); 70 | 71 | // If country selected but no cities in store, then it's loading 72 | return ( 73 |
74 | 75 | 76 | 79 | 80 | 81 | ); 82 | }; 83 | -------------------------------------------------------------------------------- /packages/examples-antd/src/examples/Basic.tsx: -------------------------------------------------------------------------------- 1 | import { Form, Button, Rate } from 'antd'; 2 | import NiceForm from '@ebay/nice-form-react'; 3 | import type { AntdNiceFormMeta } from '@ebay/nice-form-react/lib/esm/adapters/antdAdapter'; 4 | 5 | const Basic = () => { 6 | const [form] = Form.useForm(); 7 | const updateOnChange = NiceForm.useUpdateOnChange(['checkbox']); 8 | const options = ['Apple', 'Orange', 'Banana']; 9 | const meta: AntdNiceFormMeta = { 10 | columns: 1, 11 | initialValues: { obj: { input: 'Nate' } }, 12 | layout: 'horizontal', 13 | wrapperProps: { 14 | labelCol: { 15 | span: 8, 16 | }, 17 | }, 18 | fields: [ 19 | { 20 | key: 'obj.input', 21 | name: ['obj', 'input'], 22 | label: 'Input', 23 | required: true, 24 | tooltip: 'Name', 25 | help: 'Name', 26 | }, 27 | { 28 | key: 'checkbox', 29 | label: 'Checkbox', 30 | widget: 'checkbox', 31 | initialValue: true, 32 | }, 33 | { 34 | key: 'rating', 35 | label: 'Rating', 36 | widget: Rate, 37 | initialValue: 3, 38 | condition: () => { 39 | return NiceForm.getFieldValue('checkbox', meta, form); 40 | }, 41 | }, 42 | { key: 'switch', label: 'Switch', widget: 'switch', initialValue: true }, 43 | { 44 | key: 'select', 45 | label: 'Select', 46 | widget: 'select', 47 | required: true, 48 | initialValue: 'Apple', 49 | options, 50 | }, 51 | { 52 | key: 'checkbox-group', 53 | label: 'Checkbox Group', 54 | widget: 'checkbox-group', 55 | initialValue: 'Apple', 56 | options, 57 | }, 58 | { 59 | key: 'radio-group', 60 | label: 'Radio Group', 61 | widget: 'radio-group', 62 | options, 63 | initialValue: 'Orange', 64 | }, 65 | { 66 | key: 'radio-button-group', 67 | label: 'Radio Button Group', 68 | widget: 'radio-group', 69 | initialValue: 'Orange', 70 | 71 | widgetProps: { 72 | optionType: 'button', 73 | buttonStyle: 'solid', 74 | }, 75 | options, 76 | }, 77 | { 78 | key: 'password', 79 | label: 'Password', 80 | widget: 'password', 81 | required: true, 82 | rules: [{ required: true, message: 'password is required' }], 83 | }, 84 | { key: 'textarea', label: 'Textarea', widget: 'textarea' }, 85 | { key: 'number', label: 'Number', widget: 'number', fullWidth: true }, 86 | { key: 'date-picker', label: 'Date Picker', widget: 'date-picker', fullWidth: true }, 87 | ], 88 | }; 89 | const handleFinish = (values: any) => { 90 | form.validateFields().then(() => { 91 | console.log('on finish: ', values); 92 | }); 93 | }; 94 | return ( 95 |
96 | 97 | 98 | 101 | 102 | 103 | ); 104 | }; 105 | 106 | export default Basic; 107 | -------------------------------------------------------------------------------- /packages/examples-antd/src/examples/ComplexLayout.tsx: -------------------------------------------------------------------------------- 1 | import { useCallback } from 'react'; 2 | import { Form, Button } from 'antd'; 3 | import NiceForm from '@ebay/nice-form-react'; 4 | import type { AntdNiceFormMeta } from '@ebay/nice-form-react/lib/esm/adapters/antdAdapter'; 5 | 6 | export default () => { 7 | const [form] = Form.useForm(); 8 | const handleFinish = useCallback((values: any) => { 9 | console.log('Submit: ', values); 10 | }, []); 11 | const meta: AntdNiceFormMeta = { 12 | columns: 4, 13 | layout: 'vertical', // Must set for vertical layout 14 | columnGap: 12, 15 | fields: [ 16 | { 17 | key: 'label1', 18 | colSpan: 4, 19 | render() { 20 | return ( 21 |
22 | Contact Information 23 |
24 | ); 25 | }, 26 | }, 27 | { key: 'address', label: 'Address', colSpan: 4 }, 28 | { key: 'address2', label: 'Address2', colSpan: 4 }, 29 | { key: 'city', label: 'City', colSpan: 2 }, 30 | { key: 'state', label: 'State' }, 31 | { key: 'zip', label: 'Zip Code' }, 32 | { 33 | key: 'label11', 34 | colSpan: 4, 35 | render() { 36 | return ( 37 |
38 | Bed & Bath 39 |
40 | ); 41 | }, 42 | }, 43 | { 44 | key: 'homeType', 45 | label: 'Home Type', 46 | colSpan: 2, 47 | widget: 'select', 48 | initialValue: 'House', 49 | options: ['House', 'Apartment'], 50 | }, 51 | { 52 | key: 'roomType', 53 | label: 'Room Type', 54 | colSpan: 2, 55 | widget: 'select', 56 | initialValue: 'Entire home/apt', 57 | options: ['Entire home/apt', 'Shared'], 58 | }, 59 | { 60 | key: 'bedrooms', 61 | label: 'Bedrooms', 62 | colSpan: 2, 63 | widget: 'select', 64 | options: ['1 Bedroom', '2 Bedrooms'], 65 | }, 66 | { 67 | key: 'bathrooms', 68 | label: 'Bathrooms', 69 | colSpan: 2, 70 | widget: 'select', 71 | options: ['1 Bathroom', '2 Bathrooms'], 72 | }, 73 | { 74 | key: 'king', 75 | label: 'King', 76 | widget: 'number', 77 | widgetProps: { style: { width: '100%' } }, 78 | initialValue: 0, 79 | }, 80 | { 81 | key: 'queen', 82 | label: 'Queen', 83 | widget: 'number', 84 | widgetProps: { style: { width: '100%' } }, 85 | initialValue: 0, 86 | }, 87 | { 88 | key: 'full', 89 | label: 'Full', 90 | widget: 'number', 91 | widgetProps: { style: { width: '100%' } }, 92 | initialValue: 0, 93 | }, 94 | { 95 | key: 'twin', 96 | label: 'Twin', 97 | widget: 'number', 98 | widgetProps: { style: { width: '100%' } }, 99 | initialValue: 0, 100 | }, 101 | ], 102 | }; 103 | 104 | return ( 105 |
106 | 107 | 108 | 111 | 112 | 113 | ); 114 | }; 115 | -------------------------------------------------------------------------------- /packages/examples-antd/src/examples/Coordinated.tsx: -------------------------------------------------------------------------------- 1 | import { useCallback } from 'react'; 2 | import { Form, Button } from 'antd'; 3 | import type { RadioChangeEvent } from 'antd'; 4 | import NiceForm from '@ebay/nice-form-react'; 5 | import type { AntdNiceFormMeta } from '@ebay/nice-form-react/lib/esm/adapters/antdAdapter'; 6 | 7 | export default () => { 8 | const [form] = Form.useForm(); 9 | const handleFinish = useCallback((values: any) => { 10 | console.log('Submit: ', values); 11 | }, []); 12 | 13 | const meta: AntdNiceFormMeta = { 14 | fields: [ 15 | { 16 | key: 'gender', 17 | label: 'Gender', 18 | widget: 'radio-group', 19 | options: ['Male', 'Female'], 20 | onChange: (evt: RadioChangeEvent) => { 21 | if (evt.target.value === 'Male') { 22 | form.setFieldsValue({ note: 'Hi, man!' }); 23 | } else { 24 | form.setFieldsValue({ note: 'Hi, lady!' }); 25 | } 26 | }, 27 | }, 28 | { key: 'note', label: 'Note' }, 29 | ], 30 | }; 31 | 32 | return ( 33 |
34 | 35 | 36 | 39 | 40 | 41 | ); 42 | }; 43 | -------------------------------------------------------------------------------- /packages/examples-antd/src/examples/CustomComponent.tsx: -------------------------------------------------------------------------------- 1 | import { useCallback } from 'react'; 2 | import { Form, Button, Input, Select, InputNumber, Row, Col } from 'antd'; 3 | import type { InputProps } from 'antd'; 4 | import NiceForm from '@ebay/nice-form-react'; 5 | 6 | const Option = Select.Option; 7 | // Here define a custom component just for layout 8 | // For demo, it accept price string like "18.8 USD" 9 | interface PriceInputProps { 10 | value: { price: number | null; currency: string }; 11 | onChange: (value: { price: number | null; currency: string }) => void; 12 | } 13 | const PriceInput: React.FC = ({ value, onChange }) => 14 | value ? ( 15 | 16 | 17 | onChange({ price: v, currency: value.currency })} 21 | /> 22 | 23 | 24 | 31 | 32 | 33 | ) : null; 34 | // This widget is just a wrapper of Input to add a button 35 | const CaptchaInput: React.FC = (props) => ( 36 | 37 | 38 | {' '} 39 | 40 | 41 | {' '} 42 | 43 | 44 | ); 45 | export default () => { 46 | const [form] = Form.useForm(); 47 | const handleFinish = useCallback((values: any) => { 48 | console.log('Submit: ', values); 49 | }, []); 50 | const meta = { 51 | fields: [ 52 | { key: 'product', label: 'Product' }, 53 | { 54 | key: '_temp_price_currency', 55 | label: 'Price', 56 | // Set forwardRef to true if use functional component as field widget 57 | // to avoid warnings 58 | widget: PriceInput, 59 | initialValue: { price: 8, currency: 'USD' }, 60 | }, 61 | { 62 | key: 'captcha', 63 | label: 'Captcha', 64 | required: true, 65 | extra: 'We must make sure that your are a human.', 66 | widget: CaptchaInput, 67 | }, 68 | { 69 | key: 'shipDate', 70 | label: 'Ship Date', 71 | readOnly: true, 72 | viewWidget: () => { 73 | return ( 74 | 75 | 76 | 88 | 89 | 90 | - 91 | 92 | 93 | 105 | 106 | 107 | ); 108 | }, 109 | }, 110 | 111 | { key: 'note', label: 'Note' }, 112 | ], 113 | }; 114 | 115 | return ( 116 |
117 | 118 | 119 | 122 | 123 | 124 | ); 125 | }; 126 | -------------------------------------------------------------------------------- /packages/examples-antd/src/examples/DynamicFields.tsx: -------------------------------------------------------------------------------- 1 | import { Form, Button } from 'antd'; 2 | import NiceForm from '@ebay/nice-form-react'; 3 | import type { AntdNiceFormMeta } from '@ebay/nice-form-react/lib/esm/adapters/antdAdapter'; 4 | 5 | export default () => { 6 | const [form] = Form.useForm(); 7 | Form.useWatch('favoriteFruit', form); 8 | const handleFinish = (values: unknown) => { 9 | console.log('Submit: ', values); 10 | }; 11 | const meta: AntdNiceFormMeta = { 12 | fields: [ 13 | { 14 | key: 'favoriteFruit', 15 | label: 'Favorite Fruit', 16 | widget: 'radio-group', 17 | options: ['Apple', 'Orange', 'Other'], 18 | initialValue: 'Apple', 19 | }, 20 | ], 21 | }; 22 | 23 | // Push other input if choose others 24 | if (NiceForm.getFieldValue('favoriteFruit', meta, form) === 'Other') { 25 | meta.fields.push({ 26 | key: 'otherFruit', 27 | label: 'Other', 28 | }); 29 | } 30 | 31 | return ( 32 |
33 | 34 | 35 | 38 | 39 | 40 | ); 41 | }; 42 | -------------------------------------------------------------------------------- /packages/examples-antd/src/examples/FieldCondition.tsx: -------------------------------------------------------------------------------- 1 | import { Form, Button } from 'antd'; 2 | import NiceForm from '@ebay/nice-form-react'; 3 | import type { AntdNiceFormMeta } from '@ebay/nice-form-react/lib/esm/adapters/antdAdapter'; 4 | 5 | export default () => { 6 | const [form] = Form.useForm(); 7 | const favoriteFruit = Form.useWatch('favoriteFruit', form); 8 | const handleFinish = (values: unknown) => { 9 | console.log('Submit: ', values); 10 | }; 11 | const meta: AntdNiceFormMeta = { 12 | fields: [ 13 | { 14 | key: 'favoriteFruit', 15 | label: 'Favorite Fruit', 16 | widget: 'radio-group', 17 | options: ['Apple', 'Orange', 'Other'], 18 | initialValue: 'Apple', 19 | }, 20 | { 21 | key: 'otherFruit', 22 | label: 'Other', 23 | condition: () => favoriteFruit === 'Other', 24 | }, 25 | ], 26 | }; 27 | 28 | return ( 29 |
30 | 31 | 32 | 35 | 36 | 37 | ); 38 | }; 39 | -------------------------------------------------------------------------------- /packages/examples-antd/src/examples/FormInModal.tsx: -------------------------------------------------------------------------------- 1 | import { useState, useCallback } from 'react'; 2 | import { Form, Button, Modal } from 'antd'; 3 | import NiceForm from '@ebay/nice-form-react'; 4 | 5 | export default () => { 6 | const [form] = Form.useForm(); 7 | const [modalOpen, setModalOpen] = useState(false); 8 | const showModal = useCallback(() => setModalOpen(true), [setModalOpen]); 9 | const hideModal = useCallback(() => setModalOpen(false), [setModalOpen]); 10 | const [pending, setPending] = useState(false); 11 | const handleFinish = useCallback( 12 | (values: any) => { 13 | setPending(true); 14 | console.log('submit: ', values); 15 | setTimeout(() => { 16 | setPending(false); 17 | Modal.success({ title: 'Success', content: 'Submit success.', onOk: hideModal }); 18 | }, 2000); 19 | }, 20 | [setPending, hideModal], 21 | ); 22 | 23 | const meta = { 24 | disabled: pending, 25 | fields: [ 26 | { key: 'name', label: 'Name', required: true }, 27 | { key: 'desc', label: 'Description' }, 28 | ], 29 | }; 30 | 31 | return ( 32 |
33 | 36 | form.submit()} 43 | onCancel={hideModal} 44 | okText={pending ? 'Loading...' : 'Ok'} 45 | okButtonProps={{ loading: pending, disabled: pending }} 46 | cancelButtonProps={{ disabled: pending }} 47 | > 48 |
49 | 50 | 51 |
52 |
53 | ); 54 | }; 55 | -------------------------------------------------------------------------------- /packages/examples-antd/src/examples/FormList.tsx: -------------------------------------------------------------------------------- 1 | import { useCallback } from 'react'; 2 | import { Form, Button } from 'antd'; 3 | import NiceForm from '@ebay/nice-form-react'; 4 | 5 | export default () => { 6 | const meta = { 7 | layout: 'horizontal', 8 | columns: 1, 9 | initialValues: { 10 | username: 'username', 11 | items: [''], 12 | }, 13 | fields: [ 14 | { key: 'username', label: 'User Name' }, 15 | { key: 'items', label: 'Items', widget: 'form-list' }, 16 | { 17 | key: 'cities', 18 | label: 'Cities', 19 | widget: 'form-list', 20 | listItemMeta: { 21 | widget: 'select', 22 | options: ['Beijing', 'Shanghai', 'Nanjing'], 23 | }, 24 | }, 25 | { key: 'items', label: 'Items', widget: 'form-list' }, 26 | ], 27 | }; 28 | 29 | const handleFinish = useCallback((values: any) => { 30 | console.log('Submit: ', values); 31 | }, []); 32 | 33 | return ( 34 |
35 | 36 | 37 | 38 | 41 | 42 | 43 | ); 44 | }; 45 | -------------------------------------------------------------------------------- /packages/examples-antd/src/examples/FormListManual.tsx: -------------------------------------------------------------------------------- 1 | import { useCallback } from 'react'; 2 | import { Form, Button } from 'antd'; 3 | import { MinusCircleOutlined } from '@ant-design/icons'; 4 | import NiceForm from '@ebay/nice-form-react'; 5 | 6 | export default () => { 7 | const meta = { 8 | layout: 'horizontal', 9 | columns: 1, 10 | initialValues: { 11 | username: 'username', 12 | items: ['ddd', 'xxx'], 13 | }, 14 | fields: [ 15 | { key: 'username', label: 'User Name' }, 16 | { key: 'password', label: 'Password', widget: 'password' }, 17 | { 18 | key: 'items', 19 | label: 'Items', 20 | widget: Form.List, 21 | widgetProps: { name: 'items' }, 22 | children: (fields, { add, remove }) => { 23 | return ( 24 | <> 25 | { 28 | return { 29 | ...field, 30 | name: [field.name], 31 | style: { 32 | marginBottom: '10px', 33 | }, 34 | extraNode: ( 35 | <> 36 | {fields.length > 1 ? ( 37 | remove(field.name)} 46 | /> 47 | ) : null} 48 | 49 | ), 50 | }; 51 | }), 52 | }} 53 | /> 54 | 57 | 58 | ); 59 | }, 60 | }, 61 | ], 62 | }; 63 | 64 | const handleFinish = useCallback((values: any) => { 65 | console.log('Submit: ', values); 66 | }, []); 67 | 68 | return ( 69 |
70 | 71 | 72 | 73 | 76 | 77 | 78 | ); 79 | }; 80 | -------------------------------------------------------------------------------- /packages/examples-antd/src/examples/Mixed.tsx: -------------------------------------------------------------------------------- 1 | import { useCallback } from 'react'; 2 | import { Form, Input, Button } from 'antd'; 3 | import NiceForm from '@ebay/nice-form-react'; 4 | 5 | export default () => { 6 | const [form] = Form.useForm(); 7 | const handleFinish = useCallback((values: any) => console.log('Submit: ', values), []); 8 | const meta1 = { 9 | fields: [ 10 | { key: 'name.first', label: 'First Name', required: true }, 11 | { key: 'name.last', label: 'Last Name', required: true }, 12 | { key: 'dob', label: 'Date of Birth', widget: 'date-picker' }, 13 | ], 14 | }; 15 | const meta2 = { 16 | fields: [ 17 | { 18 | key: 'email', 19 | label: 'Email', 20 | rules: [{ type: 'email', message: 'Invalid email' }], 21 | }, 22 | ], 23 | }; 24 | 25 | const prefixMeta = { 26 | fields: [ 27 | { 28 | key: 'prefix', 29 | options: ['+86', '+87'], 30 | widget: 'select', 31 | noStyle: true, 32 | widgetProps: { 33 | style: { width: 70 }, 34 | noStyle: true, 35 | }, 36 | }, 37 | ], 38 | }; 39 | const prefixSelector = ; 40 | 41 | return ( 42 |
43 | 44 | 51 | 52 | 53 | 54 | 55 | 58 | 59 | 60 | ); 61 | }; 62 | -------------------------------------------------------------------------------- /packages/examples-antd/src/examples/MultipleColumns.tsx: -------------------------------------------------------------------------------- 1 | import { useState, useCallback } from 'react'; 2 | import { Form, Button } from 'antd'; 3 | import type { RadioChangeEvent } from 'antd'; 4 | import NiceForm from '@ebay/nice-form-react'; 5 | import type { AntdNiceFormMeta } from '@ebay/nice-form-react/lib/esm/adapters/antdAdapter'; 6 | 7 | export default () => { 8 | const [form] = Form.useForm(); 9 | const [columns, setColumns] = useState(2); 10 | const handleFinish = useCallback((values: any) => { 11 | console.log('Submit: ', values); 12 | }, []); 13 | const meta: AntdNiceFormMeta = { 14 | columns, 15 | fields: [ 16 | { 17 | key: 'columns', 18 | label: 'Columns', 19 | widget: 'radio-group', 20 | widgetProps: { 21 | optionType: 'button', 22 | buttonStyle: 'solid', 23 | onChange: (evt: RadioChangeEvent) => setColumns(evt.target.value), 24 | }, 25 | options: [1, 2, 3, 4], 26 | initialValue: 2, 27 | help: 'Change columns to show layout change', 28 | }, 29 | { key: 'input', label: 'Input', required: true, tooltip: 'This is the name.' }, 30 | { 31 | key: 'checkbox', 32 | label: 'Checkbox', 33 | widget: 'checkbox', 34 | initialValue: true, 35 | }, 36 | { key: 'select', label: 'Select', widget: 'select', options: ['Apple', 'Orange', 'Banana'] }, 37 | { key: 'password', label: 'Password', widget: 'password' }, 38 | { key: 'textarea', label: 'Textarea', widget: 'textarea' }, 39 | { key: 'number', label: 'Number', widget: 'number' }, 40 | { key: 'date-picker', label: 'Date Picker', widget: 'date-picker' }, 41 | ], 42 | }; 43 | return ( 44 |
45 | 46 | 47 | 50 | 51 | 52 | ); 53 | }; 54 | -------------------------------------------------------------------------------- /packages/examples-antd/src/examples/MultipleSections.tsx: -------------------------------------------------------------------------------- 1 | import { useCallback } from 'react'; 2 | import { Form, Button } from 'antd'; 3 | import NiceForm from '@ebay/nice-form-react'; 4 | import type { AntdNiceFormMeta } from '@ebay/nice-form-react/lib/esm/adapters/antdAdapter'; 5 | 6 | export default () => { 7 | const [form] = Form.useForm(); 8 | const handleFinish = useCallback((values: any) => { 9 | console.log('Submit: ', values); 10 | }, []); 11 | const meta: AntdNiceFormMeta = { 12 | columns: 1, 13 | fields: [ 14 | { key: 'name.first', label: 'First Name', required: true }, 15 | { key: 'name.last', label: 'Last Name', required: true }, 16 | { key: 'dob', label: 'Date of Birth', widget: 'date-picker', fullWidth: true }, 17 | { 18 | key: 'email', 19 | label: 'Email', 20 | rules: [{ type: 'email', message: 'Invalid email' }], 21 | }, 22 | { 23 | key: 'security', 24 | label: 'Security Question', 25 | widget: 'select', 26 | placeholder: 'Select a question...', 27 | options: ["What's your pet's name?", 'Your nick name?'], 28 | }, 29 | { key: 'answer', label: 'Security Answer' }, 30 | { key: 'address', label: 'Address' }, 31 | { key: 'city', label: 'City' }, 32 | { key: 'phone', label: 'phone' }, 33 | ], 34 | }; 35 | const meta1: AntdNiceFormMeta = { 36 | ...meta, 37 | fields: meta.fields.slice(0, 3), 38 | }; 39 | const meta2: AntdNiceFormMeta = { 40 | ...meta, 41 | fields: meta.fields.slice(3, 6), 42 | }; 43 | const meta3: AntdNiceFormMeta = { 44 | ...meta, 45 | fields: meta.fields.slice(6), 46 | }; 47 | 48 | return ( 49 |
50 |
51 | Personal Information 52 | 53 |
54 |
55 | Account Information 56 | 57 |
58 |
59 | Contact Information 60 | 61 |
62 | 63 | 66 | 67 |
68 | ); 69 | }; 70 | -------------------------------------------------------------------------------- /packages/examples-antd/src/examples/Simple.tsx: -------------------------------------------------------------------------------- 1 | import { useCallback } from 'react'; 2 | import { Form, Button } from 'antd'; 3 | import NiceForm from '@ebay/nice-form-react'; 4 | 5 | export default () => { 6 | const meta = { 7 | layout: 'horizontal', 8 | columns: 1, 9 | 10 | fields: [ 11 | { key: 'username', label: 'User Name' }, 12 | { key: 'password', label: 'Password', widget: 'password' }, 13 | ], 14 | }; 15 | 16 | const handleFinish = useCallback((values: any) => { 17 | console.log('Submit: ', values); 18 | }, []); 19 | 20 | return ( 21 |
22 | 23 | 24 | 27 | 28 | 29 | ); 30 | }; 31 | -------------------------------------------------------------------------------- /packages/examples-antd/src/examples/SingleField.tsx: -------------------------------------------------------------------------------- 1 | import { useCallback } from 'react'; 2 | import { Form, Button } from 'antd'; 3 | import NiceForm from '@ebay/nice-form-react'; 4 | 5 | export default () => { 6 | const handleFinish = useCallback((values: any) => { 7 | console.log('Submit: ', values); 8 | }, []); 9 | 10 | return ( 11 |
12 | 15 | 22 | 23 | 26 | 27 | 28 | ); 29 | }; 30 | -------------------------------------------------------------------------------- /packages/examples-antd/src/examples/Validation.tsx: -------------------------------------------------------------------------------- 1 | import { useCallback } from 'react'; 2 | import { Form, Button } from 'antd'; 3 | import NiceForm from '@ebay/nice-form-react'; 4 | import type { AntdNiceFormMeta } from '@ebay/nice-form-react/lib/esm/adapters/antdAdapter'; 5 | 6 | const MOCK_USERNAMES: { 7 | [key: string]: boolean; 8 | } = { 9 | nate: true, 10 | bood: true, 11 | kevin: true, 12 | }; 13 | 14 | export default () => { 15 | const [form] = Form.useForm(); 16 | const handleSubmit = useCallback((values: any) => { 17 | console.log('Submit: ', values); 18 | }, []); 19 | 20 | const meta: AntdNiceFormMeta = { 21 | fields: [ 22 | { 23 | key: 'username', 24 | label: 'Username', 25 | extra: 'Note: username nate, bood or kevin already exist', 26 | hasFeedback: true, // Show validation status icon in the right 27 | required: true, // this adds an entry to rules: [{ required: true, message: 'Username is required' }] 28 | rules: [ 29 | { 30 | validator: (rule, value, callback) => { 31 | // Do async validation to check if username already exists 32 | // Use setTimeout to emulate api call 33 | return new Promise((resolve, reject) => { 34 | setTimeout(() => { 35 | if (MOCK_USERNAMES[value]) { 36 | reject(new Error(`Username "${value}" already exists.`)); 37 | } else { 38 | resolve(value); 39 | } 40 | }, 1000); 41 | }); 42 | }, 43 | }, 44 | ], 45 | }, 46 | { 47 | key: 'password', 48 | label: 'Password', 49 | widget: 'password', 50 | onChange: () => { 51 | if (form.isFieldTouched('confirmPassword')) { 52 | form.validateFields(['confirmPassword']); 53 | } 54 | }, 55 | rules: [ 56 | // This is equivalent with "required: true" 57 | { 58 | required: true, 59 | message: 'Password is required', 60 | }, 61 | ], 62 | }, 63 | { 64 | key: 'confirmPassword', 65 | label: 'Confirm Passowrd', 66 | widget: 'password', 67 | required: true, 68 | rules: [ 69 | { 70 | validator: (rule, value, callback) => { 71 | return new Promise((resolve, reject) => { 72 | if (value !== form.getFieldValue('password')) { 73 | reject(new Error('Two passwords are inconsistent.')); 74 | } else { 75 | resolve(value); 76 | } 77 | }); 78 | }, 79 | }, 80 | ], 81 | }, 82 | ], 83 | }; 84 | 85 | return ( 86 |
87 | 88 | 89 | 92 | 93 | 94 | ); 95 | }; 96 | -------------------------------------------------------------------------------- /packages/examples-antd/src/examples/ViewEdit.tsx: -------------------------------------------------------------------------------- 1 | import { useCallback, useState } from 'react'; 2 | import { Form, Button, message } from 'antd'; 3 | import NiceForm from '@ebay/nice-form-react'; 4 | import dayjs from 'dayjs'; 5 | import type { AntdNiceFormMeta } from '@ebay/nice-form-react/lib/esm/adapters/antdAdapter'; 6 | 7 | const MOCK_INFO = { 8 | name: { first: 'Nate', last: 'Wang' }, 9 | email: 'myemail@gmail.com', 10 | gender: 'Male', 11 | dateOfBirth: dayjs('2100-01-01'), 12 | phone: '15988888888', 13 | city: 'Shanghai', 14 | address: 'No.1000 Some Road, Zhangjiang Park, Pudong New District', 15 | }; 16 | 17 | export default () => { 18 | const [form] = Form.useForm(); 19 | const [viewMode, setViewMode] = useState(true); 20 | const [pending, setPending] = useState(false); 21 | const [personalInfo, setPersonalInfo] = useState(MOCK_INFO); 22 | const handleFinish = useCallback((values: any) => { 23 | setPending(true); 24 | setTimeout(() => { 25 | setPending(false); 26 | setPersonalInfo(values); 27 | setViewMode(true); 28 | message.success('Information updated.'); 29 | }, 1500); 30 | }, []); 31 | 32 | const getMeta = () => { 33 | const meta: AntdNiceFormMeta = { 34 | columns: 2, 35 | viewMode, 36 | initialValues: personalInfo, 37 | fields: [ 38 | { key: 'name.first', label: 'First Name', required: true }, 39 | { key: 'name.last', label: 'Last Name', required: true }, 40 | { 41 | key: 'gender', 42 | label: 'Gender', 43 | widget: 'radio-group', 44 | options: ['Male', 'Female'], 45 | }, 46 | { 47 | key: 'dateOfBirth', 48 | label: 'Date of Birth', 49 | widget: 'date-picker', 50 | fullWidth: true, 51 | }, 52 | { key: 'address', label: 'Address', colSpan: 2 }, 53 | ], 54 | }; 55 | return meta; 56 | }; 57 | 58 | return ( 59 |
60 |
61 |

62 | Personal Information 63 | {viewMode && ( 64 | 67 | )} 68 |

69 | 70 | {!viewMode && ( 71 | 72 | 75 | 84 | 85 | )} 86 | 87 |
88 | ); 89 | }; 90 | -------------------------------------------------------------------------------- /packages/examples-antd/src/examples/ViewMode.tsx: -------------------------------------------------------------------------------- 1 | import dayjs from 'dayjs'; 2 | import type { Dayjs } from 'dayjs'; 3 | import NiceForm from '@ebay/nice-form-react'; 4 | 5 | const DateView = ({ value }: { value: Dayjs }) => value.format('MMM Do YYYY'); 6 | 7 | const ViewMode = () => { 8 | const personalInfo = { 9 | name: { first: 'Nate', last: 'Wang' }, 10 | email: 'myemail@gmail.com', 11 | gender: 'Male', 12 | dateOfBirth: dayjs('2100-01-01'), 13 | phone: '15988888888', 14 | city: 'Shanghai', 15 | address: 'No.1000 Some Road, Zhangjiang Park, Pudong New District', 16 | }; 17 | 18 | const meta = { 19 | columns: 2, 20 | viewMode: true, 21 | initialValues: personalInfo, 22 | fields: [ 23 | { key: 'name.first', label: 'First Name', tooltip: 'First name' }, 24 | { key: 'name.last', label: 'Last Name' }, 25 | { key: 'gender', label: 'Gender' }, 26 | { 27 | key: 'dateOfBirth', 28 | label: 'Date of Birth', 29 | viewWidget: DateView, 30 | }, 31 | { key: 'email', label: 'Email' }, 32 | { key: 'phone', label: 'Phone' }, 33 | { key: 'address', label: 'Address', colSpan: 2 }, 34 | { key: 'city', label: 'City' }, 35 | { key: 'zipCode', label: 'Zip Code' }, 36 | ], 37 | }; 38 | 39 | return ( 40 |
41 |
42 |

Personal Information

43 | 44 |
45 |
46 | ); 47 | }; 48 | 49 | export default ViewMode; 50 | -------------------------------------------------------------------------------- /packages/examples-antd/src/index.css: -------------------------------------------------------------------------------- 1 | @import 'antd/dist/reset.css'; 2 | body { 3 | margin: 0; 4 | padding: 0; 5 | font-family: sans-serif; 6 | } 7 | 8 | a { 9 | text-decoration: none; 10 | color: #1890ff; 11 | } 12 | 13 | #root{ 14 | height: 100%; 15 | } 16 | /* :root { 17 | font-family: Inter, Avenir, Helvetica, Arial, sans-serif; 18 | font-size: 16px; 19 | line-height: 24px; 20 | font-weight: 400; 21 | 22 | color-scheme: light dark; 23 | color: rgba(255, 255, 255, 0.87); 24 | background-color: #242424; 25 | 26 | font-synthesis: none; 27 | text-rendering: optimizeLegibility; 28 | -webkit-font-smoothing: antialiased; 29 | -moz-osx-font-smoothing: grayscale; 30 | -webkit-text-size-adjust: 100%; 31 | } 32 | 33 | a { 34 | font-weight: 500; 35 | color: #646cff; 36 | text-decoration: inherit; 37 | } 38 | a:hover { 39 | color: #535bf2; 40 | } 41 | 42 | body { 43 | margin: 0; 44 | display: flex; 45 | place-items: center; 46 | min-width: 320px; 47 | min-height: 100vh; 48 | } 49 | 50 | h1 { 51 | font-size: 3.2em; 52 | line-height: 1.1; 53 | } 54 | 55 | button { 56 | border-radius: 8px; 57 | border: 1px solid transparent; 58 | padding: 0.6em 1.2em; 59 | font-size: 1em; 60 | font-weight: 500; 61 | font-family: inherit; 62 | background-color: #1a1a1a; 63 | cursor: pointer; 64 | transition: border-color 0.25s; 65 | } 66 | button:hover { 67 | border-color: #646cff; 68 | } 69 | button:focus, 70 | button:focus-visible { 71 | outline: 4px auto -webkit-focus-ring-color; 72 | } 73 | 74 | @media (prefers-color-scheme: light) { 75 | :root { 76 | color: #213547; 77 | background-color: #ffffff; 78 | } 79 | a:hover { 80 | color: #747bff; 81 | } 82 | button { 83 | background-color: #f9f9f9; 84 | } 85 | } */ 86 | -------------------------------------------------------------------------------- /packages/examples-antd/src/main.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom/client'; 3 | // import niceFormConfig from '@ebay/nice-form-react/config'; 4 | import { config as niceFormConfig } from '@ebay/nice-form-react'; 5 | import antdAdapter from '@ebay/nice-form-react/adapters/antdAdapter'; 6 | import App from './App'; 7 | import './index.css'; 8 | 9 | niceFormConfig.addAdapter(antdAdapter); 10 | 11 | ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render( 12 | 13 | 14 | , 15 | ); 16 | -------------------------------------------------------------------------------- /packages/examples-antd/src/useHash.ts: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from 'react' 2 | 3 | const getHash = () => document.location.hash.replace('#', '') 4 | 5 | export default () => { 6 | const [hash, setHash] = useState(getHash()) 7 | useEffect(() => { 8 | function handleHashChange() { 9 | const hash = getHash() || 'basic' 10 | setHash(hash) 11 | window.scrollTo({ top: 0 }) 12 | } 13 | window.addEventListener('hashchange', handleHashChange) 14 | return () => { 15 | window.removeEventListener('hashchange', handleHashChange) 16 | } 17 | }, [setHash]) 18 | return hash 19 | } 20 | -------------------------------------------------------------------------------- /packages/examples-antd/src/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /packages/examples-antd/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ESNext", 4 | "useDefineForClassFields": true, 5 | "lib": ["DOM", "DOM.Iterable", "ESNext"], 6 | "allowJs": false, 7 | "skipLibCheck": true, 8 | "esModuleInterop": false, 9 | "allowSyntheticDefaultImports": true, 10 | "strict": true, 11 | "forceConsistentCasingInFileNames": true, 12 | "module": "ESNext", 13 | "moduleResolution": "node", 14 | "resolveJsonModule": true, 15 | "isolatedModules": true, 16 | "noEmit": true, 17 | "jsx": "react-jsx" 18 | }, 19 | "include": ["src"], 20 | "references": [{ "path": "./tsconfig.node.json" }] 21 | } 22 | -------------------------------------------------------------------------------- /packages/examples-antd/tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "composite": true, 4 | "module": "ESNext", 5 | "moduleResolution": "Node", 6 | "allowSyntheticDefaultImports": true 7 | }, 8 | "include": ["vite.config.ts"] 9 | } 10 | -------------------------------------------------------------------------------- /packages/examples-antd/vite.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite'; 2 | import react from '@vitejs/plugin-react'; 3 | // https://vitejs.dev/config/ 4 | export default defineConfig({ 5 | base: '/nice-form-react/antd', 6 | plugins: [react()], 7 | build: { 8 | outDir: '../../gh-pages/antd', 9 | minify: false, 10 | }, 11 | }); 12 | -------------------------------------------------------------------------------- /packages/examples-formik/.eslintrc.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | env: { browser: true, es2020: true }, 4 | extends: [ 5 | 'eslint:recommended', 6 | 'plugin:@typescript-eslint/recommended', 7 | 'plugin:react-hooks/recommended', 8 | ], 9 | ignorePatterns: ['dist', '.eslintrc.cjs'], 10 | parser: '@typescript-eslint/parser', 11 | plugins: ['react-refresh'], 12 | rules: { 13 | 'react-refresh/only-export-components': [ 14 | 'warn', 15 | { allowConstantExport: true }, 16 | ], 17 | }, 18 | } 19 | -------------------------------------------------------------------------------- /packages/examples-formik/.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | dist 12 | dist-ssr 13 | *.local 14 | 15 | # Editor directories and files 16 | .vscode/* 17 | !.vscode/extensions.json 18 | .idea 19 | .DS_Store 20 | *.suo 21 | *.ntvs* 22 | *.njsproj 23 | *.sln 24 | *.sw? 25 | -------------------------------------------------------------------------------- /packages/examples-formik/README.md: -------------------------------------------------------------------------------- 1 | # React + TypeScript + Vite 2 | 3 | This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules. 4 | 5 | Currently, two official plugins are available: 6 | 7 | - [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react/README.md) uses [Babel](https://babeljs.io/) for Fast Refresh 8 | - [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh 9 | 10 | ## Expanding the ESLint configuration 11 | 12 | If you are developing a production application, we recommend updating the configuration to enable type aware lint rules: 13 | 14 | - Configure the top-level `parserOptions` property like this: 15 | 16 | ```js 17 | parserOptions: { 18 | ecmaVersion: 'latest', 19 | sourceType: 'module', 20 | project: ['./tsconfig.json', './tsconfig.node.json'], 21 | tsconfigRootDir: __dirname, 22 | }, 23 | ``` 24 | 25 | - Replace `plugin:@typescript-eslint/recommended` to `plugin:@typescript-eslint/recommended-type-checked` or `plugin:@typescript-eslint/strict-type-checked` 26 | - Optionally add `plugin:@typescript-eslint/stylistic-type-checked` 27 | - Install [eslint-plugin-react](https://github.com/jsx-eslint/eslint-plugin-react) and add `plugin:react/recommended` & `plugin:react/jsx-runtime` to the `extends` list 28 | -------------------------------------------------------------------------------- /packages/examples-formik/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Nice Form Examples - Mui & Formik 8 | 9 | 10 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /packages/examples-formik/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "examples-formik", 3 | "private": true, 4 | "version": "0.0.0", 5 | "type": "module", 6 | "scripts": { 7 | "dev": "vite --port 3000", 8 | "build": "vite build --emptyOutDir", 9 | "lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0", 10 | "preview": "vite preview" 11 | }, 12 | "dependencies": { 13 | "@emotion/react": "^11.10.5", 14 | "@emotion/styled": "^11.10.5", 15 | "@mui/icons-material": "^5.14.18", 16 | "@mui/material": "^5.11.7", 17 | "@mui/x-date-pickers": "^6.16.3", 18 | "dayjs": "^1.11.7", 19 | "formik": "^2.4.5", 20 | "formik-mui-x-date-pickers": "^0.0.1", 21 | "notistack": "^3.0.1", 22 | "prismjs": "1.30.0", 23 | "react": "^18.2.0", 24 | "react-dom": "^18.2.0" 25 | }, 26 | "devDependencies": { 27 | "@ebay/nice-form-react": "^2.0.0", 28 | "@types/prismjs": "^1.26.0", 29 | "@types/react": "^18.2.15", 30 | "@types/react-dom": "^18.2.7", 31 | "@typescript-eslint/eslint-plugin": "^6.0.0", 32 | "@typescript-eslint/parser": "^6.0.0", 33 | "@vitejs/plugin-react": "^4.0.3", 34 | "eslint": "^8.45.0", 35 | "eslint-plugin-react-hooks": "^4.6.0", 36 | "eslint-plugin-react-refresh": "^0.4.3", 37 | "typescript": "^5.0.2", 38 | "vite": "^4.4.5" 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /packages/examples-formik/public/vite.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /packages/examples-formik/src/App.css: -------------------------------------------------------------------------------- 1 | .app { 2 | /* display: grid; 3 | grid-template-columns: 260px 1fr; 4 | min-height: 100vh; */ 5 | } 6 | 7 | .app .sider { 8 | background-color: #f7f7f7; 9 | font-size: 14px; 10 | position: fixed; 11 | min-height: 100vh; 12 | width: 260px; 13 | } 14 | 15 | .app .sider h1 { 16 | position: relative; 17 | background-color: #f0f0f0; 18 | padding: 10px 0 10px 28px; 19 | margin: 0; 20 | margin-bottom: 12px; 21 | font-weight: normal; 22 | } 23 | .app .sider h1 .header-name { 24 | display: block; 25 | font-size: 12px; 26 | color: #999; 27 | } 28 | 29 | .app .sider img.logo { 30 | position: absolute; 31 | right: 10px; 32 | top: 10px; 33 | width: 32px; 34 | } 35 | 36 | .app .sider h1 span.example-title { 37 | font-size: 20px; 38 | display: block; 39 | margin-top: 5px; 40 | color: #333; 41 | display: inline-block; 42 | } 43 | .app .sider h1 .ant-select { 44 | margin-top: 5px; 45 | color: #666; 46 | margin-left: 10px; 47 | } 48 | 49 | .app .sider ul, 50 | .app .sider li { 51 | margin: 0; 52 | padding: 0; 53 | list-style: none; 54 | } 55 | 56 | .app .sider li { 57 | line-height: 260%; 58 | } 59 | 60 | .app .sider li a { 61 | display: block; 62 | padding-left: 28px; 63 | } 64 | 65 | .app .sider li a.active { 66 | color: #f90; 67 | } 68 | 69 | .app .lib-switch { 70 | font-size: 14px; 71 | display: block; 72 | padding: 5px; 73 | width: 90%; 74 | border: 1px solid #ccc; 75 | margin-bottom: 10px; 76 | background-color: transparent; 77 | } 78 | 79 | .app .sider .social { 80 | padding-left: 28px; 81 | margin-top: 20px; 82 | line-height: 40px; 83 | } 84 | 85 | .app .example-container { 86 | padding: 40px; 87 | overflow-x: auto; 88 | margin-left: 260px; 89 | } 90 | 91 | .app .example-container h1 { 92 | border-bottom: 1px solid #eee; 93 | padding-bottom: 15px; 94 | margin-bottom: 35px; 95 | font-weight: normal; 96 | margin-top:0; 97 | } 98 | 99 | .app .example-container .ant-form { 100 | width: 600px; 101 | } 102 | 103 | .example-description { 104 | color: #777; 105 | font-size: 14px; 106 | margin: 10px 0 0 0; 107 | line-height: 150%; 108 | } 109 | 110 | .code-container { 111 | margin-top: 40px; 112 | grid-column: 2 / span 1; 113 | } 114 | 115 | .nice-form-react-item-view-mode .ant-form-item-label label { 116 | font-weight: bold; 117 | } 118 | -------------------------------------------------------------------------------- /packages/examples-formik/src/CodeViewer.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect } from 'react'; 2 | import Prism from 'prismjs'; 3 | import 'prismjs/themes/prism-tomorrow.css'; 4 | import 'prismjs/plugins/line-numbers/prism-line-numbers.js'; 5 | import 'prismjs/plugins/line-numbers/prism-line-numbers.css'; 6 | import 'prismjs/plugins/toolbar/prism-toolbar.css'; 7 | import 'prismjs/plugins/toolbar/prism-toolbar.js'; 8 | import 'prismjs/plugins/copy-to-clipboard/prism-copy-to-clipboard.js'; 9 | import codeBasic from './examples/Basic.tsx?raw'; 10 | import codeDynamicFields from './examples/DynamicFields.js?raw'; 11 | import codeFieldCondition from './examples/FieldCondition.js?raw'; 12 | import codeAsyncDataSource from './examples/AsyncDataSource.js?raw'; 13 | import codeMultipleColumns from './examples/MultipleColumns.js?raw'; 14 | import codeComplexLayout from './examples/ComplexLayout.js?raw'; 15 | import codeMultipleSections from './examples/MultipleSections.js?raw'; 16 | // import codeSingleField from './examples/SingleField.js?raw'; 17 | import codeValidation from './examples/Validation.js?raw'; 18 | import codeCoordinated from './examples/Coordinated.js?raw'; 19 | import codeFormInModal from './examples/FormInModal.js?raw'; 20 | import codeCustomComponent from './examples/CustomComponent.js?raw'; 21 | import codeViewEdit from './examples/ViewEdit.js?raw'; 22 | import codeMixed from './examples/Mixed.js?raw'; 23 | import codeWizard from './examples/Wizard.js?raw'; 24 | import codeSimple from './examples/Simple.js?raw'; 25 | import codeViewMode from './examples/ViewMode.js?raw'; 26 | import codeFormList from './examples/FormList.js?raw'; 27 | 28 | type CodeMap = { 29 | [key: string]: string; 30 | }; 31 | 32 | const codeMap: CodeMap = { 33 | basic: codeBasic, 34 | 'view-edit': codeViewEdit, 35 | 'dynamic-fields': codeDynamicFields, 36 | 'field-condition': codeFieldCondition, 37 | 'async-data-source': codeAsyncDataSource, 38 | 'multiple-columns': codeMultipleColumns, 39 | 'complex-layout': codeComplexLayout, 40 | 'multiple-sections': codeMultipleSections, 41 | // 'single-field': codeSingleField, 42 | validation: codeValidation, 43 | coordinated: codeCoordinated, 44 | 'form-in-modal': codeFormInModal, 45 | 'custom-component': codeCustomComponent, 46 | mixed: codeMixed, 47 | wizard: codeWizard, 48 | simple: codeSimple, 49 | 'view-mode': codeViewMode, 50 | 'form-list': codeFormList, 51 | }; 52 | 53 | interface CodeViewerProps { 54 | code: string; 55 | } 56 | 57 | const CodeViewer: React.FC = ({ code }) => { 58 | useEffect(() => { 59 | Prism.highlightAll(); 60 | }, [code]); 61 | return ( 62 |
63 |       
64 |         {codeMap[code] || `// Error: code of "${code}" not found`}
65 |       
66 |     
67 | ); 68 | }; 69 | 70 | export default CodeViewer; 71 | -------------------------------------------------------------------------------- /packages/examples-formik/src/assets/react.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /packages/examples-formik/src/examples/AsyncDataSource.tsx: -------------------------------------------------------------------------------- 1 | import { useState, useEffect } from 'react'; 2 | import { Form, Formik, useFormikContext } from 'formik'; 3 | import Button from '@mui/material/Button'; 4 | import NiceForm from '@ebay/nice-form-react'; 5 | import type { NiceFormMeta } from '@ebay/nice-form-react/types'; 6 | 7 | const MOCK_DATA: { 8 | [key: string]: string[]; 9 | } = { 10 | China: ['Beijing', 'Shanghai', 'Nanjing'], 11 | USA: ['New York', 'San Jose', 'Washton'], 12 | France: ['Paris', 'Marseille', 'Cannes'], 13 | }; 14 | 15 | // Mock fetch 16 | const fetchCities = (country: string): Promise => { 17 | return new Promise((resolve, reject) => { 18 | setTimeout(() => { 19 | if (MOCK_DATA[country]) resolve(MOCK_DATA[country]); 20 | else reject(new Error('Not found')); 21 | }, 1500); 22 | }); 23 | }; 24 | 25 | interface FormValues { 26 | country: string; 27 | city?: string; 28 | } 29 | const MyForm = () => { 30 | const [cities, setCities] = useState<{ [key: string]: string[] }>({}); 31 | const { values, setFieldValue } = useFormikContext(); 32 | const country = values.country; 33 | const loading = country && !cities[country]; 34 | 35 | const meta: NiceFormMeta = { 36 | rowGap: 18, 37 | fields: [ 38 | { 39 | key: 'country', 40 | label: 'Country', 41 | widget: 'select', 42 | options: ['China', 'USA', 'France'], 43 | placeholder: 'Select country...', 44 | initialValue: 'China', 45 | widgetProps: { 46 | fullWidth: true, 47 | onChange: () => { 48 | // Clear city value when country is changed 49 | setFieldValue('city', undefined, true); 50 | }, 51 | }, 52 | }, 53 | { 54 | key: 'city', 55 | label: 'City', 56 | widget: 'select', 57 | options: country ? cities[country] || [] : [], 58 | widgetProps: { 59 | fullWidth: true, 60 | placeholder: loading ? 'Loading...' : 'Select city...', 61 | }, 62 | disabled: loading || !country, 63 | }, 64 | ], 65 | }; 66 | 67 | useEffect(() => { 68 | if (country && !cities[country]) { 69 | fetchCities(country).then((arr) => { 70 | setCities((p) => ({ ...p, [country]: arr })); 71 | }); 72 | } 73 | }, [country, setCities, cities]); 74 | 75 | // If country selected but no cities in store, then it's loading 76 | return ( 77 |
78 | 79 | 82 | 83 | ); 84 | }; 85 | 86 | export default function AsyncDataSource() { 87 | return ( 88 | { 91 | await new Promise((r) => setTimeout(r, 2000)); 92 | alert(JSON.stringify(values, null, 2)); 93 | }} 94 | > 95 | 96 | 97 | ); 98 | } 99 | -------------------------------------------------------------------------------- /packages/examples-formik/src/examples/ComplexLayout.tsx: -------------------------------------------------------------------------------- 1 | import { Formik, Form } from 'formik'; 2 | import NiceForm from '@ebay/nice-form-react/NiceForm'; 3 | import Button from '@mui/material/Button'; 4 | import Divider from '@mui/material/Divider'; 5 | 6 | const ComplexLayout = () => { 7 | const meta = { 8 | columns: 4, 9 | layout: 'vertical', // Must set for vertical layout 10 | columnGap: 12, 11 | rowGap: 18, 12 | fields: [ 13 | { 14 | key: 'label1', 15 | colSpan: 4, 16 | render() { 17 | return ( 18 | <> 19 | CENTER 20 | 21 | 22 | ); 23 | }, 24 | }, 25 | { key: 'address', label: 'Address', colSpan: 4 }, 26 | { key: 'address2', label: 'Address2', colSpan: 4 }, 27 | { key: 'city', label: 'City', colSpan: 2 }, 28 | { key: 'state', label: 'State' }, 29 | { key: 'zip', label: 'Zip Code' }, 30 | { 31 | key: 'label11', 32 | colSpan: 4, 33 | render() { 34 | return ( 35 | <> 36 | Bed & Bath 37 | 38 | 39 | ); 40 | }, 41 | }, 42 | { 43 | key: 'homeType', 44 | label: 'Home Type', 45 | colSpan: 2, 46 | widget: 'select', 47 | initialValue: 'House', 48 | options: ['House', 'Apartment'], 49 | fullWidth: true, 50 | }, 51 | { 52 | key: 'roomType', 53 | label: 'Room Type', 54 | colSpan: 2, 55 | widget: 'select', 56 | fullWidth: true, 57 | 58 | initialValue: 'Entire home/apt', 59 | options: ['Entire home/apt', 'Shared'], 60 | }, 61 | { 62 | key: 'bedrooms', 63 | label: 'Bedrooms', 64 | colSpan: 2, 65 | widget: 'select', 66 | fullWidth: true, 67 | 68 | options: ['1 Bedroom', '2 Bedrooms'], 69 | }, 70 | { 71 | key: 'bathrooms', 72 | label: 'Bathrooms', 73 | colSpan: 2, 74 | fullWidth: true, 75 | 76 | widget: 'select', 77 | options: ['1 Bathroom', '2 Bathrooms'], 78 | }, 79 | { 80 | key: 'king', 81 | label: 'King', 82 | widgetProps: { type: 'number', style: { width: '100%' } }, 83 | initialValue: 0, 84 | }, 85 | { 86 | key: 'queen', 87 | label: 'Queen', 88 | widgetProps: { type: 'number', style: { width: '100%' } }, 89 | initialValue: 0, 90 | }, 91 | { 92 | key: 'full', 93 | label: 'Full', 94 | widgetProps: { type: 'number', style: { width: '100%' } }, 95 | initialValue: 0, 96 | }, 97 | { 98 | key: 'twin', 99 | label: 'Twin', 100 | widgetProps: { type: 'number', style: { width: '100%' } }, 101 | initialValue: 0, 102 | }, 103 | { 104 | key: 'submit', 105 | colSpan: 4, 106 | 107 | render: () => { 108 | return ( 109 | 112 | ); 113 | }, 114 | }, 115 | ], 116 | }; 117 | return ( 118 | { 121 | await new Promise((r) => setTimeout(r, 500)); 122 | alert(JSON.stringify(values, null, 2)); 123 | }} 124 | > 125 |
126 | 127 | 128 |
129 | ); 130 | }; 131 | 132 | export default ComplexLayout; 133 | -------------------------------------------------------------------------------- /packages/examples-formik/src/examples/Coordinated.tsx: -------------------------------------------------------------------------------- 1 | import Button from '@mui/material/Button'; 2 | import NiceForm from '@ebay/nice-form-react'; 3 | import { Formik, Form } from 'formik'; 4 | import type { FormikHelpers } from 'formik'; 5 | 6 | interface FormValues { 7 | gender?: string; 8 | note?: string; 9 | } 10 | 11 | const Coordinated = () => { 12 | const getMeta = (formik: FormikHelpers) => { 13 | return { 14 | rowGap: 18, 15 | fields: [ 16 | { 17 | key: 'gender', 18 | label: 'Gender', 19 | widget: 'radio-group', 20 | options: ['Male', 'Female'], 21 | widgetProps: { 22 | onChange: (evt: React.ChangeEvent, value: string) => { 23 | console.log('evt: ', evt); 24 | if (value === 'Male') { 25 | formik.setFieldValue('note', 'Hi, man!', true); 26 | } else { 27 | formik.setFieldValue('note', 'Hi, lady!', true); 28 | } 29 | }, 30 | }, 31 | }, 32 | { key: 'note', widgetProps: { placeholder: 'Note' } }, 33 | ], 34 | }; 35 | }; 36 | 37 | return ( 38 | {}} initialValues={{}}> 39 | {(formik) => ( 40 |
41 | 42 | 45 | 46 | )} 47 |
48 | ); 49 | }; 50 | export default Coordinated; 51 | -------------------------------------------------------------------------------- /packages/examples-formik/src/examples/DynamicFields.tsx: -------------------------------------------------------------------------------- 1 | import NiceForm from '@ebay/nice-form-react/NiceForm'; 2 | import { FormikMuiNiceFormMeta } from '@ebay/nice-form-react/adapters/formikMuiAdapter'; 3 | import { Formik, Form, FormikProps } from 'formik'; 4 | import Button from '@mui/material/Button'; 5 | 6 | interface FormValues { 7 | favoriteFruit: string; 8 | otherFruit?: string; 9 | } 10 | 11 | const DynamicFields = () => { 12 | const getMeta = (form: FormikProps) => { 13 | const meta: FormikMuiNiceFormMeta = { 14 | rowGap: 18, 15 | form, 16 | fields: [ 17 | { 18 | key: 'favoriteFruit', 19 | label: 'Favorite Fruit', 20 | widget: 'radio-group', 21 | options: ['Apple', 'Orange', 'Other'], 22 | initialValue: 'Apple', 23 | }, 24 | ], 25 | }; 26 | if (form.values.favoriteFruit === 'Other') { 27 | meta.fields.push({ 28 | key: 'otherFruit', 29 | label: 'Other', 30 | widget: 'text', 31 | initialValue: 'Apple', 32 | }); 33 | } 34 | return meta; 35 | }; 36 | 37 | return ( 38 | { 43 | alert(JSON.stringify(values, null, 2)); 44 | }} 45 | > 46 | {(form) => ( 47 |
48 | 49 | 52 | 53 | )} 54 |
55 | ); 56 | }; 57 | 58 | export default DynamicFields; 59 | -------------------------------------------------------------------------------- /packages/examples-formik/src/examples/FieldCondition.tsx: -------------------------------------------------------------------------------- 1 | import NiceForm from '@ebay/nice-form-react/NiceForm'; 2 | import { Formik, Form, FormikProps } from 'formik'; 3 | import Button from '@mui/material/Button'; 4 | 5 | interface FormValues { 6 | favoriteFruit: string; 7 | otherFruit?: string; 8 | } 9 | const FieldCondition = () => { 10 | const getMeta = (form: FormikProps) => { 11 | const meta = { 12 | rowGap: 18, 13 | form, 14 | fields: [ 15 | { 16 | key: 'favoriteFruit', 17 | label: 'Favorite Fruit', 18 | widget: 'radio-group', 19 | options: ['Apple', 'Orange', 'Other'], 20 | initialValue: 'Apple', 21 | }, 22 | { 23 | key: 'otherFruit', 24 | label: 'Other', 25 | condition: () => form.values.favoriteFruit === 'Other', 26 | }, 27 | { 28 | key: 'submitt', 29 | render: () => { 30 | return ( 31 | 34 | ); 35 | }, 36 | }, 37 | ], 38 | }; 39 | return meta; 40 | }; 41 | 42 | return ( 43 | { 48 | alert(JSON.stringify(values, null, 2)); 49 | }} 50 | > 51 | {(form) => ( 52 |
53 | 54 | 55 | )} 56 |
57 | ); 58 | }; 59 | 60 | export default FieldCondition; 61 | -------------------------------------------------------------------------------- /packages/examples-formik/src/examples/FormInModal.tsx: -------------------------------------------------------------------------------- 1 | import { useCallback, useState } from 'react'; 2 | import Button from '@mui/material/Button'; 3 | import Dialog from '@mui/material/Dialog'; 4 | import DialogActions from '@mui/material/DialogActions'; 5 | import DialogContent from '@mui/material/DialogContent'; 6 | import DialogTitle from '@mui/material/DialogTitle'; 7 | import NiceForm from '@ebay/nice-form-react'; 8 | import { Formik, Form } from 'formik'; 9 | import { SnackbarProvider, enqueueSnackbar } from 'notistack'; 10 | 11 | export default function FormInModal() { 12 | const [open, setOpen] = useState(false); 13 | const handleOpen = () => setOpen(true); 14 | const handleClose = () => setOpen(false); 15 | const [pending, setPending] = useState(false); 16 | 17 | const handleFinish = useCallback(() => { 18 | setPending(true); 19 | setTimeout(() => { 20 | setPending(false); 21 | enqueueSnackbar('Submit success!', { 22 | variant: 'success', 23 | anchorOrigin: { vertical: 'top', horizontal: 'center' }, 24 | }); 25 | handleClose(); 26 | }, 2000); 27 | }, [setPending]); 28 | 29 | const meta = { 30 | rowGap: 18, 31 | disabled: pending, 32 | fields: [ 33 | { 34 | key: 'name', 35 | label: 'Name', 36 | required: true, 37 | }, 38 | { 39 | key: 'desc', 40 | label: 'Description', 41 | }, 42 | ], 43 | }; 44 | 45 | return ( 46 | 47 | 50 | 51 | New Item 52 | 53 | 54 |
55 | 56 | 57 | 60 | 63 | 64 |
65 |
66 |
67 |
68 |
69 | ); 70 | } 71 | -------------------------------------------------------------------------------- /packages/examples-formik/src/examples/FormList.tsx: -------------------------------------------------------------------------------- 1 | import Button from '@mui/material/Button'; 2 | import NiceForm from '@ebay/nice-form-react'; 3 | import { Formik, Form } from 'formik'; 4 | 5 | const FormList = () => { 6 | const meta = { 7 | layout: 'horizontal', 8 | columns: 1, 9 | rowGap: 18, 10 | fields: [ 11 | { key: 'username', label: 'User Name' }, 12 | { key: 'password', label: 'Password', widgetProps: { type: 'password' } }, 13 | { 14 | key: 'friends', 15 | label: 'Friends', 16 | widget: 'form-list', 17 | fullWidth: true, 18 | listItemProps: { 19 | widget: 'select', 20 | options: ['Tom', 'Jerry'], 21 | required: true, 22 | fullWidth: true, 23 | }, 24 | }, 25 | ], 26 | }; 27 | 28 | return ( 29 | { 35 | await new Promise((r) => setTimeout(r, 500)); 36 | alert(JSON.stringify(values, null, 2)); 37 | }} 38 | > 39 |
40 | 41 | 44 | 45 |
46 | ); 47 | }; 48 | 49 | export default FormList; 50 | -------------------------------------------------------------------------------- /packages/examples-formik/src/examples/Mixed.tsx: -------------------------------------------------------------------------------- 1 | import NiceForm from '@ebay/nice-form-react'; 2 | import Button from '@mui/material/Button'; 3 | import TextField from '@mui/material/TextField'; 4 | import { AdapterDayjs } from '@mui/x-date-pickers/AdapterDayjs'; 5 | import { LocalizationProvider } from '@mui/x-date-pickers/LocalizationProvider'; 6 | import { Form, Formik } from 'formik'; 7 | 8 | const Mixed = () => { 9 | const getMeta1 = () => { 10 | return { 11 | rowGap: 18, 12 | fields: [ 13 | { 14 | key: 'name.first', 15 | label: 'First Name', 16 | required: true, 17 | widgetProps: { 18 | onChange: (e: React.ChangeEvent) => { 19 | console.log('e: ', e); 20 | }, 21 | }, 22 | }, 23 | { key: 'name.last', label: 'Last Name', required: true }, 24 | { 25 | key: 'dob', 26 | label: 'Date of Birth', 27 | widget: 'date-picker', 28 | }, 29 | ], 30 | }; 31 | }; 32 | const meta2 = { 33 | rowGap: 18, 34 | fields: [ 35 | { 36 | key: 'email', 37 | label: 'Email', 38 | }, 39 | ], 40 | }; 41 | 42 | const prefixMeta = { 43 | fields: [ 44 | { 45 | key: 'prefix', 46 | options: ['+86', '+87'], 47 | widget: 'select', 48 | widgetProps: { 49 | style: { width: 100, border: 'none' }, 50 | variant: 'standard', 51 | }, 52 | }, 53 | ], 54 | }; 55 | const prefixSelector = ; 56 | 57 | return ( 58 | { 62 | await new Promise((r) => setTimeout(r, 500)); 63 | alert(JSON.stringify(values, null, 2)); 64 | }} 65 | style={{ width: '500px' }} 66 | > 67 | {(form) => ( 68 | 69 |
70 | 71 | 86 | 87 | 90 | 91 |
92 | )} 93 |
94 | ); 95 | }; 96 | 97 | export default Mixed; 98 | -------------------------------------------------------------------------------- /packages/examples-formik/src/examples/MultipleColumns.tsx: -------------------------------------------------------------------------------- 1 | import { useState } from 'react'; 2 | import Button from '@mui/material/Button'; 3 | import NiceForm from '@ebay/nice-form-react'; 4 | import { Formik, Form, FormikProps } from 'formik'; 5 | import { LocalizationProvider } from '@mui/x-date-pickers/LocalizationProvider'; 6 | import { AdapterDayjs } from '@mui/x-date-pickers/AdapterDayjs'; 7 | import type { Dayjs } from 'dayjs'; 8 | 9 | interface FormValues { 10 | columns: number; 11 | input?: string; 12 | checkbox?: boolean; 13 | select?: string; 14 | password?: string; 15 | textarea?: string; 16 | number?: number; 17 | 'date-picker'?: Dayjs | null; 18 | } 19 | const MultipleColumns = () => { 20 | const [columns, setColumns] = useState(2); 21 | 22 | const getMeta = (form: FormikProps) => ({ 23 | columns, 24 | rowGap: 18, 25 | columnGap: 18, 26 | fields: [ 27 | { 28 | key: 'columns', 29 | label: 'Columns', 30 | widget: 'radio-group', 31 | widgetProps: { 32 | onChange: (evt: React.ChangeEvent, value: number) => { 33 | setColumns(value); 34 | form.values.columns = +evt.target.value; 35 | }, 36 | }, 37 | options: [1, 2, 3, 4], 38 | help: 'Change columns to show layout change', 39 | }, 40 | { key: 'input', label: 'Input', required: true, tooltip: 'This is the name.' }, 41 | { 42 | key: 'checkbox', 43 | label: 'Checkbox', 44 | widget: 'checkbox', 45 | initialValue: true, 46 | }, 47 | { 48 | key: 'select', 49 | label: 'Select', 50 | widget: 'select', 51 | options: ['Apple', 'Orange', 'Banana'], 52 | fullWidth: true, 53 | }, 54 | { 55 | key: 'password', 56 | label: 'Password', 57 | widgetProps: { 58 | type: 'password', 59 | }, 60 | }, 61 | { 62 | key: 'textarea', 63 | label: 'Textarea', 64 | widgetProps: { 65 | multiline: true, 66 | fullWidth: true, 67 | rows: 4, 68 | }, 69 | }, 70 | { 71 | key: 'number', 72 | label: 'Number', 73 | widgetProps: { 74 | type: 'number', 75 | }, 76 | }, 77 | { key: 'date-picker', label: 'Date Picker', widget: 'date-picker' }, 78 | { 79 | key: 'submit', 80 | render: () => ( 81 | 84 | ), 85 | }, 86 | ], 87 | }); 88 | return ( 89 | { 92 | alert(JSON.stringify(values, null, 2)); 93 | }} 94 | > 95 | {(form: FormikProps) => ( 96 | 97 |
98 | 99 | 100 |
101 | )} 102 |
103 | ); 104 | }; 105 | 106 | export default MultipleColumns; 107 | -------------------------------------------------------------------------------- /packages/examples-formik/src/examples/MultipleSections.tsx: -------------------------------------------------------------------------------- 1 | import { Formik, Form } from 'formik'; 2 | import NiceForm from '@ebay/nice-form-react/NiceForm'; 3 | import Button from '@mui/material/Button'; 4 | import Divider from '@mui/material/Divider'; 5 | import { LocalizationProvider } from '@mui/x-date-pickers/LocalizationProvider'; 6 | import { AdapterDayjs } from '@mui/x-date-pickers/AdapterDayjs'; 7 | 8 | const MultipleSections = () => { 9 | const meta = { 10 | columns: 1, 11 | rowGap: 18, 12 | fields: [ 13 | { key: 'name.first', label: 'First Name', required: true }, 14 | { key: 'name.last', label: 'Last Name', required: true }, 15 | { key: 'dob', label: 'Date of Birth', widget: 'date-picker', fullWidth: true }, 16 | { 17 | key: 'email', 18 | label: 'Email', 19 | rules: [{ type: 'email', message: 'Invalid email' }], 20 | }, 21 | { 22 | key: 'security', 23 | label: 'Security Question', 24 | widget: 'select', 25 | fullWidth: true, 26 | placeholder: 'Select a question...', 27 | options: ["What's your pet's name?", 'Your nick name?'], 28 | }, 29 | { key: 'answer', label: 'Security Answer' }, 30 | { key: 'address', label: 'Address' }, 31 | { key: 'city', label: 'City' }, 32 | { key: 'phone', label: 'phone' }, 33 | ], 34 | }; 35 | 36 | const meta1 = { 37 | ...meta, 38 | fields: meta.fields.slice(0, 3), 39 | }; 40 | const meta2 = { 41 | ...meta, 42 | fields: meta.fields.slice(3, 6), 43 | }; 44 | const meta3 = { 45 | ...meta, 46 | fields: meta.fields.slice(6), 47 | }; 48 | 49 | return ( 50 | { 53 | await new Promise((r) => setTimeout(r, 500)); 54 | alert(JSON.stringify(values, null, 2)); 55 | }} 56 | > 57 | 58 |
59 |
60 |
61 | Personal Information 62 | 63 |
64 | 65 | 66 |
67 |
68 |
69 | Account Information 70 | 71 |
72 | 73 |
74 |
75 |
76 | Contact Information 77 | 78 |
79 | 80 |
81 | 84 |
85 |
86 |
87 | ); 88 | }; 89 | 90 | export default MultipleSections; 91 | -------------------------------------------------------------------------------- /packages/examples-formik/src/examples/Simple.tsx: -------------------------------------------------------------------------------- 1 | import { Form, Formik, FormikProps } from 'formik'; 2 | import Button from '@mui/material/Button'; 3 | import NiceForm from '@ebay/nice-form-react'; 4 | import { FormikMuiNiceFormMeta } from '@ebay/nice-form-react/adapters/formikMuiAdapter'; 5 | 6 | const Simple = () => { 7 | const initialValues = { 8 | username: '', 9 | password: '', 10 | }; 11 | 12 | const getMeta = (form: FormikProps) => { 13 | const formMeta: FormikMuiNiceFormMeta = { 14 | columns: 1, 15 | rowGap: 18, 16 | form, 17 | initialValues, 18 | disabled: form.isSubmitting, 19 | fields: [ 20 | { 21 | key: 'username', 22 | label: 'User Name', 23 | widget: 'text', 24 | }, 25 | { 26 | key: 'password', 27 | label: 'Password', 28 | widget: 'text', 29 | widgetProps: { 30 | type: 'password', 31 | }, 32 | }, 33 | { 34 | key: 'submit', 35 | render: () => { 36 | return ( 37 | 40 | ); 41 | }, 42 | }, 43 | ], 44 | }; 45 | return formMeta; 46 | }; 47 | 48 | return ( 49 |
50 | { 53 | await new Promise((r) => setTimeout(r, 500)); 54 | alert(JSON.stringify(values, null, 2)); 55 | }} 56 | > 57 | {(form) => { 58 | return ( 59 |
60 | 61 | 62 | ); 63 | }} 64 |
65 |
66 | ); 67 | }; 68 | 69 | export default Simple; 70 | -------------------------------------------------------------------------------- /packages/examples-formik/src/examples/Validation.tsx: -------------------------------------------------------------------------------- 1 | import NiceForm from '@ebay/nice-form-react'; 2 | import { Formik, Form, FormikProps } from 'formik'; 3 | import Button from '@mui/material/Button'; 4 | 5 | const MOCK_USERNAMES: { 6 | [key: string]: boolean; 7 | } = { 8 | nate: true, 9 | bood: true, 10 | kevin: true, 11 | }; 12 | 13 | interface FormValues { 14 | username?: string; 15 | password?: string; 16 | confirmPassword?: string; 17 | } 18 | 19 | const Validation = () => { 20 | const getMeta = (form: FormikProps) => { 21 | const meta = { 22 | rowGap: 18, 23 | fields: [ 24 | { 25 | key: 'username', 26 | label: 'Username', 27 | placeholder: 'Note: username nate, bood or kevin already exist', 28 | hasFeedback: true, // Show validation status icon in the right 29 | required: true, // this adds an entry to rules: [{ required: true, message: 'Username is required' }] 30 | validate: (value: string) => { 31 | let error; 32 | if (MOCK_USERNAMES[value]) { 33 | error = `Username "${value}" already exists.`; 34 | } 35 | return error; 36 | }, 37 | }, 38 | { 39 | key: 'password', 40 | label: 'Password', 41 | widgetProps: { type: 'password' }, 42 | validate: (value: string) => { 43 | let error; 44 | if (!value) { 45 | error = `Password is required`; 46 | } 47 | return error; 48 | }, 49 | }, 50 | { 51 | key: 'confirmPassword', 52 | label: 'Confirm Passowrd', 53 | required: true, 54 | widgetProps: { type: 'password' }, 55 | validate: (value: string) => { 56 | let error; 57 | if (form.values.password !== value) { 58 | error = `Two passwords are inconsistent.`; 59 | } 60 | return error; 61 | }, 62 | }, 63 | { 64 | key: 'submit', 65 | render: () => ( 66 | 69 | ), 70 | }, 71 | ], 72 | }; 73 | return meta; 74 | }; 75 | 76 | return ( 77 | { 80 | await new Promise((r) => setTimeout(r, 500)); 81 | alert(JSON.stringify(values, null, 2)); 82 | }} 83 | > 84 | {(form: FormikProps) => ( 85 |
86 | 87 | 88 | )} 89 |
90 | ); 91 | }; 92 | 93 | export default Validation; 94 | -------------------------------------------------------------------------------- /packages/examples-formik/src/examples/ViewEdit.tsx: -------------------------------------------------------------------------------- 1 | import { useCallback, useState } from 'react'; 2 | import NiceForm from '@ebay/nice-form-react'; 3 | import dayjs from 'dayjs'; 4 | import { FormikMuiNiceFormMeta } from '@ebay/nice-form-react/adapters/formikMuiAdapter'; 5 | import { Form, Formik, FormikProps } from 'formik'; 6 | import { Button } from '@mui/material'; 7 | 8 | const MOCK_INFO = { 9 | name: { first: 'Nate', last: 'Wang' }, 10 | email: 'myemail@gmail.com', 11 | gender: 'Male', 12 | dateOfBirth: dayjs('2100-01-01'), 13 | phone: '15988888888', 14 | city: 'Shanghai', 15 | address: 'No.1000 Some Road, Zhangjiang Park, Pudong New District', 16 | }; 17 | const ViewEdit = () => { 18 | const [viewMode, setViewMode] = useState(true); 19 | const [personalInfo, setPersonalInfo] = useState(MOCK_INFO); 20 | const handleSubmit = useCallback( 21 | (values: typeof MOCK_INFO, { setSubmitting }: { setSubmitting: (value: boolean) => void }) => { 22 | console.log('Submit: ', values); 23 | 24 | setTimeout(() => { 25 | setSubmitting(false); 26 | setPersonalInfo(values); 27 | setViewMode(true); 28 | }, 1500); 29 | }, 30 | [], 31 | ); 32 | 33 | const getMeta = (form: FormikProps) => { 34 | const meta: FormikMuiNiceFormMeta = { 35 | form, 36 | columns: 2, 37 | disabled: form.isSubmitting, 38 | viewMode: viewMode, 39 | initialValues: personalInfo, 40 | rowGap: 20, 41 | columnGap: 20, 42 | fields: [ 43 | { 44 | key: 'name.first', 45 | name: ['name', 'first'], 46 | label: 'First Name', 47 | required: true, 48 | tooltip: 'hahahah', 49 | }, 50 | { key: 'name.last', label: 'Last Name', fullWidth: true, widget: 'text', required: true }, 51 | { key: 'gender', label: 'Gender', widget: 'radio-group', options: ['Male', 'Female'] }, 52 | { key: 'email', label: 'Email' }, 53 | { key: 'phone', label: 'Phone' }, 54 | { key: 'address', label: 'Address', colSpan: 2, clear: 'left' }, 55 | { key: 'city', label: 'City' }, 56 | { key: 'zipCode', label: 'Zip Code' }, 57 | 58 | { 59 | key: 'submit-button', 60 | clear: 'left', 61 | render: () => { 62 | return ( 63 | <> 64 | 67 | 74 | 75 | ); 76 | }, 77 | }, 78 | ], 79 | }; 80 | console.log(meta); 81 | return meta; 82 | }; 83 | 84 | return ( 85 |
86 |

87 | Personal Information 88 | {viewMode && ( 89 | 95 | )} 96 |

97 | 98 | {(form) => ( 99 |
100 | 101 | 102 | )} 103 |
104 |
105 | ); 106 | }; 107 | export default ViewEdit; 108 | -------------------------------------------------------------------------------- /packages/examples-formik/src/examples/ViewMode.tsx: -------------------------------------------------------------------------------- 1 | import dayjs from 'dayjs'; 2 | import type { Dayjs } from 'dayjs'; 3 | import NiceForm, { NiceFormMeta } from '@ebay/nice-form-react'; 4 | import { InputLabel } from '@mui/material'; 5 | import { ReactElement } from 'react'; 6 | 7 | const DateView = ({ value, label }: { value: Dayjs; label: ReactElement }) => { 8 | return ( 9 |
10 | {label} 11 |
{value.format('MMM Do YYYY')}
12 |
13 | ); 14 | }; 15 | 16 | const ViewMode = () => { 17 | const personalInfo = { 18 | name: { first: 'Nate', last: 'Wang' }, 19 | email: 'myemail@gmail.com', 20 | gender: 'Male', 21 | dateOfBirth: dayjs('2100-01-01'), 22 | phone: '15988888888', 23 | city: 'Shanghai', 24 | address: 25 | 'No.1000 Some Road, Zhangjiang Park, Pudong New District,Zhangjiang Park, Pudong New DistrictZhangjiang Park, Pudong New District', 26 | }; 27 | 28 | const meta: NiceFormMeta = { 29 | columns: 2, 30 | viewMode: true, 31 | initialValues: personalInfo, 32 | rowGap: 20, 33 | columnGap: 20, 34 | fields: [ 35 | { key: 'name.first', label: 'First Name', tooltip: 'First name' }, 36 | { key: 'name.last', label: 'Last Name' }, 37 | { key: 'gender', label: 'Gender' }, 38 | { 39 | key: 'dateOfBirth', 40 | label: 'Date of Birth', 41 | viewWidget: DateView, 42 | }, 43 | { key: 'email', label: 'Email' }, 44 | { key: 'phone', label: 'Phone' }, 45 | { key: 'address', label: 'Address', colSpan: 2 }, 46 | { key: 'city', label: 'City' }, 47 | { key: 'zipCode', label: 'Zip Code' }, 48 | ], 49 | }; 50 | 51 | return ( 52 |
53 |

Personal Information

54 | 55 |
56 | ); 57 | }; 58 | 59 | export default ViewMode; 60 | -------------------------------------------------------------------------------- /packages/examples-formik/src/index.css: -------------------------------------------------------------------------------- 1 | 2 | body { 3 | margin: 0; 4 | padding: 0; 5 | font-family: sans-serif; 6 | } 7 | 8 | 9 | 10 | a { 11 | text-decoration: none; 12 | color: #1890ff; 13 | } 14 | 15 | #root{ 16 | height: 100%; 17 | } 18 | 19 | -------------------------------------------------------------------------------- /packages/examples-formik/src/main.tsx: -------------------------------------------------------------------------------- 1 | import NiceForm, { config as niceFormConfig } from '@ebay/nice-form-react'; 2 | import formikAdapter from '@ebay/nice-form-react/adapters/formikAdapter'; 3 | import formikMuiAdapter from '@ebay/nice-form-react/adapters/formikMuiAdapter'; 4 | import { DatePicker } from '@mui/x-date-pickers/DatePicker'; 5 | import type { Dayjs } from 'dayjs'; 6 | import { fieldToDatePicker } from 'formik-mui-x-date-pickers'; 7 | import React from 'react'; 8 | import ReactDOM from 'react-dom/client'; 9 | import App from './App.tsx'; 10 | import './index.css'; 11 | 12 | const MyDatePicker = ({ ...props }) => { 13 | props.onChange = (value: Dayjs | null) => { 14 | props.form.setFieldTouched(props.field.name, true, false); 15 | props.form.setFieldValue(props.field.name, value, true); 16 | props.field.onChange(value); 17 | }; 18 | props.onBlur = () => { 19 | props.form.setFieldTouched(props.field.name, true, true); 20 | props.field.onBlur(); 21 | }; 22 | return ( 23 | 24 | {props.children} 25 | 26 | ); 27 | }; 28 | 29 | NiceForm.defineWidget('date-picker', MyDatePicker, ({ field }) => field); 30 | 31 | niceFormConfig.addAdapter(formikAdapter); 32 | niceFormConfig.addAdapter(formikMuiAdapter); 33 | 34 | ReactDOM.createRoot(document.getElementById('root')!).render( 35 | 36 | 37 | , 38 | ); 39 | -------------------------------------------------------------------------------- /packages/examples-formik/src/useHash.ts: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from 'react' 2 | 3 | const getHash = () => document.location.hash.replace('#', '') 4 | 5 | export default () => { 6 | const [hash, setHash] = useState(getHash()) 7 | useEffect(() => { 8 | function handleHashChange() { 9 | const hash = getHash() || 'basic' 10 | setHash(hash) 11 | window.scrollTo({ top: 0 }) 12 | } 13 | window.addEventListener('hashchange', handleHashChange) 14 | return () => { 15 | window.removeEventListener('hashchange', handleHashChange) 16 | } 17 | }, [setHash]) 18 | return hash 19 | } 20 | -------------------------------------------------------------------------------- /packages/examples-formik/src/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /packages/examples-formik/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2020", 4 | "useDefineForClassFields": true, 5 | "lib": ["ES2020", "DOM", "DOM.Iterable"], 6 | "module": "ESNext", 7 | "skipLibCheck": true, 8 | 9 | /* Bundler mode */ 10 | "moduleResolution": "bundler", 11 | "allowImportingTsExtensions": true, 12 | "resolveJsonModule": true, 13 | "isolatedModules": true, 14 | "noEmit": true, 15 | "jsx": "react-jsx", 16 | 17 | /* Linting */ 18 | "strict": true, 19 | "noUnusedLocals": true, 20 | "noUnusedParameters": true, 21 | "noFallthroughCasesInSwitch": true 22 | }, 23 | "include": ["src"], 24 | "references": [{ "path": "./tsconfig.node.json" }] 25 | } 26 | -------------------------------------------------------------------------------- /packages/examples-formik/tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "composite": true, 4 | "skipLibCheck": true, 5 | "module": "ESNext", 6 | "moduleResolution": "bundler", 7 | "allowSyntheticDefaultImports": true 8 | }, 9 | "include": ["vite.config.ts"] 10 | } 11 | -------------------------------------------------------------------------------- /packages/examples-formik/vite.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite'; 2 | import react from '@vitejs/plugin-react'; 3 | 4 | // https://vitejs.dev/config/ 5 | export default defineConfig({ 6 | base: '/nice-form-react/formik', 7 | plugins: [react()], 8 | build: { 9 | outDir: '../../gh-pages/formik', 10 | }, 11 | }); 12 | -------------------------------------------------------------------------------- /packages/nice-form-react/.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "trailingComma": "all", 4 | "arrowParens": "always", 5 | "printWidth": 100 6 | } 7 | -------------------------------------------------------------------------------- /packages/nice-form-react/README.md: -------------------------------------------------------------------------------- 1 | # nice-form-react 2 | A meta driven form helper for React. 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /packages/nice-form-react/babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: ['@babel/preset-react', '@babel/preset-typescript'], 3 | }; 4 | -------------------------------------------------------------------------------- /packages/nice-form-react/jest.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | 3 | module.exports = { 4 | testEnvironment: 'jsdom', 5 | roots: [path.resolve(__dirname, './tests')], 6 | coverageReporters: ['lcov', 'text', 'cobertura'], 7 | collectCoverageFrom: ['src/**/*.{ts,tsx,js,jsx}'], 8 | testMatch: ['**/tests/**/*.test.tsx'], 9 | setupFilesAfterEnv: [path.resolve(__dirname, './tests/setupAfterEnv.ts')], 10 | testTimeout: 10000, 11 | clearMocks: false, 12 | transform: { 13 | '.(js|jsx)': 'babel-jest', 14 | '.(ts|tsx)': 'ts-jest', 15 | }, 16 | transformIgnorePatterns: [`/node_modules/`, '/tests/*.{ts,tsx}'], 17 | }; 18 | -------------------------------------------------------------------------------- /packages/nice-form-react/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@ebay/nice-form-react", 3 | "private": false, 4 | "version": "2.0.3", 5 | "license": "MIT", 6 | "repository": "https://github.com/eBay/nice-form-react", 7 | "main": "./lib/cjs/index.js", 8 | "module": "./lib/esm/index.js", 9 | "types": "./lib/esm/index.d.ts", 10 | "exports": { 11 | "./package.json": "./package.json", 12 | "./*": { 13 | "types": "./lib/esm/*.d.ts", 14 | "import": "./lib/esm/*.js", 15 | "require": "./lib/cjs/*.js" 16 | }, 17 | ".": { 18 | "types": "./lib/esm/index.d.ts", 19 | "import": "./lib/esm/index.js", 20 | "require": "./lib/cjs/index.js" 21 | } 22 | }, 23 | "files": [ 24 | "lib", 25 | "src" 26 | ], 27 | "scripts": { 28 | "dev": "tsc -w --sourcemap", 29 | "gendoc": "typedoc", 30 | "build": "rm -rf lib && pnpm build:esm && pnpm build:cjs", 31 | "build:esm": "tsc --sourcemap", 32 | "build:cjs": "tsc --module commonjs --outDir lib/cjs --sourcemap", 33 | "test": "jest --ci --watchAll=false --passWithNoTests --coverage" 34 | }, 35 | "devDependencies": { 36 | "@babel/core": "^7.23.6", 37 | "@babel/preset-env": "^7.23.6", 38 | "@babel/preset-react": "^7.23.3", 39 | "@babel/preset-typescript": "^7.23.3", 40 | "@emotion/react": "^11.10.5", 41 | "@emotion/styled": "^11.10.5", 42 | "@material-ui/core": "^4.12.4", 43 | "@mui/lab": "^5.0.0-alpha.158", 44 | "@mui/material": "^5.11.7", 45 | "@mui/x-date-pickers": "^6.16.3", 46 | "@testing-library/jest-dom": "^6.1.5", 47 | "@testing-library/react": "^14.1.2", 48 | "@types/jest": "^29.5.11", 49 | "@types/react": "^18.0.26", 50 | "@types/react-dom": "^18.0.9", 51 | "antd": "^5.1.1", 52 | "babel-jest": "^29.7.0", 53 | "dayjs": "^1.11.7", 54 | "formik": "^2.4.5", 55 | "formik-mui": "^5.0.0-alpha.0", 56 | "formik-mui-x-date-pickers": "^0.0.1", 57 | "jest": "^29.7.0", 58 | "jest-environment-jsdom": "^29.7.0", 59 | "notistack": "^3.0.1", 60 | "react": "^18.2.0", 61 | "react-dom": "^18.2.0", 62 | "ts-jest": "^29.1.1", 63 | "typedoc": "^0.25.3", 64 | "typescript": "4.9.3" 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /packages/nice-form-react/public/vite.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /packages/nice-form-react/src/FormField.tsx: -------------------------------------------------------------------------------- 1 | import { CSSProperties } from 'react'; 2 | import config from './config'; 3 | import { NiceFormMeta, NiceFormField, NormalizedFormField } from './types'; 4 | 5 | /** 6 | * FormField only manages the layout of the field, and delegates the rendering of the field to the adapter. 7 | * 8 | * @param param0 9 | * @returns 10 | */ 11 | const FormField = ({ 12 | meta, 13 | field, 14 | }: { 15 | meta: NiceFormMeta; 16 | field: NiceFormField; 17 | }): React.ReactNode => { 18 | if ( 19 | field.condition && 20 | typeof field.condition === 'function' && 21 | !field.condition({ meta, field }) 22 | ) { 23 | return null; 24 | } 25 | 26 | if (field.render) { 27 | return field.render({ field: field as NormalizedFormField, meta }); 28 | } 29 | 30 | const viewMode = meta.viewMode || field.viewMode; 31 | 32 | const FormWidget = field.widget; 33 | const ViewWidget = field.viewWidget; 34 | const widgetProps = { ...field.widgetProps }; 35 | 36 | const { renderField, renderFieldWithoutLabel } = config; 37 | const content = renderField ? ( 38 | renderField({ 39 | meta, 40 | field: field as NormalizedFormField, 41 | }) 42 | ) : renderFieldWithoutLabel ? ( 43 | renderFieldWithoutLabel({ meta, field: field as NormalizedFormField }) 44 | ) : viewMode ? ( 45 | field.renderView ? ( 46 | field.renderView!(field.initialValue, { field, meta }) 47 | ) : ( 48 | //@ts-ignore 49 | 50 | ) 51 | ) : ( 52 | //@ts-ignore 53 | {field.children || null} 54 | ); 55 | 56 | if (renderField) return content; 57 | 58 | // If some field has label, then we need to set the label width 59 | const hasLabel = meta.fields.some((f) => f.label); 60 | 61 | const { layout = 'horizontal' } = meta; 62 | const isVertical = layout === 'vertical'; 63 | 64 | let labelWidth: string = ''; 65 | if (!isVertical && hasLabel) { 66 | labelWidth = String(field.labelWidth || meta.labelWidth || '33%'); 67 | if (labelWidth.endsWith('%')) { 68 | labelWidth = parseFloat(labelWidth) / (field.colSpan || 1) + '%'; 69 | } 70 | } 71 | 72 | const style = { 73 | display: 'grid', 74 | gridTemplateColumns: !isVertical && hasLabel ? `${labelWidth} 1fr` : '1fr', 75 | alignContent: 'center', 76 | }; 77 | 78 | let label = null; 79 | if (hasLabel) { 80 | const labelStyle: CSSProperties = { 81 | textAlign: viewMode || isVertical ? 'left' : 'right', 82 | marginRight: '8px', 83 | }; 84 | if (field.required) { 85 | Object.assign(labelStyle, { 86 | '::before': { 87 | content: '"*"', 88 | color: 'red', 89 | }, 90 | }); 91 | } 92 | label = ( 93 | 96 | ); 97 | } 98 | let help = null; 99 | 100 | // if renderFieldWithoutLabel method exists, it means the adapter has already handled the extra content 101 | if (!renderFieldWithoutLabel && field.help) { 102 | const helpStyle: CSSProperties = { 103 | transform: 'scale(0.7)', 104 | transformOrigin: 'left', 105 | opacity: 0.6, 106 | }; 107 | help = [ 108 | 109 | {field.help} 110 | , 111 | ]; 112 | if (hasLabel && !isVertical) { 113 | // an empty cell for just layout purpose 114 | help.unshift(); 115 | } 116 | } 117 | 118 | return ( 119 | 120 | {label} 121 | {content} 122 | {help} 123 | 124 | ); 125 | }; 126 | 127 | export default FormField; 128 | -------------------------------------------------------------------------------- /packages/nice-form-react/src/FormLayout.tsx: -------------------------------------------------------------------------------- 1 | import { CSSProperties, ReactNode } from 'react'; 2 | import { NiceFormMeta, NiceFormField } from './types'; 3 | 4 | /** 5 | * The layout component of NiceForm based on css grid. It's only used internally. 6 | * @param param0 7 | * @returns 8 | */ 9 | const FormLayout: React.FC<{ 10 | elements: { element: ReactNode; field: NiceFormField }[]; 11 | meta: NiceFormMeta; 12 | }> = ({ elements, meta }) => { 13 | const { columns = 1 } = meta; 14 | 15 | const style = { 16 | display: 'grid', 17 | gridTemplateColumns: `repeat(${columns}, 1fr)`, 18 | gridColumnGap: meta.columnGap ?? 0, 19 | gridRowGap: meta.rowGap ?? 0, 20 | }; 21 | 22 | let currentColStart = 0; 23 | return ( 24 |
25 | {elements.map(({ element, field }) => { 26 | const colSpan = field.colSpan || 1; 27 | if (field.clear && ['left', 'both'].includes(field.clear)) { 28 | currentColStart = 0; 29 | } 30 | currentColStart = currentColStart % columns; 31 | const fieldStyle: CSSProperties = { 32 | gridColumn: `${currentColStart + 1} / span ${colSpan}`, 33 | position: 'relative', 34 | }; 35 | if (field.clear && ['both', 'right'].includes(field.clear)) { 36 | currentColStart = 0; 37 | } else { 38 | currentColStart += colSpan; 39 | } 40 | return ( 41 |
42 | {element} 43 | {field.extraNode || null} 44 |
45 | ); 46 | })} 47 |
48 | ); 49 | }; 50 | export default FormLayout; 51 | -------------------------------------------------------------------------------- /packages/nice-form-react/src/adapters/formikAdapter.tsx: -------------------------------------------------------------------------------- 1 | import { ReactNode } from 'react'; 2 | import { Field, FastField, FieldProps, FormikHandlers, FieldInputProps, FieldConfig } from 'formik'; 3 | import { NiceFormAdapter, NormalizedFormField } from '../types'; 4 | import { niceFormFieldStdKeys, without } from '../utils'; 5 | 6 | export interface FormikWidgetProps extends Record { 7 | onChange?: FormikHandlers['handleChange']; 8 | onBlur?: FormikHandlers['handleBlur']; 9 | children?: ReactNode; 10 | disabled?: boolean; 11 | label?: ReactNode; 12 | fullWidth?: boolean; 13 | } 14 | 15 | // each formik widget will be passed with { field, meta } props 16 | const formikAdapter: NiceFormAdapter = { 17 | metaConverter: (meta) => { 18 | return { 19 | ...meta, 20 | fields: meta.fields.map((f: NormalizedFormField) => { 21 | // Note: formik doesn't support mixed dot and nested object: {'a.b': {c: 1} } => ['a.b', 'c'] 22 | return { 23 | ...f, 24 | name: Array.isArray(f.name) 25 | ? f.key.startsWith('!!!') 26 | ? `['${f.name.join("', '")}']` 27 | : f.name.join('.') 28 | : f.name, 29 | }; 30 | }), 31 | }; 32 | }, 33 | renderField: ({ field, meta }) => { 34 | const FormWidget = field.widget; 35 | const ViewWidget = field.viewWidget; 36 | const viewMode = meta.viewMode || field.viewMode; 37 | 38 | const wrapperProps: FieldConfig = { 39 | ...meta.wrapperProps, 40 | ...without(field, niceFormFieldStdKeys), 41 | name: field.name as string, // ensured by metaConverter 42 | }; 43 | 44 | const widgetProps: FormikWidgetProps = { 45 | ...field.widgetProps, 46 | }; 47 | 48 | if (viewMode) { 49 | return field.renderView ? ( 50 | field.renderView(field.initialValue, { field, meta }) 51 | ) : ( 52 | 58 | ); 59 | } 60 | 61 | // Allows onChange, onBlur events on widgetProps 62 | const widgetOnChange = widgetProps?.onChange; 63 | const widgetOnBlur = widgetProps?.onBlur; 64 | delete widgetProps.onChange; 65 | delete widgetProps.onBlur; 66 | 67 | const FieldComp = field.fast ? FastField : Field; 68 | return ( 69 | 70 | {({ field, form, meta }: FieldProps) => { 71 | const newFormikField: FieldInputProps = { ...field }; 72 | 73 | // allow onChange, onBlur to be called by widgetProps 74 | if (widgetOnChange) { 75 | newFormikField.onChange = (e: any) => { 76 | field.onChange(e); 77 | widgetOnChange(e); 78 | }; 79 | } 80 | if (widgetOnBlur) { 81 | newFormikField.onBlur = (e: any) => { 82 | field.onBlur(e); 83 | widgetOnBlur(e); 84 | }; 85 | } 86 | return ; 87 | }} 88 | 89 | ); 90 | }, 91 | }; 92 | 93 | export default formikAdapter; 94 | -------------------------------------------------------------------------------- /packages/nice-form-react/src/config.tsx: -------------------------------------------------------------------------------- 1 | import { NiceFormConfig, ReactComponent } from './types'; 2 | 3 | const DefaultViewWidget: ReactComponent = ({ value }) => { 4 | if (value === null || value === undefined) return 'N/A'; 5 | return String(value); 6 | }; 7 | 8 | const isHtmlTag = {}; 9 | const config: NiceFormConfig = { 10 | defaultWidget: 'input', 11 | defaultViewWidget: DefaultViewWidget, 12 | widgetMap: {}, 13 | metaConverters: [], 14 | adapters: [], 15 | addAdapter(adapter) { 16 | config.adapters.push(adapter); 17 | Object.assign(config.widgetMap, adapter.widgetMap); 18 | if (adapter.metaConverter) config.metaConverters.push(adapter.metaConverter); 19 | if (adapter.renderField) config.renderField = adapter.renderField; 20 | if (adapter.renderFieldWithoutLabel) { 21 | config.renderFieldWithoutLabel = adapter.renderFieldWithoutLabel; 22 | } 23 | 24 | if (adapter.defaultWidget) { 25 | console.log('set default widget'); 26 | config.defaultWidget = adapter.defaultWidget; 27 | } 28 | if (adapter.defaultViewWidget) { 29 | config.defaultViewWidget = adapter.defaultViewWidget; 30 | } 31 | }, 32 | defineWidget(name, widget, metaConverter) { 33 | this.widgetMap[name] = { 34 | widget, 35 | metaConverter, 36 | }; 37 | }, 38 | 39 | /** 40 | * 41 | * @param widget 42 | * @returns Get widget definition from widget name or widget definition 43 | */ 44 | getWidgetDef(widget) { 45 | if (!widget) return { widget: 'input' }; 46 | if (typeof widget === 'string') { 47 | // if widget is a string, find it from widget map 48 | const def = this.widgetMap?.[widget]; 49 | if (!def) { 50 | // check if it's a native HTML tag 51 | if (!isHtmlTag.hasOwnProperty(widget)) { 52 | try { 53 | const elStr = document.createElement(widget).toString(); 54 | isHtmlTag[widget] = !['[object HTMLElement]', '[object HTMLUnknownElement]'].includes( 55 | elStr, 56 | ); 57 | } catch (err) { 58 | isHtmlTag[widget] = false; 59 | } 60 | } 61 | if (isHtmlTag[widget]) return { widget }; 62 | throw new Error(`Widget '${widget}' not defined. Did you define it?`); 63 | } 64 | return def; 65 | } 66 | 67 | // If widget is a component, just return it 68 | return { widget }; 69 | }, 70 | }; 71 | 72 | export default config; 73 | -------------------------------------------------------------------------------- /packages/nice-form-react/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './types'; 2 | export { default as config } from './config'; 3 | export { default } from './NiceForm'; 4 | -------------------------------------------------------------------------------- /packages/nice-form-react/src/utils.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Delete properties from object, return new object. 3 | * @param obj 4 | * @param arr 5 | * @returns 6 | */ 7 | export const without = (obj: Record, arr: string[]) => { 8 | const newObj: Record = {}; 9 | Object.keys(obj).forEach((p) => { 10 | if (arr.includes(p)) return; 11 | newObj[p] = obj[p]; 12 | }); 13 | return newObj; 14 | }; 15 | 16 | /** 17 | * Check if a value is undefined. 18 | * @param value 19 | * @returns 20 | */ 21 | export const isUndefined = (value: any) => typeof value === 'undefined'; 22 | 23 | /** 24 | * Get value from object by path. The path could be an array or a string 25 | * separated by dot. 26 | * @param obj 27 | * @param prop 28 | * @returns 29 | */ 30 | export const get = (obj: Record, prop: string | string[]) => { 31 | const arr = Array.isArray(prop) ? prop : prop.split('.'); 32 | for (let i = 0; i < arr.length; i++) { 33 | if (typeof obj !== 'object' || obj === null) return undefined; 34 | if (!(arr[i] in obj)) return undefined; 35 | obj = obj[arr[i]]; 36 | } 37 | return obj; 38 | }; 39 | 40 | /** 41 | * Set value to object by path. The path could be an array or a string separated by dot. 42 | * @param obj 43 | * @param prop 44 | * @returns 45 | */ 46 | export const has = (obj: Object, prop: string | string[]) => { 47 | let arr: Array; 48 | if (!Array.isArray(prop) && !prop.startsWith('!!!')) arr = prop.split('.'); 49 | else if (typeof prop === 'string') arr = [prop]; 50 | else arr = prop; 51 | 52 | return ( 53 | arr.reduce((o, p) => { 54 | return o && o.hasOwnProperty(p) ? o[p] : undefined; 55 | }, obj) !== undefined 56 | ); 57 | }; 58 | 59 | /** 60 | * Normalize options to { value, label } format. The options could be an array of 61 | * string, array or object. 62 | * 63 | * For example: 64 | * ['a', 'b'] => [{ value: 'a', label: 'a' }, { value: 'b', label: 'b' }] 65 | * [['a', 'A'], ['b', 'B']] => [{ value: 'a', label: 'A' }, { value: 'b', label: 'B' }] 66 | * [{ value: 'a', label: 'A' }, { value: 'b', label: 'B' }] => [{ value: 'a', label: 'A' }, { value: 'b', label: 'B' }] 67 | * @param options 68 | * @returns 69 | */ 70 | export const normalizeOptions = (options: any[]) => { 71 | if (!Array.isArray(options)) { 72 | throw new Error('Options should be array in form builder meta.'); 73 | } 74 | return options.map((opt) => { 75 | if (Array.isArray(opt)) { 76 | return { value: opt[0], label: opt[1] }; 77 | } else if (typeof opt === 'object') { 78 | return opt; 79 | } else { 80 | return { value: opt, label: opt }; 81 | } 82 | }); 83 | }; 84 | 85 | /** 86 | * The standard keys for form field meta. These keys are handled by nice form itself. 87 | * Other fields are handled by adapters. 88 | */ 89 | export const niceFormFieldStdKeys = [ 90 | 'key', 91 | 'name', 92 | 'label', 93 | 'help', 94 | 'required', 95 | 'disabled', 96 | 'extraNode', 97 | 'fullWidth', 98 | 'initialValue', 99 | 'options', 100 | 'colSpan', 101 | 'rowSpan', 102 | 'viewMode', 103 | 'children', 104 | 'clear', 105 | 'render', 106 | 'renderView', 107 | 'widget', 108 | 'widgetProps', 109 | 'viewWidget', 110 | 'viewWidgetProps', 111 | 'wrapperProps', 112 | 'condition', 113 | ]; 114 | -------------------------------------------------------------------------------- /packages/nice-form-react/tests/__mocks__/fileMock.js: -------------------------------------------------------------------------------- 1 | // __mocks__/fileMock.js 2 | 3 | module.exports = 'test-file-stub'; 4 | -------------------------------------------------------------------------------- /packages/nice-form-react/tests/__mocks__/matchMedia.js: -------------------------------------------------------------------------------- 1 | Object.defineProperty(window, 'matchMedia', { 2 | value: () => { 3 | return { 4 | matches: false, 5 | addListener: () => {}, 6 | removeListener: () => {}, 7 | addEventListener: () => {}, 8 | removeEventListener: () => {}, 9 | dispatchEvent: () => {}, 10 | }; 11 | }, 12 | }); 13 | -------------------------------------------------------------------------------- /packages/nice-form-react/tests/__mocks__/styleMock.js: -------------------------------------------------------------------------------- 1 | // __mocks__/styleMock.js 2 | 3 | module.exports = {}; 4 | -------------------------------------------------------------------------------- /packages/nice-form-react/tests/antd/AsyncDataSource.test.tsx: -------------------------------------------------------------------------------- 1 | import React, { useCallback, useState, useEffect } from 'react'; 2 | import { Form, Button } from 'antd'; 3 | import NiceForm from '../../src/NiceForm'; 4 | import config from '../../src/config'; 5 | import { render, screen } from '@testing-library/react'; 6 | import antdAdapter, { AntdNiceFormMeta } from '../../src/adapters/antdAdapter'; 7 | import '@testing-library/jest-dom'; 8 | 9 | const MOCK_DATA: { 10 | [key: string]: string[]; 11 | } = { 12 | China: ['Beijing', 'Shanghai', 'Nanjing'], 13 | USA: ['New York', 'San Jose', 'Washton'], 14 | France: ['Paris', 'Marseille', 'Cannes'], 15 | }; 16 | 17 | // Mock fetch 18 | const fetchCities = (country: string): Promise => { 19 | return new Promise((resolve, reject) => { 20 | setTimeout(() => { 21 | if (MOCK_DATA[country]) resolve(MOCK_DATA[country]); 22 | else reject(new Error('Not found')); 23 | }, 1500); 24 | }); 25 | }; 26 | 27 | const AsyncDataSource = () => { 28 | const [form] = Form.useForm(); 29 | const [cities, setCities] = useState<{ [key: string]: string }>({}); 30 | const updateOnChange = NiceForm.useUpdateOnChange(['country']); 31 | const country = form.getFieldValue('country'); 32 | const loading = country && !cities[country]; 33 | 34 | const meta: AntdNiceFormMeta = { 35 | fields: [ 36 | { 37 | key: 'country', 38 | label: 'Country', 39 | widget: 'select', 40 | options: ['China', 'USA', 'France'], 41 | placeholder: 'Select country...', 42 | initialValue: 'China', 43 | widgetProps: { 44 | onChange: () => { 45 | // Clear city value when country is changed 46 | form.setFieldsValue({ city: undefined }); 47 | }, 48 | }, 49 | }, 50 | { 51 | key: 'city', 52 | label: 'City', 53 | widget: 'select', 54 | options: country 55 | ? (cities[country] as unknown as any[] | undefined) || ([] as any[] | undefined) 56 | : ([] as any[] | undefined), 57 | placeholder: loading ? 'Loading...' : 'Select city...', 58 | widgetProps: { loading }, 59 | disabled: loading || !country, 60 | }, 61 | ], 62 | }; 63 | 64 | const handleFinish = useCallback((values: any) => { 65 | console.log('Submit: ', values); 66 | }, []); 67 | 68 | useEffect(() => { 69 | if (country && !cities[country]) { 70 | fetchCities(country).then((arr) => { 71 | setCities((p) => ({ ...p, [country]: arr })); 72 | }); 73 | } 74 | }, [country, setCities, cities]); 75 | 76 | // If country selected but no cities in store, then it's loading 77 | return ( 78 |
79 | 80 | 81 | 84 | 85 | 86 | ); 87 | }; 88 | 89 | describe('antd/AsyncDataSource', () => { 90 | config.addAdapter(antdAdapter); 91 | 92 | it('renders AsyncDataSource Form using Antd', () => { 93 | render(); 94 | const countryField = screen.getByLabelText('Country'); 95 | expect(countryField).toBeInTheDocument(); 96 | }); 97 | }); 98 | -------------------------------------------------------------------------------- /packages/nice-form-react/tests/antd/Basic.test.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Form, Button, Rate } from 'antd'; 3 | import NiceForm from '../../src/NiceForm'; 4 | import config from '../../src/config'; 5 | import { render, screen } from '@testing-library/react'; 6 | import antdAdapter, { AntdNiceFormMeta } from '../../src/adapters/antdAdapter'; 7 | import '@testing-library/jest-dom'; 8 | 9 | const Basic = () => { 10 | const [form] = Form.useForm(); 11 | const updateOnChange = NiceForm.useUpdateOnChange(['checkbox']); 12 | const options = ['Apple', 'Orange', 'Banana']; 13 | const meta: AntdNiceFormMeta = { 14 | columns: 1, 15 | initialValues: { obj: { input: 12 } }, 16 | layout: 'horizontal', 17 | wrapperProps: { 18 | labelCol: { 19 | span: 8, 20 | }, 21 | }, 22 | fields: [ 23 | { 24 | key: 'obj.input', 25 | name: ['obj', 'input'], 26 | label: 'Input', 27 | required: true, 28 | tooltip: 'Name', 29 | help: 'Name', 30 | }, 31 | { 32 | key: 'checkbox', 33 | label: 'Checkbox', 34 | widget: 'checkbox', 35 | initialValue: true, 36 | }, 37 | { 38 | key: 'rating', 39 | label: 'Rating', 40 | widget: Rate, 41 | initialValue: 2, 42 | condition: () => { 43 | return NiceForm.getFieldValue('checkbox', meta, form); 44 | }, 45 | }, 46 | { key: 'switch', label: 'Switch', widget: 'switch', initialValue: true }, 47 | { 48 | key: 'select', 49 | label: 'Select', 50 | widget: 'select', 51 | required: true, 52 | options, 53 | }, 54 | { 55 | key: 'checkbox-group', 56 | label: 'Checkbox Group', 57 | widget: 'checkbox-group', 58 | options, 59 | }, 60 | { 61 | key: 'radio-group', 62 | label: 'Radio Group', 63 | widget: 'radio-group', 64 | options, 65 | }, 66 | { 67 | key: 'radio-button-group', 68 | label: 'Radio Button Group', 69 | widget: 'radio-group', 70 | widgetProps: { 71 | optionType: 'button', 72 | buttonStyle: 'solid', 73 | }, 74 | options, 75 | }, 76 | { 77 | key: 'password', 78 | label: 'Password', 79 | widget: 'password', 80 | required: true, 81 | rules: [{ required: true, message: 'password is required' }], 82 | }, 83 | { key: 'textarea', label: 'Textarea', widget: 'textarea' }, 84 | { key: 'number', label: 'Number', widget: 'number', fullWidth: true }, 85 | { key: 'date-picker', label: 'Date Picker', widget: 'date-picker', fullWidth: true }, 86 | ], 87 | }; 88 | 89 | const handleFinish = (values: any) => { 90 | form.validateFields().then(() => { 91 | console.log('on finish: ', values); 92 | }); 93 | }; 94 | return ( 95 |
96 | 97 | 98 | 101 | 102 | 103 | ); 104 | }; 105 | 106 | describe('antd/BasicTest', () => { 107 | config.addAdapter(antdAdapter); 108 | 109 | it('renders Basic Nice Form using Antd', () => { 110 | render(); 111 | const inputField = screen.getByLabelText('Input'); 112 | expect(inputField).toBeInTheDocument(); 113 | }); 114 | }); 115 | -------------------------------------------------------------------------------- /packages/nice-form-react/tests/antd/ComplexLayout.test.tsx: -------------------------------------------------------------------------------- 1 | import React, { useCallback } from 'react'; 2 | import { Form, Button } from 'antd'; 3 | import NiceForm from '../../src/NiceForm'; 4 | import config from '../../src/config'; 5 | import { render, screen } from '@testing-library/react'; 6 | import antdAdapter, { AntdNiceFormMeta } from '../../src/adapters/antdAdapter'; 7 | import '@testing-library/jest-dom'; 8 | 9 | const ComplexLayout = () => { 10 | const [form] = Form.useForm(); 11 | const handleFinish = useCallback((values: any) => { 12 | console.log('Submit: ', values); 13 | }, []); 14 | const meta: AntdNiceFormMeta = { 15 | columns: 4, 16 | layout: 'vertical', // Must set for vertical layout 17 | columnGap: 12, 18 | fields: [ 19 | { 20 | key: 'label1', 21 | colSpan: 4, 22 | render() { 23 | return ( 24 |
25 | Contact Infomation 26 |
27 | ); 28 | }, 29 | }, 30 | { key: 'address', label: 'Address', colSpan: 4 }, 31 | { key: 'address2', label: 'Address2', colSpan: 4 }, 32 | { key: 'city', label: 'City', colSpan: 2 }, 33 | { key: 'state', label: 'State' }, 34 | { key: 'zip', label: 'Zip Code' }, 35 | { 36 | key: 'label11', 37 | colSpan: 4, 38 | render() { 39 | return ( 40 |
41 | Bed & Bath 42 |
43 | ); 44 | }, 45 | }, 46 | { 47 | key: 'homeType', 48 | label: 'Home Type', 49 | colSpan: 2, 50 | widget: 'select', 51 | initialValue: 'House', 52 | options: ['House', 'Apartment'], 53 | }, 54 | { 55 | key: 'roomType', 56 | label: 'Room Type', 57 | colSpan: 2, 58 | widget: 'select', 59 | initialValue: 'Entire home/apt', 60 | options: ['Entire home/apt', 'Shared'], 61 | }, 62 | { 63 | key: 'bedrooms', 64 | label: 'Bedrooms', 65 | colSpan: 2, 66 | widget: 'select', 67 | options: ['1 Bedroom', '2 Bedrooms'], 68 | }, 69 | { 70 | key: 'bathrooms', 71 | label: 'Bathrooms', 72 | colSpan: 2, 73 | widget: 'select', 74 | options: ['1 Bathroom', '2 Bathrooms'], 75 | }, 76 | { 77 | key: 'king', 78 | label: 'King', 79 | widget: 'number', 80 | widgetProps: { style: { width: '100%' } }, 81 | initialValue: 0, 82 | }, 83 | { 84 | key: 'queen', 85 | label: 'Queen', 86 | widget: 'number', 87 | widgetProps: { style: { width: '100%' } }, 88 | initialValue: 0, 89 | }, 90 | { 91 | key: 'full', 92 | label: 'Full', 93 | widget: 'number', 94 | widgetProps: { style: { width: '100%' } }, 95 | initialValue: 0, 96 | }, 97 | { 98 | key: 'twin', 99 | label: 'Twin', 100 | widget: 'number', 101 | widgetProps: { style: { width: '100%' } }, 102 | initialValue: 0, 103 | }, 104 | ], 105 | }; 106 | 107 | return ( 108 |
109 | 110 | 111 | 114 | 115 | 116 | ); 117 | }; 118 | 119 | describe('antd/ComplexLayout', () => { 120 | config.addAdapter(antdAdapter); 121 | 122 | it('renders ComplexLayout Nice Form using Antd', () => { 123 | render(); 124 | const inputAddress = screen.getByLabelText('Address'); 125 | expect(inputAddress).toBeInTheDocument(); 126 | }); 127 | }); 128 | -------------------------------------------------------------------------------- /packages/nice-form-react/tests/antd/Coordinated.test.tsx: -------------------------------------------------------------------------------- 1 | import React, { useCallback } from 'react'; 2 | import { Form, Button } from 'antd'; 3 | import type { RadioChangeEvent } from 'antd'; 4 | import NiceForm from '../../src/NiceForm'; 5 | import config from '../../src/config'; 6 | import { render, screen } from '@testing-library/react'; 7 | import antdAdapter, { AntdNiceFormMeta } from '../../src/adapters/antdAdapter'; 8 | import '@testing-library/jest-dom'; 9 | 10 | const Coordinated = () => { 11 | const [form] = Form.useForm(); 12 | const handleFinish = useCallback((values: any) => { 13 | console.log('Submit: ', values); 14 | }, []); 15 | 16 | const meta: AntdNiceFormMeta = { 17 | fields: [ 18 | { 19 | key: 'gender', 20 | label: 'Gender', 21 | widget: 'radio-group', 22 | options: ['Male', 'Female'], 23 | onChange: (evt: RadioChangeEvent) => { 24 | if (evt.target.value === 'Male') { 25 | form.setFieldsValue({ note: 'Hi, man!' }); 26 | } else { 27 | form.setFieldsValue({ note: 'Hi, lady!' }); 28 | } 29 | }, 30 | }, 31 | { key: 'note', label: 'Note' }, 32 | ], 33 | }; 34 | 35 | return ( 36 |
37 | 38 | 39 | 42 | 43 | 44 | ); 45 | }; 46 | 47 | describe('antd/Coordinated', () => { 48 | config.addAdapter(antdAdapter); 49 | 50 | it('renders Coordinated Nice Form using Antd', () => { 51 | render(); 52 | const inputGender = screen.getByText('Gender'); 53 | expect(inputGender).toBeInTheDocument(); 54 | }); 55 | }); 56 | -------------------------------------------------------------------------------- /packages/nice-form-react/tests/antd/DynamicFields.test.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Form, Button } from 'antd'; 3 | import NiceForm from '../../src/NiceForm'; 4 | import config from '../../src/config'; 5 | import { render, screen } from '@testing-library/react'; 6 | import antdAdapter, { AntdNiceFormMeta } from '../../src/adapters/antdAdapter'; 7 | import '@testing-library/jest-dom'; 8 | 9 | const DynamicFields = () => { 10 | const [form] = Form.useForm(); 11 | Form.useWatch('favoriteFruit', form); 12 | const handleFinish = (values: unknown) => { 13 | console.log('Submit: ', values); 14 | }; 15 | const meta: AntdNiceFormMeta = { 16 | fields: [ 17 | { 18 | key: 'favoriteFruit', 19 | label: 'Favorite Fruit', 20 | widget: 'radio-group', 21 | options: ['Apple', 'Orange', 'Other'], 22 | initialValue: 'Apple', 23 | }, 24 | ], 25 | }; 26 | 27 | // Push other input if choose others 28 | if (NiceForm.getFieldValue('favoriteFruit', meta, form) === 'Other') { 29 | meta.fields.push({ 30 | key: 'otherFruit', 31 | label: 'Other', 32 | }); 33 | } 34 | 35 | return ( 36 |
37 | 38 | 39 | 42 | 43 | 44 | ); 45 | }; 46 | 47 | describe('antd/DynamicFields', () => { 48 | config.addAdapter(antdAdapter); 49 | 50 | it('renders DynamicFields Nice Form using Antd', () => { 51 | render(); 52 | const inputFavoriteFruit = screen.getByText('Favorite Fruit'); 53 | expect(inputFavoriteFruit).toBeInTheDocument(); 54 | }); 55 | }); 56 | -------------------------------------------------------------------------------- /packages/nice-form-react/tests/antd/FieldCondition.test.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Form, Button } from 'antd'; 3 | import NiceForm from '../../src/NiceForm'; 4 | import config from '../../src/config'; 5 | import { render, screen } from '@testing-library/react'; 6 | import antdAdapter, { AntdNiceFormMeta } from '../../src/adapters/antdAdapter'; 7 | import '@testing-library/jest-dom'; 8 | 9 | const FieldCondition = () => { 10 | const [form] = Form.useForm(); 11 | const favoriteFruit = Form.useWatch('favoriteFruit', form); 12 | const handleFinish = (values: unknown) => { 13 | console.log('Submit: ', values); 14 | }; 15 | const meta: AntdNiceFormMeta = { 16 | fields: [ 17 | { 18 | key: 'favoriteFruit', 19 | label: 'Favorite Fruit', 20 | widget: 'radio-group', 21 | options: ['Apple', 'Orange', 'Other'], 22 | initialValue: 'Apple', 23 | }, 24 | { 25 | key: 'otherFruit', 26 | label: 'Other', 27 | condition: () => favoriteFruit === 'Other', 28 | }, 29 | ], 30 | }; 31 | 32 | return ( 33 |
34 | 35 | 36 | 39 | 40 | 41 | ); 42 | }; 43 | 44 | describe('antd/FieldCondition', () => { 45 | config.addAdapter(antdAdapter); 46 | 47 | it('renders FieldCondition Nice Form using Antd', () => { 48 | render(); 49 | const inputFavoriteFruit = screen.getByText('Favorite Fruit'); 50 | expect(inputFavoriteFruit).toBeInTheDocument(); 51 | }); 52 | }); 53 | -------------------------------------------------------------------------------- /packages/nice-form-react/tests/antd/FormInModal.test.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useCallback } from 'react'; 2 | import { Form, Button, Modal } from 'antd'; 3 | import NiceForm from '../../src/NiceForm'; 4 | import config from '../../src/config'; 5 | import { render, screen, within, act } from '@testing-library/react'; 6 | import antdAdapter from '../../src/adapters/antdAdapter'; 7 | import '@testing-library/jest-dom'; 8 | 9 | const FormInModal = () => { 10 | const [form] = Form.useForm(); 11 | const [modalOpen, setModalOpen] = useState(false); 12 | const showModal = useCallback(() => setModalOpen(true), [setModalOpen]); 13 | const hideModal = useCallback(() => setModalOpen(false), [setModalOpen]); 14 | const [pending, setPending] = useState(false); 15 | const handleFinish = useCallback( 16 | (values: any) => { 17 | setPending(true); 18 | console.log('submit: ', values); 19 | setTimeout(() => { 20 | setPending(false); 21 | Modal.success({ title: 'Success', content: 'Submit success.', onOk: hideModal }); 22 | }, 2000); 23 | }, 24 | [setPending, hideModal], 25 | ); 26 | 27 | const meta = { 28 | disabled: pending, 29 | fields: [ 30 | { key: 'name', label: 'Name', required: true }, 31 | { key: 'desc', label: 'Description' }, 32 | ], 33 | }; 34 | 35 | return ( 36 |
37 | 40 | form.submit()} 47 | onCancel={hideModal} 48 | okText={pending ? 'Loading...' : 'Ok'} 49 | okButtonProps={{ loading: pending, disabled: pending }} 50 | cancelButtonProps={{ disabled: pending }} 51 | > 52 |
53 | 54 | 55 |
56 |
57 | ); 58 | }; 59 | 60 | describe('antd/FormInModal', () => { 61 | config.addAdapter(antdAdapter); 62 | 63 | it('renders FormInModal Nice Form using Antd', async () => { 64 | render(); 65 | 66 | // click on button to open modal 67 | const button = screen.getByRole('button', { name: /New\sItem/ }); 68 | expect(button).toBeTruthy(); 69 | 70 | act(() => { 71 | button.click(); 72 | }); 73 | 74 | const modalDialog = await screen.findByRole('dialog'); 75 | expect(modalDialog).toBeInTheDocument(); 76 | const inputName = within(modalDialog).getByLabelText('Name'); 77 | expect(inputName).toBeInTheDocument(); 78 | }); 79 | }); 80 | -------------------------------------------------------------------------------- /packages/nice-form-react/tests/antd/FormList.test.tsx: -------------------------------------------------------------------------------- 1 | import React, { useCallback } from 'react'; 2 | import { Form, Button } from 'antd'; 3 | import NiceForm from '../../src/NiceForm'; 4 | import config from '../../src/config'; 5 | import { render, screen } from '@testing-library/react'; 6 | import antdAdapter from '../../src/adapters/antdAdapter'; 7 | import '@testing-library/jest-dom'; 8 | 9 | const FormList = () => { 10 | const meta = { 11 | layout: 'horizontal', 12 | columns: 1, 13 | initialValues: { 14 | username: 'username', 15 | items: [''], 16 | }, 17 | fields: [ 18 | { key: 'username', label: 'User Name' }, 19 | { key: 'items', label: 'Items', widget: 'form-list' }, 20 | { 21 | key: 'cities', 22 | label: 'Cities', 23 | widget: 'form-list', 24 | listItemMeta: { 25 | widget: 'select', 26 | options: ['Beijing', 'Shanghai', 'Nanjing'], 27 | }, 28 | }, 29 | ], 30 | }; 31 | 32 | const handleFinish = useCallback((values: any) => { 33 | console.log('Submit: ', values); 34 | }, []); 35 | 36 | return ( 37 |
38 | 39 | 40 | 41 | 44 | 45 | 46 | ); 47 | }; 48 | 49 | describe('antd/FormList', () => { 50 | config.addAdapter(antdAdapter); 51 | 52 | it('renders FormList Nice Form using Antd', () => { 53 | render(); 54 | const inputItems = screen.getByText('Items'); 55 | expect(inputItems).toBeInTheDocument(); 56 | }); 57 | }); 58 | -------------------------------------------------------------------------------- /packages/nice-form-react/tests/antd/FormListManual.test.tsx: -------------------------------------------------------------------------------- 1 | import React, { ReactNode, useCallback } from 'react'; 2 | import { Form, Button } from 'antd'; 3 | import NiceForm from '../../src/NiceForm'; 4 | import config from '../../src/config'; 5 | import { render, screen } from '@testing-library/react'; 6 | import antdAdapter from '../../src/adapters/antdAdapter'; 7 | import '@testing-library/jest-dom'; 8 | 9 | const FormListManual = () => { 10 | const meta = { 11 | layout: 'horizontal', 12 | columns: 1, 13 | initialValues: { 14 | username: 'username', 15 | items: ['ddd', 'xxx'], 16 | }, 17 | fields: [ 18 | { key: 'username', label: 'User Name' }, 19 | { key: 'password', label: 'Password', widget: 'password' }, 20 | { 21 | key: 'items', 22 | label: 'Items', 23 | widget: Form.List, 24 | widgetProps: { name: 'items' }, 25 | children: ( 26 | fields: any[], 27 | { add, remove }: { add: Function; remove: Function }, 28 | ): ReactNode => { 29 | return ( 30 | <> 31 | { 34 | return { 35 | ...field, 36 | name: [field.name], 37 | style: { 38 | marginBottom: '10px', 39 | }, 40 | extraNode: ( 41 | <> 42 | {fields.length > 1 ? ( 43 |
remove(field.name)} 52 | /> 53 | ) : null} 54 | 55 | ), 56 | }; 57 | }), 58 | }} 59 | /> 60 | 63 | 64 | ); 65 | }, 66 | }, 67 | ], 68 | }; 69 | 70 | const handleFinish = useCallback((values: any) => { 71 | console.log('Submit: ', values); 72 | }, []); 73 | 74 | return ( 75 |
76 | 77 | 78 | 81 | 82 | 83 | ); 84 | }; 85 | 86 | describe('antd/FormListManual', () => { 87 | config.addAdapter(antdAdapter); 88 | 89 | it('renders FormListManual Nice Form using Antd', () => { 90 | render(); 91 | const inputPwd = screen.getByLabelText('Password'); 92 | expect(inputPwd).toBeInTheDocument(); 93 | }); 94 | }); 95 | -------------------------------------------------------------------------------- /packages/nice-form-react/tests/antd/Mixed.test.tsx: -------------------------------------------------------------------------------- 1 | import React, { useCallback } from 'react'; 2 | import { Form, Input, Button } from 'antd'; 3 | import NiceForm from '../../src/NiceForm'; 4 | import config from '../../src/config'; 5 | import { render, screen } from '@testing-library/react'; 6 | import antdAdapter from '../../src/adapters/antdAdapter'; 7 | import '@testing-library/jest-dom'; 8 | 9 | const Mixed = () => { 10 | const [form] = Form.useForm(); 11 | const handleFinish = useCallback((values: any) => console.log('Submit: ', values), []); 12 | const meta1 = { 13 | fields: [ 14 | { key: 'name.first', label: 'First Name', required: true }, 15 | { key: 'name.last', label: 'Last Name', required: true }, 16 | { key: 'dob', label: 'Date of Birth', widget: 'date-picker' }, 17 | ], 18 | }; 19 | const meta2 = { 20 | fields: [ 21 | { 22 | key: 'email', 23 | label: 'Email', 24 | rules: [{ type: 'email', message: 'Invalid email' }], 25 | }, 26 | ], 27 | }; 28 | 29 | const prefixMeta = { 30 | fields: [ 31 | { 32 | key: 'prefix', 33 | options: ['+86', '+87'], 34 | widget: 'select', 35 | noStyle: true, 36 | widgetProps: { 37 | style: { width: 70 }, 38 | noStyle: true, 39 | }, 40 | }, 41 | ], 42 | }; 43 | const prefixSelector = ; 44 | 45 | return ( 46 |
47 | 48 | 55 | 56 | 57 | 58 | 59 | 62 | 63 | 64 | ); 65 | }; 66 | 67 | describe('antd/Mixed', () => { 68 | config.addAdapter(antdAdapter); 69 | 70 | it('renders Mixed Nice Form using Antd', () => { 71 | render(); 72 | const inputFirstName = screen.getByLabelText('First Name'); 73 | expect(inputFirstName).toBeInTheDocument(); 74 | }); 75 | }); 76 | -------------------------------------------------------------------------------- /packages/nice-form-react/tests/antd/MultipleColumns.test.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useCallback } from 'react'; 2 | import { Form, Button } from 'antd'; 3 | import type { RadioChangeEvent } from 'antd'; 4 | import NiceForm from '../../src/NiceForm'; 5 | import config from '../../src/config'; 6 | import { render, screen } from '@testing-library/react'; 7 | import antdAdapter, { AntdNiceFormMeta } from '../../src/adapters/antdAdapter'; 8 | import '@testing-library/jest-dom'; 9 | 10 | const MultipleColumns = () => { 11 | const [form] = Form.useForm(); 12 | const [columns, setColumns] = useState(2); 13 | const handleFinish = useCallback((values: any) => { 14 | console.log('Submit: ', values); 15 | }, []); 16 | const meta: AntdNiceFormMeta = { 17 | columns, 18 | fields: [ 19 | { 20 | key: 'columns', 21 | label: 'Columns', 22 | widget: 'radio-group', 23 | widgetProps: { 24 | optionType: 'button', 25 | buttonStyle: 'solid', 26 | onChange: (evt: RadioChangeEvent) => setColumns(evt.target.value), 27 | }, 28 | options: [1, 2, 3, 4], 29 | initialValue: 2, 30 | help: 'Change columns to show layout change', 31 | }, 32 | { key: 'input', label: 'Input', required: true, tooltip: 'This is the name.' }, 33 | { 34 | key: 'checkbox', 35 | label: 'Checkbox', 36 | widget: 'checkbox', 37 | initialValue: true, 38 | }, 39 | { key: 'select', label: 'Select', widget: 'select', options: ['Apple', 'Orange', 'Banana'] }, 40 | { key: 'password', label: 'Password', widget: 'password' }, 41 | { key: 'textarea', label: 'Textarea', widget: 'textarea' }, 42 | { key: 'number', label: 'Number', widget: 'number' }, 43 | { key: 'date-picker', label: 'Date Picker', widget: 'date-picker' }, 44 | ], 45 | }; 46 | return ( 47 |
48 | 49 | 50 | 53 | 54 | 55 | ); 56 | }; 57 | 58 | describe('antd/MultipleColumns', () => { 59 | config.addAdapter(antdAdapter); 60 | 61 | it('renders MultipleColumns Form using Antd', () => { 62 | render(); 63 | const inputField = screen.getByLabelText('Input'); 64 | expect(inputField).toBeInTheDocument(); 65 | }); 66 | }); 67 | -------------------------------------------------------------------------------- /packages/nice-form-react/tests/antd/Simple.test.tsx: -------------------------------------------------------------------------------- 1 | import React, { useCallback } from 'react'; 2 | import { Form, Button } from 'antd'; 3 | import NiceForm from '../../src/NiceForm'; 4 | import config from '../../src/config'; 5 | import { render, screen } from '@testing-library/react'; 6 | import antdAdapter from '../../src/adapters/antdAdapter'; 7 | import '@testing-library/jest-dom'; 8 | 9 | const Simple = () => { 10 | const meta = { 11 | layout: 'horizontal', 12 | columns: 1, 13 | 14 | fields: [ 15 | { key: 'username', label: 'User Name' }, 16 | { key: 'password', label: 'Password', widget: 'password' }, 17 | ], 18 | }; 19 | 20 | const handleFinish = useCallback((values: any) => { 21 | console.log('Submit: ', values); 22 | }, []); 23 | 24 | return ( 25 |
26 | 27 | 28 | 31 | 32 | 33 | ); 34 | }; 35 | 36 | describe('antd/Simple', () => { 37 | config.addAdapter(antdAdapter); 38 | 39 | it('renders Simple Nice Form using Antd', () => { 40 | render(); 41 | const inputPasswd = screen.getByLabelText('Password'); 42 | expect(inputPasswd).toBeInTheDocument(); 43 | }); 44 | }); 45 | -------------------------------------------------------------------------------- /packages/nice-form-react/tests/antd/SingleField.test.tsx: -------------------------------------------------------------------------------- 1 | import React, { useCallback } from 'react'; 2 | import { Form, Button } from 'antd'; 3 | import NiceForm from '../../src/NiceForm'; 4 | import config from '../../src/config'; 5 | import { render, screen } from '@testing-library/react'; 6 | import antdAdapter from '../../src/adapters/antdAdapter'; 7 | import '@testing-library/jest-dom'; 8 | 9 | const SingleField = () => { 10 | const handleFinish = useCallback((values: any) => { 11 | console.log('Submit: ', values); 12 | }, []); 13 | 14 | return ( 15 |
16 | 19 | 26 | 27 | 30 | 31 | 32 | ); 33 | }; 34 | 35 | describe('antd/SingleField', () => { 36 | config.addAdapter(antdAdapter); 37 | 38 | it('renders SingleField Nice Form using Antd', () => { 39 | render(); 40 | const inputUsername = screen.getByPlaceholderText('Username'); 41 | expect(inputUsername).toBeInTheDocument(); 42 | }); 43 | }); 44 | -------------------------------------------------------------------------------- /packages/nice-form-react/tests/antd/Validation.test.tsx: -------------------------------------------------------------------------------- 1 | import React, { useCallback } from 'react'; 2 | import { Form, Button } from 'antd'; 3 | import NiceForm from '../../src/NiceForm'; 4 | import config from '../../src/config'; 5 | import { render, screen } from '@testing-library/react'; 6 | import antdAdapter, { AntdNiceFormMeta } from '../../src/adapters/antdAdapter'; 7 | import '@testing-library/jest-dom'; 8 | 9 | const MOCK_USERNAMES: { 10 | [key: string]: boolean; 11 | } = { 12 | nate: true, 13 | bood: true, 14 | kevin: true, 15 | }; 16 | 17 | const Validation = () => { 18 | const [form] = Form.useForm(); 19 | const handleSubmit = useCallback((values: any) => { 20 | console.log('Submit: ', values); 21 | }, []); 22 | 23 | const meta: AntdNiceFormMeta = { 24 | fields: [ 25 | { 26 | key: 'username', 27 | label: 'Username', 28 | extra: 'Note: username nate, bood or kevin already exist', 29 | hasFeedback: true, // Show validation status icon in the right 30 | required: true, // this adds an entry to rules: [{ required: true, message: 'Username is required' }] 31 | rules: [ 32 | { 33 | validator: (_, value) => { 34 | // Do async validation to check if username already exists 35 | // Use setTimeout to emulate api call 36 | return new Promise((resolve, reject) => { 37 | setTimeout(() => { 38 | if (MOCK_USERNAMES[value]) { 39 | reject(new Error(`Username "${value}" already exists.`)); 40 | } else { 41 | resolve(value); 42 | } 43 | }, 1000); 44 | }); 45 | }, 46 | }, 47 | ], 48 | }, 49 | { 50 | key: 'password', 51 | label: 'Password', 52 | widget: 'password', 53 | onChange: () => { 54 | if (form.isFieldTouched('confirmPassword')) { 55 | form.validateFields(['confirmPassword']); 56 | } 57 | }, 58 | rules: [ 59 | // This is equivalent with "required: true" 60 | { 61 | required: true, 62 | message: 'Password is required', 63 | }, 64 | ], 65 | }, 66 | { 67 | key: 'confirmPassword', 68 | label: 'Confirm Passowrd', 69 | widget: 'password', 70 | required: true, 71 | rules: [ 72 | { 73 | validator: (_, value) => { 74 | return new Promise((resolve, reject) => { 75 | if (value !== form.getFieldValue('password')) { 76 | reject(new Error('Two passwords are inconsistent.')); 77 | } else { 78 | resolve(value); 79 | } 80 | }); 81 | }, 82 | }, 83 | ], 84 | }, 85 | ], 86 | }; 87 | 88 | return ( 89 |
90 | 91 | 92 | 95 | 96 | 97 | ); 98 | }; 99 | 100 | describe('antd/Validation', () => { 101 | config.addAdapter(antdAdapter); 102 | 103 | it('renders Validation Nice Form using Antd', () => { 104 | render(); 105 | const inputUsername = screen.getByLabelText('Username'); 106 | expect(inputUsername).toBeInTheDocument(); 107 | }); 108 | }); 109 | -------------------------------------------------------------------------------- /packages/nice-form-react/tests/antd/ViewEdit.test.tsx: -------------------------------------------------------------------------------- 1 | import React, { useCallback, useState } from 'react'; 2 | import { Form, Button, message } from 'antd'; 3 | import dayjs, { Dayjs } from 'dayjs'; 4 | import NiceForm from '../../src/NiceForm'; 5 | import config from '../../src/config'; 6 | import { render, screen } from '@testing-library/react'; 7 | import antdAdapter, { AntdNiceFormMeta } from '../../src/adapters/antdAdapter'; 8 | import '@testing-library/jest-dom'; 9 | 10 | const MOCK_INFO = { 11 | name: { first: 'Nate', last: 'Wang' }, 12 | email: 'myemail@gmail.com', 13 | gender: 'Male', 14 | dateOfBirth: dayjs('2100-01-01'), 15 | phone: '15988888888', 16 | city: 'Shanghai', 17 | address: 'No.1000 Some Road, Zhangjiang Park, Pudong New District', 18 | }; 19 | 20 | const DateView = ({ value }: { value: Dayjs }) => value.format('MMM Do YYYY'); 21 | 22 | const ViewEdit = () => { 23 | const [form] = Form.useForm(); 24 | const [viewMode, setViewMode] = useState(true); 25 | const [pending, setPending] = useState(false); 26 | const [personalInfo, setPersonalInfo] = useState(MOCK_INFO); 27 | const handleFinish = useCallback((values: any) => { 28 | setPending(true); 29 | setTimeout(() => { 30 | setPending(false); 31 | setPersonalInfo(values); 32 | setViewMode(true); 33 | message.success('Infomation updated.'); 34 | }, 1500); 35 | }, []); 36 | 37 | const getMeta = () => { 38 | const meta: AntdNiceFormMeta = { 39 | columns: 2, 40 | disabled: pending, 41 | viewMode: viewMode, 42 | initialValues: personalInfo, 43 | fields: [ 44 | { 45 | key: 'name.first', 46 | name: ['name', 'first'], 47 | label: 'First Name', 48 | required: true, 49 | tooltip: 'hahahah', 50 | }, 51 | { key: 'name.last', label: 'Last Name', required: true }, 52 | { key: 'gender', label: 'Gender', widget: 'radio-group', options: ['Male', 'Female'] }, 53 | { 54 | key: 'dateOfBirth', 55 | label: 'Date of Birth', 56 | widget: 'date-picker', 57 | viewWidget: DateView, 58 | }, 59 | { key: 'email', label: 'Email' }, 60 | { key: 'phone', label: 'Phone' }, 61 | { key: 'address', label: 'Address', colSpan: 2, clear: 'left' }, 62 | { key: 'city', label: 'City' }, 63 | { key: 'zipCode', label: 'Zip Code' }, 64 | ], 65 | }; 66 | return meta; 67 | }; 68 | 69 | return ( 70 |
71 |
72 |

73 | Personal Infomation 74 | {viewMode && ( 75 | 78 | )} 79 |

80 | 81 | {!viewMode && ( 82 | 83 | 86 | 95 | 96 | )} 97 | 98 |
99 | ); 100 | }; 101 | 102 | describe('antd/ViewEdit', () => { 103 | config.addAdapter(antdAdapter); 104 | 105 | it('renders ViewEdit Nice Form using Antd', () => { 106 | render(); 107 | const inputGender = screen.getByText('Gender'); 108 | expect(inputGender).toBeInTheDocument(); 109 | }); 110 | }); 111 | -------------------------------------------------------------------------------- /packages/nice-form-react/tests/antd/ViewMode.test.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import dayjs from 'dayjs'; 3 | import type { Dayjs } from 'dayjs'; 4 | import { Form } from 'antd'; 5 | import NiceForm from '../../src/NiceForm'; 6 | import config from '../../src/config'; 7 | import { render, screen } from '@testing-library/react'; 8 | import antdAdapter from '../../src/adapters/antdAdapter'; 9 | import '@testing-library/jest-dom'; 10 | 11 | const DateView = ({ value }: { value: Dayjs }) => value.format('MMM Do YYYY'); 12 | 13 | const ViewMode = () => { 14 | const [form] = Form.useForm(); 15 | 16 | const personalInfo = { 17 | name: { first: 'Nate', last: 'Wang' }, 18 | email: 'myemail@gmail.com', 19 | gender: 'Male', 20 | dateOfBirth: dayjs('2100-01-01'), 21 | phone: '15988888888', 22 | city: 'Shanghai', 23 | address: 'No.1000 Some Road, Zhangjiang Park, Pudong New District', 24 | }; 25 | 26 | const meta = { 27 | columns: 2, 28 | viewMode: true, 29 | initialValues: personalInfo, 30 | fields: [ 31 | { key: 'name.first', label: 'First Name', tooltip: 'First name' }, 32 | { key: 'name.last', label: 'Last Name' }, 33 | { key: 'gender', label: 'Gender' }, 34 | { 35 | key: 'dateOfBirth', 36 | label: 'Date of Birth', 37 | viewWidget: DateView, 38 | }, 39 | { key: 'email', label: 'Email' }, 40 | { key: 'phone', label: 'Phone' }, 41 | { key: 'address', label: 'Address', colSpan: 2 }, 42 | { key: 'city', label: 'City' }, 43 | { key: 'zipCode', label: 'Zip Code' }, 44 | ], 45 | }; 46 | 47 | return ( 48 |
49 |
50 |

Personal Infomation

51 |
52 | 53 | 54 |
55 |
56 | ); 57 | }; 58 | 59 | describe('antd/ViewMode', () => { 60 | config.addAdapter(antdAdapter); 61 | 62 | it('renders ViewMode Nice Form using Antd', () => { 63 | render(); 64 | const inputGender = screen.getByText('Gender'); 65 | expect(inputGender).toBeInTheDocument(); 66 | }); 67 | }); 68 | -------------------------------------------------------------------------------- /packages/nice-form-react/tests/formik/AsyncDataSource.test.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from 'react'; 2 | import { Form, Formik, useFormikContext } from 'formik'; 3 | import Button from '@mui/material/Button'; 4 | import NiceForm from '../../src/NiceForm'; 5 | import { render, screen, waitFor } from '@testing-library/react'; 6 | import '@testing-library/jest-dom'; 7 | 8 | const MOCK_DATA: { 9 | [key: string]: string[]; 10 | } = { 11 | China: ['Beijing', 'Shanghai', 'Nanjing'], 12 | USA: ['New York', 'San Jose', 'Washton'], 13 | France: ['Paris', 'Marseille', 'Cannes'], 14 | }; 15 | 16 | // Mock fetch 17 | const fetchCities = (country: string): Promise => { 18 | return new Promise((resolve, reject) => { 19 | setTimeout(() => { 20 | if (MOCK_DATA[country]) resolve(MOCK_DATA[country]); 21 | else reject(new Error('Not found')); 22 | }, 1500); 23 | }); 24 | }; 25 | 26 | interface FormValues { 27 | country: string; 28 | city: string; 29 | // add other fields as necessary 30 | } 31 | const MyForm = () => { 32 | const [cities, setCities] = useState<{ [key: string]: string[] }>({}); 33 | const { values, setFieldValue } = useFormikContext(); 34 | const country = values.country; 35 | const loading = country && !cities[country]; 36 | const meta = { 37 | rowGap: 18, 38 | fields: [ 39 | { 40 | key: 'country', 41 | label: 'Country', 42 | widget: 'select', 43 | options: ['China', 'USA', 'France'], 44 | placeholder: 'Select country...', 45 | initialValue: 'China', 46 | widgetProps: { 47 | fullWidth: true, 48 | onChange: () => { 49 | // Clear city value when country is changed 50 | setFieldValue('city', undefined, true); 51 | }, 52 | }, 53 | }, 54 | { 55 | key: 'city', 56 | label: 'City', 57 | widget: 'select', 58 | options: country ? cities[country] || [] : [], 59 | widgetProps: { 60 | fullWidth: true, 61 | placeholder: loading ? 'Loading...' : 'Select city...', 62 | }, 63 | disabled: loading || !country, 64 | }, 65 | ], 66 | }; 67 | 68 | useEffect(() => { 69 | if (country && !cities[country]) { 70 | fetchCities(country).then((arr) => { 71 | setCities((p) => ({ ...p, [country]: arr })); 72 | }); 73 | } 74 | }, [country, setCities, cities]); 75 | 76 | // If country selected but no cities in store, then it's loading 77 | return ( 78 |
79 | 80 | 83 | 84 | ); 85 | }; 86 | 87 | const AsyncDataSource = () => { 88 | return ( 89 | { 92 | await new Promise((r) => setTimeout(r, 2000)); 93 | alert(JSON.stringify(values, null, 2)); 94 | }} 95 | > 96 | 97 | 98 | ); 99 | }; 100 | 101 | describe('formik/AsyncDataSource', () => { 102 | it('renders AsyncDataSource Form using formik', async () => { 103 | render(); 104 | const countryField = screen.getByText('Country'); 105 | waitFor(() => expect(countryField).toBeInTheDocument(), { timeout: 3000 }); 106 | }); 107 | }); 108 | -------------------------------------------------------------------------------- /packages/nice-form-react/tests/formik/Coordinated.test.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Button from '@mui/material/Button'; 3 | import { Formik, Form } from 'formik'; 4 | import NiceForm from '../../src/NiceForm'; 5 | import config from '../../src/config'; 6 | import formikMuiAdapter from '../../src/adapters/formikMuiAdapter'; 7 | import formikAdapter from '../../src/adapters/formikAdapter'; 8 | import { render, screen } from '@testing-library/react'; 9 | import '@testing-library/jest-dom'; 10 | 11 | config.addAdapter(formikAdapter); 12 | config.addAdapter(formikMuiAdapter); 13 | 14 | const Coordinated = () => { 15 | const getMeta = (formik: any) => { 16 | return { 17 | rowGap: 18, 18 | fields: [ 19 | { 20 | key: 'gender', 21 | label: 'Gender', 22 | widget: 'radio-group', 23 | options: ['Male', 'Female'], 24 | widgetProps: { 25 | onChange: (_: any, value: any) => { 26 | if (value === 'Male') { 27 | formik.setFieldValue('note', 'Hi, man!', true); 28 | } else { 29 | formik.setFieldValue('note', 'Hi, lady!', true); 30 | } 31 | }, 32 | }, 33 | }, 34 | { key: 'note', widgetProps: { placeholder: 'Note' } }, 35 | ], 36 | }; 37 | }; 38 | 39 | return ( 40 | {}} initialValues={{}}> 41 | {(formik) => ( 42 |
43 | 44 | 47 | 48 | )} 49 |
50 | ); 51 | }; 52 | 53 | describe('formik/Coordinated', () => { 54 | it('renders Coordinated Nice Form using Formik', async () => { 55 | render(); 56 | const inputGender = await screen.findByLabelText('Gender'); 57 | expect(inputGender).toBeInTheDocument(); 58 | }); 59 | }); 60 | -------------------------------------------------------------------------------- /packages/nice-form-react/tests/formik/DynamicFields.test.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Formik, Form } from 'formik'; 3 | import Button from '@mui/material/Button'; 4 | import NiceForm from '../../src/NiceForm'; 5 | import config from '../../src/config'; 6 | import formikMuiAdapter from '../../src/adapters/formikMuiAdapter'; 7 | import formikAdapter from '../../src/adapters/formikAdapter'; 8 | import { render, screen, waitFor } from '@testing-library/react'; 9 | import '@testing-library/jest-dom'; 10 | 11 | config.addAdapter(formikAdapter); 12 | config.addAdapter(formikMuiAdapter); 13 | 14 | const DynamicFields = () => { 15 | const getMeta = (form: any) => { 16 | const meta = { 17 | rowGap: 18, 18 | form, 19 | fields: [ 20 | { 21 | key: 'favoriteFruit', 22 | label: 'Favorite Fruit', 23 | widget: 'radio-group', 24 | options: ['Apple', 'Orange', 'Other'], 25 | initialValue: 'Apple', 26 | }, 27 | ], 28 | }; 29 | if (form.values.favoriteFruit === 'Other') { 30 | meta.fields.push({ 31 | key: 'otherFruit', 32 | label: 'Other', 33 | widget: 'text', 34 | options: [], // Add an empty array for options 35 | initialValue: 'Apple', 36 | }); 37 | } 38 | return meta; 39 | }; 40 | 41 | return ( 42 | { 47 | alert(JSON.stringify(values, null, 2)); 48 | }} 49 | > 50 | {(form) => ( 51 |
52 | 53 | 56 | 57 | )} 58 |
59 | ); 60 | }; 61 | 62 | describe('formik/DynamicFields', () => { 63 | it('renders DynamicFields Form using formik', async () => { 64 | render(); 65 | const favoriteFruit = screen.getByText('Favorite Fruit'); 66 | waitFor(() => expect(favoriteFruit).toBeInTheDocument(), { timeout: 3000 }); 67 | }); 68 | }); 69 | -------------------------------------------------------------------------------- /packages/nice-form-react/tests/formik/FieldCondition.test.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Formik, Form } from 'formik'; 3 | import Button from '@mui/material/Button'; 4 | import NiceForm from '../../src/NiceForm'; 5 | import config from '../../src/config'; 6 | import formikMuiAdapter from '../../src/adapters/formikMuiAdapter'; 7 | import formikAdapter from '../../src/adapters/formikAdapter'; 8 | import { render, screen, waitFor } from '@testing-library/react'; 9 | import '@testing-library/jest-dom'; 10 | 11 | config.addAdapter(formikAdapter); 12 | config.addAdapter(formikMuiAdapter); 13 | 14 | const FieldCondition = () => { 15 | const getMeta = (form: any) => { 16 | const meta = { 17 | rowGap: 18, 18 | form, 19 | fields: [ 20 | { 21 | key: 'favoriteFruit', 22 | label: 'Favorite Fruit', 23 | widget: 'radio-group', 24 | options: ['Apple', 'Orange', 'Other'], 25 | initialValue: 'Apple', 26 | }, 27 | { 28 | key: 'otherFruit', 29 | label: 'Other', 30 | condition: () => form.values.favoriteFruit === 'Other', 31 | }, 32 | { 33 | key: 'submitt', 34 | render: () => { 35 | return ( 36 | 39 | ); 40 | }, 41 | }, 42 | ], 43 | }; 44 | return meta; 45 | }; 46 | 47 | return ( 48 | { 53 | alert(JSON.stringify(values, null, 2)); 54 | }} 55 | > 56 | {(form) => ( 57 |
58 | 59 | 60 | )} 61 |
62 | ); 63 | }; 64 | 65 | describe('formik/FieldCondition', () => { 66 | it('renders FieldCondition Form using formik', async () => { 67 | render(); 68 | const favoriteFruit = screen.getByText('Favorite Fruit'); 69 | waitFor(() => expect(favoriteFruit).toBeInTheDocument(), { timeout: 3000 }); 70 | }); 71 | }); 72 | -------------------------------------------------------------------------------- /packages/nice-form-react/tests/formik/FormInModal.test.tsx: -------------------------------------------------------------------------------- 1 | import React, { useCallback, useState } from 'react'; 2 | import Button from '@mui/material/Button'; 3 | import Dialog from '@mui/material/Dialog'; 4 | import DialogActions from '@mui/material/DialogActions'; 5 | import DialogContent from '@mui/material/DialogContent'; 6 | import DialogTitle from '@mui/material/DialogTitle'; 7 | import { Formik, Form } from 'formik'; 8 | import { SnackbarProvider, enqueueSnackbar } from 'notistack'; 9 | import NiceForm from '../../src/NiceForm'; 10 | import config from '../../src/config'; 11 | import formikMuiAdapter from '../../src/adapters/formikMuiAdapter'; 12 | import formikAdapter from '../../src/adapters/formikAdapter'; 13 | import { render, screen, act, within } from '@testing-library/react'; 14 | import '@testing-library/jest-dom'; 15 | 16 | config.addAdapter(formikAdapter); 17 | config.addAdapter(formikMuiAdapter); 18 | 19 | const FormInModal = () => { 20 | const [open, setOpen] = useState(false); 21 | const handleOpen = () => setOpen(true); 22 | const handleClose = () => setOpen(false); 23 | const [pending, setPending] = useState(false); 24 | 25 | const handleFinish = useCallback(() => { 26 | setPending(true); 27 | setTimeout(() => { 28 | setPending(false); 29 | enqueueSnackbar('Submit success!', { 30 | variant: 'success', 31 | anchorOrigin: { vertical: 'top', horizontal: 'center' }, 32 | }); 33 | handleClose(); 34 | }, 2000); 35 | }, [setPending]); 36 | 37 | const meta = { 38 | rowGap: 18, 39 | disabled: pending, 40 | fields: [ 41 | { 42 | key: 'name', 43 | label: 'Name', 44 | required: true, 45 | }, 46 | { 47 | key: 'desc', 48 | label: 'Description', 49 | }, 50 | ], 51 | }; 52 | 53 | return ( 54 | 55 | 58 | 59 | New Item 60 | 61 | 62 |
63 | 64 | 65 | 68 | 71 | 72 |
73 |
74 |
75 |
76 |
77 | ); 78 | }; 79 | 80 | describe('formik/FormInModal', () => { 81 | it('renders FormInModal Form using formik', async () => { 82 | render(); 83 | // click on button to open modal 84 | const button = screen.getByRole('button', { name: /New\sItem/ }); 85 | expect(button).toBeTruthy(); 86 | 87 | act(() => { 88 | button.click(); 89 | }); 90 | 91 | const modalDialog = await screen.findByRole('dialog'); 92 | expect(modalDialog).toBeInTheDocument(); 93 | const inputName = within(modalDialog).getByText('Name'); 94 | expect(inputName).toBeInTheDocument(); 95 | }); 96 | }); 97 | -------------------------------------------------------------------------------- /packages/nice-form-react/tests/formik/FormList.test.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Button from '@mui/material/Button'; 3 | import { Formik, Form } from 'formik'; 4 | import NiceForm from '../../src/NiceForm'; 5 | import config from '../../src/config'; 6 | import formikMuiAdapter from '../../src/adapters/formikMuiAdapter'; 7 | import formikAdapter from '../../src/adapters/formikAdapter'; 8 | import { render, screen, waitFor } from '@testing-library/react'; 9 | import '@testing-library/jest-dom'; 10 | 11 | config.addAdapter(formikAdapter); 12 | config.addAdapter(formikMuiAdapter); 13 | 14 | const FormList = () => { 15 | const meta = { 16 | layout: 'horizontal', 17 | columns: 1, 18 | rowGap: 18, 19 | fields: [ 20 | { key: 'username', label: 'User Name' }, 21 | { key: 'password', label: 'Password', widgetProps: { type: 'password' } }, 22 | { 23 | key: 'friends', 24 | label: 'Friends', 25 | widget: 'form-list', 26 | fullWidth: true, 27 | listItemProps: { 28 | widget: 'select', 29 | options: ['Tom', 'Jerry'], 30 | required: true, 31 | fullWidth: true, 32 | }, 33 | }, 34 | ], 35 | }; 36 | 37 | return ( 38 | { 44 | await new Promise((r) => setTimeout(r, 500)); 45 | alert(JSON.stringify(values, null, 2)); 46 | }} 47 | > 48 |
49 | 50 | 53 | 54 |
55 | ); 56 | }; 57 | 58 | describe('formik/FormList', () => { 59 | it('renders FormList Form using formik', async () => { 60 | render(); 61 | const password = screen.getByLabelText('Password'); 62 | waitFor(() => expect(password).toBeInTheDocument(), { timeout: 3000 }); 63 | }); 64 | }); 65 | -------------------------------------------------------------------------------- /packages/nice-form-react/tests/formik/MultipleColumns.test.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | import Button from '@mui/material/Button'; 3 | import { Formik, Form } from 'formik'; 4 | import { LocalizationProvider } from '@mui/x-date-pickers/LocalizationProvider'; 5 | import { AdapterDayjs } from '@mui/x-date-pickers/AdapterDayjs'; 6 | import NiceForm from '../../src/NiceForm'; 7 | import config from '../../src/config'; 8 | import formikMuiAdapter from '../../src/adapters/formikMuiAdapter'; 9 | import formikAdapter from '../../src/adapters/formikAdapter'; 10 | import { render, screen, waitFor } from '@testing-library/react'; 11 | import '@testing-library/jest-dom'; 12 | 13 | config.addAdapter(formikAdapter); 14 | config.addAdapter(formikMuiAdapter); 15 | 16 | const MultipleColumns = () => { 17 | const [columns, setColumns] = useState(2); 18 | 19 | const getMeta = (form: any) => ({ 20 | columns, 21 | rowGap: 18, 22 | columnGap: 18, 23 | fields: [ 24 | { 25 | key: 'columns', 26 | label: 'Columns', 27 | widget: 'radio-group', 28 | widgetProps: { 29 | onChange: (evt: any, value: any) => { 30 | setColumns(value); 31 | form.values.columns = evt.target.value; 32 | }, 33 | }, 34 | options: [1, 2, 3, 4], 35 | help: 'Change columns to show layout change', 36 | }, 37 | { key: 'input', label: 'Input', required: true, tooltip: 'This is the name.' }, 38 | { 39 | key: 'checkbox', 40 | label: 'Checkbox', 41 | widget: 'checkbox', 42 | initialValue: true, 43 | }, 44 | { 45 | key: 'select', 46 | label: 'Select', 47 | widget: 'select', 48 | options: ['Apple', 'Orange', 'Banana'], 49 | fullWidth: true, 50 | }, 51 | { 52 | key: 'password', 53 | label: 'Password', 54 | widgetProps: { 55 | type: 'password', 56 | }, 57 | }, 58 | { 59 | key: 'textarea', 60 | label: 'Textarea', 61 | widgetProps: { 62 | multiline: true, 63 | fullWidth: true, 64 | rows: 4, 65 | }, 66 | }, 67 | { 68 | key: 'number', 69 | label: 'Number', 70 | widgetProps: { 71 | type: 'number', 72 | }, 73 | }, 74 | { 75 | key: 'submit', 76 | render: () => ( 77 | 80 | ), 81 | }, 82 | ], 83 | }); 84 | return ( 85 | { 88 | alert(JSON.stringify(values, null, 2)); 89 | }} 90 | > 91 | {(form) => ( 92 | 93 |
94 | 95 | 96 |
97 | )} 98 |
99 | ); 100 | }; 101 | 102 | describe('formik/MultipleColumns', () => { 103 | it('renders MultipleColumns Form using formik', async () => { 104 | render(); 105 | const textarea = screen.getByLabelText('Textarea'); 106 | waitFor(() => expect(textarea).toBeInTheDocument(), { timeout: 3000 }); 107 | }); 108 | }); 109 | -------------------------------------------------------------------------------- /packages/nice-form-react/tests/formik/MultipleSections.test.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Formik, Form } from 'formik'; 3 | import Button from '@mui/material/Button'; 4 | import Divider from '@mui/material/Divider'; 5 | import { LocalizationProvider } from '@mui/x-date-pickers/LocalizationProvider'; 6 | import { AdapterDayjs } from '@mui/x-date-pickers/AdapterDayjs'; 7 | import NiceForm from '../../src/NiceForm'; 8 | import config from '../../src/config'; 9 | import formikMuiAdapter from '../../src/adapters/formikMuiAdapter'; 10 | import formikAdapter from '../../src/adapters/formikAdapter'; 11 | import { render, screen, waitFor } from '@testing-library/react'; 12 | import '@testing-library/jest-dom'; 13 | 14 | config.addAdapter(formikAdapter); 15 | config.addAdapter(formikMuiAdapter); 16 | 17 | const MultipleSections = () => { 18 | const meta = { 19 | columns: 1, 20 | rowGap: 18, 21 | fields: [ 22 | { key: 'name.first', label: 'First Name', required: true }, 23 | { key: 'name.last', label: 'Last Name', required: true }, 24 | { 25 | key: 'email', 26 | label: 'Email', 27 | rules: [{ type: 'email', message: 'Invalid email' }], 28 | }, 29 | { 30 | key: 'security', 31 | label: 'Security Question', 32 | widget: 'select', 33 | fullWidth: true, 34 | placeholder: 'Select a question...', 35 | options: ["What's your pet's name?", 'Your nick name?'], 36 | }, 37 | { key: 'answer', label: 'Security Answer' }, 38 | { key: 'address', label: 'Address' }, 39 | { key: 'city', label: 'City' }, 40 | { key: 'phone', label: 'phone' }, 41 | ], 42 | }; 43 | 44 | const meta1 = { 45 | ...meta, 46 | fields: meta.fields.slice(0, 3), 47 | }; 48 | const meta2 = { 49 | ...meta, 50 | fields: meta.fields.slice(3, 6), 51 | }; 52 | const meta3 = { 53 | ...meta, 54 | fields: meta.fields.slice(6), 55 | }; 56 | 57 | return ( 58 | { 61 | await new Promise((r) => setTimeout(r, 500)); 62 | alert(JSON.stringify(values, null, 2)); 63 | }} 64 | > 65 | 66 |
67 |
68 |
69 | Personal Information 70 | 71 |
72 | 73 | 74 |
75 |
76 |
77 | Account Information 78 | 79 |
80 | 81 |
82 |
83 |
84 | Contact Infomation 85 | 86 |
87 | 88 |
89 | 92 |
93 |
94 |
95 | ); 96 | }; 97 | 98 | describe('formik/MultipleSections', () => { 99 | it('renders MultipleSections Form using formik', async () => { 100 | render(); 101 | const securityQuestion = screen.getByLabelText('Security Question'); 102 | waitFor(() => expect(securityQuestion).toBeInTheDocument(), { timeout: 3000 }); 103 | }); 104 | }); 105 | -------------------------------------------------------------------------------- /packages/nice-form-react/tests/formik/Simple.test.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Form, Formik, FormikProps } from 'formik'; 3 | import Button from '@mui/material/Button'; 4 | import NiceForm from '../../src/NiceForm'; 5 | import config from '../../src/config'; 6 | import formikMuiAdapter, { FormikMuiNiceFormMeta } from '../../src/adapters/formikMuiAdapter'; 7 | import formikAdapter from '../../src/adapters/formikAdapter'; 8 | import { render, screen, waitFor } from '@testing-library/react'; 9 | import '@testing-library/jest-dom'; 10 | 11 | config.addAdapter(formikAdapter); 12 | config.addAdapter(formikMuiAdapter); 13 | 14 | const Simple = () => { 15 | const initialValues = { 16 | username: '', 17 | password: '', 18 | }; 19 | 20 | const getMeta = (form: FormikProps) => { 21 | const formMeta: FormikMuiNiceFormMeta = { 22 | columns: 1, 23 | rowGap: 18, 24 | form, 25 | initialValues, 26 | disabled: form.isSubmitting, 27 | fields: [ 28 | { 29 | key: 'username', 30 | label: 'User Name', 31 | widget: 'text', 32 | }, 33 | { 34 | key: 'password', 35 | label: 'Password', 36 | widget: 'text', 37 | widgetProps: { 38 | type: 'password', 39 | }, 40 | }, 41 | { 42 | key: 'submit', 43 | render: () => { 44 | return ( 45 | 48 | ); 49 | }, 50 | }, 51 | ], 52 | }; 53 | return formMeta; 54 | }; 55 | 56 | return ( 57 |
58 | { 61 | await new Promise((r) => setTimeout(r, 500)); 62 | alert(JSON.stringify(values, null, 2)); 63 | }} 64 | > 65 | {(form) => { 66 | return ( 67 |
68 | 69 | 70 | ); 71 | }} 72 |
73 |
74 | ); 75 | }; 76 | 77 | describe('formik/Simple', () => { 78 | it('renders Simple Form using formik', async () => { 79 | render(); 80 | const password = screen.getByLabelText('Password'); 81 | waitFor(() => expect(password).toBeInTheDocument(), { timeout: 3000 }); 82 | }); 83 | }); 84 | -------------------------------------------------------------------------------- /packages/nice-form-react/tests/formik/Validation.test.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Formik, Form } from 'formik'; 3 | import Button from '@mui/material/Button'; 4 | import NiceForm from '../../src/NiceForm'; 5 | import config from '../../src/config'; 6 | import formikMuiAdapter from '../../src/adapters/formikMuiAdapter'; 7 | import formikAdapter from '../../src/adapters/formikAdapter'; 8 | import { render, screen, waitFor } from '@testing-library/react'; 9 | import '@testing-library/jest-dom'; 10 | 11 | config.addAdapter(formikAdapter); 12 | config.addAdapter(formikMuiAdapter); 13 | 14 | const MOCK_USERNAMES: { 15 | [key: string]: boolean; 16 | } = { 17 | nate: true, 18 | bood: true, 19 | kevin: true, 20 | }; 21 | 22 | const Validation = () => { 23 | const getMeta = (form: any) => { 24 | const meta = { 25 | rowGap: 18, 26 | fields: [ 27 | { 28 | key: 'username', 29 | label: 'Username', 30 | placeholder: 'Note: username nate, bood or kevin already exist', 31 | hasFeedback: true, // Show validation status icon in the right 32 | required: true, // this adds an entry to rules: [{ required: true, message: 'Username is required' }] 33 | validate: (value: any) => { 34 | let error; 35 | if (MOCK_USERNAMES[value]) { 36 | error = `Username "${value}" already exists.`; 37 | } 38 | return error; 39 | }, 40 | }, 41 | { 42 | key: 'password', 43 | label: 'Password', 44 | widgetProps: { type: 'password' }, 45 | onChange: () => { 46 | if (form.isFieldTouched('confirmPassword')) { 47 | form.validateFields(['confirmPassword']); 48 | } 49 | }, 50 | validate: (value: any) => { 51 | let error; 52 | if (!value) { 53 | error = `Password is required`; 54 | } 55 | return error; 56 | }, 57 | }, 58 | { 59 | key: 'confirmPassword', 60 | label: 'Confirm Passowrd', 61 | required: true, 62 | widgetProps: { type: 'password' }, 63 | validate: (value: any) => { 64 | let error; 65 | if (form.values.password !== value) { 66 | error = `Two passwords are inconsistent.`; 67 | } 68 | return error; 69 | }, 70 | }, 71 | { 72 | key: 'submit', 73 | render: () => ( 74 | 77 | ), 78 | }, 79 | ], 80 | }; 81 | return meta; 82 | }; 83 | 84 | return ( 85 | { 88 | await new Promise((r) => setTimeout(r, 500)); 89 | alert(JSON.stringify(values, null, 2)); 90 | }} 91 | > 92 | {(form) => ( 93 |
94 | 95 | 96 | )} 97 |
98 | ); 99 | }; 100 | 101 | describe('formik/Validation', () => { 102 | it('renders Validation Form using formik', async () => { 103 | render(); 104 | const username = screen.getByLabelText(/Username/); 105 | waitFor(() => expect(username).toBeInTheDocument(), { timeout: 3000 }); 106 | }); 107 | }); 108 | -------------------------------------------------------------------------------- /packages/nice-form-react/tests/formik/ViewMode.test.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import dayjs from 'dayjs'; 3 | import type { Dayjs } from 'dayjs'; 4 | import { InputLabel } from '@mui/material'; 5 | import { ReactElement } from 'react'; 6 | import NiceForm from '../../src/NiceForm'; 7 | import type { NiceFormMeta } from '../../src/types'; 8 | import config from '../../src/config'; 9 | import formikMuiAdapter from '../../src/adapters/formikMuiAdapter'; 10 | import formikAdapter from '../../src/adapters/formikAdapter'; 11 | import { render, screen, waitFor } from '@testing-library/react'; 12 | import '@testing-library/jest-dom'; 13 | 14 | config.addAdapter(formikAdapter); 15 | config.addAdapter(formikMuiAdapter); 16 | 17 | const DateView = ({ value, label }: { value: Dayjs; label: ReactElement }) => { 18 | return ( 19 |
20 | {label} 21 |
{value.format('MMM Do YYYY')}
22 |
23 | ); 24 | }; 25 | 26 | const ViewMode = () => { 27 | const personalInfo = { 28 | name: { first: 'Nate', last: 'Wang' }, 29 | email: 'myemail@gmail.com', 30 | gender: 'Male', 31 | dateOfBirth: dayjs('2100-01-01'), 32 | // phone: '15988888888', 33 | city: 'Shanghai', 34 | address: 35 | 'No.1000 Some Road, Zhangjiang Park, Pudong New District,Zhangjiang Park, Pudong New DistrictZhangjiang Park, Pudong New District', 36 | }; 37 | 38 | const meta: NiceFormMeta = { 39 | columns: 2, 40 | viewMode: true, 41 | initialValues: personalInfo, 42 | rowGap: 20, 43 | columnGap: 20, 44 | fields: [ 45 | { key: 'name.first', label: 'First Name', tooltip: 'First name' }, 46 | { key: 'name.last', label: 'Last Name' }, 47 | { key: 'gender', label: 'Gender' }, 48 | { 49 | key: 'dateOfBirth', 50 | label: 'Date of Birth', 51 | viewWidget: DateView, 52 | }, 53 | { key: 'email', label: 'Email' }, 54 | { key: 'phone', label: 'Phone' }, 55 | { key: 'address', label: 'Address', colSpan: 2 }, 56 | { key: 'city', label: 'City' }, 57 | { key: 'zipCode', label: 'Zip Code' }, 58 | ], 59 | }; 60 | 61 | return ( 62 |
63 |

Personal Infomation

64 | 65 |
66 | ); 67 | }; 68 | 69 | describe('formik/ViewMode', () => { 70 | it('renders ViewMode Form using formik', async () => { 71 | render(); 72 | const email = screen.getByText('Email'); 73 | waitFor(() => expect(email).toBeInTheDocument(), { timeout: 3000 }); 74 | }); 75 | }); 76 | -------------------------------------------------------------------------------- /packages/nice-form-react/tests/setupAfterEnv.ts: -------------------------------------------------------------------------------- 1 | import '@testing-library/jest-dom'; 2 | import './__mocks__/matchMedia.js'; -------------------------------------------------------------------------------- /packages/nice-form-react/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "outDir": "lib/esm", 4 | "module": "esnext", 5 | "target": "es5", 6 | "lib": ["es6", "dom", "es2016", "es2017"], 7 | "jsx": "react-jsx", 8 | "declaration": true, 9 | "moduleResolution": "node", 10 | "noUnusedLocals": false, 11 | "noUnusedParameters": false, 12 | "esModuleInterop": true, 13 | "noImplicitReturns": true, 14 | "noImplicitThis": true, 15 | "noImplicitAny": true, 16 | "strictNullChecks": true, 17 | "suppressImplicitAnyIndexErrors": true, 18 | "sourceMap": true, 19 | "allowSyntheticDefaultImports": true, 20 | "skipLibCheck": true, 21 | }, 22 | "include": ["src"], 23 | "exclude": ["node_modules", "lib"] 24 | } 25 | -------------------------------------------------------------------------------- /packages/nice-form-react/tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "composite": true, 4 | "module": "ESNext", 5 | "moduleResolution": "Node", 6 | "allowSyntheticDefaultImports": true 7 | }, 8 | "include": ["vite.config.ts"] 9 | } 10 | -------------------------------------------------------------------------------- /packages/nice-form-react/typedoc.json: -------------------------------------------------------------------------------- 1 | { 2 | "entryPoints": [ 3 | "src/index.ts", 4 | "src/adapters/antdAdapter.tsx", 5 | "src/adapters/formikAdapter.tsx", 6 | "src/adapters/formikMuiAdapter.tsx" 7 | ], 8 | "out": "docs/api", 9 | "excludeInternal": false, 10 | "readme": "none" 11 | } 12 | -------------------------------------------------------------------------------- /pnpm-workspace.yaml: -------------------------------------------------------------------------------- 1 | packages: 2 | # all packages in direct subdirs of packages/ 3 | - 'packages/*' --------------------------------------------------------------------------------