├── .eslintrc.json ├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md ├── pull_request_template.md └── workflows │ ├── ci.yml │ ├── sync-monorepo.mjs │ └── sync-pr-to-monorepo.yml ├── .gitignore ├── .prettierrc ├── CONTRIBUTING.md ├── Changelog.md ├── LICENSE ├── README.md ├── circle.yml ├── fixJSDOMEnvironment.ts ├── jest.config.js ├── package.json ├── src ├── components │ ├── Errors.jsx │ ├── Form.tsx │ ├── FormBuilder.tsx │ ├── FormEdit.tsx │ ├── FormGrid.tsx │ ├── ReactComponent.jsx │ ├── Report.jsx │ ├── SubmissionGrid.tsx │ ├── __tests__ │ │ └── Form.test.tsx │ └── index.ts ├── constants.ts ├── contexts │ └── FormioContext.tsx ├── hooks │ ├── __tests__ │ │ └── usePagination.test.ts │ ├── useFormioContext.ts │ ├── usePagination.tsx │ └── useTraceUpdate.ts ├── index.ts ├── modules │ ├── auth │ │ ├── actions.js │ │ ├── constants.js │ │ ├── index.js │ │ ├── reducers.js │ │ └── selectors.js │ ├── form │ │ ├── actions.js │ │ ├── constants.js │ │ ├── index.js │ │ ├── reducers.js │ │ └── selectors.js │ ├── forms │ │ ├── actions.js │ │ ├── constants.js │ │ ├── index.js │ │ ├── reducers.js │ │ └── selectors.js │ ├── index.js │ ├── root │ │ ├── index.js │ │ └── selectors.js │ ├── submission │ │ ├── actions.js │ │ ├── constants.js │ │ ├── index.js │ │ ├── reducers.js │ │ └── selectors.js │ └── submissions │ │ ├── actions.js │ │ ├── constants.js │ │ ├── index.js │ │ ├── reducers.js │ │ └── selectors.js ├── types.js └── utils.js ├── tsconfig.json ├── webpack.test.config.js └── yarn.lock /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "browser": true, 4 | "es2021": true, 5 | "node": true 6 | }, 7 | "extends": [ 8 | "eslint:recommended", 9 | "plugin:@typescript-eslint/recommended", 10 | "plugin:react/recommended", 11 | "plugin:react-hooks/recommended", 12 | "prettier" 13 | ], 14 | "parser": "@typescript-eslint/parser", 15 | "parserOptions": { 16 | "ecmaVersion": "latest", 17 | "sourceType": "module" 18 | }, 19 | "plugins": ["@typescript-eslint", "react"], 20 | "rules": { 21 | "react/prop-types": "warn", 22 | "@typescript-eslint/no-explicit-any": "warn", 23 | "react/react-in-jsx-scope": "off", 24 | "react/jsx-uses-react": "off", 25 | "@typescript-eslint/no-unused-vars": [ 26 | "error", 27 | { "ignoreRestSiblings": true } 28 | ], 29 | "no-prototype-builtins": "off" 30 | }, 31 | "settings": { 32 | "react": { 33 | "version": "detect" 34 | } 35 | }, 36 | "ignorePatterns": ["node_modules/", "dist/", "lib/"], 37 | "overrides": [ 38 | { 39 | "files": ["**/*.test.ts", "**/*.test.tsx"], 40 | "env": { 41 | "jest": true 42 | } 43 | } 44 | ] 45 | } 46 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | --- 8 | 9 | **Describe the bug** 10 | A clear and concise description of what the bug is. 11 | 12 | **To Reproduce** 13 | Steps to reproduce the behavior: 14 | 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | **Sandbox Example** 27 | A reproducible example from an online editor such as CodeSandbox or StackBlitz 28 | 29 | **Desktop (please complete the following information):** 30 | 31 | - OS: [e.g. iOS] 32 | - Browser [e.g. chrome, safari] 33 | - Version [e.g. 22] 34 | 35 | **Smartphone (please complete the following information):** 36 | 37 | - Device: [e.g. iPhone6] 38 | - OS: [e.g. iOS8.1] 39 | - Browser [e.g. stock browser, safari] 40 | - Version [e.g. 22] 41 | 42 | **Additional context** 43 | Add any other context about the problem here. 44 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | --- 8 | 9 | **Is your feature request related to a problem? Please describe.** 10 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 11 | 12 | **Describe the solution you'd like** 13 | A clear and concise description of what you want to happen. 14 | 15 | **Describe alternatives you've considered** 16 | A clear and concise description of any alternative solutions or features you've considered. 17 | 18 | **Additional context** 19 | Add any other context or screenshots about the feature request here. 20 | -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | ## Link to Jira Ticket 2 | 3 | https://formio.atlassian.net/browse/FIO-XXXX 4 | 5 | ## Description 6 | 7 | **What changed?** 8 | 9 | _Use this section to provide a summary description of the changes you've made_ 10 | 11 | **Why have you chosen this solution?** 12 | 13 | _Use this section to justify your choices_ 14 | 15 | ## Breaking Changes / Backwards Compatibility 16 | 17 | _Use this section to describe any potentially breaking changes this PR introduces or any effects this PR might have on backwards compatibility_ 18 | 19 | ## Dependencies 20 | 21 | _Use this section to list any dependent changes/PRs in other Form.io modules_ 22 | 23 | ## How has this PR been tested? 24 | 25 | _Use this section to describe how you tested your changes; if you haven't included automated tests, justify your reasoning_ 26 | 27 | ## Checklist: 28 | 29 | - [ ] I have completed the above PR template 30 | - [ ] I have commented my code, particularly in hard-to-understand areas 31 | - [ ] I have made corresponding changes to the documentation (if applicable) 32 | - [ ] My changes generate no new warnings 33 | - [ ] My changes include tests that prove my fix is effective (or that my feature works as intended) 34 | - [ ] New and existing unit/integration tests pass locally with my changes 35 | - [ ] Any dependent changes have corresponding PRs that are listed above 36 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: Build, Test, Publish 2 | 3 | on: 4 | pull_request: 5 | types: [opened, synchronize, reopened] 6 | 7 | env: 8 | NODE_VERSION: 20.x 9 | 10 | jobs: 11 | setup: 12 | runs-on: ubuntu-latest 13 | steps: 14 | - run: echo "Triggered by ${{ github.event_name }} event." 15 | 16 | - name: Check out repository code ${{ github.repository }} on ${{ github.ref }} 17 | uses: actions/checkout@v3 18 | 19 | - name: Set up Node.js ${{ env.NODE_VERSION }} 20 | uses: actions/setup-node@v3 21 | with: 22 | node-version: ${{ env.NODE_VERSION }} 23 | cache: 'npm' 24 | 25 | - name: Cache node modules 26 | uses: actions/cache@v3 27 | with: 28 | path: node_modules 29 | key: ${{ runner.os }}-node-${{ hashFiles('**/yarn.lock') }} 30 | restore-keys: | 31 | ${{ runner.os }}-node- 32 | 33 | - name: Installing dependencies 34 | if: steps.cache.outputs.cache-hit != 'true' 35 | uses: borales/actions-yarn@v4 36 | with: 37 | cmd: install --frozen-lockfile 38 | 39 | - name: Lint 40 | uses: borales/actions-yarn@v4 41 | with: 42 | cmd: lint 43 | 44 | - name: dependencies 45 | run: | 46 | echo "Installing dependencies" 47 | yarn list --depth=0 48 | 49 | ################################################################## 50 | ## Build 51 | ################################################################## 52 | build: 53 | needs: setup 54 | runs-on: ubuntu-latest 55 | steps: 56 | - name: Check out repository code ${{ github.repository }} on ${{ github.ref }} 57 | uses: actions/checkout@v3 58 | 59 | - name: Restore node modules from cache 60 | uses: actions/cache@v3 61 | with: 62 | path: node_modules 63 | key: ${{ runner.os }}-node-${{ hashFiles('**/yarn.lock') }} 64 | restore-keys: | 65 | ${{ runner.os }}-node- 66 | 67 | - name: Build 68 | uses: borales/actions-yarn@v4 69 | with: 70 | cmd: build 71 | 72 | ################################################################## 73 | ## Test 74 | ################################################################## 75 | test-current: 76 | needs: setup 77 | runs-on: ubuntu-latest 78 | steps: 79 | - name: Check out repository code ${{ github.repository }} on ${{ github.ref }} 80 | uses: actions/checkout@v3 81 | 82 | - name: Restore node modules from cache 83 | uses: actions/cache@v3 84 | with: 85 | path: node_modules 86 | key: ${{ runner.os }}-node-${{ hashFiles('**/yarn.lock') }} 87 | restore-keys: | 88 | ${{ runner.os }}-node- 89 | 90 | - name: Test 91 | uses: borales/actions-yarn@v4 92 | with: 93 | cmd: test 94 | 95 | test-target: 96 | needs: setup 97 | runs-on: ubuntu-latest 98 | steps: 99 | - name: Check out repository code ${{ github.repository }} on ${{ github.ref }} 100 | uses: actions/checkout@v3 101 | with: 102 | fetch-depth: 0 103 | 104 | - name: Merge target branch into current branch 105 | run: | 106 | git config --global user.email "pkgbot@form.io" 107 | git config --global user.name "pkgbot" 108 | git fetch origin ${{ github.event.pull_request.base.ref }}:${{ github.event.pull_request.base.ref }} 109 | git merge ${{ github.event.pull_request.base.ref }} --no-commit --no-ff 110 | if ! git merge --no-commit --no-ff ${{ github.event.pull_request.base.ref }}; then 111 | echo "Merge conflicts detected." 112 | git merge --abort 113 | exit 1 114 | else 115 | echo "Merge successful." 116 | fi 117 | 118 | - name: Set up Node.js ${{ env.NODE_VERSION }} 119 | uses: actions/setup-node@v3 120 | with: 121 | node-version: ${{ env.NODE_VERSION }} 122 | cache: 'npm' 123 | 124 | - name: Restore node modules from cache 125 | uses: actions/cache@v3 126 | with: 127 | path: node_modules 128 | key: ${{ runner.os }}-node-${{ hashFiles('**/yarn.lock') }} 129 | restore-keys: | 130 | ${{ runner.os }}-node- 131 | 132 | - name: Test 133 | uses: borales/actions-yarn@v4 134 | with: 135 | cmd: test 136 | 137 | ################################################################## 138 | ## Publish 139 | ################################################################## 140 | publish: 141 | needs: [setup, test-current, test-target] 142 | if: ${{ github.event_name == 'pull_request' && (github.event.action == 'opened' || github.event.action == 'synchronize') }} 143 | runs-on: ubuntu-latest 144 | steps: 145 | - name: Check out repository code ${{ github.repository }} on ${{ github.ref }} 146 | uses: actions/checkout@v3 147 | 148 | - name: Configure Git user 149 | run: | 150 | git config --global user.email "pkgbot@form.io" 151 | git config --global user.name "pkgbot" 152 | 153 | - name: Add npm token to .npmrc 154 | run: | 155 | echo "//registry.npmjs.org/:_authToken=${{ secrets.NPM_TOKEN }}" > .npmrc 156 | 157 | - name: Set up Node.js ${{ env.NODE_VERSION }} 158 | uses: actions/setup-node@v3 159 | with: 160 | node-version: ${{ env.NODE_VERSION }} 161 | cache: 'npm' 162 | registry-url: 'https://registry.npmjs.org/' 163 | 164 | - name: Restore node modules from cache 165 | uses: actions/cache@v3 166 | with: 167 | path: node_modules 168 | key: ${{ runner.os }}-node-${{ hashFiles('**/yarn.lock') }} 169 | restore-keys: | 170 | ${{ runner.os }}-node- 171 | 172 | - name: Build 173 | uses: borales/actions-yarn@v4 174 | with: 175 | cmd: build 176 | 177 | - name: Prepare version for publish 178 | id: prep 179 | run: | 180 | # Extract the pull request number and the short SHA of the commit 181 | PR_NUMBER=$(echo ${{ github.event.number }}) 182 | COMMIT_SHORT_SHA=$(echo "${{ github.event.pull_request.head.sha }}" | cut -c1-7) 183 | 184 | # Extract the current version from package.json 185 | CURRENT_VERSION=$(node -p "require('./package.json').version") 186 | 187 | # If the current version includes '-rc.', remove it and everything after 188 | # This step ensures that we start with a base version like '3.0.0' even if it was a release candidate 189 | BASE_VERSION=$(echo "$CURRENT_VERSION" | cut -d'-' -f1) 190 | 191 | # Construct the new version string 192 | NEW_VERSION="${BASE_VERSION}-dev.${PR_NUMBER}.${COMMIT_SHORT_SHA}" 193 | 194 | # Output the new version for use in subsequent GitHub Actions steps 195 | echo "NEW_VERSION=$NEW_VERSION" >> $GITHUB_ENV 196 | 197 | - name: Echo version to Publish 198 | run: | 199 | echo "Version to publish: $NEW_VERSION" 200 | 201 | - name: Publish to npm 202 | run: | 203 | npm version $NEW_VERSION 204 | yarn publish --tag dev 205 | -------------------------------------------------------------------------------- /.github/workflows/sync-monorepo.mjs: -------------------------------------------------------------------------------- 1 | import { syncFromGithubAction } from 'monorepo-sync'; 2 | 3 | syncFromGithubAction() 4 | .then(() => { 5 | console.log('Sync completed successfully'); 6 | process.exit(0); 7 | }) 8 | .catch((error) => { 9 | console.error('Error during sync:', error); 10 | process.exit(1); 11 | }); 12 | -------------------------------------------------------------------------------- /.github/workflows/sync-pr-to-monorepo.yml: -------------------------------------------------------------------------------- 1 | name: Sync Merged PR to Monorepo 2 | 3 | on: 4 | pull_request: 5 | types: [closed] 6 | branches: 7 | - main 8 | - master 9 | 10 | jobs: 11 | sync-to-monorepo: 12 | if: github.event.pull_request.merged == true 13 | runs-on: ubuntu-latest 14 | env: 15 | NODE_VERSION: 20.x 16 | GH_TOKEN: ${{ secrets.MONOREPO_SYNC_TOKEN }} 17 | PR_NUMBER: ${{ github.event.pull_request.number }} 18 | PR_TITLE: ${{ github.event.pull_request.title }} 19 | PR_AUTHOR: ${{ github.event.pull_request.user.login }} 20 | SOURCE_REPO_NAME: ${{ github.event.repository.name }} 21 | MONOREPO_PACKAGE_LOCATION: packages/${{ github.event.repository.name }} 22 | MONOREPO_PATH: ${{ github.workspace }}/monorepo 23 | 24 | steps: 25 | - name: Checkout repository 26 | uses: actions/checkout@v4 27 | with: 28 | fetch-depth: 1 29 | 30 | - name: Setup Node.js 31 | uses: actions/setup-node@v4 32 | with: 33 | node-version: ${{env.NODE_VERSION}} 34 | 35 | - name: Cache node modules 36 | uses: actions/cache@v3 37 | with: 38 | path: node_modules 39 | key: ${{ runner.os }}-node-${{ hashFiles('**/yarn.lock') }} 40 | restore-keys: | 41 | ${{ runner.os }}-node- 42 | 43 | - name: Installing dependencies 44 | if: steps.cache.outputs.cache-hit != 'true' 45 | uses: borales/actions-yarn@v4 46 | with: 47 | cmd: install --frozen-lockfile 48 | 49 | - name: Install zx 50 | run: yarn add zx 51 | 52 | - name: Install monorepo-sync package 53 | run: yarn add git+https://github.com/formio/monorepo-sync.git 54 | 55 | - name: Clone Monorepo 56 | run: | 57 | gh repo clone formio/formio-monorepo monorepo -- --depth=1 58 | 59 | - name: Sync to Monorepo 60 | run: | 61 | echo "Syncing PR #${PR_NUMBER}: ${PR_TITLE}" 62 | node .github/workflows/sync-monorepo.mjs 63 | #update 64 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | .bower.json 3 | *.log 4 | node_modules 5 | bower_components 6 | lib 7 | .idea 8 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "tabWidth": 4, 4 | "useTabs": true 5 | } 6 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | Thank you for helping this project become a better library :) 4 | 5 | - [Triage](#triage) 6 | - [Reporting Bugs](#reporting-bugs) 7 | - [Providing a feature request](#providing-a-feature-request) 8 | - [Pull requests](#pull-requests) 9 | 10 | ## Triage 11 | 12 | This is one of the easiest and most effective ways of helping out. If you see an open issue, try and reproduce the bug yourself, and comment with the result. If the issue is lacking any information to reproduce the bug, let the author know. 13 | 14 | ## Reporting Bugs 15 | 16 | Open an issue, making sure to follow the bug report template. 17 | 18 | ## Providing a feature request 19 | 20 | Open an issue, making sure to follow the Feature request template. 21 | 22 | ## Pull requests 23 | 24 | ### Getting started 25 | 26 | - If there isn't one already, open an issue describing the bug or feature request that you are going to solve in your pull request. 27 | - Create a fork of react-native-maps 28 | - If you already have a fork, make sure it is up to date 29 | - Git clone your fork and run `yarn` in the base of the cloned repo to setup your local development environment. 30 | - Create a branch from the master branch to start working on your changes. 31 | 32 | ### Committing 33 | 34 | - When you made your changes, run `yarn lint` & `yarn test` to make sure the code you introduced doesn't cause any obvious issues. 35 | - When you are ready to commit your changes, use the [Github conventional commits](https://gist.github.com/qoomon/5dfcdf8eec66a051ecd85625518cfd13) convention for you commit messages, as we use your commits when releasing new versions. 36 | - Use present tense: "add awesome component" not "added awesome component" 37 | - Limit the first line of the commit message to 100 characters 38 | - Reference issues and pull requests before committing 39 | 40 | ### Creating the pull request 41 | 42 | - The title of the PR needs to follow the same conventions as your commit messages, as it might be used in case of a squash merge. 43 | - Create the pull request against the beta branch. 44 | -------------------------------------------------------------------------------- /Changelog.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | 3 | All notable changes to this project will be documented in this file 4 | 5 | The format is based on [Keep a Changelog](http://keepachangelog.com/) 6 | and this project adheres to [Semantic Versioning](http://semver.org/). 7 | 8 | ## 6.0.0 9 | 10 | ### Changed 11 | 12 | - Official Release 13 | 14 | ## 6.0.0-rc.4 15 | 16 | - do not recreate form instance if the form json is not changed deeply 17 | 18 | ## 6.0.0-rc.3 19 | 20 | - Initialize 6.0.x branch 21 | 22 | ## 6.0.0-rc.1 23 | 24 | ### Changed 25 | 26 | - FIO-7733: type form component 27 | - FIO-7563 added events to FormEdit Component 28 | - Bump follow-redirects from 1.15.2 to 1.15.5 29 | - Bump @babel/traverse from 7.21.5 to 7.23.2 30 | - FIO-7489 formio js 5/bootstrap 5 updates to react library 31 | - pass down a ref callback to custom component instead of relying on the return value of ReactDOM.render 32 | - FIO-7733: React Updates 33 | - FIO-8943: upgrade dev dependencies to latest rcs; fix type error 34 | 35 | ## 5.3.0 36 | 37 | ### Changed 38 | 39 | - Official Release 40 | 41 | ## 5.3.0-rc.3 42 | 43 | ### Added 44 | 45 | - FIO-6493: added react wrapper for reports 46 | - FIO-7139: Replaced defaultProps with JS default values 47 | - FIO-7315: Added missed form options properties to the PropTypes 48 | - Upgrade @babel/core@7.23.0, chai@4.3.9, formiojs@4.17.1, typescript@5.2.2, core-js@3.32.2 49 | 50 | ## 5.3.0-rc.2 51 | 52 | ### Fixed 53 | 54 | - FIO-4301/4302: Fixes an issue where for is being set to the old formioInstance after recreating it 55 | 56 | ### Changed 57 | 58 | - Bump tough-cookie from 4.1.2 to 4.1.3 59 | - Bump semver from 5.7.1 to 5.7.2 60 | - Bump word-wrap from 1.2.3 to 1.2.4 61 | - Upgrade @babel/core@7.22.9, babel-loader@9.1.3, eslint-plugin-import@2.28.0, eslint-plugin-react@7.33.0, formiojs@4.15.1, jsdom@22.1.0, sinon@15.2.0, typescript@5.1.6, webpack@5.88.2, core-js@3.32.0 62 | - Upgrade formiojs peerDependency to formiojs@4.15.1 63 | 64 | ## 5.3.0-rc.1 65 | 66 | ### Changed 67 | 68 | - Changed formio.js to formiojs@4.15.0-rc.23 69 | - Upgrade babel-loader@9.1.2, escope@4.0.0, eslint@8.41.0, eslint-plugin-mocha@10.1.0, jsdom@22.0.0, mocha@10.2.0, react-test-renderer@18.2.0, sinon@15.1.0, typescript@5.0.4 70 | 71 | ### Fixed 72 | 73 | - FIO-6440: Upgrades dependencies and fixes warnings 74 | - add methode before on submit so it can be generated in typing 75 | - Updated `FormEdit` proptypes to match the props used within the component. Adds `saveForm` function and `saveText` string 76 | 77 | ## 5.2.4-rc.3 78 | 79 | ### Fixed 80 | 81 | - FIO-6440: Upgrades dependencies and fixes warnings 82 | 83 | ## 5.2.4-rc.2 84 | 85 | ### Fixed 86 | 87 | - FIO-4570: Fix issue with form redrawing when passing form object as props 88 | - updated props in formBuilder 89 | 90 | ## 5.2.2 91 | 92 | ### Fixed 93 | 94 | - fixed propTypes that cause error in console 95 | 96 | ## 5.2.1 97 | 98 | ### Fixed 99 | 100 | - Fixed an issue where user state is cleared before the user is logged out 101 | - added access to the form schema in change event in react form builder 102 | 103 | ## 5.2.0 104 | 105 | ### Changed 106 | 107 | - Update to work with latest React and also fixed imports from formiojs for build size. 108 | 109 | ## 5.1.1 110 | 111 | ### Fixed 112 | 113 | - Add formio dependency to submission hook 114 | 115 | ## 5.1.0 116 | 117 | ### Fixed 118 | 119 | - Remove unnecessary check for form and components 120 | 121 | ### Changed 122 | 123 | - Official release 124 | 125 | ## 5.1.0-rc.1 126 | 127 | ### Fixed 128 | 129 | - Change the way formio being stored 130 | - FIO-2660: Fixes an issue where FormBuilder reacreats a formiojs instance on each update 131 | 132 | ## 5.0.0 133 | 134 | ### Fixed 135 | 136 | - An issue with FormsGrid. 137 | 138 | ## 5.0.0-rc.3 139 | 140 | ### Changed 141 | 142 | - Changed name to @formio/react. 143 | 144 | ### Fixed 145 | 146 | - Fixes an issue where FormBuilder stucks in an infinite loop 147 | 148 | ## 5.0.0-rc.1 149 | 150 | ### Changed 151 | 152 | - Upgraded many dependencies. 153 | - added Pagination component export 154 | - Added event when form is ready 155 | 156 | ## 5.0.0-alpha.1 157 | 158 | ### Changed 159 | 160 | - Refactored to work with latest React version. 161 | 162 | ## 4.3.0 163 | 164 | ### Changed 165 | 166 | - Upgrade formio.js to 4.9.0. 167 | 168 | ## 4.2.6 169 | 170 | ### Changed 171 | 172 | - Update dependencies for security updates. 173 | 174 | ## 4.2.5 175 | 176 | ### Fixed 177 | 178 | - Check validity return correct value. 179 | 180 | ## 4.2.4 181 | 182 | ### Fixed 183 | 184 | - Empty wizard change event. 185 | - Project access not setting correctly in auth state. 186 | 187 | ## 4.2.3 188 | 189 | ### Fixed 190 | 191 | - Change event on builder. 192 | 193 | ## 4.2.2 194 | 195 | ### Added 196 | 197 | - PDF Uploaded event watcher 198 | 199 | ### Fixed 200 | 201 | - Form reset when props change 202 | - onChange and onDelet not being called in builder. 203 | 204 | ## 4.2.1 205 | 206 | ### Fixed 207 | 208 | - getForm not calculating url correctly. 209 | 210 | ## 4.2.0 211 | 212 | ### Changed 213 | 214 | - Upgrade formio.js to 4.2 branch. 215 | - Make event management generic so it can pass through all events. 216 | 217 | ## 4.0.0 218 | 219 | ### Changed 220 | 221 | - Upgrade formio.js to 4.x branch to enable templating. 222 | - Refactor of modules and new components. 223 | 224 | ## 3.1.9 225 | 226 | ### Changed 227 | 228 | - FormGrid title links from a to span to remove weirdness with router. 229 | 230 | ## 3.1.8 231 | 232 | ### Changed 233 | 234 | - Allow override of FormEdit 235 | - Auth actions and reducers to make requests more efficient. 236 | 237 | ### Added 238 | 239 | - selectIsActive selector. 240 | 241 | ## 3.1.7 242 | 243 | ### Removed 244 | 245 | - console.log statements left in. 246 | 247 | ## 3.1.6 248 | 249 | ### Removed 250 | 251 | - Title from FormEdit 252 | 253 | ### Fixed 254 | 255 | - saveForm action was not saving. 256 | 257 | ### Added 258 | 259 | - Errors component 260 | - selectError selector 261 | 262 | ## 3.1.5 263 | 264 | ### Added 265 | 266 | - Sorting of SubmissionGrid and FormGrid 267 | 268 | ## 3.1.4 269 | 270 | ### Added 271 | 272 | - Pagination to SubmissionGrid and FormGrid 273 | 274 | ### Changed 275 | 276 | - Specify query for submissions and forms reducers and remove tag. 277 | 278 | ## 3.1.3 279 | 280 | ### Added 281 | 282 | - Url to reducers 283 | 284 | ### Changed 285 | 286 | - isFetching becomes isActive 287 | - FormEdit will autogenerate name and path for new forms. 288 | 289 | ### Removed 290 | 291 | - Options parameter to actions. 292 | 293 | ## 3.1.2 294 | 295 | ### Added 296 | 297 | - New reset actions for resetting state 298 | - FormGrid component 299 | - FormEdit component 300 | - Add action callback 301 | 302 | ## Changed 303 | 304 | - Refactor SubmissionGrid component 305 | - Refactor Grid component 306 | 307 | ## 3.1.1 308 | 309 | ### Added 310 | 311 | - Option to override the renderer and builder if they have custom components. 312 | 313 | ## 3.1.0 314 | 315 | ### Changed 316 | 317 | - Refactor module code to remove unneeded complexity 318 | 319 | ## 3.0.6 320 | 321 | ### Rerelease 322 | 323 | ## 3.0.5 324 | 325 | ### Changed 326 | 327 | - Update Formio verison 328 | 329 | ### Fixed 330 | 331 | - Event emitter cross polinating between forms. 332 | - Proptypes of formprovider 333 | 334 | ## 3.0.3 335 | 336 | ### Changed 337 | 338 | - Integration tests fixed. 339 | - react/react-dom dependencies updated to version 16. 340 | 341 | ## 3.0.2 342 | 343 | ### Changed 344 | 345 | - Formio component renamed to Form. 346 | > > > > > > > origin/3.x 347 | 348 | ## 3.0.1 349 | 350 | ### Added 351 | 352 | - url property for when using form instead of src. 353 | 354 | ## 3.0.0 355 | 356 | ### Changed 357 | 358 | - Change formio.js version to 3.0.0 now that it is released. 359 | 360 | ## 2.1.1 361 | 362 | ### Fixed 363 | 364 | - Destroy event on form builder component. 365 | 366 | ## 2.1.0 367 | 368 | ### Added 369 | 370 | - Form Builder component 371 | 372 | ## 2.0.4 373 | 374 | ### Fixed 375 | 376 | - Prop type for i18n. 377 | 378 | ## 2.0.3 379 | 380 | ### Changed 381 | 382 | - Upgrade core renderer from 2.10.1 to 2.20.4 383 | 384 | ## 2.0.2 385 | 386 | ### Changed 387 | 388 | - Rebuild for failed build. 389 | 390 | ## 2.0.1 391 | 392 | ### Fixed 393 | 394 | - Allow adjusting submission while form is being created 395 | 396 | ## 2.0.0 397 | 398 | ### Changed 399 | 400 | - Renderer now based on formio.js Core Renderer. 401 | 402 | ### Removed 403 | 404 | - All helper libraries. 405 | 406 | ## 1.4.2 407 | 408 | ### Changed 409 | 410 | - Fire edit grid open event on componentDidMount instead of componentWillMount. 411 | 412 | ## 1.4.1 413 | 414 | ### Fixed 415 | 416 | - HTML output of editgrid header 417 | 418 | ### Added 419 | 420 | - Footer for editgrid 421 | 422 | ## 1.4.0 423 | 424 | ### Added 425 | 426 | - Time component 427 | - EditGrid component 428 | 429 | ## 1.3.14 430 | 431 | - Fix default formatting of empty custom error validation. 432 | 433 | ## 1.3.13 434 | 435 | ### Fixed 436 | 437 | - Disable datagrid buttons when form is read only. 438 | - Don't fire change events for readOnly forms. 439 | 440 | ## 1.3.12 441 | 442 | ### Added 443 | 444 | - Events that fire when select lists open or close. 445 | - Event that fires on add/remove from datagrid. 446 | - Event that fires on loadMore for selects. 447 | 448 | ## 1.3.11 449 | 450 | ### Reverted 451 | 452 | - Reverted revert of change to datagrids delete value. 453 | 454 | ### Fixed 455 | 456 | - Calculated Select values could return something other than an array which caused an error. 457 | 458 | ## 1.3.10 459 | 460 | ### Reverted 461 | 462 | - Reverted change to setting values that attempted to fix deleting rows in datagrids issue that had a lot of side effects. 463 | 464 | ### 1.3.9 465 | 466 | ### Fixed 467 | 468 | - Fix MinLength calculation for datagrids. 469 | - Fixed error about setState in select component. 470 | - Scenario where updating a form doesn't always set the values. 471 | 472 | ### Changed 473 | 474 | - Replace full lodash with individual functions. 475 | 476 | ## 1.3.8 477 | 478 | ### Fixed 479 | 480 | - Datagrids with select components dependent on external data weren't updating when the data updated. 481 | 482 | ## 1.3.7 483 | 484 | ### Changed 485 | 486 | - Datagrid headers won't render if there are no labels. 487 | 488 | ## 1.3.6 489 | 490 | ### Fixed 491 | 492 | - Deleting rows in datagrids didn't clear components properly. 493 | 494 | ## 1.3.5 495 | 496 | ### Fixed 497 | 498 | - Fix performance of datagrids with large data. 499 | 500 | ## 1.3.4 501 | 502 | ### Added 503 | 504 | - Onchange event will fire for input fields after 500ms of no typing instead of only on blur. 505 | 506 | ## 1.3.3 507 | 508 | ### Added 509 | 510 | - Expose mixins as exports to ease creation of custom components. 511 | 512 | ## 1.3.2 513 | 514 | Changed 515 | 516 | - Text inputs will fire change events on blur now instead of on change. Change events were too slow in redux. 517 | 518 | ## 1.3.1 519 | 520 | ### Fixed 521 | 522 | - Fixed tests dealing with input mask change and missing onChange events. 523 | 524 | ### Removed 525 | 526 | - Removing tests that don't work with current libraries. 527 | 528 | ## 1.3.0 529 | 530 | ### Changed 531 | 532 | - Swapped react-input-mask for react-text-mask for input masks. 533 | - Improved performance of input masks. 534 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Form.io 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # @formio/react 2 | 3 | A [React](http://facebook.github.io/react/) library for rendering out forms based on the [Form.io](https://www.form.io) platform. 4 | 5 | ## Install 6 | 7 | ### npm 8 | 9 | ```bash 10 | npm install @formio/react --save 11 | npm install @formio/js --save 12 | ``` 13 | 14 | ### yarn 15 | 16 | ```bash 17 | yarn add @formio/react @formio/js 18 | ``` 19 | 20 | ## Usage with Vite (Note: When using frameworks like Next.js or Create React App, no extra Vite configuration is necessary) 21 | 22 | When using @formio/react in a project built with Vite (especially for React 18 and 19), make sure you install the @vitejs/plugin-react package and configure it in your vite.config.js file. 23 | 24 | Install Vite React Plugin 25 | 26 | ```bash 27 | yarn add --dev @vitejs/plugin-react 28 | ``` 29 | 30 | In your vite.config.js, add the React plugin: 31 | 32 | ```bash 33 | import { defineConfig } from 'vite'; 34 | import react from '@vitejs/plugin-react'; 35 | 36 | export default defineConfig({ 37 | plugins: [react()], 38 | }); 39 | ``` 40 | 41 | ## Hooks 42 | 43 | ### useFormioContext 44 | 45 | A hook to supply global Formio contextual values to your React components. **Components that call `useFormioContext` must be children of a `` component.** 46 | 47 | #### Return Value 48 | 49 | `useFormioContext` returns an object with the following parameters: 50 | 51 | | Name | Type | Description | 52 | | --------------- | ------------- | -------------------------------------------------------------------------------------------------------------------------------------- | 53 | | Formio | typeof Formio | The global Formio object. Useful for various static methods as well as SDK functions that are exposed when the `new` operator is used. | 54 | | baseUrl | string | The base url for a Form.io server. | 55 | | projectUrl | string | The base url for a Form.io enterprise project. | 56 | | logout | () => void | A convenience method to logout of a Form.io session by invalidating the token and removing it from local storage. | 57 | | token | string | The Form.io JWT-token (if the user is authenticated). | 58 | | isAuthenticated | boolean | A convenience value that is toggled when logging in or out of a Form.io session. | 59 | 60 | #### Examples 61 | 62 | Use the authentication context provided by `useFormioContext` to evaluate the Form.io authentication of a user: 63 | 64 | ```tsx 65 | import { createRoot } from 'react-dom/client'; 66 | import { useFormioContext, FormGrid, FormioProvider } from '@formio/react'; 67 | 68 | const App = () => { 69 | const { isAuthenticated } = useFormioContext(); 70 | 71 | return isAuthenticated ? ( 72 | 73 | 74 | setLocation(`/form/${id}`)} 77 | /> 78 | 79 | 80 | setLocation(`/resource/${id}`)} 83 | /> 84 | 85 | 86 | ) : ( 87 | 88 | ); 89 | }; 90 | 91 | const domNode = document.getElementById('root'); 92 | const root = createRoot(domNode); 93 | root.render( 94 | 95 | 96 | , 97 | ); 98 | ``` 99 | 100 | Use the [Form.io SDK](https://help.form.io/developers/javascript-development/javascript-sdk) to interact with a Form.io server: 101 | 102 | ```tsx 103 | import { createRoot } from 'react-dom/client'; 104 | import { useEffect, useState } from 'react'; 105 | import { 106 | useFormioContext, 107 | FormioProvider, 108 | FormType, 109 | Form, 110 | } from '@formio/react'; 111 | 112 | const FormsByUser = ({ userId }: { userId: string }) => { 113 | const { Formio, projectUrl } = useFormioContext(); 114 | const [forms, setForms] = useState([]); 115 | 116 | useEffect(() => { 117 | const fetchForms = async () => { 118 | const formio = new Formio(projectUrl); 119 | try { 120 | const forms = await formio.loadForms({ 121 | params: { type: 'form', owner: userId }, 122 | }); 123 | setForms(forms); 124 | } catch (err) { 125 | console.log( 126 | `Error while loading forms for user ${userId}:`, 127 | err, 128 | ); 129 | } 130 | }; 131 | fetchForms(); 132 | }, [Formio, projectUrl, userId]); 133 | 134 | return forms.map(function (form) { 135 | return ( 136 | <> 137 |
138 |
139 | 140 | ); 141 | }); 142 | }; 143 | 144 | const domNode = document.getElementById('root'); 145 | const root = createRoot(domNode); 146 | const root = createRoot(); 147 | root.render( 148 | 149 | 150 | , 151 | ); 152 | ``` 153 | 154 | ### usePagination 155 | 156 | A hook to supply limit/skip server pagination data and methods to your React components. **Components that call `usePagination` must be children of a `` component.** 157 | 158 | #### Props 159 | 160 | | Name | Type | Description | 161 | | ------------------- | ---------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------ | 162 | | initialPage | number | The initial page to fetch. | 163 | | limit | string | The number of results per page. | 164 | | dataOrFetchFunction | T[] \| (limit: number, skip: number) => Promise | Either the complete set of data to be paginated or a function that returns data. If a function, must support limit and skip and be a stable reference. | 165 | 166 | #### Return Value 167 | 168 | `usePagination` returns an object with the following parameters: 169 | 170 | | Name | Type | Description | 171 | | --------- | ---------------------- | ---------------------------------------------------------------------------------------------------------------------------- | 172 | | data | T[] | The data at the current page. | 173 | | total | number \| undefined | If available, the total number of documents. | 174 | | page | number | The current page number. | 175 | | hasMore | boolean | A value that indicates whether more results are available from the server. Useful when no total document count is available. | 176 | | nextPage | () => void | A function that moves the data to the next available page. | 177 | | prevPage | () => void | A function that moves the data to the previous available page. | 178 | | fetchPage | (page: number) => void | A function that moves the data to a specified page. | 179 | 180 | #### Examples 181 | 182 | Paginate a set of forms: 183 | 184 | ```tsx 185 | import { createRoot } from 'react-dom/client'; 186 | import { useCallback } from 'react'; 187 | import { 188 | useFormioContext, 189 | FormioProvider, 190 | FormType, 191 | Form, 192 | } from '@formio/react'; 193 | 194 | const FormsByUser = ({ userId }: { userId: string }) => { 195 | const { Formio, projectUrl } = useFormioContext(); 196 | const fetchFunction = useCallback( 197 | (limit: number, skip: number) => { 198 | const formio = new Formio(`${projectUrl}/form`); 199 | return formio.loadForms({ params: { type: 'form', limit, skip } }); 200 | }, 201 | [Formio, projectUrl], 202 | ); 203 | const { data, page, nextPage, prevPage, hasMore } = usePagination( 204 | 1, 205 | 10, 206 | fetchFunction, 207 | ); 208 | 209 | return ( 210 |
211 |
212 | {data.map((form) => ( 213 | <> 214 | 215 |
216 | 217 | ))} 218 |
219 |
    220 |
  • 224 | Prev 225 |
  • 226 |
  • 230 | Next 231 |
  • 232 |
233 |
234 | ); 235 | }; 236 | 237 | const domNode = document.getElementById('root'); 238 | const root = createRoot(domNode); 239 | const root = createRoot(); 240 | root.render( 241 | 242 | 243 | , 244 | ); 245 | ``` 246 | 247 | ## Components 248 | 249 | ### FormioProvider 250 | 251 | A React context provider component that is required when using some hooks and components from this library. 252 | 253 | #### Props 254 | 255 | | Name | Type | Description | 256 | | ---------- | ------ | ---------------------------------------- | 257 | | Formio | object | The Formio object to be used. | 258 | | baseUrl | string | The base url of a Form.io server. | 259 | | projectUrl | string | The url of a Form.io enterprise project. | 260 | 261 | #### Examples 262 | 263 | Render a simple form from your self hosted Form.io deployment: 264 | 265 | ```JSX 266 | import { createRoot } from 'react-dom/client'; 267 | import { Form, FormioProvider } from '@formio/react'; 268 | 269 | const domNode = document.getElementById('root'); 270 | const root = createRoot(domNode); 271 | 272 | root.render( 273 | 274 | 275 | 276 | ); 277 | ``` 278 | 279 | Extend the Formio object to use external modules: 280 | 281 | ```JSX 282 | import { createRoot } from 'react-dom/client'; 283 | import { Form, FormioProvider } from '@formio/react'; 284 | import { Formio } from '@formio/js'; 285 | import premium from '@formio/premium'; 286 | 287 | const domNode = document.getElementById('root'); 288 | const root = createRoot(domNode); 289 | 290 | Formio.use(premium); 291 | root.render( 292 | 293 | 294 | 295 | ); 296 | ``` 297 | 298 | ### Form 299 | 300 | A React component wrapper around [a Form.io form](https://help.form.io/developers/form-development/form-renderer#introduction). Able to take a JSON form definition or a Form.io form URL and render the form in your React application. 301 | 302 | #### Props 303 | 304 | | Name | Type | Default | Description | 305 | | ------------------- | --------------------------------------------------------------------------------------- | ------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | 306 | | `src` | `Webform \| string` | | The JSON form definition or the source URL. If a URL, commonly from a form.io server. | 307 | | `url` | `string` | | The url of the form definition. Used in conjunction with a JSON form definition passed to `src`, this is used for file upload, OAuth, and other components or actions that need to know the URL of the Form.io form for further processing. The form will not be loaded from this url and the submission will not be saved here either. | 308 | | `submission` | `JSON` | | Submission data to fill the form. You can either load a previous submission or create a submission with some pre-filled data. If you do not provide a submissions the form will initialize an empty submission using default values from the form. | 309 | | `options` | `FormOptions` | | The form options. See [here](https://help.form.io/developers/form-development/form-renderer#form-renderer-options) for more details. | 310 | | `onFormReady` | `(instance: Webform) => void` | | A callback function that gets called when the form has rendered. It is useful for accessing the underlying @formio/js Webform instance. | 311 | | `onSubmit` | `(submission: JSON, saved?: boolean) => void` | | A callback function that gets called when the submission has started. If `src` is not a Form.io server URL, this will be the final submit event. | 312 | | `onCancelSubmit` | `() => void` | | A callback function that gets called when the submission has been canceled. | 313 | | `onSubmitDone` | `(submission: JSON) => void` | | A callback function that gets called when the submission has successfully been made to the server. This will only fire if `src` is set to a Form.io server URL. | 314 | | `onChange` | `(value: any, flags: any, modified: any) => void` | | A callback function that gets called when a value in the submission has changed. | 315 | | `onComponentChange` | `(changed: { instance: Webform; component: Component; value: any; flags: any}) => void` | | A callback function that gets called when a specific component changes. | 316 | | `onError` | `(error: EventError \| false) => void` | | A callback function that gets called when an error occurs during submission (e.g. a validation error). | 317 | | `onRender` | `(param: any) => void` | | A callback function that gets called when the form is finished rendering. `param` will depend on the form and display type. | 318 | | `onCustomEvent` | `(event: { type: string; component: Component; data: JSON; event?: Event; }) => void` | | A callback function that is triggered from a button component configured with "Event" type. | 319 | | `onPrevPage` | `(page: number, submission: JSON) => void` | | A callback function for Wizard forms that gets called when the "Previous" button is pressed. | 320 | | `onNextPage` | `(page: number, submission: JSON) => void` | | A callback function for Wizard forms that gets called when the "Next" button is pressed. | 321 | | `otherEvents` | `[event: string]: (...args: any[]) => void;` | | A "catch-all" prop for subscribing to other events (for a complete list, see [our documentation](https://help.form.io/developers/form-development/form-renderer#form-events)). | 322 | 323 | #### Examples 324 | 325 | Render a simple form from the Form.io SaaS: 326 | 327 | ```JSX 328 | import { createRoot } from 'react-dom/client'; 329 | import { Form } from '@formio/react'; 330 | 331 | const domNode = document.getElementById('root'); 332 | const root = createRoot(domNode); 333 | 334 | root.render( 335 | , 336 | ); 337 | ``` 338 | 339 | Render a simple form from a JSON form definition: 340 | 341 | ```JSX 342 | import { createRoot } from 'react-dom/client'; 343 | import { Form } from '@formio/react'; 344 | 345 | const domNode = document.getElementById('root'); 346 | const root = createRoot(domNode); 347 | 348 | const formDefinition = { 349 | type: "form", 350 | display: "form", 351 | components: [ 352 | { 353 | type: "textfield" 354 | key: "firstName", 355 | label: "First Name", 356 | input: true, 357 | }, 358 | { 359 | type: "textfield" 360 | key: "firstName", 361 | label: "First Name", 362 | input: true, 363 | }, 364 | { 365 | type: "button", 366 | key: "submit", 367 | label: "Submit", 368 | input: true 369 | } 370 | ] 371 | } 372 | 373 | root.render(); 374 | ``` 375 | 376 | Access the underlying form instance (see [here](https://help.form.io/developers/form-development/form-renderer#form-properties) for details): 377 | 378 | ```JSX 379 | import { useRef } from 'react'; 380 | import { createRoot } from 'react-dom/client'; 381 | import { Form } from '@formio/react'; 382 | 383 | const domNode = document.getElementById('root'); 384 | const root = createRoot(domNode); 385 | 386 | const formDefinition = { 387 | type: "form", 388 | display: "form", 389 | components: [ 390 | { 391 | type: "textfield" 392 | key: "firstName", 393 | label: "First Name", 394 | input: true, 395 | }, 396 | { 397 | type: "textfield" 398 | key: "firstName", 399 | label: "First Name", 400 | input: true, 401 | }, 402 | { 403 | type: "button", 404 | key: "submit", 405 | label: "Submit", 406 | input: true 407 | } 408 | ] 409 | } 410 | 411 | const App = () => { 412 | const formInstance = useRef(null); 413 | 414 | const handleFormReady = (instance) => { 415 | formInstance.current = instance; 416 | } 417 | 418 | const handleClick = () => { 419 | if (!formInstance.current) { 420 | console.log("Our form isn't quite ready yet."); 421 | return; 422 | } 423 | formInstance.current.getComponent('firstName')?.setValue('John'); 424 | formInstance.current.getComponent('lastName')?.setValue('Smith'); 425 | } 426 | 427 | return ( 428 |
429 | 430 | 431 |
432 | ); 433 | } 434 | 435 | root.render(); 436 | ``` 437 | 438 | #### Usage in Next.js 439 | 440 | A number of dependencies in the `@formio/js` rely on web APIs and browser-specific globals like `window`. Because Next.js includes a server-side rendering stage, this makes it difficult to import the Form component directly, even when used in [client components](https://nextjs.org/docs/app/building-your-application/rendering/client-components). For this reason, we recommend dynamically importing the Form component using Next.js' `dynamic` API: 441 | 442 | ```tsx 443 | 'use client'; 444 | import dynamic from 'next/dynamic'; 445 | import { Webform } from '@formio/js'; 446 | 447 | const Form = dynamic( 448 | () => import('@formio/react').then((module) => module.Form), 449 | { ssr: false }, 450 | ); 451 | 452 | export default function Home() { 453 | const formInstance = useRef(null); 454 | 455 | const handleClick = () => { 456 | if (!formInstance.current) { 457 | console.log("Our form isn't quite ready yet."); 458 | return; 459 | } 460 | formInstance.current.getComponent('firstName')?.setValue('John'); 461 | formInstance.current.getComponent('lastName')?.setValue('Smith'); 462 | }; 463 | 464 | return ( 465 |
466 | { 469 | formInstance.current = instance; 470 | }} 471 | /> 472 | 473 |
474 | ); 475 | } 476 | ``` 477 | 478 | ### FormBuilder 479 | 480 | A React component wrapper around [a Form.io form builder](https://help.form.io/developers/form-development/form-builder). Able to render the form builder in your React application. 481 | 482 | #### Props 483 | 484 | | Name | Type | Default | Description | 485 | | ------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------- | ------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | 486 | | `initialForm` | `FormType` | | The JSON form definition of the initial form to be rendered in the builder. Oftentimes, this must be a stable reference; otherwise it may destroy and recreate the underlying builder instance and cause unexpected behavior. | 487 | | `options` | `FormBuilderOptions` | | The form builder options. See [here](https://help.form.io/developers/form-development/form-builder#form-builder-options) for more details. | 488 | | `onBuilderReady` | `(instance: FormBuilder) => void` | | A callback function that gets called when the form builder has rendered. It is useful for accessing the underlying @formio/js FormBuilder instance. | 489 | | `onChange` | `(form: FormType) => void` | | A callback function that gets called when the form being built has changed. | 490 | | `onSaveComponent` | `(component: Component, original: Component, parent: Component, path: string, index: number, isNew: boolean, originalComponentSchema: Component) => void;` | | A callback function that gets called when a component is saved in the builder. | 491 | | `onEditComponent` | `(component: Component) => void` | | A callback function that gets called when a component is edited. | 492 | | `onUpdateComponent` | `(component: Component) => void` | | A callback function that is called when a component is updated. | 493 | | `onDeleteComponent` | `(component: Component, parent: Component, path: string, index: number) => void` | | A callback function that is called when a component is deleted. | 494 | 495 | #### Examples 496 | 497 | Render a simple form builder with a blank form: 498 | 499 | ```JSX 500 | import { createRoot } from 'react-dom/client'; 501 | import { FormBuilder } from '@formio/react'; 502 | 503 | const domNode = document.getElementById('root'); 504 | const root = createRoot(domNode); 505 | 506 | root.render( 507 | , 508 | ); 509 | ``` 510 | 511 | Render a builder with an initial form definition: 512 | 513 | ```JSX 514 | import { createRoot } from 'react-dom/client'; 515 | import { FormBuilder } from '@formio/react'; 516 | 517 | const domNode = document.getElementById('root'); 518 | const root = createRoot(domNode); 519 | 520 | const formDefinition = { 521 | display: "form", 522 | components: [ 523 | { 524 | type: "textfield" 525 | key: "firstName", 526 | label: "First Name", 527 | input: true, 528 | }, 529 | { 530 | type: "textfield" 531 | key: "firstName", 532 | label: "First Name", 533 | input: true, 534 | }, 535 | { 536 | type: "button", 537 | key: "submit", 538 | label: "Submit", 539 | input: true 540 | } 541 | ] 542 | } 543 | 544 | root.render(); 545 | ``` 546 | 547 | Access the underlying form builder instance (see [here](https://help.form.io/developers/form-development/form-builder#form-builder-sdk) for details): 548 | 549 | ```JSX 550 | import { useRef } from 'react'; 551 | import { createRoot } from 'react-dom/client'; 552 | import { FormBuilder } from '@formio/react'; 553 | 554 | const domNode = document.getElementById('root'); 555 | const root = createRoot(domNode); 556 | 557 | const formDefinition = { 558 | display: "form", 559 | components: [ 560 | { 561 | type: "textfield" 562 | key: "firstName", 563 | label: "First Name", 564 | input: true, 565 | }, 566 | { 567 | type: "textfield" 568 | key: "firstName", 569 | label: "First Name", 570 | input: true, 571 | }, 572 | { 573 | type: "button", 574 | key: "submit", 575 | label: "Submit", 576 | input: true 577 | } 578 | ] 579 | } 580 | 581 | const App = () => { 582 | const formBuilderInstance = useRef(null); 583 | 584 | const handleFormReady = (instance) => { 585 | formBuilderInstance.current = instance; 586 | } 587 | 588 | const handleClick = () => { 589 | if (!formBuilderInstance.current) { 590 | console.log("Our form isn't quite ready yet."); 591 | return; 592 | } 593 | console.log("Here's our builder instance:", formBuilderInstance.current); 594 | } 595 | 596 | return ( 597 |
598 | 599 | 600 |
601 | ); 602 | } 603 | 604 | root.render(); 605 | ``` 606 | 607 | ### FormEdit 608 | 609 | The FormEdit component wraps the FormBuilder component and adds a settings form, enabling direct interaction with forms to and from a Form.io server. **The `FormEdit` component must be a child of a `` component.** 610 | 611 | #### Props 612 | 613 | | Name | Type | Default | Description | 614 | | --------------------- | ------------------------------------------- | ---------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | 615 | | `initialForm` | `FormType` | `{ title: '', name: '', path: '', display: 'form' as const, type: 'form' as const, components: [] }` | The form definition of the existing form that is to be modified. | 616 | | `settingsForm` | `FormType` | `DEFAULT_SETTINGS_FORM` | The form definition for the "settings" form, which defaults to a form that defines the title, name, path, tags, and display of the form being edited. | 617 | | `settingsFormOptions` | `FormOptions` | `{}` | The options passed to the settings form. | 618 | | `onSettingsFormReady` | `(instance: Webform) => void` | | The `onFormReady` callback for the settings form. | 619 | | `onBuilderReady` | `(instance: FormioBuilder) => void` | | The `onBuilderReady` callback for the form builder. | 620 | | `builderOptions` | `FormBuilderOptions` | `{}` | The options to be passed to FormBuilder. | 621 | | `saveFormFn` | `(form: FormType) => Promise` | | Defaults to using the Form.io SDK to save the form to a Form.io server configured by ``. | 622 | | `onSaveForm` | `(form: FormType) => void` | | The callback that is called after `saveFormFn` is called (either the prop or the default). An optional function that replaces the default behavior of saving the form to a Form.io server. | 623 | 624 | #### Styling 625 | 626 | `FormEdit` takes a `components` prop that contains each "element" of the `FormEdit` component, allowing you to inject your own markup and styling. Here is its type: 627 | 628 | ```ts 629 | type ComponentProp = (props: T) => JSX.Element; 630 | type Components = { 631 | Container?: ComponentProp<{ children: ReactNode }>; 632 | SettingsFormContainer?: ComponentProp<{ children: ReactNode }>; 633 | BuilderContainer?: ComponentProp<{ children: ReactNode }>; 634 | SaveButtonContainer?: ComponentProp<{ children: ReactNode }>; 635 | SaveButton?: ComponentProp<{ 636 | onClick: () => void; 637 | }>; 638 | }; 639 | ``` 640 | 641 | #### Examples 642 | 643 | Load a simple `FormEdit` component that loads a form from a Form.io server: 644 | 645 | ```tsx 646 | import { createRoot } from 'react-dom/client'; 647 | import { useState, useEffect } from 'react'; 648 | import { 649 | FormGrid, 650 | FormioProvider, 651 | useFormioContext, 652 | FormType, 653 | } from '@formio/react'; 654 | 655 | const App = () => { 656 | const { Formio, projectUrl } = useFormioContext(); 657 | const [form, setForm] = useState(); 658 | 659 | useEffect(() => { 660 | const fetchForm = async () => { 661 | try { 662 | const formio = new Formio(`${projectUrl}/example`); 663 | const form = await formio.loadForm(); 664 | setForm(form); 665 | } catch (err) { 666 | console.log('Error while fetching form:', err); 667 | } 668 | }; 669 | }, [Formio, projectUrl]); 670 | 671 | return form ? ( 672 | console.log('Form saved to my Form.io server!')} 675 | /> 676 | ) : null; 677 | }; 678 | const domNode = document.getElementById('root'); 679 | const root = createRoot(domNode); 680 | 681 | root.render( 682 | 683 | 684 | , 685 | ); 686 | ``` 687 | 688 | Inject your own markup and styling into constituent components: 689 | 690 | ```tsx 691 | import { createRoot } from 'react-dom/client'; 692 | import { useState, useEffect } from 'react'; 693 | import { 694 | FormEdit, 695 | FormioProvider, 696 | useFormioContext, 697 | FormType, 698 | } from '@formio/react'; 699 | 700 | const App = ({ name }: { name: string }) => { 701 | const { Formio, projectUrl } = useFormioContext(); 702 | const [form, setForm] = useState(); 703 | 704 | useEffect(() => { 705 | const fetchForm = async () => { 706 | try { 707 | const formio = new Formio(`${projectUrl}/example`); 708 | const form = await formio.loadForm(); 709 | setForm(form); 710 | } catch (err) { 711 | console.log('Error while fetching form:', err); 712 | } 713 | }; 714 | }, [Formio, projectUrl]); 715 | 716 | return form ? ( 717 | console.log('Form saved to my Form.io server!')} 720 | components={{ 721 | SaveButtonContainer: ({ children }) => ( 722 |
726 | {children} 727 |
728 | ), 729 | SaveButton: ({ onClick }) => ( 730 | 733 | ), 734 | }} 735 | /> 736 | ) : null; 737 | }; 738 | const domNode = document.getElementById('root'); 739 | const root = createRoot(domNode); 740 | 741 | root.render( 742 | 743 | 744 | , 745 | ); 746 | ``` 747 | 748 | ### FormGrid 749 | 750 | The FormGrid component can be used to render a list of forms with a set of actions on each row. **The `FormGrid` component must be a child of a `` component.** 751 | 752 | #### Props 753 | 754 | | Name | Type | Default | Description | 755 | | ------------- | ---------------------------------- | ----------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | 756 | | `actions` | `Action[]` | `DEFAULT_ACTIONS` | An array of actions that correspond to buttons on each form grid row. Defaults to an Edit action (which will call the `onFormClick` function prop) and a Delete action (which will use the Form.io SDK to soft delete the form). | 757 | | `forms` | `FormType[]` | | If you'd like to manage fetching yourself, you can pass an array of forms to `FormGrid`. | 758 | | `formQuery` | `Record` | `{}` | If you don't pass the `forms` prop, `FormGrid` will use the Form.io SDK to fetch forms based on the `formQuery` prop. | 759 | | `onFormClick` | `(id: string) => void` | | A callback function called when the `FormNameContainer` is clicked. | 760 | | `limit` | `number` | `10` | The page size limit used by `usePagination`. | 761 | | `components` | `Record>` | `{}` | The list of styleable components. See [Styling](#Styling) for details. | 762 | 763 | #### Styling 764 | 765 | `FormGrid` takes a `components` prop that contains each "element" of the `FormGrid` component, allowing you to inject your own markup and styling. Here is its type: 766 | 767 | ```ts 768 | type ComponentProp = (props: T) => JSX.Element; 769 | type Components = { 770 | Container?: ComponentProp<{ children: ReactNode }>; 771 | FormContainer?: ComponentProp<{ children: ReactNode }>; 772 | FormNameContainer?: ComponentProp<{ 773 | children: ReactNode; 774 | onClick?: () => void; 775 | }>; 776 | FormActionsContainer?: ComponentProp<{ children: ReactNode }>; 777 | FormActionButton?: ComponentProp<{ 778 | action: Action; 779 | onClick: () => void; 780 | }>; 781 | PaginationContainer?: ComponentProp<{ children: ReactNode }>; 782 | PaginationButton?: ComponentProp<{ 783 | children: ReactNode; 784 | isActive?: boolean; 785 | disabled?: boolean; 786 | onClick: () => void; 787 | }>; 788 | }; 789 | ``` 790 | 791 | #### Examples 792 | 793 | Load a simple form grid that fetchs forms from a Form.io server (configured via the `` component): 794 | 795 | ```tsx 796 | import { createRoot } from 'react-dom/client'; 797 | import { FormGrid, FormioProvider } from '@formio/react'; 798 | 799 | const domNode = document.getElementById('root'); 800 | const root = createRoot(domNode); 801 | 802 | root.render( 803 | 804 | 805 | , 806 | ); 807 | ``` 808 | 809 | Inject your own markup and styling into constituent components: 810 | 811 | ```tsx 812 | import { FormGridProps, FormGrid } from '@formio/react'; 813 | import { createRoot } from 'react-dom/client'; 814 | 815 | const Container: FormGridComponentProps['Container'] = ({ children }) => ( 816 |
{children}
817 | ); 818 | 819 | const FormContainer: FormGridComponentProps['FormContainer'] = ({ 820 | children, 821 | }) => ( 822 |
823 | 824 |
825 | ); 826 | 827 | const FormNameContainer: FormGridComponentProps['FormNameContainer'] = ({ 828 | children, 829 | onClick, 830 | }) => { 831 | return ( 832 |
833 | {`${type} 834 | {children} 835 |
836 | ); 837 | }; 838 | 839 | const FormActionsContainer: FormGridComponentProps['FormActionsContainer'] = ({ 840 | children, 841 | }) =>
{children}
; 842 | 843 | const FormActionButton: FormGridComponentProps['FormActionButton'] = ({ 844 | action, 845 | onClick, 846 | }) => ( 847 | 851 | {action && action.name === 'Edit' ? 'Edit' : ''} 854 | 855 | ); 856 | 857 | const PaginationContainer: FormGridComponentProps['PaginationContainer'] = ({ 858 | children, 859 | }) =>
{children}
; 860 | 861 | const PaginationButton: FormGridComponentProps['PaginationButton'] = ({ 862 | isActive, 863 | disabled, 864 | children, 865 | onClick, 866 | }) => ( 867 | 871 | {children} 872 | 873 | ); 874 | 875 | export const MyFormGrid = () => { 876 | return ( 877 | console.log(`Form with id ${id} clicked!`)} 880 | components={{ 881 | Container, 882 | FormContainer, 883 | FormNameContainer, 884 | FormActionsContainer, 885 | FormActionButton, 886 | PaginationContainer, 887 | PaginationButton, 888 | }} 889 | /> 890 | ); 891 | }; 892 | 893 | const domNode = document.getElementById('root'); 894 | const root = createRoot(domNode); 895 | 896 | root.render( 897 | 898 | 899 | , 900 | ); 901 | ``` 902 | 903 | ### SubmissionGrid 904 | 905 | The SubmissionGrid component can be used to render a list of forms with a set of actions on each row. **The `SubmissionGrid` component must be a child of a `` component.** 906 | 907 | #### Props 908 | 909 | | Name | Type | Default | Description | 910 | | ------------------- | ---------------------------------- | ------- | ---------------------------------------------------------------------------------------------------------------------------------------- | 911 | | `submissions` | `Submission[]` | | If you'd like to manage fetching yourself, you can pass an array of submissions to `SubmissionGrid`. | 912 | | `formId` | `string` | | The id of the form whose submissions `SubmissionGrid` will fetch if the `submissions` prop is not provided. | 913 | | `submissionQuery` | `Record` | `{}` | If you don't pass the `submissions` prop, `SubmissionsGrid` will use the Form.io SDK to fetch forms based on the `submissionQuery` prop. | 914 | | `onSubmissionClick` | `(id: string) => void` | | A callback function called when the `TableBodyRowContainer` constituent component is clicked. | 915 | | `limit` | `number` | `10` | The page size limit used by `usePagination`. | 916 | | `components` | `Record>` | `{}` | The list of styleable components. See [Styling](#Styling) for details. | 917 | 918 | #### Styling 919 | 920 | `SubmissionGrid` takes a `components` prop that contains each constituent "element" of the `SubmissionGrid` component, allowing you to inject your own markup and styling. Here is its type: 921 | 922 | ```ts 923 | type ComponentProp = (props: T) => JSX.Element; 924 | type Components = { 925 | Container?: ComponentProp<{ children: ReactNode }>; 926 | TableContainer?: ComponentProp<{ children: ReactNode }>; 927 | TableHeadContainer?: ComponentProp<{ children: ReactNode }>; 928 | TableHeadCell?: ComponentProp<{ children: ReactNode }>; 929 | TableBodyRowContainer?: ComponentProp<{ 930 | children: ReactNode; 931 | onClick?: () => void; 932 | }>; 933 | TableHeaderRowContainer?: ComponentProp<{ children: ReactNode }>; 934 | TableBodyContainer?: ComponentProp<{ children: ReactNode }>; 935 | TableCell?: ComponentProp<{ children: ReactNode }>; 936 | PaginationContainer?: ComponentProp<{ children: ReactNode }>; 937 | PaginationButton?: ComponentProp<{ 938 | children: ReactNode; 939 | isActive?: boolean; 940 | disabled?: boolean; 941 | onClick: () => void; 942 | }>; 943 | }; 944 | ``` 945 | 946 | #### Examples 947 | 948 | Load a simple submission grid that fetchs forms from a Form.io server (configured via the `` component): 949 | 950 | ```tsx 951 | import { createRoot } from 'react-dom/client'; 952 | import { SubmissionGrid, FormioProvider } from '@formio/react'; 953 | 954 | const domNode = document.getElementById('root'); 955 | const root = createRoot(domNode); 956 | 957 | root.render( 958 | 959 | 960 | , 961 | ); 962 | ``` 963 | 964 | Inject your own markup and styling into constituent components: 965 | 966 | ```tsx 967 | import { SubmissionGridProps, SubmissionGrid } from '@formio/react'; 968 | import { createRoot } from 'react-dom/client'; 969 | 970 | const components: SubmissionTableProps['components'] = { 971 | Container: ({ children }) =>
{children}
, 972 | TableContainer: ({ children }) => ( 973 |
{children}
974 | ), 975 | TableHeadContainer: ({ children }) =>
{children}
, 976 | TableHeaderRowContainer: ({ children }) => ( 977 |
{children}
978 | ), 979 | TableHeadCell: ({ children }) =>
{children}
, 980 | TableBodyRowContainer: ({ children, onClick }) => ( 981 |
982 | {children} 983 |
984 | ), 985 | TableBodyContainer: ({ children }) =>
{children}
, 986 | TableCell: ({ children }) =>
{children}
, 987 | PaginationContainer: ({ children }) => ( 988 |
989 |
{children}
990 |
991 | ), 992 | PaginationButton: ({ children, isActive, onClick, disabled }) => ( 993 | 997 | {children} 998 | 999 | ), 1000 | }; 1001 | 1002 | export const MySubmissionGrid = ({ id }: { id: string }) => { 1003 | return ( 1004 | 1006 | console.log(`Submission with id ${id} clicked!`) 1007 | } 1008 | components={components} 1009 | formId={id} 1010 | /> 1011 | ); 1012 | }; 1013 | 1014 | const domNode = document.getElementById('root'); 1015 | const root = createRoot(domNode); 1016 | 1017 | root.render( 1018 | 1019 | 1020 | , 1021 | ); 1022 | ``` 1023 | 1024 | ## Modules 1025 | 1026 | Modules contain Redux actions, reducers, constants and selectors to simplify the API requests made for form.io forms. Reducers, actions and selectors all have names. This provides namespaces so the same actions and reducers can be re-used within the same redux state. 1027 | 1028 | ### root 1029 | 1030 | The root module is the container for things shared by other modules such as the selectRoot selector. 1031 | 1032 | #### Selectors 1033 | 1034 | | Name | Parameters | Description | 1035 | | ---------------- | --------------------------- | --------------------------------------- | 1036 | | `selectRoot` | name: string, state: object | Returns the state for a namespace. | 1037 | | `selectError` | name: string, state: object | Returns any errors for a namespace. | 1038 | | `selectIsActive` | name: string, state: object | Returns isActive state for a namespace. | 1039 | 1040 | ### auth 1041 | 1042 | The auth module is designed to make it easier to login, register and authenticate users within react using the form.io login system. 1043 | 1044 | #### Reducers 1045 | 1046 | | Name | Parameters | Description | 1047 | | ------ | -------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------- | 1048 | | `auth` | config: object | Mounts the user and access information to the state tree. Config is not currently used but is a placeholder to make it consistent to the other reducers. | 1049 | 1050 | #### Actions 1051 | 1052 | | Name | Parameters | Description | 1053 | | ---------- | ------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | 1054 | | `initAuth` | | This is usually used at the start of an app code. It will check the localStorage for an existing user token and if found, log them in and fetch the needed information about the user. | 1055 | | `setUser` | user: object | When a user logs in, this will set the user and fetch the access information for that user. The user object is usually a submission from the login or register form. | 1056 | | `logout` | | This action will reset everything to the default state, including removing any localStorage information. | 1057 | 1058 | ### form 1059 | 1060 | The form module is for interacting with a single form. 1061 | 1062 | #### Reducers 1063 | 1064 | | Name | Parameters | Description | 1065 | | ------ | -------------- | ------------------------------------------------------------------------------------------------------------------------------- | 1066 | | `form` | config: object | Mounts the form to the state tree. The config object should contain a name property defining a unique name for the redux state. | 1067 | 1068 | #### Actions 1069 | 1070 | | Name | Parameters | Description | 1071 | | ------------ | ------------------------------------------------------------------------------------------------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | 1072 | | `getForm` | name: string, id: string, done: function | Fetch a form from the server. If no id is provided, the name is used as the path. The `done` callback will be called when the action is complete. The first parameter is any errors and the second is the form definition. | 1073 | | `saveForm` | name: string, form: object, done: function | Save a form to the server. It will use the \_id property on the form to save it if it exists. Otherwise it will create a new form. The `done` callback will be called when the action is complete. The first parameter is any errors and the second is the form definition. | 1074 | | `deleteForm` | name: string, id: string, done: function | Delete the form on the server with the id. | 1075 | | `resetForm` | Reset this reducer back to its initial state. This is automatically called after delete but can be called other times as well. | 1076 | 1077 | #### Selectors 1078 | 1079 | | Name | Parameters | Description | 1080 | | ------------ | --------------------------- | ------------------------------------------ | 1081 | | `selectForm` | name: string, state: object | Select the form definition from the state. | 1082 | 1083 | ### forms 1084 | 1085 | The forms module handles multiple forms like a list of forms. 1086 | 1087 | #### Reducers 1088 | 1089 | | Name | Parameters | Description | 1090 | | ------- | -------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | 1091 | | `forms` | config: object | Mounts the forms to the state tree. The config object should contain a name property defining a unique name for the redux state. The config object can also contain a query property which is added to all requests for forms. For example: {tags: 'common'} would limit the lists of forms to only forms tagged with 'common'. | 1092 | 1093 | #### Actions 1094 | 1095 | | Name | Parameters | Description | 1096 | | ------------ | ------------------------------------------------------------------------------------------------------------------------------ | -------------------------------------------------------------------------------------- | 1097 | | `getForms` | name: string, page: integer, params: object | Fetch a list of forms from the server. `params` is a query object to filter the forms. | 1098 | | `resetForms` | Reset this reducer back to its initial state. This is automatically called after delete but can be called other times as well. | 1099 | 1100 | #### Selectors 1101 | 1102 | | Name | Parameters | Description | 1103 | | ------------- | --------------------------- | ---------------------------------------- | 1104 | | `selectForms` | name: string, state: object | Select the list of forms from the state. | 1105 | 1106 | ### submission 1107 | 1108 | The submission module is for interacting with a single submission. 1109 | 1110 | #### Reducers 1111 | 1112 | | Name | Parameters | Description | 1113 | | ------------ | -------------- | ------------------------------------------------------------------------------------------------------------------------------------- | 1114 | | `submission` | config: object | Mounts the submission to the state tree. The config object should contain a name property defining a unique name for the redux state. | 1115 | 1116 | #### Actions 1117 | 1118 | | Name | Parameters | Description | 1119 | | ------------------ | ------------------------------------------------------------------------------------------------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | 1120 | | `getSubmission` | name: string, id: string, formId: string, done: function | Fetch a submission from the server. The `done` callback will be called when the action is complete. The first parameter is any errors and the second is the submission. | 1121 | | `saveSubmission` | name: string, submission: object, formId: string, done: function | Save a submission to the server. It will use the \_id property on the submission to save it if it exists. Otherwise it will create a new submission. The `done` callback will be called when the action is complete. The first parameter is any errors and the second is the submission. | 1122 | | `deleteSubmission` | name: string, id: string, formId: string, done: function | Delete the submission on the server with the id. | 1123 | | `resetSubmission` | Reset this reducer back to its initial state. This is automatically called after delete but can be called other times as well. | 1124 | 1125 | #### Selectors 1126 | 1127 | | Name | Parameters | Description | 1128 | | ------------------ | --------------------------- | ------------------------------------------ | 1129 | | `selectSubmission` | name: string, state: object | Select the submission data from the state. | 1130 | 1131 | ### submissions 1132 | 1133 | The submissions module handles multiple submissions within a form, like for a list of submissions. 1134 | 1135 | #### Reducers 1136 | 1137 | | Name | Parameters | Description | 1138 | | ------------- | -------------- | -------------------------------------------------------------------------------------------------------------------------------------- | 1139 | | `submissions` | config: object | Mounts the submissions to the state tree. The config object should contain a name property defining a unique name for the redux state. | 1140 | 1141 | #### Actions 1142 | 1143 | | Name | Parameters | Description | 1144 | | ------------------ | ------------------------------------------------------------------------------------------------------------------------------ | -------------------------------------------------------------------------------------------------- | 1145 | | `getSubmissions` | name: string, page: integer, params: object, formId: string | Fetch a list of submissions from the server. `params` is a query object to filter the submissions. | 1146 | | `resetSubmissions` | Reset this reducer back to its initial state. This is automatically called after delete but can be called other times as well. | 1147 | 1148 | #### Selectors 1149 | 1150 | | Name | Parameters | Description | 1151 | | ------------------- | --------------------------- | ---------------------------------------------- | 1152 | | `selectSubmissions` | name: string, state: object | Select the list of submissions from the state. | 1153 | 1154 | ## License 1155 | 1156 | Released under the [MIT License](http://www.opensource.org/licenses/MIT). 1157 | -------------------------------------------------------------------------------- /circle.yml: -------------------------------------------------------------------------------- 1 | machine: 2 | node: 3 | version: 6.9.1 4 | -------------------------------------------------------------------------------- /fixJSDOMEnvironment.ts: -------------------------------------------------------------------------------- 1 | import JSDOMEnvironment from 'jest-environment-jsdom'; 2 | import structuredClone from '@ungap/structured-clone'; 3 | 4 | // https://github.com/facebook/jest/blob/v29.4.3/website/versioned_docs/version-29.4/Configuration.md#testenvironment-string 5 | export default class FixJSDOMEnvironment extends JSDOMEnvironment { 6 | constructor(...args: ConstructorParameters) { 7 | super(...args); 8 | 9 | // FIXME: https://github.com/jsdom/jsdom/issues/3363 10 | this.global.structuredClone = structuredClone; 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('ts-jest').JestConfigWithTsJest} */ 2 | module.exports = { 3 | preset: 'ts-jest', 4 | testEnvironment: './fixJSDOMEnvironment.ts', 5 | testPathIgnorePatterns: [ 6 | '/node_modules/', 7 | '/lib/', 8 | '/dist/', 9 | '/test/', 10 | '/.+\\.d\\.ts$', 11 | ], 12 | transform: { 13 | '^.+\\.css$': 'jest-transform-css', 14 | }, 15 | transformIgnorePatterns: [], 16 | verbose: true, 17 | }; 18 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@formio/react", 3 | "version": "6.0.0-dev.591.cd72e93", 4 | "description": "React renderer for form.io forms.", 5 | "main": "lib/index.js", 6 | "scripts": { 7 | "test": "jest", 8 | "build": "tsc --project tsconfig.json", 9 | "lint": "eslint src", 10 | "format": "prettier . --write", 11 | "prepublish": "yarn format && yarn lint && yarn build" 12 | }, 13 | "repository": { 14 | "type": "git", 15 | "url": "git+https://github.com/formio/react.git" 16 | }, 17 | "keywords": [ 18 | "React", 19 | "component", 20 | "Formio", 21 | "Forms", 22 | "react-component" 23 | ], 24 | "author": "Randall Knutson ", 25 | "license": "MIT", 26 | "bugs": { 27 | "url": "https://github.com/formio/react/issues" 28 | }, 29 | "homepage": "https://github.com/formio/react#readme", 30 | "dependencies": { 31 | "@ungap/structured-clone": "^1.2.0", 32 | "core-js": "^3.35.1", 33 | "lodash": "^4.17.21", 34 | "prop-types": "^15.8.1" 35 | }, 36 | "devDependencies": { 37 | "@formio/core": "^2.4.0", 38 | "@formio/js": "^5.1.1", 39 | "@testing-library/dom": "^10.4.0", 40 | "@testing-library/jest-dom": "^6.6.2", 41 | "@testing-library/react": "^16.1.1", 42 | "@types/jest": "^29.5.12", 43 | "@types/lodash": "^4.14.202", 44 | "@types/react": "^19.0.0", 45 | "@types/ungap__structured-clone": "^1.2.0", 46 | "@typescript-eslint/eslint-plugin": "^7.0.1", 47 | "@typescript-eslint/parser": "^7.0.1", 48 | "bootstrap": "^5.3.3", 49 | "eslint": "^8.56.0", 50 | "eslint-config-prettier": "^9.1.0", 51 | "eslint-plugin-mocha": "^10.2.0", 52 | "eslint-plugin-react": "^7.33.2", 53 | "eslint-plugin-react-hooks": "^4.6.0", 54 | "identity-obj-proxy": "^3.0.0", 55 | "jest": "^29.7.0", 56 | "jest-environment-jsdom": "^29.7.0", 57 | "jest-transform-css": "^6.0.1", 58 | "jsdom": "^22.1.0", 59 | "prettier": "3.2.4", 60 | "monorepo-sync": "git+https://github.com/johnformio/monorepo-sync.git", 61 | "react": "^19.0.0", 62 | "react-dom": "^19.0.0", 63 | "ts-jest": "^29.1.2", 64 | "ts-node": "^10.9.2", 65 | "tsc": "^2.0.4", 66 | "typescript": "^5.3.3", 67 | "webpack": "^5.88.2" 68 | }, 69 | "files": [ 70 | "lib" 71 | ], 72 | "peerDependencies": { 73 | "@formio/core": "2.4.0", 74 | "@formio/js": "5.1.1", 75 | "react": "^15.3.0 || ^16.0.0 || ^17.0.0 || ^18.1.0 || ^19.0.0", 76 | "react-dom": "^15.3.0 || ^16.0.0 || ^17.0.0 || ^18.1.0 || ^19.0.0" 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /src/components/Errors.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | 4 | const Errors = (props) => { 5 | const { type = 'danger', errors } = props; 6 | 7 | const hasErrors = (error) => { 8 | if (Array.isArray(error)) { 9 | return error.filter((item) => !!item).length !== 0; 10 | } 11 | 12 | return !!error; 13 | }; 14 | 15 | /** 16 | * @param {string|any[]} error 17 | * @returns {string|unknown[]|*} 18 | */ 19 | const formatError = (error) => { 20 | if (typeof error === 'string') { 21 | return error; 22 | } 23 | 24 | if (Array.isArray(error)) { 25 | return error.map(formatError); 26 | } 27 | 28 | // eslint-disable-next-line no-prototype-builtins 29 | if (error.hasOwnProperty('errors')) { 30 | return Object.keys(error.errors).map((key, index) => { 31 | const item = error.errors[key]; 32 | return ( 33 |
34 | 35 | {item.name} ({item.path}) 36 | {' '} 37 | - {item.message} 38 |
39 | ); 40 | }); 41 | } 42 | 43 | // If this is a standard error. 44 | // eslint-disable-next-line no-prototype-builtins 45 | if (error.hasOwnProperty('message')) { 46 | return error.message; 47 | } 48 | 49 | // If this is a joy validation error. 50 | // eslint-disable-next-line no-prototype-builtins 51 | if (error.hasOwnProperty('name') && error.name === 'ValidationError') { 52 | return error.details.map((item, index) => { 53 | return
{item.message}
; 54 | }); 55 | } 56 | 57 | // If a conflict error occurs on a form, the form is returned. 58 | // eslint-disable-next-line no-prototype-builtins 59 | if (error.hasOwnProperty('_id') && error.hasOwnProperty('display')) { 60 | return 'Another user has saved this form already. Please reload and re-apply your changes.'; 61 | } 62 | 63 | return 'An error occurred. See console logs for details.'; 64 | }; 65 | 66 | // If there are no errors, don't render anything. 67 | if (!hasErrors(errors)) { 68 | return null; 69 | } 70 | 71 | return ( 72 |
73 | {formatError(errors)} 74 |
75 | ); 76 | }; 77 | 78 | Errors.propTypes = { 79 | errors: PropTypes.any, 80 | type: PropTypes.string, 81 | }; 82 | 83 | export default Errors; 84 | -------------------------------------------------------------------------------- /src/components/Form.tsx: -------------------------------------------------------------------------------- 1 | import { CSSProperties, useEffect, useRef, useState } from 'react'; 2 | import { EventEmitter, Form as FormClass, Webform, Utils } from '@formio/js'; 3 | import { Component, Form as CoreFormType } from '@formio/core'; 4 | import structuredClone from '@ungap/structured-clone'; 5 | 6 | export type PartialExcept = Partial> & 7 | Required>; 8 | 9 | export type JSON = 10 | | string 11 | | number 12 | | boolean 13 | | null 14 | | undefined 15 | | JSON[] 16 | | { [key: string]: JSON }; 17 | 18 | // TODO: once events is typed correctly in @formio/js options, we can remove this override 19 | // TODO: `currentForm` is an option that will be deprecated once we update the Action settings form on the server 20 | export type FormOptions = FormClass['options'] & { 21 | events?: EventEmitter; 22 | currentForm?: CoreFormType; 23 | }; 24 | export type FormType = PartialExcept; 25 | export type FormSource = string | FormType; 26 | interface FormConstructor { 27 | new ( 28 | element: HTMLElement, 29 | formSource: FormSource, 30 | options: FormOptions, 31 | ): FormClass; 32 | } 33 | export type EventError = 34 | | string 35 | | Error 36 | | Error[] 37 | | { message: string } 38 | | { message: string }[]; 39 | export type Submission = { 40 | data: { [key: string]: JSON }; 41 | metadata?: { [key: string]: JSON }; 42 | state?: string; 43 | } & { 44 | [key: string]: JSON; 45 | }; 46 | export type FormProps = { 47 | className?: string; 48 | style?: CSSProperties; 49 | src: FormSource; 50 | url?: string; 51 | form?: FormType; 52 | submission?: Submission; 53 | options?: FormOptions; 54 | formioform?: FormConstructor; 55 | FormClass?: FormConstructor; 56 | formReady?: (instance: Webform) => void; 57 | onFormReady?: (instance: Webform) => void; 58 | onPrevPage?: (page: number, submission: Submission) => void; 59 | onNextPage?: (page: number, submission: Submission) => void; 60 | onCancelSubmit?: () => void; 61 | onCancelComponent?: (component: Component) => void; 62 | onChange?: (value: any, flags: any, modified: boolean) => void; 63 | onCustomEvent?: (event: { 64 | type: string; 65 | component: Component; 66 | data: { [key: string]: JSON }; 67 | event?: Event; 68 | }) => void; 69 | onComponentChange?: (changed: { 70 | instance: Webform; 71 | component: Component; 72 | value: any; 73 | flags: any; 74 | }) => void; 75 | onSubmit?: (submission: Submission, saved?: boolean) => void; 76 | onSubmitDone?: (submission: Submission) => void; 77 | onSubmitError?: (error: EventError) => void; 78 | onFormLoad?: (form: JSON) => void; 79 | onError?: (error: EventError | false) => void; 80 | onRender?: (param: any) => void; 81 | onAttach?: (param: any) => void; 82 | onBuild?: (param: any) => void; 83 | onFocus?: (instance: Webform) => void; 84 | onBlur?: (instance: Webform) => void; 85 | onInitialized?: () => void; 86 | onLanguageChanged?: () => void; 87 | onBeforeSetSubmission?: (submission: Submission) => void; 88 | onSaveDraftBegin?: () => void; 89 | onSaveDraft?: (submission: Submission) => void; 90 | onRestoreDraft?: (submission: Submission | null) => void; 91 | onSubmissionDeleted?: (submission: Submission) => void; 92 | onRequestDone?: () => void; 93 | otherEvents?: { 94 | [event: string]: (...args: any[]) => void; 95 | }; 96 | }; 97 | 98 | const getDefaultEmitter = () => { 99 | return new EventEmitter({ 100 | wildcard: false, 101 | maxListeners: 0, 102 | }); 103 | }; 104 | 105 | const onAnyEvent = ( 106 | handlers: Omit< 107 | FormProps, 108 | | 'src' 109 | | 'url' 110 | | 'form' 111 | | 'submission' 112 | | 'options' 113 | | 'formReady' 114 | | 'formioform' 115 | | 'Formio' 116 | >, 117 | ...args: [string, ...any[]] 118 | ) => { 119 | const [event, ...outputArgs] = args; 120 | if (event.startsWith('formio.')) { 121 | const funcName = `on${event.charAt(7).toUpperCase()}${event.slice(8)}`; 122 | switch (funcName) { 123 | case 'onPrevPage': 124 | if (handlers.onPrevPage) 125 | handlers.onPrevPage(outputArgs[0], outputArgs[1]); 126 | break; 127 | case 'onNextPage': 128 | if (handlers.onNextPage) 129 | handlers.onNextPage(outputArgs[0], outputArgs[1]); 130 | break; 131 | case 'onCancelSubmit': 132 | if (handlers.onCancelSubmit) handlers.onCancelSubmit(); 133 | break; 134 | case 'onCancelComponent': 135 | if (handlers.onCancelComponent) 136 | handlers.onCancelComponent(outputArgs[0]); 137 | break; 138 | case 'onChange': 139 | if (handlers.onChange) 140 | handlers.onChange( 141 | outputArgs[0], 142 | outputArgs[1], 143 | outputArgs[2], 144 | ); 145 | break; 146 | case 'onCustomEvent': 147 | if (handlers.onCustomEvent) 148 | handlers.onCustomEvent(outputArgs[0]); 149 | break; 150 | case 'onComponentChange': 151 | if (handlers.onComponentChange) 152 | handlers.onComponentChange(outputArgs[0]); 153 | break; 154 | case 'onSubmit': 155 | if (handlers.onSubmit) 156 | handlers.onSubmit(outputArgs[0], outputArgs[1]); 157 | break; 158 | case 'onSubmitDone': 159 | if (handlers.onSubmitDone) handlers.onSubmitDone(outputArgs[0]); 160 | break; 161 | case 'onSubmitError': 162 | if (handlers.onSubmitError) 163 | handlers.onSubmitError(outputArgs[0]); 164 | break; 165 | case 'onFormLoad': 166 | if (handlers.onFormLoad) handlers.onFormLoad(outputArgs[0]); 167 | break; 168 | case 'onError': 169 | if (handlers.onError) handlers.onError(outputArgs[0]); 170 | break; 171 | case 'onRender': 172 | if (handlers.onRender) handlers.onRender(outputArgs[0]); 173 | break; 174 | case 'onAttach': 175 | if (handlers.onAttach) handlers.onAttach(outputArgs[0]); 176 | break; 177 | case 'onBuild': 178 | if (handlers.onBuild) handlers.onBuild(outputArgs[0]); 179 | break; 180 | case 'onFocus': 181 | if (handlers.onFocus) handlers.onFocus(outputArgs[0]); 182 | break; 183 | case 'onBlur': 184 | if (handlers.onBlur) handlers.onBlur(outputArgs[0]); 185 | break; 186 | case 'onInitialized': 187 | if (handlers.onInitialized) handlers.onInitialized(); 188 | break; 189 | case 'onLanguageChanged': 190 | if (handlers.onLanguageChanged) handlers.onLanguageChanged(); 191 | break; 192 | case 'onBeforeSetSubmission': 193 | if (handlers.onBeforeSetSubmission) 194 | handlers.onBeforeSetSubmission(outputArgs[0]); 195 | break; 196 | case 'onSaveDraftBegin': 197 | if (handlers.onSaveDraftBegin) handlers.onSaveDraftBegin(); 198 | break; 199 | case 'onSaveDraft': 200 | if (handlers.onSaveDraft) handlers.onSaveDraft(outputArgs[0]); 201 | break; 202 | case 'onRestoreDraft': 203 | if (handlers.onRestoreDraft) 204 | handlers.onRestoreDraft(outputArgs[0]); 205 | break; 206 | case 'onSubmissionDeleted': 207 | if (handlers.onSubmissionDeleted) 208 | handlers.onSubmissionDeleted(outputArgs[0]); 209 | break; 210 | case 'onRequestDone': 211 | if (handlers.onRequestDone) handlers.onRequestDone(); 212 | break; 213 | default: 214 | break; 215 | } 216 | } 217 | if (handlers.otherEvents && handlers.otherEvents[event]) { 218 | handlers.otherEvents[event](...outputArgs); 219 | } 220 | }; 221 | 222 | const createWebformInstance = async ( 223 | FormConstructor: FormConstructor | undefined, 224 | formSource: FormSource, 225 | element: HTMLDivElement, 226 | options: FormProps['options'] = {}, 227 | ) => { 228 | if (!options?.events) { 229 | options.events = getDefaultEmitter(); 230 | } 231 | 232 | const promise = FormConstructor 233 | ? new FormConstructor(element, formSource, options) 234 | : new FormClass(element, formSource, options); 235 | const instance = await promise.ready; 236 | return instance; 237 | }; 238 | 239 | // Define effective props (aka I want to rename these props but also maintain backwards compatibility) 240 | const getEffectiveProps = (props: FormProps) => { 241 | const { FormClass, formioform, form, src, formReady, onFormReady } = props; 242 | const formConstructor = FormClass !== undefined ? FormClass : formioform; 243 | 244 | const formSource = form !== undefined ? form : src; 245 | 246 | const formReadyCallback = 247 | onFormReady !== undefined ? onFormReady : formReady; 248 | 249 | return { formConstructor, formSource, formReadyCallback }; 250 | }; 251 | 252 | export const Form = (props: FormProps) => { 253 | const renderElement = useRef(null); 254 | const currentFormJson = useRef(null); 255 | const { formConstructor, formSource, formReadyCallback } = 256 | getEffectiveProps(props); 257 | const { 258 | src, 259 | form, 260 | submission, 261 | url, 262 | options, 263 | formioform, 264 | formReady, 265 | FormClass, 266 | style, 267 | className, 268 | ...handlers 269 | } = props; 270 | const [formInstance, setFormInstance] = useState(null); 271 | 272 | useEffect(() => { 273 | return () => { 274 | if (formInstance) { 275 | formInstance.destroy(true); 276 | } 277 | }; 278 | }, [formInstance]); 279 | 280 | useEffect(() => { 281 | if ( 282 | typeof formSource === 'object' && 283 | currentFormJson.current && 284 | Utils._.isEqual(currentFormJson.current, formSource) 285 | ) { 286 | return; 287 | } 288 | 289 | const createInstance = async () => { 290 | if (renderElement.current === null) { 291 | console.warn('Form element not found'); 292 | return; 293 | } 294 | 295 | if (typeof formSource === 'undefined') { 296 | console.warn('Form source not found'); 297 | return; 298 | } 299 | currentFormJson.current = 300 | formSource && typeof formSource !== 'string' 301 | ? structuredClone(formSource) 302 | : null; 303 | const instance = await createWebformInstance( 304 | formConstructor, 305 | currentFormJson.current || formSource, 306 | renderElement.current, 307 | options, 308 | ); 309 | 310 | if (instance) { 311 | if (typeof formSource === 'string') { 312 | instance.src = formSource; 313 | } else if (typeof formSource === 'object') { 314 | instance.form = formSource; 315 | 316 | if (url) { 317 | instance.url = url; 318 | } 319 | } 320 | 321 | if (formReadyCallback) { 322 | formReadyCallback(instance); 323 | } 324 | setFormInstance(instance); 325 | } else { 326 | console.warn('Failed to create form instance'); 327 | } 328 | }; 329 | 330 | createInstance(); 331 | }, [ 332 | formConstructor, 333 | formReadyCallback, 334 | formSource, 335 | options, 336 | url, 337 | submission, 338 | ]); 339 | 340 | useEffect(() => { 341 | let onAnyHandler = null; 342 | if (formInstance && Object.keys(handlers).length > 0) { 343 | onAnyHandler = (...args: [string, ...any[]]) => 344 | onAnyEvent(handlers, ...args); 345 | formInstance.onAny(onAnyHandler); 346 | } 347 | 348 | return () => { 349 | if (formInstance && onAnyHandler) { 350 | formInstance.offAny(onAnyHandler); 351 | } 352 | }; 353 | }, [formInstance, handlers]); 354 | 355 | useEffect(() => { 356 | if (formInstance && submission) { 357 | formInstance.submission = submission; 358 | } 359 | }, [formInstance, submission]); 360 | 361 | return
; 362 | }; 363 | -------------------------------------------------------------------------------- /src/components/FormBuilder.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useRef, useState } from 'react'; 2 | import { FormBuilder as FormioFormBuilder } from '@formio/js'; 3 | import { Component } from '@formio/core'; 4 | import structuredClone from '@ungap/structured-clone'; 5 | 6 | import { FormType } from './Form'; 7 | 8 | interface BuilderConstructor { 9 | new ( 10 | element: HTMLDivElement, 11 | form: FormType, 12 | options: FormioFormBuilder['options'], 13 | ): FormioFormBuilder; 14 | } 15 | export type FormBuilderProps = { 16 | options?: FormioFormBuilder['options']; 17 | Builder?: BuilderConstructor; 18 | initialForm?: FormType; 19 | onBuilderReady?: (builder: FormioFormBuilder) => void; 20 | onChange?: (form: FormType) => void; 21 | onSaveComponent?: ( 22 | component: Component, 23 | original: Component, 24 | parent: Component, 25 | path: string, 26 | index: number, 27 | isNew: boolean, 28 | originalComponentSchema: Component, 29 | ) => void; 30 | onEditComponent?: (component: Component) => void; 31 | onUpdateComponent?: (component: Component) => void; 32 | onDeleteComponent?: ( 33 | component: Component, 34 | parent: Component, 35 | path: string, 36 | index: number, 37 | ) => void; 38 | }; 39 | 40 | const toggleEventHandlers = ( 41 | builder: FormioFormBuilder, 42 | handlers: Omit, 43 | shouldAttach: boolean = true, 44 | ) => { 45 | const fn = shouldAttach ? 'on' : 'off'; 46 | const { 47 | onSaveComponent, 48 | onEditComponent, 49 | onUpdateComponent, 50 | onDeleteComponent, 51 | onChange, 52 | } = handlers; 53 | builder.instance[fn]( 54 | 'saveComponent', 55 | ( 56 | component: Component, 57 | original: Component, 58 | parent: Component, 59 | path: string, 60 | index: number, 61 | isNew: boolean, 62 | originalComponentSchema: Component, 63 | ) => { 64 | onSaveComponent?.( 65 | component, 66 | original, 67 | parent, 68 | path, 69 | index, 70 | isNew, 71 | originalComponentSchema, 72 | ); 73 | onChange?.(structuredClone(builder.instance.form)); 74 | }, 75 | ); 76 | builder.instance[fn]('updateComponent', (component: Component) => { 77 | onUpdateComponent?.(component); 78 | onChange?.(structuredClone(builder.instance.form)); 79 | }); 80 | builder.instance[fn]( 81 | 'removeComponent', 82 | ( 83 | component: Component, 84 | parent: Component, 85 | path: string, 86 | index: number, 87 | ) => { 88 | onDeleteComponent?.(component, parent, path, index); 89 | onChange?.(structuredClone(builder.instance.form)); 90 | }, 91 | ); 92 | 93 | builder.instance[fn]('cancelComponent', (component: Component) => { 94 | onUpdateComponent?.(component); 95 | onChange?.(structuredClone(builder.instance.form)); 96 | }); 97 | 98 | builder.instance[fn]('editComponent', (component: Component) => { 99 | onEditComponent?.(component); 100 | onChange?.(structuredClone(builder.instance.form)); 101 | }); 102 | 103 | builder.instance[fn]('addComponent', () => { 104 | onChange?.(structuredClone(builder.instance.form)); 105 | }); 106 | 107 | builder.instance[fn]('pdfUploaded', () => { 108 | onChange?.(structuredClone(builder.instance.form)); 109 | }); 110 | builder.instance[fn]('setDisplay', () => { 111 | onChange?.(structuredClone(builder.instance.form)); 112 | }); 113 | }; 114 | 115 | const createBuilderInstance = async ( 116 | element: HTMLDivElement, 117 | BuilderConstructor: 118 | | BuilderConstructor 119 | | typeof FormioFormBuilder = FormioFormBuilder, 120 | form: FormType = { display: 'form', components: [] }, 121 | options: FormBuilderProps['options'] = {}, 122 | ): Promise => { 123 | options = Object.assign({}, options); 124 | form = Object.assign({}, form); 125 | 126 | const instance = new BuilderConstructor(element, form, options); 127 | 128 | await instance.ready; 129 | return instance; 130 | }; 131 | 132 | const DEFAULT_FORM: FormType = { display: 'form' as const, components: [] }; 133 | 134 | export const FormBuilder = ({ 135 | options, 136 | Builder, 137 | initialForm = DEFAULT_FORM, 138 | onBuilderReady, 139 | onChange, 140 | onDeleteComponent, 141 | onEditComponent, 142 | onSaveComponent, 143 | onUpdateComponent, 144 | }: FormBuilderProps) => { 145 | const builder = useRef(null); 146 | const renderElement = useRef(null); 147 | const [instanceIsReady, setInstanceIsReady] = useState(false); 148 | 149 | useEffect(() => { 150 | let ignore = false; 151 | const createInstance = async () => { 152 | if (!renderElement.current) { 153 | console.warn( 154 | 'FormBuilder render element not found, cannot render builder.', 155 | ); 156 | return; 157 | } 158 | const instance = await createBuilderInstance( 159 | renderElement.current, 160 | Builder, 161 | structuredClone(initialForm), 162 | options, 163 | ); 164 | if (instance) { 165 | if (ignore) { 166 | instance.instance.destroy(true); 167 | return; 168 | } 169 | 170 | if (onBuilderReady) { 171 | onBuilderReady(instance); 172 | } 173 | builder.current = instance; 174 | setInstanceIsReady(true); 175 | } else { 176 | console.warn('Failed to create FormBuilder instance'); 177 | } 178 | }; 179 | 180 | createInstance(); 181 | 182 | return () => { 183 | ignore = true; 184 | setInstanceIsReady(false); 185 | if (builder.current) { 186 | builder.current.instance.destroy(true); 187 | } 188 | }; 189 | }, [Builder, initialForm, options, onBuilderReady]); 190 | 191 | useEffect(() => { 192 | if (instanceIsReady && builder.current) { 193 | toggleEventHandlers(builder.current, { 194 | onChange, 195 | onDeleteComponent, 196 | onEditComponent, 197 | onSaveComponent, 198 | onUpdateComponent, 199 | }); 200 | } 201 | 202 | return () => { 203 | if (instanceIsReady && builder.current) { 204 | toggleEventHandlers( 205 | builder.current, 206 | { 207 | onChange, 208 | onDeleteComponent, 209 | onEditComponent, 210 | onSaveComponent, 211 | onUpdateComponent, 212 | }, 213 | false, 214 | ); 215 | } 216 | }; 217 | }, [ 218 | instanceIsReady, 219 | onChange, 220 | onDeleteComponent, 221 | onEditComponent, 222 | onSaveComponent, 223 | onUpdateComponent, 224 | ]); 225 | 226 | return
; 227 | }; 228 | -------------------------------------------------------------------------------- /src/components/FormEdit.tsx: -------------------------------------------------------------------------------- 1 | import { useRef, ReactNode, useState } from 'react'; 2 | import { FormBuilder as FormioFormBuilder } from '@formio/js'; 3 | import { FormBuilder, FormBuilderProps } from './FormBuilder'; 4 | import { Form, FormOptions, FormType, FormProps } from './Form'; 5 | import { ComponentProp } from './FormGrid'; 6 | import { useFormioContext } from '../hooks/useFormioContext'; 7 | import { Form as CoreFormType } from '@formio/core'; 8 | import Errors from './Errors'; 9 | 10 | type FormEditProps = { 11 | initialForm?: FormType; 12 | settingsForm?: FormType; 13 | settingsFormOptions?: FormOptions; 14 | onSettingsFormReady?: FormProps['onFormReady']; 15 | builderOptions?: FormBuilderProps['options']; 16 | onBuilderReady?: FormBuilderProps['onBuilderReady']; 17 | Builder?: FormBuilderProps['Builder']; 18 | saveFormFn?: (form: FormType) => Promise; 19 | onSaveForm?: (form: FormType) => void; 20 | components?: { 21 | Container?: ComponentProp<{ children: ReactNode }>; 22 | SettingsFormContainer?: ComponentProp<{ children: ReactNode }>; 23 | BuilderContainer?: ComponentProp<{ children: ReactNode }>; 24 | SaveButtonContainer?: ComponentProp<{ children: ReactNode }>; 25 | SaveButton?: ComponentProp<{ 26 | onClick: () => void; 27 | }>; 28 | }; 29 | }; 30 | 31 | type ErrorObject = { 32 | [key: string]: unknown; 33 | }; 34 | 35 | const DEFAULT_INITAL_FORM = { 36 | title: '', 37 | name: '', 38 | path: '', 39 | display: 'form' as const, 40 | type: 'form' as const, 41 | components: [ 42 | { 43 | type: 'button', 44 | label: 'Submit', 45 | key: 'submit', 46 | size: 'md', 47 | block: false, 48 | action: 'submit', 49 | disableOnInvalid: true, 50 | theme: 'primary', 51 | input: true, 52 | }, 53 | ], 54 | }; 55 | 56 | export const DEFAULT_SETTINGS_FORM = { 57 | display: 'form' as const, 58 | components: [ 59 | { 60 | label: 'Columns', 61 | columns: [ 62 | { 63 | components: [ 64 | { 65 | label: 'Form Title', 66 | labelPosition: 'left-left', 67 | applyMaskOn: 'change', 68 | tableView: true, 69 | validate: { 70 | required: true, 71 | }, 72 | key: 'title', 73 | type: 'textfield', 74 | input: true, 75 | }, 76 | { 77 | label: 'Form Name', 78 | labelPosition: 'left-left', 79 | applyMaskOn: 'change', 80 | tableView: true, 81 | calculateValue: 'value = _.camelCase(data.title);', 82 | validate: { 83 | required: true, 84 | }, 85 | key: 'name', 86 | type: 'textfield', 87 | input: true, 88 | }, 89 | { 90 | label: 'Columns', 91 | columns: [ 92 | { 93 | components: [ 94 | { 95 | label: 'Path', 96 | applyMaskOn: 'change', 97 | tableView: true, 98 | calculateValue: 99 | 'value = _.camelCase(data.title).toLowerCase();', 100 | validate: { 101 | required: true, 102 | }, 103 | key: 'path', 104 | type: 'textfield', 105 | input: true, 106 | }, 107 | ], 108 | width: 6, 109 | offset: 0, 110 | push: 0, 111 | pull: 0, 112 | size: 'md', 113 | currentWidth: 6, 114 | }, 115 | { 116 | components: [ 117 | { 118 | label: 'Display As', 119 | widget: 'choicesjs', 120 | tableView: true, 121 | data: { 122 | values: [ 123 | { 124 | label: 'Form', 125 | value: 'form', 126 | }, 127 | { 128 | label: 'Wizard', 129 | value: 'wizard', 130 | }, 131 | ], 132 | }, 133 | validate: { 134 | required: true, 135 | }, 136 | key: 'display', 137 | type: 'select', 138 | input: true, 139 | defaultValue: 'form', 140 | }, 141 | ], 142 | width: 6, 143 | offset: 0, 144 | push: 0, 145 | pull: 0, 146 | size: 'md', 147 | currentWidth: 6, 148 | }, 149 | ], 150 | key: 'columns', 151 | type: 'columns', 152 | input: false, 153 | tableView: false, 154 | }, 155 | ], 156 | width: 9, 157 | offset: 0, 158 | push: 0, 159 | pull: 0, 160 | size: 'md', 161 | currentWidth: 9, 162 | }, 163 | { 164 | components: [ 165 | { 166 | label: 'Tags', 167 | placeholder: 'Add a tag', 168 | tableView: false, 169 | key: 'tags', 170 | type: 'tags', 171 | input: true, 172 | }, 173 | ], 174 | offset: 0, 175 | push: 0, 176 | pull: 0, 177 | size: 'md', 178 | currentWidth: 3, 179 | width: 3, 180 | }, 181 | ], 182 | key: 'columns1', 183 | type: 'columns', 184 | input: false, 185 | tableView: false, 186 | }, 187 | ], 188 | }; 189 | 190 | const DEFAULT_SETTINGS_FORM_OPTIONS = {}; 191 | const DEFAULT_COMPONENTS = {}; 192 | 193 | export const FormEdit = ({ 194 | initialForm = DEFAULT_INITAL_FORM, 195 | settingsForm = DEFAULT_SETTINGS_FORM, 196 | settingsFormOptions = DEFAULT_SETTINGS_FORM_OPTIONS, 197 | components = DEFAULT_COMPONENTS, 198 | builderOptions, 199 | Builder, 200 | onSaveForm, 201 | saveFormFn, 202 | onSettingsFormReady, 203 | onBuilderReady, 204 | }: FormEditProps) => { 205 | const { Formio } = useFormioContext(); 206 | const [error, setError] = useState(null); 207 | const { 208 | Container = ({ children }) =>
{children}
, 209 | SettingsFormContainer = ({ children }) =>
{children}
, 210 | BuilderContainer = ({ children }) =>
{children}
, 211 | SaveButtonContainer = ({ children }) =>
{children}
, 212 | SaveButton = ({ onClick }) => ( 213 | 216 | ), 217 | } = components; 218 | const settingsFormData = useRef({ 219 | title: initialForm.title, 220 | name: initialForm.name, 221 | path: initialForm.path, 222 | display: initialForm.display, 223 | }); 224 | const currentForm = useRef(initialForm); 225 | const builderRef = useRef(null); 226 | 227 | const handleSaveForm = async () => { 228 | const formToSave: FormType = { 229 | ...currentForm.current, 230 | ...settingsFormData.current, 231 | }; 232 | if (saveFormFn) { 233 | try { 234 | const form = await saveFormFn(formToSave); 235 | onSaveForm?.(form); 236 | } catch (err) { 237 | console.error('Error saving form', err); 238 | } 239 | return; 240 | } 241 | const formio = new Formio( 242 | `${Formio.projectUrl || Formio.baseUrl}/form`, 243 | ); 244 | try { 245 | const form = await formio.saveForm(formToSave); 246 | onSaveForm?.(form); 247 | } catch (error) { 248 | console.error('Error saving form', error); 249 | setError(error as ErrorObject); 250 | } 251 | }; 252 | 253 | const handleBuilderReady = (builder: FormioFormBuilder) => { 254 | builderRef.current = builder; 255 | if (onBuilderReady) { 256 | onBuilderReady(builder); 257 | } 258 | }; 259 | 260 | return ( 261 | 262 | 263 | { 276 | if (modified) { 277 | if (changed.component.key === 'display') { 278 | builderRef.current?.setDisplay(data.display); 279 | } 280 | settingsFormData.current = data; 281 | } 282 | }} 283 | /> 284 | {error && } 285 | 286 | 287 | { 293 | currentForm.current = form; 294 | }} 295 | /> 296 | 297 | 298 | 299 | 300 | 301 | ); 302 | }; 303 | -------------------------------------------------------------------------------- /src/components/FormGrid.tsx: -------------------------------------------------------------------------------- 1 | import { useFormioContext } from '../hooks/useFormioContext'; 2 | import { Form as FormType } from '@formio/core'; 3 | import { usePagination } from '../hooks/usePagination'; 4 | import { JSON } from './Form'; 5 | import { ReactNode, useCallback } from 'react'; 6 | import type { JSX } from 'react'; 7 | 8 | export type Action = { 9 | name: string; 10 | fn: (id: string) => void; 11 | }; 12 | 13 | type SomeRequired = Omit & Required>; 14 | type FormFromServer = SomeRequired; 15 | 16 | export type ComponentProp = (props: T) => JSX.Element; 17 | export type FormGridProps = { 18 | actions?: Action[]; 19 | forms?: FormFromServer[]; 20 | components?: { 21 | Container?: ComponentProp<{ children: ReactNode }>; 22 | FormContainer?: ComponentProp<{ children: ReactNode }>; 23 | FormNameContainer?: ComponentProp<{ 24 | children: ReactNode; 25 | onClick?: () => void; 26 | }>; 27 | FormActionsContainer?: ComponentProp<{ children: ReactNode }>; 28 | FormActionButton?: ComponentProp<{ 29 | action: Action; 30 | onClick: () => void; 31 | }>; 32 | PaginationContainer?: ComponentProp<{ children: ReactNode }>; 33 | PaginationButton?: ComponentProp<{ 34 | children: ReactNode; 35 | isActive?: boolean; 36 | disabled?: boolean; 37 | onClick: () => void; 38 | }>; 39 | }; 40 | onFormClick?: (id: string) => void; 41 | formQuery?: { 42 | [key: string]: JSON; 43 | }; 44 | limit?: number; 45 | }; 46 | 47 | type PaginationResponse = FormType[] & { serverCount: number }; 48 | 49 | const isFormioPaginationResponse = (obj: any): obj is PaginationResponse => { 50 | return obj.serverCount !== undefined && Array.isArray(obj); 51 | }; 52 | 53 | export const DEFAULT_COMPONENTS = {}; 54 | const DEFAULT_QUERY = {}; 55 | 56 | export const FormGrid = ({ 57 | actions, 58 | components = DEFAULT_COMPONENTS, 59 | onFormClick, 60 | forms, 61 | formQuery = DEFAULT_QUERY, 62 | limit = 10, 63 | }: FormGridProps) => { 64 | const { 65 | Container = ({ children }) =>
{children}
, 66 | FormContainer = ({ children }) =>
{children}
, 67 | FormNameContainer = ({ children, onClick }) => ( 68 |
{children}
69 | ), 70 | FormActionsContainer = ({ children }) =>
{children}
, 71 | FormActionButton = ({ action }) => ( 72 | 73 | ), 74 | PaginationContainer = ({ children }) =>
    {children}
, 75 | PaginationButton = ({ children }) =>
  • {children}
  • , 76 | } = components; 77 | const { Formio } = useFormioContext(); 78 | const fetchFunction = useCallback( 79 | (limit: number, skip: number) => { 80 | const formio = new Formio('/form'); 81 | return formio.loadForms({ params: { ...formQuery, limit, skip } }); 82 | }, 83 | [formQuery, Formio], 84 | ); 85 | const dataOrFnArg = forms ? forms : fetchFunction; 86 | const { data, total, page, nextPage, prevPage, setPage, hasMore } = 87 | usePagination(1, limit, dataOrFnArg); 88 | const defaultActions = [ 89 | { name: 'Edit', fn: (id: string) => onFormClick?.(id) }, 90 | { 91 | name: 'Delete', 92 | fn: async (id: string) => { 93 | if ( 94 | window.confirm('Are you sure you want to delete this form?') 95 | ) { 96 | const formio = new Formio(`/form/${id}`); 97 | await formio.deleteForm(); 98 | setPage(1); 99 | } 100 | }, 101 | }, 102 | ]; 103 | const formActions = actions || defaultActions; 104 | return ( 105 | 106 | {data.map((form) => ( 107 | 108 | onFormClick?.(form._id)}> 109 | {form.title || form.name || form._id} 110 | 111 | 112 | {formActions.map((action, index) => ( 113 | action.fn(form._id)} 116 | key={`${action.name}-${index}`} 117 | /> 118 | ))} 119 | 120 | 121 | ))} 122 | 123 | 124 | Prev 125 | 126 | {isFormioPaginationResponse(data) && 127 | !total && 128 | Array.from( 129 | { 130 | length: Math.ceil(data.serverCount / limit), 131 | }, 132 | (_, i) => i + 1, 133 | ).map((n) => ( 134 | setPage(n)} 137 | isActive={n === page} 138 | > 139 | {n} 140 | 141 | ))} 142 | {data && 143 | total && 144 | Array.from( 145 | { 146 | length: Math.ceil(total / limit), 147 | }, 148 | (_, i) => i + 1, 149 | ).map((n) => ( 150 | setPage(n)} 153 | isActive={n === page} 154 | > 155 | {n} 156 | 157 | ))} 158 | 159 | Next 160 | 161 | 162 | 163 | ); 164 | }; 165 | -------------------------------------------------------------------------------- /src/components/ReactComponent.jsx: -------------------------------------------------------------------------------- 1 | import { Formio } from '@formio/js'; 2 | const Field = Formio.Components.components.field; 3 | 4 | export default class ReactComponent extends Field { 5 | /** 6 | * This is the first phase of component building where the component is instantiated. 7 | * 8 | * @param component - The component definition created from the settings form. 9 | * @param options - Any options passed into the renderer. 10 | * @param data - The submission data where this component's data exists. 11 | */ 12 | constructor(component, options, data) { 13 | super(component, options, data); 14 | this.reactInstance = null; 15 | } 16 | 17 | /** 18 | * This method is called any time the component needs to be rebuilt. It is most frequently used to listen to other 19 | * components using the this.on() function. 20 | */ 21 | init() { 22 | return super.init(); 23 | } 24 | 25 | /** 26 | * This method is called before the component is going to be destroyed, which is when the component instance is 27 | * destroyed. This is different from detach which is when the component instance still exists but the dom instance is 28 | * removed. 29 | */ 30 | destroy() { 31 | return super.destroy(); 32 | } 33 | /** 34 | * This method is called before a form is submitted. 35 | * It is used to perform any necessary actions or checks before the form data is sent. 36 | * 37 | */ 38 | 39 | beforeSubmit() { 40 | return super.beforeSubmit(); 41 | } 42 | 43 | /** 44 | * The second phase of component building where the component is rendered as an HTML string. 45 | * 46 | * @returns {string} - The return is the full string of the component 47 | */ 48 | render() { 49 | // For react components, we simply render as a div which will become the react instance. 50 | // By calling super.render(string) it will wrap the component with the needed wrappers to make it a full component. 51 | return super.render(`
    `); 52 | } 53 | 54 | /** 55 | * Callback ref to store a reference to the node. 56 | * 57 | * @param element - the node 58 | */ 59 | setReactInstance(element) { 60 | this.reactInstance = element; 61 | } 62 | 63 | /** 64 | * The third phase of component building where the component has been attached to the DOM as 'element' and is ready 65 | * to have its javascript events attached. 66 | * 67 | * @param element 68 | * @returns {Promise} - Return a promise that resolves when the attach is complete. 69 | */ 70 | attach(element) { 71 | super.attach(element); 72 | 73 | // The loadRefs function will find all dom elements that have the "ref" setting that match the object property. 74 | // It can load a single element or multiple elements with the same ref. 75 | this.loadRefs(element, { 76 | [`react-${this.id}`]: 'single', 77 | }); 78 | 79 | if (this.refs[`react-${this.id}`]) { 80 | this.attachReact( 81 | this.refs[`react-${this.id}`], 82 | this.setReactInstance.bind(this), 83 | ); 84 | if (this.shouldSetValue) { 85 | this.setValue(this.dataForSetting); 86 | this.updateValue(this.dataForSetting); 87 | } 88 | } 89 | return Promise.resolve(); 90 | } 91 | 92 | /** 93 | * The fourth phase of component building where the component is being removed from the page. This could be a redraw 94 | * or it is being removed from the form. 95 | */ 96 | detach() { 97 | if (this.refs[`react-${this.id}`]) { 98 | this.detachReact(this.refs[`react-${this.id}`]); 99 | } 100 | super.detach(); 101 | } 102 | 103 | /** 104 | * Override this function to insert your custom component. 105 | * 106 | * @param element 107 | * @param ref - callback ref 108 | */ 109 | attachReact(element, ref) { 110 | console.log(element, ref); 111 | return; 112 | } 113 | 114 | /** 115 | * Override this function. 116 | */ 117 | detachReact(element) { 118 | console.log(element); 119 | return; 120 | } 121 | 122 | /** 123 | * Something external has set a value and our component needs to be updated to reflect that. For example, loading a submission. 124 | * 125 | * @param value 126 | */ 127 | setValue(value) { 128 | if (this.reactInstance) { 129 | this.reactInstance.setState({ 130 | value: value, 131 | }); 132 | this.shouldSetValue = false; 133 | } else { 134 | this.shouldSetValue = true; 135 | this.dataForSetting = value; 136 | } 137 | } 138 | 139 | /** 140 | * The user has changed the value in the component and the value needs to be updated on the main submission object and other components notified of a change event. 141 | * 142 | * @param value 143 | */ 144 | updateValue = (value, flags) => { 145 | flags = flags || {}; 146 | const newValue = 147 | value === undefined || value === null ? this.getValue() : value; 148 | const changed = 149 | newValue !== undefined 150 | ? this.hasChanged(newValue, this.dataValue) 151 | : false; 152 | this.dataValue = Array.isArray(newValue) ? [...newValue] : newValue; 153 | 154 | this.updateOnChange(flags, changed); 155 | return changed; 156 | }; 157 | 158 | /** 159 | * Get the current value of the component. Should return the value set in the react component. 160 | * 161 | * @returns {*} 162 | */ 163 | getValue() { 164 | if (this.reactInstance) { 165 | return this.reactInstance.state.value; 166 | } 167 | return this.defaultValue; 168 | } 169 | 170 | /** 171 | * Override normal validation check to insert custom validation in react component. 172 | * 173 | * @param data 174 | * @param dirty 175 | * @param rowData 176 | * @returns {boolean} 177 | */ 178 | checkValidity(data, dirty, rowData) { 179 | const valid = super.checkValidity(data, dirty, rowData); 180 | if (!valid) { 181 | return false; 182 | } 183 | return this.validate(data, dirty, rowData); 184 | } 185 | 186 | /** 187 | * Do custom validation. 188 | * 189 | * @param data 190 | * @param dirty 191 | * @param rowData 192 | * @returns {boolean} 193 | */ 194 | validate(data, dirty, rowData) { 195 | console.log(data, dirty, rowData); 196 | return true; 197 | } 198 | } 199 | -------------------------------------------------------------------------------- /src/components/Report.jsx: -------------------------------------------------------------------------------- 1 | import { cloneDeep } from 'lodash/lang'; 2 | import React, { useEffect, useRef, useState } from 'react'; 3 | import PropTypes from 'prop-types'; 4 | import _isEqual from 'lodash/isEqual'; 5 | import { Formio, EventEmitter } from '@formio/js'; 6 | const FormioReport = Formio.Report; 7 | 8 | /** 9 | * @param {ReportProps} props 10 | * @returns {JSX.Element}s 11 | */ 12 | export const Report = (props) => { 13 | let instance; 14 | let createPromise; 15 | let element; 16 | const [formio, setFormio] = useState(undefined); 17 | const jsonReport = useRef(undefined); 18 | 19 | useEffect(() => () => (formio ? formio.destroy(true) : null), [formio]); 20 | 21 | const createReportInstance = (srcOrReport) => { 22 | const { options = {}, onReportReady, projectEndpoint } = props; 23 | if (projectEndpoint) { 24 | options.projectEndpoint = projectEndpoint; 25 | } 26 | 27 | instance = new FormioReport(element, srcOrReport, options); 28 | createPromise = instance.ready.then((formioInstance) => { 29 | setFormio(formioInstance); 30 | if (onReportReady) { 31 | onReportReady(formioInstance); 32 | } 33 | }); 34 | 35 | return createPromise; 36 | }; 37 | 38 | const onAnyEvent = (event, ...args) => { 39 | if (event.startsWith('formio.')) { 40 | const funcName = `on${event.charAt(7).toUpperCase()}${event.slice(8)}`; 41 | // eslint-disable-next-line no-prototype-builtins 42 | if ( 43 | props.hasOwnProperty(funcName) && 44 | typeof props[funcName] === 'function' 45 | ) { 46 | props[funcName](...args); 47 | } 48 | } 49 | }; 50 | 51 | const initializeFormio = () => { 52 | if (createPromise) { 53 | instance.onAny(onAnyEvent); 54 | } 55 | }; 56 | 57 | useEffect(() => { 58 | const { src } = props; 59 | if (src) { 60 | createReportInstance(src); 61 | initializeFormio(); 62 | } 63 | }, [props.src]); 64 | 65 | useEffect(() => { 66 | const { report } = props; 67 | // eslint-disable-next-line no-undef 68 | if (report && !_isEqual(report, jsonReport.current)) { 69 | jsonReport.current = cloneDeep(report); 70 | createReportInstance(report) 71 | .then(() => { 72 | if (formio) { 73 | formio.form = { components: [], report }; 74 | return formio; 75 | } 76 | }) 77 | .catch((err) => { 78 | console.error(err); 79 | if (formio?.form?.report) { 80 | formio.form.report = {}; 81 | } 82 | }); 83 | initializeFormio(); 84 | } 85 | }, [props.report]); 86 | 87 | useEffect(() => { 88 | const { options = {} } = props; 89 | if (!options.events) { 90 | options.events = Report.getDefaultEmitter(); 91 | } 92 | }, [props.options]); 93 | 94 | if (!FormioReport) { 95 | return ( 96 |
    97 | Report is not found in Formio. Please make sure that you are 98 | using the Formio Reporting module and it is correctly included 99 | in your application. 100 |
    101 | ); 102 | } 103 | 104 | return
    (element = el)} />; 105 | }; 106 | 107 | /** 108 | * @typedef {object} Options 109 | * @property {boolean} [readOnly] 110 | * @property {boolean} [noAlerts] 111 | * @property {object} [i18n] 112 | * @property {string} [template] 113 | * @property {string} [projectEndpoint] 114 | */ 115 | 116 | /** 117 | * @typedef {object} ReportProps 118 | * @property {string} [src] 119 | * @property {string} [projectEndpoint] 120 | * @property {object} [report] 121 | * @property {Options} [options] 122 | * @property {function} [onFormLoad] 123 | * @property {function} [onError] 124 | * @property {function} [onRender] 125 | * @property {function} [onFocus] 126 | * @property {function} [onBlur] 127 | * @property {function} [onInitialized] 128 | * @property {function} [onReportReady] 129 | * @property {function} [onChange] 130 | * @property {function} [onRowClick] 131 | * @property {function} [onRowSelectChange] 132 | * @property {function} [onFetchDataError] 133 | * @property {function} [onChangeItemsPerPage] 134 | * @property {function} [onPage] 135 | u */ 136 | Report.propTypes = { 137 | src: PropTypes.string, 138 | projectEndpoint: PropTypes.string, 139 | report: PropTypes.object, 140 | options: PropTypes.shape({ 141 | readOnly: PropTypes.bool, 142 | noAlerts: PropTypes.bool, 143 | i18n: PropTypes.object, 144 | template: PropTypes.string, 145 | language: PropTypes.string, 146 | }), 147 | onRowClick: PropTypes.func, 148 | onRowSelectChange: PropTypes.func, 149 | onFetchDataError: PropTypes.func, 150 | onChangeItemsPerPage: PropTypes.func, 151 | onPage: PropTypes.func, 152 | onChange: PropTypes.func, 153 | onFormLoad: PropTypes.func, 154 | onError: PropTypes.func, 155 | onRender: PropTypes.func, 156 | onFocus: PropTypes.func, 157 | onBlur: PropTypes.func, 158 | onInitialized: PropTypes.func, 159 | onReportReady: PropTypes.func, 160 | }; 161 | 162 | Report.getDefaultEmitter = () => { 163 | return new EventEmitter({ 164 | wildcard: false, 165 | maxListeners: 0, 166 | }); 167 | }; 168 | 169 | export default Report; 170 | -------------------------------------------------------------------------------- /src/components/SubmissionGrid.tsx: -------------------------------------------------------------------------------- 1 | import { Utils, Component } from '@formio/core'; 2 | import { useState, useEffect, useCallback, ReactNode } from 'react'; 3 | import { usePagination } from '../hooks/usePagination'; 4 | import { useFormioContext } from '../hooks/useFormioContext'; 5 | import { FormProps, FormType, JSON } from './Form'; 6 | import { ComponentProp } from './FormGrid'; 7 | 8 | type FormioPaginationResponse = FetchedSubmission[] & { 9 | serverCount: number; 10 | }; 11 | export type FetchedSubmission = NonNullable & { 12 | _id: string; 13 | }; 14 | 15 | export type SubmissionTableProps = { 16 | submissions?: FetchedSubmission[]; 17 | components?: { 18 | Container?: ComponentProp<{ children: ReactNode }>; 19 | TableContainer?: ComponentProp<{ children: ReactNode }>; 20 | TableHeadContainer?: ComponentProp<{ children: ReactNode }>; 21 | TableHeadCell?: ComponentProp<{ children: ReactNode }>; 22 | TableBodyRowContainer?: ComponentProp<{ 23 | children: ReactNode; 24 | onClick?: () => void; 25 | }>; 26 | TableHeaderRowContainer?: ComponentProp<{ children: ReactNode }>; 27 | TableBodyContainer?: ComponentProp<{ children: ReactNode }>; 28 | TableCell?: ComponentProp<{ children: ReactNode }>; 29 | PaginationContainer?: ComponentProp<{ children: ReactNode }>; 30 | PaginationButton?: ComponentProp<{ 31 | children: ReactNode; 32 | isActive?: boolean; 33 | disabled?: boolean; 34 | onClick: () => void; 35 | }>; 36 | }; 37 | onSubmissionClick?: (id: string) => void; 38 | limit: number; 39 | submissionQuery?: { 40 | [key: string]: JSON; 41 | }; 42 | formId?: string; 43 | }; 44 | type Row = { data: JSON[]; id: string }; 45 | 46 | const DEFAULT_COMPONENTS = {}; 47 | const DEFAULT_QUERY = {}; 48 | 49 | const isFormioPaginationResponse = ( 50 | obj: unknown, 51 | ): obj is FormioPaginationResponse => { 52 | return !!obj && Object.prototype.hasOwnProperty.call(obj, 'serverCount'); 53 | }; 54 | 55 | const toString = (value: JSON) => { 56 | switch (typeof value) { 57 | case 'object': 58 | case 'number': 59 | return JSON.stringify(value); 60 | default: 61 | return value; 62 | } 63 | }; 64 | const getColumnsAndCells = ( 65 | form: FormType, 66 | submissions: FetchedSubmission[], 67 | ) => { 68 | const columnsSet = new Set<{ key: string; label: string }>(); 69 | Utils.eachComponent(form.components, (component: Component) => { 70 | if ( 71 | !Object.prototype.hasOwnProperty.call(component, 'tableView') || 72 | component.tableView 73 | ) { 74 | columnsSet.add({ 75 | key: component.key, 76 | label: component.label ?? component.key, 77 | }); 78 | } 79 | }); 80 | const columns = Array.from(columnsSet); 81 | const cells: Row[] = submissions.map((submission) => { 82 | const row: JSON[] = columns.map((column) => { 83 | return submission.data?.[column.key] ?? ''; 84 | }); 85 | return { data: row, id: submission._id }; 86 | }); 87 | return { columns, cells }; 88 | }; 89 | 90 | export const SubmissionTable = ({ 91 | formId, 92 | limit, 93 | submissions, 94 | onSubmissionClick, 95 | components = DEFAULT_COMPONENTS, 96 | submissionQuery = DEFAULT_QUERY, 97 | }: SubmissionTableProps) => { 98 | const { 99 | Container = ({ children }) =>
    {children}
    , 100 | TableContainer = ({ children }) => {children}
    , 101 | TableHeadContainer = ({ children }) => {children}, 102 | TableHeaderRowContainer = ({ children }) => {children}, 103 | TableHeadCell = ({ children }) => {children}, 104 | TableBodyRowContainer = ({ children, onClick }) => ( 105 | {children} 106 | ), 107 | TableBodyContainer = ({ children }) => {children}, 108 | TableCell = ({ children }) => {children}, 109 | PaginationContainer = ({ children }) =>
      {children}
    , 110 | PaginationButton = ({ children }) =>
  • {children}
  • , 111 | } = components; 112 | const [form, setForm] = useState(); 113 | const { Formio } = useFormioContext(); 114 | const fetchFunction = useCallback( 115 | (limit: number, skip: number) => { 116 | if (!formId) { 117 | console.warn( 118 | "You're trying to fetch submissions without a form ID, did you mean to pass a submissions prop instead?", 119 | ); 120 | return Promise.resolve([]); 121 | } 122 | const formio = new Formio( 123 | `${Formio.projectUrl || Formio.baseUrl}/form/${formId}`, 124 | ); 125 | return formio.loadSubmissions({ 126 | params: { ...submissionQuery, limit, skip }, 127 | }); 128 | }, 129 | [submissionQuery, Formio, formId], 130 | ); 131 | const dataOrFnArg = submissions ? submissions : fetchFunction; 132 | const { data, total, page, nextPage, prevPage, setPage, hasMore } = 133 | usePagination(1, limit, dataOrFnArg); 134 | const { columns, cells } = form 135 | ? getColumnsAndCells(form, data) 136 | : { columns: [], cells: [] }; 137 | 138 | useEffect(() => { 139 | const fetchForm = async () => { 140 | const formio = new Formio( 141 | `${Formio.projectUrl || Formio.baseUrl}/form/${formId}`, 142 | ); 143 | setForm(await formio.loadForm()); 144 | }; 145 | fetchForm(); 146 | }, [Formio, formId]); 147 | 148 | return ( 149 | 150 | 151 | 152 | 153 | {form && 154 | columns.map(({ key, label }) => { 155 | return ( 156 | 157 | {label} 158 | 159 | ); 160 | })} 161 | 162 | 163 | 164 | {cells.map(({ data, id }, index) => ( 165 | { 168 | onSubmissionClick?.(id); 169 | }} 170 | > 171 | {form && 172 | data.map((cell, index) => ( 173 | 174 | {toString(cell)} 175 | 176 | ))} 177 | 178 | ))} 179 | 180 | 181 | 182 | 183 | Prev 184 | 185 | {isFormioPaginationResponse(data) && 186 | !total && 187 | Array.from( 188 | { 189 | length: Math.ceil(data.serverCount / limit), 190 | }, 191 | (_, i) => i + 1, 192 | ).map((n) => ( 193 | setPage(n)} 196 | isActive={n === page} 197 | > 198 | {n} 199 | 200 | ))} 201 | {data && 202 | total && 203 | Array.from( 204 | { 205 | length: Math.ceil(total / limit), 206 | }, 207 | (_, i) => i + 1, 208 | ).map((n) => ( 209 | setPage(n)} 212 | isActive={n === page} 213 | > 214 | {n} 215 | 216 | ))} 217 | {} 218 | 219 | Next 220 | 221 | 222 | 223 | ); 224 | }; 225 | -------------------------------------------------------------------------------- /src/components/__tests__/Form.test.tsx: -------------------------------------------------------------------------------- 1 | import { render } from '@testing-library/react'; 2 | import { screen } from '@testing-library/dom'; 3 | import '@testing-library/jest-dom'; 4 | 5 | import { Form } from '../Form'; 6 | 7 | const simpleForm = { 8 | display: 'form' as const, 9 | components: [ 10 | { 11 | label: 'First Name', 12 | key: 'firstName', 13 | type: 'textfield', 14 | input: true, 15 | }, 16 | { 17 | label: 'Last Name', 18 | key: 'lastName', 19 | type: 'textfield', 20 | input: true, 21 | validate: { 22 | required: true, 23 | }, 24 | }, 25 | { 26 | label: 'Submit', 27 | type: 'button', 28 | key: 'submit', 29 | input: true, 30 | disableOnInvalid: true, 31 | }, 32 | ], 33 | }; 34 | 35 | test('loads and displays a simple form', async () => { 36 | const executeTests = async () => { 37 | expect(screen.getByText('First Name')).toBeInTheDocument(); 38 | expect(screen.getByText('Last Name')).toBeInTheDocument(); 39 | expect(screen.getByText('Submit')).toBeInTheDocument(); 40 | expect(await screen.findByRole('button')).toBeDisabled(); 41 | }; 42 | render(); 43 | }); 44 | -------------------------------------------------------------------------------- /src/components/index.ts: -------------------------------------------------------------------------------- 1 | export * from './Form'; 2 | export * from './FormBuilder'; 3 | export * from './FormEdit'; 4 | export * from './FormGrid'; 5 | export * from './SubmissionGrid'; 6 | export { default as Errors } from './Errors'; 7 | export { default as ReactComponent } from './ReactComponent'; 8 | export { default as Report } from './Report'; 9 | -------------------------------------------------------------------------------- /src/constants.ts: -------------------------------------------------------------------------------- 1 | import { AllItemsPerPage } from './types'; 2 | 3 | export const defaultPageSizes = [10, 25, 50, 100, AllItemsPerPage]; 4 | -------------------------------------------------------------------------------- /src/contexts/FormioContext.tsx: -------------------------------------------------------------------------------- 1 | import { createContext, useState, useEffect } from 'react'; 2 | import { Formio as ImportedFormio } from '@formio/js'; 3 | 4 | type BaseConfigurationArgs = { 5 | baseUrl?: string; 6 | projectUrl?: string; 7 | Formio?: typeof ImportedFormio; 8 | }; 9 | 10 | const useBaseConfiguration = ({ 11 | baseUrl, 12 | projectUrl, 13 | Formio, 14 | }: BaseConfigurationArgs) => { 15 | if (!Formio) { 16 | if (baseUrl) { 17 | ImportedFormio.setBaseUrl(baseUrl); 18 | } 19 | if (projectUrl) { 20 | ImportedFormio.setProjectUrl(projectUrl); 21 | } 22 | return { 23 | Formio: ImportedFormio, 24 | baseUrl: ImportedFormio.baseUrl, 25 | projectUrl: ImportedFormio.projectUrl, 26 | }; 27 | } 28 | 29 | if (baseUrl) { 30 | Formio.setBaseUrl(baseUrl); 31 | } 32 | if (projectUrl) { 33 | Formio.setProjectUrl(projectUrl); 34 | } 35 | 36 | return { 37 | Formio, 38 | baseUrl: Formio.baseUrl, 39 | projectUrl: Formio.projectUrl, 40 | }; 41 | }; 42 | 43 | const useAuthentication = ({ Formio }: { Formio: typeof ImportedFormio }) => { 44 | const [token, setToken] = useState(Formio.getToken() || null); 45 | const [isAuthenticated, setIsAuthenticated] = useState(!!token); 46 | 47 | useEffect(() => { 48 | const handleUserEvent = async (user: unknown) => { 49 | if (user) { 50 | setToken(Formio.getToken()); 51 | setIsAuthenticated(true); 52 | } else if (isAuthenticated) { 53 | await Formio.logout(); 54 | setToken(null); 55 | setIsAuthenticated(false); 56 | } 57 | }; 58 | 59 | const handleStaleToken = async () => { 60 | if (isAuthenticated) { 61 | const user = await Formio.currentUser(); 62 | if (!user) { 63 | setToken(null); 64 | setIsAuthenticated(false); 65 | } 66 | } 67 | }; 68 | 69 | Formio.events.on('formio.user', handleUserEvent); 70 | handleStaleToken(); 71 | 72 | return () => { 73 | Formio.events.off('formio.user', handleUserEvent); 74 | }; 75 | }, [isAuthenticated, Formio]); 76 | 77 | const logout = async () => { 78 | await Formio.logout(); 79 | setToken(null); 80 | setIsAuthenticated(false); 81 | }; 82 | 83 | return { token, setToken, isAuthenticated, logout }; 84 | }; 85 | 86 | export const FormioContext = createContext< 87 | | (ReturnType & 88 | ReturnType) 89 | | null 90 | >(null); 91 | 92 | export function FormioProvider({ 93 | children, 94 | baseUrl, 95 | projectUrl, 96 | Formio, 97 | }: { children: React.ReactNode } & BaseConfigurationArgs) { 98 | const baseConfig = useBaseConfiguration({ baseUrl, projectUrl, Formio }); 99 | const auth = useAuthentication({ Formio: baseConfig.Formio }); 100 | const formio = { ...baseConfig, ...auth }; 101 | return ( 102 | 103 | {children} 104 | 105 | ); 106 | } 107 | -------------------------------------------------------------------------------- /src/hooks/__tests__/usePagination.test.ts: -------------------------------------------------------------------------------- 1 | import { renderHook } from '@testing-library/react'; 2 | import { waitFor } from '@testing-library/dom'; 3 | import { usePagination } from '../usePagination'; 4 | 5 | it('should return correct paginated data when passed a static array', () => { 6 | const myData = Array.from( 7 | { 8 | length: 100, 9 | }, 10 | (_, i) => i, 11 | ); 12 | const { result } = renderHook(() => usePagination(1, 10, myData)); 13 | const { data } = result.current; 14 | expect(data).toEqual([0, 1, 2, 3, 4, 5, 6, 7, 8, 9]); 15 | }); 16 | 17 | it('should return correct paginated data when passed a fetch function', async () => { 18 | const fakeData = Array.from({ length: 100 }, (_, i) => i); 19 | const fetchFunction = async (limit: number, skip: number) => { 20 | return fakeData.slice(skip, skip + limit); 21 | }; 22 | const { result } = renderHook(() => usePagination(1, 10, fetchFunction)); 23 | waitFor(() => { 24 | const { data } = result.current; 25 | expect(data).toEqual([0, 1, 2, 3, 4, 5, 6, 7, 8, 9]); 26 | }); 27 | }); 28 | -------------------------------------------------------------------------------- /src/hooks/useFormioContext.ts: -------------------------------------------------------------------------------- 1 | import { useContext } from 'react'; 2 | import { FormioContext } from '../contexts/FormioContext'; 3 | 4 | export function useFormioContext() { 5 | const context = useContext(FormioContext); 6 | 7 | if (!context) { 8 | throw new Error( 9 | 'useFormioContext must be used within a FormioProvider component.', 10 | ); 11 | } 12 | return context; 13 | } 14 | -------------------------------------------------------------------------------- /src/hooks/usePagination.tsx: -------------------------------------------------------------------------------- 1 | import { useState, useEffect, useCallback } from 'react'; 2 | 3 | type PaginationResult = { 4 | data: T[]; 5 | total: number | undefined; 6 | page: number; 7 | hasMore: boolean; 8 | nextPage: () => void; 9 | prevPage: () => void; 10 | setPage: (page: number) => void; 11 | fetchPage: (page: number, limit: number) => Promise; 12 | }; 13 | 14 | type FetchFunction = (limit: number, skip: number) => Promise; 15 | 16 | export function usePagination( 17 | initialPage: number, 18 | limit: number, 19 | dataOrFetchFunction: T[] | FetchFunction, 20 | ): PaginationResult { 21 | const [data, setData] = useState([]); 22 | const [page, setPage] = useState(initialPage); 23 | const [hasMore, setHasMore] = useState(true); 24 | const total: number | undefined = Array.isArray(dataOrFetchFunction) 25 | ? dataOrFetchFunction.length 26 | : undefined; 27 | let serverCount: number | undefined; 28 | 29 | const fetchPage = useCallback( 30 | async (page: number): Promise => { 31 | const skip = (page - 1) * limit; 32 | let result; 33 | if (Array.isArray(dataOrFetchFunction)) { 34 | result = dataOrFetchFunction.slice(skip, skip + limit); 35 | serverCount = dataOrFetchFunction.length; 36 | setData(result); 37 | } else { 38 | result = await dataOrFetchFunction(limit, skip); 39 | serverCount = (result as any).serverCount; 40 | setData(result); 41 | } 42 | if (serverCount !== undefined) { 43 | setHasMore(page * limit < serverCount); 44 | } else { 45 | setHasMore(result.length >= limit); 46 | } 47 | }, 48 | [limit, dataOrFetchFunction], 49 | ); 50 | 51 | const nextPage = () => { 52 | if (hasMore) { 53 | const newPage = page + 1; 54 | fetchPage(newPage); 55 | setPage(newPage); 56 | } 57 | }; 58 | 59 | const prevPage = () => { 60 | if (page > 1) { 61 | const newPage = page - 1; 62 | fetchPage(newPage); 63 | setPage(newPage); 64 | } 65 | }; 66 | 67 | useEffect(() => { 68 | fetchPage(page); 69 | }, [fetchPage, page]); 70 | 71 | return { 72 | data, 73 | page, 74 | hasMore, 75 | nextPage, 76 | prevPage, 77 | total, 78 | setPage: (page: number) => { 79 | setPage(page); 80 | fetchPage(page); 81 | }, 82 | fetchPage, 83 | }; 84 | } 85 | -------------------------------------------------------------------------------- /src/hooks/useTraceUpdate.ts: -------------------------------------------------------------------------------- 1 | import { useEffect, useRef } from 'react'; 2 | 3 | export function useTraceUpdate(props: any) { 4 | const prev = useRef(props); 5 | useEffect(() => { 6 | const changedProps = Object.entries(props).reduce((ps: any, [k, v]) => { 7 | if (prev.current[k] !== v) { 8 | ps[k] = [prev.current[k], v]; 9 | } 10 | return ps; 11 | }, {}); 12 | if (Object.keys(changedProps).length > 0) { 13 | console.log('Changed props:', changedProps); 14 | } 15 | prev.current = props; 16 | }); 17 | } 18 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import { Formio } from '@formio/js'; 2 | const Webform = Formio.Webform; 3 | const WebformBuilder = Formio.WebformBuilder; 4 | const Wizard = Formio.Wizard; 5 | const WizardBuilder = Formio.WizardBuilder; 6 | 7 | export { Webform, WebformBuilder, Wizard, WizardBuilder }; 8 | 9 | export * from './components'; 10 | export { useFormioContext } from './hooks/useFormioContext'; 11 | export { usePagination } from './hooks/usePagination'; 12 | export { FormioProvider } from './contexts/FormioContext'; 13 | export * from './constants'; 14 | export * from './modules'; 15 | export * from './types'; 16 | export * from './utils'; 17 | export { Components, Utils, Templates } from '@formio/js'; 18 | -------------------------------------------------------------------------------- /src/modules/auth/actions.js: -------------------------------------------------------------------------------- 1 | import { Formio as formiojs } from '@formio/js'; 2 | import * as type from './constants'; 3 | 4 | const requestUser = () => ({ 5 | type: type.USER_REQUEST, 6 | }); 7 | 8 | const receiveUser = (user) => ({ 9 | type: type.USER_REQUEST_SUCCESS, 10 | user, 11 | }); 12 | 13 | const failUser = (error) => ({ 14 | type: type.USER_REQUEST_FAILURE, 15 | error, 16 | }); 17 | 18 | const logoutUser = () => ({ 19 | type: type.USER_LOGOUT, 20 | }); 21 | 22 | const submissionAccessUser = (submissionAccess) => ({ 23 | type: type.USER_SUBMISSION_ACCESS, 24 | submissionAccess, 25 | }); 26 | 27 | const formAccessUser = (formAccess) => ({ 28 | type: type.USER_FORM_ACCESS, 29 | formAccess, 30 | }); 31 | 32 | const projectAccessUser = (projectAccess) => ({ 33 | type: type.USER_PROJECT_ACCESS, 34 | projectAccess, 35 | }); 36 | 37 | const rolesUser = (roles) => ({ 38 | type: type.USER_ROLES, 39 | roles, 40 | }); 41 | 42 | function transformSubmissionAccess(forms) { 43 | return Object.values(forms).reduce( 44 | (result, form) => ({ 45 | ...result, 46 | [form.name]: form.submissionAccess.reduce( 47 | (formSubmissionAccess, access) => ({ 48 | ...formSubmissionAccess, 49 | [access.type]: access.roles, 50 | }), 51 | {}, 52 | ), 53 | }), 54 | {}, 55 | ); 56 | } 57 | 58 | function transformFormAccess(forms) { 59 | return Object.values(forms).reduce( 60 | (result, form) => ({ 61 | ...result, 62 | [form.name]: form.access.reduce( 63 | (formAccess, access) => ({ 64 | ...formAccess, 65 | [access.type]: access.roles, 66 | }), 67 | {}, 68 | ), 69 | }), 70 | {}, 71 | ); 72 | } 73 | 74 | function transformProjectAccess(projectAccess) { 75 | return projectAccess.reduce( 76 | (result, access) => ({ 77 | ...result, 78 | [access.type]: access.roles, 79 | }), 80 | {}, 81 | ); 82 | } 83 | 84 | export const initAuth = () => (dispatch) => { 85 | const projectUrl = formiojs.getProjectUrl(); 86 | 87 | dispatch(requestUser()); 88 | 89 | Promise.all([ 90 | formiojs.currentUser(), 91 | formiojs 92 | .makeStaticRequest(`${projectUrl}/access`) 93 | .then((result) => { 94 | const submissionAccess = transformSubmissionAccess( 95 | result.forms, 96 | ); 97 | const formAccess = transformFormAccess(result.forms); 98 | 99 | dispatch(submissionAccessUser(submissionAccess)); 100 | dispatch(formAccessUser(formAccess)); 101 | dispatch(rolesUser(result.roles)); 102 | }) 103 | .catch(() => {}), 104 | formiojs 105 | .makeStaticRequest(projectUrl) 106 | .then((project) => { 107 | const projectAccess = transformProjectAccess(project.access); 108 | dispatch(projectAccessUser(projectAccess)); 109 | }) 110 | .catch(() => {}), 111 | ]) 112 | .then(([user]) => { 113 | if (user) { 114 | dispatch(receiveUser(user)); 115 | } else { 116 | dispatch(logoutUser()); 117 | } 118 | }) 119 | .catch((result) => { 120 | dispatch(failUser(result)); 121 | }); 122 | }; 123 | 124 | export const setUser = (user) => (dispatch) => { 125 | formiojs.setUser(user); 126 | dispatch(receiveUser(user)); 127 | }; 128 | 129 | export const logout = () => (dispatch) => { 130 | formiojs.logout().then(() => { 131 | dispatch(logoutUser()); 132 | }); 133 | }; 134 | -------------------------------------------------------------------------------- /src/modules/auth/constants.js: -------------------------------------------------------------------------------- 1 | export const USER_REQUEST = 'USER_REQUEST'; 2 | export const USER_REQUEST_SUCCESS = 'USER_REQUEST_SUCCESS'; 3 | export const USER_REQUEST_FAILURE = 'USER_REQUEST_FAILURE'; 4 | export const USER_LOGOUT = 'USER_LOGOUT'; 5 | export const USER_SUBMISSION_ACCESS = 'USER_SUBMISSION_ACCESS'; 6 | export const USER_FORM_ACCESS = 'USER_FORM_ACCESS'; 7 | export const USER_PROJECT_ACCESS = 'USER_PROJECT_ACCESS'; 8 | export const USER_ROLES = 'USER_ROLES'; 9 | -------------------------------------------------------------------------------- /src/modules/auth/index.js: -------------------------------------------------------------------------------- 1 | export * from './actions'; 2 | export * from './constants'; 3 | export * from './reducers'; 4 | export * from './selectors'; 5 | -------------------------------------------------------------------------------- /src/modules/auth/reducers.js: -------------------------------------------------------------------------------- 1 | import * as type from './constants'; 2 | 3 | const initialState = { 4 | init: false, 5 | isActive: false, 6 | user: null, 7 | authenticated: false, 8 | submissionAccess: {}, 9 | formAccess: {}, 10 | projectAccess: {}, 11 | roles: {}, 12 | is: {}, 13 | error: '', 14 | }; 15 | 16 | function mapProjectRolesToUserRoles(projectRoles, userRoles) { 17 | return Object.entries(projectRoles).reduce( 18 | (result, [name, role]) => ({ 19 | ...result, 20 | [name]: userRoles.includes(role._id), 21 | }), 22 | {}, 23 | ); 24 | } 25 | 26 | function getUserRoles(projectRoles) { 27 | return Object.keys(projectRoles).reduce( 28 | (result, name) => ({ 29 | ...result, 30 | [name]: name === 'anonymous', 31 | }), 32 | {}, 33 | ); 34 | } 35 | 36 | export const auth = 37 | () => 38 | (state = initialState, action) => { 39 | switch (action.type) { 40 | case type.USER_REQUEST: 41 | return { 42 | ...state, 43 | init: true, 44 | submissionAccess: false, 45 | isActive: true, 46 | }; 47 | case type.USER_REQUEST_SUCCESS: 48 | return { 49 | ...state, 50 | isActive: false, 51 | user: action.user, 52 | authenticated: true, 53 | is: mapProjectRolesToUserRoles( 54 | state.roles, 55 | action.user.roles, 56 | ), 57 | error: '', 58 | }; 59 | case type.USER_REQUEST_FAILURE: 60 | return { 61 | ...state, 62 | isActive: false, 63 | is: getUserRoles(state.roles), 64 | error: action.error, 65 | }; 66 | case type.USER_LOGOUT: 67 | return { 68 | ...state, 69 | user: null, 70 | isActive: false, 71 | authenticated: false, 72 | is: getUserRoles(state.roles), 73 | error: '', 74 | }; 75 | case type.USER_SUBMISSION_ACCESS: 76 | return { 77 | ...state, 78 | submissionAccess: action.submissionAccess, 79 | }; 80 | case type.USER_FORM_ACCESS: 81 | return { 82 | ...state, 83 | formAccess: action.formAccess, 84 | }; 85 | case type.USER_PROJECT_ACCESS: 86 | return { 87 | ...state, 88 | projectAccess: action.projectAccess, 89 | }; 90 | case type.USER_ROLES: 91 | return { 92 | ...state, 93 | roles: action.roles, 94 | }; 95 | default: 96 | return state; 97 | } 98 | }; 99 | -------------------------------------------------------------------------------- /src/modules/auth/selectors.js: -------------------------------------------------------------------------------- 1 | export default {}; 2 | -------------------------------------------------------------------------------- /src/modules/form/actions.js: -------------------------------------------------------------------------------- 1 | import { Formio as Formiojs } from '@formio/js'; 2 | import * as types from './constants'; 3 | import { selectForm } from './selectors'; 4 | 5 | export const clearFormError = (name) => ({ 6 | type: types.FORM_CLEAR_ERROR, 7 | name, 8 | }); 9 | 10 | const requestForm = (name, id, url) => ({ 11 | type: types.FORM_REQUEST, 12 | name, 13 | id, 14 | url, 15 | }); 16 | 17 | const receiveForm = (name, form, url) => ({ 18 | type: types.FORM_SUCCESS, 19 | form, 20 | name, 21 | url, 22 | }); 23 | 24 | const failForm = (name, err) => ({ 25 | type: types.FORM_FAILURE, 26 | error: err, 27 | name, 28 | }); 29 | 30 | export const resetForm = (name) => ({ 31 | type: types.FORM_RESET, 32 | name, 33 | }); 34 | 35 | const sendForm = (name, form) => ({ 36 | type: types.FORM_SAVE, 37 | form, 38 | name, 39 | }); 40 | 41 | export const getForm = (name, id = '', done = () => {}) => { 42 | return (dispatch, getState) => { 43 | // Check to see if the form is already loaded. 44 | const form = selectForm(name, getState()); 45 | if ( 46 | form.components && 47 | Array.isArray(form.components) && 48 | form.components.length && 49 | form._id === id 50 | ) { 51 | return; 52 | } 53 | 54 | const path = `${Formiojs.getProjectUrl()}/${id ? `form/${id}` : name}`; 55 | const formio = new Formiojs(path); 56 | 57 | dispatch(requestForm(name, id, path)); 58 | 59 | return formio 60 | .loadForm() 61 | .then((result) => { 62 | dispatch(receiveForm(name, result)); 63 | done(null, result); 64 | }) 65 | .catch((result) => { 66 | dispatch(failForm(name, result)); 67 | done(result); 68 | }); 69 | }; 70 | }; 71 | 72 | export const saveForm = (name, form, done = () => {}) => { 73 | return (dispatch) => { 74 | dispatch(sendForm(name, form)); 75 | 76 | const id = form._id; 77 | const path = `${Formiojs.getProjectUrl()}/form${id ? `/${id}` : ''}`; 78 | const formio = new Formiojs(path); 79 | 80 | formio 81 | .saveForm(form) 82 | .then((result) => { 83 | const url = `${Formiojs.getProjectUrl()}/form/${result._id}`; 84 | dispatch(receiveForm(name, result, url)); 85 | done(null, result); 86 | }) 87 | .catch((result) => { 88 | dispatch(failForm(name, result)); 89 | done(result); 90 | }); 91 | }; 92 | }; 93 | 94 | export const deleteForm = (name, id, done = () => {}) => { 95 | return (dispatch) => { 96 | const path = `${Formiojs.getProjectUrl()}/form/${id}`; 97 | const formio = new Formiojs(path); 98 | 99 | return formio 100 | .deleteForm() 101 | .then(() => { 102 | dispatch(resetForm(name)); 103 | done(); 104 | }) 105 | .catch((result) => { 106 | dispatch(failForm(name, result)); 107 | done(result); 108 | }); 109 | }; 110 | }; 111 | -------------------------------------------------------------------------------- /src/modules/form/constants.js: -------------------------------------------------------------------------------- 1 | export const FORM_CLEAR_ERROR = 'FORM_CLEAR_ERROR'; 2 | export const FORM_REQUEST = 'FORM_REQUEST'; 3 | export const FORM_SUCCESS = 'FORM_SUCCESS'; 4 | export const FORM_FAILURE = 'FORM_FAILURE'; 5 | export const FORM_SAVE = 'FORM_SAVE'; 6 | export const FORM_RESET = 'FORM_RESET'; 7 | -------------------------------------------------------------------------------- /src/modules/form/index.js: -------------------------------------------------------------------------------- 1 | export * from './actions'; 2 | export * from './constants'; 3 | export * from './reducers'; 4 | export * from './selectors'; 5 | -------------------------------------------------------------------------------- /src/modules/form/reducers.js: -------------------------------------------------------------------------------- 1 | import * as types from './constants'; 2 | 3 | export function form(config) { 4 | const initialState = { 5 | id: '', 6 | isActive: false, 7 | lastUpdated: 0, 8 | form: {}, 9 | url: '', 10 | error: '', 11 | }; 12 | 13 | return (state = initialState, action) => { 14 | // Only proceed for this form. 15 | if (action.name !== config.name) { 16 | return state; 17 | } 18 | switch (action.type) { 19 | case types.FORM_CLEAR_ERROR: 20 | return { 21 | ...state, 22 | error: '', 23 | }; 24 | case types.FORM_REQUEST: 25 | return { 26 | ...state, 27 | isActive: true, 28 | id: action.id, 29 | form: {}, 30 | url: action.url, 31 | error: '', 32 | }; 33 | case types.FORM_SUCCESS: 34 | return { 35 | ...state, 36 | isActive: false, 37 | id: action.form._id, 38 | form: action.form, 39 | url: action.url || state.url, 40 | error: '', 41 | }; 42 | case types.FORM_FAILURE: 43 | return { 44 | ...state, 45 | isActive: false, 46 | isInvalid: true, 47 | error: action.error, 48 | }; 49 | case types.FORM_SAVE: 50 | return { 51 | ...state, 52 | isActive: true, 53 | }; 54 | case types.FORM_RESET: 55 | return initialState; 56 | default: 57 | return state; 58 | } 59 | }; 60 | } 61 | -------------------------------------------------------------------------------- /src/modules/form/selectors.js: -------------------------------------------------------------------------------- 1 | import { selectRoot } from '../root'; 2 | 3 | export const selectForm = (name, state) => selectRoot(name, state).form; 4 | -------------------------------------------------------------------------------- /src/modules/forms/actions.js: -------------------------------------------------------------------------------- 1 | import { Formio as Formiojs } from '@formio/js'; 2 | 3 | import { selectRoot } from '../root'; 4 | 5 | import * as types from './constants'; 6 | 7 | export const resetForms = (name) => ({ 8 | type: types.FORMS_RESET, 9 | name, 10 | }); 11 | 12 | const requestForms = (name, page, params) => ({ 13 | type: types.FORMS_REQUEST, 14 | name, 15 | page, 16 | params, 17 | }); 18 | 19 | const receiveForms = (name, forms) => ({ 20 | type: types.FORMS_SUCCESS, 21 | name, 22 | forms, 23 | }); 24 | 25 | const failForms = (name, error) => ({ 26 | type: types.FORMS_FAILURE, 27 | name, 28 | error, 29 | }); 30 | 31 | export const indexForms = 32 | (name, page = 1, params = {}, done = () => {}) => 33 | (dispatch, getState) => { 34 | dispatch(requestForms(name, page, params)); 35 | 36 | const { limit, query, select, sort } = selectRoot(name, getState()); 37 | const formio = new Formiojs(`${Formiojs.getProjectUrl()}/form`); 38 | const requestParams = { ...query, ...params }; 39 | 40 | // Ten is the default so if set to 10, don't send. 41 | if (limit !== 10) { 42 | requestParams.limit = limit; 43 | } else { 44 | delete requestParams.limit; 45 | } 46 | 47 | if (page !== 1) { 48 | requestParams.skip = (page - 1) * limit; 49 | } else { 50 | delete requestParams.skip; 51 | } 52 | 53 | if (select) { 54 | requestParams.select = select; 55 | } else { 56 | delete requestParams.select; 57 | } 58 | 59 | if (sort) { 60 | requestParams.sort = sort; 61 | } else { 62 | delete requestParams.sort; 63 | } 64 | 65 | return formio 66 | .loadForms({ params: requestParams }) 67 | .then((result) => { 68 | dispatch(receiveForms(name, result)); 69 | done(null, result); 70 | }) 71 | .catch((error) => { 72 | dispatch(failForms(name, error)); 73 | done(error); 74 | }); 75 | }; 76 | -------------------------------------------------------------------------------- /src/modules/forms/constants.js: -------------------------------------------------------------------------------- 1 | export const FORMS_RESET = 'FORMS_RESET'; 2 | export const FORMS_REQUEST = 'FORMS_REQUEST'; 3 | export const FORMS_SUCCESS = 'FORMS_SUCCESS'; 4 | export const FORMS_FAILURE = 'FORMS_FAILURE'; 5 | -------------------------------------------------------------------------------- /src/modules/forms/index.js: -------------------------------------------------------------------------------- 1 | export * from './actions'; 2 | export * from './constants'; 3 | export * from './reducers'; 4 | export * from './selectors'; 5 | -------------------------------------------------------------------------------- /src/modules/forms/reducers.js: -------------------------------------------------------------------------------- 1 | import _pick from 'lodash/pick'; 2 | 3 | import * as types from './constants'; 4 | 5 | export function forms({ 6 | name, 7 | limit = 10, 8 | query = {}, 9 | select = '', 10 | sort = '', 11 | }) { 12 | const initialState = { 13 | error: '', 14 | forms: [], 15 | isActive: false, 16 | limit, 17 | pagination: { 18 | numPages: 0, 19 | page: 1, 20 | total: 0, 21 | }, 22 | query, 23 | select, 24 | sort, 25 | }; 26 | 27 | return (state = initialState, action) => { 28 | // Only proceed for this forms. 29 | if (action.name !== name) { 30 | return state; 31 | } 32 | 33 | switch (action.type) { 34 | case types.FORMS_RESET: 35 | return initialState; 36 | case types.FORMS_REQUEST: 37 | return { 38 | ...state, 39 | ..._pick(action.params, [ 40 | 'limit', 41 | 'query', 42 | 'select', 43 | 'sort', 44 | ]), 45 | error: '', 46 | forms: [], 47 | isActive: true, 48 | pagination: { 49 | ...state.pagination, 50 | page: action.page, 51 | }, 52 | }; 53 | case types.FORMS_SUCCESS: { 54 | const total = action.forms.serverCount; 55 | 56 | return { 57 | ...state, 58 | forms: action.forms, 59 | isActive: false, 60 | pagination: { 61 | ...state.pagination, 62 | numPages: Math.ceil(total / state.limit), 63 | total, 64 | }, 65 | }; 66 | } 67 | case types.FORMS_FAILURE: 68 | return { 69 | ...state, 70 | error: action.error, 71 | isActive: false, 72 | }; 73 | default: 74 | return state; 75 | } 76 | }; 77 | } 78 | -------------------------------------------------------------------------------- /src/modules/forms/selectors.js: -------------------------------------------------------------------------------- 1 | import { selectRoot } from '../root'; 2 | 3 | export const selectForms = (name, state) => selectRoot(name, state).forms; 4 | -------------------------------------------------------------------------------- /src/modules/index.js: -------------------------------------------------------------------------------- 1 | export * from './auth'; 2 | export * from './form'; 3 | export * from './forms'; 4 | export * from './root'; 5 | export * from './submission'; 6 | export * from './submissions'; 7 | -------------------------------------------------------------------------------- /src/modules/root/index.js: -------------------------------------------------------------------------------- 1 | export * from './selectors'; 2 | -------------------------------------------------------------------------------- /src/modules/root/selectors.js: -------------------------------------------------------------------------------- 1 | export const selectRoot = (name, state) => state[name]; 2 | export const selectError = (name, state) => selectRoot(name, state).error; 3 | export const selectIsActive = (name, state) => selectRoot(name, state).isActive; 4 | -------------------------------------------------------------------------------- /src/modules/submission/actions.js: -------------------------------------------------------------------------------- 1 | import { Formio as Formiojs } from '@formio/js'; 2 | 3 | import * as types from './constants'; 4 | 5 | export const clearSubmissionError = (name) => ({ 6 | type: types.SUBMISSION_CLEAR_ERROR, 7 | name, 8 | }); 9 | 10 | const requestSubmission = (name, id, formId, url) => ({ 11 | type: types.SUBMISSION_REQUEST, 12 | name, 13 | id, 14 | formId, 15 | url, 16 | }); 17 | 18 | const sendSubmission = (name) => ({ 19 | type: types.SUBMISSION_SAVE, 20 | name, 21 | }); 22 | 23 | const receiveSubmission = (name, submission, url) => ({ 24 | type: types.SUBMISSION_SUCCESS, 25 | name, 26 | submission, 27 | url, 28 | }); 29 | 30 | const failSubmission = (name, error) => ({ 31 | type: types.SUBMISSION_FAILURE, 32 | name, 33 | error, 34 | }); 35 | 36 | export const resetSubmission = (name) => ({ 37 | type: types.SUBMISSION_RESET, 38 | name, 39 | }); 40 | 41 | export const getSubmission = 42 | (name, id, formId, done = () => {}) => 43 | (dispatch, getState) => { 44 | // Check to see if the submission is already loaded. 45 | if (getState().id === id) { 46 | return; 47 | } 48 | 49 | const url = `${Formiojs.getProjectUrl()}/${formId ? `form/${formId}` : name}/submission/${id}`; 50 | const formio = new Formiojs(url); 51 | 52 | dispatch(requestSubmission(name, id, formId, url)); 53 | 54 | formio 55 | .loadSubmission() 56 | .then((result) => { 57 | dispatch(receiveSubmission(name, result)); 58 | done(null, result); 59 | }) 60 | .catch((error) => { 61 | dispatch(failSubmission(name, error)); 62 | done(error); 63 | }); 64 | }; 65 | 66 | export const saveSubmission = 67 | (name, data, formId, done = () => {}) => 68 | (dispatch) => { 69 | dispatch(sendSubmission(name, data)); 70 | 71 | const id = data._id; 72 | 73 | const formio = new Formiojs( 74 | `${Formiojs.getProjectUrl()}/${formId ? `form/${formId}` : name}/submission${id ? `/${id}` : ''}`, 75 | ); 76 | 77 | formio 78 | .saveSubmission(data) 79 | .then((result) => { 80 | const url = `${Formiojs.getProjectUrl()}/${formId ? `form/${formId}` : name}/submission/${result._id}`; 81 | dispatch(receiveSubmission(name, result, url)); 82 | done(null, result); 83 | }) 84 | .catch((error) => { 85 | dispatch(failSubmission(name, error)); 86 | done(error); 87 | }); 88 | }; 89 | 90 | export const deleteSubmission = 91 | (name, id, formId, done = () => {}) => 92 | (dispatch) => { 93 | const formio = new Formiojs( 94 | `${Formiojs.getProjectUrl()}/${formId ? `form/${formId}` : name}/submission/${id}`, 95 | ); 96 | 97 | return formio 98 | .deleteSubmission() 99 | .then(() => { 100 | dispatch(resetSubmission(name)); 101 | done(null, true); 102 | }) 103 | .catch((error) => { 104 | dispatch(failSubmission(name, error)); 105 | done(error); 106 | }); 107 | }; 108 | -------------------------------------------------------------------------------- /src/modules/submission/constants.js: -------------------------------------------------------------------------------- 1 | export const SUBMISSION_CLEAR_ERROR = 'SUBMISSION_CLEAR_ERROR'; 2 | export const SUBMISSION_REQUEST = 'SUBMISSION_REQUEST'; 3 | export const SUBMISSION_SAVE = 'SUBMISSION_SAVE'; 4 | export const SUBMISSION_SUCCESS = 'SUBMISSION_SUCCESS'; 5 | export const SUBMISSION_FAILURE = 'SUBMISSION_FAILURE'; 6 | export const SUBMISSION_RESET = 'SUBMISSION_RESET'; 7 | -------------------------------------------------------------------------------- /src/modules/submission/index.js: -------------------------------------------------------------------------------- 1 | export * from './actions'; 2 | export * from './constants'; 3 | export * from './reducers'; 4 | export * from './selectors'; 5 | -------------------------------------------------------------------------------- /src/modules/submission/reducers.js: -------------------------------------------------------------------------------- 1 | import * as types from './constants'; 2 | 3 | export function submission(config) { 4 | const initialState = { 5 | formId: '', 6 | id: '', 7 | isActive: false, 8 | lastUpdated: 0, 9 | submission: {}, 10 | url: '', 11 | error: '', 12 | }; 13 | 14 | return (state = initialState, action) => { 15 | // Only proceed for this form. 16 | if (action.name !== config.name) { 17 | return state; 18 | } 19 | switch (action.type) { 20 | case types.SUBMISSION_CLEAR_ERROR: 21 | return { 22 | ...state, 23 | error: '', 24 | }; 25 | case types.SUBMISSION_REQUEST: 26 | return { 27 | ...state, 28 | formId: action.formId, 29 | id: action.id, 30 | url: action.url, 31 | submission: {}, 32 | isActive: true, 33 | }; 34 | case types.SUBMISSION_SAVE: 35 | return { 36 | ...state, 37 | formId: action.formId, 38 | id: action.id, 39 | url: action.url || state.url, 40 | submission: {}, 41 | isActive: true, 42 | }; 43 | case types.SUBMISSION_SUCCESS: 44 | return { 45 | ...state, 46 | id: action.submission._id, 47 | submission: action.submission, 48 | isActive: false, 49 | error: '', 50 | }; 51 | case types.SUBMISSION_FAILURE: 52 | return { 53 | ...state, 54 | isActive: false, 55 | isInvalid: true, 56 | error: action.error, 57 | }; 58 | case types.SUBMISSION_RESET: 59 | return initialState; 60 | default: 61 | return state; 62 | } 63 | }; 64 | } 65 | -------------------------------------------------------------------------------- /src/modules/submission/selectors.js: -------------------------------------------------------------------------------- 1 | import { selectRoot } from '../root'; 2 | 3 | export const selectSubmission = (name, state) => 4 | selectRoot(name, state).submission; 5 | -------------------------------------------------------------------------------- /src/modules/submissions/actions.js: -------------------------------------------------------------------------------- 1 | import { Formio as Formiojs } from '@formio/js'; 2 | 3 | import { selectRoot } from '../root'; 4 | 5 | import * as types from './constants'; 6 | 7 | export const resetSubmissions = (name) => ({ 8 | type: types.SUBMISSIONS_RESET, 9 | name, 10 | }); 11 | 12 | const requestSubmissions = (name, page, params, formId) => ({ 13 | type: types.SUBMISSIONS_REQUEST, 14 | name, 15 | page, 16 | params, 17 | formId, 18 | }); 19 | 20 | const receiveSubmissions = (name, submissions) => ({ 21 | type: types.SUBMISSIONS_SUCCESS, 22 | name, 23 | submissions, 24 | }); 25 | 26 | const failSubmissions = (name, error) => ({ 27 | type: types.SUBMISSIONS_FAILURE, 28 | name, 29 | error, 30 | }); 31 | 32 | export const getSubmissions = 33 | (name, page = 0, params = {}, formId, done = () => {}) => 34 | (dispatch, getState) => { 35 | dispatch(requestSubmissions(name, page, params, formId)); 36 | 37 | const { limit, query, select, sort } = selectRoot(name, getState()); 38 | const formio = new Formiojs( 39 | `${Formiojs.getProjectUrl()}/${formId ? `form/${formId}` : name}/submission`, 40 | ); 41 | const requestParams = { ...query, ...params }; 42 | 43 | // Ten is the default so if set to 10, don't send. 44 | if (limit !== 10) { 45 | requestParams.limit = limit; 46 | } else { 47 | delete requestParams.limit; 48 | } 49 | 50 | if (page !== 1) { 51 | requestParams.skip = (page - 1) * limit; 52 | } else { 53 | delete requestParams.skip; 54 | } 55 | 56 | if (select) { 57 | requestParams.select = select; 58 | } else { 59 | delete requestParams.select; 60 | } 61 | 62 | if (sort) { 63 | requestParams.sort = sort; 64 | } else { 65 | delete requestParams.sort; 66 | } 67 | 68 | return formio 69 | .loadSubmissions({ params: requestParams }) 70 | .then((result) => { 71 | dispatch(receiveSubmissions(name, result)); 72 | done(null, result); 73 | }) 74 | .catch((error) => { 75 | dispatch(failSubmissions(name, error)); 76 | done(error); 77 | }); 78 | }; 79 | -------------------------------------------------------------------------------- /src/modules/submissions/constants.js: -------------------------------------------------------------------------------- 1 | export const SUBMISSIONS_RESET = 'SUBMISSIONS_RESET'; 2 | export const SUBMISSIONS_REQUEST = 'SUBMISSIONS_REQUEST'; 3 | export const SUBMISSIONS_SUCCESS = 'SUBMISSIONS_SUCCESS'; 4 | export const SUBMISSIONS_FAILURE = 'SUBMISSIONS_FAILURE'; 5 | -------------------------------------------------------------------------------- /src/modules/submissions/index.js: -------------------------------------------------------------------------------- 1 | export * from './actions'; 2 | export * from './constants'; 3 | export * from './reducers'; 4 | export * from './selectors'; 5 | -------------------------------------------------------------------------------- /src/modules/submissions/reducers.js: -------------------------------------------------------------------------------- 1 | import _pick from 'lodash/pick'; 2 | 3 | import * as types from './constants'; 4 | 5 | export function submissions({ 6 | name, 7 | limit = 10, 8 | query = {}, 9 | select = '', 10 | sort = '', 11 | }) { 12 | const initialState = { 13 | error: '', 14 | formId: '', 15 | isActive: false, 16 | limit, 17 | pagination: { 18 | numPages: 0, 19 | page: 1, 20 | total: 0, 21 | }, 22 | query, 23 | select, 24 | sort, 25 | submissions: [], 26 | }; 27 | 28 | return (state = initialState, action) => { 29 | // Only proceed for this submissions. 30 | if (action.name !== name) { 31 | return state; 32 | } 33 | 34 | switch (action.type) { 35 | case types.SUBMISSIONS_RESET: 36 | return initialState; 37 | case types.SUBMISSIONS_REQUEST: 38 | return { 39 | ...state, 40 | ..._pick(action.params, [ 41 | 'limit', 42 | 'query', 43 | 'select', 44 | 'sort', 45 | ]), 46 | error: '', 47 | formId: action.formId, 48 | isActive: true, 49 | pagination: { 50 | ...state.pagination, 51 | page: action.page, 52 | }, 53 | submissions: [], 54 | }; 55 | case types.SUBMISSIONS_SUCCESS: { 56 | const total = action.submissions.serverCount; 57 | 58 | return { 59 | ...state, 60 | isActive: false, 61 | pagination: { 62 | ...state.pagination, 63 | numPages: Math.ceil(total / state.limit), 64 | total, 65 | }, 66 | submissions: action.submissions, 67 | }; 68 | } 69 | case types.SUBMISSIONS_FAILURE: 70 | return { 71 | ...state, 72 | error: action.error, 73 | isActive: false, 74 | }; 75 | default: 76 | return state; 77 | } 78 | }; 79 | } 80 | -------------------------------------------------------------------------------- /src/modules/submissions/selectors.js: -------------------------------------------------------------------------------- 1 | import { selectRoot } from '../root'; 2 | 3 | export const selectSubmissions = (name, state) => 4 | selectRoot(name, state).submissions; 5 | -------------------------------------------------------------------------------- /src/types.js: -------------------------------------------------------------------------------- 1 | import PropTypes from 'prop-types'; 2 | 3 | export const AllItemsPerPage = 'all'; 4 | 5 | /** 6 | * @typedef Column 7 | * @type {object} 8 | * @property {string} key 9 | * @property {(boolean|string|Function)} sort 10 | * @property {string} title 11 | * @property {Function} value 12 | * @property {number} width 13 | */ 14 | 15 | /** 16 | * @constant 17 | * @type {Column} 18 | */ 19 | export const Column = PropTypes.shape({ 20 | key: PropTypes.string.isRequired, 21 | sort: PropTypes.oneOfType([ 22 | PropTypes.bool, 23 | PropTypes.string, 24 | PropTypes.func, 25 | ]), 26 | title: PropTypes.string, 27 | value: PropTypes.func, 28 | width: PropTypes.number, 29 | }); 30 | 31 | /** 32 | * @constant 33 | * @type {Column[]} 34 | */ 35 | export const Columns = PropTypes.arrayOf(Column); 36 | 37 | /** 38 | * @typedef Operation 39 | * @type {object} 40 | * @property {string} [action] 41 | * @property {string} [buttonType] 42 | * @property {string} [icon] 43 | * @property {Function} [permissionsResolver] 44 | * @property {string} [title] 45 | */ 46 | 47 | /** 48 | * @constant 49 | * @type {Operation} 50 | */ 51 | export const Operation = PropTypes.shape({ 52 | action: PropTypes.string.isRequired, 53 | buttonType: PropTypes.string, 54 | icon: PropTypes.string, 55 | permissionsResolver: PropTypes.func, 56 | title: PropTypes.string, 57 | }); 58 | 59 | /** 60 | * @constant 61 | * @type {Operation[]} 62 | */ 63 | export const Operations = PropTypes.arrayOf(Operation); 64 | 65 | /** 66 | * @typedef LabelValue 67 | * @type {object} 68 | * @property {string} label 69 | * @property {number} value 70 | */ 71 | 72 | /** 73 | * @constant 74 | * @type {(number|LabelValue)} 75 | */ 76 | export const PageSize = PropTypes.oneOfType([ 77 | PropTypes.number, 78 | PropTypes.shape({ 79 | label: PropTypes.string, 80 | value: PropTypes.number, 81 | }), 82 | PropTypes.oneOf([AllItemsPerPage]), 83 | ]); 84 | 85 | /** 86 | * @constant 87 | * @type {PageSize[]} 88 | */ 89 | export const PageSizes = PropTypes.arrayOf(PageSize); 90 | -------------------------------------------------------------------------------- /src/utils.js: -------------------------------------------------------------------------------- 1 | import { Components } from '@formio/js'; 2 | import _get from 'lodash/get'; 3 | 4 | export const getComponentDefaultColumn = (component) => ({ 5 | component: Components.create(component, null, null, true), 6 | key: `data.${component.key}`, 7 | sort: true, 8 | title: component.label || component.title || component.key, 9 | value(submission) { 10 | const cellValue = _get(submission, this.key, null); 11 | 12 | if (cellValue === null) { 13 | return ''; 14 | } 15 | 16 | const rendered = this.component.asString(cellValue); 17 | if (cellValue !== rendered) { 18 | return { 19 | content: rendered, 20 | isHtml: true, 21 | }; 22 | } 23 | 24 | return cellValue; 25 | }, 26 | }); 27 | 28 | /** 29 | * @param {import('./types').Column[]} columns 30 | */ 31 | export function setColumnsWidth(columns) { 32 | if (columns.length > 6) { 33 | columns.forEach((column) => { 34 | column.width = 2; 35 | }); 36 | } else { 37 | const columnsAmount = columns.length; 38 | const rowWidth = 12; 39 | const basewidth = Math.floor(rowWidth / columnsAmount); 40 | const remainingWidth = rowWidth - basewidth * columnsAmount; 41 | 42 | columns.forEach((column, index) => { 43 | column.width = index < remainingWidth ? basewidth + 1 : basewidth; 44 | }); 45 | } 46 | } 47 | 48 | /** 49 | * @param {Function} fn 50 | * @returns {(function(*): void)|*} 51 | */ 52 | export const stopPropagationWrapper = (fn) => (event) => { 53 | event.stopPropagation(); 54 | fn(); 55 | }; 56 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "NodeNext", 4 | "outDir": "lib", 5 | "allowJs": true, 6 | "jsx": "react-jsx", 7 | "declaration": true, 8 | "declarationMap": true, 9 | "target": "es2015", 10 | "baseUrl": "src", 11 | "strict": true, 12 | "skipLibCheck": true, 13 | "esModuleInterop": true, 14 | }, 15 | "include": ["src/**/*"], 16 | "types": [ 17 | "node", 18 | "jest", 19 | "@testing-library/jest-dom", 20 | "react", 21 | "react-dom", 22 | ], 23 | "exclude": ["node_modules", "lib", "test"], 24 | } 25 | -------------------------------------------------------------------------------- /webpack.test.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | // mode: 'development', 3 | module: { 4 | rules: [ 5 | { 6 | test: /\.jsx?$/, 7 | exclude: /node_modules/, 8 | loader: 'babel-loader', 9 | options: { 10 | presets: [ 11 | ['es2015', { modules: false }], 12 | 'react', 13 | 'stage-2', 14 | ], 15 | }, 16 | }, 17 | ], 18 | }, 19 | }; 20 | --------------------------------------------------------------------------------