├── .babelrc ├── .eslintrc.js ├── .gitignore ├── .npmignore ├── README.md ├── index.js ├── jsdom-setup.js ├── mocha.setup.js ├── package-lock.json ├── package.json ├── src ├── 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 ├── ValidationMessages.jsx ├── 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 │ │ ├── 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 ├── fields │ ├── ConfiguredField.jsx │ ├── Field.jsx │ ├── Field.spec.jsx │ ├── components │ │ ├── Checkbox.jsx │ │ ├── Checkbox.spec.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.demo.js ├── webpack.config.js └── webpack.out.json /.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-stage-0", 12 | "babel-preset-react", 13 | [ 14 | "es2015", 15 | { 16 | "modules": false 17 | } 18 | ] 19 | ], 20 | "plugins": [ 21 | "transform-class-properties" 22 | ] 23 | } 24 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: 'airbnb', 3 | "rules": { 4 | "react/prop-types": "off", 5 | "react/jsx-curly-brace-presence": "off", 6 | "react/prefer-stateless-function": "off", 7 | "jsx-a11y/anchor-is-valid": ["error", { 8 | "components": ["Link"], 9 | "specialLink": ["to"] 10 | }], 11 | "class-methods-use-this": "off", 12 | "function-paren-newline": "off", 13 | "jsx-quotes": ["error", "prefer-single"], 14 | "quote-props": ["error", "consistent"], 15 | "max-len": ["warn", { "code": 120 }], 16 | "brace-style": ["error", "stroustrup"], 17 | "no-plusplus": "off", 18 | "object-curly-newline": "off", 19 | }, 20 | "parser": "babel-eslint" 21 | } 22 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | test*.js 2 | dist 3 | node_modules 4 | mochawesome-report-unit 5 | 6 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | test*.js 2 | dist/demo.js 3 | node_modules 4 | mocheawesome-report-unit 5 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # material-ui-jsonschema-form 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 | A [live playground](https://material-ui-jsonschema-form.herokuapp.com/) is hosted on [heroku](https://dashboard.heroku.com/) 6 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | module.exports = require('./dist/bundle.js').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 | "scripts": { 4 | "build": "webpack", 5 | "build:demo": "webpack --config webpack.config.demo.js", 6 | "test": "npm run test:ci -- --watch", 7 | "heroku-postbuild": "npm run build:demo", 8 | "start": "node src/demo/server.js", 9 | "deploy": "npm run build && npm version patch && npm publish", 10 | "test:ci": "mocha-webpack --glob \"*.spec.js*\" --reporter mochawesome --require mocha.setup.js --reporter-options reportDir=mochawesome-report-unit --recursive src" 11 | }, 12 | "dependencies": { 13 | "@material-ui/icons": "^1.0.0-beta.42", 14 | "babel-core": "^6.26.0", 15 | "babel-plugin-module-resolver": "^3.1.1", 16 | "babel-preset-env": "^1.6.1", 17 | "babel-preset-es2015": "^6.24.1", 18 | "babel-preset-react": "^6.24.1", 19 | "babel-preset-stage-0": "^6.24.1", 20 | "babel-register": "^6.26.0", 21 | "body-parser": "^1.18.2", 22 | "classnames": "^2.2.5", 23 | "codemirror": "^5.36.0", 24 | "express": "^4.16.3", 25 | "immutability-helper": "^2.6.6", 26 | "lodash": "^4.17.5", 27 | "material-ui": "^1.0.0-beta.41", 28 | "react": "^16.3.1", 29 | "react-codemirror2": "^4.2.1", 30 | "react-dom": "^16.3.1", 31 | "react-hot-loader": "^3.1.3", 32 | "shortid": "^2.2.8", 33 | "typeface-roboto": "0.0.54" 34 | }, 35 | "engines": { 36 | "node": "8.9.4", 37 | "npm": "5.7.1" 38 | }, 39 | "description": "Material UI implementation of react-jsonschema-form", 40 | "devDependencies": { 41 | "babel-eslint": "^8.2.2", 42 | "babel-loader": "^7.1.4", 43 | "babel-polyfill": "^6.26.0", 44 | "chai": "^4.1.2", 45 | "chai-enzyme": "^1.0.0-beta.0", 46 | "cheerio": "^1.0.0-rc.2", 47 | "css-loader": "^0.28.11", 48 | "enzyme": "^3.3.0", 49 | "enzyme-adapter-react-16": "^1.1.1", 50 | "eslint": "^4.19.1", 51 | "eslint-config-airbnb": "^16.1.0", 52 | "eslint-loader": "^1.9.0", 53 | "eslint-plugin-class-property": "^1.1.0", 54 | "eslint-plugin-import": "^2.10.0", 55 | "eslint-plugin-jsx-a11y": "^6.0.3", 56 | "eslint-plugin-react": "^7.6.1", 57 | "extract-text-webpack-plugin": "^3.0.2", 58 | "file-loader": "^1.1.11", 59 | "html-webpack-plugin": "^2.30.1", 60 | "image-webpack-loader": "^4.2.0", 61 | "inject-loader": "^2.0.1", 62 | "jsdom": "^11.7.0", 63 | "mocha": "^4.1.0", 64 | "mocha-webpack": "^1.1.0", 65 | "mochawesome": "^3.0.2", 66 | "node-sass": "^4.8.3", 67 | "raf": "^3.4.0", 68 | "sass-loader": "^6.0.7", 69 | "sinon": "^4.5.0", 70 | "sinon-chai": "^2.14.0", 71 | "testdom": "^2.0.0", 72 | "url-loader": "^0.6.2", 73 | "webpack": "^3.11.0", 74 | "webpack-bundle-analyzer": "^2.11.1", 75 | "webpack-dev-middleware": "^2.0.6", 76 | "webpack-hot-middleware": "^2.22.0" 77 | }, 78 | "license": "ISC", 79 | "main": "index.js", 80 | "name": "material-ui-jsonschema-form", 81 | "peerDependencies": { 82 | "@material-ui/icons": "^1.0.0-beta.42", 83 | "immutability-helper": "^2.6.5", 84 | "lodash": "^4.17.5", 85 | "material-ui": "^1.0.0-beta.41", 86 | "react": "^16.2.0", 87 | "shortid": "^2.2.8" 88 | }, 89 | "version": "1.0.14" 90 | } 91 | -------------------------------------------------------------------------------- /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/styles'; 6 | import { InputLabel } from 'material-ui/Input'; 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 | else if (type === 'object') { 19 | return ; 20 | } 21 | return null; 22 | }; 23 | export const FieldSetContent = withStyles(fieldSetStyles.fieldSetContent)(RawFieldSetContent); 24 | 25 | 26 | // for unit testing 27 | export class RawFieldSet extends React.Component { 28 | shouldComponentUpdate = nextProps => !isEqual(this.props.data, nextProps.data) 29 | render() { 30 | const { className, path, classes, schema = {} } = this.props; 31 | return ( 32 |
33 | {schema.title && 34 | {schema.title} 35 | } 36 | 37 |
38 | ); 39 | } 40 | } 41 | 42 | export default withStyles(fieldSetStyles.fieldSet)(RawFieldSet); 43 | -------------------------------------------------------------------------------- /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/IconButton'; 12 | import { 13 | RawFieldSet, 14 | RawFieldSetObject, 15 | FieldSetArray, RawFieldSetArray, 16 | FieldSetContent, 17 | ReorderableFormField, RawReorderableFormField, 18 | ReorderControls, RawReorderControls, 19 | } from './FieldSet'; 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 | // check 41 | expect(wrapper).to.have.length(1); 42 | const ffComp = wrapper.find(FieldSetContent); 43 | expect(ffComp).to.have.length(1); 44 | expect(ffComp).to.have.prop('schema', schema); 45 | const legendComp = wrapper.find('legend'); 46 | expect(legendComp).to.not.be.present(); 47 | }); 48 | }); 49 | describe('FieldSetObject', () => { 50 | it('mounts with single field (control)', () => { 51 | const schema = { 52 | type: 'object', 53 | properties: { 54 | name: { 55 | 'type': 'string', 56 | 'title': 'Name', 57 | }, 58 | }, 59 | }; 60 | const data = { name: 'Bob' }; 61 | 62 | // act 63 | const wrapper = shallow(); 64 | 65 | // check 66 | expect(wrapper).to.have.length(1); 67 | expect(wrapper).to.have.prop('className').not.match(/rowClassName/); 68 | const ffComp = wrapper.find(FormField); 69 | expect(ffComp).to.have.length(1); 70 | expect(ffComp).to.have.prop('path', 'name'); 71 | expect(ffComp).to.have.prop('data', data.name); 72 | expect(ffComp).to.have.prop('objectData').deep.equal(data); 73 | }); 74 | it('respects orientation hint', () => { 75 | const schema = { 76 | type: 'object', 77 | properties: { 78 | name: { 79 | 'type': 'string', 80 | 'title': 'Name', 81 | }, 82 | }, 83 | }; 84 | const uiSchema = { 85 | 'ui:orientation': 'row', 86 | }; 87 | const data = { name: 'Bob' }; 88 | 89 | // act 90 | const wrapper = shallow( 91 | , 92 | ); 93 | 94 | // check 95 | expect(wrapper).to.have.length(1); 96 | expect(wrapper).to.have.prop('className').match(/rowClassName/); 97 | const ffComp = wrapper.find(FormField); 98 | expect(ffComp).to.have.length(1); 99 | expect(ffComp).to.have.prop('path', 'name'); 100 | expect(ffComp).to.have.prop('data', data.name); 101 | }); 102 | }); 103 | describe('FieldSetArray', () => { 104 | it('handles simple, orderable list of strings', () => { 105 | const path = 'names'; 106 | const defaultValue = 'abc'; 107 | const startIdx = 2; 108 | const schema = { 109 | type: 'array', 110 | title: 'My list', 111 | items: { 112 | 'type': 'string', 113 | 'title': 'Name', 114 | 'default': defaultValue, 115 | }, 116 | }; 117 | const onMoveItemUp = sinon.stub(); 118 | const onMoveItemDown = sinon.stub(); 119 | const onDeleteItem = sinon.stub(); 120 | forEach([0, 1], i => onMoveItemUp.withArgs(path, startIdx + i).returns(`moveUp${i}`)); 121 | forEach([0, 1], i => onMoveItemDown.withArgs(path, startIdx + i).returns(`moveDown${i}`)); 122 | forEach([0, 1], i => onDeleteItem.withArgs(path, startIdx + i).returns(`deleteItem${i}`)); 123 | const uiSchema = { 124 | items: { 125 | 'ui:widget': 'textarea', 126 | }, 127 | }; 128 | const data = ['Bob', 'Harry']; 129 | const onAddItem = sinon.spy(); 130 | 131 | // act 132 | const wrapper = shallow( 133 | , 145 | ); 146 | 147 | // check 148 | expect(wrapper).to.have.length(1); 149 | const ffComp = wrapper.find(ReorderableFormField); 150 | expect(ffComp).to.have.length(2); 151 | forEach([0, 1], (i) => { 152 | expect(ffComp.at(i)).to.have.prop('path', `names[${i + startIdx}]`); 153 | expect(ffComp.at(i)).to.have.prop('data', data[i]); 154 | expect(ffComp.at(i)).to.have.prop('schema', schema.items); 155 | expect(ffComp.at(i)).to.have.prop('uiSchema', uiSchema.items); 156 | expect(ffComp.at(i)).to.have.prop('onMoveItemUp', `moveUp${i}`); 157 | expect(ffComp.at(i)).to.have.prop('onMoveItemDown', `moveDown${i}`); 158 | expect(ffComp.at(i)).to.have.prop('onDeleteItem', `deleteItem${i}`); 159 | }); 160 | expect(ffComp.at(0)).to.have.prop('first', true); 161 | expect(ffComp.at(0)).to.have.prop('last', false); 162 | expect(ffComp.at(1)).to.have.prop('first', false); 163 | expect(ffComp.at(1)).to.have.prop('last', true); 164 | const addButton = wrapper.find(IconButton); 165 | expect(addButton).to.be.present(); 166 | addButton.simulate('click'); 167 | expect(onAddItem).to.be.calledWith(path, defaultValue); 168 | }); 169 | it('handles simple, fixed list of strings', () => { 170 | const path = 'names'; 171 | const schema = { 172 | type: 'array', 173 | title: 'My list', 174 | items: [{ 175 | 'type': 'string', 176 | 'title': 'Name', 177 | }, { 178 | 'type': 'boolean', 179 | 'title': 'Name', 180 | }], 181 | }; 182 | const uiSchema = { 183 | items: [{ 184 | 'ui:widget': 'textarea', 185 | }, { 186 | 'ui:widget': 'checkbox', 187 | }], 188 | }; 189 | const data = ['Bob', false]; 190 | 191 | // act 192 | const wrapper = shallow( 193 | , 194 | ); 195 | 196 | // check 197 | expect(wrapper).to.have.length(1); 198 | const ffComp = wrapper.find(FormField); 199 | const rffComp = wrapper.find(ReorderableFormField); 200 | expect(ffComp).to.have.length(2); 201 | expect(rffComp).to.have.length(0); 202 | forEach([0, 1], (idx) => { 203 | expect(ffComp.at(idx)).to.have.prop('path', `names[${idx}]`); 204 | expect(ffComp.at(idx)).to.have.prop('data', data[idx]); 205 | expect(ffComp.at(idx)).to.have.prop('schema', schema.items[idx]); 206 | expect(ffComp.at(idx)).to.have.prop('uiSchema', uiSchema.items[idx]); 207 | }); 208 | }); 209 | it('handles simple, fixed list of strings with additional items', () => { 210 | const path = 'names'; 211 | const onMoveItemUp = 'onMoveItemUp'; 212 | const onMoveItemDown = 'onMoveItemDown'; 213 | const onDeleteItem = 'onDeleteItem'; 214 | 215 | const schema = { 216 | type: 'array', 217 | title: 'My list', 218 | items: [{ 219 | 'type': 'string', 220 | 'title': 'Name', 221 | }, { 222 | 'type': 'boolean', 223 | 'title': 'Name', 224 | }], 225 | additionalItems: { 226 | 'type': 'string', 227 | 'title': 'Name', 228 | }, 229 | }; 230 | const uiSchema = { 231 | items: [{ 232 | 'ui:widget': 'textarea', 233 | }, { 234 | 'ui:widget': 'checkbox', 235 | }], 236 | additionalItems: { 237 | 'ui:title': 'Children', 238 | }, 239 | }; 240 | const data = ['Bob', false, 'Harry', 'Susan']; 241 | 242 | // act 243 | const wrapper = shallow( 244 | , 254 | ); 255 | 256 | // check 257 | expect(wrapper).to.have.length(1); 258 | const ffComp = wrapper.find(FormField); 259 | expect(ffComp).to.have.length(2); 260 | forEach([0, 1], (i) => { 261 | expect(ffComp.at(i)).to.have.prop('path', `names[${i}]`); 262 | expect(ffComp.at(i)).to.have.prop('data', data[i]); 263 | expect(ffComp.at(i)).to.have.prop('schema', schema.items[i]); 264 | expect(ffComp.at(i)).to.have.prop('uiSchema', uiSchema.items[i]); 265 | expect(ffComp.at(i)).to.not.have.prop('onMoveItemUp'); 266 | expect(ffComp.at(i)).to.not.have.prop('onMoveItemDown'); 267 | expect(ffComp.at(i)).to.not.have.prop('onDeleteItem'); 268 | }); 269 | const fsArrayComp = wrapper.find(FieldSetArray); 270 | expect(fsArrayComp).to.be.present(); 271 | expect(fsArrayComp).to.have.prop('path', path); 272 | expect(fsArrayComp).to.have.prop('data').deep.equal(['Harry', 'Susan']); 273 | expect(fsArrayComp).to.have.prop('schema').deep.equal({ type: 'array', items: schema.additionalItems }); 274 | expect(fsArrayComp).to.have.prop('uiSchema', uiSchema.additionalItems); 275 | expect(fsArrayComp).to.have.prop('startIdx', schema.items.length); 276 | expect(fsArrayComp).to.have.prop('onMoveItemUp', onMoveItemUp); 277 | expect(fsArrayComp).to.have.prop('onMoveItemDown', onMoveItemDown); 278 | expect(fsArrayComp).to.have.prop('onDeleteItem', onDeleteItem); 279 | }); 280 | }); 281 | describe('ReorderControls', () => { 282 | it('renders buttons with callbacks', () => { 283 | // prepare 284 | const onMoveItemUp = sinon.spy(); 285 | const onMoveItemDown = sinon.spy(); 286 | const onDeleteItem = sinon.spy(); 287 | 288 | // act 289 | const wrapper = shallow( 290 | , 296 | ); 297 | 298 | // check 299 | expect(wrapper).to.have.length(1); 300 | const buttonList = wrapper.find(IconButton); 301 | expect(buttonList).to.have.length(3); 302 | buttonList.at(0).simulate('click'); 303 | expect(onMoveItemUp).to.have.been.called; 304 | buttonList.at(1).simulate('click'); 305 | expect(onMoveItemDown).to.have.been.called; 306 | buttonList.at(2).simulate('click'); 307 | expect(onDeleteItem).to.have.been.called; 308 | }); 309 | it('ReorderControls - first', () => { 310 | // act 311 | const wrapper = shallow( 312 | , 313 | ); 314 | 315 | // check 316 | expect(wrapper).to.have.length(1); 317 | const buttonList = wrapper.find(IconButton); 318 | expect(buttonList).to.have.length(3); 319 | expect(buttonList.at(0)).to.have.prop('disabled', true); 320 | expect(buttonList.at(1)).to.have.prop('disabled', false); 321 | expect(buttonList.at(2)).to.not.have.prop('disabled'); 322 | }); 323 | it('ReorderControls - last', () => { 324 | // act 325 | const wrapper = shallow( 326 | , 327 | ); 328 | 329 | // check 330 | expect(wrapper).to.have.length(1); 331 | const buttonList = wrapper.find(IconButton); 332 | expect(buttonList).to.have.length(3); 333 | expect(buttonList.at(0)).to.have.prop('disabled', false); 334 | expect(buttonList.at(1)).to.have.prop('disabled', true); 335 | expect(buttonList.at(2)).to.not.have.prop('disabled'); 336 | }); 337 | }); 338 | describe('ReorderableFormField', () => { 339 | it('ReorderableFormField - control', () => { 340 | const path = 'path'; 341 | const first = 'first'; 342 | const last = 'last'; 343 | // act 344 | const wrapper = shallow( 345 | , 351 | ); 352 | 353 | // check 354 | const ffComp = wrapper.find(FormField); 355 | expect(ffComp).to.have.length(1); 356 | const controls = wrapper.find(ReorderControls); 357 | expect(controls).to.have.length(1); 358 | expect(controls).to.have.prop('first', first); 359 | expect(controls).to.have.prop('last', last); 360 | }); 361 | }); 362 | }); 363 | -------------------------------------------------------------------------------- /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/IconButton'; 5 | import AddCircle from '@material-ui/icons/AddCircle'; 6 | import isArray from 'lodash/isArray'; 7 | import { withStyles } from 'material-ui/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 | return ( 19 |
20 | {!isArray(schema.items) && !schema.uniqueItems && ( 21 |
22 | {(data || []).map((d, idx) => ( 23 | 38 | ))} 39 |
40 | 41 | 42 | 43 |
44 |
45 | )} 46 | {isArray(schema.items) && (data || []).map((d, idx) => { 47 | if (idx < schema.items.length) { 48 | return (); 58 | } 59 | return null; 60 | })} 61 | {(!isArray(schema.items) && schema.uniqueItems && schema.items.enum) && schema.items.enum.map(d => ())} 71 | {schema.additionalItems && 72 | 85 | } 86 |
87 | ); 88 | }; 89 | export default withStyles(fieldSetStyles.fieldSetArray)(RawFieldSetArray); 90 | -------------------------------------------------------------------------------- /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/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/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/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/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 | }, 6 | listItem: { 7 | 'border': `1px dotted ${theme.palette.primary.main}`, 8 | 'margin': theme.spacing.unit, 9 | 'padding': theme.spacing.unit, 10 | }, 11 | }), 12 | fieldSetObject: { 13 | 'root': { 14 | 'display': 'flex', 15 | 'flexDirection': 'column', 16 | '&$row': { 17 | flexDirection: 'row', 18 | }, 19 | }, 20 | 'row': {}, 21 | 'listItem': {}, 22 | }, 23 | fieldSetArray: theme => ({ 24 | 'root': { 25 | display: 'flex', 26 | flexDirection: 'column', 27 | }, 28 | 'listItem': {}, 29 | 'addItemBtn': { 30 | 'display': 'flex', 31 | 'justifyContent': 'flex-end', 32 | '&>button': { 33 | 'background': theme.palette.primary.main, 34 | 'width': '3.75em', 35 | 'color': theme.palette.common.white, 36 | 'height': '1.25em', 37 | 'borderRadius': 5, 38 | }, 39 | }, 40 | }), 41 | reorderable: { 42 | 'root': { 43 | 'display': 'flex', 44 | 'alignItems': 'baseline', 45 | 'justifyContent': 'space-between', 46 | '& >fieldset': { 47 | width: '100%', 48 | }, 49 | }, 50 | 'listItem': {}, 51 | }, 52 | reorderControls: theme => ({ 53 | root: { 54 | 'display': 'flex', 55 | 'border': `1px solid ${theme.palette.grey[400]}`, 56 | 'borderRadius': 5, 57 | '& >button': { 58 | 'borderRadius': 0, 59 | 'width': '1.25em', 60 | 'height': '1.25em', 61 | '&:not(:last-child)': { 62 | borderRight: `1px solid ${theme.palette.grey[400]}`, 63 | }, 64 | }, 65 | }, 66 | remove: { 67 | background: theme.palette.error.main, 68 | color: theme.palette.grey[800], 69 | }, 70 | }), 71 | fieldSetContent: { 72 | 'root': {}, 73 | 'listItem': {}, 74 | }, 75 | }); 76 | -------------------------------------------------------------------------------- /src/FieldSet/index.js: -------------------------------------------------------------------------------- 1 | export { default } from './FieldSet'; 2 | -------------------------------------------------------------------------------- /src/Form.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import isEqual from 'lodash/isEqual'; 3 | import { generate } from 'shortid'; 4 | import { withStyles } from 'material-ui/styles'; 5 | import Paper from 'material-ui/Paper'; 6 | import formStyles from './form-styles'; 7 | import FormField from './FormField'; 8 | import updateFormData, { addListItem, removeListItem, moveListItem } from './helpers/update-form-data'; 9 | import getValidationResult from './helpers/validation'; 10 | import ValidationMessages from './ValidationMessages'; 11 | import FormButtons from './FormButtons'; 12 | 13 | class Form extends React.Component { 14 | state = { 15 | data: this.props.formData, 16 | validation: getValidationResult(this.props.schema, this.props.formData), 17 | id: generate(), 18 | } 19 | componentWillReceiveProps = (nextProps) => { 20 | let validation; 21 | if (!isEqual(nextProps.schema, this.props.schema)) { 22 | validation = {}; 23 | } 24 | else { 25 | validation = getValidationResult(this.props.schema, nextProps.formData); 26 | } 27 | this.setState({ 28 | validation, 29 | data: nextProps.formData, 30 | }); 31 | } 32 | onChange = field => (value) => { 33 | const data = updateFormData(this.state.data, field, value); 34 | this.setState({ 35 | data, 36 | validation: getValidationResult(this.props.schema, data), 37 | }, this.notifyChange); 38 | } 39 | onMoveItemUp = (path, idx) => () => { 40 | this.setState({ data: moveListItem(this.state.data, path, idx, -1) }, this.notifyChange); 41 | } 42 | onMoveItemDown = (path, idx) => () => { 43 | this.setState({ data: moveListItem(this.state.data, path, idx, 1) }, this.notifyChange); 44 | } 45 | onDeleteItem = (path, idx) => () => { 46 | this.setState({ data: removeListItem(this.state.data, path, idx) }, this.notifyChange); 47 | } 48 | onAddItem = (path, defaultValue) => () => { 49 | this.setState({ data: addListItem(this.state.data, path, defaultValue || '') }, this.notifyChange); 50 | } 51 | onSubmit = () => { 52 | this.props.onSubmit({ formData: this.state.data }); 53 | } 54 | notifyChange = () => { 55 | const { onChange } = this.props; 56 | const { data } = this.state; 57 | if (onChange) { 58 | onChange({ formData: data }); 59 | } 60 | } 61 | render() { 62 | const { classes, formData, onSubmit, onChange, onCancel, ...rest } = this.props; 63 | const { validation, id } = this.state; 64 | return ( 65 | 66 | 67 | 80 | 81 | 82 | ); 83 | } 84 | } 85 | 86 | export default withStyles(formStyles)(Form); 87 | -------------------------------------------------------------------------------- /src/FormButtons.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import classNames from 'classnames'; 3 | import Button from 'material-ui/Button'; 4 | 5 | export class RawFormButtons extends React.Component { 6 | shouldComponentUpdate = () => false 7 | render() { 8 | const { classes, onCancel, onSubmit } = this.props; 9 | return (onCancel || onSubmit) && ( 10 |
11 | {onCancel && 12 | 19 | } 20 | {onSubmit && 21 | 29 | } 30 |
31 | ); 32 | } 33 | } 34 | 35 | export default RawFormButtons; 36 | -------------------------------------------------------------------------------- /src/FormField.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import isEqual from 'lodash/isEqual'; 3 | import { withStyles } from 'material-ui/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 | render() { 12 | const { classes, schema, data, uiSchema = {}, onChange, path, ...rest } = this.props; 13 | const { type } = schema; 14 | if (type === 'object' || type === 'array') { 15 | return
; 16 | } 17 | return ( 18 | 27 | ); 28 | } 29 | } 30 | 31 | export default withStyles(styles)(RawFormField); 32 | -------------------------------------------------------------------------------- /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 { RawFormField as FormField } from './FormField'; 11 | import Field from './fields'; 12 | import FieldSet from './FieldSet'; 13 | 14 | const classes = { 15 | field: '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 | }; 32 | const data = 'Bob'; 33 | onChange.returns('onChangeFunc'); 34 | 35 | // act 36 | const wrapper = shallow( 37 | , 38 | ); 39 | 40 | // check 41 | expect(wrapper).to.be.present(); 42 | const ffComp = wrapper.find(Field); 43 | expect(ffComp).to.have.length(1); 44 | expect(ffComp).to.have.prop('className', classes.field); 45 | expect(ffComp).to.have.prop('path', path); 46 | expect(ffComp).to.have.prop('schema', schema); 47 | expect(ffComp).to.have.prop('data', data); 48 | expect(ffComp).to.have.prop('uiSchema', uiSchema); 49 | expect(onChange).calledWith(path); 50 | expect(ffComp).to.have.prop('onChange', 'onChangeFunc'); 51 | }); 52 | it('spreads additional properties to Field', () => { 53 | const schema = { 54 | name: { 55 | 'type': 'string', 56 | }, 57 | }; 58 | const myProp = 'blah'; 59 | 60 | // act 61 | const wrapper = shallow( 62 | , 63 | ); 64 | 65 | // check 66 | expect(wrapper).to.be.present(); 67 | const ffComp = wrapper.find(Field); 68 | expect(ffComp).to.have.prop('myProp', myProp); 69 | }); 70 | it('renders object as FieldSet, passing all properties', () => { 71 | const onChange = sinon.stub(); 72 | const path = 'name'; 73 | const schema = { 74 | 'type': 'object', 75 | 'properties': { 76 | firstName: { 77 | type: 'string', 78 | title: 'First Name', 79 | }, 80 | surname: { 81 | type: 'string', 82 | title: 'Surname', 83 | }, 84 | }, 85 | }; 86 | const data = { 87 | firstName: 'Bob', 88 | surname: 'Hope', 89 | }; 90 | const uiSchema = { 91 | firstName: {}, 92 | surname: {}, 93 | }; 94 | 95 | // act 96 | const wrapper = shallow( 97 | , 98 | ); 99 | 100 | // check 101 | expect(wrapper).to.be.present(); 102 | const fsComp = wrapper.find(FieldSet); 103 | expect(fsComp).to.be.have.length(1); 104 | expect(fsComp).to.have.prop('path', path); 105 | expect(fsComp).to.have.prop('schema', schema); 106 | expect(fsComp).to.have.prop('data', data); 107 | expect(fsComp).to.have.prop('uiSchema', uiSchema); 108 | expect(fsComp).to.have.prop('onChange', onChange); 109 | }); 110 | }); 111 | -------------------------------------------------------------------------------- /src/ValidationMessages.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { withStyles } from 'material-ui/styles'; 3 | import keys from 'lodash/keys'; 4 | import filter from 'lodash/filter'; 5 | 6 | const validationStyles = {}; 7 | 8 | const Validation = ({ validation }) => ( 9 |
10 |

{validation.message}

11 |
12 | ); 13 | 14 | const Validations = ({ validation }) => ( 15 |
16 | {validation.map((v, idx) => ()) // eslint-disable-line react/no-array-index-key,max-len 17 | } 18 |
19 | ); 20 | const ValidationMessages = ({ validation }) => ( 21 |
22 | {validation && filter(keys(validation), (k) => { 23 | const v = validation[k]; 24 | return v && v.length > 0; 25 | }).map(v => ( 26 | 27 | )) 28 | } 29 |
30 | ); 31 | 32 | export default withStyles(validationStyles)(ValidationMessages); 33 | -------------------------------------------------------------------------------- /src/demo/DemoHome.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { withStyles } from 'material-ui/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.simple, 13 | } 14 | onSelectMenuItem = type => () => { 15 | this.setState({ selectedDemo: type }); 16 | } 17 | render() { 18 | const { classes } = this.props; 19 | return ( 20 |
21 | 22 | 23 |
24 | ); 25 | } 26 | } 27 | 28 | export default withStyles(styles)(Demo); 29 | -------------------------------------------------------------------------------- /src/demo/MuiRoot.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import 'typeface-roboto'; // eslint-disable-line import/extensions 3 | import CssBaseline from 'material-ui/CssBaseline'; 4 | 5 | import { MuiThemeProvider } from 'material-ui/styles'; 6 | import theme from './theme'; 7 | 8 | export default ({ children }) => ( 9 | 10 | 11 | {children} 12 | 13 | ); 14 | -------------------------------------------------------------------------------- /src/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 | -------------------------------------------------------------------------------- /src/demo/body/Body.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { withStyles } from 'material-ui/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 | -------------------------------------------------------------------------------- /src/demo/body/Example.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { withStyles } from 'material-ui/styles'; 3 | import Paper from 'material-ui/Paper'; 4 | import styles from './example-styles'; 5 | import Source from './Source'; 6 | import Form from '../../Form'; 7 | 8 | class Example extends React.Component { 9 | state = { 10 | ...this.props.data, 11 | } 12 | componentWillReceiveProps = (nextProps) => { 13 | this.setState({ 14 | ...nextProps.data, 15 | }); 16 | } 17 | onChange = type => (value) => { 18 | this.setState({ [type]: value }); 19 | } 20 | onFormChanged = ({ formData }) => { 21 | this.setState({ formData }); 22 | } 23 | onSubmit = (value) => { 24 | console.log('onSubmit: %s', JSON.stringify(value)); // eslint-disable-line no-console 25 | } 26 | onCancel = () => { 27 | this.setState({ 28 | ...this.props.data, 29 | }); 30 | } 31 | render() { 32 | const { data, classes } = this.props; 33 | const { title } = data; 34 | const { schema, uiSchema, formData } = this.state; 35 | return ( 36 | 37 |

