├── .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 |
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 |
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 |
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 |
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 | ,
21 | );
22 |
23 | const btnComp = wrapper.find('button');
24 | expect(btnComp).to.have.length(1);
25 | btnComp.simulate('click');
26 | expect(onClick).to.have.been.calledOnce;
27 | });
28 | it('test click checkbox', () => {
29 | const onChange = sinon.spy();
30 | const wrapper = shallow(
31 | ,
32 | );
33 |
34 | const btnComp = wrapper.find('input');
35 | expect(btnComp).to.have.length(1);
36 | btnComp.simulate('change');
37 | expect(onChange).to.have.been.calledOnce;
38 | });
39 | });
40 |
--------------------------------------------------------------------------------
/src/fields/components/index.js:
--------------------------------------------------------------------------------
1 | export { default as Select } from './Select';
2 | export { default as RadioGroup } from './RadioGroup';
3 | export { default as Checkbox } from './Checkbox';
4 |
--------------------------------------------------------------------------------
/src/fields/configure/configure-component.js:
--------------------------------------------------------------------------------
1 | // import Input, { InputLabel } from 'material-ui/Input'; // eslint-disable-line import/no-named-default
2 | import getComponentProps from './get-component-props';
3 | import getLabelComponentProps from './get-label-component-props';
4 | import getLabelComponent from './get-label-component';
5 | import getComponent from './get-component';
6 |
7 | const getClassName = ({ uiSchema = {} }) => {
8 | const widget = uiSchema['ui:widget'];
9 | return widget === 'textarea' ? 'textarea' : null;
10 | };
11 |
12 | export default (props) => {
13 | const { schema, uiSchema = {} } = props;
14 | const title = uiSchema['ui:title'] || schema.title;
15 | return {
16 | title,
17 | className: getClassName(props),
18 | Component: getComponent(props),
19 | componentProps: getComponentProps(props),
20 | LabelComponent: title && getLabelComponent(props),
21 | labelComponentProps: getLabelComponentProps(props),
22 | };
23 | };
24 |
--------------------------------------------------------------------------------
/src/fields/configure/configure-component.spec.js:
--------------------------------------------------------------------------------
1 | /* globals describe,it,beforeEach */
2 | // eslint-disable-next-line max-len
3 | /* eslint-disable import/no-webpack-loader-syntax,import/no-extraneous-dependencies,import/no-unresolved,no-unused-expressions */
4 | import chai, { expect } from 'chai';
5 | import sinon from 'sinon';
6 | import sinonChai from 'sinon-chai';
7 | // import configureComponent from './configure-component';
8 |
9 | const inject = require('inject-loader!./configure-component');
10 |
11 | chai.use(sinonChai);
12 |
13 | describe('configureComponent', () => {
14 | let configureComponent;
15 | const getComponentProps = sinon.stub();
16 | const getLabelComponentProps = sinon.stub();
17 | const getLabelComponent = sinon.stub();
18 | const getComponent = sinon.stub();
19 | beforeEach(() => {
20 | configureComponent = inject({
21 | './get-component-props': getComponentProps,
22 | './get-label-component-props': getLabelComponentProps,
23 | './get-label-component': getLabelComponent,
24 | './get-component': getComponent,
25 | }).default;
26 | });
27 | it('delegates to helper functions - control', () => {
28 | const props = { schema: { title: 'Default title' } };
29 | const expectedComponent = 'x';
30 | const expectedLabelComponent = 'y';
31 | const expectedComponentProps = { 'a': 'a' };
32 | const expectedLabelComponentProps = { 'b': 'b' };
33 | getComponent.returns(expectedComponent);
34 | getLabelComponent.returns(expectedLabelComponent);
35 | getComponentProps.returns(expectedComponentProps);
36 | getLabelComponentProps.returns(expectedLabelComponentProps);
37 |
38 | const { componentProps, labelComponentProps, Component, LabelComponent, className, title } = configureComponent(props);
39 |
40 | expect(Component).to.equal(expectedComponent);
41 | expect(LabelComponent).to.equal(expectedLabelComponent);
42 | expect(componentProps).to.deep.equal(expectedComponentProps);
43 | expect(labelComponentProps).to.deep.equal(expectedLabelComponentProps);
44 | expect(className).to.be.null;
45 | expect(title).to.equal(props.schema.title);
46 | });
47 | it('substitutes title for ui:title if present', () => {
48 | const schema = { 'title': 'Default title' };
49 | const uiSchema = { 'ui:title': 'Another title' };
50 | const config = configureComponent({ schema, uiSchema });
51 | expect(config.title).to.equal('Another title');
52 | });
53 | it('sets classname for textarea', () => {
54 | const schema = {};
55 | const uiSchema = { 'ui:widget': 'textarea' };
56 | const { className } = configureComponent({ schema, uiSchema });
57 | expect(className).to.equal('textarea');
58 | });
59 | it('no LabelComponent if no title', () => {
60 | const schema = {};
61 | const { LabelComponent } = configureComponent({ schema });
62 | expect(LabelComponent).to.be.undefined;
63 | });
64 | });
65 |
--------------------------------------------------------------------------------
/src/fields/configure/field-styles.js:
--------------------------------------------------------------------------------
1 | export default theme => ({
2 | root: {},
3 | });
4 |
--------------------------------------------------------------------------------
/src/fields/configure/get-component-props.js:
--------------------------------------------------------------------------------
1 | import without from 'lodash/without';
2 | import getMuiProps from './get-mui-props';
3 | import getInputType from './get-input-type';
4 | import valuesToOptions from './values-to-options';
5 |
6 | const toNumber = (v) => {
7 | if (v === '' || v === undefined) return v;
8 | const n = Number(v);
9 | return (!Number.isNaN(n) ? n : v);
10 | };
11 | const coerceValue = (type, value) => {
12 | switch (type) {
13 | case 'string':
14 | return (typeof value === 'string' ? value : String(value));
15 | case 'number':
16 | case 'integer':
17 | case 'double':
18 | case 'float':
19 | case 'decimal':
20 | return toNumber(value);
21 | default:
22 | return value;
23 | }
24 | };
25 | const onChangeHandler = (onChange, type) => (e) => {
26 | const value = coerceValue(type, e.target.value);
27 | if (value !== undefined) onChange(value);
28 | };
29 | const onCheckboxChangeHandler = (onChange, title) => (e) => {
30 | const spec = {
31 | };
32 | if (e) {
33 | spec.$push = [title];
34 | }
35 | else {
36 | spec.$apply = arr => without(arr, title);
37 | }
38 | return onChange(spec);
39 | };
40 |
41 | export default ({ schema = {}, uiSchema = {}, onChange, htmlId, data, objectData }) => {
42 | const widget = uiSchema['ui:widget'];
43 | const options = uiSchema['ui:options'] || {};
44 | const { type } = schema;
45 | const rv = {
46 | type: getInputType(type, uiSchema),
47 | onChange: onChange && onChangeHandler(onChange, type),
48 | ...getMuiProps(uiSchema),
49 | };
50 | if (schema.enum) {
51 | if (widget === 'radio') {
52 | if (options.inline) {
53 | rv.row = true;
54 | }
55 | }
56 | else if (widget === 'checkboxes') {
57 | rv.onChange = onChange && onCheckboxChangeHandler(onChange, schema.title);
58 | rv.label = schema.title;
59 | }
60 | else {
61 | rv.nullOption = 'Please select...';
62 | }
63 | rv.options = valuesToOptions(schema.enum);
64 | }
65 | else if (type === 'boolean') {
66 | rv.label = schema.title;
67 | rv.onChange = onChange;
68 | }
69 | else {
70 | rv.inputProps = {
71 | id: htmlId,
72 | };
73 | }
74 | if (widget === 'textarea') {
75 | rv.multiline = true;
76 | rv.rows = 5;
77 | }
78 | if (options.disabled) {
79 | if (typeof options.disabled === 'boolean') {
80 | rv.disabled = options.disabled;
81 | }
82 | else if (typeof options.disabled === 'function') {
83 | rv.disabled = (options.disabled).call(null, data, objectData);
84 | }
85 | }
86 | return rv;
87 | };
88 |
--------------------------------------------------------------------------------
/src/fields/configure/get-component.js:
--------------------------------------------------------------------------------
1 | // import Input, { InputLabel } from 'material-ui/Input'; // eslint-disable-line import/no-named-default
2 | const { RadioGroup, Select, Checkbox } = require('../components');
3 |
4 | const Input = require('material-ui/Input').default;
5 |
6 | export default ({ schema, uiSchema = {} }) => {
7 | // console.log('getComponent schema: %o, uiSchema: %o', schema, uiSchema);
8 | const widget = uiSchema['ui:widget'];
9 | const { type } = schema;
10 |
11 | if (schema.enum) {
12 | if (widget === 'radio') {
13 | return RadioGroup;
14 | }
15 | else if (widget === 'checkboxes') {
16 | return Checkbox;
17 | }
18 | return Select;
19 | }
20 | else if (type === 'boolean') {
21 | return Checkbox;
22 | }
23 | return Input;
24 | };
25 |
--------------------------------------------------------------------------------
/src/fields/configure/get-component.props.spec.js:
--------------------------------------------------------------------------------
1 | /* globals describe,it */
2 | /* eslint-disable import/no-webpack-loader-syntax,import/no-extraneous-dependencies,import/no-unresolved,no-unused-expressions */
3 | import chai, { expect } from 'chai';
4 | import sinon from 'sinon';
5 | import sinonChai from 'sinon-chai';
6 |
7 | import getComponentProps from './get-component-props';
8 |
9 | chai.use(sinonChai);
10 |
11 | describe('getComponentProps', () => {
12 | it('configures props for simple field', () => {
13 | const schema = {
14 | 'title': 'First name',
15 | 'type': 'string',
16 | };
17 | const required = [];
18 | const path = 'firstName';
19 | const uiSchema = {};
20 | const htmlId = 'unq';
21 | const onChange = sinon.spy();
22 | const expectedInputProps = {
23 | id: htmlId,
24 | };
25 | const componentProps = getComponentProps({ schema, uiSchema, required, path, htmlId, onChange });
26 | expect(componentProps).to.haveOwnProperty('inputProps');
27 | expect(componentProps.inputProps).to.deep.equal(expectedInputProps);
28 | expect(componentProps.type).to.equal('string');
29 | });
30 | it('creates options property from enum', () => {
31 | const schema = {
32 | 'title': 'First name',
33 | 'enum': ['one', 'two', 'three'],
34 | };
35 | const uiSchema = {};
36 | const expectedOptions = [
37 | { key: 'one', value: 'one' },
38 | { key: 'two', value: 'two' },
39 | { key: 'three', value: 'three' },
40 | ];
41 | const componentProps = getComponentProps({ schema, uiSchema });
42 | expect(componentProps).to.haveOwnProperty('options');
43 | expect(componentProps.options).to.deep.equal(expectedOptions);
44 | });
45 | describe('ui:options.disabled', () => {
46 | it('as boolean adds disabled property', () => {
47 | const schema = {
48 | 'title': 'First name',
49 | 'enum': ['one', 'two', 'three'],
50 | };
51 | const uiSchema = {
52 | 'ui:options': {
53 | disabled: true,
54 | },
55 | };
56 | const componentProps = getComponentProps({ schema, uiSchema });
57 | expect(componentProps).to.haveOwnProperty('disabled');
58 | expect(componentProps.disabled).to.equal(true);
59 | });
60 | it('as function adds disabled property', () => {
61 | const disabledStub = sinon.stub();
62 | disabledStub.returns(true);
63 | const schema = {
64 | 'title': 'First name',
65 | 'enum': ['one', 'two', 'three'],
66 | };
67 | const objectData = {
68 | x: 'one',
69 | y: 'two',
70 | };
71 | const uiSchema = {
72 | 'ui:options': {
73 | disabled: disabledStub,
74 | },
75 | };
76 | const componentProps = getComponentProps({ data: objectData.x, objectData, schema, uiSchema });
77 | expect(componentProps).to.haveOwnProperty('disabled');
78 | expect(componentProps.disabled).to.equal(true);
79 | expect(disabledStub).to.have.been.calledWith('one', objectData);
80 | });
81 | });
82 | describe('when ui:widget=radio and schema.enum', () => {
83 | it('-> options', () => {
84 | const schema = {
85 | 'title': 'First name',
86 | 'enum': ['one', 'two', 'three'],
87 | };
88 | const uiSchema = {
89 | 'ui:widget': 'radio',
90 | };
91 | const expectedOptions = [
92 | { key: 'one', value: 'one' },
93 | { key: 'two', value: 'two' },
94 | { key: 'three', value: 'three' },
95 | ];
96 | const componentProps = getComponentProps({ schema, uiSchema });
97 | expect(componentProps).to.haveOwnProperty('options');
98 | expect(componentProps.options).to.deep.equal(expectedOptions);
99 | });
100 | });
101 | describe('sets type', () => {
102 | describe('to number when type=number', () => {
103 | it('and ui:widget=updown', () => {
104 | const schema = {
105 | 'title': 'First name',
106 | 'type': 'number',
107 | };
108 | const uiSchema = {
109 | 'ui:widget': 'updown',
110 | };
111 | const componentProps = getComponentProps({ schema, uiSchema });
112 | expect(componentProps).to.haveOwnProperty('type');
113 | expect(componentProps.type).to.equal('number');
114 | });
115 | it('and ui:widget=radio', () => {
116 | const schema = {
117 | 'title': 'First name',
118 | 'type': 'number',
119 | };
120 | const uiSchema = {
121 | 'ui:widget': 'radio',
122 | };
123 | const componentProps = getComponentProps({ schema, uiSchema });
124 | expect(componentProps).to.haveOwnProperty('type');
125 | expect(componentProps.type).to.equal('number');
126 | });
127 | });
128 | describe('to number when type=integer', () => {
129 | it('and ui:widget=updown', () => {
130 | const schema = {
131 | 'title': 'First name',
132 | 'type': 'integer',
133 | };
134 | const uiSchema = {
135 | 'ui:widget': 'updown',
136 | };
137 | const componentProps = getComponentProps({ schema, uiSchema });
138 | expect(componentProps).to.haveOwnProperty('type');
139 | expect(componentProps.type).to.equal('number');
140 | });
141 | it('and ui:widget=radio', () => {
142 | const schema = {
143 | 'title': 'First name',
144 | 'type': 'integer',
145 | };
146 | const uiSchema = {
147 | 'ui:widget': 'radio',
148 | };
149 | const componentProps = getComponentProps({ schema, uiSchema });
150 | expect(componentProps).to.haveOwnProperty('type');
151 | expect(componentProps.type).to.equal('number');
152 | });
153 | });
154 | it('to password when ui:widget=password', () => {
155 | const schema = {
156 | 'title': 'Password',
157 | 'type': 'string',
158 | };
159 | const uiSchema = {
160 | 'ui:widget': 'password',
161 | };
162 | const componentProps = getComponentProps({ schema, uiSchema });
163 | expect(componentProps).to.haveOwnProperty('type');
164 | expect(componentProps.type).to.equal('password');
165 | });
166 | });
167 | describe('with ui:widget=textarea', () => {
168 | it('sets rows and multiline', () => {
169 | const schema = { 'title': 'First name', 'type': 'string' };
170 | const uiSchema = { 'ui:widget': 'textarea' };
171 | const componentProps = getComponentProps({ schema, uiSchema });
172 | expect(componentProps).to.haveOwnProperty('rows');
173 | expect(componentProps).to.haveOwnProperty('multiline');
174 | expect(componentProps.rows).to.equal(5);
175 | expect(componentProps.multiline).to.equal(true);
176 | });
177 | });
178 | it('passes mui:* properties', () => {
179 | const schema = { 'title': 'First name' };
180 | const uiSchema = { 'mui:myprop': 'boo' };
181 | const componentProps = getComponentProps({ schema, uiSchema });
182 | expect(componentProps).to.haveOwnProperty('myprop');
183 | expect(componentProps.myprop).to.equal('boo');
184 | });
185 | describe('onChange callback', () => {
186 | it('is called with event target value', () => {
187 | // prepare
188 | const schema = { 'title': 'First name' };
189 | const value = 'new value';
190 | const spy = sinon.spy();
191 |
192 | // act
193 | const componentProps = getComponentProps({ schema, onChange: spy });
194 | const { onChange } = componentProps;
195 | const domEvent = { target: { value } };
196 | onChange(domEvent);
197 |
198 | // check
199 | expect(spy).to.have.been.calledWith(value);
200 | });
201 | describe('is called with typed value', () => {
202 | it('text -> number', () => {
203 | // prepare
204 | const schema = { 'title': 'First name', 'type': 'number' };
205 | const value = '3';
206 | const spy = sinon.spy();
207 |
208 | // act
209 | const componentProps = getComponentProps({ schema, onChange: spy });
210 | const { onChange } = componentProps;
211 | const domEvent = { target: { value } };
212 | onChange(domEvent);
213 |
214 | // check
215 | expect(spy).to.have.been.calledWith(3);
216 | });
217 | it('number -> text', () => {
218 | // prepare
219 | const schema = { 'title': 'First name', 'type': 'string' };
220 | const value = 3;
221 | const spy = sinon.spy();
222 |
223 | // act
224 | const componentProps = getComponentProps({ schema, onChange: spy });
225 | const { onChange } = componentProps;
226 | const domEvent = { target: { value } };
227 | onChange(domEvent);
228 |
229 | // check
230 | expect(spy).to.have.been.calledWith('3');
231 | });
232 | });
233 | });
234 | });
235 |
--------------------------------------------------------------------------------
/src/fields/configure/get-component.spec.js:
--------------------------------------------------------------------------------
1 | /* globals describe,it,beforeEach */
2 | // eslint-disable-next-line max-len
3 | /* eslint-disable import/no-webpack-loader-syntax,import/no-extraneous-dependencies,import/no-unresolved,no-unused-expressions */
4 | import chai, { expect } from 'chai';
5 | import sinon from 'sinon';
6 | import sinonChai from 'sinon-chai';
7 |
8 | chai.use(sinonChai);
9 | const inject = require('inject-loader!./get-component');
10 |
11 | let InputSpy;
12 | let RadioGroupSpy;
13 | let CheckboxSpy;
14 | let SelectSpy;
15 | let getComponent;
16 |
17 | describe('getComponent', () => {
18 | beforeEach(() => {
19 | InputSpy = sinon.spy();
20 | RadioGroupSpy = sinon.spy();
21 | CheckboxSpy = sinon.spy();
22 | SelectSpy = sinon.spy();
23 | getComponent = inject({
24 | 'material-ui/Input': {
25 | default: InputSpy,
26 | },
27 | '../components': {
28 | RadioGroup: RadioGroupSpy,
29 | Checkbox: CheckboxSpy,
30 | Select: SelectSpy,
31 | },
32 | }).default;
33 | });
34 | it('configures props for simple field', () => {
35 | const schema = {
36 | 'title': 'First name',
37 | 'type': 'string',
38 | };
39 | const required = [];
40 | const path = 'firstName';
41 | const uiSchema = {};
42 | // const data = 'Maxamillian';
43 | const htmlId = 'unq';
44 | const onChange = sinon.spy();
45 | const Component = getComponent({ schema, uiSchema, required, path, htmlId, onChange });
46 | expect(Component.id).to.equal(InputSpy.id);
47 | });
48 | describe('yields Component', () => {
49 | describe('depending on ui:widget', () => {
50 | it('-> RadioGroup when ui:widget=radio and schema.enum', () => {
51 | const schema = {
52 | 'enum': ['one', 'two', 'three'],
53 | };
54 | const uiSchema = {
55 | 'ui:widget': 'radio',
56 | };
57 | const Component = getComponent({ schema, uiSchema });
58 | expect(Component.id).to.equal(RadioGroupSpy.id);
59 | });
60 | it('Checkbox when ui:widget=radio and schema.enum', () => {
61 | const schema = {
62 | 'enum': ['one', 'two', 'three'],
63 | };
64 | const uiSchema = {
65 | 'ui:widget': 'radio',
66 | };
67 | const Component = getComponent({ schema, uiSchema });
68 | expect(Component.id).to.equal(RadioGroupSpy.id);
69 | });
70 | });
71 | it('Checkbox when type=boolean', () => {
72 | const schema = {
73 | 'type': 'boolean',
74 | };
75 | const uiSchema = {
76 | };
77 | const Component = getComponent({ schema, uiSchema });
78 | expect(Component.id).to.equal(CheckboxSpy.id);
79 | });
80 | it('Select when schema.enum', () => {
81 | const schema = {
82 | 'enum': ['one', 'two', 'three'],
83 | };
84 | const uiSchema = {
85 | };
86 | const Component = getComponent({ schema, uiSchema });
87 | expect(Component.id).to.equal(SelectSpy.id);
88 | });
89 | it('Select when schema.enum, regardless of type', () => {
90 | const schema = {
91 | 'type': 'number',
92 | 'enum': ['one', 'two', 'three'],
93 | };
94 | const uiSchema = {
95 | };
96 | const Component = getComponent({ schema, uiSchema });
97 | expect(Component.id).to.equal(SelectSpy.id);
98 | });
99 | });
100 | });
101 |
--------------------------------------------------------------------------------
/src/fields/configure/get-input-type.js:
--------------------------------------------------------------------------------
1 |
2 | export default (type, uiSchema) => {
3 | const widget = uiSchema['ui:widget'];
4 | if (type === 'number' || type === 'integer') {
5 | if (widget === 'updown' || widget === 'radio') {
6 | return 'number';
7 | }
8 | return 'text';
9 | }
10 | if (widget === 'password') {
11 | return 'password';
12 | }
13 | return type;
14 | };
15 |
--------------------------------------------------------------------------------
/src/fields/configure/get-label-component-props.js:
--------------------------------------------------------------------------------
1 | import includes from 'lodash/includes';
2 |
3 | export default ({ htmlId, required, path }) => {
4 | const rv = {
5 | htmlFor: htmlId,
6 | required: includes(required, path),
7 | };
8 | return rv;
9 | };
10 |
--------------------------------------------------------------------------------
/src/fields/configure/get-label-component-props.spec.js:
--------------------------------------------------------------------------------
1 | /* globals describe,it */
2 | import { expect } from 'chai';
3 |
4 | import getLabelComponentProps from './get-label-component-props';
5 |
6 | describe('getLabelComponentProps', () => {
7 | it('configures props for simple field', () => {
8 | const schema = {
9 | 'title': 'First name',
10 | 'type': 'string',
11 | };
12 | const required = [];
13 | // const data = 'Maxamillian';
14 | const htmlId = 'unq';
15 | const expectedLabelProps = {
16 | htmlFor: htmlId,
17 | required: false,
18 | };
19 | const labelComponentProps = getLabelComponentProps({ schema, required, htmlId });
20 | expect(labelComponentProps).to.deep.equal(expectedLabelProps);
21 | });
22 | });
23 |
--------------------------------------------------------------------------------
/src/fields/configure/get-label-component.js:
--------------------------------------------------------------------------------
1 | // import Input, { InputLabel } from 'material-ui/Input'; // eslint-disable-line import/no-named-default
2 | import { FormLabel } from 'material-ui/Form';
3 |
4 | const { InputLabel } = require('material-ui/Input');
5 |
6 |
7 | export default ({ schema, uiSchema = {} }) => {
8 | const widget = uiSchema['ui:widget'];
9 | const { type } = schema;
10 |
11 | if (schema.enum && widget === 'radio') {
12 | return FormLabel;
13 | }
14 | // boolean
15 | if (type === 'boolean' || widget === 'checkboxes') return null;
16 | return InputLabel;
17 | };
18 |
--------------------------------------------------------------------------------
/src/fields/configure/get-label-component.spec.js:
--------------------------------------------------------------------------------
1 | /* globals describe,it,beforeEach */
2 | /* eslint-disable import/no-webpack-loader-syntax,import/no-extraneous-dependencies,import/no-unresolved,no-unused-expressions,max-len,no-unused-vars */
3 | import chai, { expect } from 'chai';
4 | import sinon from 'sinon';
5 | import sinonChai from 'sinon-chai';
6 |
7 | const inject = require('inject-loader!./get-label-component');
8 |
9 | chai.use(sinonChai);
10 |
11 | let InputLabelSpy;
12 | let getLabelComponent;
13 |
14 | chai.use(sinonChai);
15 |
16 | describe('getLabelComponent', () => {
17 | beforeEach(() => {
18 | InputLabelSpy = sinon.spy();
19 | getLabelComponent = inject({
20 | 'material-ui/Input': {
21 | InputLabel: InputLabelSpy,
22 | },
23 | }).default;
24 | });
25 | it('returns InputLabel by default', () => {
26 | const LabelComponent = getLabelComponent({ schema: {} });
27 | expect(LabelComponent.id).to.equal(InputLabelSpy.id);
28 | });
29 | });
30 |
--------------------------------------------------------------------------------
/src/fields/configure/get-mui-props.js:
--------------------------------------------------------------------------------
1 | import mapKeys from 'lodash/mapKeys';
2 | import pickBy from 'lodash/pickBy';
3 |
4 | export default props => mapKeys(pickBy(props, (v, k) => k.startsWith('mui:')), (v, k) => k.substring(4));
5 |
--------------------------------------------------------------------------------
/src/fields/configure/index.js:
--------------------------------------------------------------------------------
1 | export { default } from './configure-component';
2 |
--------------------------------------------------------------------------------
/src/fields/configure/values-to-options.js:
--------------------------------------------------------------------------------
1 | import keys from 'lodash/keys';
2 |
3 | export default (values) => {
4 | if (values instanceof Array) {
5 | return values.map(e => ({ key: e, value: e }));
6 | }
7 | if (typeof values === 'object') {
8 | return keys(values).map(e => ({ key: e, value: values[e] }));
9 | }
10 | return [{}];
11 | };
12 |
--------------------------------------------------------------------------------
/src/fields/configure/values-to-options.spec.js:
--------------------------------------------------------------------------------
1 | /* globals describe,it */
2 | import { expect } from 'chai';
3 | import valuesToOptions from './values-to-options';
4 |
5 | describe('valuesToOptions', () => {
6 | it('returns key = value form array', () => {
7 | const values = ['one', 'two', 'three'];
8 | const expected = [
9 | {
10 | key: 'one',
11 | value: 'one',
12 | },
13 | {
14 | key: 'two',
15 | value: 'two',
16 | },
17 | {
18 | key: 'three',
19 | value: 'three',
20 | },
21 | ];
22 | const actual = valuesToOptions(values);
23 | expect(actual).to.deep.equal(expected);
24 | });
25 | it('handles object', () => {
26 | const values = {
27 | one: 'One',
28 | two: 'Two',
29 | three: 'Three',
30 | };
31 | const expected = [
32 | {
33 | key: 'one',
34 | value: 'One',
35 | },
36 | {
37 | key: 'two',
38 | value: 'Two',
39 | },
40 | {
41 | key: 'three',
42 | value: 'Three',
43 | },
44 | ];
45 | const actual = valuesToOptions(values);
46 | expect(actual).to.deep.equal(expected);
47 | });
48 | });
49 |
--------------------------------------------------------------------------------
/src/fields/field-styles.js:
--------------------------------------------------------------------------------
1 | export default theme => ({
2 | root: {
3 | 'padding': theme.spacing.unit,
4 | '&$withLabel': {
5 | marginTop: theme.spacing.unit * 3,
6 | },
7 | },
8 | textarea: {
9 | '& textarea': {
10 | height: 'initial',
11 | },
12 | },
13 | description: {
14 | transform: `translateX(-${theme.spacing.unit * 2}px)`,
15 | fontSize: '80%',
16 | color: theme.palette.grey[500],
17 | },
18 | withLabel: {},
19 | });
20 |
--------------------------------------------------------------------------------
/src/fields/index.js:
--------------------------------------------------------------------------------
1 | export { default } from './Field';
2 |
--------------------------------------------------------------------------------
/src/form-field-styles.js:
--------------------------------------------------------------------------------
1 | export default () => ({
2 | root: {
3 | display: 'flex',
4 | },
5 | });
6 |
--------------------------------------------------------------------------------
/src/form-styles.js:
--------------------------------------------------------------------------------
1 | export default theme => ({
2 | root: {
3 | padding: theme.spacing.unit * 2,
4 | },
5 | formButtons: {
6 | marginTop: theme.spacing.unit * 2,
7 | justifyContent: 'flex-end',
8 | },
9 | submit: {
10 | fontSize: '100%',
11 | },
12 | cancel: {
13 | fontSize: '100%',
14 | },
15 | button: {
16 | fontSize: '100%',
17 | },
18 | });
19 |
--------------------------------------------------------------------------------
/src/helpers/get-default-value.js:
--------------------------------------------------------------------------------
1 | import mapValues from 'lodash/mapValues';
2 |
3 | const getDefaultValue = (schema) => {
4 | if (schema.default) return schema.default;
5 | switch (schema.type) {
6 | case 'object':
7 | return mapValues(schema.properties, getDefaultValue);
8 | case 'string':
9 | case 'number':
10 | default:
11 | return '';
12 | }
13 | };
14 |
15 | export default getDefaultValue;
16 |
--------------------------------------------------------------------------------
/src/helpers/get-default-value.spec.js:
--------------------------------------------------------------------------------
1 | /* globals describe,it */
2 | import { expect } from 'chai';
3 | import getDefaultValue from './get-default-value';
4 |
5 | describe('getDefaultValue', () => {
6 | it('works for string', () => {
7 | // assemble
8 | const data = {
9 | type: 'string',
10 | };
11 | const expected = '';
12 |
13 | // act
14 | const actual = getDefaultValue(data);
15 |
16 | // assert
17 | expect(actual).to.equal(expected);
18 | });
19 | it('works for string with default value', () => {
20 | // assemble
21 | const data = {
22 | type: 'string',
23 | default: 'foo',
24 | };
25 | const expected = 'foo';
26 |
27 | // act
28 | const actual = getDefaultValue(data);
29 |
30 | // assert
31 | expect(actual).to.equal(expected);
32 | });
33 | it('works for object', () => {
34 | // assemble
35 | const data = {
36 | type: 'object',
37 | };
38 | const expected = {};
39 |
40 | // act
41 | const actual = getDefaultValue(data);
42 |
43 | // assert
44 | expect(actual).to.deep.equal(expected);
45 | });
46 | it('works for object with properties', () => {
47 | // assemble
48 | const data = {
49 | type: 'object',
50 | properties: {
51 | name: {
52 | type: 'string',
53 | },
54 | },
55 | };
56 | const expected = { name: '' };
57 |
58 | // act
59 | const actual = getDefaultValue(data);
60 |
61 | // assert
62 | expect(actual).to.deep.equal(expected);
63 | });
64 | it('works for object with properties with default values', () => {
65 | // assemble
66 | const data = {
67 | type: 'object',
68 | properties: {
69 | name: {
70 | type: 'string',
71 | default: 'bar',
72 | },
73 | },
74 | };
75 | const expected = { name: 'bar' };
76 |
77 | // act
78 | const actual = getDefaultValue(data);
79 |
80 | // assert
81 | expect(actual).to.deep.equal(expected);
82 | });
83 | it('works for nested object', () => {
84 | // assemble
85 | const data = {
86 | type: 'object',
87 | properties: {
88 | name: {
89 | type: 'object',
90 | properties: {
91 | firstName: {
92 | type: 'string',
93 | },
94 | },
95 | },
96 | },
97 | };
98 | const expected = { name: { firstName: '' } };
99 |
100 | // act
101 | const actual = getDefaultValue(data);
102 |
103 | // assert
104 | expect(actual).to.deep.equal(expected);
105 | });
106 | });
107 |
--------------------------------------------------------------------------------
/src/helpers/update-form-data.js:
--------------------------------------------------------------------------------
1 | import update from 'immutability-helper';
2 | import size from 'lodash/size';
3 |
4 | const arrRegex = /^([^.]+)\[([0-9]+)\](\.(.*))?/;
5 | const dotRegex = /^([^[]+)\.(.*$)/;
6 |
7 | const applyAtPath = (path, data, spec) => {
8 | if (!path) return spec(data);
9 | const dotMatch = path.match(dotRegex);
10 | const arrMatch = path.match(arrRegex);
11 | if (!dotMatch && !arrMatch) {
12 | return { [path]: spec(data[path]) };
13 | }
14 | if (dotMatch) {
15 | const subPath = dotMatch[1];
16 | const prop = dotMatch[2];
17 | return { [subPath]: applyAtPath(prop, data[subPath], spec) };
18 | }
19 | if (arrMatch) {
20 | const subPath = arrMatch[1];
21 | const index = Number(arrMatch[2]);
22 | return { [subPath]: { [index]: applyAtPath(arrMatch[4], data[subPath][index], spec) } };
23 | }
24 | return {};
25 | };
26 |
27 | const setValueSpec = value => () => {
28 | if (typeof value === 'object' && size(value) === 1) return value;
29 | return ({ $set: value });
30 | };
31 | const pushItemSpec = value => (data) => {
32 | if (data) return ({ $push: [value] });
33 | return ({ $set: [value] });
34 | };
35 | const removeItemSpec = idx => () => ({ $splice: [[idx, 1]] });
36 | const moveItemSpec = (idx, direction) => value => ({
37 | [idx]: { $set: value[idx + direction] },
38 | [idx + direction]: { $set: value[idx] },
39 | });
40 |
41 | export default (data, path, value) => {
42 | const s = setValueSpec(value);
43 | const spec = applyAtPath(path, data, s);
44 | return update(data, spec);
45 | };
46 |
47 | export const addListItem = (data, path, value) => {
48 | const spec = applyAtPath(path, data, pushItemSpec(value));
49 | return update(data, spec);
50 | };
51 |
52 | export const removeListItem = (data, path, index) => {
53 | const spec = applyAtPath(path, data, removeItemSpec(index));
54 | return update(data, spec);
55 | };
56 |
57 | export const moveListItem = (data, path, index, direction) => {
58 | const spec = applyAtPath(path, data, moveItemSpec(index, direction));
59 | return update(data, spec);
60 | };
61 |
--------------------------------------------------------------------------------
/src/helpers/update-form-data.spec.js:
--------------------------------------------------------------------------------
1 | /* globals describe,it */
2 | import without from 'lodash/without';
3 | import { expect } from 'chai';
4 | import updateFormData, { addListItem, removeListItem, moveListItem } from './update-form-data';
5 |
6 | describe('updateFormData', () => {
7 | it('updates simple field', () => {
8 | const initial = {
9 | name: 'Bob',
10 | };
11 | const expected = {
12 | name: 'Harry',
13 | };
14 | expect(updateFormData(initial, 'name', 'Harry')).to.deep.equal(expected);
15 | });
16 | it('updates nested field', () => {
17 | const initial = {
18 | name: {
19 | firstName: 'Bob',
20 | },
21 | };
22 | const expected = {
23 | name: {
24 | firstName: 'Harry',
25 | },
26 | };
27 | expect(updateFormData(initial, 'name.firstName', 'Harry')).to.deep.equal(expected);
28 | });
29 | it('updates index field (object)', () => {
30 | const initial = {
31 | list: [{
32 | name: 'Bob',
33 | }],
34 | };
35 | const expected = {
36 | list: [{
37 | name: 'Harry',
38 | }],
39 | };
40 | expect(updateFormData(initial, 'list[0].name', 'Harry')).to.deep.equal(expected);
41 | });
42 | it('updates index field (simple)', () => {
43 | const initial = {
44 | list: ['Bob'],
45 | };
46 | const expected = {
47 | list: ['Harry'],
48 | };
49 | expect(updateFormData(initial, 'list[0]', 'Harry')).to.deep.equal(expected);
50 | });
51 | it('updates single field', () => {
52 | const initial = 'initialValue';
53 | const expected = 'updatedValue';
54 |
55 | expect(updateFormData(initial, '', 'updatedValue')).to.deep.equal(expected);
56 | });
57 | it('removes array item', () => {
58 | const initial = [
59 | 'one',
60 | 'two',
61 | 'three',
62 | ];
63 | const expected = ['one', 'three'];
64 |
65 | expect(updateFormData(initial, '', { $apply: arr => without(arr, 'two') })).to.deep.equal(expected);
66 | });
67 | it('adds array item', () => {
68 | const initial = [
69 | 'one',
70 | 'two',
71 | ];
72 | const expected = ['one', 'two', 'three'];
73 |
74 | expect(updateFormData(initial, '', { $push: ['three'] })).to.deep.equal(expected);
75 | });
76 | describe('addListItem', () => {
77 | it('adds list item', () => {
78 | const initial = {
79 | listItems: [
80 | '1',
81 | '2',
82 | ],
83 | };
84 | const expected = {
85 | listItems: [
86 | '1',
87 | '2',
88 | '3',
89 | ],
90 | };
91 |
92 | expect(addListItem(initial, 'listItems', '3')).to.deep.equal(expected);
93 | });
94 | it('adds list item to null list', () => {
95 | const initial = {
96 | listItems: null,
97 | };
98 | const expected = {
99 | listItems: [
100 | '1',
101 | ],
102 | };
103 |
104 | expect(addListItem(initial, 'listItems', '1')).to.deep.equal(expected);
105 | });
106 | it('adds list item - deep', () => {
107 | const initial = {
108 | 'myprop': {
109 | listItems: [
110 | '1',
111 | '2',
112 | ],
113 | },
114 | };
115 | const expected = {
116 | 'myprop': {
117 | listItems: [
118 | '1',
119 | '2',
120 | '3',
121 | ],
122 | },
123 | };
124 |
125 | expect(addListItem(initial, 'myprop.listItems', '3')).to.deep.equal(expected);
126 | });
127 | });
128 | describe('removeListItem', () => {
129 | it('remove list item', () => {
130 | const initial = {
131 | listItems: [
132 | '1',
133 | '2',
134 | '3',
135 | ],
136 | };
137 | const expected = {
138 | listItems: [
139 | '1',
140 | '3',
141 | ],
142 | };
143 |
144 | expect(removeListItem(initial, 'listItems', 1)).to.deep.equal(expected);
145 | });
146 | it('remove list item - deep', () => {
147 | const initial = {
148 | 'myprop': {
149 | listItems: [
150 | '1',
151 | '2',
152 | '3',
153 | ],
154 | },
155 | };
156 | const expected = {
157 | 'myprop': {
158 | listItems: [
159 | '1',
160 | '3',
161 | ],
162 | },
163 | };
164 |
165 | expect(removeListItem(initial, 'myprop.listItems', 1)).to.deep.equal(expected);
166 | });
167 | });
168 | describe('moveListItem', () => {
169 | it('moves list item up', () => {
170 | const initial = {
171 | listItems: [
172 | '1',
173 | '2',
174 | '3',
175 | ],
176 | };
177 | const expected = {
178 | listItems: [
179 | '2',
180 | '1',
181 | '3',
182 | ],
183 | };
184 |
185 | expect(moveListItem(initial, 'listItems', 1, -1)).to.deep.equal(expected);
186 | });
187 | it('moves list item down', () => {
188 | const initial = {
189 | listItems: [
190 | '1',
191 | '2',
192 | '3',
193 | ],
194 | };
195 | const expected = {
196 | listItems: [
197 | '2',
198 | '1',
199 | '3',
200 | ],
201 | };
202 |
203 | expect(moveListItem(initial, 'listItems', 0, 1)).to.deep.equal(expected);
204 | });
205 | });
206 | });
207 |
--------------------------------------------------------------------------------
/src/helpers/validation/get-validation-result.js:
--------------------------------------------------------------------------------
1 | import update from 'immutability-helper';
2 | import forOwn from 'lodash/forOwn';
3 | import mapValues from 'lodash/mapValues';
4 | import rules from './rules';
5 |
6 | const validationResult = (schema, value) => {
7 | const rv = [];
8 | forOwn(rules, (rule, ruleId) => {
9 | const result = rule(schema, value);
10 | if (result) {
11 | rv.push({
12 | rule: ruleId,
13 | ...result,
14 | });
15 | }
16 | });
17 | return rv;
18 | };
19 |
20 | const getFieldSpec = (schema, value) => {
21 | if (value === null) {
22 | return { $set: [] };
23 | }
24 | if (typeof value !== 'object') {
25 | return { $set: validationResult(schema, value) };
26 | }
27 | return mapValues(schema.properties, (s, p) => getFieldSpec(s, value[p]));
28 | };
29 |
30 | export default (schema, data) => {
31 | const spec = getFieldSpec(schema, data);
32 | return update({}, spec);
33 | };
34 |
--------------------------------------------------------------------------------
/src/helpers/validation/get-validation-result.spec.js:
--------------------------------------------------------------------------------
1 | /* globals describe,it */
2 | import { expect } from 'chai';
3 | import getValidationResult from './get-validation-result';
4 |
5 | describe('getValidations', () => {
6 | it('max len - fail', () => {
7 | const schema = {
8 | 'properties': {
9 | 'firstName': {
10 | 'title': 'First name',
11 | 'maxLength': 10,
12 | },
13 | },
14 | };
15 | const data = {
16 | firstName: 'Maxamillian',
17 | };
18 | const result = getValidationResult(schema, data);
19 | expect(result.firstName).to.have.length(1);
20 | expect(result.firstName[0].rule).to.equal('maxLength');
21 | });
22 | it('max-len - pass', () => {
23 | const schema = {
24 | 'properties': {
25 | 'firstName': {
26 | 'title': 'First name',
27 | 'maxLength': 10,
28 | },
29 | },
30 | };
31 | const data = {
32 | firstName: 'Max',
33 | };
34 | const result = getValidationResult(schema, data);
35 | expect(result.firstName).to.have.length(0);
36 | });
37 | it('min-len - fail', () => {
38 | const schema = {
39 | 'properties': {
40 | 'firstName': {
41 | 'title': 'First name',
42 | 'minLength': 3,
43 | },
44 | },
45 | };
46 | const data = {
47 | firstName: 'Mi',
48 | };
49 | const result = getValidationResult(schema, data);
50 | expect(result.firstName).to.have.length(1);
51 | expect(result.firstName[0].rule).to.equal('minLength');
52 | });
53 | it('pattern - fail', () => {
54 | const schema = {
55 | 'properties': {
56 | 'email': {
57 | 'title': 'Email',
58 | 'pattern': '[a-zA-Z]{1}[a-zA-Z\\-\\+_]@[a-zA-Z\\-_]+\\.com',
59 | },
60 | },
61 | };
62 | const data = {
63 | email: 'geoffs-fridges-at-gmail-dot-com',
64 | };
65 | const result = getValidationResult(schema, data);
66 | expect(result).to.haveOwnProperty('email');
67 | expect(result.email).to.have.length(1);
68 | expect(result.email[0].rule).to.equal('pattern');
69 | });
70 | it('pattern - pass', () => {
71 | const schema = {
72 | 'properties': {
73 | 'email': {
74 | 'title': 'Email',
75 | 'pattern': '[a-zA-Z]{1}[a-zA-Z\\-\\+_]@[a-zA-Z\\-_]+\\.com',
76 | },
77 | },
78 | };
79 | const data = {
80 | email: 'geoffs-fridges@geoff.com',
81 | };
82 | const result = getValidationResult(schema, data);
83 | expect(result).to.haveOwnProperty('email');
84 | expect(result.email).to.have.length(0);
85 | });
86 | it('minimum - fail', () => {
87 | const schema = {
88 | 'properties': {
89 | 'age': {
90 | 'title': 'Age',
91 | 'minimum': 10,
92 | },
93 | },
94 | };
95 | const data = {
96 | age: 9,
97 | };
98 | const result = getValidationResult(schema, data);
99 | expect(result.age).to.have.length(1);
100 | expect(result.age[0].rule).to.equal('minimum');
101 | });
102 | it('minimum - pass', () => {
103 | const schema = {
104 | 'properties': {
105 | 'age': {
106 | 'title': 'Age',
107 | 'minimum': 10,
108 | },
109 | },
110 | };
111 | const data = {
112 | age: 10,
113 | };
114 | const result = getValidationResult(schema, data);
115 | expect(result.age).to.have.length(0);
116 | });
117 | it('maximum - fail', () => {
118 | const schema = {
119 | 'properties': {
120 | 'age': {
121 | 'title': 'Age',
122 | 'maximum': 18,
123 | },
124 | },
125 | };
126 | const data = {
127 | age: 19,
128 | };
129 | const result = getValidationResult(schema, data);
130 | expect(result.age).to.have.length(1);
131 | expect(result.age[0].rule).to.equal('maximum');
132 | });
133 | it('maximum - pass', () => {
134 | const schema = {
135 | 'properties': {
136 | 'age': {
137 | 'title': 'Age',
138 | 'maximum': 18,
139 | },
140 | },
141 | };
142 | const data = {
143 | age: 18,
144 | };
145 | const result = getValidationResult(schema, data);
146 | expect(result.age).to.have.length(0);
147 | });
148 | it('no validations', () => {
149 | const schema = {
150 | 'properties': {
151 | 'name': {
152 | type: 'string',
153 | },
154 | },
155 | };
156 | const data = {
157 | name: 'Bob',
158 | };
159 | const result = getValidationResult(schema, data);
160 | expect(result.name).to.have.length(0);
161 | });
162 | it('no validations, no value', () => {
163 | const schema = {
164 | 'properties': {
165 | 'name': {
166 | type: 'string',
167 | },
168 | },
169 | };
170 | const data = {
171 | name: null,
172 | };
173 | const result = getValidationResult(schema, data);
174 | expect(result.name).to.have.length(0);
175 | });
176 | });
177 |
--------------------------------------------------------------------------------
/src/helpers/validation/index.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable global-require */
2 | export { default } from './get-validation-result';
3 |
--------------------------------------------------------------------------------
/src/helpers/validation/rules/index.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable global-require */
2 | export default {
3 | maxLength: require('./max-length').default,
4 | minLength: require('./min-length').default,
5 | pattern: require('./pattern').default,
6 | minimum: require('./minimum').default,
7 | maximum: require('./maximum').default,
8 | };
9 |
--------------------------------------------------------------------------------
/src/helpers/validation/rules/max-length.js:
--------------------------------------------------------------------------------
1 | import size from 'lodash/size';
2 |
3 | export default (schema, value) => {
4 | if (schema.maxLength && size(value) > schema.maxLength) {
5 | return ({ message: `'${value}' exceeds the maximum length of ${schema.maxLength} for field '${schema.title}'` });
6 | }
7 | return null;
8 | };
9 |
--------------------------------------------------------------------------------
/src/helpers/validation/rules/maximum.js:
--------------------------------------------------------------------------------
1 | export default (schema, value) => {
2 | if (schema.maximum && typeof value === 'number' && value > schema.maximum) {
3 | return ({ message: `'${schema.title}' should be <= ${schema.maximum}` });
4 | }
5 | return null;
6 | };
7 |
--------------------------------------------------------------------------------
/src/helpers/validation/rules/min-length.js:
--------------------------------------------------------------------------------
1 | import size from 'lodash/size';
2 |
3 | export default (schema, value) => {
4 | if ((schema.minLength !== undefined) && (size(value) < schema.minLength)) {
5 | return ({ message: `'${schema.title}' must be at least ${schema.minLength}` });
6 | }
7 | return null;
8 | };
9 |
--------------------------------------------------------------------------------
/src/helpers/validation/rules/minimum.js:
--------------------------------------------------------------------------------
1 | export default (schema, value) => {
2 | if (schema.minimum && typeof value === 'number' && value < schema.minimum) {
3 | return ({ message: `'${schema.title}' should be >= ${schema.minimum}` });
4 | }
5 | return null;
6 | };
7 |
--------------------------------------------------------------------------------
/src/helpers/validation/rules/pattern.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable */
2 | import size from 'lodash/size';
3 | export default (schema, value) => {
4 | if (schema.pattern && value && !RegExp(schema.pattern).test(value)) {
5 | return ({ message: `'${schema.title}' must ma tch pattern ${schema.pattern}` });
6 | }
7 | return null;
8 | };
9 |
--------------------------------------------------------------------------------
/src/index.js:
--------------------------------------------------------------------------------
1 | export { default } from './Form.jsx';
2 |
--------------------------------------------------------------------------------
/webpack.config.demo.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable */
2 | const fs = require('fs');
3 | const path = require('path');
4 | const webpack = require('webpack');
5 | const babelExclude = /node_modules/;
6 | const HtmlWebpackPlugin = require('html-webpack-plugin')
7 | const ExtractTextPlugin = require("extract-text-webpack-plugin")
8 | const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;
9 |
10 | const extractScss = new ExtractTextPlugin({ filename: "style.css", allChunks: true })
11 | const extractCss = new ExtractTextPlugin({ filename: "main.css", allChunks: true })
12 |
13 | const alias = {}
14 | if (process.env.NODE_ENV !== 'production' && process.env.NO_STUBS === undefined) {
15 | };
16 |
17 | var config = {
18 | entry: ['babel-polyfill', path.join(__dirname, 'src/demo/index.jsx')],
19 | output: {
20 | path: path.join(__dirname, 'dist'),
21 | filename: 'demo.js',
22 | publicPath: '/',
23 | },
24 | devtool: 'inline-source-map',
25 | module: {
26 | rules: [
27 | {
28 | oneOf: [
29 | {
30 | test: /\.jsx?$/,
31 | use: ['babel-loader'],
32 | exclude: babelExclude,
33 | },
34 | {
35 | test: /\.scss$/,
36 | use: extractScss.extract({
37 | use: [{
38 | loader: 'css-loader',
39 | options: {
40 | localIdentName: '[path]__[name]__[local]__[hash:base64:5]',
41 | modules: true,
42 | camelCase: true,
43 | }
44 | }, {
45 | loader: 'sass-loader',
46 | }]
47 | }),
48 | },
49 | {
50 | test: /\.css$/,
51 | use: extractCss.extract({
52 | use: [{
53 | loader: 'css-loader',
54 | }]
55 | }),
56 | },
57 | {
58 | test: /\.(gif|png|jpe?g|svg)$/i,
59 | loaders: [
60 | {
61 | loader: 'url-loader',
62 | options: {
63 | limit: 50000,
64 | },
65 | }, {
66 | loader: 'image-webpack-loader',
67 | }
68 | ]
69 | },
70 | {
71 | exclude: [/\.(js|jsx|mjs)$/, /\.html$/, /\.json$/, /.s?css$/],
72 | loader: 'file-loader',
73 | options: {
74 | name: 'static/media/[name].[hash:8].[ext]',
75 | },
76 | },
77 | ]
78 | }
79 | ]
80 | },
81 | resolve: {
82 | extensions: ['.js', '.jsx'],
83 | alias,
84 | modules: ['node_modules']
85 | },
86 | plugins: [
87 | extractCss,
88 | extractScss,
89 | new HtmlWebpackPlugin({
90 | template: 'src/demo/index.html',
91 | }),
92 | new webpack.NamedModulesPlugin(),
93 | ],
94 | target: 'web'
95 | }
96 |
97 |
98 |
99 | // PROD ONLY
100 | if (process.env.NODE_ENV === 'production') {
101 | config.plugins.push(
102 | new webpack.optimize.UglifyJsPlugin(),
103 | );
104 | }
105 | // NON-PROD ONLY
106 | else {
107 | config.plugins.push(
108 | // new CleanWebpackPlugin([path.join(__dirname, '../dist')], { root: process.cwd() }),
109 | new webpack.HotModuleReplacementPlugin(),
110 | new webpack.LoaderOptionsPlugin({
111 | options: {
112 | eslint: {
113 | configFile: path.join(__dirname, '.eslintrc.js'),
114 | failOnWarning: false,
115 | failOnError: true,
116 | ignorePatten: ['node_modules', 'dist']
117 | },
118 | },
119 | }),
120 | // new BundleAnalyzerPlugin({
121 | // analyzerMode: 'server',
122 | // openAnalyzer: false,
123 | // }),
124 | );
125 | config.entry.splice(0, 0, 'webpack-hot-middleware/client');
126 | config.entry.splice(0, 0, 'react-hot-loader/patch');
127 | config.module.rules.push(
128 | { enforce: 'pre', test: /\.jsx?$/, loader: 'eslint-loader', exclude: babelExclude },
129 | );
130 | }
131 | module.exports = config
132 |
--------------------------------------------------------------------------------
/webpack.config.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable */
2 | const fs = require('fs');
3 | const path = require('path');
4 | const webpack = require('webpack');
5 | const babelExclude = /node_modules/;
6 |
7 | const alias = {}
8 | if (process.env.NODE_ENV !== 'production' && process.env.NO_STUBS === undefined) {
9 | };
10 |
11 | var config = {
12 | entry: path.join(__dirname, 'src/index.js'),
13 | output: {
14 | path: path.join(__dirname, 'dist'),
15 | filename: 'bundle.js',
16 | libraryTarget: 'commonjs',
17 | },
18 | module: {
19 | rules: [
20 | {
21 | test: /\.jsx?$/,
22 | use: ['babel-loader'],
23 | exclude: babelExclude,
24 | },
25 | ]
26 | },
27 | resolve: {
28 | extensions: ['.js', '.jsx'],
29 | alias
30 | },
31 | externals: /^(react|material-ui(\/.*)?|immutability-helper|classnames|codemirror|lodash(\/.*)?|@material-ui\/icons(\/.*)?|react-codemirror2|shortid)$/,
32 | plugins: [
33 | ],
34 | }
35 | module.exports = config
36 |
--------------------------------------------------------------------------------