├── .babelrc
├── .editorconfig
├── .eslintrc
├── .gitignore
├── .npmignore
├── .travis.yml
├── CODE_OF_CONDUCT.md
├── ISSUE_TEMPLATE.md
├── LICENSE.md
├── PULL_REQUEST_TEMPLATE.md
├── README.md
├── devServer.js
├── docs
├── advanced-customization.md
├── definitions.md
├── dependencies.md
├── form-customization.md
├── index.md
├── theme-customization.md
└── validation.md
├── mkdocs.yml
├── netlify.toml
├── package-lock.json
├── package.json
├── playground
├── app.js
├── index.html
├── index.prod.html
└── samples
│ ├── additionalProperties.js
│ ├── alternatives.js
│ ├── anyOf.js
│ ├── arrays.js
│ ├── custom.js
│ ├── customArray.js
│ ├── customObject.js
│ ├── date.js
│ ├── errors.js
│ ├── files.js
│ ├── index.js
│ ├── large.js
│ ├── nested.js
│ ├── null.js
│ ├── nullable.js
│ ├── numbers.js
│ ├── oneOf.js
│ ├── ordering.js
│ ├── propertyDependencies.js
│ ├── references.js
│ ├── schemaDependencies.js
│ ├── simple.js
│ ├── single.js
│ ├── validation.js
│ └── widgets.js
├── src
├── components
│ ├── AddButton.js
│ ├── ErrorList.js
│ ├── Form.js
│ ├── IconButton.js
│ ├── fields
│ │ ├── ArrayField.js
│ │ ├── BooleanField.js
│ │ ├── DescriptionField.js
│ │ ├── MultiSchemaField.js
│ │ ├── NullField.js
│ │ ├── NumberField.js
│ │ ├── ObjectField.js
│ │ ├── SchemaField.js
│ │ ├── StringField.js
│ │ ├── TitleField.js
│ │ ├── UnsupportedField.js
│ │ └── index.js
│ └── widgets
│ │ ├── AltDateTimeWidget.js
│ │ ├── AltDateWidget.js
│ │ ├── BaseInput.js
│ │ ├── CheckboxWidget.js
│ │ ├── CheckboxesWidget.js
│ │ ├── ColorWidget.js
│ │ ├── DateTimeWidget.js
│ │ ├── DateWidget.js
│ │ ├── EmailWidget.js
│ │ ├── FileWidget.js
│ │ ├── HiddenWidget.js
│ │ ├── PasswordWidget.js
│ │ ├── RadioWidget.js
│ │ ├── RangeWidget.js
│ │ ├── SelectWidget.js
│ │ ├── TextWidget.js
│ │ ├── TextareaWidget.js
│ │ ├── URLWidget.js
│ │ ├── UpDownWidget.js
│ │ └── index.js
├── index.js
├── types.js
├── utils.js
├── validate.js
└── withTheme.js
├── test
├── .eslintrc
├── ArrayFieldTemplate_test.js
├── ArrayField_test.js
├── BooleanField_test.js
├── DescriptionField_test.js
├── FieldTemplate_test.js
├── FormContext_test.js
├── Form_test.js
├── NullField_test.js
├── NumberField_test.js
├── ObjectFieldTemplate_test.js
├── ObjectField_test.js
├── SchemaField_test.js
├── StringField_test.js
├── TitleField_test.js
├── anyOf_test.js
├── const_test.js
├── mocha.opts
├── oneOf_test.js
├── performance_test.js
├── setup-jsdom.js
├── test_utils.js
├── uiSchema_test.js
├── utils_test.js
├── validate_test.js
└── withTheme_test.js
├── types
├── LICENSE
├── README.md
└── index.d.ts
├── webpack.config.dev.js
├── webpack.config.dist.js
└── webpack.config.prod.js
/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "presets": [
3 | "@babel/preset-react",
4 | "@babel/preset-env"
5 | ],
6 | "plugins": [
7 | "@babel/plugin-proposal-object-rest-spread",
8 | "@babel/plugin-proposal-class-properties",
9 | [
10 | "@babel/plugin-transform-runtime",
11 | {
12 | "corejs": 2
13 | }
14 | ]
15 | ],
16 | "env": {
17 | "development": {
18 | "plugins": [
19 | "@babel/plugin-transform-react-jsx"
20 | ]
21 | }
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/.editorconfig:
--------------------------------------------------------------------------------
1 | [*]
2 |
3 | indent_style = space
4 | indent_size = 2
5 | charset = utf-8
6 | insert_final_newline = true
7 |
--------------------------------------------------------------------------------
/.eslintrc:
--------------------------------------------------------------------------------
1 | {
2 | "parser": "babel-eslint",
3 | "rules": {
4 | "react/jsx-uses-react": 2,
5 | "react/jsx-uses-vars": 2,
6 | "react/react-in-jsx-scope": 2,
7 | "react/jsx-tag-spacing": [1, {
8 | "beforeSelfClosing": "always"
9 | }],
10 | "curly": [2],
11 | "linebreak-style": [2, "unix"],
12 | "semi": [2, "always"],
13 | "comma-dangle": [0],
14 | "no-unused-vars": [2, {
15 | "vars": "all",
16 | "args": "none",
17 | "ignoreRestSiblings": true
18 | }],
19 | "no-console": [0],
20 | "object-curly-spacing": [2, "always"],
21 | "keyword-spacing": ["error"]
22 | },
23 | "env": {
24 | "es6": true,
25 | "browser": true,
26 | "node": true
27 | },
28 | "extends": "eslint:recommended",
29 | "ecmaFeatures": {
30 | "modules": true,
31 | "jsx": true,
32 | "experimentalObjectRestSpread": true
33 | },
34 | "plugins": [
35 | "jsx-a11y",
36 | "react"
37 | ]
38 | }
39 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Created by https://www.gitignore.io/api/osx,node,linux,windows
2 | # Edit at https://www.gitignore.io/?templates=osx,node,linux,windows
3 |
4 | ### Linux ###
5 | *~
6 |
7 | # temporary files which can be created if a process still has a handle open of a deleted file
8 | .fuse_hidden*
9 |
10 | # KDE directory preferences
11 | .directory
12 |
13 | # Linux trash folder which might appear on any partition or disk
14 | .Trash-*
15 |
16 | # .nfs files are created when an open file is removed but is still being accessed
17 | .nfs*
18 |
19 | ### Node ###
20 | # Logs
21 | logs
22 | *.log
23 | npm-debug.log*
24 | yarn-debug.log*
25 | yarn-error.log*
26 |
27 | # Runtime data
28 | pids
29 | *.pid
30 | *.seed
31 | *.pid.lock
32 |
33 | # Coverage directory used by tools like istanbul
34 | coverage
35 |
36 | # nyc test coverage
37 | .nyc_output
38 |
39 | # Dependency directories
40 | node_modules/
41 |
42 | # Optional npm cache directory
43 | .npm
44 |
45 | # Optional eslint cache
46 | .eslintcache
47 |
48 | # Optional REPL history
49 | .node_repl_history
50 |
51 | # Output of 'npm pack'
52 | *.tgz
53 |
54 | # Yarn Integrity file
55 | .yarn-integrity
56 |
57 | # dotenv environment variables file
58 | .env
59 | .env.test
60 |
61 | ### OSX ###
62 | # General
63 | .DS_Store
64 | .AppleDouble
65 | .LSOverride
66 |
67 | # Icon must end with two \r
68 | Icon
69 |
70 | # Thumbnails
71 | ._*
72 |
73 | # Files that might appear in the root of a volume
74 | .DocumentRevisions-V100
75 | .fseventsd
76 | .Spotlight-V100
77 | .TemporaryItems
78 | .Trashes
79 | .VolumeIcon.icns
80 | .com.apple.timemachine.donotpresent
81 |
82 | # Directories potentially created on remote AFP share
83 | .AppleDB
84 | .AppleDesktop
85 | Network Trash Folder
86 | Temporary Items
87 | .apdisk
88 |
89 | ### Windows ###
90 | # Windows thumbnail cache files
91 | Thumbs.db
92 | ehthumbs.db
93 | ehthumbs_vista.db
94 |
95 | # Dump file
96 | *.stackdump
97 |
98 | # Folder config file
99 | [Dd]esktop.ini
100 |
101 | # Recycle Bin used on file shares
102 | $RECYCLE.BIN/
103 |
104 | # Windows Installer files
105 | *.cab
106 | *.msi
107 | *.msix
108 | *.msm
109 | *.msp
110 |
111 | # Windows shortcuts
112 | *.lnk
113 |
114 | # End of https://www.gitignore.io/api/osx,node,linux,windows
115 |
116 | # Package specific files
117 |
118 | build
119 | _build
120 | site
121 | dist
122 | lib
123 | yarn.lock
124 |
125 | # IDE
126 | .vscode
127 |
128 | # Code coverage
129 | coverage
130 | .nyc_output
131 |
--------------------------------------------------------------------------------
/.npmignore:
--------------------------------------------------------------------------------
1 | src/
2 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | sudo: false
2 | language:
3 | - node_js
4 | node_js:
5 | - "6"
6 | - "7"
7 | - "8"
8 | - "9"
9 | - "10"
10 | - "11"
11 | - "12"
12 | env:
13 | - ACTION=test
14 | - ACTION="run lint"
15 | - ACTION="run cs-check"
16 | - ACTION="run dist"
17 | script:
18 | - npm $ACTION
19 |
--------------------------------------------------------------------------------
/CODE_OF_CONDUCT.md:
--------------------------------------------------------------------------------
1 | # Community Participation Guidelines
2 |
3 | This repository is governed by Mozilla's code of conduct and etiquette guidelines.
4 | For more details, please read the
5 | [Mozilla Community Participation Guidelines](https://www.mozilla.org/about/governance/policies/participation/).
6 |
7 | ## How to Report
8 | For more information on how to report violations of the Community Participation Guidelines, please read our '[How to Report](https://www.mozilla.org/about/governance/policies/participation/reporting/)' page.
9 |
10 |
16 |
--------------------------------------------------------------------------------
/ISSUE_TEMPLATE.md:
--------------------------------------------------------------------------------
1 | ### Prerequisites
2 |
3 | - [ ] I have read the [documentation](https://react-jsonschema-form.readthedocs.io/);
4 | - [ ] In the case of a bug report, I understand that providing a [SSCCE](http://sscce.org/) example is tremendously useful to the maintainers.
5 | - [ ] Ideally, I'm providing a [sample JSFiddle](https://jsfiddle.net/n1k0/f2y3fq7L/6/) or a [shared playground link](https://mozilla-services.github.io/react-jsonschema-form/) demonstrating the issue.
6 |
7 | ### Description
8 |
9 | [Description of the bug or feature]
10 |
11 | ### Steps to Reproduce
12 |
13 | 1. [First Step]
14 | 2. [Second Step]
15 | 3. [and so on...]
16 |
17 | #### Expected behavior
18 |
19 | [What you expected to happen]
20 |
21 | #### Actual behavior
22 |
23 | [What actually happened]
24 |
25 | ### Version
26 |
27 | You can usually get this information in your `package.json` or in the file URL if you're using the unpkg one.
28 |
--------------------------------------------------------------------------------
/PULL_REQUEST_TEMPLATE.md:
--------------------------------------------------------------------------------
1 | ### Reasons for making this change
2 |
3 | [Please describe them here]
4 |
5 | If this is related to existing tickets, include links to them as well.
6 |
7 | ### Checklist
8 |
9 | * [ ] **I'm updating documentation**
10 | - [ ] I've [checked the rendering](https://react-jsonschema-form.readthedocs.io/en/latest/#contributing) of the Markdown text I've added
11 | * [ ] **I'm adding or updating code**
12 | - [ ] I've added and/or updated tests
13 | - [ ] I've updated [docs](https://react-jsonschema-form.readthedocs.io/) if needed
14 | * [ ] **I'm adding a new feature**
15 | - [ ] I've updated the playground with an example use of the feature
16 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | DEPRECATION NOTICE
2 | ==================
3 |
4 | This project is no longer maintained. Current versions of [react-jsonschema-form](https://github.com/rjsf-team/react-jsonschema-form) now include a Bootstrap 4 theme. It is recommended that you switch to that instead.
5 |
--------------------------------------------------------------------------------
/devServer.js:
--------------------------------------------------------------------------------
1 | const path = require("path");
2 | const express = require("express");
3 | const webpack = require("webpack");
4 |
5 | const server = process.env.RJSF_DEV_SERVER || "localhost:8080";
6 | const splitServer = server.split(":");
7 | const host = splitServer[0];
8 | const port = splitServer[1];
9 | const env = "dev";
10 |
11 | const webpackConfig = require("./webpack.config." + env);
12 | const compiler = webpack(webpackConfig);
13 | const app = express();
14 |
15 | app.use(require("webpack-dev-middleware")(compiler, {
16 | publicPath: webpackConfig.output.publicPath,
17 | noInfo: true
18 | }));
19 |
20 | app.use(require("webpack-hot-middleware")(compiler));
21 |
22 | app.get("/favicon.ico", function(req, res) {
23 | res.status(204).end();
24 | });
25 |
26 | app.get("/", function(req, res) {
27 | res.sendFile(path.join(__dirname, "playground", "index.html"));
28 | });
29 |
30 | app.listen(port, host, function(err) {
31 | if (err) {
32 | console.log(err);
33 | return;
34 | }
35 |
36 | console.log(`Listening at http://${server}`);
37 | });
38 |
--------------------------------------------------------------------------------
/docs/definitions.md:
--------------------------------------------------------------------------------
1 | ## Schema definitions and references
2 |
3 | This library partially supports [inline schema definition dereferencing]( http://json-schema.org/latest/json-schema-core.html#rfc.section.7.2.3), which is Barbarian for *avoiding to copy and paste commonly used field schemas*:
4 |
5 | ```json
6 | {
7 | "definitions": {
8 | "address": {
9 | "type": "object",
10 | "properties": {
11 | "street_address": { "type": "string" },
12 | "city": { "type": "string" },
13 | "state": { "type": "string" }
14 | },
15 | "required": ["street_address", "city", "state"]
16 | }
17 | },
18 | "type": "object",
19 | "properties": {
20 | "billing_address": { "$ref": "#/definitions/address" },
21 | "shipping_address": { "$ref": "#/definitions/address" }
22 | }
23 | }
24 | ```
25 |
26 | *(Sample schema courtesy of the [Space Telescope Science Institute](http://spacetelescope.github.io/understanding-json-schema/structuring.html))*
27 |
28 | Note that it only supports local definition referencing; we do not plan on fetching foreign schemas over HTTP anytime soon. Basically, you can only reference a definition from the very schema object defining it.
29 |
30 |
--------------------------------------------------------------------------------
/docs/dependencies.md:
--------------------------------------------------------------------------------
1 | ## Property dependencies
2 |
3 | This library supports conditionally making fields required based on the presence of other fields.
4 |
5 | ### Unidirectional
6 |
7 | In the following example the `billing_address` field will be required if `credit_card` is defined.
8 |
9 | ```json
10 | {
11 | "type": "object",
12 |
13 | "properties": {
14 | "name": { "type": "string" },
15 | "credit_card": { "type": "number" },
16 | "billing_address": { "type": "string" }
17 | },
18 |
19 | "required": ["name"],
20 |
21 | "dependencies": {
22 | "credit_card": ["billing_address"]
23 | }
24 | }
25 | ```
26 |
27 | ### Bidirectional
28 |
29 | In the following example the `billing_address` field will be required if `credit_card` is defined and the `credit_card`
30 | field will be required if `billing_address` is defined making them both required if either is defined.
31 |
32 | ```json
33 | {
34 | "type": "object",
35 |
36 | "properties": {
37 | "name": { "type": "string" },
38 | "credit_card": { "type": "number" },
39 | "billing_address": { "type": "string" }
40 | },
41 |
42 | "required": ["name"],
43 |
44 | "dependencies": {
45 | "credit_card": ["billing_address"],
46 | "billing_address": ["credit_card"]
47 | }
48 | }
49 | ```
50 |
51 | *(Sample schemas courtesy of the [Space Telescope Science Institute](https://spacetelescope.github.io/understanding-json-schema/reference/object.html#property-dependencies))*
52 |
53 | ## Schema dependencies
54 |
55 | This library also supports modifying portions of a schema based on form data.
56 |
57 | ### Conditional
58 |
59 | ```json
60 | {
61 | "type": "object",
62 |
63 | "properties": {
64 | "name": { "type": "string" },
65 | "credit_card": { "type": "number" }
66 | },
67 |
68 | "required": ["name"],
69 |
70 | "dependencies": {
71 | "credit_card": {
72 | "properties": {
73 | "billing_address": { "type": "string" }
74 | },
75 | "required": ["billing_address"]
76 | }
77 | }
78 | }
79 | ```
80 |
81 | In this example the `billing_address` field will be displayed in the form if `credit_card` is defined.
82 |
83 | *(Sample schemas courtesy of the [Space Telescope Science Institute](https://spacetelescope.github.io/understanding-json-schema/reference/object.html#schema-dependencies))*
84 |
85 | ### Dynamic
86 |
87 | The JSON Schema standard says that the dependency is triggered if the property is present. However, sometimes it's useful to have more sophisticated rules guiding the application of the dependency. For example, maybe you have three possible values for a field, and each one should lead to adding a different question. For this, we support a very restricted use of the `oneOf` keyword.
88 |
89 | ```json
90 | {
91 | "title": "Person",
92 | "type": "object",
93 | "properties": {
94 | "Do you have any pets?": {
95 | "type": "string",
96 | "enum": [
97 | "No",
98 | "Yes: One",
99 | "Yes: More than one"
100 | ],
101 | "default": "No"
102 | }
103 | },
104 | "required": [
105 | "Do you have any pets?"
106 | ],
107 | "dependencies": {
108 | "Do you have any pets?": {
109 | "oneOf": [
110 | {
111 | "properties": {
112 | "Do you have any pets?": {
113 | "enum": [
114 | "No"
115 | ]
116 | }
117 | }
118 | },
119 | {
120 | "properties": {
121 | "Do you have any pets?": {
122 | "enum": [
123 | "Yes: One"
124 | ]
125 | },
126 | "How old is your pet?": {
127 | "type": "number"
128 | }
129 | },
130 | "required": [
131 | "How old is your pet?"
132 | ]
133 | },
134 | {
135 | "properties": {
136 | "Do you have any pets?": {
137 | "enum": [
138 | "Yes: More than one"
139 | ]
140 | },
141 | "Do you want to get rid of any?": {
142 | "type": "boolean"
143 | }
144 | },
145 | "required": [
146 | "Do you want to get rid of any?"
147 | ]
148 | }
149 | ]
150 | }
151 | }
152 | }
153 | ```
154 |
155 | In this example the user is prompted with different follow-up questions dynamically based on their answer to the first question.
156 |
157 | In these examples, the "Do you have any pets?" question is validated against the corresponding property in each schema in the `oneOf` array. If exactly one matches, the rest of that schema is merged with the existing schema.
158 |
--------------------------------------------------------------------------------
/docs/theme-customization.md:
--------------------------------------------------------------------------------
1 | ## Customizing with other frameworks
2 |
3 | ### withTheme Higher-Order Component
4 | The `withTheme` component provides an easy way to extend the functionality of react-jsonschema-form by passing in a theme object that defines custom/overridden widgets and fields, as well as any of the other possible properties of the standard rjsf `Form` component. This theme-defining object is passed as the only parameter to the HOC (`withTheme(ThemeObj)`), and the HOC will return a themed-component which you use instead of the standard `Form` component.
5 |
6 | ### Usage
7 |
8 | ```jsx
9 | import React, { Component } from 'react';
10 | import { withTheme } from 'react-jsonschema-form';
11 | import Bootstrap4Theme from 'react-jsonschema-form-theme-bs4';
12 |
13 | const ThemedForm = withTheme(Bootstrap4Theme);
14 | class Demo extends Component {
15 | render() {
16 | return
17 | }
18 | }
19 | ```
20 |
21 | ### Theme object properties
22 | The Theme object consists of the same properties as the rjsf `Form` component (such as **widgets** and **fields**). The themed-Form component merges together any theme-specific **widgets** and **fields** with the default **widgets** and **fields**. For instance, providing a single widget in **widgets** will merge this widget with all the default widgets of the rjsf `Form` component, but overrides the default if the theme's widget's name matches the default widget's name. Thus, for each default widget or field not specified/overridden, the themed-form will rely on the defaults from the rjsf `Form`. Note that you are not required to pass in either custom **widgets** or **fields** when using the custom-themed HOC component; you can make the essentially redefine the default Form by simply doing `const Form = withTheme({});`.
23 |
24 | #### Widgets and fields
25 | **widgets** and **fields** should be in the same format as shown [here](/advanced-customization/#custom-widgets-and-fields).
26 |
27 | Example theme with custom widget:
28 | ```jsx
29 | const MyCustomWidget = (props) => {
30 | return (
31 | props.onChange(event.target.value)} />
36 | );
37 | };
38 |
39 | const myWidgets = {
40 | myCustomWidget: MyCustomWidget
41 | };
42 |
43 | const ThemeObject = {widgets: myWidgets};
44 | export default ThemeObject;
45 | ```
46 |
47 | The above can be similarly done for **fields**.
48 |
49 | #### Templates
50 | Each template should be passed directly into the theme object just as you would into the rjsf Form component. Here is an example of how to use a custom [ArrayFieldTemplate](/advanced-customization/#array-field-template) and [ErrorListTemplate](/advanced-customization/#error-list-template) in the theme object:
51 | ```jsx
52 | function MyArrayFieldTemplate(props) {
53 | return (
54 |
55 | {props.items.map(element => element.children)}
56 | {props.canAdd && }
57 |
58 | );
59 | }
60 |
61 | function MyErrorListTemplate(props) {
62 | const {errors} = props;
63 | return (
64 |
65 | {errors.map(error => (
66 |
67 | {error.stack}
68 |
69 | ))}
70 |
71 | );
72 | }
73 |
74 | const ThemeObject = {
75 | ArrayFieldTemplate: MyArrayFieldTemplate,
76 | ErrorList: MyErrorListTemplate,
77 | widgets: myWidgets
78 | };
79 |
80 | export default ThemeObject;
81 | ```
82 |
83 | ### Overriding other Form props
84 | Just as the theme can override **widgets**, **fields**, any of the field templates, and set default values to properties like **showErrorList**, you can do the same with the instance of the withTheme() Form component.
85 | ```jsx
86 | const ThemeObject = {
87 | ArrayFieldTemplate: MyArrayFieldTemplate,
88 | fields: myFields,
89 | showErrorList: false,
90 | widgets: myWidgets
91 | };
92 | ```
93 |
94 | Thus, the user has higher priority than the withTheme HOC, and the theme has higher priority than the default values of the rjsf Form component (**User** > **Theme** > **Defaults**).
95 |
--------------------------------------------------------------------------------
/docs/validation.md:
--------------------------------------------------------------------------------
1 | ## Form data validation
2 |
3 | ### Live validation
4 |
5 | By default, form data are only validated when the form is submitted or when a new `formData` prop is passed to the `Form` component.
6 |
7 | You can enable live form data validation by passing a `liveValidate` prop to the `Form` component, and set it to `true`. Then, everytime a value changes within the form data tree (eg. the user entering a character in a field), a validation operation is performed, and the validation results are reflected into the form state.
8 |
9 | Be warned that this is an expensive strategy, with possibly strong impact on performances.
10 |
11 | To disable validation entirely, you can set Form's `noValidate` prop to `true`.
12 |
13 | ### HTML5 Validation
14 |
15 | By default, required field errors will cause the browser to display its standard HTML5 `required` attribute error messages and prevent form submission. If you would like to turn this off, you can set Form's `noHtml5Validate` prop to `true`, which will set `noValidate` on the `form` element.
16 |
17 | ### Custom validation
18 |
19 | Form data is always validated against the JSON schema.
20 |
21 | But it is possible to define your own custom validation rules. This is especially useful when the validation depends on several interdependent fields.
22 |
23 | ```js
24 | function validate(formData, errors) {
25 | if (formData.pass1 !== formData.pass2) {
26 | errors.pass2.addError("Passwords don't match");
27 | }
28 | return errors;
29 | }
30 |
31 | const schema = {
32 | type: "object",
33 | properties: {
34 | pass1: {type: "string", minLength: 3},
35 | pass2: {type: "string", minLength: 3},
36 | }
37 | };
38 |
39 | render((
40 |
42 | ), document.getElementById("app"));
43 | ```
44 |
45 | > Notes:
46 | > - The `validate()` function must **always** return the `errors` object
47 | > received as second argument.
48 | > - The `validate()` function is called **after** the JSON schema validation.
49 |
50 | ### Custom string formats
51 |
52 | [Pre-defined semantic formats](https://json-schema.org/latest/json-schema-validation.html#rfc.section.7) are limited. react-jsonschema-form adds two formats, `color` and `data-url`, to support certain [alternative widgets](form-customization.md#alternative-widgets). You can add formats of your own through the `customFormats` prop to your `Form` component:
53 |
54 | ```jsx
55 | const schema = {
56 | phoneNumber: {
57 | type: 'string',
58 | format: 'phone-us'
59 | }
60 | };
61 |
62 | const customFormats = {
63 | 'phone-us': /\(?\d{3}\)?[\s-]?\d{3}[\s-]?\d{4}$/
64 | };
65 |
66 | render((
67 |
104 | ), document.getElementById("app"));
105 | ```
106 |
107 | ### Custom error messages
108 |
109 | Validation error messages are provided by the JSON Schema validation by default. If you need to change these messages or make any other modifications to the errors from the JSON Schema validation, you can define a transform function that receives the list of JSON Schema errors and returns a new list.
110 |
111 | ```js
112 | function transformErrors(errors) {
113 | return errors.map(error => {
114 | if (error.name === "pattern") {
115 | error.message = "Only digits are allowed"
116 | }
117 | return error;
118 | });
119 | }
120 |
121 | const schema = {
122 | type: "object",
123 | properties: {
124 | onlyNumbersString: {type: "string", pattern: "^\\d*$"},
125 | }
126 | };
127 |
128 | render((
129 |
131 | ), document.getElementById("app"));
132 | ```
133 |
134 | > Notes:
135 | > - The `transformErrors()` function must return the list of errors. Modifying the list in place without returning it will result in an error.
136 |
137 | Each element in the `errors` list passed to `transformErrors` has the following properties:
138 |
139 | - `name`: name of the error, for example, "required" or "minLength"
140 | - `message`: message, for example, "is a required property" or "should NOT be shorter than 3 characters"
141 | - `params`: an object with the error params returned by ajv ([see doc](https://github.com/epoberezkin/ajv#error-parameters) for more info).
142 | - `property`: a string in Javascript property accessor notation to the data path of the field with the error. For example, `.name` or `['first-name']`.
143 | - `stack`: full error name, for example ".name is a required property".
144 | - `schemaPath`: JSON pointer to the schema of the keyword that failed validation. For example, `#/fields/firstName/required`. (Note: this may sometimes be wrong due to a [https://github.com/epoberezkin/ajv/issues/512](bug in ajv)).
145 |
146 | ### Error List Display
147 |
148 | To disable rendering of the error list at the top of the form, you can set the `showErrorList` prop to `false`. Doing so will still validate the form, but only the inline display will show.
149 |
150 | ```js
151 | render((
152 |
154 | ), document.getElementById("app"));
155 | ```
156 |
157 | > Note: you can also use your own [ErrorList](advanced-customization.md#error-list-template)
158 |
159 | ### The case of empty strings
160 |
161 | When a text input is empty, the field in form data is set to `undefined`. String fields that use `enum` and a `select` widget will have an empty option at the top of the options list that when selected will result in the field being `undefined`.
162 |
163 | One consequence of this is that if you have an empty string in your `enum` array, selecting that option in the `select` input will cause the field to be set to `undefined`, not an empty string.
164 |
165 | If you want to have the field set to a default value when empty you can provide a `ui:emptyValue` field in the `uiSchema` object.
166 |
--------------------------------------------------------------------------------
/mkdocs.yml:
--------------------------------------------------------------------------------
1 | site_name: react-jsonschema-form documentation
2 | docs_dir: docs
3 | theme: readthedocs
4 |
5 | nav:
6 | - Introduction: index.md
7 | - Advanced Customization: advanced-customization.md
8 | - Definitions: definitions.md
9 | - Dependencies: dependencies.md
10 | - Form Customization: form-customization.md
11 | - Theme Customization: theme-customization.md
12 | - Validation: validation.md
13 | - Playground: https://mozilla-services.github.io/react-jsonschema-form/
14 |
15 | markdown_extensions:
16 | - toc:
17 | permalink: true
18 |
--------------------------------------------------------------------------------
/netlify.toml:
--------------------------------------------------------------------------------
1 | [context.deploy-preview.environment]
2 | SHOW_NETLIFY_BADGE = "true"
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "react-jsonschema-form-bs4",
3 | "version": "1.7.1",
4 | "description": "A simple React component capable of building HTML forms out of a JSON schema.",
5 | "scripts": {
6 | "build:lib": "rimraf lib && cross-env NODE_ENV=production babel -d lib/ src/",
7 | "build:dist": "rimraf dist && cross-env NODE_ENV=production webpack --config webpack.config.dist.js",
8 | "build:playground": "rimraf build && cross-env NODE_ENV=production webpack --config webpack.config.prod.js && cp playground/index.prod.html build/index.html",
9 | "cs-check": "prettier -l $npm_package_prettierOptions \"{playground,src,test}/**/*.js\"",
10 | "cs-format": "prettier --jsx-bracket-same-line --trailing-comma es5 --use-tabs false --semi --tab-width 2 \"{playground,src,test}/**/*.js\" --write",
11 | "dist": "npm run build:lib && npm run build:dist",
12 | "lint": "eslint src test playground",
13 | "prepare": "npm run dist",
14 | "precommit": "lint-staged",
15 | "publish-to-gh-pages": "npm run build:playground && gh-pages --dist build/",
16 | "publish-to-npm": "npm run dist && npm publish",
17 | "preversion": "npm run build:playground && npm run dist && npm run cs-check && npm run lint",
18 | "start": "node devServer.js",
19 | "tdd": "cross-env NODE_ENV=test mocha --require @babel/register --watch --require ./test/setup-jsdom.js test/**/*_test.js",
20 | "test": "cross-env NODE_ENV=test mocha --require @babel/register --require ./test/setup-jsdom.js test/**/*_test.js",
21 | "test-coverage": "cross-env NODE_ENV=test nyc --reporter=lcov mocha --require @babel/register --require ./test/setup-jsdom.js test/**/*_test.js",
22 | "test-debug": "cross-env NODE_ENV=test mocha --require @babel/register --require ./test/setup-jsdom.js --debug-brk --inspect test/Form_test.js"
23 | },
24 | "prettierOptions": "--jsx-bracket-same-line --trailing-comma es5 --semi --tab-width 2",
25 | "lint-staged": {
26 | "{playground,src,test}/**/*.js": [
27 | "npm run lint",
28 | "npm run cs-format",
29 | "git add"
30 | ]
31 | },
32 | "main": "lib/index.js",
33 | "types": "types/index.d.ts",
34 | "files": [
35 | "dist",
36 | "lib",
37 | "types"
38 | ],
39 | "engineStrict": false,
40 | "engines": {
41 | "npm": ">=2.14.7",
42 | "node": ">=6"
43 | },
44 | "peerDependencies": {
45 | "react": ">=15"
46 | },
47 | "dependencies": {
48 | "@types/json-schema": "*",
49 | "@types/react": "*",
50 | "@babel/runtime-corejs2": "^7.4.5",
51 | "ajv": "^6.7.0",
52 | "core-js": "^2.5.7",
53 | "lodash.get": "^4.4.2",
54 | "lodash.pick": "^4.4.0",
55 | "lodash.topath": "^4.5.2",
56 | "prop-types": "^15.5.8",
57 | "react-is": "^16.8.4",
58 | "react-lifecycles-compat": "^3.0.4",
59 | "shortid": "^2.2.14"
60 | },
61 | "devDependencies": {
62 | "@babel/cli": "^7.4.4",
63 | "@babel/core": "^7.4.5",
64 | "@babel/plugin-proposal-class-properties": "^7.4.4",
65 | "@babel/plugin-proposal-object-rest-spread": "^7.4.4",
66 | "@babel/plugin-transform-react-jsx": "^7.3.0",
67 | "@babel/plugin-transform-runtime": "^7.4.4",
68 | "@babel/preset-env": "^7.4.5",
69 | "@babel/preset-react": "^7.0.0",
70 | "@babel/register": "^7.4.4",
71 | "atob": "^2.0.3",
72 | "babel-eslint": "^10.0.1",
73 | "babel-loader": "^8.0.6",
74 | "chai": "^3.3.0",
75 | "codemirror": "^5.30.0",
76 | "create-react-ref": "^0.1.0",
77 | "cross-env": "^2.0.1",
78 | "css-loader": "^0.23.1",
79 | "eslint": "^4.9.0",
80 | "eslint-config-react-app": "^2.0.1",
81 | "eslint-plugin-flowtype": "^2.39.1",
82 | "eslint-plugin-import": "^2.7.0",
83 | "eslint-plugin-jsx-a11y": "^5.1.1",
84 | "eslint-plugin-react": "^7.4.0",
85 | "estraverse": "^4.2.0",
86 | "estraverse-fb": "^1.3.1",
87 | "express": "^4.14.0",
88 | "gh-pages": "^0.11.0",
89 | "html": "^1.0.0",
90 | "husky": "^0.13.2",
91 | "jsdom": "^8.3.0",
92 | "json-loader": "^0.5.7",
93 | "lint-staged": "^3.3.1",
94 | "mini-css-extract-plugin": "^0.4.3",
95 | "mocha": "^5.2.0",
96 | "nyc": "^13.2.0",
97 | "prettier": "^1.15.1",
98 | "react": "^15.5.0",
99 | "react-codemirror2": "^4.1.0",
100 | "react-dom": "^15.3.2",
101 | "react-portal": "^4.2.0",
102 | "react-transform-catch-errors": "^1.0.0",
103 | "react-transform-hmr": "^1.0.1",
104 | "redbox-react": "^1.3.3",
105 | "rimraf": "^2.5.4",
106 | "sinon": "^1.17.6",
107 | "style-loader": "^0.13.1",
108 | "webpack": "^4.20.2",
109 | "webpack-cli": "^3.1.2",
110 | "webpack-dev-middleware": "^3.4.0",
111 | "webpack-hot-middleware": "^2.13.2"
112 | },
113 | "directories": {
114 | "test": "test"
115 | },
116 | "repository": {
117 | "type": "git",
118 | "url": "git+https://github.com/peterkelly/react-jsonschema-form-bs4.git"
119 | },
120 | "author": "Peter Kelly ",
121 | "keywords": [
122 | "react",
123 | "form",
124 | "json-schema"
125 | ],
126 | "license": "Apache-2.0",
127 | "homepage": "https://github.com/peterkelly/react-jsonschema-form-bs4#readme"
128 | }
129 |
--------------------------------------------------------------------------------
/playground/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | react-jsonschema-form playground
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
--------------------------------------------------------------------------------
/playground/index.prod.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | react-jsonschema-form playground
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
--------------------------------------------------------------------------------
/playground/samples/additionalProperties.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | schema: {
3 | title: "A customizable registration form",
4 | description: "A simple form with additional properties example.",
5 | type: "object",
6 | required: ["firstName", "lastName"],
7 | additionalProperties: {
8 | type: "string",
9 | },
10 | properties: {
11 | firstName: {
12 | type: "string",
13 | title: "First name",
14 | },
15 | lastName: {
16 | type: "string",
17 | title: "Last name",
18 | },
19 | },
20 | },
21 | uiSchema: {
22 | firstName: {
23 | "ui:autofocus": true,
24 | "ui:emptyValue": "",
25 | },
26 | },
27 | formData: {
28 | firstName: "Chuck",
29 | lastName: "Norris",
30 | assKickCount: "infinity",
31 | },
32 | };
33 |
--------------------------------------------------------------------------------
/playground/samples/alternatives.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | schema: {
3 | definitions: {
4 | Color: {
5 | title: "Color",
6 | type: "string",
7 | anyOf: [
8 | {
9 | type: "string",
10 | enum: ["#ff0000"],
11 | title: "Red",
12 | },
13 | {
14 | type: "string",
15 | enum: ["#00ff00"],
16 | title: "Green",
17 | },
18 | {
19 | type: "string",
20 | enum: ["#0000ff"],
21 | title: "Blue",
22 | },
23 | ],
24 | },
25 | Toggle: {
26 | title: "Toggle",
27 | type: "boolean",
28 | oneOf: [
29 | {
30 | title: "Enable",
31 | const: true,
32 | },
33 | {
34 | title: "Disable",
35 | const: false,
36 | },
37 | ],
38 | },
39 | },
40 | title: "Image editor",
41 | type: "object",
42 | required: ["currentColor", "colorMask", "blendMode"],
43 | properties: {
44 | currentColor: {
45 | $ref: "#/definitions/Color",
46 | title: "Brush color",
47 | },
48 | colorMask: {
49 | type: "array",
50 | uniqueItems: true,
51 | items: {
52 | $ref: "#/definitions/Color",
53 | },
54 | title: "Color mask",
55 | },
56 | toggleMask: {
57 | title: "Apply color mask",
58 | $ref: "#/definitions/Toggle",
59 | },
60 | colorPalette: {
61 | type: "array",
62 | title: "Color palette",
63 | items: {
64 | $ref: "#/definitions/Color",
65 | },
66 | },
67 | blendMode: {
68 | title: "Blend mode",
69 | type: "string",
70 | enum: ["screen", "multiply", "overlay"],
71 | enumNames: ["Screen", "Multiply", "Overlay"],
72 | },
73 | },
74 | },
75 | uiSchema: {
76 | blendMode: {
77 | "ui:enumDisabled": ["multiply"],
78 | },
79 | toggleMask: {
80 | "ui:widget": "radio",
81 | },
82 | },
83 | formData: {
84 | currentColor: "#00ff00",
85 | colorMask: ["#0000ff"],
86 | colorPalette: ["#ff0000"],
87 | blendMode: "screen",
88 | },
89 | };
90 |
--------------------------------------------------------------------------------
/playground/samples/anyOf.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | schema: {
3 | type: "object",
4 | properties: {
5 | age: {
6 | type: "integer",
7 | title: "Age",
8 | },
9 | items: {
10 | type: "array",
11 | items: {
12 | type: "object",
13 | anyOf: [
14 | {
15 | properties: {
16 | foo: {
17 | type: "string",
18 | },
19 | },
20 | },
21 | {
22 | properties: {
23 | bar: {
24 | type: "string",
25 | },
26 | },
27 | },
28 | ],
29 | },
30 | },
31 | },
32 | anyOf: [
33 | {
34 | title: "First method of identification",
35 | properties: {
36 | firstName: {
37 | type: "string",
38 | title: "First name",
39 | default: "Chuck",
40 | },
41 | lastName: {
42 | type: "string",
43 | title: "Last name",
44 | },
45 | },
46 | },
47 | {
48 | title: "Second method of identification",
49 | properties: {
50 | idCode: {
51 | type: "string",
52 | title: "ID code",
53 | },
54 | },
55 | },
56 | ],
57 | },
58 | formData: {},
59 | };
60 |
--------------------------------------------------------------------------------
/playground/samples/arrays.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | schema: {
3 | definitions: {
4 | Thing: {
5 | type: "object",
6 | properties: {
7 | name: {
8 | type: "string",
9 | default: "Default name",
10 | },
11 | },
12 | },
13 | },
14 | type: "object",
15 | properties: {
16 | listOfStrings: {
17 | type: "array",
18 | title: "A list of strings",
19 | items: {
20 | type: "string",
21 | default: "bazinga",
22 | },
23 | },
24 | multipleChoicesList: {
25 | type: "array",
26 | title: "A multiple choices list",
27 | items: {
28 | type: "string",
29 | enum: ["foo", "bar", "fuzz", "qux"],
30 | },
31 | uniqueItems: true,
32 | },
33 | fixedItemsList: {
34 | type: "array",
35 | title: "A list of fixed items",
36 | items: [
37 | {
38 | title: "A string value",
39 | type: "string",
40 | default: "lorem ipsum",
41 | },
42 | {
43 | title: "a boolean value",
44 | type: "boolean",
45 | },
46 | ],
47 | additionalItems: {
48 | title: "Additional item",
49 | type: "number",
50 | },
51 | },
52 | minItemsList: {
53 | type: "array",
54 | title: "A list with a minimal number of items",
55 | minItems: 3,
56 | items: {
57 | $ref: "#/definitions/Thing",
58 | },
59 | },
60 | defaultsAndMinItems: {
61 | type: "array",
62 | title: "List and item level defaults",
63 | minItems: 5,
64 | default: ["carp", "trout", "bream"],
65 | items: {
66 | type: "string",
67 | default: "unidentified",
68 | },
69 | },
70 | nestedList: {
71 | type: "array",
72 | title: "Nested list",
73 | items: {
74 | type: "array",
75 | title: "Inner list",
76 | items: {
77 | type: "string",
78 | default: "lorem ipsum",
79 | },
80 | },
81 | },
82 | unorderable: {
83 | title: "Unorderable items",
84 | type: "array",
85 | items: {
86 | type: "string",
87 | default: "lorem ipsum",
88 | },
89 | },
90 | unremovable: {
91 | title: "Unremovable items",
92 | type: "array",
93 | items: {
94 | type: "string",
95 | default: "lorem ipsum",
96 | },
97 | },
98 | noToolbar: {
99 | title: "No add, remove and order buttons",
100 | type: "array",
101 | items: {
102 | type: "string",
103 | default: "lorem ipsum",
104 | },
105 | },
106 | fixedNoToolbar: {
107 | title: "Fixed array without buttons",
108 | type: "array",
109 | items: [
110 | {
111 | title: "A number",
112 | type: "number",
113 | default: 42,
114 | },
115 | {
116 | title: "A boolean",
117 | type: "boolean",
118 | default: false,
119 | },
120 | ],
121 | additionalItems: {
122 | title: "A string",
123 | type: "string",
124 | default: "lorem ipsum",
125 | },
126 | },
127 | },
128 | },
129 | uiSchema: {
130 | listOfStrings: {
131 | items: { "ui:emptyValue": "" },
132 | },
133 | multipleChoicesList: {
134 | "ui:widget": "checkboxes",
135 | },
136 | fixedItemsList: {
137 | items: [{ "ui:widget": "textarea" }, { "ui:widget": "select" }],
138 | additionalItems: {
139 | "ui:widget": "updown",
140 | },
141 | },
142 | unorderable: {
143 | "ui:options": {
144 | orderable: false,
145 | },
146 | },
147 | unremovable: {
148 | "ui:options": {
149 | removable: false,
150 | },
151 | },
152 | noToolbar: {
153 | "ui:options": {
154 | addable: false,
155 | orderable: false,
156 | removable: false,
157 | },
158 | },
159 | fixedNoToolbar: {
160 | "ui:options": {
161 | addable: false,
162 | orderable: false,
163 | removable: false,
164 | },
165 | },
166 | },
167 | formData: {
168 | listOfStrings: ["foo", "bar"],
169 | multipleChoicesList: ["foo", "bar"],
170 | fixedItemsList: ["Some text", true, 123],
171 | nestedList: [["lorem", "ipsum"], ["dolor"]],
172 | unorderable: ["one", "two"],
173 | unremovable: ["one", "two"],
174 | noToolbar: ["one", "two"],
175 | fixedNoToolbar: [42, true, "additional item one", "additional item two"],
176 | },
177 | };
178 |
--------------------------------------------------------------------------------
/playground/samples/custom.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | schema: {
3 | title: "A localisation form",
4 | type: "object",
5 | required: ["lat", "lon"],
6 | properties: {
7 | lat: {
8 | type: "number",
9 | },
10 | lon: {
11 | type: "number",
12 | },
13 | },
14 | },
15 | uiSchema: {
16 | "ui:field": "geo",
17 | },
18 | formData: {
19 | lat: 0,
20 | lon: 0,
21 | },
22 | };
23 |
--------------------------------------------------------------------------------
/playground/samples/customArray.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 |
3 | function ArrayFieldTemplate(props) {
4 | return (
5 |
6 | {props.items &&
7 | props.items.map(element => (
8 |
9 |
{element.children}
10 | {element.hasMoveDown && (
11 |
16 | Down
17 |
18 | )}
19 | {element.hasMoveUp && (
20 |
25 | Up
26 |
27 | )}
28 |
29 | Delete
30 |
31 |
32 |
33 | ))}
34 |
35 | {props.canAdd && (
36 |
37 |
38 |
39 | Custom +
40 |
41 |
42 |
43 | )}
44 |
45 | );
46 | }
47 |
48 | export default {
49 | schema: {
50 | title: "Custom array of strings",
51 | type: "array",
52 | items: {
53 | type: "string",
54 | },
55 | },
56 | formData: ["react", "jsonschema", "form"],
57 | ArrayFieldTemplate,
58 | };
59 |
--------------------------------------------------------------------------------
/playground/samples/customObject.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 |
3 | function ObjectFieldTemplate({ TitleField, properties, title, description }) {
4 | return (
5 |
6 |
7 |
8 | {properties.map(prop => (
9 |
12 | {prop.content}
13 |
14 | ))}
15 |
16 | {description}
17 |
18 | );
19 | }
20 |
21 | export default {
22 | schema: {
23 | title: "A registration form",
24 | description:
25 | "This is the same as the simple form, but it is rendered as a bootstrap grid. Try shrinking the browser window to see it in action.",
26 | type: "object",
27 | required: ["firstName", "lastName"],
28 | properties: {
29 | firstName: {
30 | type: "string",
31 | title: "First name",
32 | },
33 | lastName: {
34 | type: "string",
35 | title: "Last name",
36 | },
37 | age: {
38 | type: "integer",
39 | title: "Age",
40 | },
41 | bio: {
42 | type: "string",
43 | title: "Bio",
44 | },
45 | password: {
46 | type: "string",
47 | title: "Password",
48 | minLength: 3,
49 | },
50 | telephone: {
51 | type: "string",
52 | title: "Telephone",
53 | minLength: 10,
54 | },
55 | },
56 | },
57 | formData: {
58 | firstName: "Chuck",
59 | lastName: "Norris",
60 | age: 75,
61 | bio: "Roundhouse kicking asses since 1940",
62 | password: "noneed",
63 | },
64 | ObjectFieldTemplate,
65 | };
66 |
--------------------------------------------------------------------------------
/playground/samples/date.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | schema: {
3 | title: "Date and time widgets",
4 | type: "object",
5 | properties: {
6 | native: {
7 | title: "Native",
8 | description:
9 | "May not work on some browsers, notably Firefox Desktop and IE.",
10 | type: "object",
11 | properties: {
12 | datetime: {
13 | type: "string",
14 | format: "date-time",
15 | },
16 | date: {
17 | type: "string",
18 | format: "date",
19 | },
20 | },
21 | },
22 | alternative: {
23 | title: "Alternative",
24 | description: "These work on most platforms.",
25 | type: "object",
26 | properties: {
27 | "alt-datetime": {
28 | type: "string",
29 | format: "date-time",
30 | },
31 | "alt-date": {
32 | type: "string",
33 | format: "date",
34 | },
35 | },
36 | },
37 | },
38 | },
39 | uiSchema: {
40 | alternative: {
41 | "alt-datetime": {
42 | "ui:widget": "alt-datetime",
43 | "ui:options": {
44 | yearsRange: [1980, 2030],
45 | },
46 | },
47 | "alt-date": {
48 | "ui:widget": "alt-date",
49 | "ui:options": {
50 | yearsRange: [1980, 2030],
51 | },
52 | },
53 | },
54 | },
55 | formData: {},
56 | };
57 |
--------------------------------------------------------------------------------
/playground/samples/errors.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | schema: {
3 | title: "Contextualized errors",
4 | type: "object",
5 | properties: {
6 | firstName: {
7 | type: "string",
8 | title: "First name",
9 | minLength: 8,
10 | pattern: "\\d+",
11 | },
12 | active: {
13 | type: "boolean",
14 | title: "Active",
15 | },
16 | skills: {
17 | type: "array",
18 | items: {
19 | type: "string",
20 | minLength: 5,
21 | },
22 | },
23 | multipleChoicesList: {
24 | type: "array",
25 | title: "Pick max two items",
26 | uniqueItems: true,
27 | maxItems: 2,
28 | items: {
29 | type: "string",
30 | enum: ["foo", "bar", "fuzz"],
31 | },
32 | },
33 | },
34 | },
35 | uiSchema: {},
36 | formData: {
37 | firstName: "Chuck",
38 | active: "wrong",
39 | skills: ["karate", "budo", "aikido"],
40 | multipleChoicesList: ["foo", "bar", "fuzz"],
41 | },
42 | };
43 |
--------------------------------------------------------------------------------
/playground/samples/files.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | schema: {
3 | title: "Files",
4 | type: "object",
5 | properties: {
6 | file: {
7 | type: "string",
8 | format: "data-url",
9 | title: "Single file",
10 | },
11 | files: {
12 | type: "array",
13 | title: "Multiple files",
14 | items: {
15 | type: "string",
16 | format: "data-url",
17 | },
18 | },
19 | },
20 | },
21 | uiSchema: {},
22 | formData: {},
23 | };
24 |
--------------------------------------------------------------------------------
/playground/samples/index.js:
--------------------------------------------------------------------------------
1 | import arrays from "./arrays";
2 | import anyOf from "./anyOf";
3 | import oneOf from "./oneOf";
4 | import nested from "./nested";
5 | import numbers from "./numbers";
6 | import simple from "./simple";
7 | import widgets from "./widgets";
8 | import ordering from "./ordering";
9 | import references from "./references";
10 | import custom from "./custom";
11 | import errors from "./errors";
12 | import large from "./large";
13 | import date from "./date";
14 | import validation from "./validation";
15 | import files from "./files";
16 | import single from "./single";
17 | import customArray from "./customArray";
18 | import customObject from "./customObject";
19 | import alternatives from "./alternatives";
20 | import propertyDependencies from "./propertyDependencies";
21 | import schemaDependencies from "./schemaDependencies";
22 | import additionalProperties from "./additionalProperties";
23 | import nullable from "./nullable";
24 | import nullField from "./null";
25 |
26 | export const samples = {
27 | Simple: simple,
28 | Nested: nested,
29 | Arrays: arrays,
30 | Numbers: numbers,
31 | Widgets: widgets,
32 | Ordering: ordering,
33 | References: references,
34 | Custom: custom,
35 | Errors: errors,
36 | Large: large,
37 | "Date & time": date,
38 | Validation: validation,
39 | Files: files,
40 | Single: single,
41 | "Custom Array": customArray,
42 | "Custom Object": customObject,
43 | Alternatives: alternatives,
44 | "Property dependencies": propertyDependencies,
45 | "Schema dependencies": schemaDependencies,
46 | "Additional Properties": additionalProperties,
47 | "Any Of": anyOf,
48 | "One Of": oneOf,
49 | "Null fields": nullField,
50 | Nullable: nullable,
51 | };
52 |
--------------------------------------------------------------------------------
/playground/samples/large.js:
--------------------------------------------------------------------------------
1 | function largeEnum(n) {
2 | const list = [];
3 | for (let i = 0; i < n; i++) {
4 | list.push("option #" + i);
5 | }
6 | return list;
7 | }
8 |
9 | module.exports = {
10 | schema: {
11 | definitions: {
12 | largeEnum: { type: "string", enum: largeEnum(100) },
13 | },
14 | title: "A rather large form",
15 | type: "object",
16 | properties: {
17 | string: {
18 | type: "string",
19 | title: "Some string",
20 | },
21 | choice1: { $ref: "#/definitions/largeEnum" },
22 | choice2: { $ref: "#/definitions/largeEnum" },
23 | choice3: { $ref: "#/definitions/largeEnum" },
24 | choice4: { $ref: "#/definitions/largeEnum" },
25 | choice5: { $ref: "#/definitions/largeEnum" },
26 | choice6: { $ref: "#/definitions/largeEnum" },
27 | choice7: { $ref: "#/definitions/largeEnum" },
28 | choice8: { $ref: "#/definitions/largeEnum" },
29 | choice9: { $ref: "#/definitions/largeEnum" },
30 | choice10: { $ref: "#/definitions/largeEnum" },
31 | },
32 | },
33 | uiSchema: {
34 | choice1: {
35 | "ui:placeholder": "Choose one",
36 | },
37 | },
38 | formData: {},
39 | };
40 |
--------------------------------------------------------------------------------
/playground/samples/nested.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | schema: {
3 | title: "A list of tasks",
4 | type: "object",
5 | required: ["title"],
6 | properties: {
7 | title: {
8 | type: "string",
9 | title: "Task list title",
10 | },
11 | tasks: {
12 | type: "array",
13 | title: "Tasks",
14 | items: {
15 | type: "object",
16 | required: ["title"],
17 | properties: {
18 | title: {
19 | type: "string",
20 | title: "Title",
21 | description: "A sample title",
22 | },
23 | details: {
24 | type: "string",
25 | title: "Task details",
26 | description: "Enter the task details",
27 | },
28 | done: {
29 | type: "boolean",
30 | title: "Done?",
31 | default: false,
32 | },
33 | },
34 | },
35 | },
36 | },
37 | },
38 | uiSchema: {
39 | tasks: {
40 | items: {
41 | details: {
42 | "ui:widget": "textarea",
43 | },
44 | },
45 | },
46 | },
47 | formData: {
48 | title: "My current tasks",
49 | tasks: [
50 | {
51 | title: "My first task",
52 | details:
53 | "Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.",
54 | done: true,
55 | },
56 | {
57 | title: "My second task",
58 | details:
59 | "Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur",
60 | done: false,
61 | },
62 | ],
63 | },
64 | };
65 |
--------------------------------------------------------------------------------
/playground/samples/null.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | schema: {
3 | title: "Null field example",
4 | description: "A short form with a null field",
5 | type: "object",
6 | required: ["firstName"],
7 | properties: {
8 | helpText: {
9 | title: "A null field",
10 | description:
11 | "Null fields like this are great for adding extra information",
12 | type: "null",
13 | },
14 | firstName: {
15 | type: "string",
16 | title: "A regular string field",
17 | default: "Chuck",
18 | },
19 | },
20 | },
21 | uiSchema: {
22 | firstName: {
23 | "ui:autofocus": true,
24 | "ui:emptyValue": "",
25 | },
26 | },
27 | formData: {},
28 | };
29 |
--------------------------------------------------------------------------------
/playground/samples/nullable.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | schema: {
3 | title: "A registration form (nullable)",
4 | description: "A simple form example using nullable types",
5 | type: "object",
6 | required: ["firstName", "lastName"],
7 | properties: {
8 | firstName: {
9 | type: "string",
10 | title: "First name",
11 | default: "Chuck",
12 | },
13 | lastName: {
14 | type: "string",
15 | title: "Last name",
16 | },
17 | age: {
18 | type: ["integer", "null"],
19 | title: "Age",
20 | },
21 | bio: {
22 | type: ["string", "null"],
23 | title: "Bio",
24 | },
25 | password: {
26 | type: "string",
27 | title: "Password",
28 | minLength: 3,
29 | },
30 | telephone: {
31 | type: "string",
32 | title: "Telephone",
33 | minLength: 10,
34 | },
35 | },
36 | },
37 | uiSchema: {
38 | firstName: {
39 | "ui:autofocus": true,
40 | "ui:emptyValue": "",
41 | },
42 | age: {
43 | "ui:widget": "updown",
44 | "ui:title": "Age of person",
45 | "ui:description": "(earthian year)",
46 | "ui:emptyValue": null,
47 | },
48 | bio: {
49 | "ui:widget": "textarea",
50 | "ui:placeholder":
51 | "Leaving this field empty will cause formData property to be `null`",
52 | "ui:emptyValue": null,
53 | },
54 | password: {
55 | "ui:widget": "password",
56 | "ui:help": "Hint: Make it strong!",
57 | },
58 | date: {
59 | "ui:widget": "alt-datetime",
60 | },
61 | telephone: {
62 | "ui:options": {
63 | inputType: "tel",
64 | },
65 | },
66 | },
67 | formData: {
68 | lastName: "Norris",
69 | age: 75,
70 | bio: null,
71 | password: "noneed",
72 | },
73 | };
74 |
--------------------------------------------------------------------------------
/playground/samples/numbers.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | schema: {
3 | type: "object",
4 | title: "Number fields & widgets",
5 | properties: {
6 | number: {
7 | title: "Number",
8 | type: "number",
9 | },
10 | integer: {
11 | title: "Integer",
12 | type: "integer",
13 | },
14 | numberEnum: {
15 | type: "number",
16 | title: "Number enum",
17 | enum: [1, 2, 3],
18 | },
19 | numberEnumRadio: {
20 | type: "number",
21 | title: "Number enum",
22 | enum: [1, 2, 3],
23 | },
24 | integerRange: {
25 | title: "Integer range",
26 | type: "integer",
27 | minimum: 42,
28 | maximum: 100,
29 | },
30 | integerRangeSteps: {
31 | title: "Integer range (by 10)",
32 | type: "integer",
33 | minimum: 50,
34 | maximum: 100,
35 | multipleOf: 10,
36 | },
37 | },
38 | },
39 | uiSchema: {
40 | integer: {
41 | "ui:widget": "updown",
42 | },
43 | numberEnumRadio: {
44 | "ui:widget": "radio",
45 | "ui:options": {
46 | inline: true,
47 | },
48 | },
49 | integerRange: {
50 | "ui:widget": "range",
51 | },
52 | integerRangeSteps: {
53 | "ui:widget": "range",
54 | },
55 | },
56 | formData: {
57 | number: 3.14,
58 | integer: 42,
59 | numberEnum: 2,
60 | integerRange: 42,
61 | integerRangeSteps: 80,
62 | },
63 | };
64 |
--------------------------------------------------------------------------------
/playground/samples/oneOf.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | schema: {
3 | type: "object",
4 | oneOf: [
5 | {
6 | properties: {
7 | lorem: {
8 | type: "string",
9 | },
10 | },
11 | required: ["lorem"],
12 | },
13 | {
14 | properties: {
15 | ipsum: {
16 | type: "string",
17 | },
18 | },
19 | required: ["ipsum"],
20 | },
21 | ],
22 | },
23 | formData: {},
24 | };
25 |
--------------------------------------------------------------------------------
/playground/samples/ordering.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | schema: {
3 | title: "A registration form",
4 | type: "object",
5 | required: ["firstName", "lastName"],
6 | properties: {
7 | password: {
8 | type: "string",
9 | title: "Password",
10 | },
11 | lastName: {
12 | type: "string",
13 | title: "Last name",
14 | },
15 | bio: {
16 | type: "string",
17 | title: "Bio",
18 | },
19 | firstName: {
20 | type: "string",
21 | title: "First name",
22 | },
23 | age: {
24 | type: "integer",
25 | title: "Age",
26 | },
27 | },
28 | },
29 | uiSchema: {
30 | "ui:order": ["firstName", "lastName", "*", "password"],
31 | age: {
32 | "ui:widget": "updown",
33 | },
34 | bio: {
35 | "ui:widget": "textarea",
36 | },
37 | password: {
38 | "ui:widget": "password",
39 | },
40 | },
41 | formData: {
42 | firstName: "Chuck",
43 | lastName: "Norris",
44 | age: 75,
45 | bio: "Roundhouse kicking asses since 1940",
46 | password: "noneed",
47 | },
48 | };
49 |
--------------------------------------------------------------------------------
/playground/samples/propertyDependencies.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | schema: {
3 | title: "Property dependencies",
4 | description: "These samples are best viewed without live validation.",
5 | type: "object",
6 | properties: {
7 | unidirectional: {
8 | title: "Unidirectional",
9 | src:
10 | "https://spacetelescope.github.io/understanding-json-schema/reference/object.html#dependencies",
11 | type: "object",
12 | properties: {
13 | name: {
14 | type: "string",
15 | },
16 | credit_card: {
17 | type: "number",
18 | },
19 | billing_address: {
20 | type: "string",
21 | },
22 | },
23 | required: ["name"],
24 | dependencies: {
25 | credit_card: ["billing_address"],
26 | },
27 | },
28 | bidirectional: {
29 | title: "Bidirectional",
30 | src:
31 | "https://spacetelescope.github.io/understanding-json-schema/reference/object.html#dependencies",
32 | description:
33 | "Dependencies are not bidirectional, you can, of course, define the bidirectional dependencies explicitly.",
34 | type: "object",
35 | properties: {
36 | name: {
37 | type: "string",
38 | },
39 | credit_card: {
40 | type: "number",
41 | },
42 | billing_address: {
43 | type: "string",
44 | },
45 | },
46 | required: ["name"],
47 | dependencies: {
48 | credit_card: ["billing_address"],
49 | billing_address: ["credit_card"],
50 | },
51 | },
52 | },
53 | },
54 | uiSchema: {
55 | unidirectional: {
56 | credit_card: {
57 | "ui:help":
58 | "If you enter anything here then billing_address will become required.",
59 | },
60 | billing_address: {
61 | "ui:help":
62 | "It’s okay to have a billing address without a credit card number.",
63 | },
64 | },
65 | bidirectional: {
66 | credit_card: {
67 | "ui:help":
68 | "If you enter anything here then billing_address will become required.",
69 | },
70 | billing_address: {
71 | "ui:help":
72 | "If you enter anything here then credit_card will become required.",
73 | },
74 | },
75 | },
76 | formData: {
77 | unidirectional: {
78 | name: "Tim",
79 | },
80 | bidirectional: {
81 | name: "Jill",
82 | },
83 | },
84 | };
85 |
--------------------------------------------------------------------------------
/playground/samples/references.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | schema: {
3 | definitions: {
4 | address: {
5 | type: "object",
6 | properties: {
7 | street_address: { type: "string" },
8 | city: { type: "string" },
9 | state: { type: "string" },
10 | },
11 | required: ["street_address", "city", "state"],
12 | },
13 | node: {
14 | type: "object",
15 | properties: {
16 | name: { type: "string" },
17 | children: {
18 | type: "array",
19 | items: {
20 | $ref: "#/definitions/node",
21 | },
22 | },
23 | },
24 | },
25 | },
26 | type: "object",
27 | properties: {
28 | billing_address: {
29 | title: "Billing address",
30 | $ref: "#/definitions/address",
31 | },
32 | shipping_address: {
33 | title: "Shipping address",
34 | $ref: "#/definitions/address",
35 | },
36 | tree: {
37 | title: "Recursive references",
38 | $ref: "#/definitions/node",
39 | },
40 | },
41 | },
42 | uiSchema: {
43 | "ui:order": ["shipping_address", "billing_address", "tree"],
44 | },
45 | formData: {
46 | billing_address: {
47 | street_address: "21, Jump Street",
48 | city: "Babel",
49 | state: "Neverland",
50 | },
51 | shipping_address: {
52 | street_address: "221B, Baker Street",
53 | city: "London",
54 | state: "N/A",
55 | },
56 | tree: {
57 | name: "root",
58 | children: [{ name: "leaf" }],
59 | },
60 | },
61 | };
62 |
--------------------------------------------------------------------------------
/playground/samples/schemaDependencies.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | schema: {
3 | title: "Schema dependencies",
4 | description: "These samples are best viewed without live validation.",
5 | type: "object",
6 | properties: {
7 | simple: {
8 | src:
9 | "https://spacetelescope.github.io/understanding-json-schema/reference/object.html#dependencies",
10 | title: "Simple",
11 | type: "object",
12 | properties: {
13 | name: {
14 | type: "string",
15 | },
16 | credit_card: {
17 | type: "number",
18 | },
19 | },
20 | required: ["name"],
21 | dependencies: {
22 | credit_card: {
23 | properties: {
24 | billing_address: {
25 | type: "string",
26 | },
27 | },
28 | required: ["billing_address"],
29 | },
30 | },
31 | },
32 | conditional: {
33 | title: "Conditional",
34 | $ref: "#/definitions/person",
35 | },
36 | arrayOfConditionals: {
37 | title: "Array of conditionals",
38 | type: "array",
39 | items: {
40 | $ref: "#/definitions/person",
41 | },
42 | },
43 | fixedArrayOfConditionals: {
44 | title: "Fixed array of conditionals",
45 | type: "array",
46 | items: [
47 | {
48 | title: "Primary person",
49 | $ref: "#/definitions/person",
50 | },
51 | ],
52 | additionalItems: {
53 | title: "Additional person",
54 | $ref: "#/definitions/person",
55 | },
56 | },
57 | },
58 | definitions: {
59 | person: {
60 | title: "Person",
61 | type: "object",
62 | properties: {
63 | "Do you have any pets?": {
64 | type: "string",
65 | enum: ["No", "Yes: One", "Yes: More than one"],
66 | default: "No",
67 | },
68 | },
69 | required: ["Do you have any pets?"],
70 | dependencies: {
71 | "Do you have any pets?": {
72 | oneOf: [
73 | {
74 | properties: {
75 | "Do you have any pets?": {
76 | enum: ["No"],
77 | },
78 | },
79 | },
80 | {
81 | properties: {
82 | "Do you have any pets?": {
83 | enum: ["Yes: One"],
84 | },
85 | "How old is your pet?": {
86 | type: "number",
87 | },
88 | },
89 | required: ["How old is your pet?"],
90 | },
91 | {
92 | properties: {
93 | "Do you have any pets?": {
94 | enum: ["Yes: More than one"],
95 | },
96 | "Do you want to get rid of any?": {
97 | type: "boolean",
98 | },
99 | },
100 | required: ["Do you want to get rid of any?"],
101 | },
102 | ],
103 | },
104 | },
105 | },
106 | },
107 | },
108 | uiSchema: {
109 | simple: {
110 | credit_card: {
111 | "ui:help":
112 | "If you enter anything here then billing_address will be dynamically added to the form.",
113 | },
114 | },
115 | conditional: {
116 | "Do you want to get rid of any?": {
117 | "ui:widget": "radio",
118 | },
119 | },
120 | arrayOfConditionals: {
121 | items: {
122 | "Do you want to get rid of any?": {
123 | "ui:widget": "radio",
124 | },
125 | },
126 | },
127 | fixedArrayOfConditionals: {
128 | items: {
129 | "Do you want to get rid of any?": {
130 | "ui:widget": "radio",
131 | },
132 | },
133 | additionalItems: {
134 | "Do you want to get rid of any?": {
135 | "ui:widget": "radio",
136 | },
137 | },
138 | },
139 | },
140 | formData: {
141 | simple: {
142 | name: "Randy",
143 | },
144 | conditional: {
145 | "Do you have any pets?": "No",
146 | },
147 | arrayOfConditionals: [
148 | {
149 | "Do you have any pets?": "Yes: One",
150 | "How old is your pet?": 6,
151 | },
152 | {
153 | "Do you have any pets?": "Yes: More than one",
154 | "Do you want to get rid of any?": false,
155 | },
156 | ],
157 | fixedArrayOfConditionals: [
158 | {
159 | "Do you have any pets?": "No",
160 | },
161 | {
162 | "Do you have any pets?": "Yes: One",
163 | "How old is your pet?": 6,
164 | },
165 | {
166 | "Do you have any pets?": "Yes: More than one",
167 | "Do you want to get rid of any?": true,
168 | },
169 | ],
170 | },
171 | };
172 |
--------------------------------------------------------------------------------
/playground/samples/simple.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | schema: {
3 | title: "A registration form",
4 | description: "A simple form example.",
5 | type: "object",
6 | required: ["firstName", "lastName"],
7 | properties: {
8 | firstName: {
9 | type: "string",
10 | title: "First name",
11 | default: "Chuck",
12 | },
13 | lastName: {
14 | type: "string",
15 | title: "Last name",
16 | },
17 | age: {
18 | type: "integer",
19 | title: "Age",
20 | },
21 | bio: {
22 | type: "string",
23 | title: "Bio",
24 | },
25 | password: {
26 | type: "string",
27 | title: "Password",
28 | minLength: 3,
29 | },
30 | telephone: {
31 | type: "string",
32 | title: "Telephone",
33 | minLength: 10,
34 | },
35 | },
36 | },
37 | uiSchema: {
38 | firstName: {
39 | "ui:autofocus": true,
40 | "ui:emptyValue": "",
41 | },
42 | age: {
43 | "ui:widget": "updown",
44 | "ui:title": "Age of person",
45 | "ui:description": "(earthian year)",
46 | },
47 | bio: {
48 | "ui:widget": "textarea",
49 | },
50 | password: {
51 | "ui:widget": "password",
52 | "ui:help": "Hint: Make it strong!",
53 | },
54 | date: {
55 | "ui:widget": "alt-datetime",
56 | },
57 | telephone: {
58 | "ui:options": {
59 | inputType: "tel",
60 | },
61 | },
62 | },
63 | formData: {
64 | lastName: "Norris",
65 | age: 75,
66 | bio: "Roundhouse kicking asses since 1940",
67 | password: "noneed",
68 | },
69 | };
70 |
--------------------------------------------------------------------------------
/playground/samples/single.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | schema: {
3 | title: "A single-field form",
4 | type: "string",
5 | },
6 | formData: "initial value",
7 | uiSchema: {},
8 | };
9 |
--------------------------------------------------------------------------------
/playground/samples/validation.js:
--------------------------------------------------------------------------------
1 | function validate({ pass1, pass2 }, errors) {
2 | if (pass1 !== pass2) {
3 | errors.pass2.addError("Passwords don't match.");
4 | }
5 | return errors;
6 | }
7 |
8 | function transformErrors(errors) {
9 | return errors.map(error => {
10 | if (error.name === "minimum" && error.property === "instance.age") {
11 | return Object.assign({}, error, {
12 | message: "You need to be 18 because of some legal thing",
13 | });
14 | }
15 | return error;
16 | });
17 | }
18 |
19 | export default {
20 | schema: {
21 | title: "Custom validation",
22 | description:
23 | "This form defines custom validation rules checking that the two passwords match.",
24 | type: "object",
25 | properties: {
26 | pass1: {
27 | title: "Password",
28 | type: "string",
29 | minLength: 3,
30 | },
31 | pass2: {
32 | title: "Repeat password",
33 | type: "string",
34 | minLength: 3,
35 | },
36 | age: {
37 | title: "Age",
38 | type: "number",
39 | minimum: 18,
40 | },
41 | },
42 | },
43 | uiSchema: {
44 | pass1: { "ui:widget": "password" },
45 | pass2: { "ui:widget": "password" },
46 | },
47 | formData: {},
48 | validate,
49 | transformErrors,
50 | };
51 |
--------------------------------------------------------------------------------
/playground/samples/widgets.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 |
3 | export default {
4 | schema: {
5 | title: "Widgets",
6 | type: "object",
7 | properties: {
8 | stringFormats: {
9 | type: "object",
10 | title: "String formats",
11 | properties: {
12 | email: {
13 | type: "string",
14 | format: "email",
15 | },
16 | uri: {
17 | type: "string",
18 | format: "uri",
19 | },
20 | },
21 | },
22 | boolean: {
23 | type: "object",
24 | title: "Boolean field",
25 | properties: {
26 | default: {
27 | type: "boolean",
28 | title: "checkbox (default)",
29 | description: "This is the checkbox-description",
30 | },
31 | radio: {
32 | type: "boolean",
33 | title: "radio buttons",
34 | description: "This is the radio-description",
35 | },
36 | select: {
37 | type: "boolean",
38 | title: "select box",
39 | description: "This is the select-description",
40 | },
41 | },
42 | },
43 | string: {
44 | type: "object",
45 | title: "String field",
46 | properties: {
47 | default: {
48 | type: "string",
49 | title: "text input (default)",
50 | },
51 | textarea: {
52 | type: "string",
53 | title: "textarea",
54 | },
55 | color: {
56 | type: "string",
57 | title: "color picker",
58 | default: "#151ce6",
59 | },
60 | },
61 | },
62 | secret: {
63 | type: "string",
64 | default: "I'm a hidden string.",
65 | },
66 | disabled: {
67 | type: "string",
68 | title: "A disabled field",
69 | default: "I am disabled.",
70 | },
71 | readonly: {
72 | type: "string",
73 | title: "A readonly field",
74 | default: "I am read-only.",
75 | },
76 | readonly2: {
77 | type: "string",
78 | title: "Another readonly field",
79 | default: "I am also read-only.",
80 | readOnly: true,
81 | },
82 | widgetOptions: {
83 | title: "Custom widget with options",
84 | type: "string",
85 | default: "I am yellow",
86 | },
87 | selectWidgetOptions: {
88 | title: "Custom select widget with options",
89 | type: "string",
90 | enum: ["foo", "bar"],
91 | enumNames: ["Foo", "Bar"],
92 | },
93 | },
94 | },
95 | uiSchema: {
96 | boolean: {
97 | radio: {
98 | "ui:widget": "radio",
99 | },
100 | select: {
101 | "ui:widget": "select",
102 | },
103 | },
104 | string: {
105 | textarea: {
106 | "ui:widget": "textarea",
107 | "ui:options": {
108 | rows: 5,
109 | },
110 | },
111 | color: {
112 | "ui:widget": "color",
113 | },
114 | },
115 | secret: {
116 | "ui:widget": "hidden",
117 | },
118 | disabled: {
119 | "ui:disabled": true,
120 | },
121 | readonly: {
122 | "ui:readonly": true,
123 | },
124 | widgetOptions: {
125 | "ui:widget": ({ value, onChange, options }) => {
126 | const { backgroundColor } = options;
127 | return (
128 | onChange(event.target.value)}
131 | style={{ backgroundColor }}
132 | value={value}
133 | />
134 | );
135 | },
136 | "ui:options": {
137 | backgroundColor: "yellow",
138 | },
139 | },
140 | selectWidgetOptions: {
141 | "ui:widget": ({ value, onChange, options }) => {
142 | const { enumOptions, backgroundColor } = options;
143 | return (
144 | onChange(event.target.value)}>
149 | {enumOptions.map(({ label, value }, i) => {
150 | return (
151 |
152 | {label}
153 |
154 | );
155 | })}
156 |
157 | );
158 | },
159 | "ui:options": {
160 | backgroundColor: "pink",
161 | },
162 | },
163 | },
164 | formData: {
165 | stringFormats: {
166 | email: "chuck@norris.net",
167 | uri: "http://chucknorris.com/",
168 | },
169 | boolean: {
170 | default: true,
171 | radio: true,
172 | select: true,
173 | },
174 | string: {
175 | default: "Hello...",
176 | textarea: "... World",
177 | },
178 | secret: "I'm a hidden string.",
179 | },
180 | };
181 |
--------------------------------------------------------------------------------
/src/components/AddButton.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import IconButton from "./IconButton";
3 |
4 | export default function AddButton({ className, onClick, disabled }) {
5 | return (
6 |
18 | );
19 | }
20 |
--------------------------------------------------------------------------------
/src/components/ErrorList.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 |
3 | export default function ErrorList(props) {
4 | const { errors } = props;
5 | return (
6 |
7 |
Errors
8 |
9 | {errors.map((error, i) => {
10 | return (
11 |
12 | {error.stack}
13 |
14 | );
15 | })}
16 |
17 |
18 | );
19 | }
20 |
--------------------------------------------------------------------------------
/src/components/IconButton.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 |
3 | export default function IconButton(props) {
4 | const { type = "default", icon, className, ...otherProps } = props;
5 | return (
6 |
10 |
11 |
12 | );
13 | }
14 |
--------------------------------------------------------------------------------
/src/components/fields/BooleanField.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import * as types from "../../types";
3 |
4 | import {
5 | getWidget,
6 | getUiOptions,
7 | optionsList,
8 | getDefaultRegistry,
9 | } from "../../utils";
10 |
11 | function BooleanField(props) {
12 | const {
13 | schema,
14 | name,
15 | uiSchema,
16 | idSchema,
17 | formData,
18 | registry = getDefaultRegistry(),
19 | required,
20 | disabled,
21 | readonly,
22 | autofocus,
23 | onChange,
24 | onFocus,
25 | onBlur,
26 | rawErrors,
27 | } = props;
28 | const { title } = schema;
29 | const { widgets, formContext } = registry;
30 | const { widget = "checkbox", ...options } = getUiOptions(uiSchema);
31 | const Widget = getWidget(schema, widget, widgets);
32 |
33 | let enumOptions;
34 |
35 | if (Array.isArray(schema.oneOf)) {
36 | enumOptions = optionsList({
37 | oneOf: schema.oneOf.map(option => ({
38 | ...option,
39 | title: option.title || (option.const === true ? "yes" : "no"),
40 | })),
41 | });
42 | } else {
43 | enumOptions = optionsList({
44 | enum: schema.enum || [true, false],
45 | enumNames:
46 | schema.enumNames ||
47 | (schema.enum && schema.enum[0] === false
48 | ? ["no", "yes"]
49 | : ["yes", "no"]),
50 | });
51 | }
52 |
53 | return (
54 |
71 | );
72 | }
73 |
74 | if (process.env.NODE_ENV !== "production") {
75 | BooleanField.propTypes = types.fieldProps;
76 | }
77 |
78 | BooleanField.defaultProps = {
79 | uiSchema: {},
80 | disabled: false,
81 | readonly: false,
82 | autofocus: false,
83 | };
84 |
85 | export default BooleanField;
86 |
--------------------------------------------------------------------------------
/src/components/fields/DescriptionField.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import PropTypes from "prop-types";
3 |
4 | function DescriptionField(props) {
5 | const { id, description } = props;
6 | if (!description) {
7 | return null;
8 | }
9 | if (typeof description === "string") {
10 | return (
11 |
12 | {description}
13 |
14 | );
15 | } else {
16 | return (
17 |
18 | {description}
19 |
20 | );
21 | }
22 | }
23 |
24 | if (process.env.NODE_ENV !== "production") {
25 | DescriptionField.propTypes = {
26 | id: PropTypes.string,
27 | description: PropTypes.oneOfType([PropTypes.string, PropTypes.element]),
28 | };
29 | }
30 |
31 | export default DescriptionField;
32 |
--------------------------------------------------------------------------------
/src/components/fields/MultiSchemaField.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from "react";
2 | import PropTypes from "prop-types";
3 | import * as types from "../../types";
4 | import {
5 | getUiOptions,
6 | getWidget,
7 | guessType,
8 | retrieveSchema,
9 | getDefaultFormState,
10 | getMatchingOption,
11 | } from "../../utils";
12 |
13 | class AnyOfField extends Component {
14 | constructor(props) {
15 | super(props);
16 |
17 | const { formData, options } = this.props;
18 |
19 | this.state = {
20 | selectedOption: this.getMatchingOption(formData, options),
21 | };
22 | }
23 |
24 | componentWillReceiveProps(nextProps) {
25 | const matchingOption = this.getMatchingOption(
26 | nextProps.formData,
27 | nextProps.options
28 | );
29 |
30 | if (matchingOption === this.state.selectedOption) {
31 | return;
32 | }
33 |
34 | this.setState({ selectedOption: matchingOption });
35 | }
36 |
37 | getMatchingOption(formData, options) {
38 | const { definitions } = this.props.registry;
39 |
40 | let option = getMatchingOption(formData, options, definitions);
41 | if (option !== 0) {
42 | return option;
43 | }
44 | // If the form data matches none of the options, use the currently selected
45 | // option, assuming it's available; otherwise use the first option
46 | return this && this.state ? this.state.selectedOption : 0;
47 | }
48 |
49 | onOptionChange = option => {
50 | const selectedOption = parseInt(option, 10);
51 | const { formData, onChange, options, registry } = this.props;
52 | const { definitions } = registry;
53 | const newOption = retrieveSchema(
54 | options[selectedOption],
55 | definitions,
56 | formData
57 | );
58 |
59 | // If the new option is of type object and the current data is an object,
60 | // discard properties added using the old option.
61 | let newFormData = undefined;
62 | if (
63 | guessType(formData) === "object" &&
64 | (newOption.type === "object" || newOption.properties)
65 | ) {
66 | newFormData = Object.assign({}, formData);
67 |
68 | const optionsToDiscard = options.slice();
69 | optionsToDiscard.splice(selectedOption, 1);
70 |
71 | // Discard any data added using other options
72 | for (const option of optionsToDiscard) {
73 | if (option.properties) {
74 | for (const key in option.properties) {
75 | if (newFormData.hasOwnProperty(key)) {
76 | delete newFormData[key];
77 | }
78 | }
79 | }
80 | }
81 | }
82 | // Call getDefaultFormState to make sure defaults are populated on change.
83 | onChange(
84 | getDefaultFormState(options[selectedOption], newFormData, definitions)
85 | );
86 |
87 | this.setState({
88 | selectedOption: parseInt(option, 10),
89 | });
90 | };
91 |
92 | render() {
93 | const {
94 | baseType,
95 | disabled,
96 | errorSchema,
97 | formData,
98 | idPrefix,
99 | idSchema,
100 | onBlur,
101 | onChange,
102 | onFocus,
103 | options,
104 | registry,
105 | safeRenderCompletion,
106 | uiSchema,
107 | } = this.props;
108 |
109 | const _SchemaField = registry.fields.SchemaField;
110 | const { widgets } = registry;
111 | const { selectedOption } = this.state;
112 | const { widget = "select", ...uiOptions } = getUiOptions(uiSchema);
113 | const Widget = getWidget({ type: "number" }, widget, widgets);
114 |
115 | const option = options[selectedOption] || null;
116 | let optionSchema;
117 |
118 | if (option) {
119 | // If the subschema doesn't declare a type, infer the type from the
120 | // parent schema
121 | optionSchema = option.type
122 | ? option
123 | : Object.assign({}, option, { type: baseType });
124 | }
125 |
126 | const enumOptions = options.map((option, index) => ({
127 | label: option.title || `Option ${index + 1}`,
128 | value: index,
129 | }));
130 |
131 | return (
132 |
133 |
134 |
144 |
145 |
146 | {option !== null && (
147 | <_SchemaField
148 | schema={optionSchema}
149 | uiSchema={uiSchema}
150 | errorSchema={errorSchema}
151 | idSchema={idSchema}
152 | idPrefix={idPrefix}
153 | formData={formData}
154 | onChange={onChange}
155 | onBlur={onBlur}
156 | onFocus={onFocus}
157 | registry={registry}
158 | safeRenderCompletion={safeRenderCompletion}
159 | disabled={disabled}
160 | />
161 | )}
162 |
163 | );
164 | }
165 | }
166 |
167 | AnyOfField.defaultProps = {
168 | disabled: false,
169 | errorSchema: {},
170 | idSchema: {},
171 | uiSchema: {},
172 | };
173 |
174 | if (process.env.NODE_ENV !== "production") {
175 | AnyOfField.propTypes = {
176 | options: PropTypes.arrayOf(PropTypes.object).isRequired,
177 | baseType: PropTypes.string,
178 | uiSchema: PropTypes.object,
179 | idSchema: PropTypes.object,
180 | formData: PropTypes.any,
181 | errorSchema: PropTypes.object,
182 | registry: types.registry.isRequired,
183 | };
184 | }
185 |
186 | export default AnyOfField;
187 |
--------------------------------------------------------------------------------
/src/components/fields/NullField.js:
--------------------------------------------------------------------------------
1 | import { Component } from "react";
2 | import * as types from "../../types";
3 |
4 | class NullField extends Component {
5 | componentDidMount() {
6 | if (this.props.formData === undefined) {
7 | this.props.onChange(null);
8 | }
9 | }
10 |
11 | render() {
12 | return null;
13 | }
14 | }
15 |
16 | if (process.env.NODE_ENV !== "production") {
17 | NullField.propTypes = types.fieldProps;
18 | }
19 |
20 | export default NullField;
21 |
--------------------------------------------------------------------------------
/src/components/fields/NumberField.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 |
3 | import * as types from "../../types";
4 | import { asNumber } from "../../utils";
5 |
6 | // Matches a string that ends in a . character, optionally followed by a sequence of
7 | // digits followed by any number of 0 characters up until the end of the line.
8 | // Ensuring that there is at least one prefixed character is important so that
9 | // you don't incorrectly match against "0".
10 | const trailingCharMatcherWithPrefix = /\.([0-9]*0)*$/;
11 |
12 | // This is used for trimming the trailing 0 and . characters without affecting
13 | // the rest of the string. Its possible to use one RegEx with groups for this
14 | // functionality, but it is fairly complex compared to simply defining two
15 | // different matchers.
16 | const trailingCharMatcher = /[0.]0*$/;
17 |
18 | /**
19 | * The NumberField class has some special handling for dealing with trailing
20 | * decimal points and/or zeroes. This logic is designed to allow trailing values
21 | * to be visible in the input element, but not be represented in the
22 | * corresponding form data.
23 | *
24 | * The algorithm is as follows:
25 | *
26 | * 1. When the input value changes the value is cached in the component state
27 | *
28 | * 2. The value is then normalized, removing trailing decimal points and zeros,
29 | * then passed to the "onChange" callback
30 | *
31 | * 3. When the component is rendered, the formData value is checked against the
32 | * value cached in the state. If it matches the cached value, the cached
33 | * value is passed to the input instead of the formData value
34 | */
35 | class NumberField extends React.Component {
36 | constructor(props) {
37 | super(props);
38 |
39 | this.state = {
40 | lastValue: props.value,
41 | };
42 | }
43 |
44 | handleChange = value => {
45 | // Cache the original value in component state
46 | this.setState({ lastValue: value });
47 |
48 | // Normalize decimals that don't start with a zero character in advance so
49 | // that the rest of the normalization logic is simpler
50 | if (`${value}`.charAt(0) === ".") {
51 | value = `0${value}`;
52 | }
53 |
54 | // Check that the value is a string (this can happen if the widget used is a
55 | // , due to an enum declaration etc) then, if the value ends in a
56 | // trailing decimal point or multiple zeroes, strip the trailing values
57 | let processed =
58 | typeof value === "string" && value.match(trailingCharMatcherWithPrefix)
59 | ? asNumber(value.replace(trailingCharMatcher, ""))
60 | : asNumber(value);
61 |
62 | this.props.onChange(processed);
63 | };
64 |
65 | render() {
66 | const { StringField } = this.props.registry.fields;
67 | const { formData, ...props } = this.props;
68 | const { lastValue } = this.state;
69 |
70 | let value = formData;
71 |
72 | if (typeof lastValue === "string" && value) {
73 | // Construct a regular expression that checks for a string that consists
74 | // of the formData value suffixed with zero or one '.' characters and zero
75 | // or more '0' characters
76 | const re = new RegExp(`${value}`.replace(".", "\\.") + "\\.?0*$");
77 |
78 | // If the cached "lastValue" is a match, use that instead of the formData
79 | // value to prevent the input value from changing in the UI
80 | if (lastValue.match(re)) {
81 | value = lastValue;
82 | }
83 | }
84 |
85 | return (
86 |
87 | );
88 | }
89 | }
90 |
91 | if (process.env.NODE_ENV !== "production") {
92 | NumberField.propTypes = types.fieldProps;
93 | }
94 |
95 | NumberField.defaultProps = {
96 | uiSchema: {},
97 | };
98 |
99 | export default NumberField;
100 |
--------------------------------------------------------------------------------
/src/components/fields/ObjectField.js:
--------------------------------------------------------------------------------
1 | import AddButton from "../AddButton";
2 | import React, { Component } from "react";
3 | import * as types from "../../types";
4 |
5 | import {
6 | orderProperties,
7 | retrieveSchema,
8 | getDefaultRegistry,
9 | getUiOptions,
10 | ADDITIONAL_PROPERTY_FLAG,
11 | } from "../../utils";
12 |
13 | function DefaultObjectFieldTemplate(props) {
14 | const canExpand = function canExpand() {
15 | const { formData, schema, uiSchema } = props;
16 | if (!schema.additionalProperties) {
17 | return false;
18 | }
19 | const { expandable } = getUiOptions(uiSchema);
20 | if (expandable === false) {
21 | return expandable;
22 | }
23 | // if ui:options.expandable was not explicitly set to false, we can add
24 | // another property if we have not exceeded maxProperties yet
25 | if (schema.maxProperties !== undefined) {
26 | return Object.keys(formData).length < schema.maxProperties;
27 | }
28 | return true;
29 | };
30 |
31 | const { TitleField, DescriptionField } = props;
32 | return (
33 |
34 | {(props.uiSchema["ui:title"] || props.title) && (
35 |
41 | )}
42 | {props.description && (
43 |
48 | )}
49 | {props.properties.map(prop => prop.content)}
50 | {canExpand() && (
51 |
56 | )}
57 |
58 | );
59 | }
60 |
61 | class ObjectField extends Component {
62 | static defaultProps = {
63 | uiSchema: {},
64 | formData: {},
65 | errorSchema: {},
66 | idSchema: {},
67 | required: false,
68 | disabled: false,
69 | readonly: false,
70 | };
71 |
72 | state = {
73 | additionalProperties: {},
74 | };
75 |
76 | isRequired(name) {
77 | const schema = this.props.schema;
78 | return (
79 | Array.isArray(schema.required) && schema.required.indexOf(name) !== -1
80 | );
81 | }
82 |
83 | onPropertyChange = (name, addedByAdditionalProperties = false) => {
84 | return (value, errorSchema) => {
85 | if (!value && addedByAdditionalProperties) {
86 | // Don't set value = undefined for fields added by
87 | // additionalProperties. Doing so removes them from the
88 | // formData, which causes them to completely disappear
89 | // (including the input field for the property name). Unlike
90 | // fields which are "mandated" by the schema, these fields can
91 | // be set to undefined by clicking a "delete field" button, so
92 | // set empty values to the empty string.
93 | value = "";
94 | }
95 | const newFormData = { ...this.props.formData, [name]: value };
96 | this.props.onChange(
97 | newFormData,
98 | errorSchema &&
99 | this.props.errorSchema && {
100 | ...this.props.errorSchema,
101 | [name]: errorSchema,
102 | }
103 | );
104 | };
105 | };
106 |
107 | onDropPropertyClick = key => {
108 | return event => {
109 | event.preventDefault();
110 | const { onChange, formData } = this.props;
111 | const copiedFormData = { ...formData };
112 | delete copiedFormData[key];
113 | onChange(copiedFormData);
114 | };
115 | };
116 |
117 | getAvailableKey = (preferredKey, formData) => {
118 | var index = 0;
119 | var newKey = preferredKey;
120 | while (formData.hasOwnProperty(newKey)) {
121 | newKey = `${preferredKey}-${++index}`;
122 | }
123 | return newKey;
124 | };
125 |
126 | onKeyChange = oldValue => {
127 | return (value, errorSchema) => {
128 | if (oldValue === value) {
129 | return;
130 | }
131 | value = this.getAvailableKey(value, this.props.formData);
132 | const newFormData = { ...this.props.formData };
133 | const newKeys = { [oldValue]: value };
134 | const keyValues = Object.keys(newFormData).map(key => {
135 | const newKey = newKeys[key] || key;
136 | return { [newKey]: newFormData[key] };
137 | });
138 | const renamedObj = Object.assign({}, ...keyValues);
139 | this.props.onChange(
140 | renamedObj,
141 | errorSchema &&
142 | this.props.errorSchema && {
143 | ...this.props.errorSchema,
144 | [value]: errorSchema,
145 | }
146 | );
147 | };
148 | };
149 |
150 | getDefaultValue(type) {
151 | switch (type) {
152 | case "string":
153 | return "New Value";
154 | case "array":
155 | return [];
156 | case "boolean":
157 | return false;
158 | case "null":
159 | return null;
160 | case "number":
161 | return 0;
162 | case "object":
163 | return {};
164 | default:
165 | // We don't have a datatype for some reason (perhaps additionalProperties was true)
166 | return "New Value";
167 | }
168 | }
169 |
170 | handleAddClick = schema => () => {
171 | const type = schema.additionalProperties.type;
172 | const newFormData = { ...this.props.formData };
173 | newFormData[
174 | this.getAvailableKey("newKey", newFormData)
175 | ] = this.getDefaultValue(type);
176 | this.props.onChange(newFormData);
177 | };
178 |
179 | render() {
180 | const {
181 | uiSchema,
182 | formData,
183 | errorSchema,
184 | idSchema,
185 | name,
186 | required,
187 | disabled,
188 | readonly,
189 | idPrefix,
190 | onBlur,
191 | onFocus,
192 | registry = getDefaultRegistry(),
193 | } = this.props;
194 | const { definitions, fields, formContext } = registry;
195 | const { SchemaField, TitleField, DescriptionField } = fields;
196 | const schema = retrieveSchema(this.props.schema, definitions, formData);
197 | const title = schema.title === undefined ? name : schema.title;
198 | const description = uiSchema["ui:description"] || schema.description;
199 | let orderedProperties;
200 | try {
201 | const properties = Object.keys(schema.properties || {});
202 | orderedProperties = orderProperties(properties, uiSchema["ui:order"]);
203 | } catch (err) {
204 | return (
205 |
206 |
207 | Invalid {name || "root"} object field configuration:
208 | {err.message} .
209 |
210 |
{JSON.stringify(schema)}
211 |
212 | );
213 | }
214 |
215 | const Template =
216 | uiSchema["ui:ObjectFieldTemplate"] ||
217 | registry.ObjectFieldTemplate ||
218 | DefaultObjectFieldTemplate;
219 |
220 | const templateProps = {
221 | title: uiSchema["ui:title"] || title,
222 | description,
223 | TitleField,
224 | DescriptionField,
225 | properties: orderedProperties.map(name => {
226 | const addedByAdditionalProperties = schema.properties[
227 | name
228 | ].hasOwnProperty(ADDITIONAL_PROPERTY_FLAG);
229 | return {
230 | content: (
231 |
257 | ),
258 | name,
259 | readonly,
260 | disabled,
261 | required,
262 | };
263 | }),
264 | readonly,
265 | disabled,
266 | required,
267 | idSchema,
268 | uiSchema,
269 | schema,
270 | formData,
271 | formContext,
272 | };
273 | return ;
274 | }
275 | }
276 |
277 | if (process.env.NODE_ENV !== "production") {
278 | ObjectField.propTypes = types.fieldProps;
279 | }
280 |
281 | export default ObjectField;
282 |
--------------------------------------------------------------------------------
/src/components/fields/StringField.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import * as types from "../../types";
3 |
4 | import {
5 | getWidget,
6 | getUiOptions,
7 | isSelect,
8 | optionsList,
9 | getDefaultRegistry,
10 | hasWidget,
11 | } from "../../utils";
12 |
13 | function StringField(props) {
14 | const {
15 | schema,
16 | name,
17 | uiSchema,
18 | idSchema,
19 | formData,
20 | required,
21 | disabled,
22 | readonly,
23 | autofocus,
24 | onChange,
25 | onBlur,
26 | onFocus,
27 | registry = getDefaultRegistry(),
28 | rawErrors,
29 | } = props;
30 | const { title, format } = schema;
31 | const { widgets, formContext } = registry;
32 | const enumOptions = isSelect(schema) && optionsList(schema);
33 | let defaultWidget = enumOptions ? "select" : "text";
34 | if (format && hasWidget(schema, format, widgets)) {
35 | defaultWidget = format;
36 | }
37 | const { widget = defaultWidget, placeholder = "", ...options } = getUiOptions(
38 | uiSchema
39 | );
40 | const Widget = getWidget(schema, widget, widgets);
41 | return (
42 |
60 | );
61 | }
62 |
63 | if (process.env.NODE_ENV !== "production") {
64 | StringField.propTypes = types.fieldProps;
65 | }
66 |
67 | StringField.defaultProps = {
68 | uiSchema: {},
69 | disabled: false,
70 | readonly: false,
71 | autofocus: false,
72 | };
73 |
74 | export default StringField;
75 |
--------------------------------------------------------------------------------
/src/components/fields/TitleField.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import PropTypes from "prop-types";
3 |
4 | const REQUIRED_FIELD_SYMBOL = "*";
5 |
6 | function TitleField(props) {
7 | const { id, title, required } = props;
8 | return (
9 |
10 | {title}
11 | {required && {REQUIRED_FIELD_SYMBOL} }
12 |
13 | );
14 | }
15 |
16 | if (process.env.NODE_ENV !== "production") {
17 | TitleField.propTypes = {
18 | id: PropTypes.string,
19 | title: PropTypes.string,
20 | required: PropTypes.bool,
21 | };
22 | }
23 |
24 | export default TitleField;
25 |
--------------------------------------------------------------------------------
/src/components/fields/UnsupportedField.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import PropTypes from "prop-types";
3 |
4 | function UnsupportedField({ schema, idSchema, reason }) {
5 | return (
6 |
7 |
8 | Unsupported field schema
9 | {idSchema && idSchema.$id && (
10 |
11 | {" for"} field {idSchema.$id}
12 |
13 | )}
14 | {reason && : {reason} }.
15 |
16 | {schema &&
{JSON.stringify(schema, null, 2)} }
17 |
18 | );
19 | }
20 |
21 | if (process.env.NODE_ENV !== "production") {
22 | UnsupportedField.propTypes = {
23 | schema: PropTypes.object.isRequired,
24 | idSchema: PropTypes.object,
25 | reason: PropTypes.string,
26 | };
27 | }
28 |
29 | export default UnsupportedField;
30 |
--------------------------------------------------------------------------------
/src/components/fields/index.js:
--------------------------------------------------------------------------------
1 | import ArrayField from "./ArrayField";
2 | import BooleanField from "./BooleanField";
3 | import DescriptionField from "./DescriptionField";
4 | import MultiSchemaField from "./MultiSchemaField";
5 | import NumberField from "./NumberField";
6 | import ObjectField from "./ObjectField";
7 | import SchemaField from "./SchemaField";
8 | import StringField from "./StringField";
9 | import TitleField from "./TitleField";
10 | import NullField from "./NullField";
11 | import UnsupportedField from "./UnsupportedField";
12 |
13 | export default {
14 | AnyOfField: MultiSchemaField,
15 | ArrayField,
16 | BooleanField,
17 | DescriptionField,
18 | NumberField,
19 | ObjectField,
20 | OneOfField: MultiSchemaField,
21 | SchemaField,
22 | StringField,
23 | TitleField,
24 | NullField,
25 | UnsupportedField,
26 | };
27 |
--------------------------------------------------------------------------------
/src/components/widgets/AltDateTimeWidget.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import PropTypes from "prop-types";
3 | import AltDateWidget from "./AltDateWidget";
4 |
5 | function AltDateTimeWidget(props) {
6 | const { AltDateWidget } = props.registry.widgets;
7 | return ;
8 | }
9 |
10 | if (process.env.NODE_ENV !== "production") {
11 | AltDateTimeWidget.propTypes = {
12 | schema: PropTypes.object.isRequired,
13 | id: PropTypes.string.isRequired,
14 | value: PropTypes.string,
15 | required: PropTypes.bool,
16 | onChange: PropTypes.func,
17 | options: PropTypes.object,
18 | };
19 | }
20 |
21 | AltDateTimeWidget.defaultProps = {
22 | ...AltDateWidget.defaultProps,
23 | time: true,
24 | };
25 |
26 | export default AltDateTimeWidget;
27 |
--------------------------------------------------------------------------------
/src/components/widgets/AltDateWidget.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from "react";
2 | import PropTypes from "prop-types";
3 |
4 | import { shouldRender, parseDateString, toDateString, pad } from "../../utils";
5 |
6 | function rangeOptions(start, stop) {
7 | let options = [];
8 | for (let i = start; i <= stop; i++) {
9 | options.push({ value: i, label: pad(i, 2) });
10 | }
11 | return options;
12 | }
13 |
14 | function readyForChange(state) {
15 | return Object.keys(state).every(key => state[key] !== -1);
16 | }
17 |
18 | function DateElement(props) {
19 | const {
20 | type,
21 | range,
22 | value,
23 | select,
24 | rootId,
25 | disabled,
26 | readonly,
27 | autofocus,
28 | registry,
29 | onBlur,
30 | } = props;
31 | const id = rootId + "_" + type;
32 | const { SelectWidget } = registry.widgets;
33 | return (
34 | select(type, value)}
45 | onBlur={onBlur}
46 | />
47 | );
48 | }
49 |
50 | class AltDateWidget extends Component {
51 | static defaultProps = {
52 | time: false,
53 | disabled: false,
54 | readonly: false,
55 | autofocus: false,
56 | options: {
57 | yearsRange: [1900, new Date().getFullYear() + 2],
58 | },
59 | };
60 |
61 | constructor(props) {
62 | super(props);
63 | this.state = parseDateString(props.value, props.time);
64 | }
65 |
66 | componentWillReceiveProps(nextProps) {
67 | this.setState(parseDateString(nextProps.value, nextProps.time));
68 | }
69 |
70 | shouldComponentUpdate(nextProps, nextState) {
71 | return shouldRender(this, nextProps, nextState);
72 | }
73 |
74 | onChange = (property, value) => {
75 | this.setState(
76 | { [property]: typeof value === "undefined" ? -1 : value },
77 | () => {
78 | // Only propagate to parent state if we have a complete date{time}
79 | if (readyForChange(this.state)) {
80 | this.props.onChange(toDateString(this.state, this.props.time));
81 | }
82 | }
83 | );
84 | };
85 |
86 | setNow = event => {
87 | event.preventDefault();
88 | const { time, disabled, readonly, onChange } = this.props;
89 | if (disabled || readonly) {
90 | return;
91 | }
92 | const nowDateObj = parseDateString(new Date().toJSON(), time);
93 | this.setState(nowDateObj, () => onChange(toDateString(this.state, time)));
94 | };
95 |
96 | clear = event => {
97 | event.preventDefault();
98 | const { time, disabled, readonly, onChange } = this.props;
99 | if (disabled || readonly) {
100 | return;
101 | }
102 | this.setState(parseDateString("", time), () => onChange(undefined));
103 | };
104 |
105 | get dateElementProps() {
106 | const { time, options } = this.props;
107 | const { year, month, day, hour, minute, second } = this.state;
108 | const data = [
109 | {
110 | type: "year",
111 | range: options.yearsRange,
112 | value: year,
113 | },
114 | { type: "month", range: [1, 12], value: month },
115 | { type: "day", range: [1, 31], value: day },
116 | ];
117 | if (time) {
118 | data.push(
119 | { type: "hour", range: [0, 23], value: hour },
120 | { type: "minute", range: [0, 59], value: minute },
121 | { type: "second", range: [0, 59], value: second }
122 | );
123 | }
124 | return data;
125 | }
126 |
127 | render() {
128 | const {
129 | id,
130 | disabled,
131 | readonly,
132 | autofocus,
133 | registry,
134 | onBlur,
135 | options,
136 | } = this.props;
137 | return (
138 |
139 | {this.dateElementProps.map((elemProps, i) => (
140 |
141 |
151 |
152 | ))}
153 | {(options.hideNowButton !== "undefined"
154 | ? !options.hideNowButton
155 | : true) && (
156 |
157 |
158 | Now
159 |
160 |
161 | )}
162 | {(options.hideClearButton !== "undefined"
163 | ? !options.hideClearButton
164 | : true) && (
165 |
166 |
170 | Clear
171 |
172 |
173 | )}
174 |
175 | );
176 | }
177 | }
178 |
179 | if (process.env.NODE_ENV !== "production") {
180 | AltDateWidget.propTypes = {
181 | schema: PropTypes.object.isRequired,
182 | id: PropTypes.string.isRequired,
183 | value: PropTypes.string,
184 | required: PropTypes.bool,
185 | disabled: PropTypes.bool,
186 | readonly: PropTypes.bool,
187 | autofocus: PropTypes.bool,
188 | onChange: PropTypes.func,
189 | onBlur: PropTypes.func,
190 | time: PropTypes.bool,
191 | options: PropTypes.object,
192 | };
193 | }
194 |
195 | export default AltDateWidget;
196 |
--------------------------------------------------------------------------------
/src/components/widgets/BaseInput.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import PropTypes from "prop-types";
3 |
4 | function BaseInput(props) {
5 | // Note: since React 15.2.0 we can't forward unknown element attributes, so we
6 | // exclude the "options" and "schema" ones here.
7 | if (!props.id) {
8 | console.log("No id for", props);
9 | throw new Error(`no id for props ${JSON.stringify(props)}`);
10 | }
11 | const {
12 | value,
13 | readonly,
14 | disabled,
15 | autofocus,
16 | onBlur,
17 | onFocus,
18 | options,
19 | schema,
20 | formContext,
21 | registry,
22 | rawErrors,
23 | ...inputProps
24 | } = props;
25 |
26 | // If options.inputType is set use that as the input type
27 | if (options.inputType) {
28 | inputProps.type = options.inputType;
29 | } else if (!inputProps.type) {
30 | // If the schema is of type number or integer, set the input type to number
31 | if (schema.type === "number") {
32 | inputProps.type = "number";
33 | // Setting step to 'any' fixes a bug in Safari where decimals are not
34 | // allowed in number inputs
35 | inputProps.step = "any";
36 | } else if (schema.type === "integer") {
37 | inputProps.type = "number";
38 | // Since this is integer, you always want to step up or down in multiples
39 | // of 1
40 | inputProps.step = "1";
41 | } else {
42 | inputProps.type = "text";
43 | }
44 | }
45 |
46 | // If multipleOf is defined, use this as the step value. This mainly improves
47 | // the experience for keyboard users (who can use the up/down KB arrows).
48 | if (schema.multipleOf) {
49 | inputProps.step = schema.multipleOf;
50 | }
51 |
52 | if (typeof schema.minimum !== "undefined") {
53 | inputProps.min = schema.minimum;
54 | }
55 |
56 | if (typeof schema.maximum !== "undefined") {
57 | inputProps.max = schema.maximum;
58 | }
59 |
60 | const _onChange = ({ target: { value } }) => {
61 | return props.onChange(value === "" ? options.emptyValue : value);
62 | };
63 |
64 | return (
65 | onBlur(inputProps.id, event.target.value))}
74 | onFocus={onFocus && (event => onFocus(inputProps.id, event.target.value))}
75 | />
76 | );
77 | }
78 |
79 | BaseInput.defaultProps = {
80 | required: false,
81 | disabled: false,
82 | readonly: false,
83 | autofocus: false,
84 | };
85 |
86 | if (process.env.NODE_ENV !== "production") {
87 | BaseInput.propTypes = {
88 | id: PropTypes.string.isRequired,
89 | placeholder: PropTypes.string,
90 | value: PropTypes.any,
91 | required: PropTypes.bool,
92 | disabled: PropTypes.bool,
93 | readonly: PropTypes.bool,
94 | autofocus: PropTypes.bool,
95 | onChange: PropTypes.func,
96 | onBlur: PropTypes.func,
97 | onFocus: PropTypes.func,
98 | };
99 | }
100 |
101 | export default BaseInput;
102 |
--------------------------------------------------------------------------------
/src/components/widgets/CheckboxWidget.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import PropTypes from "prop-types";
3 | import DescriptionField from "../fields/DescriptionField.js";
4 |
5 | // Check to see if a schema specifies that a value must be true
6 | function schemaRequiresTrueValue(schema) {
7 | // Check if const is a truthy value
8 | if (schema.const) {
9 | return true;
10 | }
11 |
12 | // Check if an enum has a single value of true
13 | if (schema.enum && schema.enum.length === 1 && schema.enum[0] === true) {
14 | return true;
15 | }
16 |
17 | // If anyOf has a single value, evaluate the subschema
18 | if (schema.anyOf && schema.anyOf.length === 1) {
19 | return schemaRequiresTrueValue(schema.anyOf[0]);
20 | }
21 |
22 | // If oneOf has a single value, evaluate the subschema
23 | if (schema.oneOf && schema.oneOf.length === 1) {
24 | return schemaRequiresTrueValue(schema.oneOf[0]);
25 | }
26 |
27 | // Evaluate each subschema in allOf, to see if one of them requires a true
28 | // value
29 | if (schema.allOf) {
30 | return schema.allOf.some(schemaRequiresTrueValue);
31 | }
32 | }
33 |
34 | function CheckboxWidget(props) {
35 | const {
36 | schema,
37 | id,
38 | value,
39 | disabled,
40 | readonly,
41 | label,
42 | autofocus,
43 | onBlur,
44 | onFocus,
45 | onChange,
46 | } = props;
47 |
48 | // Because an unchecked checkbox will cause html5 validation to fail, only add
49 | // the "required" attribute if the field value must be "true", due to the
50 | // "const" or "enum" keywords
51 | const required = schemaRequiresTrueValue(schema);
52 |
53 | return (
54 |
55 | {schema.description && (
56 |
57 | )}
58 |
59 |
60 | onChange(event.target.checked)}
69 | onBlur={onBlur && (event => onBlur(id, event.target.checked))}
70 | onFocus={onFocus && (event => onFocus(id, event.target.checked))}
71 | />
72 |
73 | {label}
74 |
75 |
76 |
77 | );
78 | }
79 |
80 | CheckboxWidget.defaultProps = {
81 | autofocus: false,
82 | };
83 |
84 | if (process.env.NODE_ENV !== "production") {
85 | CheckboxWidget.propTypes = {
86 | schema: PropTypes.object.isRequired,
87 | id: PropTypes.string.isRequired,
88 | value: PropTypes.bool,
89 | required: PropTypes.bool,
90 | disabled: PropTypes.bool,
91 | readonly: PropTypes.bool,
92 | autofocus: PropTypes.bool,
93 | onChange: PropTypes.func,
94 | };
95 | }
96 |
97 | export default CheckboxWidget;
98 |
--------------------------------------------------------------------------------
/src/components/widgets/CheckboxesWidget.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import PropTypes from "prop-types";
3 |
4 | function selectValue(value, selected, all) {
5 | const at = all.indexOf(value);
6 | const updated = selected.slice(0, at).concat(value, selected.slice(at));
7 | // As inserting values at predefined index positions doesn't work with empty
8 | // arrays, we need to reorder the updated selection to match the initial order
9 | return updated.sort((a, b) => all.indexOf(a) > all.indexOf(b));
10 | }
11 |
12 | function deselectValue(value, selected) {
13 | return selected.filter(v => v !== value);
14 | }
15 |
16 | function CheckboxesWidget(props) {
17 | const { id, disabled, options, value, autofocus, readonly, onChange } = props;
18 | const { enumOptions, enumDisabled, inline } = options;
19 | return (
20 |
21 | {enumOptions.map((option, index) => {
22 | const checkboxId = `${id}_${index}`;
23 | const checked = value.indexOf(option.value) !== -1;
24 | const itemDisabled =
25 | enumDisabled && enumDisabled.indexOf(option.value) != -1;
26 | const disabledCls =
27 | disabled || itemDisabled || readonly ? "disabled" : "";
28 | const checkbox = (
29 |
{
37 | const all = enumOptions.map(({ value }) => value);
38 | if (event.target.checked) {
39 | onChange(selectValue(option.value, value, all));
40 | } else {
41 | onChange(deselectValue(option.value, value));
42 | }
43 | }}
44 | />
45 | );
46 | return inline ? (
47 |
48 | {checkbox}
49 |
50 | {option.label}
51 |
52 |
53 | ) : (
54 |
55 | {checkbox}
56 |
57 | {option.label}
58 |
59 |
60 | );
61 | })}
62 |
63 | );
64 | }
65 |
66 | CheckboxesWidget.defaultProps = {
67 | autofocus: false,
68 | options: {
69 | inline: false,
70 | },
71 | };
72 |
73 | if (process.env.NODE_ENV !== "production") {
74 | CheckboxesWidget.propTypes = {
75 | schema: PropTypes.object.isRequired,
76 | id: PropTypes.string.isRequired,
77 | options: PropTypes.shape({
78 | enumOptions: PropTypes.array,
79 | inline: PropTypes.bool,
80 | }).isRequired,
81 | value: PropTypes.any,
82 | required: PropTypes.bool,
83 | readonly: PropTypes.bool,
84 | disabled: PropTypes.bool,
85 | multiple: PropTypes.bool,
86 | autofocus: PropTypes.bool,
87 | onChange: PropTypes.func,
88 | };
89 | }
90 |
91 | export default CheckboxesWidget;
92 |
--------------------------------------------------------------------------------
/src/components/widgets/ColorWidget.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import PropTypes from "prop-types";
3 |
4 | function ColorWidget(props) {
5 | const {
6 | disabled,
7 | readonly,
8 | registry: {
9 | widgets: { BaseInput },
10 | },
11 | } = props;
12 | return ;
13 | }
14 |
15 | if (process.env.NODE_ENV !== "production") {
16 | ColorWidget.propTypes = {
17 | schema: PropTypes.object.isRequired,
18 | id: PropTypes.string.isRequired,
19 | value: PropTypes.string,
20 | required: PropTypes.bool,
21 | disabled: PropTypes.bool,
22 | readonly: PropTypes.bool,
23 | autofocus: PropTypes.bool,
24 | onChange: PropTypes.func,
25 | };
26 | }
27 |
28 | export default ColorWidget;
29 |
--------------------------------------------------------------------------------
/src/components/widgets/DateTimeWidget.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import PropTypes from "prop-types";
3 | import { pad } from "../../utils";
4 |
5 | export function utcToLocal(jsonDate) {
6 | if (!jsonDate) {
7 | return "";
8 | }
9 |
10 | // required format of `"yyyy-MM-ddThh:mm" followed by optional ":ss" or ":ss.SSS"
11 | // https://html.spec.whatwg.org/multipage/input.html#local-date-and-time-state-(type%3Ddatetime-local)
12 | // > should be a _valid local date and time string_ (not GMT)
13 |
14 | // Note - date constructor passed local ISO-8601 does not correctly
15 | // change time to UTC in node pre-8
16 | const date = new Date(jsonDate);
17 |
18 | const yyyy = pad(date.getFullYear(), 4);
19 | const MM = pad(date.getMonth() + 1, 2);
20 | const dd = pad(date.getDate(), 2);
21 | const hh = pad(date.getHours(), 2);
22 | const mm = pad(date.getMinutes(), 2);
23 | const ss = pad(date.getSeconds(), 2);
24 | const SSS = pad(date.getMilliseconds(), 3);
25 |
26 | return `${yyyy}-${MM}-${dd}T${hh}:${mm}:${ss}.${SSS}`;
27 | }
28 |
29 | export function localToUTC(dateString) {
30 | if (dateString) {
31 | return new Date(dateString).toJSON();
32 | }
33 | }
34 |
35 | function DateTimeWidget(props) {
36 | const {
37 | value,
38 | onChange,
39 | registry: {
40 | widgets: { BaseInput },
41 | },
42 | } = props;
43 | return (
44 | onChange(localToUTC(value))}
49 | />
50 | );
51 | }
52 |
53 | if (process.env.NODE_ENV !== "production") {
54 | DateTimeWidget.propTypes = {
55 | value: PropTypes.string,
56 | };
57 | }
58 |
59 | export default DateTimeWidget;
60 |
--------------------------------------------------------------------------------
/src/components/widgets/DateWidget.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import PropTypes from "prop-types";
3 |
4 | function DateWidget(props) {
5 | const {
6 | onChange,
7 | registry: {
8 | widgets: { BaseInput },
9 | },
10 | } = props;
11 | return (
12 | onChange(value || undefined)}
16 | />
17 | );
18 | }
19 |
20 | if (process.env.NODE_ENV !== "production") {
21 | DateWidget.propTypes = {
22 | value: PropTypes.string,
23 | };
24 | }
25 |
26 | export default DateWidget;
27 |
--------------------------------------------------------------------------------
/src/components/widgets/EmailWidget.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import PropTypes from "prop-types";
3 |
4 | function EmailWidget(props) {
5 | const { BaseInput } = props.registry.widgets;
6 | return ;
7 | }
8 |
9 | if (process.env.NODE_ENV !== "production") {
10 | EmailWidget.propTypes = {
11 | value: PropTypes.string,
12 | };
13 | }
14 |
15 | export default EmailWidget;
16 |
--------------------------------------------------------------------------------
/src/components/widgets/FileWidget.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from "react";
2 | import PropTypes from "prop-types";
3 |
4 | import { dataURItoBlob, shouldRender } from "../../utils";
5 |
6 | function addNameToDataURL(dataURL, name) {
7 | return dataURL.replace(";base64", `;name=${encodeURIComponent(name)};base64`);
8 | }
9 |
10 | function processFile(file) {
11 | const { name, size, type } = file;
12 | return new Promise((resolve, reject) => {
13 | const reader = new window.FileReader();
14 | reader.onerror = reject;
15 | reader.onload = event => {
16 | resolve({
17 | dataURL: addNameToDataURL(event.target.result, name),
18 | name,
19 | size,
20 | type,
21 | });
22 | };
23 | reader.readAsDataURL(file);
24 | });
25 | }
26 |
27 | function processFiles(files) {
28 | return Promise.all([].map.call(files, processFile));
29 | }
30 |
31 | function FilesInfo(props) {
32 | const { filesInfo } = props;
33 | if (filesInfo.length === 0) {
34 | return null;
35 | }
36 | return (
37 |
38 | {filesInfo.map((fileInfo, key) => {
39 | const { name, size, type } = fileInfo;
40 | return (
41 |
42 | {name} ({type}, {size} bytes)
43 |
44 | );
45 | })}
46 |
47 | );
48 | }
49 |
50 | function extractFileInfo(dataURLs) {
51 | return dataURLs
52 | .filter(dataURL => typeof dataURL !== "undefined")
53 | .map(dataURL => {
54 | const { blob, name } = dataURItoBlob(dataURL);
55 | return {
56 | name: name,
57 | size: blob.size,
58 | type: blob.type,
59 | };
60 | });
61 | }
62 |
63 | class FileWidget extends Component {
64 | constructor(props) {
65 | super(props);
66 | const { value } = props;
67 | const values = Array.isArray(value) ? value : [value];
68 | this.state = { values, filesInfo: extractFileInfo(values) };
69 | }
70 |
71 | shouldComponentUpdate(nextProps, nextState) {
72 | return shouldRender(this, nextProps, nextState);
73 | }
74 |
75 | onChange = event => {
76 | const { multiple, onChange } = this.props;
77 | processFiles(event.target.files).then(filesInfo => {
78 | const state = {
79 | values: filesInfo.map(fileInfo => fileInfo.dataURL),
80 | filesInfo,
81 | };
82 | this.setState(state, () => {
83 | if (multiple) {
84 | onChange(state.values);
85 | } else {
86 | onChange(state.values[0]);
87 | }
88 | });
89 | });
90 | };
91 |
92 | render() {
93 | const { multiple, id, readonly, disabled, autofocus } = this.props;
94 | const { filesInfo } = this.state;
95 | return (
96 |
97 |
98 | (this.inputRef = ref)}
100 | id={id}
101 | type="file"
102 | disabled={readonly || disabled}
103 | onChange={this.onChange}
104 | defaultValue=""
105 | autoFocus={autofocus}
106 | multiple={multiple}
107 | />
108 |
109 |
110 |
111 | );
112 | }
113 | }
114 |
115 | FileWidget.defaultProps = {
116 | autofocus: false,
117 | };
118 |
119 | if (process.env.NODE_ENV !== "production") {
120 | FileWidget.propTypes = {
121 | multiple: PropTypes.bool,
122 | value: PropTypes.oneOfType([
123 | PropTypes.string,
124 | PropTypes.arrayOf(PropTypes.string),
125 | ]),
126 | autofocus: PropTypes.bool,
127 | };
128 | }
129 |
130 | export default FileWidget;
131 |
--------------------------------------------------------------------------------
/src/components/widgets/HiddenWidget.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import PropTypes from "prop-types";
3 |
4 | function HiddenWidget({ id, value }) {
5 | return (
6 |
11 | );
12 | }
13 |
14 | if (process.env.NODE_ENV !== "production") {
15 | HiddenWidget.propTypes = {
16 | id: PropTypes.string.isRequired,
17 | value: PropTypes.oneOfType([
18 | PropTypes.string,
19 | PropTypes.number,
20 | PropTypes.bool,
21 | ]),
22 | };
23 | }
24 |
25 | export default HiddenWidget;
26 |
--------------------------------------------------------------------------------
/src/components/widgets/PasswordWidget.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import PropTypes from "prop-types";
3 |
4 | function PasswordWidget(props) {
5 | const { BaseInput } = props.registry.widgets;
6 | return ;
7 | }
8 |
9 | if (process.env.NODE_ENV !== "production") {
10 | PasswordWidget.propTypes = {
11 | value: PropTypes.string,
12 | };
13 | }
14 |
15 | export default PasswordWidget;
16 |
--------------------------------------------------------------------------------
/src/components/widgets/RadioWidget.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import PropTypes from "prop-types";
3 |
4 | function RadioWidget(props) {
5 | const {
6 | options,
7 | value,
8 | required,
9 | disabled,
10 | readonly,
11 | autofocus,
12 | onBlur,
13 | onFocus,
14 | onChange,
15 | id,
16 | } = props;
17 | // Generating a unique field name to identify this set of radio buttons
18 | const name = Math.random().toString();
19 | const { enumOptions, enumDisabled, inline } = options;
20 | // checked={checked} has been moved above name={name}, As mentioned in #349;
21 | // this is a temporary fix for radio button rendering bug in React, facebook/react#7630.
22 | return (
23 |
24 | {enumOptions.map((option, i) => {
25 | const checked = option.value === value;
26 | const itemDisabled =
27 | enumDisabled && enumDisabled.indexOf(option.value) != -1;
28 | const disabledCls =
29 | disabled || itemDisabled || readonly ? "disabled" : "";
30 | const radio = (
31 |
onChange(option.value)}
41 | onBlur={onBlur && (event => onBlur(id, event.target.value))}
42 | onFocus={onFocus && (event => onFocus(id, event.target.value))}
43 | />
44 | );
45 |
46 | return inline ? (
47 |
48 | {radio}
49 |
50 | {option.label}
51 |
52 |
53 | ) : (
54 |
55 | {radio}
56 |
57 | {option.label}
58 |
59 |
60 | );
61 | })}
62 |
63 | );
64 | }
65 |
66 | RadioWidget.defaultProps = {
67 | autofocus: false,
68 | };
69 |
70 | if (process.env.NODE_ENV !== "production") {
71 | RadioWidget.propTypes = {
72 | schema: PropTypes.object.isRequired,
73 | id: PropTypes.string.isRequired,
74 | options: PropTypes.shape({
75 | enumOptions: PropTypes.array,
76 | inline: PropTypes.bool,
77 | }).isRequired,
78 | value: PropTypes.any,
79 | required: PropTypes.bool,
80 | disabled: PropTypes.bool,
81 | readonly: PropTypes.bool,
82 | autofocus: PropTypes.bool,
83 | onChange: PropTypes.func,
84 | };
85 | }
86 | export default RadioWidget;
87 |
--------------------------------------------------------------------------------
/src/components/widgets/RangeWidget.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import PropTypes from "prop-types";
3 |
4 | import { rangeSpec } from "../../utils";
5 |
6 | function RangeWidget(props) {
7 | const {
8 | schema,
9 | value,
10 | registry: {
11 | widgets: { BaseInput },
12 | },
13 | } = props;
14 | return (
15 |
16 |
17 | {value}
18 |
19 | );
20 | }
21 |
22 | if (process.env.NODE_ENV !== "production") {
23 | RangeWidget.propTypes = {
24 | value: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
25 | };
26 | }
27 |
28 | export default RangeWidget;
29 |
--------------------------------------------------------------------------------
/src/components/widgets/SelectWidget.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import PropTypes from "prop-types";
3 |
4 | import { asNumber, guessType } from "../../utils";
5 |
6 | const nums = new Set(["number", "integer"]);
7 |
8 | /**
9 | * This is a silly limitation in the DOM where option change event values are
10 | * always retrieved as strings.
11 | */
12 | function processValue(schema, value) {
13 | // "enum" is a reserved word, so only "type" and "items" can be destructured
14 | const { type, items } = schema;
15 | if (value === "") {
16 | return undefined;
17 | } else if (type === "array" && items && nums.has(items.type)) {
18 | return value.map(asNumber);
19 | } else if (type === "boolean") {
20 | return value === "true";
21 | } else if (type === "number") {
22 | return asNumber(value);
23 | }
24 |
25 | // If type is undefined, but an enum is present, try and infer the type from
26 | // the enum values
27 | if (schema.enum) {
28 | if (schema.enum.every(x => guessType(x) === "number")) {
29 | return asNumber(value);
30 | } else if (schema.enum.every(x => guessType(x) === "boolean")) {
31 | return value === "true";
32 | }
33 | }
34 |
35 | return value;
36 | }
37 |
38 | function getValue(event, multiple) {
39 | if (multiple) {
40 | return [].slice
41 | .call(event.target.options)
42 | .filter(o => o.selected)
43 | .map(o => o.value);
44 | } else {
45 | return event.target.value;
46 | }
47 | }
48 |
49 | function SelectWidget(props) {
50 | const {
51 | schema,
52 | id,
53 | options,
54 | value,
55 | required,
56 | disabled,
57 | readonly,
58 | multiple,
59 | autofocus,
60 | onChange,
61 | onBlur,
62 | onFocus,
63 | placeholder,
64 | } = props;
65 | const { enumOptions, enumDisabled } = options;
66 | const emptyValue = multiple ? [] : "";
67 | return (
68 | {
79 | const newValue = getValue(event, multiple);
80 | onBlur(id, processValue(schema, newValue));
81 | })
82 | }
83 | onFocus={
84 | onFocus &&
85 | (event => {
86 | const newValue = getValue(event, multiple);
87 | onFocus(id, processValue(schema, newValue));
88 | })
89 | }
90 | onChange={event => {
91 | const newValue = getValue(event, multiple);
92 | onChange(processValue(schema, newValue));
93 | }}>
94 | {!multiple && schema.default === undefined && (
95 | {placeholder}
96 | )}
97 | {enumOptions.map(({ value, label }, i) => {
98 | const disabled = enumDisabled && enumDisabled.indexOf(value) != -1;
99 | return (
100 |
101 | {label}
102 |
103 | );
104 | })}
105 |
106 | );
107 | }
108 |
109 | SelectWidget.defaultProps = {
110 | autofocus: false,
111 | };
112 |
113 | if (process.env.NODE_ENV !== "production") {
114 | SelectWidget.propTypes = {
115 | schema: PropTypes.object.isRequired,
116 | id: PropTypes.string.isRequired,
117 | options: PropTypes.shape({
118 | enumOptions: PropTypes.array,
119 | }).isRequired,
120 | value: PropTypes.any,
121 | required: PropTypes.bool,
122 | disabled: PropTypes.bool,
123 | readonly: PropTypes.bool,
124 | multiple: PropTypes.bool,
125 | autofocus: PropTypes.bool,
126 | onChange: PropTypes.func,
127 | onBlur: PropTypes.func,
128 | onFocus: PropTypes.func,
129 | };
130 | }
131 |
132 | export default SelectWidget;
133 |
--------------------------------------------------------------------------------
/src/components/widgets/TextWidget.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import PropTypes from "prop-types";
3 |
4 | function TextWidget(props) {
5 | const { BaseInput } = props.registry.widgets;
6 | return ;
7 | }
8 |
9 | if (process.env.NODE_ENV !== "production") {
10 | TextWidget.propTypes = {
11 | value: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
12 | id: PropTypes.string,
13 | };
14 | }
15 |
16 | export default TextWidget;
17 |
--------------------------------------------------------------------------------
/src/components/widgets/TextareaWidget.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import PropTypes from "prop-types";
3 |
4 | function TextareaWidget(props) {
5 | const {
6 | id,
7 | options,
8 | placeholder,
9 | value,
10 | required,
11 | disabled,
12 | readonly,
13 | autofocus,
14 | onChange,
15 | onBlur,
16 | onFocus,
17 | } = props;
18 | const _onChange = ({ target: { value } }) => {
19 | return onChange(value === "" ? options.emptyValue : value);
20 | };
21 | return (
22 |