{title}

38 |
39 |
40 |
41 | 42 |
43 |
44 | 45 | 46 |
47 |
48 |
49 |
57 |
58 |
59 |
60 | ); 61 | } 62 | } 63 | export default withStyles(styles)(Example); 64 | -------------------------------------------------------------------------------- /src/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/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 | componentWillReceiveProps = (nextProps) => { 42 | const source = JSON.stringify(nextProps.source, null, 2); 43 | this.setState({ 44 | source, 45 | valid: isValid(source), 46 | }); 47 | } 48 | onChange = (editor, data, value) => { 49 | this.setState({ source: value }); 50 | } 51 | onBeforeChange = (editor, data, value) => { 52 | const { onChange } = this.props; 53 | const parsed = isValid(value); 54 | 55 | this.setState({ 56 | valid: parsed, 57 | source: value, 58 | }); 59 | if (parsed && onChange) { 60 | onChange(parsed); 61 | } 62 | } 63 | render() { 64 | const { source, valid } = this.state; 65 | const { classes, title } = this.props; 66 | const Icon = valid ? Valid : Invalid; 67 | return ( 68 |
69 |
70 |
71 | 72 |
73 |

{title}

74 |
75 |
76 |
77 | 83 |
84 |
85 |
86 | ); 87 | } 88 | } 89 | export default withStyles(sourceStyles)(Source); 90 | -------------------------------------------------------------------------------- /src/demo/body/body-styles.js: -------------------------------------------------------------------------------- 1 | 2 | export default theme => ({ 3 | body: { 4 | 'padding': theme.spacing.unit * 2, 5 | [theme.breakpoints.up('lg')]: { 6 | width: 'calc(100% - 250px)', 7 | marginLeft: 250, 8 | }, 9 | }, 10 | }); 11 | -------------------------------------------------------------------------------- /src/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: '80%', 31 | marginLeft: theme.spacing.unit * 2, 32 | }, 33 | 'title': { 34 | 'marginLeft': theme.spacing.unit * 2, 35 | }, 36 | 'invalid': { 37 | 38 | }, 39 | 'ctr': {}, 40 | }); 41 | -------------------------------------------------------------------------------- /src/demo/body/example-data.js: -------------------------------------------------------------------------------- 1 | import { simple, nested } from './examples'; 2 | 3 | export default ({ 4 | simple: { 5 | title: 'Simple', 6 | examples: [ 7 | simple, 8 | ], 9 | }, 10 | nested: { 11 | title: 'Nested', 12 | examples: [ 13 | nested, 14 | ], 15 | }, 16 | }); 17 | -------------------------------------------------------------------------------- /src/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 | -------------------------------------------------------------------------------- /src/demo/body/index.js: -------------------------------------------------------------------------------- 1 | export { default } from './Body'; 2 | -------------------------------------------------------------------------------- /src/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 | -------------------------------------------------------------------------------- /src/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 | 12 | -------------------------------------------------------------------------------- /src/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 | -------------------------------------------------------------------------------- /src/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 | -------------------------------------------------------------------------------- /src/demo/examples/arrays/form-data.json: -------------------------------------------------------------------------------- 1 | { 2 | "fixedItemsList": [ 3 | "Some text", 4 | true, 5 | "one" 6 | ] 7 | } 8 | -------------------------------------------------------------------------------- /src/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 | 12 | -------------------------------------------------------------------------------- /src/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 | -------------------------------------------------------------------------------- /src/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 | -------------------------------------------------------------------------------- /src/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 | -------------------------------------------------------------------------------- /src/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 | 12 | -------------------------------------------------------------------------------- /src/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 | -------------------------------------------------------------------------------- /src/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 | -------------------------------------------------------------------------------- /src/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 budget from './budget'; 8 | import multipleChoice from './multiple-choice'; 9 | import radioChoice from './radio-choice'; 10 | 11 | export default ({ 12 | simple, 13 | single, 14 | nested, 15 | numbers, 16 | arrays, 17 | validation, 18 | budget, 19 | multipleChoice, 20 | radioChoice, 21 | }); 22 | -------------------------------------------------------------------------------- /src/demo/examples/multiple-choice/form-data.json: -------------------------------------------------------------------------------- 1 | { 2 | "multipleChoicesList": [ 3 | "foo", 4 | "bar" 5 | ] 6 | } 7 | -------------------------------------------------------------------------------- /src/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 | 12 | -------------------------------------------------------------------------------- /src/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 | -------------------------------------------------------------------------------- /src/demo/examples/multiple-choice/ui-schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "multipleChoicesList": { 3 | "ui:widget": "checkboxes" 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /src/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 | -------------------------------------------------------------------------------- /src/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 | 12 | -------------------------------------------------------------------------------- /src/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 | -------------------------------------------------------------------------------- /src/demo/examples/nested/ui-schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "tasks": { 3 | "items": { 4 | "details": { 5 | "ui:widget": "textarea" 6 | } 7 | } 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/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 | -------------------------------------------------------------------------------- /src/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 | 12 | -------------------------------------------------------------------------------- /src/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 | -------------------------------------------------------------------------------- /src/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 | 12 | -------------------------------------------------------------------------------- /src/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 | -------------------------------------------------------------------------------- /src/demo/examples/numbers/nested/ui-schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "tasks": { 3 | "items": { 4 | "details": { 5 | "ui:widget": "textarea" 6 | } 7 | } 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/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 | -------------------------------------------------------------------------------- /src/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 | -------------------------------------------------------------------------------- /src/demo/examples/radio-choice/form-data.json: -------------------------------------------------------------------------------- 1 | { 2 | "numberEnumRadio": 3 3 | } 4 | -------------------------------------------------------------------------------- /src/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 | 12 | -------------------------------------------------------------------------------- /src/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 | -------------------------------------------------------------------------------- /src/demo/examples/radio-choice/ui-schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "numberEnumRadio": { 3 | "ui:widget": "radio", 4 | "ui:options": { 5 | "inline": true 6 | } 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /src/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 | -------------------------------------------------------------------------------- /src/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 | 12 | -------------------------------------------------------------------------------- /src/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 | -------------------------------------------------------------------------------- /src/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 | -------------------------------------------------------------------------------- /src/demo/examples/single/form-data.json: -------------------------------------------------------------------------------- 1 | "initial value" 2 | -------------------------------------------------------------------------------- /src/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 | 12 | -------------------------------------------------------------------------------- /src/demo/examples/single/schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "title": "A single-field form", 3 | "type": "string" 4 | } 5 | -------------------------------------------------------------------------------- /src/demo/examples/single/ui-schema.json: -------------------------------------------------------------------------------- 1 | {} 2 | -------------------------------------------------------------------------------- /src/demo/examples/validation/form-data.json: -------------------------------------------------------------------------------- 1 | { 2 | "age": 8 3 | } 4 | -------------------------------------------------------------------------------- /src/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 | 12 | -------------------------------------------------------------------------------- /src/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 | -------------------------------------------------------------------------------- /src/demo/examples/validation/ui-schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "pass1": { 3 | "ui:widget": "password" 4 | }, 5 | "pass2": { 6 | "ui:widget": "password" 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /src/demo/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 7 | 8 | 9 | 10 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /src/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 | -------------------------------------------------------------------------------- /src/demo/main.scss: -------------------------------------------------------------------------------- 1 | body { 2 | padding: 0; 3 | } 4 | 5 | p { 6 | margin: 0; 7 | padding: 0; 8 | } 9 | -------------------------------------------------------------------------------- /src/demo/menu/LeftDrawer.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { withStyles } from 'material-ui/styles'; 3 | import Drawer from 'material-ui/Drawer'; 4 | import Hidden from 'material-ui/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 | -------------------------------------------------------------------------------- /src/demo/menu/Menu.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { withStyles } from 'material-ui/styles'; 3 | import AppBar from 'material-ui/AppBar'; 4 | import Toolbar from 'material-ui/Toolbar'; 5 | import Typography from 'material-ui/Typography'; 6 | import Hidden from 'material-ui/Hidden'; 7 | import IconButton from 'material-ui/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 | material-ui-jsonschema-form 39 |
40 |
41 | 42 |
43 | ); 44 | } 45 | } 46 | export default withStyles(menuStyles)(RawMenuAppBar); 47 | -------------------------------------------------------------------------------- /src/demo/menu/MenuItems.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import keys from 'lodash/keys'; 3 | import { withStyles } from 'material-ui/styles'; 4 | import List, { ListItem, ListItemText, ListSubheader } from 'material-ui/List'; 5 | import examples from '../examples'; 6 | 7 | import menuStyles from './menu-styles'; 8 | 9 | export default withStyles(menuStyles)(({ toggleDrawer, classes, onSelectMenuItem }) => ( 10 |
17 | Showcase}> 18 | {keys(examples).map(e => ( 19 | 20 | 21 | 22 | ))} 23 | 24 |
25 | )); 26 | 27 | -------------------------------------------------------------------------------- /src/demo/menu/index.js: -------------------------------------------------------------------------------- 1 | export { default } from './Menu'; 2 | -------------------------------------------------------------------------------- /src/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 | -------------------------------------------------------------------------------- /src/demo/server.js: -------------------------------------------------------------------------------- 1 | require('babel-register'); 2 | require('./server.main'); 3 | -------------------------------------------------------------------------------- /src/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 | -------------------------------------------------------------------------------- /src/demo/theme.js: -------------------------------------------------------------------------------- 1 | import { createMuiTheme } from 'material-ui/styles'; 2 | import teal from 'material-ui/colors/teal'; 3 | import red from 'material-ui/colors/red'; 4 | import blue from 'material-ui/colors/lightBlue'; 5 | 6 | const theme = { 7 | palette: { 8 | primary: { 9 | main: blue[600], 10 | }, 11 | secondary: teal, 12 | error: red, 13 | }, 14 | overrides: { 15 | MuiInput: { 16 | root: { 17 | fontSize: 'inherit', 18 | }, 19 | }, 20 | }, 21 | }; 22 | 23 | export default createMuiTheme(theme); 24 | -------------------------------------------------------------------------------- /src/fields/ConfiguredField.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import classNames from 'classnames'; 3 | import { withStyles } from 'material-ui/styles'; 4 | import { FormControl, FormHelperText } from 'material-ui/Form'; 5 | import Input from 'material-ui/Input'; 6 | import fieldStyles from './field-styles'; 7 | 8 | // for unit testing only 9 | export class RawConfiguredField extends React.Component { 10 | shouldComponentUpdate = nextProps => this.props.data !== nextProps.data 11 | render() { 12 | const { 13 | classes = {}, data, type, descriptionText, helpText, Component = Input, LabelComponent, labelComponentProps = {}, 14 | title, className, componentProps = {}, id, 15 | } = this.props; 16 | return ( 17 | 18 | {LabelComponent && title && 19 | {title} 22 | 23 | } 24 | {descriptionText &&

