├── .babelrc ├── .eslintrc.js ├── .gitignore ├── .npmignore ├── .nvmrc ├── .travis.yml ├── .unibeautifyrc.yml ├── README.md ├── demo ├── DemoHome.jsx ├── MuiRoot.jsx ├── Root.jsx ├── body │ ├── Body.jsx │ ├── Example.jsx │ ├── Source.jsx │ ├── body-styles.js │ ├── editor-styles.js │ ├── example-data.js │ ├── example-styles.js │ └── index.js ├── examples │ ├── arrays.bak │ │ ├── form-data.json │ │ ├── index.js │ │ ├── schema.json │ │ └── ui-schema.json │ ├── arrays │ │ ├── form-data.json │ │ ├── index.js │ │ ├── schema.json │ │ └── ui-schema.json │ ├── budget │ │ ├── form-data.json │ │ ├── index.js │ │ ├── schema.json │ │ └── ui-schema.json │ ├── custom-errorlist │ │ ├── form-data.json │ │ ├── index.jsx │ │ ├── schema.json │ │ └── ui-schema.json │ ├── index.js │ ├── multiple-choice │ │ ├── form-data.json │ │ ├── index.js │ │ ├── schema.json │ │ └── ui-schema.json │ ├── nested │ │ ├── form-data.json │ │ ├── index.js │ │ ├── schema.json │ │ └── ui-schema.json │ ├── numbers │ │ ├── form-data.json │ │ ├── index.js │ │ ├── nested │ │ │ ├── form-data.json │ │ │ ├── index.js │ │ │ ├── schema.json │ │ │ └── ui-schema.json │ │ ├── schema.json │ │ └── ui-schema.json │ ├── radio-choice │ │ ├── form-data.json │ │ ├── index.js │ │ ├── schema.json │ │ └── ui-schema.json │ ├── simple │ │ ├── form-data.json │ │ ├── index.js │ │ ├── schema.json │ │ └── ui-schema.json │ ├── single │ │ ├── form-data.json │ │ ├── index.js │ │ ├── schema.json │ │ └── ui-schema.json │ └── validation │ │ ├── form-data.json │ │ ├── index.js │ │ ├── schema.json │ │ └── ui-schema.json ├── index.html ├── index.jsx ├── main.scss ├── menu │ ├── LeftDrawer.jsx │ ├── Menu.jsx │ ├── MenuItems.jsx │ ├── index.js │ └── menu-styles.js ├── server.js ├── server.main.js └── theme.js ├── gulpfile.js ├── index.js ├── jsdom-setup.js ├── mocha.setup.js ├── package-lock.json ├── package.json ├── src ├── ErrorList.jsx ├── FieldSet │ ├── FieldSet.jsx │ ├── FieldSet.spec.jsx │ ├── FieldSetArray.jsx │ ├── FieldSetObject.jsx │ ├── ReorderControls.jsx │ ├── ReorderableFormField.jsx │ ├── field-set-styles.js │ └── index.js ├── Form.jsx ├── FormButtons.jsx ├── FormField.jsx ├── FormField.spec.jsx ├── fields │ ├── ConfiguredField.jsx │ ├── Field.jsx │ ├── Field.spec.jsx │ ├── components │ │ ├── Checkbox.jsx │ │ ├── Checkbox.spec.jsx │ │ ├── PopoverInfo.jsx │ │ ├── RadioGroup.jsx │ │ ├── Select.jsx │ │ ├── dom-events.spec.jsx │ │ └── index.js │ ├── configure │ │ ├── configure-component.js │ │ ├── configure-component.spec.js │ │ ├── field-styles.js │ │ ├── get-component-props.js │ │ ├── get-component.js │ │ ├── get-component.props.spec.js │ │ ├── get-component.spec.js │ │ ├── get-input-type.js │ │ ├── get-label-component-props.js │ │ ├── get-label-component-props.spec.js │ │ ├── get-label-component.js │ │ ├── get-label-component.spec.js │ │ ├── get-mui-props.js │ │ ├── index.js │ │ ├── values-to-options.js │ │ └── values-to-options.spec.js │ ├── field-styles.js │ └── index.js ├── form-field-styles.js ├── form-styles.js ├── helpers │ ├── get-default-value.js │ ├── get-default-value.spec.js │ ├── update-form-data.js │ ├── update-form-data.spec.js │ └── validation │ │ ├── get-validation-result.js │ │ ├── get-validation-result.spec.js │ │ ├── index.js │ │ └── rules │ │ ├── index.js │ │ ├── max-length.js │ │ ├── maximum.js │ │ ├── min-length.js │ │ ├── minimum.js │ │ └── pattern.js └── index.js └── webpack.config.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "development": { 4 | "plugins": [ 5 | "react-hot-loader/babel" 6 | ] 7 | } 8 | }, 9 | "presets": [ 10 | "@babel/preset-env", 11 | "@babel/preset-react" 12 | ], 13 | "plugins": [ 14 | "@babel/plugin-syntax-dynamic-import", 15 | "@babel/plugin-syntax-import-meta", 16 | "@babel/plugin-proposal-class-properties", 17 | "@babel/plugin-proposal-json-strings", 18 | [ 19 | "@babel/plugin-proposal-decorators", 20 | { 21 | "legacy": true 22 | } 23 | ], 24 | "@babel/plugin-proposal-function-sent", 25 | "@babel/plugin-proposal-export-namespace-from", 26 | "@babel/plugin-proposal-numeric-separator", 27 | "@babel/plugin-proposal-throw-expressions", 28 | "@babel/plugin-proposal-export-default-from", 29 | "@babel/plugin-proposal-logical-assignment-operators", 30 | "@babel/plugin-proposal-optional-chaining", 31 | [ 32 | "@babel/plugin-proposal-pipeline-operator", 33 | { 34 | "proposal": "minimal" 35 | } 36 | ], 37 | "@babel/plugin-proposal-nullish-coalescing-operator", 38 | "@babel/plugin-proposal-do-expressions", 39 | "@babel/plugin-proposal-function-bind" 40 | ] 41 | } 42 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: ['airbnb', 'prettier', 'prettier/react'], 3 | plugins: ['prettier'], 4 | rules: { 5 | 'react/jsx-filename-extension': [ 6 | 1, 7 | { 8 | extensions: ['.js', '.jsx'] 9 | } 10 | ], 11 | 'react/prop-types': 0, 12 | 'react/destructuring-assignment': 1, 13 | 'no-underscore-dangle': 0, 14 | 'import/imports-first': ['error', 'absolute-first'], 15 | 'import/newline-after-import': 'error' 16 | }, 17 | parser: 'babel-eslint' 18 | }; 19 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | dist 2 | test*.js 3 | node_modules 4 | mochawesome-report-unit 5 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | *.spec.js 2 | .babelrc 3 | .eslintrc.js 4 | gulpfile.js 5 | jsdom-setup.js 6 | mocha.setup.js 7 | webpack.config.js 8 | 9 | demo 10 | mochawesome-report-unit 11 | node_modules 12 | src 13 | -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | v10 2 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | sudo: false 3 | node_js: 4 | - "10" 5 | install: 6 | - npm ci 7 | # Keep the npm cache around to speed up installs 8 | cache: 9 | directories: 10 | - "$HOME/.npm" 11 | script: 12 | - npm run lint 13 | - npm run test 14 | -------------------------------------------------------------------------------- /.unibeautifyrc.yml: -------------------------------------------------------------------------------- 1 | TypeScript: 2 | beautifiers: ["Prettier"] 3 | indent_style: tab 4 | end_with_semicolon: true 5 | quotes: single 6 | end_with_comma: true 7 | 8 | javascriptreact: 9 | beautifiers: ["Prettier"] 10 | indent_style: tab 11 | end_with_semicolon: true 12 | quotes: single 13 | end_with_comma: true 14 | 15 | JavaScript: 16 | beautifiers: ["Prettier"] 17 | indent_style: tab 18 | end_with_semicolon: true 19 | quotes: single 20 | end_with_comma: true 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # [jsonschema-form-for-material-ui](https://www.npmjs.com/package/jsonschema-form-for-material-ui) 2 | 3 | A [Material UI](http://www.material-ui.com/) port of [react-jsonschema-form](https://github.com/mozilla-services/react-jsonschema-form). 4 | 5 | [Project](https://github.com/alphaeadevelopment/material-ui-jsonschema-form) forked from [Graham King](https://github.com/alphaeadevelopment). 6 | 7 | The initial project had lots of stuff in it, including a strict node version and demo server. 8 | 9 | This package: 10 | 11 | - Updated packages 12 | - Not pre-bundled 13 | - Better layout 14 | - Will be supported and updated 15 | 16 | I will be monitoring Mozilla's repo for changes: they plan on becoming ui-agnostic, and I will migrate this project into a wrapper of that one when that happens (which will be a major version bump) 17 | 18 | _Update_: Mozilla is doing something similar: See https://github.com/mozilla-services/react-jsonschema-form/issues/1222 for details 19 | 20 | # Installation 21 | 22 | ``` 23 | npm install --save jsonschema-form-for-material-ui 24 | ``` 25 | 26 | # Usage 27 | 28 | ```js 29 | import SchemaForm from 'jsonschema-form-for-material-ui'; 30 | 31 | const styles = theme => ({ 32 | field: {}, 33 | formButtons: {}, 34 | root: {}, 35 | }); 36 | 37 | const schema = { 38 | "title": "A registration form", 39 | "description": "A simple form example.", 40 | "type": "object", 41 | "required": [ 42 | "firstName", 43 | "lastName" 44 | ], 45 | "properties": { 46 | "firstName": { 47 | "type": "string", 48 | "title": "First name" 49 | }, 50 | "lastName": { 51 | "type": "string", 52 | "title": "Last name" 53 | }, 54 | "age": { 55 | "type": "integer", 56 | "title": "Age" 57 | } 58 | } 59 | } 60 | 61 | const uiSchema = { 62 | "firstName": { 63 | "ui:autofocus": true, 64 | "ui:emptyValue": "" 65 | }, 66 | "age": { 67 | "ui:widget": "updown", 68 | "ui:title": "Age of person", 69 | "ui:description": "This description will be in a Popover" 70 | } 71 | } 72 | 73 | const initialFormData = { 74 | "firstName": "Chuck", 75 | "lastName": "Norris", 76 | "age": 75, 77 | } 78 | 79 | 88 | ``` 89 | 90 | # API 91 | 92 | | Prop | Description | 93 | | --------------- | ---------------------------------------------------------------------------------------- | 94 | | schema | The JSON Schema that will be the base of the form | 95 | | classes | `withStyles()` classes that get passed to root components for better styling of the form | 96 | | uiSchema | Extra styling for fields.
Each key references one schema key
| 97 | | formData | The initial data with which to populate the form | 98 | | onCancel | Called when the `Cancel` button is pressed | 99 | | onSubmit | Called when the `Submit` button is pressed | 100 | | onChange | Called when form data is changed | 101 | | cancelText | Text for the `Cancel` button (`Cancel` by default) | 102 | | submitText | Text for the `Submit` button (`Submit` by default) | 103 | | showErrorList | Boolean to display the error list | 104 | | showHelperError | Boolean to display error in FormHelperText | 105 | 106 | ## Classes 107 | 108 | | name | element | 109 | | ----------- | ------------------------------- | 110 | | root | The surrounding `Paper` element | 111 | | field | Fields container | 112 | | formButtons | Button div | 113 | | button | Cancel/Submit form button | 114 | | cancel | Cancel form button | 115 | | submit | Submit form button | 116 | 117 | ## In-depth prop descriptions 118 | 119 | ### uiSchema 120 | 121 | #### ui:widget - `string` 122 | 123 | This setting handles the input type that will be shown. 124 | 125 | Default - `textarea` 126 | 127 | - radio 128 | - updown 129 | - password 130 | - textarea 131 | - checkboxes 132 | - ~~alt-datetime (~~`todo`~~)~~ 133 | 134 | #### ui:title - `string` 135 | 136 | Title of field that will be shown 137 | 138 | #### ui:description - `string` 139 | 140 | The description text that will be shown when hovering on the info icon 141 | 142 | #### ui:options - `object` 143 | 144 | inline - `boolean` 145 | 146 | disabled - `boolean` 147 | 148 | disabled - `function(data, objectData)` should return boolean 149 | 150 | ~~inputType (~~`todo`~~) - Format the input to a specific type (e.g. Phone, Credit Card, Date, etc)~~ 151 | 152 | #### ui:help - `string` 153 | 154 | Help text that will be shown below the input 155 | 156 | #### ui:orientation - `string` 157 | 158 | row 159 | 160 | default 161 | 162 | # Contributing 163 | 164 | Issues and pull requests welcome! 165 | 166 | Give the initial author credit, too. 167 | -------------------------------------------------------------------------------- /demo/DemoHome.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { withStyles } from '@material-ui/core/styles'; 3 | import Menu from './menu'; 4 | import Body from './body'; 5 | import './main.scss'; // eslint-disable-line import/no-unresolved,import/no-extraneous-dependencies 6 | import examples from './examples'; 7 | 8 | const styles = ({}); 9 | 10 | class Demo extends React.Component { 11 | state = { 12 | selectedDemo: examples.single, 13 | } 14 | 15 | onSelectMenuItem = type => () => { 16 | this.setState({ selectedDemo: type }); 17 | } 18 | 19 | render() { 20 | const { classes } = this.props; 21 | const { selectedDemo } = this.state; 22 | return ( 23 |
24 | 25 | 26 |
27 | ); 28 | } 29 | } 30 | 31 | export default withStyles(styles)(Demo); 32 | -------------------------------------------------------------------------------- /demo/MuiRoot.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import 'typeface-roboto'; // eslint-disable-line import/extensions 3 | import CssBaseline from '@material-ui/core/CssBaseline'; 4 | 5 | import { MuiThemeProvider } from '@material-ui/core/styles'; 6 | import theme from './theme'; 7 | 8 | export default ({ children }) => ( 9 | 10 | 11 | {children} 12 | 13 | ); 14 | -------------------------------------------------------------------------------- /demo/Root.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import MuiRoot from './MuiRoot'; 3 | import DemoHome from './DemoHome'; 4 | 5 | export default () => ( 6 |
7 | 8 | 9 | 10 |
11 | ); 12 | -------------------------------------------------------------------------------- /demo/body/Body.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { withStyles } from '@material-ui/core/styles'; 3 | import styles from './body-styles'; 4 | import Example from './Example'; 5 | 6 | export default withStyles(styles)(({ selectedDemo, classes }) => ( 7 |
8 | {} 9 |
10 | )); 11 | -------------------------------------------------------------------------------- /demo/body/Example.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { createStyles, withStyles } from '@material-ui/core/styles'; 3 | import Paper from '@material-ui/core/Paper'; 4 | import styles from './example-styles'; 5 | import Source from './Source'; 6 | import Form from '../../src/Form'; 7 | 8 | const formStyles = theme => createStyles({ 9 | field: { 10 | paddingLeft: theme.spacing.unit * 4, 11 | }, 12 | formButtons: { 13 | order: 2, 14 | }, 15 | root: { 16 | display: 'flex', 17 | padding: theme.spacing.unit, 18 | }, 19 | }); 20 | 21 | class Example extends React.Component { 22 | state = { 23 | ...this.props.data, // eslint-disable-line react/destructuring-assignment 24 | } 25 | 26 | componentWillReceiveProps = ({ data }) => { 27 | this.setState({ 28 | ...data, 29 | }); 30 | } 31 | 32 | onChange = type => (value) => { 33 | this.setState({ 34 | [type]: value, 35 | }); 36 | } 37 | 38 | onFormChanged = ({ formData }) => { 39 | this.setState({ formData }); 40 | } 41 | 42 | onSubmit = (value) => { 43 | console.log('onSubmit: %s', JSON.stringify(value)); // eslint-disable-line no-console 44 | } 45 | 46 | onCancel = () => { 47 | const { data } = this.props; 48 | this.setState({ 49 | ...data, 50 | }); 51 | } 52 | 53 | render() { 54 | const { data, classes } = this.props; 55 | const { title, showErrorList, showHelperError, ErrorList } = data; 56 | const { schema, uiSchema, formData } = this.state; 57 | return ( 58 | 59 |

{title}

60 |
61 |
62 |
63 | 64 |
65 |
66 | 67 | 68 |
69 |
70 |
71 |
85 |
86 |
87 |
88 | ); 89 | } 90 | } 91 | export default withStyles(styles)(Example); 92 | -------------------------------------------------------------------------------- /demo/body/Source.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import classNames from 'classnames'; 3 | import { Controlled as CodeMirror } from 'react-codemirror2'; 4 | import 'codemirror/lib/codemirror.css'; 5 | import 'codemirror/theme/material.css'; 6 | import 'codemirror/mode/javascript/javascript'; // eslint-disable-line 7 | import Valid from '@material-ui/icons/CheckCircle'; 8 | import Invalid from '@material-ui/icons/HighlightOff'; 9 | import { withStyles } from '@material-ui/core/styles'; 10 | import sourceStyles from './editor-styles'; 11 | 12 | const cmOptions = { 13 | // mode: { name: 'javascript', json: true }, 14 | // theme: 'material', 15 | smartIndent: true, 16 | lineNumbers: true, 17 | lineWrapping: true, 18 | readOnly: false, 19 | }; 20 | 21 | const isValid = (value) => { 22 | let obj; 23 | try { 24 | obj = JSON.parse(value); 25 | } 26 | catch (e) { 27 | return false; 28 | } 29 | return obj; 30 | }; 31 | 32 | class Source extends React.Component { 33 | constructor(props) { 34 | super(props); 35 | const source = JSON.stringify(this.props.source, null, 2); 36 | this.state = { 37 | source, 38 | valid: isValid(source), 39 | }; 40 | } 41 | 42 | componentWillReceiveProps = (nextProps) => { 43 | const source = JSON.stringify(nextProps.source, null, 2); 44 | this.setState({ 45 | source, 46 | valid: isValid(source), 47 | }); 48 | } 49 | 50 | onChange = (editor, data, value) => { 51 | this.setState({ source: value }); 52 | } 53 | 54 | onBeforeChange = (editor, data, value) => { 55 | const { onChange } = this.props; 56 | const parsed = isValid(value); 57 | 58 | this.setState({ 59 | valid: parsed, 60 | source: value, 61 | }); 62 | if (parsed && onChange) { 63 | onChange(parsed); 64 | } 65 | } 66 | 67 | render() { 68 | const { source, valid } = this.state; 69 | const { classes, title } = this.props; 70 | const Icon = valid ? Valid : Invalid; 71 | return ( 72 |
73 |
74 |
75 | 76 |
77 |

