├── .babelrc ├── .circleci └── config.yml ├── .eslintrc ├── .gitignore ├── .vscode └── settings.json ├── Changelog.md ├── LICENSE ├── README.md ├── circle.yml ├── formio.css ├── package-lock.json ├── package.json ├── src ├── components │ ├── FormBuilder.js │ ├── FormComponents │ │ ├── button │ │ │ └── Button.js │ │ ├── columns │ │ │ ├── Columns.js │ │ │ └── styles.js │ │ ├── componentUtils │ │ │ ├── getDefaultValue.js │ │ │ ├── safeSingleToMultiple.js │ │ │ └── validators.js │ │ ├── content │ │ │ ├── Content.js │ │ │ └── styles.js │ │ ├── datetime │ │ │ ├── Datetime.js │ │ │ └── styles.js │ │ ├── day │ │ │ └── Day.js │ │ ├── fieldset │ │ │ ├── Fieldset.js │ │ │ └── styles.js │ │ ├── h2 │ │ │ ├── H2.js │ │ │ └── styles.js │ │ ├── h3 │ │ │ ├── H3.js │ │ │ └── styles.js │ │ ├── htmlelement │ │ │ ├── HtmlElement.js │ │ │ └── styles.js │ │ ├── index.js │ │ ├── panel │ │ │ ├── Panel.js │ │ │ └── styles.js │ │ ├── radio │ │ │ ├── Radio.js │ │ │ └── styles.js │ │ ├── select │ │ │ ├── Select.js │ │ │ └── styles.js │ │ ├── selectboxes │ │ │ └── SelectBoxes.js │ │ ├── sharedComponents │ │ │ ├── Base.js │ │ │ ├── Input.js │ │ │ ├── Multi.js │ │ │ ├── Select.js │ │ │ ├── Tooltip.js │ │ │ └── Value.js │ │ ├── signature │ │ │ ├── Signature.js │ │ │ └── styles.js │ │ ├── switch │ │ │ └── Switch.js │ │ ├── tests │ │ │ ├── Columns.spec.js │ │ │ ├── Content.spec.js │ │ │ ├── Fieldset.spec.js │ │ │ ├── H2.spec.js │ │ │ ├── H3.spec.js │ │ │ ├── Panel.spec.js │ │ │ ├── Radio.spec.js │ │ │ ├── Select.spec.js │ │ │ ├── SelectBoxes.spec.js │ │ │ ├── Signature.spec.js │ │ │ ├── Switch.spec.js │ │ │ ├── TextField.spec.js │ │ │ ├── Textarea.spec.js │ │ │ └── __snapshots__ │ │ │ │ ├── Content.spec.js.snap │ │ │ │ ├── H2.spec.js.snap │ │ │ │ ├── H3.spec.js.snap │ │ │ │ ├── Radio.spec.js.snap │ │ │ │ ├── Select.spec.js.snap │ │ │ │ ├── SelectBoxes.spec.js.snap │ │ │ │ ├── Switch.spec.js.snap │ │ │ │ ├── TextField.spec.js.snap │ │ │ │ └── Textarea.spec.js.snap │ │ ├── textarea │ │ │ ├── Textarea.js │ │ │ └── styles.js │ │ └── textfield │ │ │ └── TextField.js │ ├── Formio.js │ ├── FormioComponentsList.js │ ├── FormioConfirm.js │ ├── FormioGrid.js │ └── index.js ├── defaultTheme │ ├── colors.js │ └── index.js ├── factories │ ├── FormioComponents.js │ └── index.js ├── formio │ ├── index.js │ ├── providers │ │ ├── index.js │ │ └── storage │ │ │ ├── base64.js │ │ │ ├── dropbox.js │ │ │ ├── index.js │ │ │ ├── s3.js │ │ │ └── url.js │ └── utils │ │ ├── builder.js │ │ ├── fixtures │ │ ├── components.json │ │ ├── components2.json │ │ ├── components3.json │ │ └── submission1.json │ │ ├── index.js │ │ ├── jsonlogic │ │ ├── operators.js │ │ └── operators.spec.js │ │ ├── utils.js │ │ └── utils.spec.js ├── index.js └── util │ ├── constants.js │ └── index.js ├── test ├── .setup.js ├── __snapshots__ │ └── changes.spec.js.snap ├── changes.spec.js ├── forms │ ├── componentSpec.js │ ├── empty.json │ └── tmp.json ├── submissions-wip.js └── validations.spec.js └── yarn.lock /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["react-native"] 3 | } -------------------------------------------------------------------------------- /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | # Javascript Node CircleCI 2.0 configuration file 2 | # 3 | # Check https://circleci.com/docs/2.0/language-javascript/ for more details 4 | # 5 | version: 2 6 | jobs: 7 | build: 8 | docker: 9 | # specify the version you desire here 10 | - image: circleci/node:7.10 11 | 12 | # Specify service dependencies here if necessary 13 | # CircleCI maintains a library of pre-built images 14 | # documented at https://circleci.com/docs/2.0/circleci-images/ 15 | # - image: circleci/mongo:3.4.4 16 | 17 | working_directory: ~/repo 18 | 19 | steps: 20 | - checkout 21 | 22 | # Download and cache dependencies 23 | - restore_cache: 24 | keys: 25 | - v1-dependencies-{{ checksum "package.json" }} 26 | # fallback to using the latest cache if no exact match is found 27 | - v1-dependencies- 28 | 29 | - run: yarn install 30 | 31 | - save_cache: 32 | paths: 33 | - node_modules 34 | key: v1-dependencies-{{ checksum "package.json" }} 35 | 36 | # run tests! 37 | - run: yarn test 38 | 39 | 40 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "root": true, 3 | "parser": "babel-eslint", 4 | "parserOptions": { 5 | "sourceType": "module", 6 | "ecmaVersion": 2017 7 | }, 8 | "rules": { 9 | "max-len": 0, 10 | "max-depth": [1, 5], 11 | "max-statements": 0, 12 | "no-unused-vars": 0 13 | }, 14 | "env": { 15 | "es6": true, 16 | "jest": true, 17 | "node": true 18 | }, 19 | "plugins": [ 20 | "react", 21 | "react-native" 22 | ], 23 | "globals": { 24 | "require": true, 25 | "module": true, 26 | "global": true, 27 | "fetch": true 28 | }, 29 | "extends": [ 30 | "eslint-config-formio", 31 | "plugin:react/recommended", 32 | "plugin:react-native/all" 33 | ] 34 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .bower.json 2 | *.log 3 | node_modules 4 | bower_components 5 | .DS_Store -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | } -------------------------------------------------------------------------------- /Changelog.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | All notable changes to this project will be documented in this file 3 | 4 | The format is based on [Keep a Changelog](http://keepachangelog.com/) 5 | and this project adheres to [Semantic Versioning](http://semver.org/). 6 | 7 | ## 1.5.4 8 | ### Changed 9 | - Editgrid rows close now removes new rows but just close existing rows. 10 | - Changed events when editing and saving rows. 11 | 12 | ## 1.5.3 13 | ### Fixed 14 | - Datagrid not always refreshing when a component is hidden. 15 | 16 | ## 1.5.2 17 | ### Fixed 18 | - Stop signature clearing on resize. 19 | 20 | ## 1.5.1 21 | ### Fixed 22 | - Allow select component to set headers. 23 | 24 | ## 1.5.0 25 | ### Added 26 | - Number and currency components can be limited to a certain number of decimals. 27 | 28 | ## 1.4.26 29 | ### Fixed 30 | - Crash in signature component when in read only mode. 31 | 32 | ## 1.4.25 33 | ### Fixed 34 | - Nesting datagrids passing parameters wrong causing remove rows to call on wrong datagrid. 35 | 36 | ## 1.4.24 37 | ### Fixed 38 | - Pass input variable to select custom values so that manual filtering can be done. 39 | 40 | ## 1.4.23 41 | ### Fixed 42 | - Add typecheck to date parsing. 43 | 44 | ## 1.4.22 45 | ### Fixed 46 | - Date picker on android only returns the date portion which makes the rendered date appear on the day before. 47 | 48 | ## 1.4.21 49 | ### Changed 50 | - Upgrade formiojs to 2.25.4 51 | 52 | ### Fixed 53 | - Fix skipInit preventing default values being set on conditionally visible fields. 54 | 55 | ## 1.4.20 56 | ### Fixed 57 | - Fix crash of editgrid when using the default template that contains a util function. 58 | - Fix crash of editgrid when not using skipInit that would attempt to change the array to an object. 59 | 60 | ## 1.4.19 61 | ### Fixed 62 | - Day component didn't properly implement custom validations. 63 | 64 | ## 1.4.18 65 | ### Fixed 66 | - Hiding items in editgrids didn't remove their values. 67 | 68 | ## 1.4.17 69 | ### Changed 70 | - Upgrade react-widgets version to 4.x. 71 | 72 | ## 1.4.16 73 | ### Changed 74 | - Decrease animations to 10ms 75 | 76 | ## 1.4.15 77 | ### Fixed 78 | - Crash in select resources if not json due to change in 1.4.14 79 | 80 | ## 1.4.14 81 | ### Fixed 82 | - Fix performance of rendering select json components with lots of items in the json array. 83 | 84 | ## 1.4.13 85 | ### Fixed 86 | - Select components with lots of json were slow to filter. Pre-rendering the data on load speeds it up. 87 | 88 | ## 1.4.12 89 | ### Fixed 90 | - Datagrids and containers embedded within other datagrids and containers not setting values properly. 91 | 92 | ## 1.4.11 93 | ### Added 94 | - Support for column definitions. 95 | 96 | ## 1.4.10 97 | ### Fixed 98 | - Crash when empty rows are deleted from datagrids. 99 | 100 | ## 1.4.9 101 | ### Fixed 102 | - Input masks not initializing validity properly. 103 | 104 | ## 1.4.8 105 | ### Fixed 106 | - Select JSON with a value property not finding item when loading in a submission. 107 | 108 | ## 1.4.7 109 | ### Fixed 110 | - Disable editgrid controls when form is read-only. 111 | - Edit grid was not fully calculating validity on form load. 112 | 113 | ## 1.4.6 114 | ### Fixed 115 | - Input mask not required for required fields. 116 | 117 | ## 1.4.5 118 | ### Fixed 119 | - Edit grid editing was showing all fields as invalid. 120 | 121 | ## 1.4.4 122 | ### Fixed 123 | - Fix issue where Select JSON fields couldn't set the value field. 124 | 125 | ## 1.4.3 126 | ### Fixed 127 | - Fix issue where components in a container that hide at the same time can't fail to clear data. 128 | 129 | ## 1.4.2 130 | ### Changed 131 | - Fire edit grid open event on componentDidMount instead of componentWillMount. 132 | 133 | ## 1.4.1 134 | ### Fixed 135 | - HTML output of editgrid header 136 | 137 | ### Added 138 | - Footer for editgrid 139 | 140 | ## 1.4.0 141 | ### Added 142 | - Time component 143 | - EditGrid component 144 | 145 | ## 1.3.14 146 | - Fix default formatting of empty custom error validation. 147 | 148 | ## 1.3.13 149 | ### Fixed 150 | - Disable datagrid buttons when form is read only. 151 | - Don't fire change events for readOnly forms. 152 | 153 | ## 1.3.12 154 | ### Added 155 | - Events that fire when select lists open or close. 156 | - Event that fires on add/remove from datagrid. 157 | - Event that fires on loadMore for selects. 158 | 159 | ## 1.3.11 160 | ### Reverted 161 | - Reverted revert of change to datagrids delete value. 162 | 163 | ### Fixed 164 | - Calculated Select values could return something other than an array which caused an error. 165 | 166 | ## 1.3.10 167 | ### Reverted 168 | - Reverted change to setting values that attempted to fix deleting rows in datagrids issue that had a lot of side effects. 169 | 170 | ### 1.3.9 171 | ### Fixed 172 | - Fix MinLength calculation for datagrids. 173 | - Fixed error about setState in select component. 174 | - Scenario where updating a form doesn't always set the values. 175 | 176 | ### Changed 177 | - Replace full lodash with individual functions. 178 | 179 | ## 1.3.8 180 | ### Fixed 181 | - Datagrids with select components dependent on external data weren't updating when the data updated. 182 | 183 | ## 1.3.7 184 | ### Changed 185 | - Datagrid headers won't render if there are no labels. 186 | 187 | ## 1.3.6 188 | ### Fixed 189 | - Deleting rows in datagrids didn't clear components properly. 190 | 191 | ## 1.3.5 192 | ### Fixed 193 | - Fix performance of datagrids with large data. 194 | 195 | ## 1.3.4 196 | ### Added 197 | - Onchange event will fire for input fields after 500ms of no typing instead of only on blur. 198 | 199 | ## 1.3.3 200 | ### Added 201 | - Expose mixins as exports to ease creation of custom components. 202 | 203 | ## 1.3.2 204 | Changed 205 | - Text inputs will fire change events on blur now instead of on change. Change events were too slow in redux. 206 | 207 | ## 1.3.1 208 | ### Fixed 209 | - Fixed tests dealing with input mask change and missing onChange events. 210 | 211 | ### Removed 212 | - Removing tests that don't work with current libraries. 213 | 214 | ## 1.3.0 215 | ### Changed 216 | - Swapped react-input-mask for react-text-mask for input masks. 217 | - Improved performance of input masks. 218 | -------------------------------------------------------------------------------- /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 | ## This repository is now considered legacy and no longer supported. Please take a look at our recent repositories and help documentation at the following links. 2 | - https://help.form.io 3 | - https://github.com/formio/formio.js 4 | - https://github.com/formio/formio 5 | - https://github.com/formio/react 6 | - https://github.com/formio/angular 7 | - https://github.com/formio/vue 8 | 9 | # React Native Formio 10 | 11 | A [React](http://facebook.github.io/react/) component for rendering out forms based on the [Form.io](https://www.form.io) platform. 12 | 13 | ## Install 14 | 15 | ### npm 16 | 17 | `React Formio` can be used on the server, or bundled for the client using an 18 | npm-compatible packaging system such as [Browserify](http://browserify.org/) or 19 | [webpack](http://webpack.github.io/). 20 | 21 | ``` 22 | npm install react-formio --save 23 | ``` 24 | 25 | ### Browser bundle 26 | 27 | The browser bundle exposes a global `Formio` variable and expects to find 28 | a global `React` variable to work with. 29 | 30 | You can find it in the [/dist directory](https://github.com/formio/react-formio/tree/master/dist/build). 31 | 32 | ## Usage 33 | 34 | Give `Formio` a `src` property and render: 35 | 36 | ** For es5 require() modules. ** 37 | ```javascript 38 | var React = require('react'); 39 | var ReactDOM = require('react-dom'); 40 | var Formio = require('react-formio').Formio; 41 | ``` 42 | 43 | ** For es6 import/export modules. ** 44 | ```javascript 45 | import React from 'react'; 46 | import ReactDOM from 'react-dom'; 47 | import {Formio} from 'react-formio'; 48 | ``` 49 | 50 | ```javascript 51 | ReactDOM.render( 52 | 53 | , document.getElementById('example') 54 | ); 55 | ``` 56 | 57 | ## Props 58 | 59 | ### `src` : `string` 60 | 61 | The form API source from [form.io](https://www.form.io) or your custom formio server. 62 | 63 | See the [Creating a form](http://help.form.io/userguide/#new-form) 64 | for where to set the API Path for your form. 65 | 66 | You can also pass in the submission url as the `src` and the form will render with the data populated from the submission. 67 | 68 | ### `form` : `object` 69 | 70 | An object representing the form. Use this instead of src for custom forms. 71 | 72 | **Note:** `src` will override this property if used. 73 | 74 | ### `submission`: `Object` 75 | 76 | An object representing the default data for the form. 77 | 78 | **Note:** `src` will override this if a submission url is entered. 79 | 80 | ### `onChange` : `(submission: object, key: string, value: mixed)` 81 | 82 | A function callback that will be called when any field is changed. The full submission is passed as well as the field 83 | that is changing's key and value. 84 | 85 | ### `onFormSubmit` : `(submission: object)` 86 | 87 | A function callback that will be called when a submission is successful. 88 | 89 | ### `onFormError` : `(response: object)` 90 | 91 | A function callback that will be called when a submisison is unsuccessful. 92 | 93 | ### `onFormLoad` : `(form: object)` 94 | 95 | A function callback that will be called with a form is finished loading. 96 | 97 | ### `onSubmissionLoad` : `(submission: object)` 98 | 99 | A function callback that will be called after a submission is loaded. 100 | 101 | ### `onElementRender` : `(element: object)` 102 | 103 | A function callback that will be called each time a component is rendered. 104 | 105 | ### `options : object` 106 | 107 | A settings object to pass various options into the form. skipInit will stop the form from initialling setting values 108 | on the submission object which will result in data only changing when a user interacts with the form. 109 | 110 | ```javascript 111 | options={ 112 | skipInit: true 113 | } 114 | ``` 115 | 116 | ## License 117 | Released under the [MIT License](http://www.opensource.org/licenses/MIT). 118 | -------------------------------------------------------------------------------- /circle.yml: -------------------------------------------------------------------------------- 1 | machine: 2 | node: 3 | version: 6.9.1 4 | -------------------------------------------------------------------------------- /formio.css: -------------------------------------------------------------------------------- 1 | 2 | /* FA-877 - Fix component widths in the data grid with prefix/sufix. */ 3 | .input-group { 4 | width: 100%; 5 | } 6 | .form-field-type-datagrid .input-group { 7 | width: auto; 8 | } 9 | 10 | .form-field-type-datagrid .form-group { 11 | margin-bottom: 0; 12 | } 13 | 14 | .form-field-type-datagrid .datagrid-table { 15 | margin-bottom: 0px; 16 | } 17 | 18 | .form-field-type-datagrid .datagrid-add { 19 | margin-bottom: 20px; 20 | } 21 | 22 | .form-field-type-datetime .input-group, 23 | .form-field-type-datetime .input-group-btn { 24 | width: 100%; 25 | } 26 | .form-field-type-datetime .input-group-btn { 27 | width: auto; 28 | } 29 | 30 | .m-signature-pad { 31 | border: 1px solid #e8e8e8; 32 | background-color: #F5F5EB; 33 | border-radius: 4px; 34 | } 35 | 36 | .has-error .m-signature-pad--body { 37 | border-color: #b94a48; 38 | -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075); 39 | box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075); 40 | } 41 | 42 | .m-signature-pad--body { 43 | left: 20px; 44 | right: 20px; 45 | top: 20px; 46 | bottom: 60px; 47 | border: 1px solid #f4f4f4; 48 | } 49 | 50 | .m-signature-pad--body canvas { 51 | width: 100%; 52 | height: 100%; 53 | border-radius: 4px; 54 | box-shadow: 0 0 5px rgba(0, 0, 0, 0.02) inset; 55 | } 56 | 57 | @media screen and (max-width: 1024px) { 58 | .m-signature-pad { 59 | top: 0; 60 | left: 0; 61 | right: 0; 62 | bottom: 0; 63 | width: auto; 64 | height: auto; 65 | min-width: 250px; 66 | min-height: 140px; 67 | } 68 | } 69 | 70 | @media screen and (min-device-width: 768px) and (max-device-width: 1024px) { 71 | .m-signature-pad { 72 | margin: 10%; 73 | } 74 | } 75 | 76 | @media screen and (max-height: 320px) { 77 | .m-signature-pad--body { 78 | left: 0; 79 | right: 0; 80 | top: 0; 81 | bottom: 32px; 82 | } 83 | } 84 | 85 | .formio-signature-footer { 86 | color: #C3C3C3; 87 | margin: 0 10px; 88 | text-align: center; 89 | } 90 | 91 | .formio-content-centered { 92 | text-align: center; 93 | } 94 | 95 | .filepicker.dropzone { 96 | width: 100%; 97 | position: relative; 98 | height: auto; 99 | } 100 | 101 | .dz-default.dz-message { 102 | text-align: center; 103 | border-style: dashed; 104 | border-width: 2px; 105 | border-color: #666; 106 | border-radius: 5px; 107 | padding: 15px; 108 | margin: 10px 0px; 109 | } 110 | 111 | .formio-dropzone-default-content { 112 | width: 100%; 113 | height: auto; 114 | border-style: dashed; 115 | border-width: 2px; 116 | border-color: #666; 117 | border-radius: 5px; 118 | padding: 15px; 119 | margin-top: 10px; 120 | } 121 | 122 | .formio-dropzone-margin { 123 | margin: 10px 0; 124 | } 125 | 126 | .formio-dropzone-error-content { 127 | width: 100%; 128 | height: auto; 129 | background-color: #E99002; 130 | margin-bottom: 10px; 131 | padding: 15px; 132 | text-align: left; 133 | } 134 | 135 | .formio-dropzone-table { 136 | width: 1%; 137 | white-space: nowrap; 138 | } 139 | 140 | .progress-bar { 141 | float: left; 142 | width: 0; 143 | height: 20px; 144 | font-size: 12px; 145 | line-height: 20px; 146 | color: #fff; 147 | text-align: center; 148 | background-color: #337ab7; 149 | -webkit-box-shadow: inset 0 -1px 0 rgba(0,0,0,.15); 150 | box-shadow: inset 0 -1px 0 rgba(0,0,0,.15); 151 | -webkit-transition: width .6s ease; 152 | -o-transition: width .6s ease; 153 | transition: width .6s ease; 154 | margin-bottom: 10px; 155 | } 156 | 157 | .dz-drag-hover > .dz-default.dz-message { 158 | border-color: #127abe; 159 | } 160 | 161 | .rw-list-option { 162 | height: 1.8em; 163 | } 164 | 165 | .field-required:after { 166 | color: red; 167 | content: " *"; 168 | } 169 | 170 | .field-required-inline { 171 | color:red; 172 | z-index: 5; 173 | } 174 | 175 | .quill { 176 | border: 1px solid #ccc; 177 | box-shadow: 0px 0px 5px #ddd; 178 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-native-formio", 3 | "version": "0.0.1", 4 | "description": "React native rendering library for form.io embedded forms.", 5 | "main": "src/index.js", 6 | "scripts": { 7 | "test": "jest", 8 | "lint": "eslint src", 9 | "prepublish": "npm run lint" 10 | }, 11 | "repository": { 12 | "type": "git", 13 | "url": "git+https://github.com/formio/react-native-formio.git" 14 | }, 15 | "keywords": [ 16 | "React", 17 | "component", 18 | "Formio", 19 | "Forms", 20 | "react--native-component" 21 | ], 22 | "author": "Randall Knutson ", 23 | "license": "MIT", 24 | "bugs": { 25 | "url": "https://github.com/formio/react-native-formio/issues" 26 | }, 27 | "homepage": "https://github.com/formio/react-native-formio#readme", 28 | "dependencies": { 29 | "eventemitter2": "^5.0.1", 30 | "json-logic-js": "^1.2.2", 31 | "lodash": "^4.15.0", 32 | "moment": "^2.22.2", 33 | "prop-types": "^15.6.1", 34 | "react-native-device-info": "^0.22.3", 35 | "react-native-elements": "^0.19.1", 36 | "react-native-htmlview": "^0.13.0", 37 | "react-native-material-dropdown": "^0.11.1", 38 | "react-native-modal-datetime-picker": "^6.0.0", 39 | "react-native-multiple-select": "^0.4.4", 40 | "react-native-select-multiple": "^1.2.0", 41 | "react-native-signature-capture": "^0.4.9", 42 | "react-native-simple-radio-button": "^2.7.2", 43 | "react-native-vector-icons": "^4.6.0", 44 | "react-text-mask-hoc": "^0.10.6", 45 | "rn-tooltip": "^1.0.1" 46 | }, 47 | "devDependencies": { 48 | "babel-eslint": "^8.2.6", 49 | "babel-jest": "23.4.0", 50 | "babel-plugin-module-resolver": "^3.1.1", 51 | "babel-preset-env": "^1.7.0", 52 | "babel-preset-jest": "^23.2.0", 53 | "babel-preset-react-native": "^4.0.0", 54 | "eslint": "^3.2.2", 55 | "eslint-config-airbnb": "^10.0.0", 56 | "eslint-config-formio": "^1.1.0", 57 | "eslint-loader": "^1.5.0", 58 | "eslint-plugin-jsx-a11y": "^2.0.1", 59 | "eslint-plugin-react": "^6.10.0", 60 | "eslint-plugin-react-native": "^2.3.1", 61 | "jest": "23.4.0", 62 | "react": "^16.4.2", 63 | "react-native": "^0.56.0", 64 | "react-native-create-bridge": "^2.0.1", 65 | "react-test-renderer": "^16.4.2", 66 | "written-number": "^0.8.1" 67 | }, 68 | "peerDependencies": { 69 | "react": "^16.2.0", 70 | "react-native": "*" 71 | }, 72 | "files": [ 73 | "dist", 74 | "lib", 75 | "formio.css" 76 | ], 77 | "jest": { 78 | "preset": "react-native", 79 | "transform": { 80 | "^.+\\.(js|jsx)$": "babel-jest" 81 | }, 82 | "setupFiles": [ 83 | "/test/.setup.js" 84 | ], 85 | "transformIgnorePatterns": [ 86 | "node_modules/(?!(react-native|react-native-clean-form|react-native-vector-icons|react-text-mask-hoc|react-native-elements|react-native-simple-radio-button)/)" 87 | ], 88 | "moduleFileExtensions": [ 89 | "js", 90 | "jsx" 91 | ] 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /src/components/FormBuilder.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {View, StyleSheet} from 'react-native'; 3 | 4 | const formBuilderStyles = StyleSheet.create({ 5 | wrapper: { 6 | height: '500px' 7 | } 8 | }); 9 | 10 | export default class FormBuilder extends React.Component { 11 | render() { 12 | return Form Builder; 13 | } 14 | } 15 | 16 | -------------------------------------------------------------------------------- /src/components/FormComponents/button/Button.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {StyleSheet} from 'react-native'; 3 | import BaseComponent from '../sharedComponents/Base'; 4 | import {Button as ButtonElement} from 'react-native-elements'; 5 | import DeviceInfo from 'react-native-device-info'; 6 | 7 | export default class Button extends BaseComponent { 8 | constructor(props) { 9 | super(props); 10 | this.onClick = this.onClick.bind(this); 11 | this.getButtonType = this.getButtonType.bind(this); 12 | } 13 | 14 | getButtonType() { 15 | switch (this.props.component.action) { 16 | case 'submit': 17 | case 'saveState': 18 | return 'submit'; 19 | case 'reset': 20 | return 'reset'; 21 | case 'event': 22 | case 'oauth': 23 | default: 24 | return 'button'; 25 | } 26 | } 27 | 28 | onClick(event) { 29 | if (this.props.readOnly) { 30 | event.preventDefault(); 31 | this.props.resetForm(); 32 | return; 33 | } 34 | switch (this.props.component.action) { 35 | case 'submit': 36 | this.props.onSubmit(event); 37 | break; 38 | case 'saveState': 39 | this.props.onSave(event); 40 | break; 41 | case 'event': 42 | this.props.onEvent(this.props.component.event); 43 | break; 44 | case 'oauth': 45 | /* eslint-disable no-console */ 46 | console.warn('You must add the OAuth button to a form for it to function properly'); 47 | /* eslint-enable no-console */ 48 | break; 49 | case 'delete': 50 | this.props.onEvent('deleteSubmission'); 51 | break; 52 | case 'reset': 53 | event.preventDefault(); 54 | this.props.resetForm(); 55 | break; 56 | } 57 | } 58 | 59 | render() { 60 | let buttonWidth; 61 | const {component} = this.props; 62 | if (component.block) { 63 | buttonWidth = '100%'; 64 | } 65 | else { 66 | buttonWidth = DeviceInfo.isTablet() ? 250 : 150; 67 | } 68 | 69 | const styles = StyleSheet.flatten({ 70 | button: { 71 | width: buttonWidth, 72 | alignSelf: 'center', 73 | marginHorizontal: 10, 74 | paddingHorizontal: component.block ? 20 : 0, 75 | marginTop: 20, 76 | marginBottom: 10 77 | }, 78 | }); 79 | 80 | const getIconName = (value) => value.split(' ')[1].split('-')[1]; 81 | const disabled = this.props.readOnly || this.props.isSubmitting || (component.disableOnInvalid && !this.props.isFormValid); 82 | const submitting = this.props.isSubmitting && component.action === 'submit'; 83 | 84 | const leftIcon = component.leftIcon ? {name: getIconName(component.leftIcon), type: 'font-awesome'} : null; 85 | const rightIcon = component.rightIcon ? {name: getIconName(component.rightIcon), type: 'font-awesome'} : null; 86 | 87 | return ( 88 | 100 | ); 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /src/components/FormComponents/columns/Columns.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {FormioComponentsList} from '../../../components'; 3 | import PropTypes from 'prop-types'; 4 | import {View} from 'react-native'; 5 | import styles from './styles'; 6 | 7 | const Columns = (props) => ( 8 | 9 | {props.component.columns.map((column, index) => ( 10 | 15 | ))} 16 | 17 | ); 18 | 19 | Columns.propTypes = { 20 | component: PropTypes.object, 21 | theme: PropTypes.any, 22 | colors: PropTypes.object, 23 | }; 24 | 25 | export default Columns; 26 | -------------------------------------------------------------------------------- /src/components/FormComponents/columns/styles.js: -------------------------------------------------------------------------------- 1 | 2 | import {StyleSheet} from 'react-native'; 3 | 4 | const styles = StyleSheet.flatten({ 5 | columns: { 6 | flex: 1, 7 | flexDirection: 'row', 8 | borderBottomWidth: 1, 9 | paddingVertical: 20, 10 | }, 11 | }); 12 | 13 | export default styles; 14 | -------------------------------------------------------------------------------- /src/components/FormComponents/componentUtils/getDefaultValue.js: -------------------------------------------------------------------------------- 1 | import {safeSingleToMultiple} from './safeSingleToMultiple'; 2 | 3 | const getCustomDefault = (component) => { 4 | const value = component.customDefaultValue.toString(); 5 | return value; 6 | }; 7 | 8 | export const getDefaultValue = (value, component, getInitialValue, onChangeCustom) => { 9 | // Allow components to set different default values. 10 | if (value == null) { 11 | if (component.hasOwnProperty('customDefaultValue')) { 12 | try { 13 | value = getCustomDefault(component); 14 | } 15 | catch (e) { 16 | /* eslint-disable no-console */ 17 | console.warn('An error occurrend in a custom default value in ' + component.key, e); 18 | /* eslint-enable no-console */ 19 | value = ''; 20 | } 21 | } 22 | else if (component.hasOwnProperty('defaultValue')) { 23 | value = component.defaultValue; 24 | if (typeof onChangeCustom === 'function') { 25 | value = onChangeCustom(value); 26 | } 27 | } 28 | else if (typeof getInitialValue === 'function') { 29 | value = getInitialValue(); 30 | } 31 | else { 32 | value = ''; 33 | } 34 | } 35 | value = safeSingleToMultiple(value, component); 36 | return value; 37 | }; 38 | -------------------------------------------------------------------------------- /src/components/FormComponents/componentUtils/safeSingleToMultiple.js: -------------------------------------------------------------------------------- 1 | 2 | export const safeSingleToMultiple = (value, component) => { 3 | // Don't do anything to datagrid or containers. 4 | if ((component.type === 'datagrid') || (component.type === 'container') || (component.type === 'editgrid')) { 5 | return value; 6 | } 7 | // If this was a single but is not a multivalue. 8 | if (component.multiple && !Array.isArray(value)) { 9 | if (component.type === 'select' && !value) { 10 | value = []; 11 | } 12 | else { 13 | value = [value]; 14 | } 15 | } 16 | // If this was a multivalue but is now single value. 17 | // RE-60 :-Need to return the value as array of object instead of object while converting a multivalue to single value for datagrid component 18 | else if (!component.multiple && Array.isArray(value)) { 19 | value = value[0]; 20 | } 21 | // Set dates to Date object. 22 | if (component.type === 'datetime' && value && !(value instanceof Date)) { 23 | value = new Date(value); 24 | } 25 | return value; 26 | }; 27 | -------------------------------------------------------------------------------- /src/components/FormComponents/componentUtils/validators.js: -------------------------------------------------------------------------------- 1 | 2 | export const validateItem = (item, component, data) => { 3 | let state = { 4 | isValid: true, 5 | errorType: '', 6 | errorMessage: '', 7 | item, 8 | }; 9 | // Check for no validation criteria 10 | if (!component.validate) { 11 | return state; 12 | } 13 | // Required 14 | if (component.validate.required) { 15 | // Multivalue and selectboxes are exceptions since !![] === true and !!{} === true. 16 | if (component.type === 'selectboxes' && !Object.keys(item).reduce((prev, cur) => { 17 | return prev || item[cur]; 18 | }, false)) { 19 | state.isValid = false; 20 | state.errorType = 'required'; 21 | state.errorMessage = (component.label || component.key) + ' is required.'; 22 | } 23 | else if (!item) { 24 | state.isValid = false; 25 | state.errorType = 'required'; 26 | state.errorMessage = (component.label || component.key) + ' is required.'; 27 | } 28 | } 29 | // Email 30 | if (state.isValid && component.type === 'email' && !item.match(/\S+@\S+/)) { 31 | state.isValid = false; 32 | state.errorType = 'email'; 33 | state.errorMessage = (component.label || component.key) + ' must be a valid email.'; 34 | } 35 | // MaxLength 36 | if (state.isValid && component.validate.maxLength && item.length > component.validate.maxLength) { 37 | state.isValid = false; 38 | state.errorType = 'maxlength'; 39 | state.errorMessage = (component.label || component.key) + ' cannot be longer than ' + (component.validate.maxLength) + ' characters.'; 40 | } 41 | // MinLength 42 | if (state.isValid && component.validate.minLength && item.length < component.validate.minLength) { 43 | state.isValid = false; 44 | state.errorType = 'minlength'; 45 | state.errorMessage = (component.label || component.key) + ' cannot be shorter than ' + (component.validate.minLength) + ' characters.'; 46 | } 47 | // MaxValue 48 | if (state.isValid && component.validate.max && item > component.validate.max) { 49 | state.isValid = false; 50 | state.errorType = 'max'; 51 | state.errorMessage = (component.label || component.key) + ' cannot be greater than ' + component.validate.max; 52 | } 53 | // MinValue 54 | if (state.isValid && component.validate.min && item < component.validate.min) { 55 | state.isValid = false; 56 | state.errorType = 'min'; 57 | state.errorMessage = (component.label || component.key) + ' cannot be less than ' + component.validate.min; 58 | } 59 | // Regex 60 | if (state.isValid && component.validate.pattern) { 61 | const re = new RegExp(component.validate.pattern, 'g'); 62 | state.isValid = item.match(re); 63 | if (!state.isValid) { 64 | state.errorType = 'regex'; 65 | state.errorMessage = (component.label || component.key) + ' must match the expression: ' + component.validate.pattern; 66 | } 67 | } 68 | // Input Mask 69 | if (component.inputMask && item.includes('_')) { 70 | state.isValid = false; 71 | state.errorType = 'mask'; 72 | state.errorMessage = (component.label || component.key) + ' must use the format ' + component.inputMask; 73 | } 74 | // Custom 75 | if (state.isValid && component.validate.custom) { 76 | let custom = component.validate.custom; 77 | custom = custom.replace(/({{\s+(.*)\s+}})/, (match, $1, $2) => data[$2]); 78 | const input = item; 79 | let valid; 80 | try { 81 | valid = eval(custom); 82 | state.isValid = (valid === true); 83 | } 84 | catch (e) { 85 | /* eslint-disable no-console */ 86 | console.warn('A syntax error occurred while computing custom values in ' + component.key, e); 87 | /* eslint-enable no-console */ 88 | } 89 | if (!state.isValid) { 90 | state.errorType = 'custom'; 91 | state.errorMessage = valid; 92 | } 93 | } 94 | return state; 95 | }; 96 | 97 | export const validate = (value, component, data, validateCustom) => { 98 | let state = { 99 | isValid: true, 100 | errorType: '', 101 | errorMessage: '' 102 | }; 103 | 104 | // Allow components to have custom validation. 105 | if (typeof validateCustom === 'function') { 106 | const customValidation = validateCustom(value); 107 | if (!customValidation.isValid) { 108 | state.isValid = false; 109 | state.errorType = 'day'; 110 | state.errorMessage = customValidation.errorMessage; 111 | } 112 | } 113 | // Validate each item if multiple. 114 | if (component.multiple) { 115 | const items = []; 116 | value.forEach(item => { 117 | if (state.isValid) { 118 | state = validateItem(item, component, data); 119 | items.push(state.item); 120 | } 121 | state.item = items; 122 | }); 123 | 124 | if (component.validate && component.validate.required && (!(value instanceof Array) || value.length === 0)) { 125 | state.isValid = false; 126 | state.errorType = 'required'; 127 | state.errorMessage = (component.label || component.key) + ' is required.'; 128 | } 129 | } 130 | else { 131 | state = validateItem(value, component, data); 132 | } 133 | return state; 134 | }; 135 | -------------------------------------------------------------------------------- /src/components/FormComponents/content/Content.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {View, Linking} from 'react-native'; 3 | import {StyleSheet} from 'react-native'; 4 | import HTMLView from 'react-native-htmlview'; 5 | import BaseComponent from '../sharedComponents/Base'; 6 | import styles from './styles'; 7 | 8 | export default class Content extends BaseComponent { 9 | 10 | constructor(props) { 11 | super(props); 12 | this.onLinkPress = this.onLinkPress.bind(this); 13 | this.getHtmlStyles = this.getHtmlStyles.bind(this); 14 | } 15 | 16 | getHtmlStyles() { 17 | return { 18 | p: { 19 | ...StyleSheet.flatten(styles.p), 20 | color: this.props.colors.textColor, 21 | }, 22 | }; 23 | } 24 | 25 | onLinkPress(url) { 26 | Linking.openURL(url) 27 | .catch((e) => { 28 | return e; 29 | }); 30 | } 31 | 32 | render() { 33 | return ( 34 | 35 | 41 | 42 | ); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/components/FormComponents/content/styles.js: -------------------------------------------------------------------------------- 1 | import {StyleSheet} from 'react-native'; 2 | 3 | const textColor = '#86939e'; 4 | const styles = StyleSheet.create({ 5 | content: { 6 | flex: 1, 7 | marginHorizontal: 20, 8 | marginTop: 10, 9 | marginBottom: 0, 10 | }, 11 | p: { 12 | fontSize: 16, 13 | marginTop: 5, 14 | marginBottom: 0, 15 | color: textColor, 16 | lineHeight: 25, 17 | textAlign: 'justify', 18 | }, 19 | }); 20 | 21 | export default styles; 22 | -------------------------------------------------------------------------------- /src/components/FormComponents/datetime/Datetime.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import DateTimePicker from 'react-native-modal-datetime-picker'; 3 | import MultiComponent from '../sharedComponents/Multi'; 4 | import {View, Text} from 'react-native'; 5 | import {Button} from 'react-native-elements'; 6 | import styles from './styles'; 7 | import moment from 'moment'; 8 | 9 | export default class Datetime extends MultiComponent { 10 | constructor(props) { 11 | super(props); 12 | this.getInitialValue = this.getInitialValue.bind(this); 13 | this.getMode = this.getMode.bind(this); 14 | this.getDisplayFormat = this.getDisplayFormat.bind(this); 15 | this.getResultFormat = this.getResultFormat.bind(this); 16 | this.onConfirm = this.onConfirm.bind(this); 17 | this.togglePicker = this.togglePicker.bind(this); 18 | this.getSingleElement = this.getSingleElement.bind(this); 19 | } 20 | 21 | getInitialValue(value) { 22 | if (!this.props) { 23 | return moment().toDate(); 24 | } 25 | 26 | const dateFormat = this.props.component.dateFirst ? 'DD/MM/YYYY' : 'MM/DD/YYYY'; 27 | if (value && value.item && moment(value.item, dateFormat).isValid()) { 28 | return moment(value.item, dateFormat).toDate(); 29 | } 30 | else if (this.props.component.defaultDate) { 31 | return moment(this.props.component.defaultDate, dateFormat).toDate(); 32 | } 33 | else { 34 | return moment().toDate(); 35 | } 36 | } 37 | 38 | getMode() { 39 | switch (this.props.component.type) { 40 | case 'datetime': 41 | return 'datetime'; 42 | case 'day': 43 | return 'date'; 44 | case 'time': 45 | return 'time'; 46 | default: 47 | return 'date'; 48 | } 49 | } 50 | 51 | getDisplayFormat() { 52 | switch (this.props.component.type) { 53 | case 'datetime': 54 | return 'MMMM DD, YYYY hh:mm A'; 55 | case 'day': 56 | return 'MMMM DD, YYYY'; 57 | case 'time': 58 | return 'hh:mm A'; 59 | default: 60 | return 'MMMM DD, YYYY'; 61 | } 62 | } 63 | 64 | getResultFormat() { 65 | const dateFirst = this.props.component.dateFirst; 66 | switch (this.props.component.type) { 67 | case 'datetime': 68 | return dateFirst ? 'DD/MM/YYYY : hh:mm A' : 'MM/DD/YYYY : hh:mm A'; 69 | case 'day': 70 | return dateFirst ? 'DD/MM/YYYY' : 'MM/DD/YYYY'; 71 | case 'time': 72 | return 'hh:mm A'; 73 | default: 74 | return dateFirst ? 'DD/MM/YYYY' : 'MM/DD/YYYY'; 75 | } 76 | } 77 | 78 | onConfirm(value, index) { 79 | const selected = moment(value); 80 | const dateFormat = this.getResultFormat(); 81 | if (selected.isValid()) { 82 | const date = selected.format(dateFormat).toString(); 83 | this.setValue(date, index); 84 | } 85 | else { 86 | // this fixes date module returning invalid date 87 | //if confirm button was pressed without touching date picker. 88 | value = moment().format(dateFormat).toString(); 89 | this.setValue(value.toISOString(), index); 90 | } 91 | this.togglePicker(); 92 | } 93 | 94 | togglePicker() { 95 | this.setState({ 96 | open: !this.state.open 97 | }); 98 | } 99 | 100 | getSingleElement(value, index) { 101 | const {component, name, readOnly} = this.props; 102 | const dateFormat = this.props.component.dateFirst ? 'DD/MM/YYYY' : 'MM/DD/YYYY'; 103 | return ( 104 | 105 | {this.state.value && this.state.value.item && 106 | { 107 | moment(this.state.value.item, dateFormat).format(this.getDisplayFormat())} 108 | } 109 | 142 | 143 | ); 144 | } 145 | else { 146 | const error = this.state.isPristine || data.isValid ? false : true; 147 | const Element = this.getSingleElement(data, 0, error); 148 | const errorText = error ? ({data.errorMessage}) : null; 149 | 150 | Component = ( 151 | 152 | 153 | 154 | {inputLabel} 155 | {component.tooltip && } 160 | 161 | {Element} 162 | 163 | {errorText} 164 | {component.description && {component.description}} 165 | 166 | ); 167 | } 168 | return Component; 169 | } 170 | } 171 | 172 | MultiComponent.propTypes = { 173 | component: PropTypes.any, 174 | onChange: PropTypes.func, 175 | theme: PropTypes.object, 176 | colors: PropTypes.object 177 | }; 178 | -------------------------------------------------------------------------------- /src/components/FormComponents/sharedComponents/Select.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {View, Text, StyleSheet} from 'react-native'; 3 | import Icon from 'react-native-vector-icons/dist/FontAwesome'; 4 | import {Dropdown} from 'react-native-material-dropdown'; 5 | import MultiSelect from 'react-native-multiple-select'; 6 | import {FormLabel} from 'react-native-elements'; 7 | import get from 'lodash/get'; 8 | import isEqual from 'lodash/isEqual'; 9 | import PropTypes from 'prop-types'; 10 | import DeviceInfo from 'react-native-device-info'; 11 | import ValueComponent from './Value'; 12 | import Tooltip from './Tooltip'; 13 | import colors from '../../../defaultTheme/colors'; 14 | 15 | export default class SelectComponent extends ValueComponent { 16 | constructor(props) { 17 | super(props); 18 | this.data = {...this.props.data}; 19 | this.state = { 20 | selectItems: [], 21 | searchTerm: '', 22 | hasNextPage: false, 23 | open: false 24 | }; 25 | this.willReceiveProps = this.willReceiveProps.bind(this); 26 | this.onChangeSelect = this.onChangeSelect.bind(this); 27 | this.onSearch = this.onSearch.bind(this); 28 | this.valueField = this.valueField.bind(this); 29 | this.getElements = this.getElements.bind(this); 30 | this.onToggle = this.onToggle.bind(this); 31 | } 32 | 33 | willReceiveProps(nextProps) { 34 | if (this.props.component.refreshOn && !nextProps.formPristine) { 35 | const refreshOn = this.props.component.refreshOn; 36 | this.refresh = false; 37 | if (refreshOn === 'data') { 38 | if (!isEqual(this.data, nextProps.data)) { 39 | this.refresh = true; 40 | } 41 | } 42 | else { 43 | if ((!this.data.hasOwnProperty(refreshOn) && nextProps.hasOwnProperty(refreshOn)) || this.data[refreshOn] !== nextProps.data[refreshOn]) { 44 | this.refresh = true; 45 | } 46 | else if (nextProps && nextProps.row && nextProps.row.hasOwnProperty(refreshOn) && this.props.row[refreshOn] !== nextProps.row[refreshOn]) { 47 | this.refresh = true; 48 | } 49 | } 50 | if (this.refresh && this.props.component.clearOnRefresh) { 51 | this.setValue(this.getDefaultValue()); 52 | } 53 | } 54 | if (this.refresh) { 55 | this.refreshItems(); 56 | this.refresh = false; 57 | } 58 | this.data = {...nextProps.data}; 59 | } 60 | 61 | valueField() { 62 | let valueFieldItem = this.props.component.valueProperty || 'value'; 63 | if (typeof this.getValueField === 'function') { 64 | valueFieldItem = this.getValueField(); 65 | } 66 | return valueFieldItem; 67 | } 68 | 69 | onChangeSelect(selected) { 70 | let value; 71 | if (this.props.component.multiple) { 72 | value = this.state.selectItems.filter((i) => selected.includes(i.label)) 73 | .map((i) => i.value); 74 | } 75 | else { 76 | value = selected; 77 | } 78 | if (Array.isArray(value) && this.valueField()) { 79 | value.forEach((val, index) => { 80 | value[index] = (typeof val === 'object' ? get(val, this.valueField()) : val); 81 | }); 82 | } 83 | else if (typeof value === 'object' && this.valueField()) { 84 | value = get(value, this.valueField()); 85 | } 86 | this.setValue(value); 87 | } 88 | 89 | onSearch(text) { 90 | this.setState({ 91 | searchTerm: text 92 | }); 93 | if (typeof this.refreshItems === 'function') { 94 | this.refreshItems(text); 95 | } 96 | } 97 | 98 | elementLayout(position) { 99 | switch (position) { 100 | case 'top': 101 | return { 102 | flexDirection: 'column', 103 | }; 104 | case 'left-left': 105 | case 'left-right': 106 | return { 107 | flexDirection: 'row', 108 | alignItems: 'flex-start', 109 | }; 110 | case 'right-left': 111 | case 'right-right': 112 | return { 113 | flexDirection: 'row-reverse', 114 | flex: 1, 115 | marginHorizontal: 20, 116 | }; 117 | case 'bottom': 118 | return { 119 | flexDirection: 'column-reverse', 120 | }; 121 | default: 122 | return { 123 | flexDirection: 'column', 124 | }; 125 | } 126 | } 127 | 128 | onToggle(isOpen) { 129 | this.props.onEvent('selectToggle', this, isOpen); 130 | this.setState(prevState => { 131 | prevState.open = isOpen; 132 | return prevState; 133 | }); 134 | } 135 | 136 | getElements() { 137 | const selectStyle = StyleSheet.create({ 138 | wrapper: { 139 | flex: 1, 140 | marginHorizontal: 20, 141 | marginTop: 20 142 | }, 143 | container: { 144 | zIndex: 1000, 145 | }, 146 | containerSingle: { 147 | flex: 1, 148 | marginTop: 0, 149 | marginBottom: 15, 150 | }, 151 | label: { 152 | maxWidth: DeviceInfo.isTablet() ? 580 : 210, 153 | color: this.props.theme.Label.color, 154 | fontSize: DeviceInfo.isTablet() ? this.props.theme.Label.fontSize : 12, 155 | }, 156 | mainElement: this.elementLayout(this.props.component.labelPosition), 157 | labelWrapper: { 158 | flexDirection: 'row', 159 | marginTop: this.props.component.labelPosition === 'top' || this.props.component.labelPosition === 'bottom' ? 0 : 40, 160 | marginRight: this.props.component.labelPosition === 'left-left' || this.props.component.labelPosition === 'left-right' ? 10 : 0, 161 | }, 162 | list: { 163 | backgroundColor: colors.mainBackground, 164 | }, 165 | descriptionText: { 166 | fontSize: DeviceInfo.isTablet() ? 12 : 10, 167 | marginLeft: 20, 168 | marginTop: 10, 169 | }, 170 | }); 171 | 172 | const {component} = this.props; 173 | const labelText = component.label && !component.hideLabel ? component.label : ''; 174 | const requiredInline = (!component.label && component.validate && component.validate.required ? : ''); 175 | const multiMode = component.multiple; 176 | let values; 177 | let Element; 178 | 179 | const inputLabel = labelText ? 180 | {requiredInline} {component.label} : null; 181 | 182 | if (multiMode) { 183 | values = this.state.value && this.state.value.item ? this.state.selectItems.filter((i) => this.state.value.item.includes(i.value)) : []; 184 | Element = ( 185 | v.label)} 197 | tagRemoveIconColor={this.props.colors.primary1Color} 198 | selectedItemTextColor={this.props.colors.primary1Color} 199 | selectedItemIconColor={this.props.colors.primary1Color} 200 | /> 201 | ); 202 | } 203 | else { 204 | values = this.state.value ? this.state.value.item : ''; 205 | Element = ( 206 | 218 | ); 219 | } 220 | return ( 221 | 222 | 223 | 224 | {inputLabel} 225 | {component.tooltip && } 236 | 237 | {Element} 238 | 239 | {component.description && {component.description}} 240 | 241 | ); 242 | } 243 | } 244 | 245 | SelectComponent.propTypes = { 246 | data: PropTypes.any, 247 | component: PropTypes.any, 248 | row: PropTypes.object, 249 | readOnly: PropTypes.bool, 250 | value: PropTypes.any, 251 | onEvent: PropTypes.func, 252 | }; 253 | -------------------------------------------------------------------------------- /src/components/FormComponents/sharedComponents/Tooltip.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import RNTooltip from 'rn-tooltip'; 4 | import {Text, StyleSheet} from 'react-native'; 5 | import {Icon} from 'react-native-elements'; 6 | 7 | const Tooltip = (props) => { 8 | const customStyles = props.styles || {}; 9 | const styles = StyleSheet.create({ 10 | tooltipText: { 11 | flexWrap: 'wrap', 12 | color: props.color, 13 | }, 14 | tooltipIcon: { 15 | marginLeft: -20, 16 | marginTop: 13, 17 | ...customStyles.icon 18 | }, 19 | }); 20 | return ( 21 | {props.text}} 23 | backgroundColor={props.backgroundColor} 24 | height={props.text.length < 20 ? 70 : props.text.length + 20} 25 | width={props.text.length < 20 ? 100 : props.text.length + 140} 26 | > 27 | 28 | 29 | ); 30 | }; 31 | 32 | Tooltip.propTypes = { 33 | color: PropTypes.string, 34 | backgroundColor: PropTypes.string, 35 | text: PropTypes.string, 36 | styles: PropTypes.any, 37 | }; 38 | 39 | export default Tooltip; 40 | -------------------------------------------------------------------------------- /src/components/FormComponents/sharedComponents/Value.js: -------------------------------------------------------------------------------- 1 | import {deepEqual} from '../../../util'; 2 | import clone from 'lodash/clone'; 3 | import BaseComponent from './Base'; 4 | import PropTypes from 'prop-types'; 5 | import {validate} from '../componentUtils/validators'; 6 | import {safeSingleToMultiple} from '../componentUtils/safeSingleToMultiple'; 7 | import {getDefaultValue} from '../componentUtils/getDefaultValue'; 8 | 9 | export default class ValueComponent extends BaseComponent { 10 | constructor(props) { 11 | super(props); 12 | const value = getDefaultValue(this.props.value, this.props.component, this.getInitialValue, this.onChangeCustom); 13 | const valid = this.validate(value); 14 | this.state = { 15 | open: false, 16 | showSignaturePad: false, 17 | value: value, 18 | isValid: valid.isValid, 19 | errorType: valid.errorType, 20 | errorMessage: valid.errorMessage, 21 | isPristine: true 22 | }; 23 | if (typeof this.customState === 'function') { 24 | this.state = this.customState(this.state); 25 | } 26 | this.data = {}; 27 | this.validate = this.validate.bind(this); 28 | this.onChange = this.onChange.bind(this); 29 | this.setValue = this.setValue.bind(this); 30 | this.getDisplay = this.getDisplay.bind(this); 31 | this.getElements = this.getElements.bind(this); 32 | } 33 | 34 | componentDidMount() { 35 | this.unmounting = false; 36 | if (!this.props.options || !this.props.options.skipInit || !this.props.options.isInit) { 37 | this.setValue(this.state.value, null, true); 38 | } 39 | if (typeof this.props.attachToForm === 'function') { 40 | this.props.attachToForm(this); 41 | } 42 | } 43 | 44 | componentWillUnmount() { 45 | this.unmounting = true; 46 | if (typeof this.props.detachFromForm === 'function') { 47 | this.props.detachFromForm(this); 48 | } 49 | } 50 | 51 | componentDidUpdate(prevProps) { 52 | const {component} = prevProps; 53 | let value; 54 | if (component.hasOwnProperty('calculateValue') && component.calculateValue) { 55 | if (!deepEqual(this.data, this.props.data)) { 56 | this.data = clone(this.props.data); 57 | try { 58 | const result = eval('(function(data, row) { const value = [];' + component.calculateValue.toString() + '; return value; })(this.data, this.props.row)'); 59 | if (this.state.value !== result) { 60 | this.setValue(result); 61 | } 62 | } 63 | catch (e) { 64 | /* eslint-disable no-console */ 65 | console.warn('An error occurred calculating a value for ' + component.key, e); 66 | /* eslint-enable no-console */ 67 | } 68 | } 69 | } 70 | 71 | if (this.props.value && (!prevProps.value || prevProps.value !== this.props.value)) { 72 | value = safeSingleToMultiple(this.props.value, this.props.component); 73 | } 74 | 75 | // This occurs when a datagrid row is deleted. 76 | let defaultValue = getDefaultValue(value, this.props.component, this.getInitialValue, this.onChangeCustom); 77 | if (value === null && this.state.value !== defaultValue) { 78 | value = defaultValue; 79 | this.setState({ 80 | isPristine: true 81 | }); 82 | } 83 | if (typeof value !== 'undefined' && value !== null) { 84 | const valid = this.validate(value); 85 | this.setState({ 86 | value: valid, 87 | isValid: valid.isValid, 88 | errorType: valid.errorType, 89 | errorMessage: valid.errorMessage 90 | }); 91 | } 92 | if (typeof this.willReceiveProps === 'function') { 93 | this.willReceiveProps(this.props); 94 | } 95 | } 96 | 97 | validate(value) { 98 | return validate(value, this.props.component, this.props.data, this.validateCustom); 99 | } 100 | 101 | onChange(event) { 102 | let value = event.nativeEvent.text; 103 | // Allow components to respond to onChange event. 104 | if (typeof this.props.onChangeCustom === 'function') { 105 | value = this.props.onChangeCustom(value); 106 | } 107 | const index = (this.props.component.multiple ? event.nativeEvent.target : null); 108 | this.setValue(value, index); 109 | } 110 | 111 | setValue(value, index, pristine) { 112 | if (index === undefined) { 113 | index = null; 114 | } 115 | let newValue; 116 | if (index !== null && Array.isArray(this.state.value)) { 117 | // Clone so we keep state immutable. 118 | newValue = clone(this.state.value); 119 | newValue[index] = value; 120 | } 121 | else { 122 | newValue = value; 123 | } 124 | const validatedValue = this.validate(newValue); 125 | this.setState({ 126 | isPristine: !!pristine, 127 | value: validatedValue, 128 | }, () => { 129 | if (typeof this.props.onChange === 'function') { 130 | if (!this.state.isPristine || (this.props.value && this.props.value.item !== this.state.value.item)) { 131 | this.props.onChange(this); 132 | } 133 | } 134 | }); 135 | } 136 | 137 | getDisplay(component, value) { 138 | if (typeof this.getValueDisplay === 'function') { 139 | if (Array.isArray(value) && component.multiple && component.type !== 'file') { 140 | return value.map(this.getValueDisplay.bind(null, component)).join(', '); 141 | } 142 | else { 143 | return this.getValueDisplay(component, value); 144 | } 145 | } 146 | if (Array.isArray(value)) { 147 | return value.join(', '); 148 | } 149 | // If this is still an object, something went wrong and we don't know what to do with it. 150 | if (typeof value === 'object') { 151 | return '[object Object]'; 152 | } 153 | return value; 154 | } 155 | 156 | render() { 157 | let element; 158 | if (typeof this.props.onElementRender === 'function') { 159 | element = this.props.onElementRender(this, element); 160 | } 161 | element = this.getElements(); 162 | return element; 163 | } 164 | } 165 | 166 | ValueComponent.propTypes = { 167 | data: PropTypes.any, 168 | options: PropTypes.object, 169 | component: PropTypes.any, 170 | value: PropTypes.any, 171 | row: PropTypes.any, 172 | onChange: PropTypes.func, 173 | onElementRender: PropTypes.func, 174 | attachToForm: PropTypes.func, 175 | detachFromForm: PropTypes.func, 176 | }; 177 | -------------------------------------------------------------------------------- /src/components/FormComponents/signature/Signature.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {View, Image, Text, Modal} from 'react-native'; 3 | import {Button} from 'react-native-elements'; 4 | import ValueComponent from '../sharedComponents/Value'; 5 | import SignatureCapture from 'react-native-signature-capture'; 6 | import DeviceInfo from 'react-native-device-info'; 7 | import styles from './styles'; 8 | 9 | const isTablet = DeviceInfo.isTablet(); 10 | 11 | export default class Signature extends ValueComponent { 12 | constructor(props) { 13 | super(props); 14 | this.toggleSignaturePad = this.toggleSignaturePad.bind(this); 15 | this.willReceiveProps = this.willReceiveProps.bind(this); 16 | this.saveSignature = this.saveSignature.bind(this); 17 | this.clearSignature = this.clearSignature.bind(this); 18 | this.onEnd = this.onEnd.bind(this); 19 | this.getElements = this.getElements.bind(this); 20 | this.signature = null; 21 | } 22 | 23 | componentDidMount() { 24 | if (!this.signature) { 25 | return; 26 | } 27 | if (this.state.value) { 28 | this.signature.fromDataURL(this.state.value); 29 | } 30 | else { 31 | this.signature.resetImage(); 32 | } 33 | } 34 | 35 | willReceiveProps(nextProps) { 36 | if (!this.signature) { 37 | return; 38 | } 39 | if (this.props.value !== nextProps.value) { 40 | this.signature.fromDataURL(nextProps.value); 41 | } 42 | } 43 | 44 | onEnd(image) { 45 | if (!image || !image.encoded) { 46 | return; 47 | } 48 | const signature = `data:image/png;base64,${image.encoded}`; 49 | this.setValue(signature); 50 | this.toggleSignaturePad(); 51 | } 52 | 53 | toggleSignaturePad() { 54 | this.setState({ 55 | showSignaturePad: !this.state.showSignaturePad 56 | }); 57 | } 58 | 59 | saveSignature() { 60 | this.signature.saveImage(); 61 | } 62 | 63 | clearSignature() { 64 | this.signature.resetImage(); 65 | this.setValue(null); 66 | } 67 | 68 | getElements() { 69 | const {component} = this.props; 70 | 71 | if (this.props.readOnly) { 72 | const image = typeof this.state.value === 'object' ? this.state.value.item : this.state.value; 73 | return ( 74 | 75 | 80 | 81 | ); 82 | } 83 | return ( 84 | 85 | {this.state.value && this.state.value.item && 86 | 91 | } 92 | 42 | ); 43 | } 44 | }, 45 | visible: true 46 | }; 47 | }); 48 | if (form && form.components) { 49 | FormioUtils.eachComponent(form.components, (component, path) => { 50 | if (component.input && component.tableView && component.key && path.indexOf('.') === -1) { 51 | columns.push({ 52 | component: component, 53 | property: 'data.' + component.key, 54 | header: { 55 | label: component.label || component.key 56 | }, 57 | cell: { 58 | format: this.formatCell 59 | }, 60 | visible: true 61 | }); 62 | } 63 | }); 64 | } 65 | if (!buttons.length) { 66 | return columns; 67 | } 68 | if (this.props.buttonLocation === 'right') { 69 | return columns.concat(buttons); 70 | } 71 | else { 72 | return buttons.concat(columns); 73 | } 74 | } 75 | 76 | componentWillReceiveProps(nextProps) { 77 | if (nextProps.form !== this.props.form) { 78 | this.setState({ 79 | columns: this.columnsFromForm(nextProps.form) 80 | }); 81 | } 82 | if (nextProps.submissions !== this.state.submissions) { 83 | this.setState({ 84 | submissions: nextProps.submissions 85 | }); 86 | } 87 | if (nextProps.pagination.page !== this.state.pagination.page) { 88 | this.setState(curState => curState.pagination.page = nextProps.pagination.page); 89 | } 90 | if (nextProps.pagination.numPage !== this.state.pagination.numPage) { 91 | this.setState(curState => curState.pagination.numPage = nextProps.pagination.numPage); 92 | } 93 | if (nextProps.pagination.size !== this.state.pagination.size) { 94 | this.setState(curState => curState.pagination.size = nextProps.pagination.size); 95 | } 96 | } 97 | 98 | componentDidMount() { 99 | if (this.props.src) { 100 | this.formio = new Formiojs(this.props.src); 101 | this.loadForm(); 102 | this.loadSubmissions(); 103 | } 104 | } 105 | 106 | loadForm() { 107 | this.formio.loadForm().then(form => { 108 | this.setState({ 109 | columns: this.columnsFromForm(form) 110 | }); 111 | }); 112 | } 113 | 114 | loadSubmissions() { 115 | this.formio.loadSubmissions({ 116 | params: { 117 | ...this.props.query, 118 | limit: this.state.pagination.size, 119 | skip: (this.state.pagination.page - 1) * this.state.pagination.size 120 | } 121 | }).then(submissions => { 122 | this.setState(curState => { 123 | curState.submissions = submissions; 124 | curState.pagination.numPage = Math.ceil(submissions.serverCount / this.state.pagination.size); 125 | return curState; 126 | }); 127 | }); 128 | } 129 | 130 | onButtonClick(event, type, row) { 131 | event.preventDefault(); 132 | event.stopPropagation(); 133 | if (typeof this.props.onButtonClick === 'function') { 134 | this.props.onButtonClick(type, row._id); 135 | } 136 | } 137 | 138 | onRowClick(row) { 139 | return { 140 | onClick: () => { 141 | if (typeof this.props.onButtonClick === 'function') { 142 | this.props.onButtonClick('row', row._id); 143 | } 144 | } 145 | }; 146 | } 147 | 148 | onPageChange(page) { 149 | if (typeof this.props.onPageChange === 'function') { 150 | this.props.onPageChange(page); 151 | } 152 | else { 153 | this.setState(curState => curState.pagination.page = page, this.loadSubmissions); 154 | } 155 | } 156 | 157 | render() { 158 | let rows = this.state.columns; 159 | return ( 160 | 161 | {rows} 162 | 163 | ); 164 | } 165 | } 166 | 167 | FormioGrid.defaultProps = { 168 | form: {}, 169 | submissions: [], 170 | query: { 171 | sort: '-created' 172 | }, 173 | pagination: { 174 | page: 1, 175 | numPage: 1, 176 | sizes: [25, 50, 75], 177 | size: 25 178 | }, 179 | buttons: [], 180 | buttonLocation: 'right' 181 | }; 182 | 183 | FormioGrid.propTypes = { 184 | src: PropTypes.string, 185 | form: PropTypes.object, 186 | submissions: PropTypes.object, 187 | buttons: PropTypes.array, 188 | pagination: PropTypes.shape({ 189 | page: PropTypes.number, 190 | numPage: PropTypes.number, 191 | sizes: PropTypes.arrayOf(PropTypes.number), 192 | size: PropTypes.number 193 | }), 194 | query: PropTypes.shape({ 195 | sort: PropTypes.string 196 | }), 197 | buttonLocation: PropTypes.string, 198 | onButtonClick: PropTypes.func, 199 | onPageChange: PropTypes.func, 200 | }; 201 | -------------------------------------------------------------------------------- /src/components/index.js: -------------------------------------------------------------------------------- 1 | export {default as Formio} from './Formio'; 2 | export {default as FormBuilder} from './FormBuilder'; 3 | export {default as FormioComponentsList} from './FormioComponentsList'; 4 | export {default as FormioConfirm} from './FormioConfirm'; 5 | export {default as FormioGrid} from './FormioGrid'; 6 | -------------------------------------------------------------------------------- /src/defaultTheme/colors.js: -------------------------------------------------------------------------------- 1 | export default { 2 | mainBackground: '#ffffff', 3 | primary1Color: '#03a9f4', 4 | primary2Color: '#1976d2', 5 | primary3Color: '#bdbdbd', 6 | accent1Color: '#ec407a', 7 | accent2Color: 'rgba(245, 245, 245, 0.99)', 8 | accent3Color: '#9e9e9e', 9 | textColor: 'rgba(0, 0, 0, 0.87)', 10 | secondaryTextColor: 'rgba(0, 0, 0, 0.54)', 11 | alternateTextColor: '#ffffff', 12 | borderColor: '#9e9e9e', 13 | disabledColor: '#000000', 14 | pickerColor: '#2196f3', 15 | shadowColor: 'rgba(0, 0, 0, 0.54)', 16 | errorColor: 'rgb(244, 67, 54)', 17 | successColor: '#4BB543', 18 | }; 19 | -------------------------------------------------------------------------------- /src/defaultTheme/index.js: -------------------------------------------------------------------------------- 1 | import colors from './colors'; 2 | 3 | const Theme = { 4 | Main: { 5 | backgroundColor: colors.mainBackground 6 | }, 7 | Button: { 8 | backgroundColor: colors.primary1Color, 9 | color: colors.alternateTextColor, 10 | fontSize: 12, 11 | fontWeight: 700, 12 | height: 45 13 | }, 14 | ErrorMessage: { 15 | color: colors.errorColor, 16 | fontSize: 10, 17 | marginBottom: 15, 18 | textAlign: 'right' 19 | }, 20 | Fieldset: { 21 | borderBottomWidth: 1, 22 | borderBottomColor: colors.borderColor, 23 | labelColor: colors.textColor, 24 | labelSize: 9, 25 | labelWeight: 700, 26 | labelHeight: 25, 27 | paddingTop: 12, 28 | paddingBottom: 12, 29 | paddingLeft: 8, 30 | paddingRight: 8, 31 | }, 32 | Input: { 33 | borderColor: colors.borderColor, 34 | borderColorOnError: colors.errorColor, 35 | color: colors.textColor, 36 | placeholderTextColor: colors.primary3Color, 37 | fontSize: 14, 38 | lineHeight: 18 39 | }, 40 | Label: { 41 | color: colors.textColor, 42 | fontSize: 12, 43 | stackedHeight: 25 44 | }, 45 | Select: { 46 | 47 | } 48 | }; 49 | 50 | export default Theme; 51 | -------------------------------------------------------------------------------- /src/factories/FormioComponents.js: -------------------------------------------------------------------------------- 1 | const components = {}; 2 | const groups = { 3 | __component: { 4 | title: 'Basic Components' 5 | }, 6 | advanced: { 7 | title: 'Special Components' 8 | }, 9 | layout: { 10 | title: 'Layout Components' 11 | } 12 | }; 13 | 14 | export const FormioComponents = { 15 | addGroup: (name, group) => { 16 | groups[name] = group; 17 | }, 18 | register: (type, component, group) => { 19 | if (!components[type]) { 20 | components[type] = component; 21 | } 22 | else { 23 | Object.assign(components[type], component); 24 | } 25 | 26 | // Set the type for this component. 27 | if (!components[type].group) { 28 | components[type].group = group || '__component'; 29 | } 30 | }, 31 | getComponent: (type) => { 32 | return components.hasOwnProperty(type) ? components[type] : components['custom']; 33 | }, 34 | components, 35 | groups 36 | }; 37 | -------------------------------------------------------------------------------- /src/factories/index.js: -------------------------------------------------------------------------------- 1 | export * from './FormioComponents'; 2 | -------------------------------------------------------------------------------- /src/formio/providers/index.js: -------------------------------------------------------------------------------- 1 | import storage from './storage'; 2 | export default storage; 3 | -------------------------------------------------------------------------------- /src/formio/providers/storage/base64.js: -------------------------------------------------------------------------------- 1 | /** THIS FILE IS NOT USABLE, NEEDS TO BE RECREATED IN REACT NATIVE */ 2 | 3 | const base64 = () => ({ 4 | title: 'Base64', 5 | name: 'base64', 6 | uploadFile(file, fileName) { 7 | const reader = {}; 8 | 9 | return new Promise((resolve, reject) => { 10 | reader.onload = (event) => { 11 | const url = event.target.result; 12 | resolve({ 13 | storage: 'base64', 14 | name: fileName, 15 | url: url, 16 | size: file.size, 17 | type: file.type, 18 | data: url.replace(`data:${file.type};base64,`, '') 19 | }); 20 | }; 21 | 22 | reader.onerror = () => { 23 | return reject(this); 24 | }; 25 | 26 | reader.readAsDataURL(file); 27 | }); 28 | }, 29 | downloadFile(file) { 30 | // Return the original as there is nothing to do. 31 | return Promise.resolve(file); 32 | } 33 | }); 34 | 35 | base64.title = 'Base64'; 36 | export default base64; 37 | -------------------------------------------------------------------------------- /src/formio/providers/storage/dropbox.js: -------------------------------------------------------------------------------- 1 | /** THIS FILE IS NOT USABLE, NEEDS TO BE RECREATED IN REACT NATIVE */ 2 | 3 | const dropbox = (formio) => ({ 4 | uploadFile(file, fileName, dir, progressCallback) { 5 | return new Promise(((resolve, reject) => { 6 | // Send the file with data. 7 | const xhr = {}; 8 | 9 | if (typeof progressCallback === 'function') { 10 | xhr.upload.onprogress = progressCallback; 11 | } 12 | 13 | const fd = {}; 14 | fd.append('name', fileName); 15 | fd.append('dir', dir); 16 | fd.append('file', file); 17 | 18 | // Fire on network error. 19 | xhr.onerror = (err) => { 20 | err.networkError = true; 21 | reject(err); 22 | }; 23 | 24 | xhr.onload = () => { 25 | if (xhr.status >= 200 && xhr.status < 300) { 26 | const response = JSON.parse(xhr.response); 27 | response.storage = 'dropbox'; 28 | response.size = file.size; 29 | response.type = file.type; 30 | response.url = response.path_lower; 31 | resolve(response); 32 | } 33 | else { 34 | reject(xhr.response || 'Unable to upload file'); 35 | } 36 | }; 37 | 38 | xhr.onabort = reject; 39 | 40 | xhr.open('POST', `${formio.formUrl}/storage/dropbox`); 41 | const token = formio.getToken(); 42 | if (token) { 43 | xhr.setRequestHeader('x-jwt-token', token); 44 | } 45 | xhr.send(fd); 46 | })); 47 | }, 48 | downloadFile(file) { 49 | const token = formio.getToken(); 50 | file.url = 51 | `${formio.formUrl}/storage/dropbox?path_lower=${file.path_lower}${token ? `&x-jwt-token=${token}` : ''}`; 52 | return Promise.resolve(file); 53 | } 54 | }); 55 | 56 | dropbox.title = 'Dropbox'; 57 | export default dropbox; 58 | -------------------------------------------------------------------------------- /src/formio/providers/storage/index.js: -------------------------------------------------------------------------------- 1 | import base64 from './base64'; 2 | import dropbox from './dropbox'; 3 | import s3 from './s3'; 4 | import url from './url'; 5 | export default { 6 | base64: base64, 7 | dropbox: dropbox, 8 | s3: s3, 9 | url: url 10 | }; 11 | -------------------------------------------------------------------------------- /src/formio/providers/storage/s3.js: -------------------------------------------------------------------------------- 1 | /** THIS FILE IS NOT USABLE, NEEDS TO BE RECREATED IN REACT NATIVE */ 2 | 3 | const s3 = (formio) => ({ 4 | uploadFile(file, fileName, dir, progressCallback) { 5 | return new Promise(((resolve, reject) => { 6 | // Send the pre response to sign the upload. 7 | const pre = {}; 8 | 9 | const prefd = {}; 10 | prefd.append('name', fileName); 11 | prefd.append('size', file.size); 12 | prefd.append('type', file.type); 13 | 14 | // This only fires on a network error. 15 | pre.onerror = (err) => { 16 | err.networkError = true; 17 | reject(err); 18 | }; 19 | 20 | pre.onabort = reject; 21 | 22 | pre.onload = () => { 23 | if (pre.status >= 200 && pre.status < 300) { 24 | const response = JSON.parse(pre.response); 25 | 26 | // Send the file with data. 27 | const xhr = {}; 28 | 29 | if (typeof progressCallback === 'function') { 30 | xhr.upload.onprogress = progressCallback; 31 | } 32 | 33 | response.data.fileName = fileName; 34 | response.data.key += dir + fileName; 35 | 36 | const fd = {}; 37 | for (const key in response.data) { 38 | fd.append(key, response.data[key]); 39 | } 40 | fd.append('file', file); 41 | 42 | // Fire on network error. 43 | xhr.onerror = (err) => { 44 | err.networkError = true; 45 | reject(err); 46 | }; 47 | 48 | xhr.onload = () => { 49 | if (xhr.status >= 200 && xhr.status < 300) { 50 | resolve({ 51 | storage: 's3', 52 | name: fileName, 53 | bucket: response.bucket, 54 | key: response.data.key, 55 | url: response.url + response.data.key, 56 | acl: response.data.acl, 57 | size: file.size, 58 | type: file.type 59 | }); 60 | } 61 | else { 62 | reject(xhr.response || 'Unable to upload file'); 63 | } 64 | }; 65 | 66 | xhr.onabort = reject; 67 | 68 | xhr.open('POST', response.url); 69 | 70 | xhr.send(fd); 71 | } 72 | else { 73 | reject(pre.response || 'Unable to sign file'); 74 | } 75 | }; 76 | 77 | pre.open('POST', `${formio.formUrl}/storage/s3`); 78 | 79 | pre.setRequestHeader('Accept', 'application/json'); 80 | pre.setRequestHeader('Content-Type', 'application/json; charset=UTF-8'); 81 | const token = formio.getToken(); 82 | if (token) { 83 | pre.setRequestHeader('x-jwt-token', token); 84 | } 85 | 86 | pre.send(JSON.stringify({ 87 | name: fileName, 88 | size: file.size, 89 | type: file.type 90 | })); 91 | })); 92 | }, 93 | downloadFile(file) { 94 | if (file.acl !== 'public-read') { 95 | return formio.makeRequest('file', `${formio.formUrl}/storage/s3?bucket=${file.bucket}&key=${file.key}`, 'GET'); 96 | } 97 | else { 98 | return Promise.resolve(file); 99 | } 100 | } 101 | }); 102 | 103 | s3.title = 'S3'; 104 | export default s3; 105 | -------------------------------------------------------------------------------- /src/formio/providers/storage/url.js: -------------------------------------------------------------------------------- 1 | /** THIS FILE IS NOT USABLE, NEEDS TO BE RECREATED IN REACT NATIVE */ 2 | 3 | const url = (formio) => ({ 4 | title: 'Url', 5 | name: 'url', 6 | uploadFile(file, fileName, dir, progressCallback, url) { 7 | return new Promise(((resolve, reject) => { 8 | const data = { 9 | dir, 10 | file, 11 | name: fileName 12 | }; 13 | 14 | // Send the file with data. 15 | const xhr = {}; 16 | 17 | if (typeof progressCallback === 'function') { 18 | xhr.upload.onprogress = progressCallback; 19 | } 20 | 21 | const fd = {}; 22 | for (const key in data) { 23 | fd.append(key, data[key]); 24 | } 25 | 26 | xhr.onload = () => { 27 | if (xhr.status >= 200 && xhr.status < 300) { 28 | // Need to test if xhr.response is decoded or not. 29 | let respData = {}; 30 | try { 31 | respData = (typeof xhr.response === 'string') ? JSON.parse(xhr.response) : {}; 32 | respData = (respData && respData.data) ? respData.data : respData; 33 | } 34 | catch (err) { 35 | respData = {}; 36 | } 37 | 38 | const url = respData.hasOwnProperty('url') ? respData.url : `${xhr.responseURL}/${fileName}`; 39 | resolve({ 40 | storage: 'url', 41 | name: fileName, 42 | url, 43 | size: file.size, 44 | type: file.type, 45 | data: respData 46 | }); 47 | } 48 | else { 49 | reject(xhr.response || 'Unable to upload file'); 50 | } 51 | }; 52 | 53 | // Fire on network error. 54 | xhr.onerror = () => reject(xhr); 55 | 56 | xhr.onabort = () => reject(xhr); 57 | 58 | xhr.open('POST', url); 59 | const token = formio.getToken(); 60 | if (token) { 61 | xhr.setRequestHeader('x-jwt-token', token); 62 | } 63 | xhr.send(fd); 64 | })); 65 | }, 66 | downloadFile(file) { 67 | // Return the original as there is nothing to do. 68 | return Promise.resolve(file); 69 | } 70 | }); 71 | 72 | url.title = 'Url'; 73 | export default url; 74 | -------------------------------------------------------------------------------- /src/formio/utils/builder.js: -------------------------------------------------------------------------------- 1 | import _ from 'lodash'; 2 | import {eachComponent} from './utils'; 3 | export default { 4 | /** 5 | * Iterate the given key to make it unique. 6 | * 7 | * @param {String} key 8 | * Modify the component key to be unique. 9 | * 10 | * @returns {String} 11 | * The new component key. 12 | */ 13 | iterateKey(key) { 14 | if (!key.match(/(\d+)$/)) { 15 | return `${key}2`; 16 | } 17 | 18 | return key.replace(/(\d+)$/, function(suffix) { 19 | return Number(suffix) + 1; 20 | }); 21 | }, 22 | 23 | /** 24 | * Appends a number to a component.key to keep it unique 25 | * 26 | * @param {Object} form 27 | * The components parent form. 28 | * @param {Object} component 29 | * The component to uniquify 30 | */ 31 | uniquify(form, component) { 32 | let changed = false; 33 | const formKeys = {}; 34 | eachComponent(form.components, function(comp) { 35 | formKeys[comp.key] = true; 36 | }); 37 | 38 | // Recurse into all child components. 39 | eachComponent([component], (component) => { 40 | // Skip key uniquification if this component doesn't have a key. 41 | if (!component.key) { 42 | return; 43 | } 44 | 45 | while (formKeys.hasOwnProperty(component.key)) { 46 | component.key = this.iterateKey(component.key); 47 | changed = true; 48 | } 49 | }, true); 50 | return changed; 51 | }, 52 | 53 | additionalShortcuts: { 54 | button: [ 55 | 'Enter', 56 | 'Esc' 57 | ] 58 | }, 59 | 60 | getAlphaShortcuts() { 61 | return _.range('A'.charCodeAt(), 'Z'.charCodeAt() + 1).map((charCode) => String.fromCharCode(charCode)); 62 | }, 63 | 64 | getAdditionalShortcuts(type) { 65 | return this.additionalShortcuts[type] || []; 66 | }, 67 | 68 | getBindedShortcuts(components, input) { 69 | const result = []; 70 | 71 | eachComponent(components, function(component) { 72 | if (component === input) { 73 | return; 74 | } 75 | 76 | if (component.shortcut) { 77 | result.push(component.shortcut); 78 | } 79 | if (component.values) { 80 | component.values.forEach((value) => { 81 | if (value.shortcut) { 82 | result.push(value.shortcut); 83 | } 84 | }); 85 | } 86 | }, true); 87 | 88 | return result; 89 | }, 90 | 91 | getAvailableShortcuts(form, component) { 92 | if (!component) { 93 | return []; 94 | } 95 | return [''].concat(_.difference( 96 | this.getAlphaShortcuts().concat(this.getAdditionalShortcuts(component.type)), 97 | this.getBindedShortcuts(form.components, component)) 98 | ); 99 | } 100 | }; 101 | -------------------------------------------------------------------------------- /src/formio/utils/fixtures/components.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "type": "textfield", 4 | "key": "one", 5 | "order": 1 6 | }, 7 | { 8 | "input": false, 9 | "key": "parent1", 10 | "components": [ 11 | { 12 | "type": "textfield", 13 | "key": "two", 14 | "order": 2 15 | }, 16 | { 17 | "input": false, 18 | "key": "parent2", 19 | "columns": [ 20 | { 21 | "components": [ 22 | { 23 | "type": "textfield", 24 | "key": "three", 25 | "order": 3 26 | } 27 | ] 28 | }, 29 | { 30 | "components": [ 31 | { 32 | "rows": [ 33 | [ 34 | { 35 | "components": [ 36 | { 37 | "key": "four", 38 | "order": 4, 39 | "type": "textfield" 40 | } 41 | ] 42 | }, 43 | { 44 | "components": [ 45 | { 46 | "key": "five", 47 | "order": 5, 48 | "type": "textfield" 49 | } 50 | ] 51 | } 52 | ], 53 | [ 54 | { 55 | "components": [ 56 | { 57 | "key": "six", 58 | "order": 6, 59 | "type": "textfield" 60 | } 61 | ] 62 | }, 63 | { 64 | "components": [ 65 | { 66 | "key": "seven", 67 | "order": 7, 68 | "type": "textarea", 69 | "rows": 3 70 | } 71 | ] 72 | } 73 | ] 74 | ], 75 | "type": "table" 76 | } 77 | ] 78 | } 79 | ], 80 | "type": "columns" 81 | } 82 | ], 83 | "type": "well" 84 | }, 85 | { 86 | "key": "eight", 87 | "order": 8, 88 | "type": "button" 89 | } 90 | ] 91 | -------------------------------------------------------------------------------- /src/formio/utils/fixtures/submission1.json: -------------------------------------------------------------------------------- 1 | { 2 | "data": { 3 | "name": "John Smith", 4 | "age": "30", 5 | "colors": [ 6 | {"value": "red", "label": "Red"}, 7 | {"value": "blue", "label": "Blue"}, 8 | {"value": "green", "label": "Green"} 9 | ], 10 | "selectboxes": { 11 | "car": false, 12 | "boat": true 13 | }, 14 | "mycontainer": { 15 | "checked": true, 16 | "animalname": "Fluffy" 17 | } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/formio/utils/index.js: -------------------------------------------------------------------------------- 1 | import * as FormioUtils from './utils'; 2 | if (typeof global === 'object') { 3 | global.FormioUtils = FormioUtils; 4 | } 5 | export default FormioUtils; 6 | -------------------------------------------------------------------------------- /src/formio/utils/jsonlogic/operators.js: -------------------------------------------------------------------------------- 1 | // Use only immutable useful functions from Lodash. 2 | // Visit https://lodash.com/docs for more info. 3 | export const lodashOperators = [ 4 | // Array 5 | 'chunk', 6 | 'compact', 7 | 'concat', 8 | 'difference', 9 | 'differenceBy', 10 | 'differenceWith', 11 | 'drop', 12 | 'dropRight', 13 | 'dropRightWhile', 14 | 'dropWhile', 15 | 'findIndex', 16 | 'findLastIndex', 17 | 'first', 18 | 'flatten', 19 | 'flattenDeep', 20 | 'flattenDepth', 21 | 'fromPairs', 22 | 'head', 23 | 'indexOf', 24 | 'initial', 25 | 'intersection', 26 | 'intersectionBy', 27 | 'intersectionWith', 28 | 'join', 29 | 'last', 30 | 'lastIndexOf', 31 | 'nth', 32 | 'slice', 33 | 'sortedIndex', 34 | 'sortedIndexBy', 35 | 'sortedIndexOf', 36 | 'sortedLastIndex', 37 | 'sortedLastIndexBy', 38 | 'sortedLastIndexOf', 39 | 'sortedUniq', 40 | 'sortedUniqBy', 41 | 'tail', 42 | 'take', 43 | 'takeRight', 44 | 'takeRightWhile', 45 | 'takeWhile', 46 | 'union', 47 | 'unionBy', 48 | 'unionWith', 49 | 'uniq', 50 | 'uniqBy', 51 | 'uniqWith', 52 | 'unzip', 53 | 'unzipWith', 54 | 'without', 55 | 'xor', 56 | 'xorBy', 57 | 'xorWith', 58 | 'zip', 59 | 'zipObject', 60 | 'zipObjectDeep', 61 | 'zipWith', 62 | // Collection 63 | 'countBy', 64 | 'every', 65 | 'filter', 66 | 'find', 67 | 'findLast', 68 | 'flatMap', 69 | 'flatMapDeep', 70 | 'flatMapDepth', 71 | 'groupBy', 72 | 'includes', 73 | 'invokeMap', 74 | 'keyBy', 75 | 'map', 76 | 'orderBy', 77 | 'partition', 78 | 'reduce', 79 | 'reduceRight', 80 | 'reject', 81 | 'sample', 82 | 'sampleSize', 83 | 'shuffle', 84 | 'size', 85 | 'some', 86 | 'sortBy', 87 | // Date 88 | 'now', 89 | // Function 90 | 'flip', 91 | 'negate', 92 | 'overArgs', 93 | 'partial', 94 | 'partialRight', 95 | 'rearg', 96 | 'rest', 97 | 'spread', 98 | // Lang 99 | 'castArray', 100 | 'clone', 101 | 'cloneDeep', 102 | 'cloneDeepWith', 103 | 'cloneDeep', 104 | 'conformsTo', 105 | 'eq', 106 | 'gt', 107 | 'gte', 108 | 'isArguments', 109 | 'isArray', 110 | 'isArrayBuffer', 111 | 'isArrayLike', 112 | 'isArrayLikeObject', 113 | 'isBoolean', 114 | 'isBuffer', 115 | 'isDate', 116 | 'isElement', 117 | 'isEmpty', 118 | 'isEqual', 119 | 'isEqualWith', 120 | 'isError', 121 | 'isFinite', 122 | 'isFunction', 123 | 'isInteger', 124 | 'isLength', 125 | 'isMap', 126 | 'isMatch', 127 | 'isMatchWith', 128 | 'isNaN', 129 | 'isNative', 130 | 'isNil', 131 | 'isNull', 132 | 'isNumber', 133 | 'isObject', 134 | 'isObjectLike', 135 | 'isPlainObject', 136 | 'isRegExp', 137 | 'isSafeInteger', 138 | 'isSet', 139 | 'isString', 140 | 'isSymbol', 141 | 'isTypedArray', 142 | 'isUndefined', 143 | 'isWeakMap', 144 | 'isWeakSet', 145 | 'lt', 146 | 'lte', 147 | 'toArray', 148 | 'toFinite', 149 | 'toInteger', 150 | 'toLength', 151 | 'toNumber', 152 | 'toPlainObject', 153 | 'toSafeInteger', 154 | 'toString', 155 | // Math 156 | 'add', 157 | 'ceil', 158 | 'divide', 159 | 'floor', 160 | 'max', 161 | 'maxBy', 162 | 'mean', 163 | 'meanBy', 164 | 'min', 165 | 'minBy', 166 | 'multiply', 167 | 'round', 168 | 'subtract', 169 | 'sum', 170 | 'sumBy', 171 | // Number 172 | 'clamp', 173 | 'inRange', 174 | 'random', 175 | // Object 176 | 'at', 177 | 'entries', 178 | 'entriesIn', 179 | 'findKey', 180 | 'findLastKey', 181 | 'functions', 182 | 'functionsIn', 183 | 'get', 184 | 'has', 185 | 'hasIn', 186 | 'invert', 187 | 'invertBy', 188 | 'invoke', 189 | 'keys', 190 | 'keysIn', 191 | 'mapKeys', 192 | 'mapValues', 193 | 'omit', 194 | 'omitBy', 195 | 'pick', 196 | 'pickBy', 197 | 'result', 198 | 'toPairs', 199 | 'toPairsIn', 200 | 'transform', 201 | 'values', 202 | 'valuesIn', 203 | // String 204 | 'camelCase', 205 | 'capitalize', 206 | 'deburr', 207 | 'endsWith', 208 | 'escape', 209 | 'escapeRegExp', 210 | 'kebabCase', 211 | 'lowerCase', 212 | 'lowerFirst', 213 | 'pad', 214 | 'padEnd', 215 | 'padStart', 216 | 'parseInt', 217 | 'repeat', 218 | 'replace', 219 | 'snakeCase', 220 | 'split', 221 | 'startCase', 222 | 'startsWith', 223 | 'toLower', 224 | 'toUpper', 225 | 'trim', 226 | 'trimEnd', 227 | 'trimStart', 228 | 'truncate', 229 | 'unescape', 230 | 'upperCase', 231 | 'upperFirst', 232 | 'words', 233 | // Util 234 | 'cond', 235 | 'conforms', 236 | 'constant', 237 | 'defaultTo', 238 | 'flow', 239 | 'flowRight', 240 | 'identity', 241 | 'iteratee', 242 | 'matches', 243 | 'matchesProperty', 244 | 'method', 245 | 'methodOf', 246 | 'nthArg', 247 | 'over', 248 | 'overEvery', 249 | 'overSome', 250 | 'property', 251 | 'propertyOf', 252 | 'range', 253 | 'rangeRight', 254 | 'stubArray', 255 | 'stubFalse', 256 | 'stubObject', 257 | 'stubString', 258 | 'stubTrue', 259 | 'times', 260 | 'toPath', 261 | 'uniqueId', 262 | ]; 263 | -------------------------------------------------------------------------------- /src/formio/utils/jsonlogic/operators.spec.js: -------------------------------------------------------------------------------- 1 | import {jsonLogic} from '../utils'; 2 | 3 | describe.skip('Lodash operators', () => { 4 | describe.skip('Arrays', () => { 5 | 6 | }); 7 | describe.skip('Collection', () => { 8 | 9 | }); 10 | describe.skip('Date', () => { 11 | 12 | }); 13 | describe.skip('Function', () => { 14 | 15 | }); 16 | describe.skip('Lang', () => { 17 | it('isEqual', () => { 18 | const logicTrue = {_isEqual: [[2, 3], [2, 3]]}; 19 | const logicFalse = {_isEqual: [[2, 3], [2, 4]]}; 20 | expect(jsonLogic.apply(logicTrue)).to.be.equal(true); 21 | expect(jsonLogic.apply(logicFalse)).to.be.equal(false); 22 | }); 23 | }); 24 | describe.skip('Math', () => { 25 | it('add', () => { 26 | const logic = {_add: [2, 3]}; 27 | expect(jsonLogic.apply(logic)).to.be.equal(5); 28 | }); 29 | it('ceil', () => { 30 | const logic = {_ceil: [4.006]}; 31 | expect(jsonLogic.apply(logic)).to.be.equal(5); 32 | }); 33 | it('divide', () => { 34 | const logic = {_divide: [6, 3]}; 35 | expect(jsonLogic.apply(logic)).to.be.equal(2); 36 | }); 37 | it('floor', () => { 38 | const logic = {_floor: [4.906]}; 39 | expect(jsonLogic.apply(logic)).to.be.equal(4); 40 | }); 41 | it('max', () => { 42 | const logic = {_max: [[2, 5, 6, 3]]}; 43 | expect(jsonLogic.apply(logic)).to.be.equal(6); 44 | }); 45 | it('maxBy', () => { 46 | const data = [{n: 4}, {n: 2}, {n: 8}, {n: 6}]; 47 | const logic1 = {_maxBy: [{'var': ''}, 'n']}; 48 | const logic2 = {_maxBy: [{'var': ''}, {_property: 'n'}]}; 49 | expect(jsonLogic.apply(logic1, data)).to.be.equal(data[2]); 50 | expect(jsonLogic.apply(logic2, data)).to.be.equal(data[2]); 51 | }); 52 | it('mean', () => { 53 | const logic = {_mean: [[2, 5, 6, 3]]}; 54 | expect(jsonLogic.apply(logic)).to.be.equal(4); 55 | }); 56 | it('meanBy', () => { 57 | const data = [{n: 4}, {n: 2}, {n: 8}, {n: 6}]; 58 | const logic1 = {_meanBy: [{'var': ''}, 'n']}; 59 | const logic2 = {_meanBy: [{'var': ''}, {_property: 'n'}]}; 60 | expect(jsonLogic.apply(logic1, data)).to.be.equal(5); 61 | expect(jsonLogic.apply(logic2, data)).to.be.equal(5); 62 | }); 63 | it('min', () => { 64 | const logic = {_min: [[2, 5, 6, 3]]}; 65 | expect(jsonLogic.apply(logic)).to.be.equal(2); 66 | }); 67 | it('minBy', () => { 68 | const data = [{n: 4}, {n: 2}, {n: 8}, {n: 6}]; 69 | const logic1 = {_minBy: [{'var': ''}, 'n']}; 70 | const logic2 = {_minBy: [{'var': ''}, {_property: 'n'}]}; 71 | expect(jsonLogic.apply(logic1, data)).to.be.equal(data[1]); 72 | expect(jsonLogic.apply(logic2, data)).to.be.equal(data[1]); 73 | }); 74 | it('multiply', () => { 75 | const logic = {_multiply: [2, 3]}; 76 | expect(jsonLogic.apply(logic)).to.be.equal(6); 77 | }); 78 | it('round', () => { 79 | const logic1 = {_round: [4.006]}; 80 | const logic2 = {_round: [4.906]}; 81 | expect(jsonLogic.apply(logic1)).to.be.equal(4); 82 | expect(jsonLogic.apply(logic2)).to.be.equal(5); 83 | }); 84 | it('multiply', () => { 85 | const logic = {_multiply: [2, 3]}; 86 | expect(jsonLogic.apply(logic)).to.be.equal(6); 87 | }); 88 | it('subtract', () => { 89 | const logic = {_subtract: [2, 3]}; 90 | expect(jsonLogic.apply(logic)).to.be.equal(-1); 91 | }); 92 | it('sum', () => { 93 | const logic = {_sum: [[2, 3]]}; 94 | expect(jsonLogic.apply(logic)).to.be.equal(5); 95 | }); 96 | it('sumBy', () => { 97 | const data = [{n: 4}, {n: 2}, {n: 8}, {n: 6}]; 98 | const logic1 = {_sumBy: [{'var': ''}, 'n']}; 99 | const logic2 = {_sumBy: [{'var': ''}, {_property: 'n'}]}; 100 | expect(jsonLogic.apply(logic1, data)).to.be.equal(20); 101 | expect(jsonLogic.apply(logic2, data)).to.be.equal(20); 102 | }); 103 | }); 104 | describe.skip('Number', () => { 105 | 106 | }); 107 | describe.skip('Object', () => { 108 | 109 | }); 110 | describe.skip('String', () => { 111 | 112 | }); 113 | describe.skip('Util', () => { 114 | it('property', () => { 115 | const data = [ 116 | {'a': {'b': 2}}, 117 | {'a': {'b': 1}} 118 | ]; 119 | const logic = {_sumBy: [{'var': ''}, {_property: 'a.b'}]}; 120 | expect(jsonLogic.apply(logic, data)).to.be.equal(3); 121 | }); 122 | }); 123 | }); 124 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | // Master export file for multiple components and features. 2 | export * from './components'; 3 | export * from './factories'; 4 | export * from './util'; 5 | -------------------------------------------------------------------------------- /src/util/constants.js: -------------------------------------------------------------------------------- 1 | export const ErrorTypes = { 2 | FieldValueError: 'FieldValueError', 3 | FormFetchError: 'FormFetchError', 4 | FormLoadError: 'FormLoadError', 5 | SubmissionError: 'SubmissionError', 6 | VieldValidationError: 'VieldValidationError', 7 | }; 8 | -------------------------------------------------------------------------------- /src/util/index.js: -------------------------------------------------------------------------------- 1 | import get from 'lodash/get'; 2 | import has from 'lodash/has'; 3 | import isEqual from 'lodash/isEqual'; 4 | 5 | function defineTransformerOutsideStrictMode() { 6 | var safeGlobalName = '____formioSelectMixinGetTransformer'; 7 | var globalObject = typeof global !== 'undefined' ? global : {}; 8 | 9 | /* We are essentially doing this, but because we're in strict mode by default in all babeled 10 | * modules, we need to escape it 11 | * 12 | * //string-replace callback, called for every match in the template. 13 | * function transform (_, expression) { 14 | * //bring the properties of 'props' into local scope so that the expression can reference them 15 | * with (props) { 16 | * return eval(expression); //evaluate the expression. 17 | * } 18 | * } 19 | */ 20 | 21 | // This escapes strict mode. 22 | try { 23 | (1,eval)('function '+safeGlobalName+' (props) { return function (_, exp) { with(props) { return eval(exp); } } }'); 24 | } 25 | catch (error) { 26 | return ''; 27 | } 28 | 29 | var ret = eval(safeGlobalName); 30 | 31 | // Cleanup 32 | // This can throw errors in some versions of IE for unknown reasons at this time. 33 | if (globalObject[safeGlobalName]) { 34 | delete globalObject[safeGlobalName]; 35 | } 36 | 37 | return ret; 38 | } 39 | 40 | var getTransformer = defineTransformerOutsideStrictMode(); 41 | 42 | /** 43 | * This function is intended to mimic the Angular $interpolate function. 44 | * 45 | * Since template strings are created using Angular syntax, we need to mimic rendering them to strings. This function 46 | * will take the template and the associated variables and render it out. It currently does basic compiling but 47 | * is not full featured compatible with Angular. 48 | * 49 | * @param template 50 | * @param variables 51 | * @returns {string|XML|*|void} 52 | */ 53 | export const interpolate = (template, variables) => { 54 | var transform = getTransformer(variables); 55 | //find all {{ }} expression blocks and then replace the blocks with their evaluation. 56 | try { 57 | return template.replace(/\{\s*\{([^\}]*)\}\s*\}/gm, transform); 58 | } 59 | catch (error) { 60 | return ''; 61 | } 62 | }; 63 | 64 | /** 65 | * This function serializes an object to a queryString. 66 | * @param obj 67 | * @returns {string} 68 | */ 69 | export const serialize = obj => { 70 | var str = []; 71 | for (var p in obj) { 72 | if (obj.hasOwnProperty(p)) { 73 | str.push(encodeURIComponent(p) + '=' + encodeURIComponent(obj[p])); 74 | } 75 | } 76 | return str.join('&'); 77 | }; 78 | 79 | //helper function to render raw html under a react element. 80 | export const raw = function(html) { 81 | return {dangerouslySetInnerHTML: {__html: html}}; 82 | }; 83 | 84 | export const fileSize = function(a, b, c, d, e) { 85 | /* eslint-disable space-before-function-paren */ 86 | return (b = Math, c = b.log, d = 1024, e = c(a) / c(d) | 0, a / b.pow(d, e)).toFixed(2) + ' ' + (e ? 'kMGTPEZY'[--e] + 'B' : 'Bytes'); 87 | }; 88 | 89 | // Resolve nested values within an array. 90 | export const nested = function({rowData, column}) { 91 | const {property} = column; 92 | 93 | if (!property) { 94 | return {}; 95 | } 96 | 97 | if (!has(rowData, property)) { 98 | //console.warn( // eslint-disable-line no-console 99 | // `resolve.nested - Failed to find "${property}" property from`, 100 | // rowData 101 | //); 102 | 103 | return {}; 104 | } 105 | 106 | return { 107 | ...rowData, 108 | [property]: get(rowData, property) 109 | }; 110 | }; 111 | 112 | export const deepEqual = isEqual; 113 | -------------------------------------------------------------------------------- /test/.setup.js: -------------------------------------------------------------------------------- 1 | 2 | import { Response, Headers, Request } from 'node-fetch'; 3 | 4 | global.Response = Response; 5 | global.Headers = Headers; 6 | global.Request = Request; 7 | 8 | jest.useFakeTimers() 9 | 10 | jest.mock('react-native-signature-capture', () => { 11 | return () => ('SignatureCapture') 12 | }) 13 | 14 | jest.mock('react-native-material-dropdown', () => { 15 | return { 16 | Dropdown: ()=> ('Dropdown') 17 | } 18 | }) 19 | 20 | jest.mock('react-native-multiple-select', () => { 21 | return () => ('MultiSelect') 22 | }) 23 | 24 | jest.mock('react-native-modal-datetime-picker', () => { 25 | return () => ('DateTimePicker') 26 | }) 27 | 28 | jest.mock('react-native-htmlview', () => { 29 | return () => ('HTMLView') 30 | }) 31 | 32 | jest.mock('react-native-device-info', () => { 33 | return { 34 | isTablet: () => (true) 35 | } 36 | }) 37 | 38 | jest.mock('react-native-select-multiple', () => { 39 | return () => ('SelectBoxes') 40 | }) -------------------------------------------------------------------------------- /test/__snapshots__/changes.spec.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`Change Events @change fires change events on a form 1`] = ` 4 | 17 | 18 | 25 | 32 | 40 | 69 | 86 | 87 | 88 | 89 | 106 | 107 | 108 | 109 | My Textfield 110 | 111 | 128 | 129 | 130 | 131 | 132 | 148 | 181 | 182 | 183 | 184 | 185 | 186 | `; 187 | -------------------------------------------------------------------------------- /test/forms/componentSpec.js: -------------------------------------------------------------------------------- 1 | 2 | //Spec for textfeild component 3 | export var textfeild = { 4 | 'input': true, 5 | 'tableView': true, 6 | 'inputType': 'text', 7 | 'inputMask': '', 8 | 'label': 'My Textfield', 9 | 'key': 'myTextfield', 10 | 'placeholder': '', 11 | 'prefix': '', 12 | 'suffix': '', 13 | 'multiple': false, 14 | 'defaultValue': '', 15 | 'protected': false, 16 | 'unique': false, 17 | 'persistent': true, 18 | 'validate': { 19 | 'required': false, 20 | 'minLength': '', 21 | 'maxLength': '', 22 | 'pattern': '', 23 | 'custom': '', 24 | 'customPrivate': false 25 | }, 26 | 'conditional': { 27 | 'show': null, 28 | 'when': null, 29 | 'eq': '' 30 | }, 31 | 'type': 'textfield' 32 | }, 33 | //Spec for password component 34 | password= { 35 | 'input': true, 36 | 'tableView': false, 37 | 'inputType': 'password', 38 | 'label': 'Password', 39 | 'key': 'password', 40 | 'placeholder': '', 41 | 'prefix': '', 42 | 'suffix': '', 43 | 'protected': true, 44 | 'persistent': true, 45 | 'inputMask': '', 46 | 'multiple': false, 47 | 'defaultValue': '', 48 | 'unique': false, 49 | 'validate': { 50 | 'required': false, 51 | 'minLength': '', 52 | 'maxLength': '', 53 | 'pattern': '', 54 | 'custom': '', 55 | 'customPrivate': false 56 | }, 57 | 'conditional': { 58 | 'show': null, 59 | 'when': null, 60 | 'eq': '' 61 | }, 62 | 'type': 'password' 63 | }, 64 | //Spec for phoneNumber component 65 | phoneNumber= { 66 | 'conditional': { 67 | 'eq': '', 68 | 'when': null, 69 | 'show': '' 70 | }, 71 | 'type': 'phoneNumber', 72 | 'validate': { 73 | 'required': false 74 | }, 75 | 'defaultValue': '', 76 | 'persistent': true, 77 | 'unique': false, 78 | 'protected': false, 79 | 'multiple': false, 80 | 'suffix': '', 81 | 'prefix': '', 82 | 'placeholder': '', 83 | 'key': 'phoneNumber', 84 | 'label': 'Phone Number', 85 | 'inputMask': '(999) 999-9999', 86 | 'tableView': true, 87 | 'input': true 88 | }; 89 | -------------------------------------------------------------------------------- /test/forms/empty.json: -------------------------------------------------------------------------------- 1 | { 2 | "title": "Textfield", 3 | "display": "form", 4 | "type": "form", 5 | "name": "textfield", 6 | "path": "textfield", 7 | "components": [] 8 | } -------------------------------------------------------------------------------- /test/forms/tmp.json: -------------------------------------------------------------------------------- 1 | { 2 | "_id": "58ae037c593e8c00967f124b", 3 | "modified": "2017-02-23T21:53:22.291Z", 4 | "title": "conditional", 5 | "display": "form", 6 | "type": "form", 7 | "name": "conditional", 8 | "path": "conditional", 9 | "project": "5811179a7fd506006b4a8327", 10 | "created": "2017-02-22T21:32:44.599Z", 11 | "components": [ 12 | { 13 | "conditional": { 14 | "eq": "", 15 | "when": null, 16 | "show": "" 17 | }, 18 | "tags": [], 19 | "type": "checkbox", 20 | "validate": { 21 | "required": false 22 | }, 23 | "clearOnHide": true, 24 | "persistent": true, 25 | "protected": false, 26 | "defaultValue": false, 27 | "key": "visible", 28 | "datagridLabel": true, 29 | "label": "Visible", 30 | "hideLabel": true, 31 | "tableView": true, 32 | "inputType": "checkbox", 33 | "input": true, 34 | "lockKey": true 35 | }, 36 | { 37 | "conditional": { 38 | "eq": "true", 39 | "when": "visible", 40 | "show": "true" 41 | }, 42 | "tags": [], 43 | "type": "datagrid", 44 | "clearOnHide": true, 45 | "persistent": true, 46 | "protected": false, 47 | "key": "datagrid1", 48 | "label": "Datagrid", 49 | "tableView": true, 50 | "components": [ 51 | { 52 | "hideLabel": true, 53 | "isNew": false, 54 | "tags": [], 55 | "type": "textfield", 56 | "conditional": { 57 | "eq": "", 58 | "when": null, 59 | "show": "" 60 | }, 61 | "validate": { 62 | "customPrivate": false, 63 | "custom": "", 64 | "pattern": "", 65 | "maxLength": "", 66 | "minLength": "", 67 | "required": true 68 | }, 69 | "clearOnHide": true, 70 | "persistent": true, 71 | "unique": false, 72 | "protected": false, 73 | "defaultValue": "", 74 | "multiple": false, 75 | "suffix": "", 76 | "prefix": "", 77 | "placeholder": "", 78 | "key": "textfield", 79 | "label": "Textfield", 80 | "inputMask": "", 81 | "inputType": "text", 82 | "tableView": true, 83 | "input": true 84 | } 85 | ], 86 | "tree": true, 87 | "input": true 88 | } 89 | ], 90 | "owner": "55673dc04f0405dd28205bb7", 91 | "submissionAccess": [], 92 | "access": [ 93 | { 94 | "type": "read_all", 95 | "roles": [ 96 | "5811179a7fd506006b4a8328", 97 | "5811179a7fd506006b4a8329", 98 | "5811179a7fd506006b4a832a" 99 | ] 100 | } 101 | ], 102 | "tags": [] 103 | }/**/ -------------------------------------------------------------------------------- /test/submissions-wip.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Formio from '../src/Formio'; 3 | import renderer from 'react-test-renderer'; 4 | import {fetch, Headers} from 'whatwg-fetch'; 5 | global.window.Headers = Headers; 6 | //global.window.fetch = fetch; 7 | 8 | describe.skip('Submission tests @submission', function() { 9 | beforeAll(function(done) { 10 | var form = { 11 | 'title': 'My Form', 12 | 'display': 'form', 13 | 'type': 'form', 14 | 'name': 'myform', 15 | 'path': 'myform', 16 | 'components': [ 17 | { 18 | 'input': true, 19 | 'tableView': true, 20 | 'inputType': 'text', 21 | 'inputMask': '', 22 | 'label': 'Name', 23 | 'key': 'name', 24 | 'placeholder': '', 25 | 'prefix': '', 26 | 'suffix': '', 27 | 'multiple': false, 28 | 'defaultValue': '', 29 | 'protected': false, 30 | 'unique': false, 31 | 'persistent': true, 32 | 'validate': { 33 | 'required': false, 34 | 'minLength': '', 35 | 'maxLength': '', 36 | 'pattern': '', 37 | 'custom': '', 38 | 'customPrivate': false 39 | }, 40 | 'type': 'textfield' 41 | }, 42 | { 43 | 'input': true, 44 | 'tableView': true, 45 | 'label': 'Message', 46 | 'key': 'message', 47 | 'placeholder': '', 48 | 'prefix': '', 49 | 'suffix': '', 50 | 'rows': 3, 51 | 'multiple': false, 52 | 'defaultValue': '', 53 | 'protected': false, 54 | 'persistent': true, 55 | 'validate': { 56 | 'required': false, 57 | 'minLength': '', 58 | 'maxLength': '', 59 | 'pattern': '', 60 | 'custom': '' 61 | }, 62 | 'type': 'textarea' 63 | }, 64 | { 65 | 'input': true, 66 | 'label': 'Submit', 67 | 'tableView': false, 68 | 'key': 'submit', 69 | 'size': 'md', 70 | 'leftIcon': '', 71 | 'rightIcon': '', 72 | 'block': false, 73 | 'action': 'submit', 74 | 'disableOnInvalid': true, 75 | 'theme': 'primary', 76 | 'type': 'button' 77 | } 78 | ] 79 | }; 80 | 81 | jest.mock('fetch') 82 | .withArgs('https://myproject.form.io/myform', jest.match.any).yields(null, {statusCode: 200, headers: {}}, Promise.resolve(form)); 83 | 84 | done(); 85 | }); 86 | 87 | it('Submits the form to form.io', function(done) { 88 | var onFormLoad = function(form) { 89 | done(); 90 | }; 91 | const element = renderer.create( 92 | 96 | ); 97 | }); 98 | 99 | afterAll(function(done) { 100 | done(); 101 | }); 102 | }); 103 | --------------------------------------------------------------------------------