{descriptionText}

} 25 | 31 | {helpText && {helpText}} 32 |
33 | ); 34 | } 35 | } 36 | export default withStyles(fieldStyles)(RawConfiguredField); 37 | -------------------------------------------------------------------------------- /src/fields/Field.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import configureComponent from './configure'; 3 | import ConfiguredField from './ConfiguredField'; 4 | 5 | export default (props) => { 6 | const { path, id, schema, data, uiSchema } = props; 7 | const { type } = schema; 8 | const htmlId = `${id}_${path}`; 9 | const { 10 | Component, LabelComponent, componentProps, labelComponentProps, className, title, 11 | } = configureComponent({ ...props, htmlId }); 12 | 13 | const descriptionText = uiSchema['ui:description']; 14 | const helpText = uiSchema['ui:help']; 15 | return ( 16 | 29 | ); 30 | }; 31 | -------------------------------------------------------------------------------- /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/Input'; 11 | import { FormControl, FormHelperText, FormLabel } from 'material-ui/Form'; 12 | import { RadioGroup } from './components'; 13 | import { ConfiguredField } from './Field'; 14 | 15 | const classes = { 16 | description: 'description', 17 | root: 'rootClassName', 18 | myComp: 'myCompClassName', 19 | withLabel: 'withLabelClass', 20 | }; 21 | 22 | chai.use(chaiEnzyme()); 23 | chai.use(sinonChai); 24 | Enzyme.configure({ adapter: new Adapter() }); 25 | 26 | describe('Field', () => { 27 | it('mounts with standard attributes (control)', () => { 28 | const componentProps = { 29 | multiline: true, 30 | }; 31 | const data = 'Hello'; 32 | const type = 'string'; 33 | const wrapper = shallow( 34 | , 35 | ); 36 | // test FormControl properties 37 | const FC = wrapper.find(FormControl); 38 | expect(FC).to.have.length(1); 39 | expect(FC).to.have.prop('className').match(/rootClassName/).not.match(/withLabelClass/); 40 | 41 | // no helpText, descriptionText or LabelComponent 42 | expect(FC.children()).to.have.length(1); // control 43 | 44 | // test Component properties 45 | const Component = wrapper.find(Input); // control 46 | expect(Component).to.be.present(); 47 | expect(Component).to.have.prop('multiline', componentProps.multiline); 48 | expect(Component).to.have.prop('value', data); 49 | expect(Component).to.have.prop('type', type); 50 | expect(Component).to.not.have.prop('className'); // control 51 | }); 52 | it('applies given className', () => { 53 | const wrapper = shallow(); 54 | const Component = wrapper.find(Input); 55 | expect(Component).to.be.present(); 56 | expect(Component).to.have.prop('className', classes.myComp); 57 | }); 58 | it('renders provided Component', () => { 59 | const wrapper = shallow(); 60 | expect(wrapper.find(Input)).to.not.be.present(); 61 | expect(wrapper.find(RadioGroup)).to.be.present(); 62 | }); 63 | it('renders provided LabelComponent with title and labelComponentProps', () => { 64 | const labelComponentProps = { 65 | style: 'bold', 66 | }; 67 | const title = 'Hello'; 68 | const DummyLabel = ({ children }) =>
{children}
; 69 | 70 | const wrapper = shallow( 71 | , 72 | ); 73 | 74 | const labelComp = wrapper.find(DummyLabel); 75 | expect(labelComp).to.be.present(); 76 | expect(labelComp).to.have.prop('style', labelComponentProps.style); 77 | expect(labelComp.children()).to.have.length(1); 78 | expect(labelComp.childAt(0)).to.have.text(title); 79 | }); 80 | it('renders provided descriptionText', () => { 81 | const descriptionText = 'This is a field'; 82 | const wrapper = shallow(); 83 | 84 | const descriptionComp = wrapper.find('p'); 85 | expect(descriptionComp).to.have.length(1); 86 | expect(descriptionComp).to.have.prop('className', classes.description); 87 | expect(descriptionComp).to.have.text(descriptionText); 88 | }); 89 | it('renders provided helpText', () => { 90 | const helpText = 'Help! I need somebody!'; 91 | const id = 'unq-id'; 92 | const wrapper = shallow(); 93 | 94 | const helpComp = wrapper.find(FormHelperText); 95 | expect(helpComp).to.be.present(); 96 | expect(helpComp).to.have.prop('id', `${id}-help`); 97 | expect(helpComp.children()).to.have.length(1); 98 | expect(helpComp.childAt(0).text()).to.equal(helpText); 99 | }); 100 | it('calls onChange', () => { 101 | const onChange = sinon.spy(); 102 | const data = 'Some value'; 103 | const componentProps = { 104 | onChange, 105 | }; 106 | const wrapper = shallow(); 107 | 108 | const inputComp = wrapper.find(Input); 109 | inputComp.simulate('change', 'value'); 110 | expect(onChange).to.be.calledWith('value'); 111 | }); 112 | it('has withLabel className ', () => { 113 | const wrapper = shallow(); 114 | 115 | expect(wrapper).prop('className').match(/withLabelClass/); 116 | }); 117 | }); 118 | -------------------------------------------------------------------------------- /src/fields/components/Checkbox.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Checkbox from 'material-ui/Checkbox'; 3 | import { FormControlLabel } from 'material-ui/Form'; 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/Form'; 11 | import Checkbox from 'material-ui/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/RadioGroup.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Radio, { RadioGroup } from 'material-ui/Radio'; 3 | import { FormControlLabel } from 'material-ui/Form'; 4 | 5 | export default ({ path, options = [], value, onChange, inputProps, nullOption, ...rest }) => ( 6 | 13 | {options.map(o => } label={o.value} />)} 14 | 15 | ); 16 | -------------------------------------------------------------------------------- /src/fields/components/Select.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Select from 'material-ui/Select'; 3 | import { MenuItem } from 'material-ui/Menu'; 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 |