├── .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 | 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 | 69 | ), document.getElementById("app")); 70 | ``` 71 | 72 | Format values can be anything AJV’s [`addFormat` method](https://github.com/epoberezkin/ajv#addformatstring-name-stringregexpfunctionobject-format---ajv) accepts. 73 | 74 | ### Custom schema validation 75 | 76 | To have your schemas validated against any other meta schema than draft-07 (the current version of [JSON Schema](http://json-schema.org/)), make sure your schema has a `$schema` attribute that enables the validator to use the correct meta schema. For example: 77 | 78 | ```json 79 | { 80 | "$schema": "http://json-schema.org/draft-04/schema#", 81 | ... 82 | } 83 | ``` 84 | 85 | Note that react-jsonschema-form only supports the latest version of JSON Schema, draft-07, by default. To support additional meta schemas pass them through the `additionalMetaSchemas` prop to your `Form` component: 86 | 87 | ```jsx 88 | const additionalMetaSchemas = require("ajv/lib/refs/json-schema-draft-04.json"); 89 | 90 | render(( 91 | 93 | ), document.getElementById("app")); 94 | ``` 95 | 96 | In this example `schema` passed as props to `Form` component can be validated against draft-07 (default) and by draft-04 (added), depending on the value of `$schema` attribute. 97 | 98 | `additionalMetaSchemas` also accepts more than one meta schema: 99 | 100 | ```jsx 101 | render(( 102 | 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 | 18 | )} 19 | {element.hasMoveUp && ( 20 | 27 | )} 28 | 31 |
32 |
33 | ))} 34 | 35 | {props.canAdd && ( 36 |
37 |

38 | 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 | 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 |
7 |

8 | 16 |

17 |
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 | 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 | //