├── .gitignore ├── README.md ├── docs ├── asset-manifest.json ├── index.html ├── robots.txt └── static │ ├── css │ ├── main.829e0053.css │ └── main.829e0053.css.map │ └── js │ ├── main.f57bf6a8.js │ ├── main.f57bf6a8.js.LICENSE.txt │ └── main.f57bf6a8.js.map ├── package-lock.json ├── package.json ├── public ├── index.html └── robots.txt ├── screenshot.png └── src ├── App.js ├── components └── forms │ ├── AdvancedForm.js │ ├── Checkbox.js │ ├── ConditionalField.js │ ├── FormField.js │ ├── RadioGroup.js │ ├── Select.js │ ├── TextArea.js │ ├── TextField.js │ ├── helpers.js │ └── index.js ├── index.css └── index.js /.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules 2 | /build 3 | .DS_Store 4 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # React Advanced Form 2 | 3 | An example of a schema-based form system in React. Define your schema, and pass it into the form. Supports basic conditional schema. 4 | 5 | ![](./screenshot.png) 6 | 7 | ### Example 8 | 9 | The schema system support text input, checkbox, radio group, select, textarea, and conditonal fields. It is presented in these examples with plain HTML and CSS based forms, but can easily be modified for use with any React UI framework, such as Material UI, Semantic UI, etc. 10 | 11 | ```js 12 | const formSchema = [ 13 | { name: 'name', label: 'Name', componentType: 'text', required: true }, 14 | { name: 'playable', label: 'Playable', componentType: 'checkbox' }, 15 | { 16 | name: 'race', 17 | label: 'Race', 18 | componentType: 'radioGroup', 19 | options: [ 20 | { label: 'Human', value: 'human' }, 21 | { label: 'Dwarf', value: 'dwarf' }, 22 | { label: 'Elf', value: 'elf' }, 23 | ], 24 | }, 25 | { 26 | name: 'class', 27 | label: 'Class', 28 | componentType: 'select', 29 | options: [ 30 | { label: 'Ranger', value: 'ranger' }, 31 | { label: 'Wizard', value: 'wizard' }, 32 | { label: 'Healer', value: 'healer' }, 33 | ], 34 | }, 35 | { 36 | name: 'spell', 37 | label: 'Spell', 38 | componentType: 'select', 39 | options: [ 40 | { label: 'Fire', value: 'fire' }, 41 | { label: 'Ice', value: 'ice' }, 42 | ], 43 | condition: { key: 'class', value: 'wizard', operator: '=' }, 44 | }, 45 | { 46 | name: 'description', 47 | label: 'Description', 48 | componentType: 'textarea', 49 | }, 50 | ] 51 | ``` 52 | 53 | Simply pass the schema into the component, and handle the submit event. 54 | 55 | ```jsx 56 | 57 | ``` 58 | -------------------------------------------------------------------------------- /docs/asset-manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "files": { 3 | "main.css": "/static/css/main.829e0053.css", 4 | "main.js": "/static/js/main.f57bf6a8.js", 5 | "index.html": "/index.html", 6 | "main.829e0053.css.map": "/static/css/main.829e0053.css.map", 7 | "main.f57bf6a8.js.map": "/static/js/main.f57bf6a8.js.map" 8 | }, 9 | "entrypoints": [ 10 | "static/css/main.829e0053.css", 11 | "static/js/main.f57bf6a8.js" 12 | ] 13 | } -------------------------------------------------------------------------------- /docs/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Advanced Form 8 | 9 | 10 | 11 | 12 | 13 |
14 | 15 | 16 | -------------------------------------------------------------------------------- /docs/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /docs/static/css/main.829e0053.css: -------------------------------------------------------------------------------- 1 | *,:after,:before{box-sizing:border-box}body{-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale;background:#f0f0f0;font-family:-apple-system,BlinkMacSystemFont,sans-serif;margin:0;padding:1rem}h1{margin:1rem auto 2rem;max-width:1000px}pre{word-wrap:break-word;font-family:Menlo,monospace;font-size:1rem;white-space:pre-wrap}.flex{display:flex;gap:2rem;margin:0 auto;max-width:1000px;width:100%}.section{background:#fff;border:1px solid #dedede;border-radius:8px;padding:2rem}.form.section{flex:0.5 1}.results.section{flex:1 1}.advanced-form input,.advanced-form select,.advanced-form textarea{border:1px solid #aaa;border-radius:4px;margin:0;padding:.5rem}.advanced-form input[type=radio]{margin-right:.5rem}.advanced-form input[type=text],.advanced-form select,.advanced-form textarea{width:100%}.advanced-form textarea{min-height:100px}.advanced-form label{display:block;font-weight:600;margin-bottom:.5rem}.advanced-form .field{margin-bottom:1rem}.advanced-form .error,.advanced-form .required{color:#d63642}.advanced-form .error{display:block;margin:.25rem 0}.advanced-form button{background:#3642d6;border:1px solid #3642d6;border-radius:4px;color:#fff;cursor:pointer;font-size:1rem;font-weight:600;padding:.5rem 1rem}.advanced-form:hover{-webkit-filter:brightness(1.1);filter:brightness(1.1)}.advanced-form button:disabled{opacity:.5} 2 | /*# sourceMappingURL=main.829e0053.css.map*/ -------------------------------------------------------------------------------- /docs/static/css/main.829e0053.css.map: -------------------------------------------------------------------------------- 1 | {"version":3,"file":"static/css/main.829e0053.css","mappings":"AAAA,iBAGE,qBACF,CAEA,KAKE,kCAAmC,CACnC,iCAAkC,CAHlC,kBAAmB,CACnB,uDAA0D,CAH1D,QAAS,CACT,YAKF,CAEA,GACE,qBAAsB,CACtB,gBACF,CAEA,IAGE,oBAAqB,CADrB,2BAA6B,CAD7B,cAAe,CAGf,oBACF,CAEA,MAGE,YAAa,CACb,QAAS,CAHT,aAAc,CACd,gBAAiB,CAGjB,UACF,CAEA,SACE,eAAiB,CACjB,wBAAyB,CACzB,iBAAkB,CAClB,YACF,CAEA,cACE,UACF,CAEA,iBACE,QACF,CAEA,mEAGE,qBAAsB,CACtB,iBAAkB,CAElB,QAAS,CADT,aAEF,CAEA,iCACE,kBACF,CAEA,8EAGE,UACF,CAEA,wBACE,gBACF,CAEA,qBAEE,aAAc,CADd,eAAgB,CAEhB,mBACF,CAEA,sBACE,kBACF,CAEA,+CAEE,aACF,CAEA,sBAEE,aAAc,CADd,eAEF,CAEA,sBAGE,kBAAmB,CAFnB,wBAAyB,CACzB,iBAAkB,CAElB,UAAY,CAIZ,cAAe,CADf,cAAe,CADf,eAAgB,CADhB,kBAIF,CAEA,qBACE,8BAAuB,CAAvB,sBACF,CAEA,+BACE,UACF","sources":["index.css"],"sourcesContent":["*,\n*::before,\n*::after {\n box-sizing: border-box;\n}\n\nbody {\n margin: 0;\n padding: 1rem;\n background: #f0f0f0;\n font-family: -apple-system, BlinkMacSystemFont, sans-serif;\n -webkit-font-smoothing: antialiased;\n -moz-osx-font-smoothing: grayscale;\n}\n\nh1 {\n margin: 1rem auto 2rem;\n max-width: 1000px;\n}\n\npre {\n font-size: 1rem;\n font-family: Menlo, monospace;\n word-wrap: break-word;\n white-space: pre-wrap;\n}\n\n.flex {\n margin: 0 auto;\n max-width: 1000px;\n display: flex;\n gap: 2rem;\n width: 100%;\n}\n\n.section {\n background: white;\n border: 1px solid #dedede;\n border-radius: 8px;\n padding: 2rem;\n}\n\n.form.section {\n flex: 0.5;\n}\n\n.results.section {\n flex: 1;\n}\n\n.advanced-form input,\n.advanced-form select,\n.advanced-form textarea {\n border: 1px solid #aaa;\n border-radius: 4px;\n padding: 0.5rem;\n margin: 0;\n}\n\n.advanced-form input[type='radio'] {\n margin-right: 0.5rem;\n}\n\n.advanced-form input[type='text'],\n.advanced-form textarea,\n.advanced-form select {\n width: 100%;\n}\n\n.advanced-form textarea {\n min-height: 100px;\n}\n\n.advanced-form label {\n font-weight: 600;\n display: block;\n margin-bottom: 0.5rem;\n}\n\n.advanced-form .field {\n margin-bottom: 1rem;\n}\n\n.advanced-form .required,\n.advanced-form .error {\n color: #d63642;\n}\n\n.advanced-form .error {\n margin: 0.25rem 0;\n display: block;\n}\n\n.advanced-form button {\n border: 1px solid #3642d6;\n border-radius: 4px;\n background: #3642d6;\n color: white;\n padding: 0.5rem 1rem;\n font-weight: 600;\n font-size: 1rem;\n cursor: pointer;\n}\n\n.advanced-form:hover {\n filter: brightness(1.1);\n}\n\n.advanced-form button:disabled {\n opacity: 0.5;\n}\n"],"names":[],"sourceRoot":""} -------------------------------------------------------------------------------- /docs/static/js/main.f57bf6a8.js.LICENSE.txt: -------------------------------------------------------------------------------- 1 | /* 2 | object-assign 3 | (c) Sindre Sorhus 4 | @license MIT 5 | */ 6 | 7 | /** @license React v0.20.2 8 | * scheduler.production.min.js 9 | * 10 | * Copyright (c) Facebook, Inc. and its affiliates. 11 | * 12 | * This source code is licensed under the MIT license found in the 13 | * LICENSE file in the root directory of this source tree. 14 | */ 15 | 16 | /** @license React v16.13.1 17 | * react-is.production.min.js 18 | * 19 | * Copyright (c) Facebook, Inc. and its affiliates. 20 | * 21 | * This source code is licensed under the MIT license found in the 22 | * LICENSE file in the root directory of this source tree. 23 | */ 24 | 25 | /** @license React v17.0.2 26 | * react-dom.production.min.js 27 | * 28 | * Copyright (c) Facebook, Inc. and its affiliates. 29 | * 30 | * This source code is licensed under the MIT license found in the 31 | * LICENSE file in the root directory of this source tree. 32 | */ 33 | 34 | /** @license React v17.0.2 35 | * react-jsx-runtime.production.min.js 36 | * 37 | * Copyright (c) Facebook, Inc. and its affiliates. 38 | * 39 | * This source code is licensed under the MIT license found in the 40 | * LICENSE file in the root directory of this source tree. 41 | */ 42 | 43 | /** @license React v17.0.2 44 | * react.production.min.js 45 | * 46 | * Copyright (c) Facebook, Inc. and its affiliates. 47 | * 48 | * This source code is licensed under the MIT license found in the 49 | * LICENSE file in the root directory of this source tree. 50 | */ 51 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-advanced-form", 3 | "version": "0.1.0", 4 | "private": false, 5 | "dependencies": { 6 | "formik": "^2.2.9", 7 | "react": "^17.0.2", 8 | "react-dom": "^17.0.2", 9 | "react-scripts": "5.0.0", 10 | "yup": "^0.32.11" 11 | }, 12 | "scripts": { 13 | "start": "react-scripts start", 14 | "build": "react-scripts build", 15 | "test": "react-scripts test", 16 | "eject": "react-scripts eject" 17 | }, 18 | "eslintConfig": { 19 | "extends": [ 20 | "react-app", 21 | "react-app/jest" 22 | ] 23 | }, 24 | "browserslist": { 25 | "production": [ 26 | ">0.2%", 27 | "not dead", 28 | "not op_mini all" 29 | ], 30 | "development": [ 31 | "last 1 chrome version", 32 | "last 1 firefox version", 33 | "last 1 safari version" 34 | ] 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | Advanced Form 9 | 10 | 11 | 12 |
13 | 14 | 15 | -------------------------------------------------------------------------------- /public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/taniarascia/react-advanced-form/7ba60565d478d1566be378144adb99df03b86698/screenshot.png -------------------------------------------------------------------------------- /src/App.js: -------------------------------------------------------------------------------- 1 | import { useState } from 'react' 2 | import { AdvancedForm } from './components/forms/AdvancedForm' 3 | 4 | export default function App() { 5 | const [formValues, setFormValues] = useState([]) 6 | 7 | const handleSubmit = async (values, { setSubmitting }) => { 8 | setSubmitting(true) 9 | setFormValues(values) 10 | await new Promise((r) => setTimeout(r, 1000)) 11 | setSubmitting(false) 12 | } 13 | 14 | const formSchema = [ 15 | { name: 'name', label: 'Name', componentType: 'text', required: true }, 16 | { name: 'playable', label: 'Playable', componentType: 'checkbox' }, 17 | { 18 | name: 'race', 19 | label: 'Race', 20 | componentType: 'radioGroup', 21 | options: [ 22 | { label: 'Human', value: 'human' }, 23 | { label: 'Dwarf', value: 'dwarf' }, 24 | { label: 'Elf', value: 'elf' }, 25 | ], 26 | }, 27 | { 28 | name: 'class', 29 | label: 'Class', 30 | componentType: 'select', 31 | options: [ 32 | { label: 'Ranger', value: 'ranger' }, 33 | { label: 'Wizard', value: 'wizard' }, 34 | { label: 'Healer', value: 'healer' }, 35 | ], 36 | }, 37 | { 38 | name: 'spell', 39 | label: 'Spell', 40 | componentType: 'select', 41 | options: [ 42 | { label: 'Fire', value: 'fire' }, 43 | { label: 'Ice', value: 'ice' }, 44 | ], 45 | condition: { key: 'class', value: 'wizard', operator: '=' }, 46 | }, 47 | { 48 | name: 'description', 49 | label: 'Description', 50 | componentType: 'textarea', 51 | }, 52 | ] 53 | 54 | return ( 55 | <> 56 |

Advanced Form

57 | 58 |
59 |
60 | 61 |
62 |
63 |
{JSON.stringify(formValues, null, 2)}
64 |
65 |
66 | 67 | ) 68 | } 69 | -------------------------------------------------------------------------------- /src/components/forms/AdvancedForm.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { Formik, Field } from 'formik' 3 | 4 | import { 5 | Checkbox, 6 | Select, 7 | TextArea, 8 | TextField, 9 | RadioGroup, 10 | ConditionalField, 11 | } from '.' 12 | import { 13 | getInitialValues, 14 | getDefaultValues, 15 | getValidationSchema, 16 | } from './helpers' 17 | 18 | const components = [ 19 | { componentType: 'text', component: TextField }, 20 | { componentType: 'textarea', component: TextArea }, 21 | { componentType: 'select', component: Select }, 22 | { componentType: 'checkbox', component: Checkbox }, 23 | { componentType: 'radioGroup', component: RadioGroup }, 24 | ] 25 | 26 | export const AdvancedForm = ({ 27 | schema, 28 | onSubmit, 29 | initialValues, 30 | onClose, 31 | buttonLabel = 'Submit', 32 | ...props 33 | }) => { 34 | const defaultValues = getDefaultValues(schema) 35 | const validationSchema = getValidationSchema(schema) 36 | 37 | return ( 38 | 45 | {({ 46 | handleSubmit, 47 | isSubmitting, 48 | isValid, 49 | setFieldValue, 50 | setFieldTouched, 51 | values, 52 | }) => { 53 | return ( 54 |
55 | {schema.map(({ componentType, condition, ...formSchema }) => { 56 | if ( 57 | !components.some( 58 | (component) => component.componentType === componentType, 59 | ) 60 | ) { 61 | return null 62 | } 63 | 64 | const Component = components.find( 65 | (component) => component.componentType === componentType, 66 | ).component 67 | 68 | if (condition) { 69 | return ( 70 | { 10 | return ( 11 | 12 | 20 | 21 | ) 22 | } 23 | -------------------------------------------------------------------------------- /src/components/forms/ConditionalField.js: -------------------------------------------------------------------------------- 1 | import { useEffect } from 'react' 2 | 3 | export const ConditionalField = ({ show, onCollapse, onShow, children }) => { 4 | useEffect(() => { 5 | if (show) { 6 | onShow() 7 | } else { 8 | onCollapse() 9 | } 10 | // eslint-disable-next-line react-hooks/exhaustive-deps 11 | }, [show]) 12 | 13 | return show ? children : null 14 | } 15 | -------------------------------------------------------------------------------- /src/components/forms/FormField.js: -------------------------------------------------------------------------------- 1 | export const FormField = ({ 2 | id, 3 | label, 4 | required, 5 | children, 6 | formProps: { touched, errors }, 7 | }) => { 8 | const hasError = errors[id] && touched[id] 9 | 10 | return ( 11 |
12 | 16 | {children} 17 | {hasError && {errors[id]}} 18 |
19 | ) 20 | } 21 | -------------------------------------------------------------------------------- /src/components/forms/RadioGroup.js: -------------------------------------------------------------------------------- 1 | import { FormField } from './FormField' 2 | 3 | export const RadioGroup = ({ 4 | label, 5 | field: { name, value, ...fieldProps }, 6 | form, 7 | required, 8 | options, 9 | ...props 10 | }) => { 11 | return ( 12 | 13 | {options.map((option) => ( 14 |
15 | 25 | {option.label} 26 |
27 | ))} 28 |
29 | ) 30 | } 31 | -------------------------------------------------------------------------------- /src/components/forms/Select.js: -------------------------------------------------------------------------------- 1 | import { FormField } from './FormField' 2 | 3 | export const Select = ({ 4 | label, 5 | field: { name, value, ...fieldProps }, 6 | form, 7 | required, 8 | options, 9 | ...props 10 | }) => { 11 | return ( 12 | 13 | 21 | 22 | ) 23 | } 24 | -------------------------------------------------------------------------------- /src/components/forms/TextArea.js: -------------------------------------------------------------------------------- 1 | import { FormField } from './FormField' 2 | 3 | export const TextArea = ({ 4 | label, 5 | field: { name, value, ...fieldProps }, 6 | form, 7 | required, 8 | ...props 9 | }) => { 10 | return ( 11 | 12 |