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