{title}

78 |
79 |
80 |
81 | 87 |
88 |
89 |
90 | ); 91 | } 92 | } 93 | 94 | export default withStyles(sourceStyles)(Source); 95 | -------------------------------------------------------------------------------- /demo/body/body-styles.js: -------------------------------------------------------------------------------- 1 | export default theme => ({ 2 | body: { 3 | 'padding': theme.spacing.unit * 2, 4 | [theme.breakpoints.up('lg')]: { 5 | width: 'calc(100% - 250px)', 6 | marginLeft: 250, 7 | }, 8 | }, 9 | }); 10 | -------------------------------------------------------------------------------- /demo/body/editor-styles.js: -------------------------------------------------------------------------------- 1 | export default theme => ({ 2 | 'root': { 3 | '& $ctr': { 4 | 'borderStyle': 'solid', 5 | 'borderWidth': 1, 6 | 'borderColor': theme.palette.grey[500], 7 | 'borderRadius': '5px', 8 | 'flexDirection': 'column', 9 | 'display': 'flex', 10 | '&$invalid': { 11 | '& $icon': { 12 | color: 'red', 13 | }, 14 | }, 15 | '& $icon': { 16 | color: 'green', 17 | }, 18 | '& >div:first-child': { 19 | 'display': 'flex', 20 | 'alignItems': 'center', 21 | 'borderBottomStyle': 'solid', 22 | 'borderBottomWidth': 1, 23 | 'borderColor': theme.palette.grey[500], 24 | 'backgroundColor': theme.palette.grey[300], 25 | }, 26 | }, 27 | }, 28 | 29 | 'icon': { 30 | fontSize: 'inherit', 31 | marginLeft: theme.spacing.unit * 2, 32 | }, 33 | 'title': { 34 | 'marginLeft': theme.spacing.unit * 2, 35 | }, 36 | 'invalid': { 37 | 38 | }, 39 | 'ctr': {}, 40 | }); 41 | -------------------------------------------------------------------------------- /demo/body/example-data.js: -------------------------------------------------------------------------------- 1 | import examples from '../examples'; 2 | 3 | const { simple, nested } = examples; 4 | 5 | export default ({ 6 | simple: { 7 | title: 'Simple', 8 | examples: [ 9 | simple, 10 | ], 11 | }, 12 | nested: { 13 | title: 'Nested', 14 | examples: [ 15 | nested, 16 | ], 17 | }, 18 | }); 19 | -------------------------------------------------------------------------------- /demo/body/example-styles.js: -------------------------------------------------------------------------------- 1 | export default theme => ({ 2 | 'root': { 3 | 'padding': theme.spacing.unit, 4 | '& $ctr': { 5 | 'display': 'flex', 6 | '& $sourceCtr': { 7 | 'flex': 21, 8 | 'display': 'flex', 9 | 'marginRight': theme.spacing.unit, 10 | 'flexDirection': 'column', 11 | '& >div:first-child': { 12 | marginBottom: theme.spacing.unit, 13 | }, 14 | '& >div:nth-child(2)': { 15 | 'display': 'flex', 16 | '& >div:first-child': { 17 | flex: 13, 18 | marginRight: theme.spacing.unit, 19 | }, 20 | '& >div:nth-child(2)': { 21 | flex: 21, 22 | }, 23 | }, 24 | }, 25 | '& $display': { 26 | flex: 13, 27 | }, 28 | }, 29 | }, 30 | 'sourceCtr': {}, 31 | 'display': {}, 32 | 'ctr': {}, 33 | }); 34 | -------------------------------------------------------------------------------- /demo/body/index.js: -------------------------------------------------------------------------------- 1 | export { default } from './Body'; 2 | -------------------------------------------------------------------------------- /demo/examples/arrays.bak/form-data.json: -------------------------------------------------------------------------------- 1 | { 2 | "listOfStrings": [ 3 | "foo", 4 | "bar" 5 | ], 6 | "multipleChoicesList": [ 7 | "foo", 8 | "bar", 9 | "fuzz" 10 | ], 11 | "fixedItemsList": [ 12 | "Some text", 13 | true, 14 | 123 15 | ], 16 | "minItemsList": [ 17 | { 18 | "name": "Default name" 19 | }, 20 | { 21 | "name": "Default name" 22 | }, 23 | { 24 | "name": "Default name" 25 | } 26 | ], 27 | "defaultsAndMinItems": [ 28 | "carp", 29 | "trout", 30 | "bream", 31 | "unidentified", 32 | "unidentified" 33 | ], 34 | "nestedList": [ 35 | [ 36 | "lorem", 37 | "ipsum" 38 | ], 39 | [ 40 | "dolor" 41 | ] 42 | ], 43 | "unorderable": [ 44 | "one", 45 | "two" 46 | ], 47 | "unremovable": [ 48 | "one", 49 | "two" 50 | ], 51 | "noToolbar": [ 52 | "one", 53 | "two" 54 | ], 55 | "fixedNoToolbar": [ 56 | 42, 57 | true, 58 | "additional item one", 59 | "additional item two" 60 | ] 61 | } 62 | -------------------------------------------------------------------------------- /demo/examples/arrays.bak/index.js: -------------------------------------------------------------------------------- 1 | import schema from './schema.json'; 2 | import uiSchema from './ui-schema.json'; 3 | import formData from './form-data.json'; 4 | 5 | export default ({ 6 | title: 'Arrays', 7 | schema, 8 | uiSchema, 9 | formData, 10 | }); 11 | -------------------------------------------------------------------------------- /demo/examples/arrays.bak/schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "definitions": { 3 | "Thing": { 4 | "type": "object", 5 | "properties": { 6 | "name": { 7 | "type": "string", 8 | "default": "Default name" 9 | } 10 | } 11 | } 12 | }, 13 | "type": "object", 14 | "properties": { 15 | "listOfStrings": { 16 | "type": "array", 17 | "title": "A list of strings", 18 | "items": { 19 | "type": "string", 20 | "default": "bazinga" 21 | } 22 | }, 23 | "multipleChoicesList": { 24 | "type": "array", 25 | "title": "A multiple choices list", 26 | "items": { 27 | "type": "string", 28 | "enum": [ 29 | "foo", 30 | "bar", 31 | "fuzz", 32 | "qux" 33 | ] 34 | }, 35 | "uniqueItems": true 36 | }, 37 | "fixedItemsList": { 38 | "type": "array", 39 | "title": "A list of fixed items", 40 | "items": [ 41 | { 42 | "title": "A string value", 43 | "type": "string", 44 | "default": "lorem ipsum" 45 | }, 46 | { 47 | "title": "a boolean value", 48 | "type": "boolean" 49 | } 50 | ], 51 | "additionalItems": { 52 | "title": "Additional item", 53 | "type": "number" 54 | } 55 | }, 56 | "minItemsList": { 57 | "type": "array", 58 | "title": "A list with a minimal number of items", 59 | "minItems": 3, 60 | "items": { 61 | "$ref": "#/definitions/Thing" 62 | } 63 | }, 64 | "defaultsAndMinItems": { 65 | "type": "array", 66 | "title": "List and item level defaults", 67 | "minItems": 5, 68 | "default": [ 69 | "carp", 70 | "trout", 71 | "bream" 72 | ], 73 | "items": { 74 | "type": "string", 75 | "default": "unidentified" 76 | } 77 | }, 78 | "nestedList": { 79 | "type": "array", 80 | "title": "Nested list", 81 | "items": { 82 | "type": "array", 83 | "title": "Inner list", 84 | "items": { 85 | "type": "string", 86 | "default": "lorem ipsum" 87 | } 88 | } 89 | }, 90 | "unorderable": { 91 | "title": "Unorderable items", 92 | "type": "array", 93 | "items": { 94 | "type": "string", 95 | "default": "lorem ipsum" 96 | } 97 | }, 98 | "unremovable": { 99 | "title": "Unremovable items", 100 | "type": "array", 101 | "items": { 102 | "type": "string", 103 | "default": "lorem ipsum" 104 | } 105 | }, 106 | "noToolbar": { 107 | "title": "No add, remove and order buttons", 108 | "type": "array", 109 | "items": { 110 | "type": "string", 111 | "default": "lorem ipsum" 112 | } 113 | }, 114 | "fixedNoToolbar": { 115 | "title": "Fixed array without buttons", 116 | "type": "array", 117 | "items": [ 118 | { 119 | "title": "A number", 120 | "type": "number", 121 | "default": 42 122 | }, 123 | { 124 | "title": "A boolean", 125 | "type": "boolean", 126 | "default": false 127 | } 128 | ], 129 | "additionalItems": { 130 | "title": "A string", 131 | "type": "string", 132 | "default": "lorem ipsum" 133 | } 134 | } 135 | } 136 | } 137 | -------------------------------------------------------------------------------- /demo/examples/arrays.bak/ui-schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "listOfStrings": { 3 | "items": { 4 | "ui:emptyValue": "" 5 | } 6 | }, 7 | "multipleChoicesList": { 8 | "ui:widget": "checkboxes" 9 | }, 10 | "fixedItemsList": { 11 | "items": [ 12 | { 13 | "ui:widget": "textarea" 14 | }, 15 | { 16 | "ui:widget": "select" 17 | } 18 | ], 19 | "additionalItems": { 20 | "ui:widget": "updown" 21 | } 22 | }, 23 | "unorderable": { 24 | "ui:options": { 25 | "orderable": false 26 | } 27 | }, 28 | "unremovable": { 29 | "ui:options": { 30 | "removable": false 31 | } 32 | }, 33 | "noToolbar": { 34 | "ui:options": { 35 | "addable": false, 36 | "orderable": false, 37 | "removable": false 38 | } 39 | }, 40 | "fixedNoToolbar": { 41 | "ui:options": { 42 | "addable": false, 43 | "orderable": false, 44 | "removable": false 45 | } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /demo/examples/arrays/form-data.json: -------------------------------------------------------------------------------- 1 | { 2 | "fixedItemsList": [ 3 | "Some text", 4 | true, 5 | "one" 6 | ] 7 | } 8 | -------------------------------------------------------------------------------- /demo/examples/arrays/index.js: -------------------------------------------------------------------------------- 1 | import schema from './schema.json'; 2 | import uiSchema from './ui-schema.json'; 3 | import formData from './form-data.json'; 4 | 5 | export default ({ 6 | title: 'Arrays', 7 | schema, 8 | uiSchema, 9 | formData, 10 | }); 11 | -------------------------------------------------------------------------------- /demo/examples/arrays/schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "object", 3 | "properties": { 4 | "fixedItemsList": { 5 | "type": "array", 6 | "title": "A list of fixed items", 7 | "items": [ 8 | { 9 | "title": "A string value", 10 | "type": "string", 11 | "default": "lorem ipsum" 12 | }, 13 | { 14 | "title": "a boolean value", 15 | "type": "boolean" 16 | } 17 | ], 18 | "additionalItems": { 19 | "title": "Relations", 20 | "type": "string", 21 | "default": "" 22 | } 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /demo/examples/arrays/ui-schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "fixedItemsList": { 3 | "items": [ 4 | { 5 | "ui:widget": "textarea" 6 | }, 7 | { 8 | "ui:widget": "select" 9 | } 10 | ] 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /demo/examples/budget/form-data.json: -------------------------------------------------------------------------------- 1 | { 2 | "roles": [ 3 | { 4 | "location": "uk", 5 | "allocations": [ 6 | { 7 | "name": "jim", 8 | "role": "pm", 9 | "location": "in" 10 | } 11 | ] 12 | } 13 | ] 14 | } 15 | -------------------------------------------------------------------------------- /demo/examples/budget/index.js: -------------------------------------------------------------------------------- 1 | import schema from './schema.json'; 2 | import uiSchema from './ui-schema.json'; 3 | import formData from './form-data.json'; 4 | 5 | export default ({ 6 | title: 'Budget', 7 | schema, 8 | uiSchema, 9 | formData, 10 | }); 11 | -------------------------------------------------------------------------------- /demo/examples/budget/schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "object", 3 | "properties": { 4 | "roles": { 5 | "title": "Roles", 6 | "type": "array", 7 | "items": { 8 | "type": "object", 9 | "title": "Role", 10 | "default": { 11 | "allocations": [] 12 | }, 13 | "properties": { 14 | "location": { 15 | "title": "Location", 16 | "type": "string" 17 | }, 18 | "allocations": { 19 | "title": "Allocations", 20 | "type": "array", 21 | "items": { 22 | "default": {}, 23 | "type": "object", 24 | "properties": { 25 | "name": { 26 | "title": "Resource", 27 | "type": "string" 28 | }, 29 | "rate": { 30 | "title": "Rate", 31 | "type": "string" 32 | }, 33 | "location": { 34 | "title": "Location", 35 | "type": "string" 36 | } 37 | } 38 | } 39 | } 40 | } 41 | } 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /demo/examples/budget/ui-schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "roles": { 3 | "items": { 4 | "allocations": { 5 | "items": { 6 | "ui:orientation": "row" 7 | } 8 | } 9 | } 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /demo/examples/custom-errorlist/form-data.json: -------------------------------------------------------------------------------- 1 | { 2 | "age": 8 3 | } 4 | -------------------------------------------------------------------------------- /demo/examples/custom-errorlist/index.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import schema from './schema.json'; 3 | import uiSchema from './ui-schema.json'; 4 | import formData from './form-data.json'; 5 | 6 | 7 | function ErrorListTemplate({ errors }) { 8 | return ( 9 | 18 | ); 19 | } 20 | 21 | 22 | export default ({ 23 | title: 'Custom Error List', 24 | schema, 25 | uiSchema, 26 | formData, 27 | showErrorList: true, 28 | showHelperError: false, 29 | ErrorList: ErrorListTemplate, 30 | }); 31 | -------------------------------------------------------------------------------- /demo/examples/custom-errorlist/schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "title": "Custom Error List", 3 | "description": "This form has a custom error list component", 4 | "type": "object", 5 | "properties": { 6 | "pass1": { 7 | "title": "Password", 8 | "type": "string", 9 | "minLength": 20 10 | }, 11 | "pass2": { 12 | "title": "Repeat password", 13 | "type": "string", 14 | "minLength": 20 15 | }, 16 | "age": { 17 | "title": "Age", 18 | "type": "number", 19 | "minimum": 65 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /demo/examples/custom-errorlist/ui-schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "pass1": { 3 | "ui:widget": "password" 4 | }, 5 | "pass2": { 6 | "ui:widget": "password" 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /demo/examples/index.js: -------------------------------------------------------------------------------- 1 | import simple from './simple'; 2 | import nested from './nested'; 3 | import single from './single'; 4 | import numbers from './numbers'; 5 | import arrays from './arrays'; 6 | import validation from './validation'; 7 | import customErrorList from './custom-errorlist'; 8 | import budget from './budget'; 9 | import multipleChoice from './multiple-choice'; 10 | import radioChoice from './radio-choice'; 11 | // import nestedObject from './nestedObject'; 12 | 13 | export default ({ 14 | simple, 15 | single, 16 | nested, 17 | numbers, 18 | arrays, 19 | validation, 20 | customErrorList, 21 | budget, 22 | multipleChoice, 23 | radioChoice, 24 | // nestedObject, 25 | }); 26 | -------------------------------------------------------------------------------- /demo/examples/multiple-choice/form-data.json: -------------------------------------------------------------------------------- 1 | { 2 | "multipleChoicesList": [ 3 | "foo", 4 | "bar" 5 | ] 6 | } 7 | -------------------------------------------------------------------------------- /demo/examples/multiple-choice/index.js: -------------------------------------------------------------------------------- 1 | import schema from './schema.json'; 2 | import uiSchema from './ui-schema.json'; 3 | import formData from './form-data.json'; 4 | 5 | export default ({ 6 | title: 'Multiple Choice', 7 | schema, 8 | uiSchema, 9 | formData, 10 | }); 11 | -------------------------------------------------------------------------------- /demo/examples/multiple-choice/schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "object", 3 | "properties": { 4 | "multipleChoicesList": { 5 | "type": "array", 6 | "title": "A multiple choices list", 7 | "items": { 8 | "type": "string", 9 | "enum": [ 10 | "foo", 11 | "bar", 12 | "fuzz", 13 | "qux" 14 | ] 15 | }, 16 | "uniqueItems": true 17 | } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /demo/examples/multiple-choice/ui-schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "multipleChoicesList": { 3 | "ui:widget": "checkboxes" 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /demo/examples/nested/form-data.json: -------------------------------------------------------------------------------- 1 | { 2 | "title": "My current tasks", 3 | "tasks": [ 4 | { 5 | "title": "My first task", 6 | "details": "Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.", 7 | "done": true 8 | }, 9 | { 10 | "title": "My second task", 11 | "details": "Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur", 12 | "done": false 13 | } 14 | ] 15 | } 16 | -------------------------------------------------------------------------------- /demo/examples/nested/index.js: -------------------------------------------------------------------------------- 1 | import schema from './schema.json'; 2 | import uiSchema from './ui-schema.json'; 3 | import formData from './form-data.json'; 4 | 5 | export default ({ 6 | title: 'Nested', 7 | schema, 8 | uiSchema, 9 | formData, 10 | }); 11 | -------------------------------------------------------------------------------- /demo/examples/nested/schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "title": "A list of tasks", 3 | "type": "object", 4 | "required": [ 5 | "title" 6 | ], 7 | "properties": { 8 | "title": { 9 | "type": "string", 10 | "title": "Task list title" 11 | }, 12 | "tasks": { 13 | "type": "array", 14 | "title": "Tasks", 15 | "items": { 16 | "type": "object", 17 | "required": [ 18 | "title" 19 | ], 20 | "properties": { 21 | "title": { 22 | "type": "string", 23 | "title": "Title", 24 | "description": "A sample title" 25 | }, 26 | "details": { 27 | "type": "string", 28 | "title": "Task details", 29 | "description": "Enter the task details" 30 | }, 31 | "done": { 32 | "type": "boolean", 33 | "title": "Done?", 34 | "default": false 35 | } 36 | } 37 | } 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /demo/examples/nested/ui-schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "tasks": { 3 | "items": { 4 | "details": { 5 | "ui:widget": "textarea" 6 | } 7 | } 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /demo/examples/numbers/form-data.json: -------------------------------------------------------------------------------- 1 | { 2 | "number": 3.14, 3 | "integer": 42, 4 | "numberEnum": 2, 5 | "numberEnumRadio": 2, 6 | "integerRange": 42, 7 | "integerRangeSteps": 80 8 | } 9 | -------------------------------------------------------------------------------- /demo/examples/numbers/index.js: -------------------------------------------------------------------------------- 1 | import schema from './schema.json'; 2 | import uiSchema from './ui-schema.json'; 3 | import formData from './form-data.json'; 4 | 5 | export default ({ 6 | title: 'Numbers', 7 | schema, 8 | uiSchema, 9 | formData, 10 | }); 11 | -------------------------------------------------------------------------------- /demo/examples/numbers/nested/form-data.json: -------------------------------------------------------------------------------- 1 | { 2 | "title": "My current tasks", 3 | "tasks": [ 4 | { 5 | "title": "My first task", 6 | "details": "Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.", 7 | "done": true 8 | }, 9 | { 10 | "title": "My second task", 11 | "details": "Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur", 12 | "done": false 13 | } 14 | ] 15 | } 16 | -------------------------------------------------------------------------------- /demo/examples/numbers/nested/index.js: -------------------------------------------------------------------------------- 1 | import schema from './schema.json'; 2 | import uiSchema from './ui-schema.json'; 3 | import formData from './form-data.json'; 4 | 5 | export default ({ 6 | title: 'Nested', 7 | schema, 8 | uiSchema, 9 | formData, 10 | }); 11 | -------------------------------------------------------------------------------- /demo/examples/numbers/nested/schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "title": "A list of tasks", 3 | "type": "object", 4 | "required": [ 5 | "title" 6 | ], 7 | "properties": { 8 | "title": { 9 | "type": "string", 10 | "title": "Task list title" 11 | }, 12 | "tasks": { 13 | "type": "array", 14 | "title": "Tasks", 15 | "items": { 16 | "type": "object", 17 | "required": [ 18 | "title" 19 | ], 20 | "properties": { 21 | "title": { 22 | "type": "string", 23 | "title": "Title", 24 | "description": "A sample title" 25 | }, 26 | "details": { 27 | "type": "string", 28 | "title": "Task details", 29 | "description": "Enter the task details" 30 | }, 31 | "done": { 32 | "type": "boolean", 33 | "title": "Done?", 34 | "default": false 35 | } 36 | } 37 | } 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /demo/examples/numbers/nested/ui-schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "tasks": { 3 | "items": { 4 | "details": { 5 | "ui:widget": "textarea" 6 | } 7 | } 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /demo/examples/numbers/schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "object", 3 | "title": "Number fields & widgets", 4 | "properties": { 5 | "number": { 6 | "title": "Number", 7 | "type": "number" 8 | }, 9 | "integer": { 10 | "title": "Integer", 11 | "type": "integer" 12 | }, 13 | "numberEnum": { 14 | "type": "number", 15 | "title": "Number enum", 16 | "enum": [ 17 | 1, 18 | 2, 19 | 3 20 | ] 21 | }, 22 | "numberEnumRadio": { 23 | "type": "number", 24 | "title": "Number enum", 25 | "enum": [ 26 | 1, 27 | 2, 28 | 3 29 | ] 30 | }, 31 | "integerRange": { 32 | "title": "Integer range", 33 | "type": "integer", 34 | "minimum": 42, 35 | "maximum": 100 36 | }, 37 | "integerRangeSteps": { 38 | "title": "Integer range (by 10)", 39 | "type": "integer", 40 | "minimum": 50, 41 | "maximum": 100, 42 | "multipleOf": 10 43 | } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /demo/examples/numbers/ui-schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "integer": { 3 | "ui:widget": "updown" 4 | }, 5 | "numberEnumRadio": { 6 | "ui:widget": "radio", 7 | "ui:options": { 8 | "inline": true 9 | } 10 | }, 11 | "integerRange": { 12 | "ui:widget": "range" 13 | }, 14 | "integerRangeSteps": { 15 | "ui:widget": "range" 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /demo/examples/radio-choice/form-data.json: -------------------------------------------------------------------------------- 1 | { 2 | "numberEnumRadio": 3 3 | } 4 | -------------------------------------------------------------------------------- /demo/examples/radio-choice/index.js: -------------------------------------------------------------------------------- 1 | import schema from './schema.json'; 2 | import uiSchema from './ui-schema.json'; 3 | import formData from './form-data.json'; 4 | 5 | export default ({ 6 | title: 'Radio Choice', 7 | schema, 8 | uiSchema, 9 | formData, 10 | }); 11 | -------------------------------------------------------------------------------- /demo/examples/radio-choice/schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "object", 3 | "title": "Radio choice", 4 | "properties": { 5 | "numberEnumRadio": { 6 | "type": "number", 7 | "title": "Number enum", 8 | "enum": [ 9 | 1, 10 | 2, 11 | 3 12 | ] 13 | } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /demo/examples/radio-choice/ui-schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "numberEnumRadio": { 3 | "ui:widget": "radio", 4 | "ui:options": { 5 | "inline": true 6 | } 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /demo/examples/simple/form-data.json: -------------------------------------------------------------------------------- 1 | { 2 | "firstName": "Chuck", 3 | "lastName": "Norris", 4 | "age": 75, 5 | "bio": "Roundhouse kicking asses since 1940", 6 | "password": "noneed" 7 | } 8 | -------------------------------------------------------------------------------- /demo/examples/simple/index.js: -------------------------------------------------------------------------------- 1 | import schema from './schema.json'; 2 | import uiSchema from './ui-schema.json'; 3 | import formData from './form-data.json'; 4 | 5 | export default ({ 6 | title: 'Simple', 7 | schema, 8 | uiSchema, 9 | formData, 10 | }); 11 | -------------------------------------------------------------------------------- /demo/examples/simple/schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "title": "A registration form", 3 | "description": "A simple form example.", 4 | "type": "object", 5 | "required": [ 6 | "firstName", 7 | "lastName" 8 | ], 9 | "properties": { 10 | "firstName": { 11 | "type": "string", 12 | "title": "First name" 13 | }, 14 | "lastName": { 15 | "type": "string", 16 | "title": "Last name" 17 | }, 18 | "age": { 19 | "type": "integer", 20 | "title": "Age" 21 | }, 22 | "bio": { 23 | "type": "string", 24 | "title": "Bio" 25 | }, 26 | "password": { 27 | "type": "string", 28 | "title": "Password", 29 | "minLength": 3 30 | }, 31 | "telephone": { 32 | "type": "string", 33 | "title": "Telephone", 34 | "minLength": 10 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /demo/examples/simple/ui-schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "firstName": { 3 | "ui:autofocus": true, 4 | "ui:emptyValue": "" 5 | }, 6 | "age": { 7 | "ui:widget": "updown", 8 | "ui:title": "Age of person", 9 | "ui:description": "(earthian year)" 10 | }, 11 | "bio": { 12 | "ui:widget": "textarea" 13 | }, 14 | "password": { 15 | "ui:widget": "password", 16 | "ui:help": "Hint: Make it strong!" 17 | }, 18 | "date": { 19 | "ui:widget": "alt-datetime" 20 | }, 21 | "telephone": { 22 | "ui:options": { 23 | "inputType": "tel" 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /demo/examples/single/form-data.json: -------------------------------------------------------------------------------- 1 | "initial value" 2 | -------------------------------------------------------------------------------- /demo/examples/single/index.js: -------------------------------------------------------------------------------- 1 | import schema from './schema.json'; 2 | import uiSchema from './ui-schema.json'; 3 | import formData from './form-data.json'; 4 | 5 | export default ({ 6 | title: 'Single', 7 | schema, 8 | uiSchema, 9 | formData, 10 | }); 11 | -------------------------------------------------------------------------------- /demo/examples/single/schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "title": "A single-field form", 3 | "type": "string", 4 | "description": "Testing testing 123" 5 | } 6 | -------------------------------------------------------------------------------- /demo/examples/single/ui-schema.json: -------------------------------------------------------------------------------- 1 | {} 2 | -------------------------------------------------------------------------------- /demo/examples/validation/form-data.json: -------------------------------------------------------------------------------- 1 | { 2 | "age": 8 3 | } 4 | -------------------------------------------------------------------------------- /demo/examples/validation/index.js: -------------------------------------------------------------------------------- 1 | import schema from './schema.json'; 2 | import uiSchema from './ui-schema.json'; 3 | import formData from './form-data.json'; 4 | 5 | export default ({ 6 | title: 'Validation', 7 | schema, 8 | uiSchema, 9 | formData, 10 | }); 11 | -------------------------------------------------------------------------------- /demo/examples/validation/schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "title": "Custom validation", 3 | "description": "This form defines custom validation rules checking that the two passwords match.", 4 | "type": "object", 5 | "properties": { 6 | "pass1": { 7 | "title": "Password", 8 | "type": "string", 9 | "minLength": 3 10 | }, 11 | "pass2": { 12 | "title": "Repeat password", 13 | "type": "string", 14 | "minLength": 3 15 | }, 16 | "age": { 17 | "title": "Age", 18 | "type": "number", 19 | "minimum": 18 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /demo/examples/validation/ui-schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "pass1": { 3 | "ui:widget": "password" 4 | }, 5 | "pass2": { 6 | "ui:widget": "password" 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /demo/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 |
loading...
10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /demo/index.jsx: -------------------------------------------------------------------------------- 1 | /* globals document */ 2 | import React from 'react'; 3 | import { render } from 'react-dom'; 4 | import { AppContainer } from 'react-hot-loader'; 5 | import Root from './Root'; 6 | 7 | const doRender = Component => render( 8 | 9 | 10 | , 11 | document.getElementById('react-root'), 12 | ); 13 | 14 | doRender(Root); 15 | 16 | // Webpack Hot Module Replacement API 17 | if (module.hot) { 18 | module.hot.accept('./Root', () => { 19 | doRender(Root); 20 | }); 21 | } 22 | -------------------------------------------------------------------------------- /demo/main.scss: -------------------------------------------------------------------------------- 1 | body { 2 | padding: 0; 3 | } 4 | 5 | p { 6 | margin: 0; 7 | padding: 0; 8 | } 9 | -------------------------------------------------------------------------------- /demo/menu/LeftDrawer.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { withStyles } from '@material-ui/core/styles'; 3 | import Drawer from '@material-ui/core/Drawer'; 4 | import Hidden from '@material-ui/core/Hidden'; 5 | import MenuItems from './MenuItems'; 6 | import menuStyles from './menu-styles'; 7 | 8 | export default withStyles(menuStyles)(({ classes, open, toggleDrawer, onSelectMenuItem }) => ( 9 |
10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 |
21 | )); 22 | -------------------------------------------------------------------------------- /demo/menu/Menu.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { withStyles } from '@material-ui/core/styles'; 3 | import AppBar from '@material-ui/core/AppBar'; 4 | import Toolbar from '@material-ui/core/Toolbar'; 5 | import Typography from '@material-ui/core/Typography'; 6 | import Hidden from '@material-ui/core/Hidden'; 7 | import IconButton from '@material-ui/core/IconButton'; 8 | import MenuIcon from '@material-ui/icons/Menu'; 9 | import LeftDrawer from './LeftDrawer'; 10 | import menuStyles from './menu-styles'; 11 | 12 | class RawMenuAppBar extends React.Component { 13 | state = { 14 | drawerOpen: false 15 | }; 16 | 17 | toggleDrawer = visible => () => { 18 | this.setState({ drawerOpen: visible }); 19 | }; 20 | 21 | render() { 22 | const { classes, onSelectMenuItem } = this.props; 23 | const { drawerOpen } = this.state; 24 | return ( 25 | 26 | 27 | 28 | 34 | 35 | 36 | 37 |
38 | 39 | material-ui-jsonschema-form 40 | 41 |
42 |
43 | 48 |
49 | ); 50 | } 51 | } 52 | export default withStyles(menuStyles)(RawMenuAppBar); 53 | -------------------------------------------------------------------------------- /demo/menu/MenuItems.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import keys from 'lodash/keys'; 3 | import { withStyles } from '@material-ui/core/styles'; 4 | import List from '@material-ui/core/List'; 5 | import ListItem from '@material-ui/core/ListItem'; 6 | import ListItemText from '@material-ui/core/ListItemText'; 7 | import ListSubheader from '@material-ui/core/ListSubheader'; 8 | 9 | import examples from '../examples'; 10 | 11 | import menuStyles from './menu-styles'; 12 | 13 | export default withStyles(menuStyles)(({ toggleDrawer, classes, onSelectMenuItem }) => ( 14 |
21 | Showcase}> 22 | {keys(examples).map(e => ( 23 | 24 | 25 | 26 | ))} 27 | 28 |
29 | )); 30 | -------------------------------------------------------------------------------- /demo/menu/index.js: -------------------------------------------------------------------------------- 1 | export { default } from './Menu'; 2 | -------------------------------------------------------------------------------- /demo/menu/menu-styles.js: -------------------------------------------------------------------------------- 1 | 2 | export default theme => ({ 3 | // root: { 4 | // width: '100%', 5 | // }, 6 | // flex: { 7 | // flex: 1, 8 | // }, 9 | // drawerList: { 10 | // width: 250, 11 | // }, 12 | // flexCtr: { 13 | // display: 'flex', 14 | // alignItems: 'center', 15 | // width: '100%', 16 | // justifyContent: 'space-between', 17 | // }, 18 | // menuButton: { 19 | // marginLeft: -12, 20 | // marginRight: 20, 21 | // }, 22 | 23 | // projectList: { 24 | // display: 'flex', 25 | // }, 26 | // popperClose: { 27 | // pointerEvents: 'none', 28 | // }, 29 | permanentLeftDrawer: { 30 | }, 31 | drawerList: { 32 | width: 250, 33 | }, 34 | toolbar: { 35 | [theme.breakpoints.up('lg')]: { 36 | width: 'calc(100% - 250px)', 37 | marginLeft: 250, 38 | }, 39 | '& h2': { 40 | marginLeft: theme.spacing.unit * 3, 41 | }, 42 | }, 43 | }); 44 | -------------------------------------------------------------------------------- /demo/server.js: -------------------------------------------------------------------------------- 1 | require('@babel/register'); 2 | require('./server.main'); 3 | -------------------------------------------------------------------------------- /demo/server.main.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable global-require,import/no-extraneous-dependencies */ 2 | const express = require('express'); 3 | const path = require('path'); 4 | const bodyParser = require('body-parser'); 5 | 6 | const port = process.env.PORT || 3000; 7 | 8 | const app = express(); 9 | app.use(bodyParser.json()); 10 | 11 | if (process.env.NODE_ENV !== 'production') { 12 | const webpack = require('webpack'); 13 | const webpackConfig = require('../webpack.config.demo'); 14 | const webpackCompiler = webpack(webpackConfig); 15 | const webpackDevOptions = { 16 | noInfo: true, publicPath: webpackConfig.output.publicPath, 17 | }; 18 | app.use(require('webpack-dev-middleware')(webpackCompiler, webpackDevOptions)); 19 | app.use(require('webpack-hot-middleware')(webpackCompiler)); 20 | } 21 | // serve static files from webpack dist dir 22 | const publicPath = path.join(__dirname, '../dist'); 23 | app.use(express.static(publicPath)); 24 | 25 | // ping for load balancer checking health 26 | app.get('/ping', (req, res) => res.status(200).send()); 27 | 28 | app.listen(port, () => { 29 | console.log('Listening on %s', port); // eslint-disable-line no-console 30 | }); 31 | -------------------------------------------------------------------------------- /demo/theme.js: -------------------------------------------------------------------------------- 1 | import { createMuiTheme } from '@material-ui/core/styles'; 2 | import teal from '@material-ui/core/colors/teal'; 3 | import red from '@material-ui/core/colors/red'; 4 | import blue from '@material-ui/core/colors/lightBlue'; 5 | 6 | const theme = { 7 | palette: { 8 | primary: { 9 | main: blue[600] 10 | }, 11 | secondary: teal, 12 | error: red 13 | }, 14 | typography: { 15 | useNextVariants: true 16 | }, 17 | overrides: { 18 | MuiInput: { 19 | root: { 20 | fontSize: 'inherit' 21 | } 22 | } 23 | } 24 | }; 25 | 26 | export default createMuiTheme(theme); 27 | -------------------------------------------------------------------------------- /gulpfile.js: -------------------------------------------------------------------------------- 1 | const gulp = require('gulp'); 2 | const path = require('path'); 3 | const babel = require('gulp-babel'); 4 | const mocha = require('gulp-mocha'); 5 | 6 | gulp.task('build', () => 7 | gulp 8 | .src('src/**/*.{js,jsx}') 9 | .pipe( 10 | babel({ 11 | envName: process.env.NODE_ENV || 'production', 12 | configFile: './.babelrc' 13 | }) 14 | ) 15 | .pipe(gulp.dest('dist')) 16 | ); 17 | 18 | gulp.task( 19 | 'test', 20 | gulp.series(['build'], () => 21 | gulp.src('src/**/*.spec.{js,jsx}', { read: false }).pipe( 22 | mocha({ 23 | require: path.resolve(__dirname, 'mocha.setup.js'), 24 | reporter: 'mochawesome', 25 | reporterOptions: { 26 | reportDir: 'mochawesome-report-unit' 27 | } 28 | }) 29 | ) 30 | ) 31 | ); 32 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | module.exports = require('./dist').default; 2 | -------------------------------------------------------------------------------- /jsdom-setup.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable import/no-extraneous-dependencies */ 2 | 3 | // https://stackoverflow.com/questions/41194264/mocha-react-navigator-is-not-defined 4 | const { JSDOM } = require('jsdom'); 5 | 6 | const jsdom = new JSDOM(''); 7 | const { window } = jsdom; 8 | 9 | // function copyProps(src, target) { 10 | // const props = Object.getOwnPropertyNames(src) 11 | // .filter(prop => typeof target[prop] === 'undefined') 12 | // .reduce((result, prop) => ({ 13 | // ...result, 14 | // [prop]: Object.getOwnPropertyDescriptor(src, prop), 15 | // }), {}); 16 | // Object.defineProperties(target, props); 17 | // } 18 | 19 | global.window = window; 20 | global.document = window.document; 21 | 22 | global.navigator = { 23 | userAgent: 'node.js', 24 | }; 25 | require('raf').polyfill(global); 26 | -------------------------------------------------------------------------------- /mocha.setup.js: -------------------------------------------------------------------------------- 1 | // require('testdom')(''); 2 | require('./jsdom-setup'); 3 | require('@babel/register'); 4 | 5 | const gs = JSON.stringify; 6 | global.JSON_stringify = gs; 7 | 8 | function newStringify(val) { 9 | return gs(val, null, 2); 10 | } 11 | JSON.stringify = newStringify; // eslint-disable-line no-extend-native 12 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "author": "Graham King ", 3 | "contributors": [ 4 | "Seva Maltsev " 5 | ], 6 | "husky": { 7 | "hooks": { 8 | "pre-commit": "lint-staged" 9 | } 10 | }, 11 | "lint-staged": { 12 | "*.{js,jsx,json,css,md}": [ 13 | "prettier --write", 14 | "git add" 15 | ] 16 | }, 17 | "scripts": { 18 | "build": "NODE_ENV=production gulp build", 19 | "test": "gulp test", 20 | "lint": "eslint src/**/*.js src/**/*.jsx", 21 | "start": "NODE_ENV=development webpack-dev-server", 22 | "deploy": "npm run build && npm version patch && npm publish" 23 | }, 24 | "license": "ISC", 25 | "main": "index.js", 26 | "name": "jsonschema-form-for-material-ui", 27 | "version": "1.4.3", 28 | "repository": { 29 | "type": "git", 30 | "url": "https://github.com/TwoAbove/jsonschema-form-for-material-ui.git" 31 | }, 32 | "homepage": "https://github.com/TwoAbove/jsonschema-form-for-material-ui", 33 | "bugs": { 34 | "url": "https://github.com/TwoAbove/jsonschema-form-for-material-ui/issues" 35 | }, 36 | "prettier": { 37 | "semi": true, 38 | "singleQuote": true 39 | }, 40 | "keywords": [ 41 | "Material UI", 42 | "react-jsonschema-form", 43 | "jsonschema", 44 | "json-schema", 45 | "json", 46 | "schema", 47 | "form", 48 | "react", 49 | "material-ui" 50 | ], 51 | "description": "Material UI implementation of react-jsonschema-form", 52 | "devDependencies": { 53 | "@babel/core": "^7.3.4", 54 | "@babel/plugin-proposal-class-properties": "^7.3.4", 55 | "@babel/plugin-proposal-decorators": "^7.3.0", 56 | "@babel/plugin-proposal-do-expressions": "^7.2.0", 57 | "@babel/plugin-proposal-export-default-from": "^7.2.0", 58 | "@babel/plugin-proposal-export-namespace-from": "^7.2.0", 59 | "@babel/plugin-proposal-function-bind": "^7.2.0", 60 | "@babel/plugin-proposal-function-sent": "^7.2.0", 61 | "@babel/plugin-proposal-json-strings": "^7.2.0", 62 | "@babel/plugin-proposal-logical-assignment-operators": "^7.2.0", 63 | "@babel/plugin-proposal-nullish-coalescing-operator": "^7.2.0", 64 | "@babel/plugin-proposal-numeric-separator": "^7.2.0", 65 | "@babel/plugin-proposal-optional-chaining": "^7.2.0", 66 | "@babel/plugin-proposal-pipeline-operator": "^7.3.2", 67 | "@babel/plugin-proposal-throw-expressions": "^7.2.0", 68 | "@babel/plugin-syntax-dynamic-import": "^7.2.0", 69 | "@babel/plugin-syntax-import-meta": "^7.2.0", 70 | "@babel/preset-env": "^7.3.4", 71 | "@babel/preset-react": "^7.0.0", 72 | "@babel/register": "^7.0.0", 73 | "@material-ui/core": "^3.9.2", 74 | "@material-ui/icons": "^3.0.2", 75 | "babel-eslint": "^10.0.1", 76 | "babel-loader": "^8.0.5", 77 | "body-parser": "^1.18.3", 78 | "chai": "^4.2.0", 79 | "chai-enzyme": "^1.0.0-beta.1", 80 | "cheerio": "^1.0.0-rc.2", 81 | "chokidar": "^2.1.2", 82 | "codemirror": "^5.44.0", 83 | "css-loader": "^2.1.0", 84 | "enzyme": "^3.9.0", 85 | "enzyme-adapter-react-16": "^1.10.0", 86 | "eslint": "^5.15.0", 87 | "eslint-config-airbnb": "^17.1.0", 88 | "eslint-config-prettier": "^4.1.0", 89 | "eslint-plugin-import": "^2.16.0", 90 | "eslint-plugin-jsx-a11y": "^6.2.1", 91 | "eslint-plugin-prettier": "^3.0.1", 92 | "eslint-plugin-react": "^7.12.4", 93 | "express": "^4.16.4", 94 | "extract-text-webpack-plugin": "^4.0.0-beta.0", 95 | "file-loader": "^3.0.1", 96 | "gulp": "^4.0.0", 97 | "gulp-babel": "^8.0.0", 98 | "gulp-mocha": "^6.0.0", 99 | "html-webpack-plugin": "^3.2.0", 100 | "husky": "^1.3.1", 101 | "image-webpack-loader": "^4.6.0", 102 | "jsdom": "^13.2.0", 103 | "lint-staged": "^8.1.5", 104 | "mocha": "^6.2.0", 105 | "mochawesome": "^3.1.1", 106 | "node-sass": "^4.12.0", 107 | "prettier": "^1.16.4", 108 | "proxyquire": "^2.1.0", 109 | "raf": "^3.4.1", 110 | "react-codemirror2": "^5.1.0", 111 | "react-dom": "^16.8.3", 112 | "react-hot-loader": "^4.7.2", 113 | "sass-loader": "^7.1.0", 114 | "sinon": "^7.2.7", 115 | "sinon-chai": "^3.3.0", 116 | "testdom": "^3.0.0", 117 | "typeface-roboto": "0.0.54", 118 | "url-loader": "^1.1.2", 119 | "webpack": "^4.29.6", 120 | "webpack-bundle-analyzer": "^3.4.1", 121 | "webpack-cli": "^3.2.3", 122 | "webpack-dev-middleware": "^3.6.0", 123 | "webpack-dev-server": "^3.2.1", 124 | "webpack-hot-middleware": "^2.24.3" 125 | }, 126 | "peerDependencies": { 127 | "@material-ui/core": "^3.9.2", 128 | "@material-ui/icons": "^3.0.2", 129 | "lodash": "^4.17.15", 130 | "react": "^16.4.2" 131 | }, 132 | "dependencies": { 133 | "classnames": "^2.2.6", 134 | "immutability-helper": "^3.0.0", 135 | "prop-types": "^15.7.2", 136 | "shortid": "^2.2.14" 137 | } 138 | } 139 | -------------------------------------------------------------------------------- /src/ErrorList.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import keys from 'lodash/keys'; 4 | import { withStyles } from '@material-ui/core/styles'; 5 | import filter from 'lodash/filter'; 6 | import List from '@material-ui/core/List'; 7 | import ListItem from '@material-ui/core/ListItem'; 8 | import ListItemText from '@material-ui/core/ListItemText'; 9 | import ListItemIcon from '@material-ui/core/ListItemIcon'; 10 | import ErrorOutline from '@material-ui/icons/ErrorOutline'; 11 | 12 | const errorsStyles = { 13 | errorList: { 14 | backgroundColor: '#ffa3a3', 15 | borderColor: '#ebccd1', 16 | color: '#f44336', 17 | clear: 'both', 18 | }, 19 | panelHeading: { 20 | color: '#a94442', 21 | backgroundColor: '#f98989', 22 | borderColor: '#ebccd1', 23 | }, 24 | }; 25 | 26 | const Error = ({ errors }) => ( 27 | 28 | ); 29 | 30 | const Errors = ({ errors, anchor, classes }) => ( 31 | { 34 | document.getElementById(anchor).focus(); // eslint-disable-line 35 | }} 36 | > 37 | { 38 | errors.map((v, idx) => ()) // eslint-disable-line react/no-array-index-key,max-len 39 | } 40 | 41 | ); 42 | 43 | const hasErrors = (errors) => { 44 | let errorsFlag = false; 45 | 46 | Object.values(errors).forEach((error) => { 47 | if (error.length !== 0) { 48 | errorsFlag = true; 49 | } 50 | }); 51 | return errorsFlag; 52 | }; 53 | 54 | const ErrorList = ({ errors, field, classes }) => ( 55 |
56 | { 57 | hasErrors(errors) ? ( 58 | 62 | 63 | 64 | 65 | 66 | 67 | )} 68 | > 69 | { 70 | filter(keys(errors), (k) => { 71 | const v = errors[k]; 72 | return v && v.length > 0; 73 | }).map(v => ( 74 | 75 | )) 76 | } 77 | 78 | ) : null 79 | } 80 |
81 | ); 82 | 83 | export default withStyles(errorsStyles)(ErrorList); 84 | -------------------------------------------------------------------------------- /src/FieldSet/FieldSet.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import classNames from 'classnames'; 3 | import endsWith from 'lodash/endsWith'; 4 | import isEqual from 'lodash/isEqual'; 5 | import { withStyles } from '@material-ui/core/styles'; 6 | import InputLabel from '@material-ui/core/InputLabel'; 7 | import fieldSetStyles from './field-set-styles'; 8 | import FieldSetArray from './FieldSetArray'; 9 | import FieldSetObject from './FieldSetObject'; 10 | 11 | 12 | export const RawFieldSetContent = (props) => { 13 | const { schema = {} } = props; 14 | const { type } = schema; 15 | if (type === 'array') { 16 | return ; 17 | } 18 | if (type === 'object') { 19 | return ; 20 | } 21 | return null; 22 | }; 23 | 24 | export const FieldSetContent = withStyles(fieldSetStyles.fieldSetContent)(RawFieldSetContent); 25 | 26 | // for unit testing 27 | export class RawFieldSet extends React.Component { 28 | shouldComponentUpdate = nextProps => !isEqual(this.props.data, nextProps.data) 29 | 30 | render() { 31 | const { className, path, classes, schema = {} } = this.props; 32 | return ( 33 |
34 | {schema.title 35 | && {schema.title} 36 | } 37 | 38 |
39 | ); 40 | } 41 | } 42 | 43 | export default withStyles(fieldSetStyles.fieldSet)(RawFieldSet); 44 | -------------------------------------------------------------------------------- /src/FieldSet/FieldSet.spec.jsx: -------------------------------------------------------------------------------- 1 | /* globals describe,it */ 2 | /* eslint-disable no-unused-expressions */ 3 | import React from 'react'; 4 | import forEach from 'lodash/forEach'; 5 | import chai, { expect } from 'chai'; 6 | import Enzyme, { shallow } from 'enzyme'; 7 | import chaiEnzyme from 'chai-enzyme'; 8 | import Adapter from 'enzyme-adapter-react-16'; 9 | import sinon from 'sinon'; 10 | import sinonChai from 'sinon-chai'; 11 | import IconButton from '@material-ui/core/IconButton'; 12 | import { RawFieldSet, FieldSetContent } from './FieldSet'; 13 | import { RawFieldSetObject } from './FieldSetObject'; 14 | import { RawFieldSetArray } from './FieldSetArray'; 15 | import ReorderControls, { RawReorderControls } from './ReorderControls'; 16 | import ReorderableFormField, { 17 | RawReorderableFormField 18 | } from './ReorderableFormField'; 19 | 20 | import FormField from '../FormField'; 21 | 22 | const classes = { 23 | root: 'rootClassName', 24 | row: 'rowClassName' 25 | }; 26 | 27 | chai.use(chaiEnzyme()); 28 | chai.use(sinonChai); 29 | Enzyme.configure({ adapter: new Adapter() }); 30 | 31 | describe('FieldSet', () => { 32 | describe('FieldSet - control', () => { 33 | it('mounts (control)', () => { 34 | const schema = {}; 35 | const data = {}; 36 | 37 | // act 38 | const wrapper = shallow( 39 | 40 | ); 41 | 42 | // check 43 | expect(wrapper).to.have.length(1); 44 | const ffComp = wrapper.find(FieldSetContent); 45 | expect(ffComp).to.have.length(1); 46 | expect(ffComp).to.have.prop('schema', schema); 47 | const legendComp = wrapper.find('legend'); 48 | expect(legendComp).to.not.be.present(); 49 | }); 50 | }); 51 | describe('FieldSetObject', () => { 52 | it('mounts with single field (control)', () => { 53 | const schema = { 54 | type: 'object', 55 | properties: { 56 | name: { 57 | type: 'string', 58 | title: 'Name' 59 | } 60 | } 61 | }; 62 | const data = { name: 'Bob' }; 63 | 64 | // act 65 | const wrapper = shallow( 66 | 67 | ); 68 | 69 | // check 70 | expect(wrapper).to.have.length(1); 71 | expect(wrapper) 72 | .to.have.prop('className') 73 | .not.match(/rowClassName/); 74 | const ffComp = wrapper.find(FormField); 75 | expect(ffComp).to.have.length(1); 76 | expect(ffComp).to.have.prop('path', 'name'); 77 | expect(ffComp).to.have.prop('data', data.name); 78 | expect(ffComp) 79 | .to.have.prop('objectData') 80 | .deep.equal(data); 81 | }); 82 | it('respects orientation hint', () => { 83 | const schema = { 84 | type: 'object', 85 | properties: { 86 | name: { 87 | type: 'string', 88 | title: 'Name' 89 | } 90 | } 91 | }; 92 | const uiSchema = { 93 | 'ui:orientation': 'row' 94 | }; 95 | const data = { name: 'Bob' }; 96 | 97 | // act 98 | const wrapper = shallow( 99 | 105 | ); 106 | 107 | // check 108 | expect(wrapper).to.have.length(1); 109 | expect(wrapper) 110 | .to.have.prop('className') 111 | .match(/rowClassName/); 112 | const ffComp = wrapper.find(FormField); 113 | expect(ffComp).to.have.length(1); 114 | expect(ffComp).to.have.prop('path', 'name'); 115 | expect(ffComp).to.have.prop('data', data.name); 116 | }); 117 | }); 118 | describe('FieldSetArray', () => { 119 | it('handles simple, orderable list of strings', () => { 120 | const path = 'names'; 121 | const defaultValue = 'abc'; 122 | const startIdx = 2; 123 | const schema = { 124 | type: 'array', 125 | title: 'My list', 126 | items: { 127 | type: 'string', 128 | title: 'Name', 129 | default: defaultValue 130 | } 131 | }; 132 | const onMoveItemUp = sinon.stub(); 133 | const onMoveItemDown = sinon.stub(); 134 | const onDeleteItem = sinon.stub(); 135 | forEach([0, 1], i => 136 | onMoveItemUp.withArgs(path, startIdx + i).returns(`moveUp${i}`) 137 | ); 138 | forEach([0, 1], i => 139 | onMoveItemDown.withArgs(path, startIdx + i).returns(`moveDown${i}`) 140 | ); 141 | forEach([0, 1], i => 142 | onDeleteItem.withArgs(path, startIdx + i).returns(`deleteItem${i}`) 143 | ); 144 | const uiSchema = { 145 | items: { 146 | 'ui:widget': 'textarea' 147 | } 148 | }; 149 | const data = ['Bob', 'Harry']; 150 | const onAddItem = sinon.spy(); 151 | 152 | // act 153 | const wrapper = shallow( 154 | 166 | ); 167 | 168 | // check 169 | expect(wrapper).to.have.length(1); 170 | const ffComp = wrapper.find(ReorderableFormField); 171 | expect(ffComp).to.have.length(2); 172 | forEach([0, 1], i => { 173 | expect(ffComp.at(i)).to.have.prop('path', `names[${i + startIdx}]`); 174 | expect(ffComp.at(i)).to.have.prop('data', data[i]); 175 | expect(ffComp.at(i)).to.have.prop('schema', schema.items); 176 | expect(ffComp.at(i)).to.have.prop('uiSchema', uiSchema.items); 177 | expect(ffComp.at(i)).to.have.prop('onMoveItemUp', `moveUp${i}`); 178 | expect(ffComp.at(i)).to.have.prop('onMoveItemDown', `moveDown${i}`); 179 | expect(ffComp.at(i)).to.have.prop('onDeleteItem', `deleteItem${i}`); 180 | }); 181 | expect(ffComp.at(0)).to.have.prop('first', true); 182 | expect(ffComp.at(0)).to.have.prop('last', false); 183 | expect(ffComp.at(1)).to.have.prop('first', false); 184 | expect(ffComp.at(1)).to.have.prop('last', true); 185 | const addButton = wrapper.find(IconButton); 186 | expect(addButton).to.be.present(); 187 | addButton.simulate('click'); 188 | expect(onAddItem).to.be.calledWith(path, defaultValue); 189 | }); 190 | it('handles simple, fixed list of strings', () => { 191 | const path = 'names'; 192 | const schema = { 193 | type: 'array', 194 | title: 'My list', 195 | items: [ 196 | { 197 | type: 'string', 198 | title: 'Name' 199 | }, 200 | { 201 | type: 'boolean', 202 | title: 'Name' 203 | } 204 | ] 205 | }; 206 | const uiSchema = { 207 | items: [ 208 | { 209 | 'ui:widget': 'textarea' 210 | }, 211 | { 212 | 'ui:widget': 'checkbox' 213 | } 214 | ] 215 | }; 216 | const data = ['Bob', false]; 217 | 218 | // act 219 | const wrapper = shallow( 220 | 227 | ); 228 | 229 | // check 230 | expect(wrapper).to.have.length(1); 231 | const ffComp = wrapper.find(FormField); 232 | const rffComp = wrapper.find(ReorderableFormField); 233 | expect(ffComp).to.have.length(2); 234 | expect(rffComp).to.have.length(0); 235 | forEach([0, 1], idx => { 236 | expect(ffComp.at(idx)).to.have.prop('path', `names[${idx}]`); 237 | expect(ffComp.at(idx)).to.have.prop('data', data[idx]); 238 | expect(ffComp.at(idx)).to.have.prop('schema', schema.items[idx]); 239 | expect(ffComp.at(idx)).to.have.prop('uiSchema', uiSchema.items[idx]); 240 | }); 241 | }); 242 | it('handles simple, fixed list of strings with additional items', () => { 243 | const path = 'names'; 244 | const onMoveItemUp = 'onMoveItemUp'; 245 | const onMoveItemDown = 'onMoveItemDown'; 246 | const onDeleteItem = 'onDeleteItem'; 247 | 248 | const schema = { 249 | type: 'array', 250 | title: 'My list', 251 | items: [ 252 | { 253 | type: 'string', 254 | title: 'Name' 255 | }, 256 | { 257 | type: 'boolean', 258 | title: 'Name' 259 | } 260 | ], 261 | additionalItems: { 262 | type: 'string', 263 | title: 'Name' 264 | } 265 | }; 266 | const uiSchema = { 267 | items: [ 268 | { 269 | 'ui:widget': 'textarea' 270 | }, 271 | { 272 | 'ui:widget': 'checkbox' 273 | } 274 | ], 275 | additionalItems: { 276 | 'ui:title': 'Children' 277 | } 278 | }; 279 | const data = ['Bob', false, 'Harry', 'Susan']; 280 | 281 | // act 282 | const wrapper = shallow( 283 | 293 | ); 294 | 295 | // check 296 | expect(wrapper).to.have.length(1); 297 | const ffComp = wrapper.find(FormField); 298 | expect(ffComp).to.have.length(2); 299 | forEach([0, 1], i => { 300 | expect(ffComp.at(i)).to.have.prop('path', `names[${i}]`); 301 | expect(ffComp.at(i)).to.have.prop('data', data[i]); 302 | expect(ffComp.at(i)).to.have.prop('schema', schema.items[i]); 303 | expect(ffComp.at(i)).to.have.prop('uiSchema', uiSchema.items[i]); 304 | expect(ffComp.at(i)).to.not.have.prop('onMoveItemUp'); 305 | expect(ffComp.at(i)).to.not.have.prop('onMoveItemDown'); 306 | expect(ffComp.at(i)).to.not.have.prop('onDeleteItem'); 307 | }); 308 | const fsArrayComp = wrapper.find(RawFieldSetArray); 309 | expect(fsArrayComp).to.be.present(); 310 | expect(fsArrayComp).to.have.prop('path', path); 311 | expect(fsArrayComp) 312 | .to.have.prop('data') 313 | .deep.equal(['Harry', 'Susan']); 314 | expect(fsArrayComp) 315 | .to.have.prop('schema') 316 | .deep.equal({ type: 'array', items: schema.additionalItems }); 317 | expect(fsArrayComp).to.have.prop('uiSchema', uiSchema.additionalItems); 318 | expect(fsArrayComp).to.have.prop('startIdx', schema.items.length); 319 | expect(fsArrayComp).to.have.prop('onMoveItemUp', onMoveItemUp); 320 | expect(fsArrayComp).to.have.prop('onMoveItemDown', onMoveItemDown); 321 | expect(fsArrayComp).to.have.prop('onDeleteItem', onDeleteItem); 322 | }); 323 | }); 324 | describe('ReorderControls', () => { 325 | it('renders buttons with callbacks', () => { 326 | // prepare 327 | const onMoveItemUp = sinon.spy(); 328 | const onMoveItemDown = sinon.spy(); 329 | const onDeleteItem = sinon.spy(); 330 | 331 | // act 332 | const wrapper = shallow( 333 | 339 | ); 340 | 341 | // check 342 | expect(wrapper).to.have.length(1); 343 | const buttonList = wrapper.find(IconButton); 344 | expect(buttonList).to.have.length(3); 345 | buttonList.at(0).simulate('click'); 346 | expect(onMoveItemUp).to.have.been.called; 347 | buttonList.at(1).simulate('click'); 348 | expect(onMoveItemDown).to.have.been.called; 349 | buttonList.at(2).simulate('click'); 350 | expect(onDeleteItem).to.have.been.called; 351 | }); 352 | it('ReorderControls - first', () => { 353 | // act 354 | const wrapper = shallow( 355 | 356 | ); 357 | 358 | // check 359 | expect(wrapper).to.have.length(1); 360 | const buttonList = wrapper.find(IconButton); 361 | expect(buttonList).to.have.length(3); 362 | expect(buttonList.at(0)).to.have.prop('disabled', true); 363 | expect(buttonList.at(1)).to.have.prop('disabled', false); 364 | expect(buttonList.at(2)).to.not.have.prop('disabled'); 365 | }); 366 | it('ReorderControls - last', () => { 367 | // act 368 | const wrapper = shallow( 369 | 370 | ); 371 | 372 | // check 373 | expect(wrapper).to.have.length(1); 374 | const buttonList = wrapper.find(IconButton); 375 | expect(buttonList).to.have.length(3); 376 | expect(buttonList.at(0)).to.have.prop('disabled', false); 377 | expect(buttonList.at(1)).to.have.prop('disabled', true); 378 | expect(buttonList.at(2)).to.not.have.prop('disabled'); 379 | }); 380 | }); 381 | describe('ReorderableFormField', () => { 382 | it('ReorderableFormField - control', () => { 383 | const path = 'path'; 384 | const first = 'first'; 385 | const last = 'last'; 386 | // act 387 | const wrapper = shallow( 388 | 394 | ); 395 | 396 | // check 397 | const ffComp = wrapper.find(FormField); 398 | expect(ffComp).to.have.length(1); 399 | const controls = wrapper.find(ReorderControls); 400 | expect(controls).to.have.length(1); 401 | expect(controls).to.have.prop('first', first); 402 | expect(controls).to.have.prop('last', last); 403 | }); 404 | }); 405 | }); 406 | -------------------------------------------------------------------------------- /src/FieldSet/FieldSetArray.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import includes from 'lodash/includes'; 3 | import slice from 'lodash/slice'; 4 | import IconButton from '@material-ui/core/IconButton'; 5 | import AddCircle from '@material-ui/icons/AddCircle'; 6 | import isArray from 'lodash/isArray'; 7 | import { withStyles } from '@material-ui/core/styles'; 8 | import FormField from '../FormField'; 9 | import fieldSetStyles from './field-set-styles'; 10 | import getDefaultValue from '../helpers/get-default-value'; 11 | import ReorderableFormField from './ReorderableFormField'; 12 | 13 | export const RawFieldSetArray = (props) => { 14 | const { 15 | startIdx = 0, className, classes, 16 | schema = {}, uiSchema = {}, data, path, onMoveItemUp, onMoveItemDown, onDeleteItem, ...rest 17 | } = props; 18 | 19 | return ( 20 |
21 | {!isArray(schema.items) && !schema.uniqueItems && ( 22 |
23 | {(data || []).map((d, idx) => ( 24 | 39 | ))} 40 |
41 | 42 | 43 | 44 |
45 |
46 | )} 47 | {isArray(schema.items) && (data || []).map((d, idx) => { 48 | if (idx < schema.items.length) { 49 | return ( 50 | 60 | ); 61 | } 62 | return null; 63 | })} 64 | {(!isArray(schema.items) && schema.uniqueItems && schema.items.enum) && schema.items.enum.map(d => ( 65 | 75 | ))} 76 | {schema.additionalItems 77 | && ( 78 | 91 | ) 92 | } 93 |
94 | ); 95 | }; 96 | export default withStyles(fieldSetStyles.fieldSetArray)(RawFieldSetArray); 97 | -------------------------------------------------------------------------------- /src/FieldSet/FieldSetObject.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import classNames from 'classnames'; 3 | import keys from 'lodash/keys'; 4 | import { withStyles } from '@material-ui/core/styles'; 5 | import FormField from '../FormField'; 6 | import fieldSetStyles from './field-set-styles'; 7 | 8 | export const RawFieldSetObject = ({ className, classes, schema = {}, uiSchema = {}, data = {}, path, ...rest }) => { 9 | const orientation = (uiSchema['ui:orientation'] === 'row' ? classes.row : null); 10 | return ( 11 |
12 | {keys(schema.properties).map((p) => { 13 | const newPath = path ? `${path}.${p}` : p; 14 | return ( 15 | 25 | ); 26 | })} 27 |
28 | ); 29 | }; 30 | export default withStyles(fieldSetStyles.fieldSetObject)(RawFieldSetObject); 31 | -------------------------------------------------------------------------------- /src/FieldSet/ReorderControls.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import IconButton from '@material-ui/core/IconButton'; 3 | import ArrowUpward from '@material-ui/icons/ArrowUpward'; 4 | import ArrowDownward from '@material-ui/icons/ArrowDownward'; 5 | import RemoveCircle from '@material-ui/icons/RemoveCircle'; 6 | import { withStyles } from '@material-ui/core/styles'; 7 | import fieldSetStyles from './field-set-styles'; 8 | 9 | export const RawReorderControls = ({ first, last, classes, onMoveItemUp, onMoveItemDown, onDeleteItem }) => ( 10 |
11 | 12 | 13 | 14 |
15 | ); 16 | export default withStyles(fieldSetStyles.reorderControls)(RawReorderControls); 17 | -------------------------------------------------------------------------------- /src/FieldSet/ReorderableFormField.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import classNames from 'classnames'; 3 | import { withStyles } from '@material-ui/core/styles'; 4 | import FormField from '../FormField'; 5 | import fieldSetStyles from './field-set-styles'; 6 | import ReorderControls from './ReorderControls'; 7 | 8 | export const RawReorderableFormField = ({ 9 | first, last, className, classes, path, onMoveItemUp, onMoveItemDown, onDeleteItem, ...rest 10 | }) => 11 | ( 12 |
13 | 17 | 24 |
25 | ); 26 | export default withStyles(fieldSetStyles.reorderable)(RawReorderableFormField); 27 | -------------------------------------------------------------------------------- /src/FieldSet/field-set-styles.js: -------------------------------------------------------------------------------- 1 | export default ({ 2 | fieldSet: theme => ({ 3 | root: { 4 | display: 'flex', 5 | flexDirection: 'column', 6 | border: 0, 7 | }, 8 | listItem: { 9 | 'border': `1px dotted ${theme.palette.primary.main}`, 10 | 'margin': theme.spacing.unit, 11 | 'padding': theme.spacing.unit, 12 | }, 13 | }), 14 | fieldSetObject: { 15 | 'root': { 16 | 'display': 'flex', 17 | 'flexDirection': 'column', 18 | '&$row': { 19 | flexDirection: 'row', 20 | }, 21 | }, 22 | 'row': {}, 23 | 'listItem': {}, 24 | }, 25 | fieldSetArray: theme => ({ 26 | 'root': { 27 | display: 'flex', 28 | flexDirection: 'column', 29 | }, 30 | 'listItem': {}, 31 | 'addItemBtn': { 32 | 'display': 'flex', 33 | 'justifyContent': 'flex-end', 34 | '&>button': { 35 | 'background': theme.palette.primary.main, 36 | 'width': '3.75em', 37 | 'color': theme.palette.common.white, 38 | 'height': '1.25em', 39 | 'borderRadius': 5, 40 | }, 41 | }, 42 | }), 43 | reorderable: { 44 | 'root': { 45 | 'display': 'flex', 46 | 'alignItems': 'baseline', 47 | 'justifyContent': 'space-between', 48 | '& >fieldset': { 49 | width: '100%', 50 | }, 51 | }, 52 | 'listItem': {}, 53 | }, 54 | reorderControls: theme => ({ 55 | root: { 56 | 'display': 'flex', 57 | 'border': `1px solid ${theme.palette.grey[400]}`, 58 | 'borderRadius': 5, 59 | '& >button': { 60 | 'borderRadius': 0, 61 | 'width': '1.25em', 62 | 'height': '1.25em', 63 | '&:not(:last-child)': { 64 | borderRight: `1px solid ${theme.palette.grey[400]}`, 65 | }, 66 | }, 67 | }, 68 | remove: { 69 | background: theme.palette.error.main, 70 | color: theme.palette.grey[800], 71 | }, 72 | }), 73 | fieldSetContent: { 74 | 'root': {}, 75 | 'listItem': {}, 76 | }, 77 | }); 78 | -------------------------------------------------------------------------------- /src/FieldSet/index.js: -------------------------------------------------------------------------------- 1 | export { default } from './FieldSet'; 2 | -------------------------------------------------------------------------------- /src/Form.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import isEqual from 'lodash/isEqual'; 4 | import { generate } from 'shortid'; 5 | import { withStyles } from '@material-ui/core/styles'; 6 | import Paper from '@material-ui/core/Paper'; 7 | import formStyles from './form-styles'; 8 | import FormField from './FormField'; 9 | import updateFormData, { 10 | addListItem, 11 | removeListItem, 12 | moveListItem 13 | } from './helpers/update-form-data'; 14 | import getValidationResult from './helpers/validation'; 15 | import DefaultErrorList from './ErrorList'; 16 | import FormButtons from './FormButtons'; 17 | 18 | class Form extends React.Component { 19 | static defaultProps = { 20 | uiSchema: {}, 21 | showErrorList: false, 22 | showHelperError: true, 23 | ErrorList: DefaultErrorList 24 | }; 25 | 26 | state = { 27 | data: this.props.formData, 28 | errors: getValidationResult(this.props.schema, this.props.formData), 29 | id: generate() 30 | }; 31 | 32 | componentWillReceiveProps = nextProps => { 33 | let errors; 34 | if (!isEqual(nextProps.schema, this.props.schema)) { 35 | errors = {}; 36 | } else { 37 | errors = getValidationResult(this.props.schema, nextProps.formData); 38 | } 39 | this.setState({ 40 | errors, 41 | data: nextProps.formData 42 | }); 43 | }; 44 | 45 | onChange = field => value => { 46 | // eslint-disable-next-line react/no-access-state-in-setstate 47 | const data = updateFormData(this.state.data, field, value); 48 | this.setState( 49 | { 50 | data, 51 | errors: getValidationResult(this.props.schema, data) 52 | }, 53 | this.notifyChange 54 | ); 55 | }; 56 | 57 | onMoveItemUp = (path, idx) => () => { 58 | this.setState( 59 | prevState => ({ data: moveListItem(prevState.data, path, idx, -1) }), 60 | this.notifyChange 61 | ); 62 | }; 63 | 64 | onMoveItemDown = (path, idx) => () => { 65 | this.setState( 66 | prevState => ({ data: moveListItem(prevState.data, path, idx, 1) }), 67 | this.notifyChange 68 | ); 69 | }; 70 | 71 | onDeleteItem = (path, idx) => () => { 72 | this.setState( 73 | prevState => ({ data: removeListItem(prevState.data, path, idx) }), 74 | this.notifyChange 75 | ); 76 | }; 77 | 78 | onAddItem = (path, defaultValue) => () => { 79 | this.setState( 80 | prevState => ({ 81 | data: addListItem(prevState.data, path, defaultValue || '') 82 | }), 83 | this.notifyChange 84 | ); 85 | }; 86 | 87 | onSubmit = () => { 88 | const { onSubmit } = this.props; 89 | const { data } = this.state; 90 | onSubmit({ formData: data }); 91 | }; 92 | 93 | notifyChange = () => { 94 | const { onChange } = this.props; 95 | const { data } = this.state; 96 | if (onChange) { 97 | onChange({ formData: data }); 98 | } 99 | }; 100 | 101 | render() { 102 | const { 103 | classes, 104 | formData, 105 | onSubmit, 106 | onChange, 107 | onCancel, 108 | cancelText, 109 | submitText, 110 | showErrorList, 111 | ErrorList, 112 | buttonProps, 113 | ...rest 114 | } = this.props; 115 | const { errors, id, data } = this.state; 116 | return ( 117 | 118 | {showErrorList ? : null} 119 |
120 | 134 |
135 | 144 |
145 | ); 146 | } 147 | } 148 | export default withStyles(formStyles)(Form); 149 | 150 | Form.propTypes = { 151 | schema: PropTypes.object.isRequired, 152 | classes: PropTypes.object, 153 | uiSchema: PropTypes.object, 154 | buttonProps: PropTypes.object, 155 | formData: PropTypes.any, 156 | onChange: PropTypes.func, 157 | onSubmit: PropTypes.func, 158 | onCancel: PropTypes.func, 159 | cancelText: PropTypes.string, 160 | submitText: PropTypes.string, 161 | showErrorList: PropTypes.bool, 162 | showHelperError: PropTypes.bool, 163 | ErrorList: PropTypes.func 164 | }; 165 | -------------------------------------------------------------------------------- /src/FormButtons.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import classNames from 'classnames'; 3 | import Button from '@material-ui/core/Button'; 4 | 5 | export class RawFormButtons extends React.Component { 6 | shouldComponentUpdate = () => false; 7 | 8 | render() { 9 | const { 10 | classes, 11 | onCancel, 12 | onSubmit, 13 | buttonProps, 14 | hasExternalOnSubmit, 15 | cancelText = 'Cancel', 16 | submitText = 'Submit' 17 | } = this.props; 18 | 19 | const cancelProps = Object.assign( 20 | { 21 | variant: 'text', 22 | onClick: onCancel 23 | }, 24 | buttonProps 25 | ); 26 | const submitProps = Object.assign( 27 | { 28 | variant: 'contained', 29 | color: 'primary', 30 | onClick: onSubmit 31 | }, 32 | buttonProps 33 | ); 34 | return ( 35 | (onCancel || onSubmit) && ( 36 |
37 | {onCancel && ( 38 | 44 | )} 45 | {hasExternalOnSubmit && ( 46 | 52 | )} 53 |
54 | ) 55 | ); 56 | } 57 | } 58 | 59 | export default RawFormButtons; 60 | -------------------------------------------------------------------------------- /src/FormField.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import isEqual from 'lodash/isEqual'; 3 | import { withStyles } from '@material-ui/core/styles'; 4 | import FieldSet from './FieldSet'; 5 | import Field from './fields'; 6 | import styles from './form-field-styles'; 7 | 8 | // exported for unit testing 9 | export class RawFormField extends React.Component { 10 | shouldComponentUpdate = nextProps => !isEqual(this.props.data, nextProps.data) 11 | 12 | render() { 13 | const { classes, schema, data, uiSchema = {}, onChange, path, ...rest } = this.props; 14 | const { type } = schema; 15 | if (type === 'object' || type === 'array') { 16 | return ( 17 |
26 | ); 27 | } 28 | return ( 29 | 38 | ); 39 | } 40 | } 41 | 42 | export default withStyles(styles)(RawFormField); 43 | -------------------------------------------------------------------------------- /src/FormField.spec.jsx: -------------------------------------------------------------------------------- 1 | /* globals describe,it */ 2 | /* eslint-disable no-unused-expressions */ 3 | import React from 'react'; 4 | import chai, { expect } from 'chai'; 5 | import Enzyme, { shallow } from 'enzyme'; 6 | import chaiEnzyme from 'chai-enzyme'; 7 | import Adapter from 'enzyme-adapter-react-16'; 8 | import sinon from 'sinon'; 9 | import sinonChai from 'sinon-chai'; 10 | import FormField, { RawFormField } from './FormField'; 11 | import Field from './fields'; 12 | import FieldSet from './FieldSet'; 13 | 14 | const classes = { 15 | root: 'fieldClassName' 16 | }; 17 | 18 | chai.use(chaiEnzyme()); 19 | chai.use(sinonChai); 20 | Enzyme.configure({ adapter: new Adapter() }); 21 | 22 | describe('FormField', () => { 23 | it('mounts with single field (control)', () => { 24 | const path = 'name'; 25 | const onChange = sinon.stub(); 26 | const schema = { 27 | type: 'string', 28 | title: 'First Name' 29 | }; 30 | const uiSchema = {}; 31 | const data = 'Bob'; 32 | onChange.returns('onChangeFunc'); 33 | 34 | // act 35 | const wrapper = shallow( 36 | 44 | ); 45 | 46 | // check 47 | expect(wrapper).to.be.present(); 48 | const ffComp = wrapper.find(Field); 49 | expect(ffComp).to.have.length(1); 50 | expect(ffComp).to.have.prop('className', classes.root); 51 | expect(ffComp).to.have.prop('path', path); 52 | expect(ffComp).to.have.prop('schema', schema); 53 | expect(ffComp).to.have.prop('data', data); 54 | expect(ffComp).to.have.prop('uiSchema', uiSchema); 55 | expect(onChange).calledWith(path); 56 | expect(ffComp).to.have.prop('onChange', 'onChangeFunc'); 57 | }); 58 | it('spreads additional properties to Field', () => { 59 | const schema = { 60 | name: { 61 | type: 'string' 62 | } 63 | }; 64 | const myProp = 'blah'; 65 | 66 | // act 67 | const wrapper = shallow( 68 | 69 | ); 70 | 71 | // check 72 | expect(wrapper).to.be.present(); 73 | const ffComp = wrapper.find(Field); 74 | expect(ffComp).to.have.prop('myProp', myProp); 75 | }); 76 | it('renders object as FieldSet, passing all properties', () => { 77 | const onChange = sinon.stub(); 78 | const path = 'name'; 79 | const schema = { 80 | type: 'object', 81 | properties: { 82 | firstName: { 83 | type: 'string', 84 | title: 'First Name' 85 | }, 86 | surname: { 87 | type: 'string', 88 | title: 'Surname' 89 | } 90 | } 91 | }; 92 | const data = { 93 | firstName: 'Bob', 94 | surname: 'Hope' 95 | }; 96 | const uiSchema = { 97 | firstName: {}, 98 | surname: {} 99 | }; 100 | 101 | // act 102 | const wrapper = shallow( 103 | 111 | ); 112 | 113 | // check 114 | expect(wrapper).to.be.present(); 115 | const fsComp = wrapper.find(FieldSet); 116 | expect(fsComp).to.be.have.length(1); 117 | expect(fsComp).to.have.prop('path', path); 118 | expect(fsComp).to.have.prop('schema', schema); 119 | expect(fsComp).to.have.prop('data', data); 120 | expect(fsComp).to.have.prop('uiSchema', uiSchema); 121 | expect(fsComp).to.have.prop('onChange', onChange); 122 | }); 123 | }); 124 | -------------------------------------------------------------------------------- /src/fields/ConfiguredField.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import classNames from 'classnames'; 3 | 4 | import { withStyles } from '@material-ui/core/styles'; 5 | import FormControl from '@material-ui/core/FormControl'; 6 | import FormHelperText from '@material-ui/core/FormHelperText'; 7 | import Input from '@material-ui/core/Input'; 8 | 9 | import Tooltip from '@material-ui/core/Tooltip'; 10 | import IconButton from '@material-ui/core/IconButton'; 11 | import InfoIcon from '@material-ui/icons/InfoOutlined'; 12 | 13 | import fieldStyles from './field-styles'; 14 | // import PopoverInfo from './components/PopoverInfo'; removed for fix animation problems 15 | 16 | // for unit testing only 17 | export class RawConfiguredField extends React.Component { 18 | shouldComponentUpdate = nextProps => { 19 | const { data } = this.props; 20 | if (data !== nextProps.data) { 21 | return true; 22 | } 23 | return false; 24 | }; 25 | 26 | formatErrorMessages = () => { 27 | const { errors } = this.props; 28 | return errors.map(error => error.message).toString(); 29 | }; 30 | 31 | render() { 32 | const { 33 | classes = {}, 34 | data, 35 | type, 36 | descriptionText, 37 | helpText: helpT, 38 | showHelperError, 39 | Component = Input, 40 | LabelComponent, 41 | labelComponentProps = {}, 42 | title, 43 | className, 44 | componentProps = {}, 45 | id, 46 | errors 47 | } = this.props; 48 | const helpText = 49 | showHelperError && errors && errors.length > 0 50 | ? this.formatErrorMessages() 51 | : helpT; 52 | return ( 53 | 0} 55 | className={classNames(classes.root, { 56 | [classes.withLabel]: LabelComponent 57 | })} 58 | > 59 | {LabelComponent && title && ( 60 | 61 | {title} 62 | {descriptionText ? ( 63 | 64 | 65 | 66 | 67 | 68 | ) : null} 69 | 70 | )} 71 | {descriptionText && !(LabelComponent && title) ? ( 72 | 73 | 74 | 75 | 76 | 77 | ) : null} 78 | 85 | {helpText} 86 | 87 | ); 88 | } 89 | } 90 | export default withStyles(fieldStyles)(RawConfiguredField); 91 | -------------------------------------------------------------------------------- /src/fields/Field.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import configureComponent from './configure'; 4 | import ConfiguredField from './ConfiguredField'; 5 | 6 | export default props => { 7 | const { path, id, schema, data, uiSchema, errors, showHelperError } = props; 8 | const { type } = schema; 9 | const htmlId = `${id}_${path}`; 10 | const configuredProps = configureComponent({ ...props, htmlId }); 11 | const descriptionText = uiSchema['ui:description'] || schema.description; 12 | const helpText = uiSchema['ui:help']; 13 | return ( 14 | 24 | ); 25 | }; 26 | -------------------------------------------------------------------------------- /src/fields/Field.spec.jsx: -------------------------------------------------------------------------------- 1 | /* globals describe,it */ 2 | /* eslint-disable no-unused-expressions */ 3 | import React from 'react'; 4 | import chai, { expect } from 'chai'; 5 | import Enzyme, { shallow } from 'enzyme'; 6 | import chaiEnzyme from 'chai-enzyme'; 7 | import Adapter from 'enzyme-adapter-react-16'; 8 | import sinon from 'sinon'; 9 | import sinonChai from 'sinon-chai'; 10 | import Input from '@material-ui/core/Input'; 11 | import FormControl from '@material-ui/core/FormControl'; 12 | import FormHelperText from '@material-ui/core/FormHelperText'; 13 | import FormLabel from '@material-ui/core/FormLabel'; 14 | import Tooltip from '@material-ui/core/Tooltip'; 15 | 16 | import { RadioGroup } from './components'; 17 | import { RawConfiguredField } from './ConfiguredField'; 18 | 19 | const classes = { 20 | description: 'description', 21 | root: 'rootClassName', 22 | myComp: 'myCompClassName', 23 | withLabel: 'withLabelClass' 24 | }; 25 | 26 | chai.use(chaiEnzyme()); 27 | chai.use(sinonChai); 28 | Enzyme.configure({ adapter: new Adapter() }); 29 | 30 | describe('Field', () => { 31 | it('mounts with standard attributes (control)', () => { 32 | const componentProps = { 33 | multiline: true 34 | }; 35 | const data = 'Hello'; 36 | const type = 'string'; 37 | const wrapper = shallow( 38 | 44 | ); 45 | // test FormControl properties 46 | const FC = wrapper.find(FormControl); 47 | expect(FC).to.have.length(1); 48 | expect(FC) 49 | .to.have.prop('className') 50 | .match(/rootClassName/) 51 | .not.match(/withLabelClass/); 52 | 53 | // no helpText, descriptionText or LabelComponent 54 | expect(FC.children()).to.have.length(2); // control 55 | 56 | // test Component properties 57 | const Component = wrapper.find(Input); // control 58 | expect(Component).to.be.present(); 59 | expect(Component).to.have.prop('multiline', componentProps.multiline); 60 | expect(Component).to.have.prop('value', data); 61 | expect(Component).to.have.prop('type', type); 62 | expect(Component).to.not.have.prop('className'); // control 63 | }); 64 | 65 | it('applies given className', () => { 66 | const wrapper = shallow( 67 | 68 | ); 69 | const Component = wrapper.find(Input); 70 | expect(Component).to.be.present(); 71 | expect(Component).to.have.prop('className', classes.myComp); 72 | }); 73 | 74 | it('renders provided Component', () => { 75 | const wrapper = shallow(); 76 | expect(wrapper.find(Input)).to.not.be.present(); 77 | expect(wrapper.find(RadioGroup)).to.be.present(); 78 | }); 79 | 80 | it('renders provided LabelComponent with title and labelComponentProps', () => { 81 | const labelComponentProps = { 82 | style: 'bold' 83 | }; 84 | const title = 'Hello'; 85 | const DummyLabel = ({ children }) =>
{children}
; 86 | 87 | const wrapper = shallow( 88 | 93 | ); 94 | 95 | const labelComp = wrapper.find(DummyLabel); 96 | expect(labelComp).to.be.present(); 97 | expect(labelComp).to.have.prop('style', labelComponentProps.style); 98 | expect(labelComp.children()).to.have.length(1); 99 | expect(labelComp.childAt(0)).to.have.text(title); 100 | }); 101 | 102 | it('renders provided descriptionText', () => { 103 | const descriptionText = 'This is a field'; 104 | const wrapper = shallow( 105 | 106 | ); 107 | 108 | const descriptionComp = wrapper.find(Tooltip); 109 | expect(descriptionComp).to.have.length(1); 110 | expect(descriptionComp).to.have.prop('title', descriptionText); 111 | }); 112 | 113 | it('renders provided helpText', () => { 114 | const helpText = 'Help! I need somebody!'; 115 | const id = 'unq-id'; 116 | const wrapper = shallow(); 117 | 118 | const helpComp = wrapper.find(FormHelperText); 119 | expect(helpComp).to.be.present(); 120 | expect(helpComp).to.have.prop('id', `${id}-help`); 121 | expect(helpComp.children()).to.have.length(1); 122 | expect(helpComp.childAt(0).text()).to.equal(helpText); 123 | }); 124 | 125 | it('calls onChange', () => { 126 | const onChange = sinon.spy(); 127 | const data = 'Some value'; 128 | const componentProps = { 129 | onChange 130 | }; 131 | const wrapper = shallow( 132 | 133 | ); 134 | 135 | const inputComp = wrapper.find(Input); 136 | inputComp.simulate('change', 'value'); 137 | expect(onChange).to.be.calledWith('value'); 138 | }); 139 | 140 | it('has withLabel className ', () => { 141 | const wrapper = shallow( 142 | 143 | ); 144 | 145 | expect(wrapper) 146 | .prop('className') 147 | .match(/withLabelClass/); 148 | }); 149 | }); 150 | -------------------------------------------------------------------------------- /src/fields/components/Checkbox.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Checkbox from '@material-ui/core/Checkbox'; 3 | import FormControlLabel from '@material-ui/core/FormControlLabel'; 4 | 5 | const doOnChange = onChange => (e, checked) => onChange(checked); 6 | 7 | export default ({ path, label, value, type, onChange, ...rest }) => ( 8 | 16 | } 17 | label={label} 18 | /> 19 | ); 20 | -------------------------------------------------------------------------------- /src/fields/components/Checkbox.spec.jsx: -------------------------------------------------------------------------------- 1 | /* globals describe,it */ 2 | /* eslint-disable no-unused-expressions */ 3 | import React from 'react'; 4 | import chai, { expect } from 'chai'; 5 | import sinon from 'sinon'; 6 | import sinonChai from 'sinon-chai'; 7 | import chaiEnzyme from 'chai-enzyme'; 8 | import Enzyme, { mount, shallow } from 'enzyme'; 9 | import Adapter from 'enzyme-adapter-react-16'; 10 | import FormControlLabel from '@material-ui/core/FormControlLabel'; 11 | import Checkbox from '@material-ui/core/Checkbox'; 12 | import { default as CheckboxComp } from './Checkbox'; // eslint-disable-line import/no-named-default 13 | 14 | chai.use(sinonChai); 15 | 16 | chai.use(chaiEnzyme()); 17 | Enzyme.configure({ adapter: new Adapter() }); 18 | 19 | describe('Checkbox', () => { 20 | it('mounts with standard attributes (control)', () => { 21 | const checked = true; 22 | const path = 'done'; 23 | const label = 'Done'; 24 | const wrapper = mount( 25 | , 26 | ); 27 | 28 | const fcComp = wrapper.find(FormControlLabel); 29 | expect(fcComp).to.have.length(1); 30 | expect(fcComp.prop('label')).to.equal(label); 31 | 32 | const cbComp = wrapper.find(Checkbox); 33 | expect(cbComp).to.have.length(1); 34 | expect(cbComp.prop('checked')).to.equal(checked); 35 | expect(cbComp.prop('value')).to.equal(path); 36 | }); 37 | it('passes additional properties to the Checkbox component', () => { 38 | const props = { 39 | color: 'primary', 40 | }; 41 | const wrapper = mount( 42 | , 43 | ); 44 | 45 | const cbComp = wrapper.find(Checkbox); 46 | expect(cbComp.prop('color')).to.equal(props.color); 47 | }); 48 | it('calls onChange when clicked', () => { 49 | const onChange = sinon.spy(); 50 | const checked = true; 51 | const wrapper = mount( 52 | , 53 | ); 54 | 55 | const cbComp = wrapper.find('input'); 56 | expect(cbComp).to.have.length(1); 57 | cbComp.simulate('change'); 58 | expect(onChange).to.have.been.calledOnce; 59 | }); 60 | }); 61 | -------------------------------------------------------------------------------- /src/fields/components/PopoverInfo.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import IconButton from '@material-ui/core/IconButton'; 4 | import InfoIcon from '@material-ui/icons/InfoOutlined'; 5 | import Popover from '@material-ui/core/Popover'; 6 | import Typography from '@material-ui/core/Typography'; 7 | 8 | import debounce from 'lodash/debounce'; 9 | 10 | const getLTofEl = el => { 11 | const box = el.getBoundingClientRect(); 12 | return { 13 | top: box.top, 14 | left: box.left 15 | }; 16 | }; 17 | 18 | class PopoverInfo extends React.Component { 19 | state = { 20 | anchorEl: null 21 | }; 22 | 23 | handlePopoverOpen = debounce( 24 | event => { 25 | this.setState({ anchorEl: event.currentTarget }); 26 | }, 27 | 250, 28 | { leading: true } 29 | ); 30 | 31 | handlePopoverClose = debounce( 32 | () => { 33 | this.setState({ anchorEl: null }); 34 | }, 35 | 250, 36 | { leading: true } 37 | ); 38 | 39 | getAnchorPosition = () => { 40 | const { anchorEl } = this.state; 41 | const { popUpOffset } = this.props; 42 | if (!anchorEl) { 43 | return { top: 0, left: 0 }; 44 | } 45 | const pos = getLTofEl(anchorEl); 46 | if (popUpOffset) { 47 | if (popUpOffset.top) pos.top += popUpOffset.top; 48 | if (popUpOffset.left) pos.left += popUpOffset.left; 49 | } 50 | return pos; 51 | }; 52 | 53 | render() { 54 | const { anchorEl } = this.state; 55 | const open = Boolean(anchorEl); 56 | const { descriptionText, classes = {} } = this.props; 57 | return ( 58 | 67 | 68 | 82 | {descriptionText} 83 | 84 | 85 | ); 86 | } 87 | } 88 | export default PopoverInfo; 89 | -------------------------------------------------------------------------------- /src/fields/components/RadioGroup.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Radio from '@material-ui/core/Radio'; 3 | import RadioGroup from '@material-ui/core/RadioGroup'; 4 | import FormControlLabel from '@material-ui/core/FormControlLabel'; 5 | 6 | export default ({ path, options = [], value, onChange, inputProps, nullOption, ...rest }) => ( 7 | 14 | {options.map(o => } label={o.value} />)} 15 | 16 | ); 17 | -------------------------------------------------------------------------------- /src/fields/components/Select.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Select from '@material-ui/core/Select'; 3 | import MenuItem from '@material-ui/core/MenuItem'; 4 | 5 | export default ({ type, value = '', options, nullOption, onChange, ...rest }) => ( 6 | 14 | ); 15 | -------------------------------------------------------------------------------- /src/fields/components/dom-events.spec.jsx: -------------------------------------------------------------------------------- 1 | /* globals describe,it */ 2 | /* eslint-disable no-unused-expressions */ 3 | import React from 'react'; 4 | import chai, { expect } from 'chai'; 5 | import sinon from 'sinon'; 6 | import sinonChai from 'sinon-chai'; 7 | import Enzyme, { shallow } from 'enzyme'; 8 | import Adapter from 'enzyme-adapter-react-16'; 9 | import chaiEnzyme from 'chai-enzyme'; 10 | 11 | chai.use(sinonChai); 12 | 13 | chai.use(chaiEnzyme()); 14 | Enzyme.configure({ adapter: new Adapter() }); 15 | 16 | describe('dom events', () => { 17 | it('test click button', () => { 18 | const onClick = sinon.spy(); 19 | const wrapper = shallow( 20 |