├── .github ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md └── workflows │ └── npm-publish.yml ├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── demo └── src │ ├── ExampleFormContainer.js │ ├── ExampleFormSubmitOutside.js │ ├── components │ └── CustomText.js │ ├── css │ ├── simple-sidebar.css │ └── styles.css │ ├── index.js │ ├── schema │ ├── all-available-fields.js │ ├── async.js │ ├── autosave.js │ ├── basic.js │ ├── containers.js │ ├── custom-renderer.js │ ├── external-event-handlers.js │ ├── index.js │ └── validation.js │ └── source │ ├── autocomplete.js │ └── external-handlers.js ├── nwb.config.js ├── package-lock.json ├── package.json ├── src ├── Container │ ├── ButtonGroup.js │ ├── EditableGrid.js │ ├── Fieldset.js │ ├── Form.js │ ├── HtmlTag.js │ ├── Tabs.js │ └── index.js ├── Element.js ├── ErrorManager.js ├── Field │ ├── Autocomplete.js │ ├── Button.js │ ├── Checkbox.js │ ├── CodeEditor.js │ ├── ErrorMessage.js │ ├── FileUploader.js │ ├── InnerText.js │ ├── Label.js │ ├── Radio.js │ ├── ReactSelect.js │ ├── Switch.js │ ├── Text.js │ ├── Textarea.js │ ├── Wysiwyg.js │ └── index.js ├── Form.js ├── Renderer.js ├── Template │ ├── Default.js │ └── index.js ├── css │ └── autocomplete.css ├── index.js ├── registry.js ├── utils.js └── withFormConfig.js └── tests ├── .eslintrc ├── Container ├── ButtonGroup.test.js ├── Div.test.js ├── EditableGrid.test.js ├── Fieldset.test.js └── Tabs.test.js ├── Fields ├── Autocomplete.test.js ├── Checkbox.test.js ├── CodeEditor.test.js ├── FileUploader.test.js ├── Radio.test.js ├── ReactSelect.test.js ├── Switch.test.js ├── Textarea.test.js └── Wysiwyg.test.js └── test-utils.js /.github/CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to making participation in our 7 | project and our community a harassment-free experience for everyone, 8 | regardless of age, body size, disability, ethnicity, sex 9 | characteristics, gender identity and expression, level of experience, 10 | education, socio-economic status, nationality, personal appearance, 11 | race, religion, or sexual identity and orientation. 12 | 13 | ## Our Standards 14 | 15 | Examples of behavior that contributes to creating a positive environment 16 | include: 17 | 18 | - Using welcoming and inclusive language 19 | - Being respectful of differing viewpoints and experiences 20 | - Gracefully accepting constructive criticism 21 | - Focusing on what is best for the community 22 | - Showing empathy towards other community members 23 | 24 | Examples of unacceptable behavior by participants include: 25 | 26 | - The use of sexualized language or imagery and unwelcome sexual attention or advances 27 | - Trolling, insulting/derogatory comments, and personal or political attacks 28 | - Public or private harassment 29 | - Publishing others' private information, such as a physical or electronic address, without explicit permission 30 | - Other conduct which could reasonably be considered inappropriate in a professional setting 31 | 32 | ## Our Responsibilities 33 | 34 | Project maintainers are responsible for clarifying the standards of 35 | acceptable behavior and are expected to take appropriate and fair 36 | corrective action in response to any instances of unacceptable behavior. 37 | 38 | Project maintainers have the right and responsibility to remove, edit, 39 | or reject comments, commits, code, wiki edits, issues, and other 40 | contributions that are not aligned to this Code of Conduct, or to ban 41 | temporarily or permanently any contributor for other behaviors that they 42 | deem inappropriate, threatening, offensive, or harmful. 43 | 44 | ## Scope 45 | 46 | This Code of Conduct applies both within project spaces and in public 47 | spaces when an individual is representing the project or its community. 48 | Examples of representing a project or community include using an 49 | official project e-mail address, posting via an official social media 50 | account, or acting as an appointed representative at an online or 51 | offline event. Representation of a project may be further defined and 52 | clarified by project maintainers. 53 | 54 | ## Enforcement 55 | 56 | Instances of abusive, harassing, or otherwise unacceptable behavior may 57 | be reported by contacting the project team at info@flipbyte.com. All 58 | complaints will be reviewed and investigated and will result in a 59 | response that is deemed necessary and appropriate to the circumstances. 60 | The project team is obligated to maintain confidentiality with regard to 61 | the reporter of an incident. Further details of specific enforcement 62 | policies may be posted separately. 63 | 64 | Project maintainers who do not follow or enforce the Code of Conduct in 65 | good faith may face temporary or permanent repercussions as determined 66 | by other members of the project's leadership. 67 | 68 | ## Attribution 69 | 70 | This Code of Conduct is adapted from the [Contributor 71 | Covenant][homepage], version 1.4, available at 72 | 73 | 74 | For answers to common questions about this code of conduct, see 75 | 76 | 77 | [homepage]: https://www.contributor-covenant.org 78 | -------------------------------------------------------------------------------- /.github/CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | ## Prerequisites 2 | 3 | [Node.js](http://nodejs.org/) >= 6 must be installed. 4 | 5 | ## Installation 6 | 7 | - Running `npm install` in the component's root directory will install everything you need for development. 8 | 9 | ## Demo Development Server 10 | 11 | - `npm start` will run a development server with the component's demo app at with hot module reloading. 12 | 13 | ## Running Tests 14 | 15 | - `npm test` will run the tests once. 16 | - `npm run test:coverage` will run the tests and produce a coverage report in `coverage/`. 17 | - `npm run test:watch` will run the tests on every change. 18 | 19 | ## Building 20 | 21 | - `npm run build` will build the component for publishing to npm and also bundle the demo app. 22 | - `npm run clean` will delete built resources. 23 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: bug 6 | assignees: '' 7 | --- 8 | 9 | ### Describe the bug 10 | 11 | 12 | 13 | ### To Reproduce 14 | 15 | 16 | 17 | ### Expected behavior 18 | 19 | 20 | 21 | ### Screenshots 22 | 23 | 24 | 25 | ### Additional context 26 | 27 | 28 | 29 | ### Your environment 30 | 31 | | Software | Version(s) | 32 | | ---------------- | ---------- | 33 | | formik-json | | 34 | | React | | 35 | | Browser | | 36 | | Operating System | | 37 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: enhancement 6 | assignees: '' 7 | --- 8 | 9 | ### Is your feature request related to a problem? Please describe. 10 | 11 | 13 | 14 | ### Describe the solution you'd like 15 | 16 | 17 | 18 | ### Describe alternatives you've considered 19 | 20 | 22 | 23 | ### Additional context 24 | 25 | 26 | -------------------------------------------------------------------------------- /.github/workflows/npm-publish.yml: -------------------------------------------------------------------------------- 1 | # This is a basic workflow to help you get started with Actions 2 | 3 | name: CD 4 | 5 | # Controls when the workflow will run 6 | on: 7 | # Triggers the workflow on push or pull request events but only for the master branch 8 | push: 9 | branches: [ master ] 10 | 11 | # A workflow run is made up of one or more jobs that can run sequentially or in parallel 12 | jobs: 13 | publish: 14 | runs-on: ubuntu-latest 15 | steps: 16 | - uses: actions/checkout@v1 17 | - uses: actions/setup-node@v1 18 | with: 19 | node-version: 10 20 | - run: npm install 21 | - uses: JS-DevTools/npm-publish@v1 22 | with: 23 | token: ${{ secrets.NPM_TOKEN }} 24 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /coverage 2 | /demo/dist 3 | /es 4 | /lib 5 | /node_modules 6 | /umd 7 | npm-debug.log* 8 | .DS_Store 9 | .idea 10 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false 2 | 3 | language: node_js 4 | node_js: 5 | - 8 6 | 7 | before_install: 8 | - npm install codecov.io coveralls 9 | 10 | after_success: 11 | - cat ./coverage/lcov.info | ./node_modules/codecov.io/bin/codecov.io.js 12 | - cat ./coverage/lcov.info | ./node_modules/coveralls/bin/coveralls.js 13 | 14 | branches: 15 | only: 16 | - master 17 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Flipbyte Solutions B.V. 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # formik-json 2 | 3 | [Developed by Flipbyte](https://www.flipbyte.com/) 4 | 5 | [![Build Status][build-badge]][build] 6 | [![npm package][npm-badge]][npm] 7 | [![Coverage Status][coveralls-badge]][coveralls] 8 | [![license][license-badge]][license] 9 | [![Codacy Badge][codacy-badge]][codacy] 10 | 11 | formik-json is a wrapper for Formik to easily create forms using JSON / 12 | Javascript Object for defining the elements. 13 | 14 | ## Examples 15 | 16 | - [Demo](https://flipbyte.github.io/formik-json-schema) 17 | 18 | ## Pre-requisites 19 | 20 | The component depends on a few third-party plugins for adding WYSIWYG, 21 | select, auto-complete, validation etc. Few of them already come packaged 22 | with the extension and few need to be installed separately in your 23 | project. 24 | 25 | ## Installation 26 | 27 | You can install Formik-json using following steps. 28 | 29 | ```js 30 | $ npm i @flipbyte/formik-json 31 | ``` 32 | 33 | ### Quick Start Guide 34 | 35 | Once you've finished installation, you can add the form to any of your 36 | components as follows: 37 | 38 | #### Import the Form component 39 | 40 | ```js 41 | import { Form } from '@flipbyte/formik-json'; 42 | ``` 43 | 44 | ##### Prepare your form object 45 | 46 | ```js 47 | { 48 | "id": "my-new-form", 49 | "label": "My New Form", 50 | "type": "container", 51 | "renderer": "form", 52 | "elements": { 53 | "save": { 54 | "type": "field", 55 | "renderer": "button", 56 | "name": "save", 57 | "label": "Save", 58 | "htmlClass": "btn-success", 59 | "buttonType": "submit" 60 | }, 61 | "main": { 62 | "type": "container", 63 | "renderer": "div", 64 | "htmlClass": "row", 65 | "elements": { 66 | "title": { 67 | "name": "attributes.0.title", 68 | "label": "Title", 69 | "type": "field", 70 | "renderer": "text", 71 | "fieldType": "text" 72 | }, 73 | .... 74 | } 75 | }, 76 | .... 77 | } 78 | } 79 | ``` 80 | 81 | ##### Add the component anywhere you want 82 | 83 | ```js 84 |
87 | ``` 88 | 89 | ### Form Component 90 | 91 | * * * 92 | 93 | Form component requires the following properties: 94 | 95 | | Key | Description | 96 | | ----------------- | --------------------------------------------------------------------------------------------------- | 97 | | schema | your schema object | 98 | | onUpdate | callback when the values are updated | 99 | | initialValues | check [](https://jaredpalmer.com/formik/docs/api/formik) | 100 | | Formik properties | You can add any of the [](https://jaredpalmer.com/formik/docs/api/formik) component props | 101 | 102 | ### Schema object 103 | 104 | * * * 105 | 106 | Schema object contains elements which can be one of 2 types: either 107 | "container" or "field" Each type needs a renderer to render the specific 108 | component. The "container" has an "elements" key within which you can 109 | define either new containers or fields. 110 | 111 | schema object that has the following keys (all required): 112 | 113 | | Key | Description | 114 | | -------- | --------------------------------------------------------------------------------------------------------------- | 115 | | id | the ID for the form | 116 | | label | the title for the form | 117 | | type | "container" | 118 | | renderer | "form" (you can use other renderers but if you want the form to have a `` tag use the "form" renderer.) | 119 | | elements | is an object used to define the elements within the container | 120 | 121 | Note: The schema object can only have one container. You can have 122 | multiple containers and fields inside the elements object of the main 123 | schema object. 124 | 125 | "elements" is an object with key-value pair where value is another 126 | object. The value object can either be a of type "container" or "field". 127 | 128 | Each container or field requires a renderer which can be set using 129 | "renderer": "{your_renderer}". You can define you own renderers for both 130 | containers and keys or use the ones that come with the module. 131 | 132 | ### Following are the properties for each type of container 133 | 134 | #### Common container properties 135 | 136 | | Not applicable | Field | Property | Description | 137 | | --------------------------- | ------------------- | -------- | ------------------------------------------------------------------------------------------------------------ | 138 | | none | type | String | "container" | 139 | | button-group | name | String | is used to prepend parent container's name to the children fields when "prefixNameToElement" is set to true. | 140 | | editable-grid, tabs | elements | {} | is an object that can hold one or more fields or containers within it. | 141 | | editable-grid, button-group | prefixNameToElement | Bool | | 142 | | | showWhen | String | Check [when-condition](https://github.com/flipbyte/when-condition) | 143 | | | comment | String | comment / description for the container | 144 | | | commentClass | String | html class for the comment element | 145 | 146 | #### Container specific properties 147 | 148 | | Container | Field | Property | Description | 149 | | ------------- | ---------------------- | --------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------ | 150 | | editable-grid | renderer | String | editable-grid | 151 | | | fields | {} | An object with one or more field definitions in a key-value pair | 152 | | | buttons | `{"add": "Add", "remove": "X", "duplicate": "Duplicate"}` | has 3 properties, all optional. These can be either function that returns the button or string which is the label for a button | 153 | | | isObject | Bool | whether the grid displays an object. If set to true, buttons (add, remove and duplicate) will be disabled. | 154 | | | isSortable | Bool | whether the grid rows can be dragged and sorted | 155 | | | tableContainerClass | String | htmlClass for the div wrapping the editable-grid | 156 | | | tableClass | String | htmlClass for the main editable grid | 157 | | div | renderer | String | div | 158 | | | name | String | is used to prepend parent container's name to the children fields when "prefixNameToElement" is set to true. | 159 | | | htmlClass | String | htmlClass for the div element | 160 | | html-tag | renderer | String | html-tag | 161 | | | name | String | is used to prepend parent container's name to the children fields when "prefixNameToElement" is set to true. | 162 | | | as | String | html tag to be used (Default: 'div') | 163 | | | htmlClass | String | htmlClass for the html-tag element | 164 | | fieldset | renderer | String | fieldset | 165 | | | name | String | is used to prepend parent container's name to the children fields when "prefixNameToElement" is set to true. | 166 | | | title | String | label for the fieldset | 167 | | | cardClass | String | htmlClass for the main wrapping container | 168 | | | cardHeaderClass | String | htmlClass for the header of the wrapping container | 169 | | | cardHeaderActionsClass | String | htmlClass for the container holding the disclose buttons in the header of the container | 170 | | | cardBodyClass | String | htmlClass for the body of the container | 171 | | form | renderer | String | form | 172 | | | name | String | is used to prepend parent container's name to the children fields when "prefixNameToElement" is set to true. | 173 | | | htmlClass | String | any character | 174 | | tabs | renderer | String | tabs | 175 | | | name | String | is used to prepend parent container's name to the children fields when "prefixNameToElement" is set to true. | 176 | | | tabs | {} | Object | 177 | | | cardClass | String | same as fieldset | 178 | | | rowClass | String | htmlClass for the row div | 179 | | | tabListClass | String | htmlClass for tab list | 180 | | | tabListItemClass | String | htmlClass for tab list item | 181 | | | tabContentClass | String | htmlClass for tab content container | 182 | | | tabColumnClass | String | htmlClass for tabs container | 183 | | | contentColumnClass | String | htmlClass for wrapping the tab content container | 184 | | | tabActiveClass | String | htmlClass for active tabs | 185 | | | tabPaneClass | String | htmlClass for single tab pane | 186 | | button-group | renderer | String | button-group | 187 | | | elements | {} | the elements can only be of type: "field" with renderer: "button". | 188 | 189 | ### Following are the properties for each type of field 190 | 191 | #### Common field properties 192 | 193 | | Field | Type | Property | Description | | 194 | | :---- | :------------- | :-------------: | :---------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | 195 | | | name | String | html field name attribute | | 196 | | | label | String | the label for the field | | 197 | | | type | String | "field" | | 198 | | | labelClass | String | html class for the label html element | | 199 | | | formGroupClass | String | html class for the div that wraps the form field | | 200 | | | validation | String | Check [yup-schema](https://github.com/flipbyte/yup-schema) | | 201 | | | showWhen | String | Check [when-condition](https://github.com/flipbyte/when-condition) | | 202 | | | enabledWhen | String | Check [when-condition](https://github.com/flipbyte/when-condition) | | 203 | | | fieldClass | String | html class for the main html/3-rd party form field | | 204 | | | comment | String | comment / description that goes below the field | | 205 | | | commentAs | String | define the HTML tag to be used for wrapping the comment. (Default: ) | | 206 | | | commentClass | String | html class for the comment element | | 207 | | | template | React Component | String | define your custom template for the field (check `src/Template/Default.js`) or set the template in the template registry using `registerTemplate` and pass the string key here | 208 | | | errorAs | String | define the HTML tag to be used for wrapping the error. (Default:
) | | 209 | | | errorClass | String | html class for the error element | | 210 | 211 | #### Field specific properties 212 | 213 | | Field | Type | Property | Description | 214 | | :------------ | :--------------- | :------: | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | 215 | | checkbox | renderer | String | checkbox | 216 | | | name | String | html field name attribute | 217 | | | label | String | the label for the field | 218 | | | type | String | "field" | 219 | | | attributes | {} | is an object that can hold other html field related attributes (if any). Only ones that are not defined using any other key will be used. For example: name already has it's own key and hence "name" key inside the attributes object will do nothing. | 220 | | | options | Array | Array of objects with keys "value" and "label" | 221 | | code-editor | renderer | String | code-editor | 222 | | | name | String | html field name attribute | 223 | | | label | String | the label for the field | 224 | | | options | {} | | 225 | | | defaultValue | String | default field value (untested) | 226 | | | attributes | {} | is an object that can hold other html field related attributes (if any). Only ones that are not defined using any other key will be used. For example: name already has it's own key and hence "name" key inside the attributes object will do nothing. | 227 | | radio | renderer | String | radio | 228 | | | name | String | html field name attribute | 229 | | | label | String | the label for the field | 230 | | | options | {} | | 231 | | | attributes | {} | is an object that can hold other html field related attributes (if any). Only ones that are not defined using any other key will be used. For example: name already has it's own key and hence "name" key inside the attributes object will do nothing. | 232 | | react-select | renderer | String | react-select | 233 | | | name | String | html field name attribute | 234 | | | label | String | the label for the field | 235 | | | options | {} | array of objects with value and label keys. Example: `[{"label": "Item 1", "value": "value-1"}]` | 236 | | | defaultValue | String | default field value (untested) | 237 | | | isMulti | Bool | whether it's a mult-select | 238 | | | isClearable | Bool | displays a clear select button on the select which will clear the selected options | 239 | | | isDisabled | Bool | disables the select when set to true | 240 | | | noOptionsMessage | Function | refer [ReactSelect Props](https://react-select.com/props) | 241 | | switch | renderer | String | switch | 242 | | text | renderer | String | text | 243 | | | attributes | {} | is an object that can hold other html field related attributes (if any). Only ones that are not defined using any other key will be used. For example: name already has it's own key and hence "name" key inside the attributes object will do nothing. | 244 | | | fieldType | String | HTML input type. The value that goes into | 245 | | | defaultValue | String | default field value (untested) | 246 | | | icon | String | fontawesome icon class | 247 | | | inputGroupClass | String | html class for the div that wraps an icon and an input element together | 248 | | textarea | renderer | String | textarea | 249 | | | attributes | {} | is an object that can hold other html field related attributes (if any). Only ones that are not defined using any other key will be used. For example: name already has it's own key and hence "name" key inside the attributes object will do nothing. | 250 | | | rows | Number | Number of rows that the text-area container should show by default | 251 | | wysiwyg | renderer | String | wysiwyg | 252 | | | attributes | {} | is an object that can hold other html field related attributes (if any). Only ones that are not defined using any other key will be used. For example: name already has it's own key and hence "name" key inside the attributes object will do nothing. | 253 | | | options | {} | [React-quill wysiwyg options](https://github.com/zenoamaro/react-quill) | 254 | | | rows | Number | Number of rows that the wysiwyg container should show by default | 255 | | | textareaClass | String | the class for the textarea that will show the raw html for the content entered in the wysiwyg | 256 | | autocomplete | renderer | String | autocomplete | 257 | | | attributes | {} | is an object that can hold other html field related attributes (if any). Only ones that are not defined using any other key will be used. For example: name already has it's own key and hence "name" key inside the attributes object will do nothing. | 258 | | | defaultValue | String | default field value (untested) | 259 | | | options | {} | Options available in [react-autosuggest plugin](https://github.com/moroshko/react-autosuggest) | 260 | | file-uploader | renderer | String | fileuploader | 261 | | | options | {} | Options available in [react-dropzone plugin](https://react-dropzone.js.org/) | 262 | | | formGroupClass | String | html class for the div that wraps the form field | 263 | | inner-text | renderer | String | inner-text | 264 | | | as | String | HTML tag to use for the inner-text field | 265 | | | htmlClass | String | HTML class for the tag used | 266 | | | defaultValue | String | Either used as a static value for the HTML element or as a placeholder when is not defined | 267 | | button | renderer | String | button | 268 | | | content | String | button inner html | 269 | 270 | ## Thank You 271 | 272 | If you have suggestions, comments or ideas to develop more such 273 | solutions, you can write to us at 274 | [Flipbyte.com](https://www.flipbyte.com/#ht-top-footer). PRs are welcome. 275 | 276 | ## License 277 | 278 | The MIT License (MIT) 279 | 280 | [build-badge]: https://travis-ci.org/flipbyte/formik-json.svg?branch=master 281 | 282 | [build]: https://travis-ci.org/flipbyte/formik-json 283 | 284 | [npm-badge]: https://img.shields.io/npm/v/@flipbyte/formik-json.svg 285 | 286 | [npm]: https://www.npmjs.com/package/@flipbyte/formik-json 287 | 288 | [coveralls-badge]: https://coveralls.io/repos/github/flipbyte/formik-json-schema/badge.svg?branch=master 289 | 290 | [coveralls]: https://coveralls.io/github/flipbyte/formik-json-schema?branch=master 291 | 292 | [license-badge]: https://badgen.now.sh/badge/license/MIT 293 | 294 | [license]: ./LICENSE 295 | 296 | [codacy-badge]: https://api.codacy.com/project/badge/Grade/18e71277b7e94ad9aca885b5ba3d890c 297 | 298 | [codacy]: https://www.codacy.com/app/easeq/formik-json?utm_source=github.com&utm_medium=referral&utm_content=flipbyte/formik-json&utm_campaign=Badge_Grade 299 | -------------------------------------------------------------------------------- /demo/src/ExampleFormContainer.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { render } from 'react-dom'; 3 | import { Form } from '../../src'; 4 | 5 | const ExampleFormContainer = ({ title, className, id, formProps }) => 6 |
7 |
{ title }
8 | 9 |
10 | 11 | export default ExampleFormContainer; 12 | -------------------------------------------------------------------------------- /demo/src/ExampleFormSubmitOutside.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { Form } from '../../src'; 3 | 4 | import basicForm from './schema/basic' 5 | 6 | const ref = React.createRef() 7 | const ExampleFormSubmitOutside = () => { 8 | 9 | return ( 10 |
11 |
Outside submit
12 | 13 | 14 | 17 |
18 | ) 19 | } 20 | 21 | export default ExampleFormSubmitOutside; 22 | -------------------------------------------------------------------------------- /demo/src/components/CustomText.js: -------------------------------------------------------------------------------- 1 | import _ from 'lodash'; 2 | import React from 'react'; 3 | import Label from '../../../src/Field/Label'; 4 | import ErrorMessage from '../../../src/Field/ErrorMessage'; 5 | import { changeHandler, joinNames } from '../../../src/utils'; 6 | 7 | const CustomText = ({ config, formik, value = '', error }) => { 8 | const { 9 | name, 10 | label, 11 | type, 12 | attributes, 13 | fieldType, 14 | defaultValue, 15 | icon, 16 | labelClass = '', 17 | fieldClass = 'form-control', 18 | formGroupClass = 'form-group' 19 | } = config; 20 | 21 | const { setFieldValue, handleChange, handleBlur } = formik; 22 | const isInputGroup = icon ? true : false; 23 | const currentValue = value; 24 | return ( 25 | 26 | 35 | { currentValue &&
36 | Your unique id for { currentValue } is { _.uniqueId() } 37 |
} 38 |
39 | ); 40 | } 41 | 42 | export default CustomText; 43 | -------------------------------------------------------------------------------- /demo/src/css/simple-sidebar.css: -------------------------------------------------------------------------------- 1 | /*! 2 | * Start Bootstrap - Simple Sidebar (https://startbootstrap.com/template-overviews/simple-sidebar) 3 | * Copyright 2013-2019 Start Bootstrap 4 | * Licensed under MIT (https://github.com/BlackrockDigital/startbootstrap-simple-sidebar/blob/master/LICENSE) 5 | */ 6 | body { 7 | overflow-x: hidden; 8 | } 9 | 10 | #sidebar-wrapper { 11 | min-height: 100vh; 12 | margin-left: -15rem; 13 | -webkit-transition: margin .25s ease-out; 14 | -moz-transition: margin .25s ease-out; 15 | -o-transition: margin .25s ease-out; 16 | transition: margin .25s ease-out; 17 | height: 100%; 18 | position: fixed; 19 | z-index: 1; 20 | top: 0; 21 | left: 0; 22 | overflow-x: hidden; 23 | } 24 | 25 | #sidebar-wrapper .sidebar-heading { 26 | padding: 0.875rem 1.25rem; 27 | font-size: 1.2rem; 28 | } 29 | 30 | #sidebar-wrapper .list-group { 31 | width: 15rem; 32 | } 33 | 34 | #page-content-wrapper { 35 | min-width: 100vw; 36 | margin-left: 0; 37 | } 38 | 39 | .content { 40 | position: fixed; 41 | overflow: scroll; 42 | height: 100%; 43 | top: 60px; 44 | } 45 | 46 | #wrapper.toggled #sidebar-wrapper { 47 | margin-left: 0; 48 | } 49 | 50 | @media (min-width: 768px) { 51 | #sidebar-wrapper { 52 | margin-left: 0; 53 | } 54 | 55 | #page-content-wrapper { 56 | min-width: 0; 57 | width: 100%; 58 | margin-left: 15rem; 59 | } 60 | 61 | .content { 62 | position: relative; 63 | height: auto; 64 | overflow: none; 65 | top: auto; 66 | } 67 | 68 | #wrapper.toggled #sidebar-wrapper { 69 | margin-left: -15rem; 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /demo/src/css/styles.css: -------------------------------------------------------------------------------- 1 | .is-invalid { 2 | border: 1px solid red 3 | } 4 | 5 | .invalid-feedback { 6 | display: block; 7 | } 8 | 9 | .form-container { 10 | margin-bottom: 50px; 11 | } 12 | 13 | div.scrollmenu { 14 | background-color: #f8f9fa; 15 | overflow: auto; 16 | white-space: nowrap; 17 | } 18 | 19 | div.scrollmenu a { 20 | display: inline-block; 21 | color: #000; 22 | text-align: center; 23 | padding: 14px; 24 | text-decoration: none; 25 | } 26 | 27 | div.scrollmenu a:hover { 28 | background-color: #ccc; 29 | } 30 | 31 | .sticky { 32 | position: fixed; 33 | top: 0; 34 | width: 100%; 35 | z-index: 11 36 | } 37 | 38 | @media (min-width: 768px) { 39 | div.scrollmenu { 40 | display: none; 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /demo/src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { render } from 'react-dom'; 3 | import forms from './schema'; 4 | import ExampleFormContainer from './ExampleFormContainer'; 5 | import ExampleFormSubmitOutside from './ExampleFormSubmitOutside'; 6 | 7 | import '../../node_modules/bootstrap/dist/css/bootstrap.css'; 8 | import '../../node_modules/@fortawesome/fontawesome-free/css/all.css'; 9 | import '../../node_modules/codemirror/lib/codemirror.css'; 10 | import '../../node_modules/react-quill/dist/quill.snow.css'; 11 | import '../../src/css/autocomplete.css'; 12 | import './css/simple-sidebar.css'; 13 | import './css/styles.css'; 14 | require('codemirror/mode/xml/xml'); 15 | 16 | const Demo = () => ( 17 |
18 | 36 |
37 |
38 | { forms.map(({ id, title }, index) => { title } ) } 39 |
40 |
41 | { forms.map((form, index) => )} 42 | 43 |
44 |
45 |
46 | ); 47 | 48 | render(, document.querySelector('#demo')) 49 | -------------------------------------------------------------------------------- /demo/src/schema/all-available-fields.js: -------------------------------------------------------------------------------- 1 | import { 2 | onSuggestionsFetchRequested, 3 | onSuggestionsClearRequested, 4 | getSuggestionValue, 5 | renderSuggestion 6 | } from '../source/autocomplete'; 7 | 8 | import { 9 | save 10 | } from '../source/external-handlers'; 11 | 12 | export default { 13 | onSubmit: save.bind(this), 14 | initialValues: { 15 | radio: 1 16 | }, 17 | schema: { 18 | id: "all-available-fields", 19 | label: "All available fields", 20 | type: "container", 21 | renderer: "form", 22 | elements: { 23 | text: { 24 | name: "text", 25 | label: "Text", 26 | type: "field", 27 | renderer: "text", 28 | fieldType: "text", 29 | comment: "This is a field comment. You can add your text here." 30 | }, 31 | inputGroup: { 32 | type: 'container', 33 | renderer: 'div', 34 | htmlClass: 'input-group', 35 | elements: { 36 | prepend: { 37 | type: 'container', 38 | renderer: 'div', 39 | htmlClass: 'input-group-prepend', 40 | elements: { 41 | innerText: { 42 | type: 'field', 43 | renderer: 'inner-text', 44 | name: 'prependInnerText', 45 | htmlClass: 'input-group-text', 46 | defaultValue: '@', 47 | wrapAs: null 48 | } 49 | } 50 | }, 51 | text: { 52 | name: "text", 53 | type: "field", 54 | renderer: "text", 55 | fieldType: "text", 56 | wrapAs: null, 57 | comment: 'This is a field comment. You can add your text here', 58 | commentAs: 'small', 59 | commentClass: 'd-block text-muted order-last w-100', 60 | validation: [['string'], ['required'], ['min', 100]] 61 | }, 62 | append: { 63 | type: 'container', 64 | renderer: 'div', 65 | htmlClass: 'input-group-append', 66 | elements: { 67 | innerText: { 68 | type: 'field', 69 | renderer: 'inner-text', 70 | name: 'appendInnerText', 71 | htmlClass: 'input-group-text', 72 | defaultValue: 'pixels', 73 | wrapAs: null 74 | } 75 | } 76 | } 77 | } 78 | }, 79 | innerText: { 80 | type: 'field', 81 | renderer: 'inner-text', 82 | name: 'innerText', 83 | as: 'p', 84 | htmlClass: 'text-muted d-block mb-3 mt-1', 85 | defaultValue: 'This is some raw html text content: Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry\'s standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book. It has survived not only five centuries, but also the leap into electronic typesetting, remaining essentially unchanged. It was popularised in the 1960s with the release of Letraset sheets containing Lorem Ipsum passages, and more recently with desktop publishing software like Aldus PageMaker including versions of Lorem Ipsum' 86 | }, 87 | autocomplete: { 88 | name: "autocomplete", 89 | label: "Autocomplete", 90 | type: "field", 91 | renderer: "autocomplete", 92 | position: 10, 93 | initialSuggestions: [], 94 | options: { 95 | onSuggestionsFetchRequested: onSuggestionsFetchRequested, 96 | onSuggestionsClearRequested: onSuggestionsClearRequested, 97 | getSuggestionValue: getSuggestionValue, 98 | renderSuggestion: renderSuggestion, 99 | inputProps: {} 100 | } 101 | }, 102 | reactSelect: { 103 | type: "field", 104 | renderer: "react-select", 105 | name: "react-select", 106 | label: "React Select", 107 | isCreatable: false, 108 | comment: "Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry's standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book. It has survived not only five centuries, but also the leap into electronic typesetting, remaining essentially unchanged. It was popularised in the 1960s with the release of Letraset sheets containing Lorem Ipsum passages, and more recently with desktop publishing software like Aldus PageMaker including versions of Lorem Ipsum.", 109 | options: [{ 110 | value: 0, 111 | label: "No" 112 | }, { 113 | value: 1, 114 | label: "Yes" 115 | }], 116 | formGroupClass: "form-group mb-4" 117 | }, 118 | reactSelectMulti: { 119 | type: "field", 120 | renderer: "react-select", 121 | name: "react-select-multi", 122 | label: "React Select Multi", 123 | isMulti: true, 124 | isCreatable: true, 125 | options: [{ 126 | value: 'chocolate', 127 | label: 'Chocolate' 128 | }, { 129 | value: 'strawberry', 130 | label: 'Strawberry' 131 | }, { 132 | value: 'vanilla', 133 | label: 'Vanilla' 134 | }], 135 | formGroupClass: "form-group mb-4", 136 | validation: [['array'], ['of', [['string']]]] 137 | }, 138 | reactSelectCreatableMulti: { 139 | type: "field", 140 | renderer: "react-select", 141 | name: "react-select-creatable-multi", 142 | label: "React Select Creatable Multi", 143 | isMulti: true, 144 | isCreatable: true, 145 | options: [], 146 | menuIsOpen: false, 147 | defaultValue: [], 148 | components: { 149 | DropdownIndicator: null 150 | }, 151 | formGroupClass: "form-group mb-4", 152 | validation: [['array'], ['of', [['string']]]] 153 | }, 154 | textarea: { 155 | name: "description", 156 | label: "Description", 157 | type: "field", 158 | renderer: "textarea", 159 | }, 160 | singleCheckbox: { 161 | name: "singleCheckbox", 162 | label: "Single option", 163 | type: "field", 164 | renderer: "checkbox", 165 | labelClass: "mr-2", 166 | fieldClass: "d-inline", 167 | options: [{ 168 | value: 'checkbox-1', 169 | label: 'Checkbox 1' 170 | }], 171 | validation: [ 172 | ['bool'], 173 | ['test', 'singleCheckbox.0', 'You have to select this value', value => value === true], 174 | ['required', 'You have to select this value'] 175 | ] 176 | }, 177 | multiCheckbox: { 178 | name: "multiCheckbox", 179 | label: "Multiple options", 180 | type: "field", 181 | renderer: "checkbox", 182 | options: [{ 183 | value: 'checkbox-1', 184 | label: 'Checkbox 1' 185 | }, { 186 | value: 'checkbox-2', 187 | label: 'Checkbox 2' 188 | }, { 189 | value: 'checkbox-3', 190 | label: 'Checkbox 3' 191 | }, { 192 | value: 'checkbox-4', 193 | label: 'Checkbox 4' 194 | }] 195 | }, 196 | radio: { 197 | name: "radio", 198 | label: "Radio", 199 | type: "field", 200 | renderer: "radio", 201 | options: [{ 202 | value: 1, 203 | title: "One" 204 | }, { 205 | value: 2, 206 | title: "Two" 207 | }, { 208 | value: 3, 209 | title: "Three" 210 | }] 211 | }, 212 | switch: { 213 | name: "switch", 214 | label: "Switch", 215 | type: "field", 216 | renderer: "switch" 217 | }, 218 | wysiwyg: { 219 | name: "wysiwyg", 220 | label: "Wysiwyg", 221 | type: "field", 222 | renderer: "wysiwyg", 223 | htmlClass: "flutter-wysiwyg-size-3", 224 | attributes: { 225 | style: { 226 | height: 200 227 | } 228 | }, 229 | validation: [['string'], ['required'], ['min', 100]] 230 | }, 231 | codeeditor: { 232 | name: "codeeditor", 233 | label: "Code Editor", 234 | type: "field", 235 | renderer: "code-editor", 236 | formGroupClass: "mt-5", 237 | fieldClass: "border", 238 | options: { 239 | mode: "xml", 240 | lineNumbers: true 241 | }, 242 | validation: [['string'], ['required'], ['min', 100]] 243 | }, 244 | fileUploader: { 245 | name: "fileUploader", 246 | label: "File Uploader", 247 | type: "field", 248 | renderer: "file-uploader", 249 | options: { 250 | accept: ['image/*', 'text/*'], 251 | multiple: true, 252 | onDrop: (formik, config, acceptedFiles) => { 253 | console.log(formik, config, acceptedFiles); 254 | } 255 | } 256 | }, 257 | buttonsGroup: { 258 | type: "container", 259 | renderer: "button-group", 260 | buttonsContainerClass: "buttons-container mt-2", 261 | elements: { 262 | save: { 263 | type: "field", 264 | renderer: "button", 265 | name: "save", 266 | content: "Save", 267 | fieldClass: "btn-success float-right", 268 | buttonType: "submit", 269 | } 270 | } 271 | } 272 | } 273 | } 274 | } 275 | -------------------------------------------------------------------------------- /demo/src/schema/async.js: -------------------------------------------------------------------------------- 1 | import { 2 | onSuggestionsFetchRequested, 3 | onSuggestionsClearRequested, 4 | getSuggestionValue, 5 | renderSuggestion 6 | } from '../source/autocomplete'; 7 | 8 | import { asyncFill } from '../source/external-handlers'; 9 | 10 | export default { 11 | initialValues: { 12 | valueWithoutAField: 1 13 | }, 14 | schema: { 15 | id: "async", 16 | label: "Async", 17 | type: "container", 18 | renderer: "form", 19 | elements: { 20 | asyncFill: { 21 | type: "field", 22 | renderer: "button", 23 | name: "button", 24 | content: "Async fill", 25 | fieldClass: "btn-danger float-right", 26 | buttonType: "button", 27 | onClick: asyncFill 28 | }, 29 | text: { 30 | name: "text1", 31 | label: "Text", 32 | type: "field", 33 | renderer: "text", 34 | fieldType: "text" 35 | }, 36 | autocomplete: { 37 | name: "autocomplete", 38 | label: "Autocomplete", 39 | type: "field", 40 | renderer: "autocomplete", 41 | position: 10, 42 | initialSuggestions: [], 43 | options: { 44 | onSuggestionsFetchRequested: onSuggestionsFetchRequested, 45 | onSuggestionsClearRequested: onSuggestionsClearRequested, 46 | getSuggestionValue: getSuggestionValue, 47 | renderSuggestion: renderSuggestion, 48 | inputProps: {} 49 | } 50 | } 51 | } 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /demo/src/schema/autosave.js: -------------------------------------------------------------------------------- 1 | import { autosave } from '../source/external-handlers'; 2 | 3 | export default { 4 | onUpdate: autosave.bind(this), 5 | initialValues: {}, 6 | schema: { 7 | id: "autosave", 8 | label: "Autosave", 9 | type: "container", 10 | renderer: "form", 11 | elements: { 12 | title: { 13 | name: "title", 14 | label: "Title", 15 | type: "field", 16 | renderer: "text", 17 | fieldType: "text", 18 | validation: [ 19 | ['string'], 20 | ['required'], 21 | ['min', 3] 22 | ] 23 | }, 24 | text: { 25 | name: "autosave_text", 26 | label: "Autosave text", 27 | type: "field", 28 | renderer: "wysiwyg", 29 | htmlClass: "flutter-wysiwyg-size-3", 30 | attributes: { 31 | style: { 32 | height: 200 33 | } 34 | }, 35 | validation: [ 36 | ['string'], 37 | ['required'], 38 | ['min', 3] 39 | ] 40 | } 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /demo/src/schema/basic.js: -------------------------------------------------------------------------------- 1 | import { 2 | save 3 | } from '../source/external-handlers'; 4 | 5 | export default { 6 | onSubmit: save.bind(this), 7 | initialValues: {}, 8 | schema: { 9 | id: "basic", 10 | label: "Basic", 11 | type: "container", 12 | renderer: "form", 13 | configSource: (formik, config) => { 14 | return new Promise((resolve, reject) => { 15 | fetch('http://google.com') // Call the fetch function passing the url of the API as a parameter 16 | .then(function(data) { 17 | // Your code for handling the data you get from the API 18 | console.log(data); 19 | resolve(); 20 | }) 21 | .catch(function() { 22 | // This is where you run code if the server returns any errors 23 | }) 24 | }) 25 | }, 26 | elements: { 27 | name: { 28 | name: "name", 29 | label: "Name", 30 | type: "field", 31 | renderer: "text", 32 | fieldType: "text", 33 | validation: [ 34 | ['string'], 35 | ['required'], 36 | ['min', 3] 37 | ] 38 | }, 39 | email: { 40 | name: "email", 41 | label: "Email", 42 | type: "field", 43 | renderer: "text", 44 | fieldType: "text", 45 | validation: [ 46 | ['string'], 47 | ['required'], 48 | ['email'] 49 | ] 50 | }, 51 | telephone: { 52 | name: "telephone", 53 | label: "Telephone (enabled if email is empty)", 54 | type: "field", 55 | renderer: "text", 56 | fieldType: "telephone", 57 | enabledWhen: ['or', ['isOfType', 'email', 'undefined'], ['is', 'email', '']], 58 | }, 59 | message: { 60 | name: "message", 61 | label: "Message (visible if name is not empty)", 62 | type: "field", 63 | renderer: "textarea", 64 | validation: [ 65 | ['string'], 66 | ['required'] 67 | ], 68 | showWhen: ['not', ['or', ['is', 'name', ''], ['isOfType', 'name', 'undefined']]], 69 | }, 70 | save: { 71 | type: "field", 72 | renderer: "button", 73 | content: "Save", 74 | fieldClass: "btn-success float-right", 75 | buttonType: "submit" 76 | } 77 | } 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /demo/src/schema/custom-renderer.js: -------------------------------------------------------------------------------- 1 | import CustomText from '../components/CustomText'; 2 | 3 | export default { 4 | initialValues: {}, 5 | schema: { 6 | id: "custom-renderer", 7 | label: "Custom Renderer", 8 | type: "container", 9 | renderer: "form", 10 | elements: { 11 | customField: { 12 | name: "customField", 13 | label: "Custom text field", 14 | type: "field", 15 | renderer: CustomText, 16 | fieldType: "text" 17 | } 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /demo/src/schema/external-event-handlers.js: -------------------------------------------------------------------------------- 1 | import { 2 | onSuggestionsFetchRequested, 3 | onSuggestionsClearRequested, 4 | getSuggestionValue, 5 | renderSuggestion 6 | } from '../source/autocomplete'; 7 | 8 | import { 9 | alertTextOnChange, 10 | hideTextField, 11 | save, 12 | reset, 13 | unsetAutocomplete 14 | } from '../source/external-handlers'; 15 | 16 | export default { 17 | onSubmit: save.bind(this), 18 | onReset: reset.bind(this), 19 | initialValues: { 20 | valueWithoutAField: 1 21 | }, 22 | schema: { 23 | id: "external-event-handlers", 24 | label: "External event handlers", 25 | type: "container", 26 | renderer: "form", 27 | elements: { 28 | reactSelect: { 29 | type: "field", 30 | renderer: "react-select", 31 | name: "react-select", 32 | label: "Hide textfield", 33 | options: [ 34 | { 35 | value: 0, 36 | label: "No" 37 | }, 38 | { 39 | value: 1, 40 | label: "Yes" 41 | } 42 | ], 43 | formGroupClass: "form-group mb-4", 44 | onChange: hideTextField 45 | }, 46 | text: { 47 | name: "text1", 48 | label: "Text", 49 | type: "field", 50 | renderer: "text", 51 | fieldType: "text", 52 | onChange: alertTextOnChange, 53 | }, 54 | autocomplete: { 55 | name: "autocomplete", 56 | label: "Autocomplete", 57 | type: "field", 58 | renderer: "autocomplete", 59 | position: 10, 60 | initialSuggestions: [], 61 | options: { 62 | onSuggestionsFetchRequested: onSuggestionsFetchRequested, 63 | onSuggestionsClearRequested: onSuggestionsClearRequested, 64 | getSuggestionValue: getSuggestionValue, 65 | renderSuggestion: renderSuggestion, 66 | inputProps: {} 67 | } 68 | }, 69 | buttonsGroup: { 70 | type: "container", 71 | renderer: "button-group", 72 | buttonsContainerClass: "buttons-container mt-2", 73 | elements: { 74 | save: { 75 | type: "field", 76 | renderer: "button", 77 | name: "save", 78 | content: "Save", 79 | fieldClass: "btn-success float-right", 80 | buttonType: "submit" 81 | }, 82 | reset: { 83 | type: "field", 84 | renderer: "button", 85 | name: "reset", 86 | content: "Reset", 87 | fieldClass: "btn-primary float-right", 88 | buttonType: "reset", 89 | }, 90 | unsetAutocomplete: { 91 | type: "field", 92 | renderer: "button", 93 | name: "button", 94 | content: "Unset autocomplete", 95 | fieldClass: "btn-danger float-right", 96 | buttonType: "reset", 97 | onClick: unsetAutocomplete 98 | } 99 | } 100 | } 101 | } 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /demo/src/schema/index.js: -------------------------------------------------------------------------------- 1 | import basic from './basic'; 2 | import allAvailableFields from './all-available-fields'; 3 | import externalEventHandlers from './external-event-handlers'; 4 | import async from './async'; 5 | import containers from './containers'; 6 | import validation from './validation'; 7 | import autosave from './autosave'; 8 | import customRenderer from './custom-renderer'; 9 | 10 | export default [ 11 | { 12 | title: "Basic form", 13 | id: "basic-form", 14 | className: "mb-4", 15 | formProps: basic 16 | }, { 17 | title: "All available fields", 18 | id: "all-available-fields", 19 | className: "mb-4", 20 | formProps: allAvailableFields 21 | }, { 22 | title: "External event handlers", 23 | id: "external-event-handlers", 24 | className: "mb-4", 25 | formProps: externalEventHandlers 26 | }, { 27 | title: "Async", 28 | id: "async", 29 | className: "mb-4", 30 | formProps: async 31 | }, { 32 | title: "Containers", 33 | id: "containers", 34 | className: "mb-4", 35 | formProps: containers 36 | }, { 37 | title: "Validation", 38 | id: "validation", 39 | className: "mb-4", 40 | formProps: validation 41 | }, { 42 | title: "Custom Renderer", 43 | id: "custom-renderer", 44 | className: "mb-4", 45 | formProps: customRenderer 46 | }, { 47 | title: "Autosave", 48 | id: "autosave", 49 | className: "mb-4", 50 | formProps: autosave 51 | } 52 | ] 53 | -------------------------------------------------------------------------------- /demo/src/schema/validation.js: -------------------------------------------------------------------------------- 1 | import { 2 | onSuggestionsFetchRequested, 3 | onSuggestionsClearRequested, 4 | getSuggestionValue, 5 | renderSuggestion 6 | } from '../source/autocomplete'; 7 | 8 | import { save } from '../source/external-handlers'; 9 | 10 | export default { 11 | onSubmit: save.bind(this), 12 | initialValues: { 13 | radio: 1 14 | }, 15 | schema: { 16 | id: "validation", 17 | label: "Validation", 18 | type: "container", 19 | renderer: "form", 20 | elements: { 21 | text: { 22 | name: "text", 23 | label: "Text (First character alphabet and the rest alpha or integer)", 24 | type: "field", 25 | renderer: "text", 26 | fieldType: "text", 27 | validation: [['string'], ['required']] 28 | }, 29 | url: { 30 | name: "url", 31 | label: "Url (Required and needs to be a valid URL)", 32 | type: "field", 33 | renderer: "text", 34 | fieldType: "text", 35 | validation: [['string'], ['required'], ['url']] 36 | }, 37 | autocomplete: { 38 | name: "autocomplete", 39 | label: "Autocomplete (Required if text is empty)", 40 | type: "field", 41 | renderer: "autocomplete", 42 | position: 10, 43 | initialSuggestions: [], 44 | options: { 45 | onSuggestionsFetchRequested: onSuggestionsFetchRequested, 46 | onSuggestionsClearRequested: onSuggestionsClearRequested, 47 | getSuggestionValue: getSuggestionValue, 48 | renderSuggestion: renderSuggestion, 49 | inputProps: {} 50 | }, 51 | validation: [['string'], ['when', 'text', { 52 | is: undefined, 53 | then: [['string'], ['required'], ['min', 2]] 54 | }]] 55 | }, 56 | reactSelect: { 57 | type: "field", 58 | renderer: "react-select", 59 | name: "react-select", 60 | label: "React Select", 61 | options: [ 62 | { 63 | value: 0, 64 | label: "No" 65 | }, 66 | { 67 | value: 1, 68 | label: "Yes" 69 | } 70 | ], 71 | formGroupClass: "form-group mb-4", 72 | validation: [['boolean'], ['required']] 73 | }, 74 | submit: { 75 | content: "Submit", 76 | type: "field", 77 | renderer: "button", 78 | buttonType: "submit", 79 | fieldClass: "btn-success" 80 | } 81 | } 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /demo/src/source/autocomplete.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | const countries = [ 4 | { 5 | label: 'United States', 6 | value: 'US' 7 | }, 8 | { 9 | label: 'United Kingdom', 10 | value: 'UK' 11 | }, 12 | { 13 | label: 'Germany', 14 | value: 'DE' 15 | }, 16 | { 17 | label: 'Netherlands', 18 | value: 'NL' 19 | }, 20 | { 21 | label: 'France', 22 | value: 'FR' 23 | }, 24 | { 25 | label: 'Sweden', 26 | value: 'SE' 27 | }, 28 | ]; 29 | 30 | const getSuggestions = value => { 31 | const inputValue = value.trim().toLowerCase(); 32 | const inputLength = inputValue.length; 33 | 34 | return inputLength === 0 ? [] : countries.filter(country => 35 | country.label.toLowerCase().slice(0, inputLength) === inputValue 36 | ); 37 | }; 38 | 39 | export const getSuggestionValue = ( formikProps, config, extra, { label } ) => label; 40 | export const renderSuggestion = ( formikProps, config, extra, { label } ) => { label }; 41 | export const onSuggestionsFetchRequested = ( formikProps, config, { stateUpdater }, { value } ) => 42 | stateUpdater({ suggestions: getSuggestions(value) }); 43 | 44 | export const onSuggestionsClearRequested = ( formikProps, config, extra ) => 45 | extra.stateUpdater({ suggestions: [] }) 46 | -------------------------------------------------------------------------------- /demo/src/source/external-handlers.js: -------------------------------------------------------------------------------- 1 | import _ from 'lodash'; 2 | 3 | let delay = 1000; 4 | 5 | let textChangeTimeout = 0; 6 | export const alertTextOnChange = ( formikProps, fieldConfig, event ) => { 7 | clearTimeout(textChangeTimeout); 8 | textChangeTimeout = setTimeout(function () { 9 | alert('Current value in the text input: ' + event.target.value); 10 | }, delay); 11 | } 12 | 13 | export const hideTextField = ( formikProps, fieldConfig, value ) => 14 | document.getElementById('text1').parentElement.style.display = value ? 'none' : ''; 15 | 16 | export const save = ( values, formikProps ) => { 17 | console.log(values); 18 | alert(JSON.stringify(values)); 19 | formikProps.setSubmitting(false); 20 | } 21 | 22 | export const reset = ( values, formikProps ) => { 23 | console.log(values); 24 | formikProps.setValues({ 25 | reactSelect: 0 26 | }); 27 | } 28 | 29 | export const unsetAutocomplete = ( formikProps, fieldConfig, event ) => { 30 | console.log('Being unsetting autocomplete value...'); 31 | formikProps.setFieldValue('autocomplete', ''); 32 | console.log('End unsetting autocomplete value...'); 33 | } 34 | 35 | 36 | export const asyncFill = ( { setValues }, fieldConfig, event ) => { 37 | setTimeout(function() { 38 | setValues({ 39 | text1: 'This is an auto-filled value', 40 | autocomplete: 'This option isn\'t available in the suggestions' 41 | }) 42 | }, 2000); 43 | } 44 | 45 | export const autosave = _.debounce(( formik ) => { 46 | if(!_.isEqual(formik.values)) 47 | console.log('autosave', formik.values); 48 | }, 1000); 49 | -------------------------------------------------------------------------------- /nwb.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | type: 'react-component', 3 | npm: { 4 | esModules: true, 5 | umd: { 6 | global: 'FormikJson', 7 | externals: { 8 | react: 'React', 9 | 'react-dom': 'ReactDOM', 10 | lodash: '_', 11 | } 12 | } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@flipbyte/formik-json", 3 | "version": "0.6.2", 4 | "description": "formik-json is a wrapper for Formik to easily create forms using JSON / Javascript Object for defining the elements", 5 | "main": "lib/index.js", 6 | "module": "es/index.js", 7 | "files": [ 8 | "css", 9 | "es", 10 | "lib", 11 | "umd" 12 | ], 13 | "repository": { 14 | "type": "git", 15 | "url": "https://github.com/flipbyte/formik-json-schema.git" 16 | }, 17 | "scripts": { 18 | "build": "nwb build-react-component --copy-files", 19 | "clean": "nwb clean-module && nwb clean-demo", 20 | "prepublishOnly": "npm run build", 21 | "start": "nwb serve-react-demo", 22 | "test": "nwb test-react", 23 | "test:coverage": "nwb test-react --coverage", 24 | "test:watch": "nwb test-react --server", 25 | "lint-md": "remark .", 26 | "publish-demo": "npm run build && gh-pages -d demo/dist" 27 | }, 28 | "dependencies": { 29 | "@flipbyte/when-condition": "^0.6.0", 30 | "@flipbyte/yup-schema": "^0.1.5", 31 | "codemirror": "^5.41.0", 32 | "formik": "^2.1.4", 33 | "react-autosuggest": "^9.4.3", 34 | "react-codemirror2": "^7.0.0", 35 | "react-dropzone": "^10.2.1", 36 | "react-quill": "^1.3.3", 37 | "react-select": "^2.3.0", 38 | "react-sortable-hoc": "^1.11.0", 39 | "react-switch": "^5.0.1", 40 | "shallowequal": "^1.1.0" 41 | }, 42 | "peerDependencies": { 43 | "lodash": "^4.17.13", 44 | "react": "^16.8.6", 45 | "react-dom": "^16.8.6", 46 | "prop-types": "^15.6.2", 47 | "@fortawesome/fontawesome-free": "^5.6.3" 48 | }, 49 | "devDependencies": { 50 | "@fortawesome/fontawesome-free": "^5.6.3", 51 | "axios": "^0.21.1", 52 | "babel-plugin-transform-import-styles": "0.0.11", 53 | "bootstrap": "^4.3.1", 54 | "enzyme": "^3.9.0", 55 | "enzyme-adapter-react-16": "^1.10.0", 56 | "gh-pages": "^2.0.1", 57 | "inferno": "^5.0.0", 58 | "load-styles": "^2.0.0", 59 | "nwb": "0.23.x", 60 | "react": "^16.8.6", 61 | "react-dom": "^16.8.6", 62 | "remark-lint": "^6.0.4" 63 | }, 64 | "author": "", 65 | "homepage": "", 66 | "license": "MIT", 67 | "keywords": [ 68 | "react-json-form", 69 | "react-json-schema-form", 70 | "json-schema-form", 71 | "json-form", 72 | "formik-json", 73 | "formik-schema", 74 | "schema-form", 75 | "form", 76 | "validation", 77 | "bootstrap" 78 | ], 79 | "remarkConfig": { 80 | "plugins": [ 81 | "remark-preset-lint-recommended" 82 | ] 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /src/Container/ButtonGroup.js: -------------------------------------------------------------------------------- 1 | import _ from 'lodash'; 2 | import React from 'react'; 3 | import Element from '../Element'; 4 | import PropTypes from 'prop-types'; 5 | 6 | const ButtonGroup = ({ 7 | config: { 8 | elements, 9 | buttonsContainerClass = 'buttons-container', 10 | buttonGroupClass = 'btn-group' 11 | } 12 | }) => ( 13 |
14 |
15 | { _.map(elements, (element, key) => 16 | ) 17 | } 18 |
19 |
20 | ); 21 | 22 | ButtonGroup.propTypes = { 23 | config: PropTypes.shape({ 24 | buttonsContainerClass: PropTypes.string, 25 | buttonGroupClass: PropTypes.string, 26 | elements: PropTypes.object.isRequired 27 | }) 28 | } 29 | 30 | export default ButtonGroup; 31 | -------------------------------------------------------------------------------- /src/Container/EditableGrid.js: -------------------------------------------------------------------------------- 1 | import _ from 'lodash'; 2 | import React from 'react'; 3 | import Element from '../Element'; 4 | import PropTypes from 'prop-types'; 5 | import { FieldArray } from 'formik'; 6 | import { joinNames } from '../utils'; 7 | import { SortableContainer, SortableElement, SortableHandle } from 'react-sortable-hoc'; 8 | 9 | const onSortEnd = ( move, { oldIndex, newIndex } ) => move(oldIndex, newIndex); 10 | const SortableItem = SortableElement((props) => renderTableRow(props)); 11 | const SortableTableBody = SortableContainer((props) => renderTableBody(props)); 12 | const SortableRowHandle = SortableHandle((props) => renderSortableHandle(props)); 13 | 14 | const renderTableBody = ({ isObject = false, isSortable, hasValue, arrayValues, ...rowProps }) => ( 15 | 16 | { hasValue ? _.map(arrayValues, ( value, index ) => 17 | isObject === false && isSortable 18 | ? 19 | : renderTableRow({ ...rowProps, index, rowIndex: index, value, isObject }) 20 | ) : null } 21 | 22 | ); 23 | 24 | const renderTableRow = ({ fieldArrayName, elements, arrayActions, rowIndex, buttons, isSortable, value = {}, isObject = false }) => ( 25 | 26 | { isObject === false && isSortable && } 27 | { _.map(elements, ({ name, label, ...rest }, key) => ( 28 | 29 | 30 | 31 | ))} 32 | { isObject === false && !!buttons && ( 33 | 34 | { !!buttons.remove && ( 35 | _.isFunction(buttons.remove) 36 | ? buttons.remove(arrayActions, rowIndex, value) 37 | : ( 38 | 43 | ) 44 | ) 45 | } 46 | { !!buttons.duplicate && ( 47 | _.isFunction(buttons.duplicate) 48 | ? buttons.duplicate(arrayActions, value, rowIndex) 49 | : ( 50 | 55 | ) 56 | ) 57 | } 58 | 59 | )} 60 | 61 | ); 62 | 63 | const renderSortableHandle = ( props ) => 64 | 65 | const EditableGrid = ({ 66 | config: { 67 | name: fieldArrayName, 68 | isObject = false, 69 | elements, 70 | buttons, 71 | isSortable = true, 72 | comment, 73 | commentClass = 'text-muted d-block mb-3', 74 | tableContainerClass = 'table-responsive', 75 | tableClass = 'table table-bordered flutter-editable-grid' 76 | }, 77 | formik 78 | }) => { 79 | const { values, errors, touched } = formik; 80 | const arrayFields = _.mapValues(_.assign({}, elements), () => ''); 81 | const arrayValues = _.get(values, fieldArrayName); 82 | const hasValue = _.size(arrayValues) > 0; 83 | const tableWidth = _.map(elements, 'width').reduce(( sum, num ) => sum + num, 50) || '100%'; 84 | const additionalColumnCount = isSortable ? 2 : 1; 85 | 86 | return ( 87 | { 90 | const bodyProps = { 91 | arrayValues, hasValue, elements, fieldArrayName, arrayActions, buttons, isSortable, isObject 92 | }; 93 | 94 | return ( 95 |
96 | { comment && { comment } } 97 | 98 | 99 | 100 | { isObject === false && isSortable && } 101 | { _.map(elements, ({ label, width }, key) => 102 | 103 | ) } 104 | { isObject === false && !!buttons && !!buttons.remove && } 105 | 106 | 107 | { isObject === false && isSortable 108 | ? 114 | : renderTableBody(bodyProps) 115 | } 116 | 117 | 118 | { isObject === false && !!buttons && !!buttons.add && 119 | 131 | } 132 | 133 | 134 |
{ label }
120 | { _.isFunction(buttons.add) 121 | ? buttons.add(arrayActions, arrayFields) 122 | : ( 123 | 128 | ) 129 | } 130 |
135 |
136 | ) 137 | }} /> 138 | ); 139 | } 140 | 141 | EditableGrid.propTypes = { 142 | config: PropTypes.shape({ 143 | name: PropTypes.string.isRequired, 144 | elements: PropTypes.object.isRequired, 145 | buttons: PropTypes.exact({ 146 | add: PropTypes.oneOfType([ 147 | PropTypes.string, 148 | PropTypes.func 149 | ]), 150 | remove: PropTypes.oneOfType([ 151 | PropTypes.string, 152 | PropTypes.func 153 | ]), 154 | duplicate: PropTypes.oneOfType([ 155 | PropTypes.string, 156 | PropTypes.func 157 | ]), 158 | }), 159 | isSortable: PropTypes.bool, 160 | tableContainerClass: PropTypes.string, 161 | tableClass: PropTypes.string 162 | }) 163 | } 164 | 165 | export default EditableGrid; 166 | -------------------------------------------------------------------------------- /src/Container/Fieldset.js: -------------------------------------------------------------------------------- 1 | import _ from 'lodash'; 2 | import Element from '../Element'; 3 | import { getName } from '../utils'; 4 | import React, { useState } from 'react'; 5 | import PropTypes from 'prop-types'; 6 | 7 | const Fieldset = ({ 8 | config: { 9 | name: containerName = '', 10 | title, 11 | elements, 12 | collapsible = true, 13 | collapsed = false, 14 | hasHeaderIcon = true, 15 | comment, 16 | commentClass = 'text-muted d-block mb-3', 17 | headerIconClass = 'fa fa-align-justify', 18 | cardClass = 'card flutter-fieldset', 19 | cardHeaderClass = 'card-header', 20 | cardHeaderIconCollapsedClass = 'fas fa-angle-down', 21 | cardHeaderIconDisclosedClass = 'fas fa-angle-up', 22 | cardHeaderActionsClass = 'card-header-actions', 23 | cardBodyClass = 'card-body' 24 | } 25 | }) => { 26 | const [ isCollapsed, setIsCollapsed ] = useState(collapsible && collapsed); 27 | const toggle = (event) => { 28 | if(false === collapsible) { 29 | event.preventDefault(); 30 | return; 31 | } 32 | setIsCollapsed(isCollapsed => !isCollapsed); 33 | } 34 | 35 | return ( 36 |
37 | { !!title && 38 |
39 | { hasHeaderIcon && } 40 | { title } 41 | { collapsible &&
42 | 43 | 44 | 45 |
} 46 | 47 |
48 | } 49 |
50 |
51 | { comment && { comment } } 52 | { _.map(elements, ({ name, ...config }, key) => ( 53 | 58 | ))} 59 |
60 |
61 |
62 | ); 63 | } 64 | 65 | Fieldset.propTypes = { 66 | config: PropTypes.shape({ 67 | name: PropTypes.string, 68 | title: PropTypes.string, 69 | elements: PropTypes.object.isRequired, 70 | cardClass: PropTypes.string, 71 | cardHeaderClass: PropTypes.string, 72 | cardHeaderActionsClass: PropTypes.string, 73 | cardBodyClass: PropTypes.string 74 | }) 75 | } 76 | 77 | export default Fieldset; 78 | -------------------------------------------------------------------------------- /src/Container/Form.js: -------------------------------------------------------------------------------- 1 | import _ from 'lodash'; 2 | import React from 'react'; 3 | import Element from '../Element'; 4 | import { getName } from '../utils'; 5 | import PropTypes from 'prop-types'; 6 | import { Form as FormikDOMForm } from 'formik'; 7 | 8 | const Form = ({ config }) => { 9 | const { name: containerName = '', elements, htmlClass = 'form-horizontal', comment, commentClass = 'text-muted d-block mb-3' } = config; 10 | 11 | return( 12 | 13 | { comment && { comment } } 14 | { _.map(elements, ({ name, ...config }, key) => ( 15 | 19 | ))} 20 | 21 | ); 22 | } 23 | 24 | Form.propTypes = { 25 | config: PropTypes.shape({ 26 | name: PropTypes.string, 27 | htmlClass: PropTypes.string, 28 | elements: PropTypes.object.isRequired 29 | }) 30 | } 31 | 32 | export default Form; 33 | -------------------------------------------------------------------------------- /src/Container/HtmlTag.js: -------------------------------------------------------------------------------- 1 | import _ from 'lodash'; 2 | import React from 'react'; 3 | import Element from '../Element'; 4 | import PropTypes from 'prop-types'; 5 | import { joinNames } from '../utils'; 6 | 7 | const HtmlTag = ({ 8 | config: { 9 | name: containerName = '', 10 | as: Component = 'div', 11 | elements, 12 | htmlClass, 13 | comment, 14 | commentClass = 'text-muted d-block mb-3' 15 | } 16 | }) => ( 17 | 18 | { comment && { comment } } 19 | { _.map(elements, ({ name, ...rest }, key) => ( 20 | 24 | ))} 25 | 26 | ); 27 | 28 | HtmlTag.propTypes = { 29 | config: PropTypes.shape({ 30 | name: PropTypes.string, 31 | elements: PropTypes.object.isRequired, 32 | htmlClass: PropTypes.string 33 | }) 34 | } 35 | 36 | export default HtmlTag; 37 | -------------------------------------------------------------------------------- /src/Container/Tabs.js: -------------------------------------------------------------------------------- 1 | import _ from 'lodash'; 2 | import Element from '../Element'; 3 | import { joinNames } from '../utils'; 4 | import React, { useEffect, useRef, useState } from 'react'; 5 | import PropTypes from 'prop-types'; 6 | import shallowequal from 'shallowequal'; 7 | 8 | const tabPaneInvalid = { 9 | color: '#dc3545', 10 | }; 11 | 12 | const tabPaneActiveInvalid = { 13 | color: '#fff', 14 | backgroundColor: '#dc3545', 15 | border: '1px solid #dc3545' 16 | }; 17 | 18 | const Tabs = ({ config = {} }) => { 19 | const { 20 | elements = {}, 21 | name: containerName = '', 22 | cardClass = 'card', 23 | cardBodyClass = 'card-body', 24 | rowClass = 'row', 25 | tabListClass = 'nav flex-column nav-pills', 26 | tabListItemClass = 'nav-link', 27 | tabContentClass = 'tab-content flutter-rjf-tab-content', 28 | tabColumnClass = 'col-sm-12 col-md-3', 29 | contentColumnClass = 'col-sm-12 col-md-9', 30 | tabActiveClass = ' active ', 31 | tabPaneClass = 'tab-pane fade show', 32 | } = config; 33 | const tabContentEl = useRef({}); 34 | const [ activeTab, setActiveTab ] = useState(_.first(_.keys(elements))); 35 | const [ isValid, setIsValid ] = useState([]); 36 | const [ tabs, setTabs ] = useState({}); 37 | const [ tabContent, setTabContent ] = useState({}); 38 | const [ tabId ] = useState(_.uniqueId('list-tab-')); 39 | 40 | const tabValidations = _(isValid); 41 | 42 | useEffect(() => { 43 | _.map(elements, (tab, key) => { 44 | const { label, elements: content, active, name, comment, commentClass } = tab; 45 | 46 | setTabs((state) => ({ 47 | ...state, 48 | [key]: label 49 | })); 50 | 51 | setTabContent((state) => ({ 52 | ...state, 53 | [key]: { name, content, comment, commentClass } 54 | })); 55 | 56 | if (active) { 57 | setActiveTab(key); 58 | } 59 | }); 60 | }, []); 61 | 62 | useEffect(() => { 63 | const node = tabContentEl.current; 64 | var panes = _.map(node.children, child => child.querySelector('.is-invalid') !== null) 65 | if(!shallowequal(isValid, panes)) { 66 | setIsValid(panes) 67 | } 68 | }); 69 | 70 | return ( 71 |
72 |
73 |
74 |
75 | 101 |
102 |
103 |
104 | { _.map(tabContent, ( 105 | { name: tabName = '', content, comment, commentClass = 'text-muted d-block mb-3' }, 106 | tabKey, 107 | index 108 | ) => ( 109 |
115 | { comment && { comment } } 116 | { _.map(content, ({ name, ...rest }, key) => ( 117 | 122 | ))} 123 |
124 | ))} 125 |
126 |
127 |
128 |
129 |
130 | ); 131 | }; 132 | 133 | Tabs.propTypes = { 134 | config: PropTypes.shape({ 135 | name: PropTypes.string, 136 | elements: PropTypes.object.isRequired, 137 | cardClass: PropTypes.string, 138 | cardBodyClass: PropTypes.string, 139 | rowClass: PropTypes.string, 140 | tabListClass: PropTypes.string, 141 | tabListItemClass: PropTypes.string, 142 | tabContentClass: PropTypes.string, 143 | tabColumnClass: PropTypes.string, 144 | contentColumnClass: PropTypes.string, 145 | tabActiveClass: PropTypes.string, 146 | tabPaneClass: PropTypes.string, 147 | }) 148 | } 149 | 150 | 151 | export default Tabs; 152 | -------------------------------------------------------------------------------- /src/Container/index.js: -------------------------------------------------------------------------------- 1 | import { registerContainer } from '../registry'; 2 | 3 | import HtmlTag from './HtmlTag'; 4 | import Form from './Form'; 5 | import Tabs from './Tabs'; 6 | import Fieldset from './Fieldset'; 7 | import ButtonGroup from './ButtonGroup'; 8 | import EditableGrid from './EditableGrid'; 9 | 10 | registerContainer('div', HtmlTag); 11 | registerContainer('html-tag', HtmlTag); 12 | registerContainer('form', Form); 13 | registerContainer('tabs', Tabs); 14 | registerContainer('fieldset', Fieldset); 15 | registerContainer('button-group', ButtonGroup); 16 | registerContainer('editable-grid', EditableGrid); 17 | -------------------------------------------------------------------------------- /src/Element.js: -------------------------------------------------------------------------------- 1 | import _ from 'lodash'; 2 | import { connect } from 'formik'; 3 | import React, { useEffect, useState } from 'react'; 4 | import ElementRenderer from './Renderer'; 5 | import { FIELD } from './registry'; 6 | import shallowequal from 'shallowequal'; 7 | 8 | const Element = ({ config, update, formik }) => { 9 | const { configSource, dataSource } = config; 10 | const [ hasLoadedConfig, setHasLoadedConfig ] = useState(false); 11 | const [ hasLoadedData, setHasLoadedData ] = useState(dataSource ? false : true); 12 | const [ hasMounted, setHasMounted ] = useState(update !== false); 13 | const [ loadedConfig, setLoadedConfig ] = useState(undefined); 14 | 15 | /** 16 | * After load data 17 | * 18 | * @param {mixed} value 19 | * @return {void} 20 | */ 21 | const loadDataAfter = (value) => setHasLoadedData(true); 22 | 23 | /** 24 | * After load config 25 | * 26 | * @param {object} newConfig 27 | * @return {void} 28 | */ 29 | const loadConfigAfter = (newConfig) => { 30 | setHasLoadedConfig(true); 31 | setLoadedConfig(_.assign({}, config, newConfig)) 32 | }; 33 | 34 | /** 35 | * On mount, load if there is a valid config source, 36 | * load the data from the config source and handle 37 | * whether future loads should be possible 38 | */ 39 | useEffect(() => { 40 | if (!hasLoadedConfig && typeof configSource === 'function') { 41 | configSource(formik, config).then(loadConfigAfter).catch((err) => {}); 42 | } 43 | 44 | return () => setHasLoadedConfig(false); 45 | }, []); 46 | 47 | /** 48 | * If the value of update changes or if the form is currently validating (during submission), 49 | * set that value for hasMounted => true 50 | */ 51 | useEffect(() => { 52 | setHasMounted((hasMounted) => { 53 | if (hasMounted) { 54 | return hasMounted; 55 | } 56 | 57 | return update !== false || formik.isValidating === true; 58 | }); 59 | }, [ update, formik.isValidating ]); 60 | 61 | /** 62 | * If a valid dataSource exists, call the dataSource when the element is mounted. 63 | * Also, call this when initialValues have changed and the component is mounted 64 | * 65 | * The latter is useful when you update the data on the server and reinitialize the 66 | * values of the form top-down where the value of this particular field comes from a dataSource 67 | */ 68 | useEffect(() => { 69 | if (typeof dataSource === 'function' && hasMounted) { 70 | dataSource(formik, config).then(loadDataAfter).catch((err) => {}); 71 | } 72 | }, [ hasMounted, formik.initialValues ]); 73 | 74 | return hasMounted && ( 75 | 76 | ); 77 | } 78 | 79 | export default connect( 80 | React.memo(Element, ({ config, formik, update }, nextProps) => ( 81 | update === nextProps.update 82 | && shallowequal(config, nextProps.config) 83 | && formik.initialValues === nextProps.formik.initialValues 84 | && formik.isValidating === nextProps.formik.isValidating 85 | && formik.isSubmitting === nextProps.formik.isSubmitting 86 | )) 87 | ); 88 | -------------------------------------------------------------------------------- /src/ErrorManager.js: -------------------------------------------------------------------------------- 1 | import _ from 'lodash'; 2 | import React, { useState } from 'react'; 3 | import { useFormikContext } from 'formik'; 4 | 5 | /** 6 | * Error manager component that displays error only when it's right 7 | * 8 | * The component sets the global formik submitCount in it's local state for reference 9 | * The local submit count is used to make sure the error message is not shown on it's initial load 10 | * 11 | * The local submitcount will be set to 1 less than the value of submitCount if the form is being 12 | * submitted when the fields are mounted. This is done so that fields (such as tab fields) that are mounted 13 | * for the sole purpose of showing error messages correctly, show the error message right during the first load 14 | * 15 | * The error message will be visible only after the first touch or first form submission so that 16 | * form submitted with fields hidden do not show the a message when they show up when a certain condition is 17 | * satisified, fieldset disclosed, tab opened, editable grid field added etc. 18 | * 19 | * @param {string} name 20 | * @param {object} formik 21 | * @param {function} children 22 | */ 23 | const ErrorManager = ({ name, children }) => { 24 | // Set submitCount on initial mount. 25 | const formik = useFormikContext(); 26 | const { submitCount: formikSubmitCount, isSubmitting, errors, touched } = useFormikContext(); 27 | const [ submitCount ] = useState(isSubmitting ? formikSubmitCount - 1 : formikSubmitCount); 28 | const isTouched = _.get(touched, name); 29 | const errorMessage = _.get(errors, name); 30 | const error = !_.isEmpty(errorMessage) && (isTouched || formikSubmitCount > submitCount) ? errorMessage : false; 31 | 32 | return children(error); 33 | }; 34 | 35 | export default ErrorManager; 36 | -------------------------------------------------------------------------------- /src/Field/Autocomplete.js: -------------------------------------------------------------------------------- 1 | import _ from 'lodash'; 2 | import React, { Component } from 'react'; 3 | import Autosuggest from 'react-autosuggest'; 4 | import { changeHandler, setFieldValueWrapper } from '../utils'; 5 | // import '../css/autocomplete.css'; 6 | 7 | class Autocomplete extends Component { 8 | static autosuggestCallbackKeys = [ 9 | 'onSuggestionsFetchRequested', 10 | 'onSuggestionsClearRequested', 11 | 'getSuggestionValue', 12 | 'renderSuggestion', 13 | 'onSuggestionSelected', 14 | 'onSuggestionHighlighted', 15 | 'shouldRenderSuggestions', 16 | 'renderSectionTitle', 17 | 'getSectionSuggestions', 18 | 'renderInputComponent', 19 | 'renderSuggestionsContainer' 20 | ] 21 | 22 | constructor(props) { 23 | super(props); 24 | 25 | const { initialSuggestions = [] } = this.props; 26 | this.state = { suggestions: initialSuggestions }; 27 | this.initOptions(); 28 | } 29 | 30 | initOptions() { 31 | this.prepareCallbacks(); 32 | this.autosuggestOptions = _.assign({ inputProps: {} }, this.props.config.options, this.callbacks); 33 | this.inputClassName = this.autosuggestOptions.inputProps.className || 'react-autosuggest__input'; 34 | } 35 | 36 | prepareCallbacks() { 37 | const { formik, config } = this.props; 38 | const stateUpdater = this.setState.bind(this); 39 | 40 | this.callbacks = _.reduce(Autocomplete.autosuggestCallbackKeys, ( callbacks, key ) => { 41 | if(_.isFunction(config.options[key])) { 42 | callbacks[key] = config.options[key].bind(this, formik, config, { stateUpdater }); 43 | } 44 | 45 | return callbacks; 46 | }, {}); 47 | } 48 | 49 | render() { 50 | const { config, formik, error, value } = this.props; 51 | const { name } = config; 52 | const { setFieldValue, handleBlur } = formik; 53 | 54 | this.autosuggestOptions.inputProps.name = name; 55 | this.autosuggestOptions.inputProps.value = value || ''; 56 | this.autosuggestOptions.inputProps.onChange = ( event, { newValue, method } ) => 57 | changeHandler(setFieldValueWrapper(setFieldValue, name), formik, config, newValue); 58 | this.autosuggestOptions.inputProps.onBlur = handleBlur.bind(this); 59 | this.autosuggestOptions.inputProps.className = this.inputClassName + ( error ? ' is-invalid ' : '' ) 60 | 61 | return ; 62 | } 63 | } 64 | 65 | export default React.memo(Autocomplete); 66 | -------------------------------------------------------------------------------- /src/Field/Button.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | const Button = ({ config, formik }) => { 4 | const { content, fieldClass, buttonType, onClick } = config; 5 | const { isSubmitting } = formik; 6 | 7 | let buttonProps = { 8 | type: buttonType ? buttonType : 'button' , 9 | className: 'btn ' + fieldClass, 10 | disabled: isSubmitting 11 | }; 12 | 13 | if(typeof onClick === 'function') { 14 | buttonProps.onClick = onClick.bind(this, formik, config); 15 | } 16 | 17 | return ( 18 | 21 | ); 22 | } 23 | 24 | export default Button; 25 | -------------------------------------------------------------------------------- /src/Field/Checkbox.js: -------------------------------------------------------------------------------- 1 | import _ from 'lodash'; 2 | import React from 'react'; 3 | import PropTypes from 'prop-types'; 4 | import { changeHandler } from '../utils'; 5 | 6 | const Checkbox = ({ config, formik, value, error }) => { 7 | const { 8 | name, 9 | attributes, 10 | options = [], 11 | formCheckClass = 'form-check', 12 | fieldClass = 'form-check-input', 13 | formCheckLabelClass = 'form-check-label', 14 | } = config; 15 | 16 | const { handleChange, handleBlur } = formik; 17 | const checkboxValue = value || []; 18 | return options.map(({ value, label}, key, index) => { 19 | const fieldName = _.kebabCase(name + ' ' + value); 20 | return ( 21 |
22 | 35 |
36 | ); 37 | }); 38 | } 39 | 40 | Checkbox.propTypes = { 41 | config: PropTypes.shape({ 42 | name: PropTypes.string.isRequired, 43 | options: PropTypes.array.isRequired, 44 | formCheckClass: PropTypes.string 45 | }) 46 | } 47 | 48 | export default React.memo(Checkbox); 49 | -------------------------------------------------------------------------------- /src/Field/CodeEditor.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Controlled as CodeMirror } from 'react-codemirror2'; 3 | import { changeHandler, setFieldValueWrapper } from '../utils'; 4 | 5 | const CodeEditor = ({ config, formik, value, error }) => { 6 | const { 7 | name, 8 | options, 9 | defaultValue, 10 | attributes, 11 | fieldClass = '' 12 | } = config; 13 | const { setFieldValue, handleBlur } = formik; 14 | const selectedValue = value || ''; 15 | 16 | return ( 17 | { 23 | changeHandler(setFieldValueWrapper(setFieldValue, name), formik, config, value) 24 | }} 25 | onBlur={(editor, event) => { 26 | return ( 27 | handleBlur({ 28 | ...event, 29 | target: { 30 | ...event.target, 31 | name 32 | } 33 | }) 34 | ); 35 | }} 36 | value={ selectedValue } 37 | { ...attributes } 38 | /> 39 | ); 40 | } 41 | 42 | export default React.memo(CodeEditor); 43 | -------------------------------------------------------------------------------- /src/Field/ErrorMessage.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | const ErrorMessage = ({ 4 | name, 5 | error, 6 | className = 'invalid-feedback order-last', 7 | as: Component = 'div' 8 | }) => error && ( 9 | { error } 10 | ); 11 | 12 | export default React.memo(ErrorMessage); 13 | -------------------------------------------------------------------------------- /src/Field/FileUploader.js: -------------------------------------------------------------------------------- 1 | import React, { useMemo } from 'react'; 2 | import { useDropzone } from 'react-dropzone'; 3 | import { changeHandler } from '../utils'; 4 | 5 | const Thumb = ({ key, file }) =>
6 | 7 | const thumbsContainer = { 8 | display: 'flex', 9 | flexDirection: 'row', 10 | flexWrap: 'wrap', 11 | marginTop: 16 12 | } 13 | 14 | const thumb = { 15 | display: 'inline-flex', 16 | borderRadius: 2, 17 | border: '1px solid #eaeaea', 18 | marginBottom: 8, 19 | marginRight: 8, 20 | width: 100, 21 | height: 100, 22 | padding: 4, 23 | boxSizing: 'border-box' 24 | } 25 | 26 | const thumbInner = { 27 | display: 'flex', 28 | minWidth: 0, 29 | overflow: 'hidden' 30 | } 31 | 32 | const img = { 33 | display: 'block', 34 | width: 'auto', 35 | height: '100%' 36 | } 37 | 38 | const baseStyle = { 39 | width: '100%', 40 | height: 200, 41 | borderWidth: 2, 42 | borderColor: '#666', 43 | borderStyle: 'dashed', 44 | borderRadius: 5, 45 | padding: 10 46 | } 47 | 48 | const activeStyle = { 49 | borderColor: '#6c6', 50 | backgroundColor: '#eee' 51 | } 52 | 53 | const acceptStyle = { 54 | borderColor: '#00e676' 55 | } 56 | 57 | const rejectStyle = { 58 | borderColor: '#ff1744' 59 | } 60 | 61 | const prepareFileUploderOptions = ({ onDrop, onDropAccepted, onDropRejected, ...options }, formik, config) => { 62 | options.onDrop = onDrop ? onDrop.bind(this, formik, config) : null; 63 | options.onDropAccepted = onDropAccepted ? onDropAccepted.bind(this, formik, config) : null; 64 | options.onDropRejected = onDropRejected ? onDropRejected.bind(this, formik, config) : null; 65 | 66 | return options; 67 | } 68 | 69 | const FileUploader = ({ config, formik, value, error }) => { 70 | const { 71 | name, 72 | options, 73 | placeholder, 74 | disabledText, 75 | zoneActiveText, 76 | hasThumbs = false 77 | } = config; 78 | const { setFieldValue } = formik; 79 | const selectedValue = value; 80 | const { 81 | acceptedFiles, 82 | getRootProps, 83 | getInputProps, 84 | isDragActive, 85 | isDragAccept, 86 | isDragReject 87 | } = useDropzone({ ...prepareFileUploderOptions({ ...options }, formik, config) }); 88 | 89 | const style = useMemo(() => ({ 90 | ...baseStyle, 91 | ...(isDragActive ? activeStyle : {}), 92 | ...(isDragAccept ? acceptStyle : {}), 93 | ...(isDragReject ? rejectStyle : {}) 94 | }), [ 95 | isDragActive, 96 | isDragReject 97 | ]); 98 | 99 | return ( 100 |
101 |
102 | 103 | { isDragActive 104 | ?

Drop the files here ...

105 | :

Drag 'n' drop some files here, or click to select files

106 | } 107 |
108 | 117 |
118 | ); 119 | } 120 | 121 | export default React.memo(FileUploader) 122 | -------------------------------------------------------------------------------- /src/Field/InnerText.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | const InnerText = ({ config, formik, value, error }) => { 4 | const { 5 | name, 6 | as: Component = 'span', 7 | htmlClass, 8 | defaultValue = '', 9 | ...attributes 10 | } = config; 11 | 12 | return ( 13 | 14 | { value || defaultValue } 15 | 16 | ); 17 | }; 18 | 19 | export default React.memo(InnerText); 20 | -------------------------------------------------------------------------------- /src/Field/Label.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | const Label = ({ children, ...attributes }) => children ? : null; 4 | 5 | export default React.memo(Label); 6 | -------------------------------------------------------------------------------- /src/Field/Radio.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { changeHandler } from '../utils'; 3 | 4 | const Radio = ({ config, formik, value, error }) => { 5 | const { 6 | name, 7 | type, 8 | attributes, 9 | options, 10 | formCheckClass = 'form-check', 11 | fieldClass = 'form-check-input', 12 | formCheckLabelClass = 'form-check-label', 13 | } = config; 14 | const { handleChange, handleBlur } = formik; 15 | 16 | return options.map(( option ) => ( 17 |
18 | 32 |
33 | )); 34 | } 35 | 36 | export default React.memo(Radio); 37 | -------------------------------------------------------------------------------- /src/Field/ReactSelect.js: -------------------------------------------------------------------------------- 1 | import _ from 'lodash'; 2 | import React, { useState } from 'react'; 3 | import Select from 'react-select'; 4 | import CreatableSelect from 'react-select/lib/Creatable'; 5 | import { changeHandler, setFieldValueWrapper } from '../utils'; 6 | 7 | const prepareOptions = ( options ) => ( 8 | _.reduce(options, (result, value) => { 9 | if(!_.isObject(value) && !_.isEmpty(value)) { 10 | result.push({ value: value, label: value }) 11 | } else { 12 | result.push(value); 13 | } 14 | 15 | return result; 16 | }, []) 17 | ); 18 | 19 | const getSelectedOptions = ( options, values, isCreatable ) => { 20 | const getSelectedOption = ( value ) => { 21 | const selectedOption = _.filter(options, _.matches({ value })); 22 | return !_.isEmpty(selectedOption) 23 | ? selectedOption 24 | : isCreatable ? [{ value, label: value }] : null; 25 | } 26 | 27 | if (values) { 28 | if(!_.isObject(values)) { 29 | return getSelectedOption(values) 30 | } 31 | 32 | return _.reduce(values, (result, value) => { 33 | const selectedOption = getSelectedOption(value); 34 | if(_.isEmpty(selectedOption)) { 35 | return result; 36 | } 37 | 38 | return _.union(result, selectedOption); 39 | }, []) 40 | } 41 | 42 | return null; 43 | } 44 | 45 | const ReactSelect = ({ config, formik, value, error }) => { 46 | const { 47 | name, 48 | isMulti, 49 | defaultValue, 50 | fieldClass = '', 51 | noOptionsMessage, 52 | isDisabled = false, 53 | isClearable = false, 54 | isCreatable = false, 55 | options: initialOptions, 56 | ...attributes 57 | } = config; 58 | const { setFieldValue, handleBlur } = formik; 59 | const options = prepareOptions(initialOptions); 60 | const selectedValue = value || defaultValue; 61 | const selectedOption = getSelectedOptions(options, selectedValue, isCreatable); 62 | const [inputValue, setInputValue] = useState(''); 63 | 64 | var selectProps = { 65 | name, 66 | isMulti, 67 | noOptionsMessage, 68 | isClearable, 69 | isDisabled, 70 | id: name, 71 | inputValue, 72 | className: fieldClass + ( error ? ' is-invalid ' : '' ), 73 | onChange: ( selectedOptions ) => { 74 | const selectedValues = !isMulti 75 | ? selectedOptions.value 76 | : _.reduce(selectedOptions, (result, option) => [ ...result, option.value ], []); 77 | 78 | 79 | return changeHandler( 80 | setFieldValueWrapper(setFieldValue, name), 81 | formik, 82 | config, 83 | selectedValues 84 | ); 85 | }, 86 | onBlur: (event) => { 87 | return handleBlur({ 88 | ...event, 89 | target: { 90 | ...event.target, 91 | name 92 | } 93 | }); 94 | }, 95 | onInputChange: (inputValue) => { 96 | changeHandler(setInputValue, formik, config, inputValue, 'onInputChange'); 97 | }, 98 | onKeyDown: (event) => { 99 | if (!isMulti || !inputValue || !selectedValue || selectedValue.indexOf(inputValue) > -1) { 100 | return; 101 | } 102 | 103 | switch (event.key) { 104 | case 'Enter': 105 | case 'Tab': 106 | changeHandler( 107 | setFieldValueWrapper(setFieldValue, name), 108 | formik, 109 | config, 110 | [ ...selectedValue, inputValue ], 111 | 'onChange' 112 | ); 113 | setInputValue(''); 114 | event.preventDefault(); 115 | } 116 | }, 117 | ...attributes 118 | }; 119 | selectProps = _.assign(selectProps, { options }); 120 | 121 | if (selectedOption) { 122 | selectProps.value = selectedOption; 123 | } 124 | 125 | const SelectComponent = isCreatable ? CreatableSelect : Select; 126 | return ; 127 | } 128 | 129 | export default React.memo(ReactSelect); 130 | -------------------------------------------------------------------------------- /src/Field/Switch.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { default as ReactSwitch } from "react-switch"; 3 | import { changeHandler, setFieldValueWrapper } from '../utils'; 4 | 5 | const Switch = ({ config, formik, value = false, error }) => { 6 | const { 7 | name, 8 | fieldClass = 'switch d-block' 9 | } = config; 10 | const { setFieldValue } = formik; 11 | 12 | return ( 13 | 19 | ); 20 | } 21 | 22 | export default React.memo(Switch); 23 | -------------------------------------------------------------------------------- /src/Field/Text.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { changeHandler } from '../utils'; 3 | 4 | const Text = ({ config, formik, value = '', error }) => { 5 | const { 6 | name, 7 | type, 8 | attributes, 9 | fieldType, 10 | defaultValue, 11 | icon, 12 | fieldClass = 'form-control', 13 | inputGroupClass = 'input-group' 14 | } = config; 15 | 16 | const { handleChange, handleBlur } = formik; 17 | const isInputGroup = icon ? true : false; 18 | 19 | return ( 20 | isInputGroup ? 21 |
22 |
23 | 24 | 25 | 26 |
27 | 37 |
: 38 | 48 | ); 49 | } 50 | 51 | export default React.memo(Text); 52 | -------------------------------------------------------------------------------- /src/Field/Textarea.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { changeHandler } from '../utils'; 3 | 4 | const Textarea = ({ config, formik, value = '', error }) => { 5 | const { 6 | name, 7 | type, 8 | attributes, 9 | rows, 10 | fieldClass = 'form-control' 11 | } = config; 12 | const { handleChange, handleBlur } = formik; 13 | 14 | return ( 15 |