From ca5ce7494f57315941d1aa5f34e540cd09780bc0 Mon Sep 17 00:00:00 2001 From: Ashwin Ramaswami Date: Sat, 6 Jan 2018 10:10:12 -0500 Subject: [PATCH 01/90] fix typo in css code --- README.md | 1 - 1 file changed, 1 deletion(-) diff --git a/README.md b/README.md index ecf859c256..a97308074a 100644 --- a/README.md +++ b/README.md @@ -1228,7 +1228,6 @@ i.glyphicon { display: none; } .array-item-move-up::after { content: 'Move Up'; } .array-item-move-down::after { content: 'Move Down'; } .array-item-remove::after { content: 'Remove'; } -} ``` ### Custom SchemaField From c5c52f11a9ce2a0150fab2e9c84dbe6a1c9af30d Mon Sep 17 00:00:00 2001 From: Razeel Akbar Date: Sun, 6 Jan 2019 00:23:42 +0400 Subject: [PATCH 02/90] Docs updated: Firefox date, time support (#1115) Firefox date, time input types partial support starting from Firefox 57 --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index ca4428cf0b..b597906077 100644 --- a/README.md +++ b/README.md @@ -368,6 +368,8 @@ Please note that, even though they are standardized, `datetime-local` and `date` - `alt-datetime`: Six `select` elements are used to select the year, the month, the day, the hour, the minute and the second; - `alt-date`: Three `select` elements are used to select the year, month and the day. +> **Firefox 57 - 66**: Firefox partially supporting `date` and `time` input types, but not `datetime-local`, `month` or `week` + ![](http://i.imgur.com/VF5tY60.png) You can customize the list of years displayed in the `year` dropdown by providing a ``yearsRange`` property to ``ui:options`` in your uiSchema. Its also possible to remove the `Now` and `Clear` buttons with the `hideNowButton` and `hideClearButton` options. From 4b9c4758a132189b4b2e25e2523650eb442f7149 Mon Sep 17 00:00:00 2001 From: Lucian Buzzo Date: Mon, 7 Jan 2019 10:18:40 +0000 Subject: [PATCH 03/90] Fix failing unit tests related to time widget (#1119) It appears that due to the recent change from 2018 to 2019, there is one additional year in the select list, which caused the unit tests to fail. Signed-off-by: Lucian --- test/StringField_test.js | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/test/StringField_test.js b/test/StringField_test.js index 8bb9d2b079..4a9625ca76 100644 --- a/test/StringField_test.js +++ b/test/StringField_test.js @@ -773,7 +773,8 @@ describe("StringField", () => { ); expect(lengths).eql([ - 121 + 1, // from 1900 to 2020 + undefined + // from 1900 to current year + 2 (inclusive) + 1 undefined option + new Date().getFullYear() - 1900 + 3 + 1, 12 + 1, 31 + 1, 24 + 1, @@ -1019,7 +1020,8 @@ describe("StringField", () => { ); expect(lengths).eql([ - 121 + 1, // from 1900 to 2020 + undefined + // from 1900 to current year + 2 (inclusive) + 1 undefined option + new Date().getFullYear() - 1900 + 3 + 1, 12 + 1, 31 + 1, ]); From 65ca9de8b32f89d36d0e50f418dbc6c24d9d8e72 Mon Sep 17 00:00:00 2001 From: Ethan Glasser-Camp Date: Wed, 9 Jan 2019 22:28:34 -0500 Subject: [PATCH 04/90] Bump 1.0.7 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index e2a1f623d6..1db7ca591a 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "react-jsonschema-form", - "version": "1.0.6", + "version": "1.0.7", "description": "A simple React component capable of building HTML forms out of a JSON schema.", "scripts": { "build:readme": "toctoc README.md -w", From 73794911b1fff5d81e3c50b7190e37d99c5b4c0b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateusz=20Puczy=C5=84ski?= Date: Thu, 10 Jan 2019 16:15:05 +0100 Subject: [PATCH 05/90] Add delete button for additionalProperties key-value pair (#1123) * add del button functionality * delete unnecesary console.logs * fix tests * remove unnecesary console.log * Fix disappearing key-value pair and repair delete button * Add .vscode to gitignore * fix tests * Make test work for select/radio button * Add test cases * Remove one file from PR and one helper function * Add one classname for value input * remove accidentally added .vscode * linted codebase * remove inline styles, make use of bootstrap 3 styles and fix related tests * revert changes to .gitignore --- src/components/fields/ObjectField.js | 40 +++++++++++++++-- src/components/fields/SchemaField.js | 58 +++++++++++++++++-------- src/components/fields/StringField.js | 1 - test/ArrayFieldTemplate_test.js | 2 +- test/ArrayField_test.js | 3 +- test/ObjectField_test.js | 65 ++++++++++++++++++++++++++-- test/SchemaField_test.js | 2 +- 7 files changed, 142 insertions(+), 29 deletions(-) diff --git a/src/components/fields/ObjectField.js b/src/components/fields/ObjectField.js index 4b7edb41f3..982aa3aaff 100644 --- a/src/components/fields/ObjectField.js +++ b/src/components/fields/ObjectField.js @@ -7,6 +7,7 @@ import { retrieveSchema, getDefaultRegistry, getUiOptions, + ADDITIONAL_PROPERTY_FLAG, } from "../../utils"; function DefaultObjectFieldTemplate(props) { @@ -79,9 +80,24 @@ class ObjectField extends Component { ); } - onPropertyChange = name => { + onPropertyChange = (name, addedByAdditionalProperties = false) => { return (value, errorSchema) => { - const newFormData = { ...this.props.formData, [name]: value }; + let newFormData; + //section below sets zero value of input field to empty string + //instead of undefined, so that value input in additionalProperties + //doesn't disappear when emptied + if (!value && addedByAdditionalProperties) { + newFormData = { + ...this.props.formData, + [name]: "", + }; + } else { + newFormData = { + ...this.props.formData, + [name]: value, + }; + } + this.props.onChange( newFormData, errorSchema && @@ -93,6 +109,16 @@ class ObjectField extends Component { }; }; + onDropIndexClick = key => { + return event => { + event.preventDefault(); + const { onChange, formData } = this.props; + const copiedFormData = { ...formData }; + delete copiedFormData[key]; + onChange(copiedFormData); + }; + }; + getAvailableKey = (preferredKey, formData) => { var index = 0; var newKey = preferredKey; @@ -176,7 +202,6 @@ class ObjectField extends Component { const title = schema.title === undefined ? name : schema.title; const description = uiSchema["ui:description"] || schema.description; let orderedProperties; - try { const properties = Object.keys(schema.properties); orderedProperties = orderProperties(properties, uiSchema["ui:order"]); @@ -200,6 +225,9 @@ class ObjectField extends Component { TitleField, DescriptionField, properties: orderedProperties.map(name => { + const addedByAdditionalProperties = schema.properties[ + name + ].hasOwnProperty(ADDITIONAL_PROPERTY_FLAG); return { content: ( ), name, diff --git a/src/components/fields/SchemaField.js b/src/components/fields/SchemaField.js index fd7ff9a795..1d10983c05 100644 --- a/src/components/fields/SchemaField.js +++ b/src/components/fields/SchemaField.js @@ -1,4 +1,5 @@ import { ADDITIONAL_PROPERTY_FLAG } from "../../utils"; +import IconButton from "../IconButton"; import React from "react"; import PropTypes from "prop-types"; @@ -107,7 +108,6 @@ function ErrorList(props) { ); } - function DefaultTemplate(props) { const { id, @@ -121,35 +121,57 @@ function DefaultTemplate(props) { required, displayLabel, onKeyChange, + onDropIndexClick, } = props; if (hidden) { return children; } + const additional = props.schema.hasOwnProperty(ADDITIONAL_PROPERTY_FLAG); const keyLabel = `${label} Key`; return (
- {additional && ( -
-
); } - if (process.env.NODE_ENV !== "production") { DefaultTemplate.propTypes = { id: PropTypes.string, @@ -186,6 +208,7 @@ function SchemaFieldRender(props) { idPrefix, name, onKeyChange, + onDropIndexClick, required, registry = getDefaultRegistry(), } = props; @@ -285,6 +308,7 @@ function SchemaFieldRender(props) { label, hidden, onKeyChange, + onDropIndexClick, required, disabled, readonly, diff --git a/src/components/fields/StringField.js b/src/components/fields/StringField.js index 91fcf6911e..6ef3f3cbff 100644 --- a/src/components/fields/StringField.js +++ b/src/components/fields/StringField.js @@ -34,7 +34,6 @@ function StringField(props) { uiSchema ); const Widget = getWidget(schema, widget, widgets); - return ( { ArrayFieldTemplate, }); - expect(node.querySelectorAll(".field-array div")).to.have.length.of(3); + expect(node.querySelectorAll(".field-array div")).to.have.length.of(6); }); }); diff --git a/test/ArrayField_test.js b/test/ArrayField_test.js index 633c57347c..40e7f90b3a 100644 --- a/test/ArrayField_test.js +++ b/test/ArrayField_test.js @@ -24,7 +24,8 @@ describe("ArrayField", () => { const { node } = createFormComponent({ schema: { type: "array" } }); expect( - node.querySelector(".field-array > .unsupported-field").textContent + node.querySelector(".field-array > div > div > .unsupported-field") + .textContent ).to.contain("Missing items definition"); }); }); diff --git a/test/ObjectField_test.js b/test/ObjectField_test.js index a0554e4b5f..b546ba55bf 100644 --- a/test/ObjectField_test.js +++ b/test/ObjectField_test.js @@ -197,7 +197,7 @@ describe("ObjectField", () => { }, }); const labels = [].map.call( - node.querySelectorAll(".field > label"), + node.querySelectorAll(".field > div > div > label"), l => l.textContent ); @@ -212,7 +212,7 @@ describe("ObjectField", () => { }, }); const labels = [].map.call( - node.querySelectorAll(".field > label"), + node.querySelectorAll(".field > div > div> label"), l => l.textContent ); @@ -277,7 +277,7 @@ describe("ObjectField", () => { }, }); const labels = [].map.call( - node.querySelectorAll(".field > label"), + node.querySelectorAll(".field > div > div > label"), l => l.textContent ); @@ -310,7 +310,7 @@ describe("ObjectField", () => { }, }); const labels = [].map.call( - node.querySelectorAll(".field > label"), + node.querySelectorAll(".field > div > div > label"), l => l.textContent ); @@ -678,5 +678,62 @@ describe("ObjectField", () => { expect(node.querySelector(".object-property-expand button")).to.be.null; }); + + it("should not have delete button if expand button has not been clicked", () => { + const { node } = createFormComponent({ schema }); + + expect(node.querySelector(".form-group > .btn-danger")).eql(null); + }); + + it("should have delete button if expand button has been clicked", () => { + const { node } = createFormComponent({ + schema, + }); + + expect( + node.querySelector(".form-group > .row > .col-xs-2 .btn-danger") + ).eql(null); + + Simulate.click(node.querySelector(".object-property-expand button")); + + expect(node.querySelector(".form-group > .row > .col-xs-2 > .btn-danger")) + .to.not.be.null; + }); + + it("delete button should delete key-value pair", () => { + const { node } = createFormComponent({ + schema, + formData: { first: 1 }, + }); + expect(node.querySelector("#root_first-key").value).to.eql("first"); + Simulate.click( + node.querySelector(".form-group > .row > .col-xs-2 > .btn-danger") + ); + expect(node.querySelector("#root_first-key")).to.not.exist; + }); + + it("delete button should delete correct pair", () => { + const { node } = createFormComponent({ + schema, + formData: { first: 1, second: 2, third: 3 }, + }); + const selector = ".form-group > .row > .col-xs-2 > .btn-danger"; + expect(node.querySelectorAll(selector).length).to.eql(3); + Simulate.click(node.querySelectorAll(selector)[1]); + expect(node.querySelector("#root_second-key")).to.not.exist; + expect(node.querySelectorAll(selector).length).to.eql(2); + }); + + it("deleting content of value input should not delete pair", () => { + const { comp, node } = createFormComponent({ + schema, + formData: { first: 1 }, + }); + + Simulate.change(node.querySelector("#root_first"), { + target: { value: "" }, + }); + expect(comp.state.formData["first"]).eql(""); + }); }); }); diff --git a/test/SchemaField_test.js b/test/SchemaField_test.js index fa503d767e..504e722ce8 100644 --- a/test/SchemaField_test.js +++ b/test/SchemaField_test.js @@ -313,7 +313,7 @@ describe("SchemaField", () => { submit(node); const matches = node.querySelectorAll( - "form > .form-group > div > .error-detail .text-danger" + "form > .form-group > div > div > div > .error-detail .text-danger" ); expect(matches).to.have.length.of(1); expect(matches[0].textContent).to.eql("container"); From 23644059fb6188f15f20955579906d0730b6d229 Mon Sep 17 00:00:00 2001 From: Ethan Glasser-Camp Date: Thu, 10 Jan 2019 10:35:57 -0500 Subject: [PATCH 06/90] Fix README about how to push This keeps biting me. --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index b597906077..3e098e4b42 100644 --- a/README.md +++ b/README.md @@ -1869,7 +1869,7 @@ $ git commit -m "Bump version $VERSION" $ git tag v$VERSION $ npm run dist $ npm publish -$ git push --tags origin +$ git push --tags origin master ``` ## FAQ From 4ec38908d36feebaf6b0c3bf550416175cf0ef72 Mon Sep 17 00:00:00 2001 From: Ethan Glasser-Camp Date: Thu, 10 Jan 2019 10:41:30 -0500 Subject: [PATCH 07/90] Rename onDropIndexClick in objects to onDropPropertyClick --- src/components/fields/ObjectField.js | 4 ++-- src/components/fields/SchemaField.js | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/components/fields/ObjectField.js b/src/components/fields/ObjectField.js index 982aa3aaff..9502997561 100644 --- a/src/components/fields/ObjectField.js +++ b/src/components/fields/ObjectField.js @@ -109,7 +109,7 @@ class ObjectField extends Component { }; }; - onDropIndexClick = key => { + onDropPropertyClick = key => { return event => { event.preventDefault(); const { onChange, formData } = this.props; @@ -250,7 +250,7 @@ class ObjectField extends Component { registry={registry} disabled={disabled} readonly={readonly} - onDropIndexClick={this.onDropIndexClick} + onDropPropertyClick={this.onDropPropertyClick} /> ), name, diff --git a/src/components/fields/SchemaField.js b/src/components/fields/SchemaField.js index 1d10983c05..2afb66f478 100644 --- a/src/components/fields/SchemaField.js +++ b/src/components/fields/SchemaField.js @@ -121,7 +121,7 @@ function DefaultTemplate(props) { required, displayLabel, onKeyChange, - onDropIndexClick, + onDropPropertyClick, } = props; if (hidden) { return children; @@ -164,7 +164,7 @@ function DefaultTemplate(props) { tabIndex="-1" style={{ border: "0" }} disabled={props.disabled || props.readonly} - onClick={onDropIndexClick(props.label)} + onClick={onDropPropertyClick(props.label)} /> )}
@@ -208,7 +208,7 @@ function SchemaFieldRender(props) { idPrefix, name, onKeyChange, - onDropIndexClick, + onDropPropertyClick, required, registry = getDefaultRegistry(), } = props; @@ -308,7 +308,7 @@ function SchemaFieldRender(props) { label, hidden, onKeyChange, - onDropIndexClick, + onDropPropertyClick, required, disabled, readonly, From 7027672c22f4d700e9d5ec1dd0a99fa5cac101c2 Mon Sep 17 00:00:00 2001 From: Ethan Glasser-Camp Date: Thu, 10 Jan 2019 11:07:29 -0500 Subject: [PATCH 08/90] Refactor onPropertyChange and reword comment --- src/components/fields/ObjectField.js | 23 +++++++++-------------- 1 file changed, 9 insertions(+), 14 deletions(-) diff --git a/src/components/fields/ObjectField.js b/src/components/fields/ObjectField.js index 9502997561..b7bb4546d7 100644 --- a/src/components/fields/ObjectField.js +++ b/src/components/fields/ObjectField.js @@ -82,22 +82,17 @@ class ObjectField extends Component { onPropertyChange = (name, addedByAdditionalProperties = false) => { return (value, errorSchema) => { - let newFormData; - //section below sets zero value of input field to empty string - //instead of undefined, so that value input in additionalProperties - //doesn't disappear when emptied if (!value && addedByAdditionalProperties) { - newFormData = { - ...this.props.formData, - [name]: "", - }; - } else { - newFormData = { - ...this.props.formData, - [name]: value, - }; + // Don't set value = undefined for fields added by + // additionalProperties. Doing so removes them from the + // formData, which causes them to completely disappear + // (including the input field for the property name). Unlike + // fields which are "mandated" by the schema, these fields can + // be set to undefined by clicking a "delete field" button, so + // set empty values to the empty string. + value = ""; } - + const newFormData = { ...this.props.formData, [name]: value }; this.props.onChange( newFormData, errorSchema && From 69211d9a55222ee595c928a6ad3c14f0dbcc36c2 Mon Sep 17 00:00:00 2001 From: Ethan Glasser-Camp Date: Thu, 10 Jan 2019 11:12:51 -0500 Subject: [PATCH 09/90] Force an empty uiSchema if none is provided Fixes #1114. --- playground/app.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/playground/app.js b/playground/app.js index bf4f868c35..2a8dffdb61 100644 --- a/playground/app.js +++ b/playground/app.js @@ -357,6 +357,9 @@ class App extends Component { load = data => { // Reset the ArrayFieldTemplate whenever you load new data const { ArrayFieldTemplate, ObjectFieldTemplate } = data; + // uiSchema is missing on some examples. Provide a default to + // clear the field in all cases. + const { uiSchema = {} } = data; // force resetting form component instance this.setState({ form: false }, _ => this.setState({ @@ -364,6 +367,7 @@ class App extends Component { form: true, ArrayFieldTemplate, ObjectFieldTemplate, + uiSchema, }) ); }; From 3d1524d2b834cee8221fa9f85935b3f112ffbb48 Mon Sep 17 00:00:00 2001 From: Nataliya Karatkova Date: Thu, 10 Jan 2019 17:04:52 +0000 Subject: [PATCH 10/90] A small semantic fix (#1112) Change `div` to `ul`, update react key --- README.md | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index 3e098e4b42..1510fb3f39 100644 --- a/README.md +++ b/README.md @@ -1088,15 +1088,13 @@ An error list template is basically a React stateless component being passed err function ErrorListTemplate(props) { const {errors} = props; return ( -
- {errors.map((error, i) => { - return ( -
  • +
      + {errors.map(error => ( +
    • {error.stack}
    • - ); - })} -
  • + ))} + ); } From 3a37dd23961aa31537dfd4bb3930bb3e713e290c Mon Sep 17 00:00:00 2001 From: 0xflotus <0xflotus@gmail.com> Date: Thu, 10 Jan 2019 19:48:10 +0100 Subject: [PATCH 11/90] fixed individually (#1109) --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 1510fb3f39..66e3c1ac79 100644 --- a/README.md +++ b/README.md @@ -1259,7 +1259,7 @@ render(( #### Customizing widgets text input -All the widgets that render a text input use the `BaseInput` component internally. If you need to customize all text inputs without customizing all widgets individially, you can provide a `BaseInput` component in the `widgets` property of `Form` (see [Custom component registration](#custom-component-registration). +All the widgets that render a text input use the `BaseInput` component internally. If you need to customize all text inputs without customizing all widgets individually, you can provide a `BaseInput` component in the `widgets` property of `Form` (see [Custom component registration](#custom-component-registration). ### Custom field components From a3e0e9a39775ec8a36427d4b1b57989b3a2fa5a8 Mon Sep 17 00:00:00 2001 From: olzraiti Date: Thu, 10 Jan 2019 21:10:33 +0200 Subject: [PATCH 12/90] Pass disabled and readonly to object field template (#1088) * ObjectField passes readonly & disabled prop to template * Update README --- README.md | 2 ++ src/components/fields/ObjectField.js | 2 ++ 2 files changed, 4 insertions(+) diff --git a/README.md b/README.md index 66e3c1ac79..1565672133 100644 --- a/README.md +++ b/README.md @@ -1063,7 +1063,9 @@ The following props are passed to each `ObjectFieldTemplate`: - `TitleField`: The `TitleField` from the registry (in case you wanted to utilize it). - `title`: A string value containing the title for the object. - `description`: A string value containing the description for the object. +- `disabled`: A boolean value stating if the object is disabled. - `properties`: An array of object representing the properties in the array. Each of the properties represent a child with properties described below. +- `readonly`: A boolean value stating if the object is read-only. - `required`: A boolean value stating if the object is required. - `schema`: The schema object for this object. - `uiSchema`: The uiSchema object for this object field. diff --git a/src/components/fields/ObjectField.js b/src/components/fields/ObjectField.js index b7bb4546d7..01ea152b14 100644 --- a/src/components/fields/ObjectField.js +++ b/src/components/fields/ObjectField.js @@ -254,6 +254,8 @@ class ObjectField extends Component { required, }; }), + readonly, + disabled, required, idSchema, uiSchema, From dfd3acc94a54d6557e3ef1c4fb204f3c516e6dee Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alexis=20Pi=C3=A9plu?= Date: Thu, 10 Jan 2019 14:22:28 -0500 Subject: [PATCH 13/90] Fix: #1104 Clear ajv errors before each validate (#1105) * Clear ajv errors before validate it prevent persisting not wanted errors * #1104 Add link to issue on code --- src/validate.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/validate.js b/src/validate.js index 3c8273e7a3..3f28fab683 100644 --- a/src/validate.js +++ b/src/validate.js @@ -155,6 +155,7 @@ export default function validateFormData( transformErrors ) { try { + ajv.errors = null; // Clear old errors to prevent presisting errors, see #1104 ajv.validate(schema, formData); } catch (e) { // swallow errors thrown in ajv due to invalid schemas, these From 1a6c8677357a82dfdd15386d18e98570e9869ba0 Mon Sep 17 00:00:00 2001 From: Ethan Glasser-Camp Date: Thu, 10 Jan 2019 14:23:08 -0500 Subject: [PATCH 14/90] Clear ajv.errors as soon as we're done with it --- src/validate.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/validate.js b/src/validate.js index 3f28fab683..12e0a2fca7 100644 --- a/src/validate.js +++ b/src/validate.js @@ -155,7 +155,6 @@ export default function validateFormData( transformErrors ) { try { - ajv.errors = null; // Clear old errors to prevent presisting errors, see #1104 ajv.validate(schema, formData); } catch (e) { // swallow errors thrown in ajv due to invalid schemas, these @@ -163,6 +162,8 @@ export default function validateFormData( } let errors = transformAjvErrors(ajv.errors); + // Clear errors to prevent persistent errors, see #1104 + ajv.errors = null; if (typeof transformErrors === "function") { errors = transformErrors(errors); From 597a7ba4eb4634f735be879aaf53c3e5ffe687ea Mon Sep 17 00:00:00 2001 From: Ashwin Ramaswami Date: Thu, 10 Jan 2019 12:01:32 -0800 Subject: [PATCH 15/90] Give ids to array and object fields (#1102) * fix typo in css code * Give ids to array fields and object fields, give "root" id to the topmost fieldset --- src/components/fields/ArrayField.js | 4 ++-- src/components/fields/ObjectField.js | 2 +- test/ArrayField_test.js | 4 +++- test/Form_test.js | 2 ++ test/ObjectField_test.js | 4 +++- 5 files changed, 11 insertions(+), 5 deletions(-) diff --git a/src/components/fields/ArrayField.js b/src/components/fields/ArrayField.js index a85bd6fa0f..05a45f582f 100644 --- a/src/components/fields/ArrayField.js +++ b/src/components/fields/ArrayField.js @@ -103,7 +103,7 @@ function DefaultArrayItem(props) { function DefaultFixedArrayFieldTemplate(props) { return ( -
    +
    +
    +
    {(props.uiSchema["ui:title"] || props.title) && ( { it("should render a fieldset", () => { const { node } = createFormComponent({ schema }); - expect(node.querySelectorAll("fieldset")).to.have.length.of(1); + const fieldset = node.querySelectorAll("fieldset"); + expect(fieldset).to.have.length.of(1); + expect(fieldset[0].id).eql("root"); }); it("should render a fieldset legend", () => { diff --git a/test/Form_test.js b/test/Form_test.js index 85a39d7f89..793e3a0758 100644 --- a/test/Form_test.js +++ b/test/Form_test.js @@ -127,6 +127,7 @@ describe("Form", () => { ids.push(input.getAttribute("id")); } expect(ids).to.eql(["rjsf_count"]); + expect(node.querySelector("fieldset").id).to.eql("rjsf"); }); }); @@ -151,6 +152,7 @@ describe("Form", () => { ids.push(input.getAttribute("id")); } expect(ids).to.eql(["rjsf_count"]); + expect(node.querySelector("fieldset").id).to.eql("rjsf"); }); it("should work with oneOf", function() { diff --git a/test/ObjectField_test.js b/test/ObjectField_test.js index b546ba55bf..2e115813d6 100644 --- a/test/ObjectField_test.js +++ b/test/ObjectField_test.js @@ -40,7 +40,9 @@ describe("ObjectField", () => { it("should render a fieldset", () => { const { node } = createFormComponent({ schema }); - expect(node.querySelectorAll("fieldset")).to.have.length.of(1); + const fieldset = node.querySelectorAll("fieldset"); + expect(fieldset).to.have.length.of(1); + expect(fieldset[0].id).eql("root"); }); it("should render a fieldset legend", () => { From b5a286af95a917f3bc52ea825544d448bb71cb8e Mon Sep 17 00:00:00 2001 From: Ethan Glasser-Camp Date: Thu, 10 Jan 2019 16:01:02 -0500 Subject: [PATCH 16/90] Bump v1.1.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 1db7ca591a..d9d93b62aa 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "react-jsonschema-form", - "version": "1.0.7", + "version": "1.1.0", "description": "A simple React component capable of building HTML forms out of a JSON schema.", "scripts": { "build:readme": "toctoc README.md -w", From a2f3e6880b1b85db89012fdff08a9232edac887b Mon Sep 17 00:00:00 2001 From: Christian Sletten Eide Date: Mon, 14 Jan 2019 20:43:11 +0100 Subject: [PATCH 17/90] Handle sort arrays better (#1125) * move array item from index to newIndex * formating * moved reordering to a function * adds test for array sorting --- src/components/fields/ArrayField.js | 27 +++++++++--------- test/ArrayField_test.js | 44 +++++++++++++++++++++++++++++ 2 files changed, 57 insertions(+), 14 deletions(-) diff --git a/src/components/fields/ArrayField.js b/src/components/fields/ArrayField.js index 05a45f582f..9537a635d3 100644 --- a/src/components/fields/ArrayField.js +++ b/src/components/fields/ArrayField.js @@ -276,20 +276,19 @@ class ArrayField extends Component { } } } - onChange( - formData.map((item, i) => { - // i is string, index and newIndex are numbers, - // so using "==" to compare - if (i == newIndex) { - return formData[index]; - } else if (i == index) { - return formData[newIndex]; - } else { - return item; - } - }), - newErrorSchema - ); + + function reOrderArray() { + // Copy item + let newFormData = formData.slice(); + + // Moves item from index to newIndex + newFormData.splice(index, 1); + newFormData.splice(newIndex, 0, formData[index]); + + return newFormData; + } + + onChange(reOrderArray(), newErrorSchema); }; }; diff --git a/test/ArrayField_test.js b/test/ArrayField_test.js index 43fb98f790..299b670904 100644 --- a/test/ArrayField_test.js +++ b/test/ArrayField_test.js @@ -272,6 +272,50 @@ describe("ArrayField", () => { expect(inputs[2].value).eql("bar"); }); + it("should move from first to last in the list", () => { + function moveAnywhereArrayItemTemplate(props) { + const buttons = []; + for (let i = 0; i < 3; i++) { + buttons.push( + + ); + } + return ( +
    + {props.children} + {buttons} +
    + ); + } + + function moveAnywhereArrayFieldTemplate(props) { + return ( +
    + {props.items.map(moveAnywhereArrayItemTemplate)} +
    + ); + } + + const { node } = createFormComponent({ + schema, + formData: ["foo", "bar", "baz"], + ArrayFieldTemplate: moveAnywhereArrayFieldTemplate, + }); + + const button = node.querySelector(".item-0 .array-item-move-to-2"); + Simulate.click(button); + + const inputs = node.querySelectorAll(".field-string input[type=text]"); + expect(inputs[0].value).eql("bar"); + expect(inputs[1].value).eql("baz"); + expect(inputs[2].value).eql("foo"); + }); + it("should disable move buttons on the ends of the list", () => { const { node } = createFormComponent({ schema, From 66d800e91566c5efa5c89668342577c458cdb204 Mon Sep 17 00:00:00 2001 From: Lucian Buzzo Date: Mon, 14 Jan 2019 19:49:28 +0000 Subject: [PATCH 18/90] Add preliminary support for the anyOf keyword (#1118) * Add preliminary support for the anyOf keyword This change introduces support for the `anyOf` keyword. The keyword is NOT supported in arrays (using the `items` keyword). Options provided in the `anyOf` keyword are rendered using a new component called `AnyOfField` this can be overridden with a custom field using the `fields` property. Signed-off-by: Lucian * Refactor unnecessary .map * Change selected option based on form data * Update README * Update README FAQ * Share prop types between fields --- README.md | 14 +- playground/samples/anyOf.js | 37 ++++ playground/samples/index.js | 2 + src/components/fields/AnyOfField.js | 163 ++++++++++++++ src/components/fields/ArrayField.js | 31 +-- src/components/fields/BooleanField.js | 25 +-- src/components/fields/NumberField.js | 12 +- src/components/fields/ObjectField.js | 24 +- src/components/fields/SchemaField.js | 53 +++-- src/components/fields/StringField.js | 30 +-- src/components/fields/index.js | 2 + src/types.js | 36 +++ src/validate.js | 13 ++ test/anyOf_test.js | 302 ++++++++++++++++++++++++++ test/validate_test.js | 32 ++- 15 files changed, 649 insertions(+), 127 deletions(-) create mode 100644 playground/samples/anyOf.js create mode 100644 src/components/fields/AnyOfField.js create mode 100644 src/types.js create mode 100644 test/anyOf_test.js diff --git a/README.md b/README.md index 1565672133..511f641e80 100644 --- a/README.md +++ b/README.md @@ -1803,7 +1803,12 @@ This component follows [JSON Schema](http://json-schema.org/documentation.html) * `additionalItems` keyword for arrays This keyword works when `items` is an array. `additionalItems: true` is not supported because there's no widget to represent an item of any type. In this case it will be treated as no additional items allowed. `additionalItems` being a valid schema is supported. * `anyOf`, `allOf`, and `oneOf`, or multiple `types` (i.e. `"type": ["string", "array"]` - Nobody yet has come up with a PR that adds this feature with a simple and easy-to-understand UX. + The `anyOf` keyword is supported but has the following caveats: + - The `anyOf` keyword is not supported when used inside the `items` keyword + for arrays. + - Properties declared inside the `anyOf` should not overlap with properties + "outside" of the `anyOf`. + You can use `oneOf` with [schema dependencies](#schema-dependencies) to dynamically add schema properties based on input data but this feature does not bring general support for `oneOf` elsewhere in a schema. ## Tips and tricks @@ -1876,7 +1881,12 @@ $ git push --tags origin master ### Q: Does rjsf support `oneOf`, `anyOf`, multiple types in an array, etc.? -A: Not yet (except for a special case where you can use `oneOf` in [schema dependencies](#schema-dependencies)), but perhaps you will be the person whose PR will finally add the feature in a way that gets merged. For inspiration, see [#329](https://github.com/mozilla-services/react-jsonschema-form/pull/329) or [#417](https://github.com/mozilla-services/react-jsonschema-form/pull/417). See also: [#52](https://github.com/mozilla-services/react-jsonschema-form/issues/52), [#151](https://github.com/mozilla-services/react-jsonschema-form/issues/151), [#171](https://github.com/mozilla-services/react-jsonschema-form/issues/171), [#200](https://github.com/mozilla-services/react-jsonschema-form/issues/200), [#282](https://github.com/mozilla-services/react-jsonschema-form/issues/282), [#302](https://github.com/mozilla-services/react-jsonschema-form/pull/302), [#330](https://github.com/mozilla-services/react-jsonschema-form/issues/330), [#430](https://github.com/mozilla-services/react-jsonschema-form/issues/430), [#522](https://github.com/mozilla-services/react-jsonschema-form/issues/522), [#538](https://github.com/mozilla-services/react-jsonschema-form/issues/538), [#551](https://github.com/mozilla-services/react-jsonschema-form/issues/551), [#552](https://github.com/mozilla-services/react-jsonschema-form/issues/552), or [#648](https://github.com/mozilla-services/react-jsonschema-form/issues/648). +A: The `anyOf` keyword is supported but has the following caveats: + - The `anyOf` keyword is not supported when used inside the `items` keyword + for arrays. + - Properties declared inside the `anyOf` should not overlap with properties + "outside" of the `anyOf`. +There is also special cased where you can use `oneOf` in [schema dependencies](#schema-dependencies), If you'd like to help improve support for these keywords, see the following issues for inspiration [#329](https://github.com/mozilla-services/react-jsonschema-form/pull/329) or [#417](https://github.com/mozilla-services/react-jsonschema-form/pull/417). See also: [#52](https://github.com/mozilla-services/react-jsonschema-form/issues/52), [#151](https://github.com/mozilla-services/react-jsonschema-form/issues/151), [#171](https://github.com/mozilla-services/react-jsonschema-form/issues/171), [#200](https://github.com/mozilla-services/react-jsonschema-form/issues/200), [#282](https://github.com/mozilla-services/react-jsonschema-form/issues/282), [#302](https://github.com/mozilla-services/react-jsonschema-form/pull/302), [#330](https://github.com/mozilla-services/react-jsonschema-form/issues/330), [#430](https://github.com/mozilla-services/react-jsonschema-form/issues/430), [#522](https://github.com/mozilla-services/react-jsonschema-form/issues/522), [#538](https://github.com/mozilla-services/react-jsonschema-form/issues/538), [#551](https://github.com/mozilla-services/react-jsonschema-form/issues/551), [#552](https://github.com/mozilla-services/react-jsonschema-form/issues/552), or [#648](https://github.com/mozilla-services/react-jsonschema-form/issues/648). ### Q: Will react-jsonschema-form support Material, Ant-Design, Foundation, or [some other specific widget library or frontend style]? diff --git a/playground/samples/anyOf.js b/playground/samples/anyOf.js new file mode 100644 index 0000000000..432b0e0be1 --- /dev/null +++ b/playground/samples/anyOf.js @@ -0,0 +1,37 @@ +module.exports = { + schema: { + type: "object", + properties: { + age: { + type: "integer", + title: "Age", + }, + }, + anyOf: [ + { + title: "First method of identification", + properties: { + firstName: { + type: "string", + title: "First name", + default: "Chuck", + }, + lastName: { + type: "string", + title: "Last name", + }, + }, + }, + { + title: "Second method of identification", + properties: { + idCode: { + type: "string", + title: "ID code", + }, + }, + }, + ], + }, + formData: {}, +}; diff --git a/playground/samples/index.js b/playground/samples/index.js index b496c0fb6d..8ad13741e4 100644 --- a/playground/samples/index.js +++ b/playground/samples/index.js @@ -1,4 +1,5 @@ import arrays from "./arrays"; +import anyOf from "./anyOf"; import nested from "./nested"; import numbers from "./numbers"; import simple from "./simple"; @@ -40,4 +41,5 @@ export const samples = { "Property dependencies": propertyDependencies, "Schema dependencies": schemaDependencies, "Additional Properties": additionalProperties, + "Optional Forms": anyOf, }; diff --git a/src/components/fields/AnyOfField.js b/src/components/fields/AnyOfField.js new file mode 100644 index 0000000000..52fbc74c28 --- /dev/null +++ b/src/components/fields/AnyOfField.js @@ -0,0 +1,163 @@ +import React, { Component } from "react"; +import PropTypes from "prop-types"; +import * as types from "../../types"; +import { guessType } from "../../utils"; +import { isValid } from "../../validate"; + +class AnyOfField extends Component { + constructor(props) { + super(props); + + const { formData, schema } = this.props; + + this.state = { + selectedOption: this.getMatchingOption(formData, schema.anyOf), + }; + } + + componentWillReceiveProps(nextProps) { + const matchingOption = this.getMatchingOption( + nextProps.formData, + nextProps.schema.anyOf + ); + + if (matchingOption === this.state.selectedOption) { + return; + } + + this.setState({ selectedOption: matchingOption }); + } + + getMatchingOption(formData, options) { + for (let i = 0; i < options.length; i++) { + if (isValid(options[i], formData)) { + return i; + } + } + + // If the form data matches none of the options, use the first option + return 0; + } + + onOptionChange = event => { + const selectedOption = parseInt(event.target.value, 10); + const { formData, onChange, schema } = this.props; + const options = schema.anyOf; + + if (guessType(formData) === "object") { + const newFormData = Object.assign({}, formData); + + const optionsToDiscard = options.slice(); + optionsToDiscard.splice(selectedOption, 1); + + // Discard any data added using other options + for (const option of optionsToDiscard) { + if (option.properties) { + for (const key in option.properties) { + if (newFormData.hasOwnProperty(key)) { + delete newFormData[key]; + } + } + } + } + + onChange(newFormData); + } else { + onChange(undefined); + } + + this.setState({ + selectedOption: parseInt(event.target.value, 10), + }); + }; + + render() { + const { + disabled, + errorSchema, + formData, + idPrefix, + idSchema, + onBlur, + onChange, + onFocus, + schema, + registry, + safeRenderCompletion, + uiSchema, + } = this.props; + + const _SchemaField = registry.fields.SchemaField; + const { selectedOption } = this.state; + + const baseType = schema.type; + const options = schema.anyOf || []; + const option = options[selectedOption] || null; + let optionSchema; + + if (option) { + // If the subschema doesn't declare a type, infer the type from the + // parent schema + optionSchema = option.type + ? option + : Object.assign({}, option, { type: baseType }); + } + + return ( +
    +
    + +
    + + {option !== null && ( + <_SchemaField + schema={optionSchema} + uiSchema={uiSchema} + errorSchema={errorSchema} + idSchema={idSchema} + idPrefix={idPrefix} + formData={formData} + onChange={onChange} + onBlur={onBlur} + onFocus={onFocus} + registry={registry} + safeRenderCompletion={safeRenderCompletion} + disabled={disabled} + /> + )} +
    + ); + } +} + +AnyOfField.defaultProps = { + disabled: false, + errorSchema: {}, + idSchema: {}, + uiSchema: {}, +}; + +if (process.env.NODE_ENV !== "production") { + AnyOfField.propTypes = { + schema: PropTypes.object.isRequired, + uiSchema: PropTypes.object, + idSchema: PropTypes.object, + formData: PropTypes.any, + errorSchema: PropTypes.object, + registry: types.registry.isRequired, + }; +} + +export default AnyOfField; diff --git a/src/components/fields/ArrayField.js b/src/components/fields/ArrayField.js index 9537a635d3..f60e5b86ff 100644 --- a/src/components/fields/ArrayField.js +++ b/src/components/fields/ArrayField.js @@ -1,8 +1,8 @@ import AddButton from "../AddButton"; import IconButton from "../IconButton"; import React, { Component } from "react"; -import PropTypes from "prop-types"; import includes from "core-js/library/fn/array/includes"; +import * as types from "../../types"; import UnsupportedField from "./UnsupportedField"; import { @@ -660,34 +660,7 @@ class ArrayField extends Component { } if (process.env.NODE_ENV !== "production") { - ArrayField.propTypes = { - schema: PropTypes.object.isRequired, - uiSchema: PropTypes.shape({ - "ui:options": PropTypes.shape({ - addable: PropTypes.bool, - orderable: PropTypes.bool, - removable: PropTypes.bool, - }), - }), - idSchema: PropTypes.object, - errorSchema: PropTypes.object, - onChange: PropTypes.func.isRequired, - onBlur: PropTypes.func, - onFocus: PropTypes.func, - formData: PropTypes.array, - required: PropTypes.bool, - disabled: PropTypes.bool, - readonly: PropTypes.bool, - autofocus: PropTypes.bool, - registry: PropTypes.shape({ - widgets: PropTypes.objectOf( - PropTypes.oneOfType([PropTypes.func, PropTypes.object]) - ).isRequired, - fields: PropTypes.objectOf(PropTypes.func).isRequired, - definitions: PropTypes.object.isRequired, - formContext: PropTypes.object.isRequired, - }), - }; + ArrayField.propTypes = types.fieldProps; } export default ArrayField; diff --git a/src/components/fields/BooleanField.js b/src/components/fields/BooleanField.js index 00014f28c9..7677a528ff 100644 --- a/src/components/fields/BooleanField.js +++ b/src/components/fields/BooleanField.js @@ -1,5 +1,5 @@ import React from "react"; -import PropTypes from "prop-types"; +import * as types from "../../types"; import { getWidget, @@ -55,28 +55,7 @@ function BooleanField(props) { } if (process.env.NODE_ENV !== "production") { - BooleanField.propTypes = { - schema: PropTypes.object.isRequired, - uiSchema: PropTypes.object, - idSchema: PropTypes.object, - onChange: PropTypes.func.isRequired, - onFocus: PropTypes.func, - onBlur: PropTypes.func, - formData: PropTypes.bool, - required: PropTypes.bool, - disabled: PropTypes.bool, - readonly: PropTypes.bool, - autofocus: PropTypes.bool, - registry: PropTypes.shape({ - widgets: PropTypes.objectOf( - PropTypes.oneOfType([PropTypes.func, PropTypes.object]) - ).isRequired, - fields: PropTypes.objectOf(PropTypes.func).isRequired, - definitions: PropTypes.object.isRequired, - formContext: PropTypes.object.isRequired, - }), - rawErrors: PropTypes.arrayOf(PropTypes.string), - }; + BooleanField.propTypes = types.fieldProps; } BooleanField.defaultProps = { diff --git a/src/components/fields/NumberField.js b/src/components/fields/NumberField.js index 30c297b19b..1f7f7c112d 100644 --- a/src/components/fields/NumberField.js +++ b/src/components/fields/NumberField.js @@ -1,6 +1,6 @@ import React from "react"; -import PropTypes from "prop-types"; +import * as types from "../../types"; import { asNumber } from "../../utils"; function NumberField(props) { @@ -14,15 +14,7 @@ function NumberField(props) { } if (process.env.NODE_ENV !== "production") { - NumberField.propTypes = { - schema: PropTypes.object.isRequired, - uiSchema: PropTypes.object, - idSchema: PropTypes.object, - onChange: PropTypes.func.isRequired, - formData: PropTypes.oneOfType([PropTypes.number, PropTypes.string]), - required: PropTypes.bool, - formContext: PropTypes.object.isRequired, - }; + NumberField.propTypes = types.fieldProps; } NumberField.defaultProps = { diff --git a/src/components/fields/ObjectField.js b/src/components/fields/ObjectField.js index 8132739626..9eb1732d8c 100644 --- a/src/components/fields/ObjectField.js +++ b/src/components/fields/ObjectField.js @@ -1,6 +1,6 @@ import AddButton from "../AddButton"; import React, { Component } from "react"; -import PropTypes from "prop-types"; +import * as types from "../../types"; import { orderProperties, @@ -198,7 +198,7 @@ class ObjectField extends Component { const description = uiSchema["ui:description"] || schema.description; let orderedProperties; try { - const properties = Object.keys(schema.properties); + const properties = Object.keys(schema.properties || {}); orderedProperties = orderProperties(properties, uiSchema["ui:order"]); } catch (err) { return ( @@ -268,25 +268,7 @@ class ObjectField extends Component { } if (process.env.NODE_ENV !== "production") { - ObjectField.propTypes = { - schema: PropTypes.object.isRequired, - uiSchema: PropTypes.object, - errorSchema: PropTypes.object, - idSchema: PropTypes.object, - onChange: PropTypes.func.isRequired, - formData: PropTypes.object, - required: PropTypes.bool, - disabled: PropTypes.bool, - readonly: PropTypes.bool, - registry: PropTypes.shape({ - widgets: PropTypes.objectOf( - PropTypes.oneOfType([PropTypes.func, PropTypes.object]) - ).isRequired, - fields: PropTypes.objectOf(PropTypes.func).isRequired, - definitions: PropTypes.object.isRequired, - formContext: PropTypes.object.isRequired, - }), - }; + ObjectField.propTypes = types.fieldProps; } export default ObjectField; diff --git a/src/components/fields/SchemaField.js b/src/components/fields/SchemaField.js index 2afb66f478..f55e8a030e 100644 --- a/src/components/fields/SchemaField.js +++ b/src/components/fields/SchemaField.js @@ -2,9 +2,11 @@ import { ADDITIONAL_PROPERTY_FLAG } from "../../utils"; import IconButton from "../IconButton"; import React from "react"; import PropTypes from "prop-types"; +import * as types from "../../types"; import { isMultiSelect, + isSelect, retrieveSchema, toIdSchema, getDefaultRegistry, @@ -36,6 +38,13 @@ function getFieldComponent(schema, uiSchema, idSchema, fields) { } const componentName = COMPONENT_TYPES[getSchemaType(schema)]; + + // If the type is not defined and the schema uses 'anyOf', don't render + // a field and let the AnyOfField component handle the form display + if (!componentName && schema.anyOf) { + return () => null; + } + return componentName in fields ? fields[componentName] : () => { @@ -124,7 +133,7 @@ function DefaultTemplate(props) { onDropPropertyClick, } = props; if (hidden) { - return children; + return
    {children}
    ; } const additional = props.schema.hasOwnProperty(ADDITIONAL_PROPERTY_FLAG); @@ -320,7 +329,35 @@ function SchemaFieldRender(props) { uiSchema, }; - return {field}; + const _AnyOfField = registry.fields.AnyOfField; + + return ( + + {field} + + {/* + If the schema `anyOf` can be rendered as a select control, don't + render the `anyOf` selection and let `StringField` component handle + rendering + */} + {schema.anyOf && !isSelect(schema) && ( + <_AnyOfField + disabled={disabled} + errorSchema={errorSchema} + formData={formData} + idPrefix={idPrefix} + idSchema={idSchema} + onBlur={props.onBlur} + onChange={props.onChange} + onFocus={props.onFocus} + schema={schema} + registry={registry} + safeRenderCompletion={props.safeRenderCompletion} + uiSchema={uiSchema} + /> + )} + + ); } class SchemaField extends React.Component { @@ -354,17 +391,7 @@ if (process.env.NODE_ENV !== "production") { idSchema: PropTypes.object, formData: PropTypes.any, errorSchema: PropTypes.object, - registry: PropTypes.shape({ - widgets: PropTypes.objectOf( - PropTypes.oneOfType([PropTypes.func, PropTypes.object]) - ).isRequired, - fields: PropTypes.objectOf(PropTypes.func).isRequired, - definitions: PropTypes.object.isRequired, - ArrayFieldTemplate: PropTypes.func, - ObjectFieldTemplate: PropTypes.func, - FieldTemplate: PropTypes.func, - formContext: PropTypes.object.isRequired, - }), + registry: types.registry.isRequired, }; } diff --git a/src/components/fields/StringField.js b/src/components/fields/StringField.js index 6ef3f3cbff..31dc31580b 100644 --- a/src/components/fields/StringField.js +++ b/src/components/fields/StringField.js @@ -1,5 +1,5 @@ import React from "react"; -import PropTypes from "prop-types"; +import * as types from "../../types"; import { getWidget, @@ -57,33 +57,7 @@ function StringField(props) { } if (process.env.NODE_ENV !== "production") { - StringField.propTypes = { - schema: PropTypes.object.isRequired, - uiSchema: PropTypes.object.isRequired, - idSchema: PropTypes.object, - onChange: PropTypes.func.isRequired, - onBlur: PropTypes.func, - onFocus: PropTypes.func, - formData: PropTypes.oneOfType([ - PropTypes.string, - PropTypes.number, - PropTypes.bool, - ]), - registry: PropTypes.shape({ - widgets: PropTypes.objectOf( - PropTypes.oneOfType([PropTypes.func, PropTypes.object]) - ).isRequired, - fields: PropTypes.objectOf(PropTypes.func).isRequired, - definitions: PropTypes.object.isRequired, - formContext: PropTypes.object.isRequired, - }), - formContext: PropTypes.object.isRequired, - required: PropTypes.bool, - disabled: PropTypes.bool, - readonly: PropTypes.bool, - autofocus: PropTypes.bool, - rawErrors: PropTypes.arrayOf(PropTypes.string), - }; + StringField.propTypes = types.fieldProps; } StringField.defaultProps = { diff --git a/src/components/fields/index.js b/src/components/fields/index.js index c8e92e5dde..9337e209f1 100644 --- a/src/components/fields/index.js +++ b/src/components/fields/index.js @@ -1,3 +1,4 @@ +import AnyOfField from "./AnyOfField"; import ArrayField from "./ArrayField"; import BooleanField from "./BooleanField"; import DescriptionField from "./DescriptionField"; @@ -9,6 +10,7 @@ import TitleField from "./TitleField"; import UnsupportedField from "./UnsupportedField"; export default { + AnyOfField, ArrayField, BooleanField, DescriptionField, diff --git a/src/types.js b/src/types.js new file mode 100644 index 0000000000..4d536df70e --- /dev/null +++ b/src/types.js @@ -0,0 +1,36 @@ +import PropTypes from "prop-types"; + +export const registry = PropTypes.shape({ + ArrayFieldTemplate: PropTypes.func, + FieldTemplate: PropTypes.func, + ObjectFieldTemplate: PropTypes.func, + definitions: PropTypes.object.isRequired, + fields: PropTypes.objectOf(PropTypes.func).isRequired, + formContext: PropTypes.object.isRequired, + widgets: PropTypes.objectOf( + PropTypes.oneOfType([PropTypes.func, PropTypes.object]) + ).isRequired, +}); + +export const fieldProps = { + autofocus: PropTypes.bool, + disabled: PropTypes.bool, + errorSchema: PropTypes.object, + formData: PropTypes.any, + idSchema: PropTypes.object, + onBlur: PropTypes.func, + onChange: PropTypes.func.isRequired, + onFocus: PropTypes.func, + rawErrors: PropTypes.arrayOf(PropTypes.string), + readonly: PropTypes.bool, + registry: registry.isRequired, + required: PropTypes.bool, + schema: PropTypes.object.isRequired, + uiSchema: PropTypes.shape({ + "ui:options": PropTypes.shape({ + addable: PropTypes.bool, + orderable: PropTypes.bool, + removable: PropTypes.bool, + }), + }), +}; diff --git a/src/validate.js b/src/validate.js index 12e0a2fca7..222b163287 100644 --- a/src/validate.js +++ b/src/validate.js @@ -184,3 +184,16 @@ export default function validateFormData( return { errors: newErrors, errorSchema: newErrorSchema }; } + +/** + * Validates data against a schema, returning true if the data is valid, or + * false otherwise. If the schema is invalid, then this function will return + * false. + */ +export function isValid(schema, data) { + try { + return ajv.validate(schema, data); + } catch (e) { + return false; + } +} diff --git a/test/anyOf_test.js b/test/anyOf_test.js new file mode 100644 index 0000000000..ed5e67475b --- /dev/null +++ b/test/anyOf_test.js @@ -0,0 +1,302 @@ +import React from "react"; +import { expect } from "chai"; +import { Simulate } from "react-addons-test-utils"; + +import { createFormComponent, createSandbox, setProps } from "./test_utils"; + +describe("anyOf", () => { + let sandbox; + + beforeEach(() => { + sandbox = createSandbox(); + }); + + afterEach(() => { + sandbox.restore(); + }); + + it("should not render a select element if the anyOf keyword is not present", () => { + const schema = { + type: "object", + properties: { + foo: { type: "string" }, + }, + }; + + const { node } = createFormComponent({ + schema, + }); + + expect(node.querySelectorAll("select")).to.have.length.of(0); + }); + + it("should render a select element if the anyOf keyword is present", () => { + const schema = { + type: "object", + anyOf: [ + { + properties: { + foo: { type: "string" }, + }, + }, + { + properties: { + bar: { type: "string" }, + }, + }, + ], + }; + + const { node } = createFormComponent({ + schema, + }); + + expect(node.querySelectorAll("select")).to.have.length.of(1); + }); + + it("should change the rendered form when the select value is changed", () => { + const schema = { + type: "object", + anyOf: [ + { + properties: { + foo: { type: "string" }, + }, + }, + { + properties: { + bar: { type: "string" }, + }, + }, + ], + }; + + const { node } = createFormComponent({ + schema, + }); + + expect(node.querySelectorAll("#root_foo")).to.have.length.of(1); + expect(node.querySelectorAll("#root_bar")).to.have.length.of(0); + + const $select = node.querySelector("select"); + + Simulate.change($select, { + target: { value: $select.options[1].value }, + }); + + expect(node.querySelectorAll("#root_foo")).to.have.length.of(0); + expect(node.querySelectorAll("#root_bar")).to.have.length.of(1); + }); + + it("should handle change events", () => { + const schema = { + type: "object", + anyOf: [ + { + properties: { + foo: { type: "string" }, + }, + }, + { + properties: { + bar: { type: "string" }, + }, + }, + ], + }; + + const { comp, node } = createFormComponent({ + schema, + }); + + Simulate.change(node.querySelector("input#root_foo"), { + target: { value: "Lorem ipsum dolor sit amet" }, + }); + + expect(comp.state.formData.foo).eql("Lorem ipsum dolor sit amet"); + }); + + it("should clear previous data when changing options", () => { + const schema = { + type: "object", + properties: { + buzz: { type: "string" }, + }, + anyOf: [ + { + properties: { + foo: { type: "string" }, + }, + }, + { + properties: { + bar: { type: "string" }, + }, + }, + ], + }; + + const { comp, node } = createFormComponent({ + schema, + }); + + Simulate.change(node.querySelector("input#root_buzz"), { + target: { value: "Lorem ipsum dolor sit amet" }, + }); + + Simulate.change(node.querySelector("input#root_foo"), { + target: { value: "Consectetur adipiscing elit" }, + }); + + expect(comp.state.formData.buzz).eql("Lorem ipsum dolor sit amet"); + expect(comp.state.formData.foo).eql("Consectetur adipiscing elit"); + + const $select = node.querySelector("select"); + + Simulate.change($select, { + target: { value: $select.options[1].value }, + }); + + expect(comp.state.formData.hasOwnProperty("foo")).to.be.false; + expect(comp.state.formData.buzz).eql("Lorem ipsum dolor sit amet"); + }); + + it("should support options with different types", () => { + const schema = { + type: "object", + properties: { + userId: { + anyOf: [ + { + type: "number", + }, + { + type: "string", + }, + ], + }, + }, + }; + + const { comp, node } = createFormComponent({ + schema, + }); + + Simulate.change(node.querySelector("input#root_userId"), { + target: { value: 12345 }, + }); + + expect(comp.state.formData).eql({ + userId: 12345, + }); + + const $select = node.querySelector("select"); + + Simulate.change($select, { + target: { value: $select.options[1].value }, + }); + + expect(comp.state.formData).eql({ + userId: undefined, + }); + + Simulate.change(node.querySelector("input#root_userId"), { + target: { value: "Lorem ipsum dolor sit amet" }, + }); + + expect(comp.state.formData).eql({ + userId: "Lorem ipsum dolor sit amet", + }); + }); + + it("should support custom fields", () => { + const schema = { + type: "object", + properties: { + userId: { + anyOf: [ + { + type: "number", + }, + { + type: "string", + }, + ], + }, + }, + }; + + const CustomField = () => { + return
    ; + }; + + const { node } = createFormComponent({ + schema, + fields: { + AnyOfField: CustomField, + }, + }); + + expect(node.querySelectorAll("#custom-anyof-field")).to.have.length(1); + }); + + it("should select the correct field when the form is rendered from existing data", () => { + const schema = { + type: "object", + properties: { + userId: { + anyOf: [ + { + type: "number", + }, + { + type: "string", + }, + ], + }, + }, + }; + + const { node } = createFormComponent({ + schema, + formData: { + userId: "foobarbaz", + }, + }); + + expect(node.querySelector("select").value).eql("1"); + }); + + it("should select the correct field when the formData property is updated", () => { + const schema = { + type: "object", + properties: { + userId: { + anyOf: [ + { + type: "number", + }, + { + type: "string", + }, + ], + }, + }, + }; + + const { comp, node } = createFormComponent({ + schema, + }); + + expect(node.querySelector("select").value).eql("0"); + + setProps(comp, { + schema, + formData: { + userId: "foobarbaz", + }, + }); + + expect(node.querySelector("select").value).eql("1"); + }); +}); diff --git a/test/validate_test.js b/test/validate_test.js index 0b352b74bc..cb437e894e 100644 --- a/test/validate_test.js +++ b/test/validate_test.js @@ -3,10 +3,40 @@ import { expect } from "chai"; import sinon from "sinon"; import { Simulate } from "react-addons-test-utils"; -import validateFormData, { toErrorList } from "../src/validate"; +import validateFormData, { isValid, toErrorList } from "../src/validate"; import { createFormComponent } from "./test_utils"; describe("Validation", () => { + describe("validate.isValid()", () => { + it("should return true if the data is valid against the schema", () => { + const schema = { + type: "object", + properties: { + foo: { type: "string" }, + }, + }; + + expect(isValid(schema, { foo: "bar" })).to.be.true; + }); + + it("should return false if the data is not valid against the schema", () => { + const schema = { + type: "object", + properties: { + foo: { type: "string" }, + }, + }; + + expect(isValid(schema, { foo: 12345 })).to.be.false; + }); + + it("should return false if the schema is invalid", () => { + const schema = "foobarbaz"; + + expect(isValid(schema, { foo: "bar" })).to.be.false; + }); + }); + describe("validate.validateFormData()", () => { describe("No custom validate function", () => { const illFormedKey = "bar.'\"[]()=+*&^%$#@!"; From 4c81b02742270e5225c826025f5e99efdc35ea32 Mon Sep 17 00:00:00 2001 From: Ethan Glasser-Camp Date: Mon, 14 Jan 2019 14:57:50 -0500 Subject: [PATCH 19/90] Bump version v1.2.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index d9d93b62aa..79ecf38dc3 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "react-jsonschema-form", - "version": "1.1.0", + "version": "1.2.0", "description": "A simple React component capable of building HTML forms out of a JSON schema.", "scripts": { "build:readme": "toctoc README.md -w", From f27a24db1b3acf51fc774df5093129fd605010b9 Mon Sep 17 00:00:00 2001 From: Maria Comas Date: Fri, 18 Jan 2019 22:31:17 +0100 Subject: [PATCH 20/90] Fix typos (#1135) --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 511f641e80..e66becb86e 100644 --- a/README.md +++ b/README.md @@ -512,7 +512,7 @@ const uiSchema = { #### `expandable` option -If `additionalProperties` contains a schema object, an add button for new properies is shown by default. The UX for editing properties whose names are user-defined is still experimental. +If `additionalProperties` contains a schema object, an add button for new properties is shown by default. The UX for editing properties whose names are user-defined is still experimental. You can turn support for `additionalProperties` off with the `expandable` option in `uiSchema`: @@ -1334,7 +1334,7 @@ A field component will always be passed the following props: - `formData`: The data for this field; - `errorSchema`: The tree of errors for this field and its children; - `registry`: A [registry](#the-registry-object) object (read next). - - `formContext`: A [formContext](#the-formcontext-object) object (read next next). + - `formContext`: A [formContext](#the-formcontext-object) object (read next). #### The `registry` object From f72be02aa40a077d707b286d012bfcceb1247a62 Mon Sep 17 00:00:00 2001 From: Jeff Balboni Date: Fri, 18 Jan 2019 16:43:26 -0500 Subject: [PATCH 21/90] Fix corner case of empty items array (#871) --- src/utils.js | 5 ++++- test/utils_test.js | 20 ++++++++++++++++++++ 2 files changed, 24 insertions(+), 1 deletion(-) diff --git a/src/utils.js b/src/utils.js index 47856324d2..7deaa3edf7 100644 --- a/src/utils.js +++ b/src/utils.js @@ -157,9 +157,12 @@ function computeDefaults(schema, parentDefaults, definitions = {}) { if (schema.minItems > defaultsLength) { const defaultEntries = defaults || []; // populate the array with the defaults + const fillerSchema = Array.isArray(schema.items) + ? schema.additionalItems + : schema.items; const fillerEntries = fill( new Array(schema.minItems - defaultsLength), - computeDefaults(schema.items, schema.items.defaults, definitions) + computeDefaults(fillerSchema, fillerSchema.defaults, definitions) ); // then fill up the rest with either the item default or empty, up to minItems diff --git a/test/utils_test.js b/test/utils_test.js index 0d97f0551c..7ba1be7601 100644 --- a/test/utils_test.js +++ b/test/utils_test.js @@ -292,6 +292,26 @@ describe("utils", () => { foo: 42, }); }); + + it("should fill array with additional items schema when items is empty", () => { + const schema = { + type: "object", + properties: { + array: { + type: "array", + minItems: 1, + additionalItems: { + type: "string", + default: "foo", + }, + items: [], + }, + }, + }; + expect(getDefaultFormState(schema, {})).eql({ + array: ["foo"], + }); + }); }); }); From b068705b32c9409058b826c7a7d88aa090ec8d29 Mon Sep 17 00:00:00 2001 From: Lucian Buzzo Date: Fri, 18 Jan 2019 21:43:51 +0000 Subject: [PATCH 22/90] Expand anyOf support to also support oneOf (#1133) Signed-off-by: Lucian --- README.md | 16 +- playground/samples/index.js | 4 +- playground/samples/oneOf.js | 24 ++ .../{AnyOfField.js => MultiSchemaField.js} | 17 +- src/components/fields/SchemaField.js | 32 +- src/components/fields/index.js | 5 +- test/oneOf_test.js | 302 ++++++++++++++++++ 7 files changed, 369 insertions(+), 31 deletions(-) create mode 100644 playground/samples/oneOf.js rename src/components/fields/{AnyOfField.js => MultiSchemaField.js} (91%) create mode 100644 test/oneOf_test.js diff --git a/README.md b/README.md index e66becb86e..f5d8ffdda7 100644 --- a/README.md +++ b/README.md @@ -1792,8 +1792,6 @@ The JSON Schema standard says that the dependency is triggered if the property i In this example the user is prompted with different follow-up questions dynamically based on their answer to the first question. -Note that this is quite far from complete `oneOf` support! - 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. ## JSON Schema supporting status @@ -1803,13 +1801,9 @@ This component follows [JSON Schema](http://json-schema.org/documentation.html) * `additionalItems` keyword for arrays This keyword works when `items` is an array. `additionalItems: true` is not supported because there's no widget to represent an item of any type. In this case it will be treated as no additional items allowed. `additionalItems` being a valid schema is supported. * `anyOf`, `allOf`, and `oneOf`, or multiple `types` (i.e. `"type": ["string", "array"]` - The `anyOf` keyword is supported but has the following caveats: - - The `anyOf` keyword is not supported when used inside the `items` keyword - for arrays. - - Properties declared inside the `anyOf` should not overlap with properties - "outside" of the `anyOf`. + The `anyOf` and `oneOf` keywords are supported, however, properties declared inside the `anyOf/oneOf` should not overlap with properties "outside" of the `anyOf/oneOf`. - You can use `oneOf` with [schema dependencies](#schema-dependencies) to dynamically add schema properties based on input data but this feature does not bring general support for `oneOf` elsewhere in a schema. + You can also use `oneOf` with [schema dependencies](#schema-dependencies) to dynamically add schema properties based on input data. ## Tips and tricks @@ -1881,11 +1875,7 @@ $ git push --tags origin master ### Q: Does rjsf support `oneOf`, `anyOf`, multiple types in an array, etc.? -A: The `anyOf` keyword is supported but has the following caveats: - - The `anyOf` keyword is not supported when used inside the `items` keyword - for arrays. - - Properties declared inside the `anyOf` should not overlap with properties - "outside" of the `anyOf`. +A: The `anyOf` and `oneOf` keywords are supported, however, properties declared inside the `anyOf/oneOf` should not overlap with properties "outside" of the `anyOf/oneOf`. There is also special cased where you can use `oneOf` in [schema dependencies](#schema-dependencies), If you'd like to help improve support for these keywords, see the following issues for inspiration [#329](https://github.com/mozilla-services/react-jsonschema-form/pull/329) or [#417](https://github.com/mozilla-services/react-jsonschema-form/pull/417). See also: [#52](https://github.com/mozilla-services/react-jsonschema-form/issues/52), [#151](https://github.com/mozilla-services/react-jsonschema-form/issues/151), [#171](https://github.com/mozilla-services/react-jsonschema-form/issues/171), [#200](https://github.com/mozilla-services/react-jsonschema-form/issues/200), [#282](https://github.com/mozilla-services/react-jsonschema-form/issues/282), [#302](https://github.com/mozilla-services/react-jsonschema-form/pull/302), [#330](https://github.com/mozilla-services/react-jsonschema-form/issues/330), [#430](https://github.com/mozilla-services/react-jsonschema-form/issues/430), [#522](https://github.com/mozilla-services/react-jsonschema-form/issues/522), [#538](https://github.com/mozilla-services/react-jsonschema-form/issues/538), [#551](https://github.com/mozilla-services/react-jsonschema-form/issues/551), [#552](https://github.com/mozilla-services/react-jsonschema-form/issues/552), or [#648](https://github.com/mozilla-services/react-jsonschema-form/issues/648). ### Q: Will react-jsonschema-form support Material, Ant-Design, Foundation, or [some other specific widget library or frontend style]? diff --git a/playground/samples/index.js b/playground/samples/index.js index 8ad13741e4..4f5f9346e8 100644 --- a/playground/samples/index.js +++ b/playground/samples/index.js @@ -1,5 +1,6 @@ import arrays from "./arrays"; import anyOf from "./anyOf"; +import oneOf from "./oneOf"; import nested from "./nested"; import numbers from "./numbers"; import simple from "./simple"; @@ -41,5 +42,6 @@ export const samples = { "Property dependencies": propertyDependencies, "Schema dependencies": schemaDependencies, "Additional Properties": additionalProperties, - "Optional Forms": anyOf, + "Any Of": anyOf, + "One Of": oneOf, }; diff --git a/playground/samples/oneOf.js b/playground/samples/oneOf.js new file mode 100644 index 0000000000..4ed4004916 --- /dev/null +++ b/playground/samples/oneOf.js @@ -0,0 +1,24 @@ +module.exports = { + schema: { + type: "object", + oneOf: [ + { + properties: { + lorem: { + type: "string", + }, + }, + required: ["lorem"], + }, + { + properties: { + ipsum: { + type: "string", + }, + }, + required: ["ipsum"], + }, + ], + }, + formData: {}, +}; diff --git a/src/components/fields/AnyOfField.js b/src/components/fields/MultiSchemaField.js similarity index 91% rename from src/components/fields/AnyOfField.js rename to src/components/fields/MultiSchemaField.js index 52fbc74c28..e11b9434a9 100644 --- a/src/components/fields/AnyOfField.js +++ b/src/components/fields/MultiSchemaField.js @@ -8,17 +8,17 @@ class AnyOfField extends Component { constructor(props) { super(props); - const { formData, schema } = this.props; + const { formData, options } = this.props; this.state = { - selectedOption: this.getMatchingOption(formData, schema.anyOf), + selectedOption: this.getMatchingOption(formData, options), }; } componentWillReceiveProps(nextProps) { const matchingOption = this.getMatchingOption( nextProps.formData, - nextProps.schema.anyOf + nextProps.options ); if (matchingOption === this.state.selectedOption) { @@ -41,8 +41,7 @@ class AnyOfField extends Component { onOptionChange = event => { const selectedOption = parseInt(event.target.value, 10); - const { formData, onChange, schema } = this.props; - const options = schema.anyOf; + const { formData, onChange, options } = this.props; if (guessType(formData) === "object") { const newFormData = Object.assign({}, formData); @@ -73,6 +72,7 @@ class AnyOfField extends Component { render() { const { + baseType, disabled, errorSchema, formData, @@ -81,7 +81,7 @@ class AnyOfField extends Component { onBlur, onChange, onFocus, - schema, + options, registry, safeRenderCompletion, uiSchema, @@ -90,8 +90,6 @@ class AnyOfField extends Component { const _SchemaField = registry.fields.SchemaField; const { selectedOption } = this.state; - const baseType = schema.type; - const options = schema.anyOf || []; const option = options[selectedOption] || null; let optionSchema; @@ -151,7 +149,8 @@ AnyOfField.defaultProps = { if (process.env.NODE_ENV !== "production") { AnyOfField.propTypes = { - schema: PropTypes.object.isRequired, + options: PropTypes.arrayOf(PropTypes.object).isRequired, + baseType: PropTypes.string, uiSchema: PropTypes.object, idSchema: PropTypes.object, formData: PropTypes.any, diff --git a/src/components/fields/SchemaField.js b/src/components/fields/SchemaField.js index f55e8a030e..b0965678e4 100644 --- a/src/components/fields/SchemaField.js +++ b/src/components/fields/SchemaField.js @@ -39,9 +39,9 @@ function getFieldComponent(schema, uiSchema, idSchema, fields) { const componentName = COMPONENT_TYPES[getSchemaType(schema)]; - // If the type is not defined and the schema uses 'anyOf', don't render - // a field and let the AnyOfField component handle the form display - if (!componentName && schema.anyOf) { + // If the type is not defined and the schema uses 'anyOf' or 'oneOf', don't + // render a field and let the MultiSchemaField component handle the form display + if (!componentName && (schema.anyOf || schema.oneOf)) { return () => null; } @@ -330,14 +330,15 @@ function SchemaFieldRender(props) { }; const _AnyOfField = registry.fields.AnyOfField; + const _OneOfField = registry.fields.OneOfField; return ( {field} {/* - If the schema `anyOf` can be rendered as a select control, don't - render the `anyOf` selection and let `StringField` component handle + If the schema `anyOf` or 'oneOf' can be rendered as a select control, don't + render the selection and let `StringField` component handle rendering */} {schema.anyOf && !isSelect(schema) && ( @@ -350,7 +351,26 @@ function SchemaFieldRender(props) { onBlur={props.onBlur} onChange={props.onChange} onFocus={props.onFocus} - schema={schema} + options={schema.anyOf} + baseType={schema.type} + registry={registry} + safeRenderCompletion={props.safeRenderCompletion} + uiSchema={uiSchema} + /> + )} + + {schema.oneOf && !isSelect(schema) && ( + <_OneOfField + disabled={disabled} + errorSchema={errorSchema} + formData={formData} + idPrefix={idPrefix} + idSchema={idSchema} + onBlur={props.onBlur} + onChange={props.onChange} + onFocus={props.onFocus} + options={schema.oneOf} + baseType={schema.type} registry={registry} safeRenderCompletion={props.safeRenderCompletion} uiSchema={uiSchema} diff --git a/src/components/fields/index.js b/src/components/fields/index.js index 9337e209f1..758cb322ab 100644 --- a/src/components/fields/index.js +++ b/src/components/fields/index.js @@ -1,7 +1,7 @@ -import AnyOfField from "./AnyOfField"; import ArrayField from "./ArrayField"; import BooleanField from "./BooleanField"; import DescriptionField from "./DescriptionField"; +import MultiSchemaField from "./MultiSchemaField"; import NumberField from "./NumberField"; import ObjectField from "./ObjectField"; import SchemaField from "./SchemaField"; @@ -10,12 +10,13 @@ import TitleField from "./TitleField"; import UnsupportedField from "./UnsupportedField"; export default { - AnyOfField, + AnyOfField: MultiSchemaField, ArrayField, BooleanField, DescriptionField, NumberField, ObjectField, + OneOfField: MultiSchemaField, SchemaField, StringField, TitleField, diff --git a/test/oneOf_test.js b/test/oneOf_test.js new file mode 100644 index 0000000000..25fe39f97d --- /dev/null +++ b/test/oneOf_test.js @@ -0,0 +1,302 @@ +import React from "react"; +import { expect } from "chai"; +import { Simulate } from "react-addons-test-utils"; + +import { createFormComponent, createSandbox, setProps } from "./test_utils"; + +describe("oneOf", () => { + let sandbox; + + beforeEach(() => { + sandbox = createSandbox(); + }); + + afterEach(() => { + sandbox.restore(); + }); + + it("should not render a select element if the oneOf keyword is not present", () => { + const schema = { + type: "object", + properties: { + foo: { type: "string" }, + }, + }; + + const { node } = createFormComponent({ + schema, + }); + + expect(node.querySelectorAll("select")).to.have.length.of(0); + }); + + it("should render a select element if the oneOf keyword is present", () => { + const schema = { + type: "object", + oneOf: [ + { + properties: { + foo: { type: "string" }, + }, + }, + { + properties: { + bar: { type: "string" }, + }, + }, + ], + }; + + const { node } = createFormComponent({ + schema, + }); + + expect(node.querySelectorAll("select")).to.have.length.of(1); + }); + + it("should change the rendered form when the select value is changed", () => { + const schema = { + type: "object", + oneOf: [ + { + properties: { + foo: { type: "string" }, + }, + }, + { + properties: { + bar: { type: "string" }, + }, + }, + ], + }; + + const { node } = createFormComponent({ + schema, + }); + + expect(node.querySelectorAll("#root_foo")).to.have.length.of(1); + expect(node.querySelectorAll("#root_bar")).to.have.length.of(0); + + const $select = node.querySelector("select"); + + Simulate.change($select, { + target: { value: $select.options[1].value }, + }); + + expect(node.querySelectorAll("#root_foo")).to.have.length.of(0); + expect(node.querySelectorAll("#root_bar")).to.have.length.of(1); + }); + + it("should handle change events", () => { + const schema = { + type: "object", + oneOf: [ + { + properties: { + foo: { type: "string" }, + }, + }, + { + properties: { + bar: { type: "string" }, + }, + }, + ], + }; + + const { comp, node } = createFormComponent({ + schema, + }); + + Simulate.change(node.querySelector("input#root_foo"), { + target: { value: "Lorem ipsum dolor sit amet" }, + }); + + expect(comp.state.formData.foo).eql("Lorem ipsum dolor sit amet"); + }); + + it("should clear previous data when changing options", () => { + const schema = { + type: "object", + properties: { + buzz: { type: "string" }, + }, + oneOf: [ + { + properties: { + foo: { type: "string" }, + }, + }, + { + properties: { + bar: { type: "string" }, + }, + }, + ], + }; + + const { comp, node } = createFormComponent({ + schema, + }); + + Simulate.change(node.querySelector("input#root_buzz"), { + target: { value: "Lorem ipsum dolor sit amet" }, + }); + + Simulate.change(node.querySelector("input#root_foo"), { + target: { value: "Consectetur adipiscing elit" }, + }); + + expect(comp.state.formData.buzz).eql("Lorem ipsum dolor sit amet"); + expect(comp.state.formData.foo).eql("Consectetur adipiscing elit"); + + const $select = node.querySelector("select"); + + Simulate.change($select, { + target: { value: $select.options[1].value }, + }); + + expect(comp.state.formData.hasOwnProperty("foo")).to.be.false; + expect(comp.state.formData.buzz).eql("Lorem ipsum dolor sit amet"); + }); + + it("should support options with different types", () => { + const schema = { + type: "object", + properties: { + userId: { + oneOf: [ + { + type: "number", + }, + { + type: "string", + }, + ], + }, + }, + }; + + const { comp, node } = createFormComponent({ + schema, + }); + + Simulate.change(node.querySelector("input#root_userId"), { + target: { value: 12345 }, + }); + + expect(comp.state.formData).eql({ + userId: 12345, + }); + + const $select = node.querySelector("select"); + + Simulate.change($select, { + target: { value: $select.options[1].value }, + }); + + expect(comp.state.formData).eql({ + userId: undefined, + }); + + Simulate.change(node.querySelector("input#root_userId"), { + target: { value: "Lorem ipsum dolor sit amet" }, + }); + + expect(comp.state.formData).eql({ + userId: "Lorem ipsum dolor sit amet", + }); + }); + + it("should support custom fields", () => { + const schema = { + type: "object", + properties: { + userId: { + oneOf: [ + { + type: "number", + }, + { + type: "string", + }, + ], + }, + }, + }; + + const CustomField = () => { + return
    ; + }; + + const { node } = createFormComponent({ + schema, + fields: { + OneOfField: CustomField, + }, + }); + + expect(node.querySelectorAll("#custom-oneof-field")).to.have.length(1); + }); + + it("should select the correct field when the form is rendered from existing data", () => { + const schema = { + type: "object", + properties: { + userId: { + oneOf: [ + { + type: "number", + }, + { + type: "string", + }, + ], + }, + }, + }; + + const { node } = createFormComponent({ + schema, + formData: { + userId: "foobarbaz", + }, + }); + + expect(node.querySelector("select").value).eql("1"); + }); + + it("should select the correct field when the formData property is updated", () => { + const schema = { + type: "object", + properties: { + userId: { + oneOf: [ + { + type: "number", + }, + { + type: "string", + }, + ], + }, + }, + }; + + const { comp, node } = createFormComponent({ + schema, + }); + + expect(node.querySelector("select").value).eql("0"); + + setProps(comp, { + schema, + formData: { + userId: "foobarbaz", + }, + }); + + expect(node.querySelector("select").value).eql("1"); + }); +}); From 0a48b41a4b13d25e8b610500276fc4ef429d2b18 Mon Sep 17 00:00:00 2001 From: Ashwin Ramaswami Date: Fri, 18 Jan 2019 17:15:13 -0800 Subject: [PATCH 23/90] doc: fix spacing --- README.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/README.md b/README.md index f5d8ffdda7..2c248a518f 100644 --- a/README.md +++ b/README.md @@ -1799,8 +1799,11 @@ In these examples, the "Do you have any pets?" question is validated against the This component follows [JSON Schema](http://json-schema.org/documentation.html) specs. Due to the limitation of form widgets, there are some exceptions as follows: * `additionalItems` keyword for arrays + This keyword works when `items` is an array. `additionalItems: true` is not supported because there's no widget to represent an item of any type. In this case it will be treated as no additional items allowed. `additionalItems` being a valid schema is supported. + * `anyOf`, `allOf`, and `oneOf`, or multiple `types` (i.e. `"type": ["string", "array"]` + The `anyOf` and `oneOf` keywords are supported, however, properties declared inside the `anyOf/oneOf` should not overlap with properties "outside" of the `anyOf/oneOf`. You can also use `oneOf` with [schema dependencies](#schema-dependencies) to dynamically add schema properties based on input data. From 4a1f75486b94732111e570e5dc1e600e3a6130d3 Mon Sep 17 00:00:00 2001 From: Ashwin Ramaswami Date: Fri, 18 Jan 2019 17:59:43 -0800 Subject: [PATCH 24/90] doc: rename, fix typos for form disabled section --- README.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 2c248a518f..61b9ed9f75 100644 --- a/README.md +++ b/README.md @@ -60,7 +60,7 @@ A [live playground](https://mozilla-services.github.io/react-jsonschema-form/) i - [Field labels](#field-labels) - [HTML5 Input Types](#html5-input-types) - [Form attributes](#form-attributes) - - [Form disable](#form-disable) + - [Disabling a form](#disabling-a-form) - [Advanced customization](#advanced-customization) - [Field template](#field-template) - [Array Field Template](#array-field-template) @@ -903,9 +903,9 @@ The `Form` component supports the following html attributes: schema={} /> ``` -### Form disable +### Disabling a form -Its possible to disable the whole form by setting the `disabled` prop. The `disabled` prop is then forwarded down thru each field of the form. +It's possible to disable the whole form by setting the `disabled` prop. The `disabled` prop is then forwarded down to each field of the form. ```jsx
    ``` -If you just want to disable some of the fields see the `ui:disabled` parameter in the uiSchema directive. +If you just want to disable some of the fields, see the [`ui:disabled`](#disabled-fields) parameter in the `uiSchema` directive. ## Advanced customization From 98d7bc3522ee19ae908bdfa16a64d9d76df74335 Mon Sep 17 00:00:00 2001 From: Eric Lanehart Date: Sat, 19 Jan 2019 10:10:07 -0500 Subject: [PATCH 25/90] Fix custom boolean option labels using oneOf schemas (#1137) * Allow boolean option label overrides with oneOf schema * Add oneOf RadioWidget playground sample * Document oneOf RadioWidget usage * Allow boolean enum options to be re-ordered for consistency with oneOf behavior --- README.md | 43 ++++++++++++++ playground/samples/alternatives.js | 21 +++++++ src/components/fields/BooleanField.js | 25 ++++++-- test/BooleanField_test.js | 85 +++++++++++++++++++++++++++ 4 files changed, 170 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 61b9ed9f75..12e4260d00 100644 --- a/README.md +++ b/README.md @@ -659,6 +659,49 @@ This will be rendered as follows: ``` +This also works for radio buttons: + +```js +const schema = { + "type": "boolean", + "oneOf": [ + { + "const": true, + "title": "Yes" + }, + { + "const": false, + "title": "No" + } + ] +}; + +const uiSchema = { + "ui:widget": "radio" +}; +``` + +This will be rendered as follows: + +```html +
    +
    + +
    +
    + +
    +
    +``` + A live example of both approaches side-by-side can be found in the **Alternatives** tab of the [playground](https://mozilla-services.github.io/react-jsonschema-form/). ### Disabled attribute for `enum` fields diff --git a/playground/samples/alternatives.js b/playground/samples/alternatives.js index cfdbc6e441..fd1647cfaa 100644 --- a/playground/samples/alternatives.js +++ b/playground/samples/alternatives.js @@ -22,6 +22,20 @@ module.exports = { }, ], }, + Toggle: { + title: "Toggle", + type: "boolean", + oneOf: [ + { + title: "Enable", + const: true, + }, + { + title: "Disable", + const: false, + }, + ], + }, }, title: "Image editor", type: "object", @@ -39,6 +53,10 @@ module.exports = { }, title: "Color mask", }, + toggleMask: { + title: "Apply color mask", + $ref: "#/definitions/Toggle", + }, colorPalette: { type: "array", title: "Color palette", @@ -58,6 +76,9 @@ module.exports = { blendMode: { "ui:enumDisabled": ["multiply"], }, + toggleMask: { + "ui:widget": "radio", + }, }, formData: { currentColor: "#00ff00", diff --git a/src/components/fields/BooleanField.js b/src/components/fields/BooleanField.js index 7677a528ff..37f134ed2d 100644 --- a/src/components/fields/BooleanField.js +++ b/src/components/fields/BooleanField.js @@ -29,10 +29,27 @@ function BooleanField(props) { const { widgets, formContext } = registry; const { widget = "checkbox", ...options } = getUiOptions(uiSchema); const Widget = getWidget(schema, widget, widgets); - const enumOptions = optionsList({ - enum: [true, false], - enumNames: schema.enumNames || ["yes", "no"], - }); + + let enumOptions; + + if (Array.isArray(schema.oneOf)) { + enumOptions = optionsList({ + oneOf: schema.oneOf.map(option => ({ + ...option, + title: option.title || (option.const === true ? "yes" : "no"), + })), + }); + } else { + enumOptions = optionsList({ + enum: schema.enum || [true, false], + enumNames: + schema.enumNames || + (schema.enum && schema.enum[0] === false + ? ["no", "yes"] + : ["yes", "no"]), + }); + } + return ( { expect(node.querySelector(".field input").checked).eql(true); }); + it("should have default enum option labels for radio widgets", () => { + const { node } = createFormComponent({ + schema: { + type: "boolean", + }, + formData: true, + uiSchema: { "ui:widget": "radio" }, + }); + + const labels = [].map.call( + node.querySelectorAll(".field-radio-group label"), + label => label.textContent + ); + expect(labels).eql(["yes", "no"]); + }); + + it("should support enum option ordering for radio widgets", () => { + const { node } = createFormComponent({ + schema: { + type: "boolean", + enum: [false, true], + }, + formData: true, + uiSchema: { "ui:widget": "radio" }, + }); + + const labels = [].map.call( + node.querySelectorAll(".field-radio-group label"), + label => label.textContent + ); + expect(labels).eql(["no", "yes"]); + }); + it("should support enumNames for radio widgets", () => { const { node } = createFormComponent({ schema: { @@ -124,6 +157,58 @@ describe("BooleanField", () => { expect(labels).eql(["Yes", "No"]); }); + it("should support oneOf titles for radio widgets", () => { + const { node } = createFormComponent({ + schema: { + type: "boolean", + oneOf: [ + { + const: true, + title: "Yes", + }, + { + const: false, + title: "No", + }, + ], + }, + formData: true, + uiSchema: { "ui:widget": "radio" }, + }); + + const labels = [].map.call( + node.querySelectorAll(".field-radio-group label"), + label => label.textContent + ); + expect(labels).eql(["Yes", "No"]); + }); + + it("should preserve oneOf option ordering for radio widgets", () => { + const { node } = createFormComponent({ + schema: { + type: "boolean", + oneOf: [ + { + const: false, + title: "No", + }, + { + const: true, + title: "Yes", + }, + ], + }, + formData: true, + uiSchema: { "ui:widget": "radio" }, + }); + + const labels = [].map.call( + node.querySelectorAll(".field-radio-group label"), + label => label.textContent + ); + expect(labels).eql(["No", "Yes"]); + }); + it("should support inline radio widgets", () => { const { node } = createFormComponent({ schema: { type: "boolean" }, From c17f3bda344a3d1c084d6fbc463622c0e5e9feb4 Mon Sep 17 00:00:00 2001 From: mike391 Date: Sat, 19 Jan 2019 11:57:11 -0500 Subject: [PATCH 26/90] Add hidden widget for arrays (#978) --- src/utils.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/utils.js b/src/utils.js index 7deaa3edf7..9292942743 100644 --- a/src/utils.js +++ b/src/utils.js @@ -52,6 +52,7 @@ const widgetMap = { select: "SelectWidget", checkboxes: "CheckboxesWidget", files: "FileWidget", + hidden: "HiddenWidget", }, }; From 863912d4a4bdd985fc4e57411cbfe3054ce49b2a Mon Sep 17 00:00:00 2001 From: Ashwin Ramaswami Date: Sat, 19 Jan 2019 09:00:55 -0800 Subject: [PATCH 27/90] chore: fix package-lock version --- package-lock.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package-lock.json b/package-lock.json index 38b2ad0033..8396368a42 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "react-jsonschema-form", - "version": "1.0.5", + "version": "1.2.0", "lockfileVersion": 1, "requires": true, "dependencies": { From e4e67ad21f1106511d0070ea9eecd43f162bbafb Mon Sep 17 00:00:00 2001 From: Ashwin Ramaswami Date: Sat, 19 Jan 2019 09:01:10 -0800 Subject: [PATCH 28/90] test: add tests for rendering hidden object/array --- test/ArrayField_test.js | 10 ++++++++++ test/ObjectField_test.js | 10 ++++++++++ 2 files changed, 20 insertions(+) diff --git a/test/ArrayField_test.js b/test/ArrayField_test.js index 299b670904..d111af65af 100644 --- a/test/ArrayField_test.js +++ b/test/ArrayField_test.js @@ -64,6 +64,16 @@ describe("ArrayField", () => { expect(description.id).eql("root__description"); }); + it("should render a hidden list", () => { + const { node } = createFormComponent({ + schema, + uiSchema: { + "ui:widget": "hidden", + }, + }); + expect(node.querySelector("div.hidden > fieldset")).to.exist; + }); + it("should render a customized title", () => { const CustomTitleField = ({ title }) =>
    {title}
    ; diff --git a/test/ObjectField_test.js b/test/ObjectField_test.js index 2e115813d6..c9e83611b3 100644 --- a/test/ObjectField_test.js +++ b/test/ObjectField_test.js @@ -54,6 +54,16 @@ describe("ObjectField", () => { expect(legend.id).eql("root__title"); }); + it("should render a hidden object", () => { + const { node } = createFormComponent({ + schema, + uiSchema: { + "ui:widget": "hidden", + }, + }); + expect(node.querySelector("div.hidden > fieldset")).to.exist; + }); + it("should render a customized title", () => { const CustomTitleField = ({ title }) =>
    {title}
    ; From 7b87c82f12169a3c515e8d9fca12e80abac8f626 Mon Sep 17 00:00:00 2001 From: Ashwin Ramaswami Date: Sat, 19 Jan 2019 22:05:32 -0800 Subject: [PATCH 29/90] fix: populate id for radio boolean fields, fixes #1139 --- src/components/widgets/RadioWidget.js | 3 ++- test/BooleanField_test.js | 21 +++++++++++++++++++++ 2 files changed, 23 insertions(+), 1 deletion(-) diff --git a/src/components/widgets/RadioWidget.js b/src/components/widgets/RadioWidget.js index fbbb343342..962fc069bb 100644 --- a/src/components/widgets/RadioWidget.js +++ b/src/components/widgets/RadioWidget.js @@ -10,6 +10,7 @@ function RadioWidget(props) { readonly, autofocus, onChange, + id, } = props; // Generating a unique field name to identify this set of radio buttons const name = Math.random().toString(); @@ -17,7 +18,7 @@ function RadioWidget(props) { // checked={checked} has been moved above name={name}, As mentioned in #349; // this is a temporary fix for radio button rendering bug in React, facebook/react#7630. return ( -
    +
    {enumOptions.map((option, i) => { const checked = option.value === value; const itemDisabled = diff --git a/test/BooleanField_test.js b/test/BooleanField_test.js index fe90f7420a..57cfa11591 100644 --- a/test/BooleanField_test.js +++ b/test/BooleanField_test.js @@ -30,6 +30,16 @@ describe("BooleanField", () => { ).to.have.length.of(1); }); + it("should render a boolean field with the expected id", () => { + const { node } = createFormComponent({ + schema: { + type: "boolean", + }, + }); + + expect(node.querySelector(".field input[type=checkbox]").id).eql("root"); + }); + it("should render a boolean field with a label", () => { const { node } = createFormComponent({ schema: { @@ -107,6 +117,17 @@ describe("BooleanField", () => { expect(node.querySelector(".field input").checked).eql(true); }); + it("should render radio widgets with the expected id", () => { + const { node } = createFormComponent({ + schema: { + type: "boolean", + }, + uiSchema: { "ui:widget": "radio" }, + }); + + expect(node.querySelector(".field-radio-group").id).eql("root"); + }); + it("should have default enum option labels for radio widgets", () => { const { node } = createFormComponent({ schema: { From 75571baeb7bba6dfc7971931bd042e0436199ae1 Mon Sep 17 00:00:00 2001 From: jorgerobles Date: Sun, 20 Jan 2019 18:21:27 +0100 Subject: [PATCH 30/90] Add encodeUriComponent to file name (#967) Fix for non-standard file names (with spaces, apostrophes, etc) that prevent to be used. --- src/components/widgets/FileWidget.js | 2 +- test/StringField_test.js | 32 +++++++++++++++++++++++++++- 2 files changed, 32 insertions(+), 2 deletions(-) diff --git a/src/components/widgets/FileWidget.js b/src/components/widgets/FileWidget.js index ecd4d4aa0e..856dad00a3 100644 --- a/src/components/widgets/FileWidget.js +++ b/src/components/widgets/FileWidget.js @@ -4,7 +4,7 @@ import PropTypes from "prop-types"; import { dataURItoBlob, shouldRender, setState } from "../../utils"; function addNameToDataURL(dataURL, name) { - return dataURL.replace(";base64", `;name=${name};base64`); + return dataURL.replace(";base64", `;name=${encodeURIComponent(name)};base64`); } function processFile(file) { diff --git a/test/StringField_test.js b/test/StringField_test.js index 4a9625ca76..2846031cfd 100644 --- a/test/StringField_test.js +++ b/test/StringField_test.js @@ -1522,7 +1522,6 @@ describe("StringField", () => { default: initialValue, }, }); - expect(comp.state.formData).eql(initialValue); }); @@ -1554,6 +1553,37 @@ describe("StringField", () => { ); }); + it("should encode file name with encodeURIComponent", () => { + const nonUriEncodedValue = "fileáéí óú1.txt"; + const uriEncodedValue = "file%C3%A1%C3%A9%C3%AD%20%C3%B3%C3%BA1.txt"; + + sandbox.stub(window, "FileReader").returns({ + set onload(fn) { + fn({ target: { result: "data:text/plain;base64,x=" } }); + }, + readAsDataUrl() {}, + }); + + const { comp, node } = createFormComponent({ + schema: { + type: "string", + format: "data-url", + }, + }); + + Simulate.change(node.querySelector("[type=file]"), { + target: { + files: [{ name: nonUriEncodedValue, size: 1, type: "type" }], + }, + }); + + return new Promise(setImmediate).then(() => + expect(comp.state.formData).eql( + "data:text/plain;name=" + uriEncodedValue + ";base64,x=" + ) + ); + }); + it("should render the widget with the expected id", () => { const { node } = createFormComponent({ schema: { From 55c0261f24fd5b07fbc567230ebaa79729656b10 Mon Sep 17 00:00:00 2001 From: Ashwin Ramaswami Date: Sun, 20 Jan 2019 16:22:46 -0800 Subject: [PATCH 31/90] doc: fix typo --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 12e4260d00..1ceaef7c86 100644 --- a/README.md +++ b/README.md @@ -1677,7 +1677,7 @@ This library partially supports [inline schema definition dereferencing]( http:/ *(Sample schema courtesy of the [Space Telescope Science Institute](http://spacetelescope.github.io/understanding-json-schema/structuring.html))* -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. +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. ## Property dependencies From cf463421fc671146eea377b00608949837bc5890 Mon Sep 17 00:00:00 2001 From: Ashwin Ramaswami Date: Sun, 20 Jan 2019 17:20:08 -0800 Subject: [PATCH 32/90] #556 - handle recursive references to deep schema definitions (#1142) * #556 - handle recursive references to deep schema definitions * test: add tests for recursive references * fix: handle multiple recursive references to deep schema definitions --- src/utils.js | 3 ++ test/Form_test.js | 86 +++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 89 insertions(+) diff --git a/src/utils.js b/src/utils.js index 9292942743..8c69cccae8 100644 --- a/src/utils.js +++ b/src/utils.js @@ -394,6 +394,9 @@ function findSchemaDefinition($ref, definitions = {}) { let current = definitions; for (let part of parts) { part = part.replace(/~1/g, "/").replace(/~0/g, "~"); + while (current.hasOwnProperty("$ref")) { + current = findSchemaDefinition(current.$ref, definitions); + } if (current.hasOwnProperty(part)) { current = current[part]; } else { diff --git a/test/Form_test.js b/test/Form_test.js index 793e3a0758..23b6ea8169 100644 --- a/test/Form_test.js +++ b/test/Form_test.js @@ -541,6 +541,92 @@ describe("Form", () => { expect(node.querySelector("#root_children_0_name")).to.exist; }); + it("should follow recursive references", () => { + const schema = { + definitions: { + bar: { $ref: "#/definitions/qux" }, + qux: { type: "string" }, + }, + type: "object", + required: ["foo"], + properties: { + foo: { $ref: "#/definitions/bar" }, + }, + }; + const { node } = createFormComponent({ schema }); + + expect(node.querySelectorAll("input[type=text]")).to.have.length.of(1); + }); + + it("should follow multiple recursive references", () => { + const schema = { + definitions: { + bar: { $ref: "#/definitions/bar2" }, + bar2: { $ref: "#/definitions/qux" }, + qux: { type: "string" }, + }, + type: "object", + required: ["foo"], + properties: { + foo: { $ref: "#/definitions/bar" }, + }, + }; + const { node } = createFormComponent({ schema }); + + expect(node.querySelectorAll("input[type=text]")).to.have.length.of(1); + }); + + it("should handle recursive references to deep schema definitions", () => { + const schema = { + definitions: { + testdef: { + $ref: "#/definitions/testdefref", + }, + testdefref: { + type: "object", + properties: { + bar: { type: "string" }, + }, + }, + }, + type: "object", + properties: { + foo: { $ref: "#/definitions/testdef/properties/bar" }, + }, + }; + + const { node } = createFormComponent({ schema }); + + expect(node.querySelectorAll("input[type=text]")).to.have.length.of(1); + }); + + it("should handle multiple recursive references to deep schema definitions", () => { + const schema = { + definitions: { + testdef: { + $ref: "#/definitions/testdefref1", + }, + testdefref1: { + $ref: "#/definitions/testdefref2", + }, + testdefref2: { + type: "object", + properties: { + bar: { type: "string" }, + }, + }, + }, + type: "object", + properties: { + foo: { $ref: "#/definitions/testdef/properties/bar" }, + }, + }; + + const { node } = createFormComponent({ schema }); + + expect(node.querySelectorAll("input[type=text]")).to.have.length.of(1); + }); + it("should priorize definition over schema type property", () => { // Refs bug #140 const schema = { From c43e5044e85211ea8719f0f17e037a3919a38aff Mon Sep 17 00:00:00 2001 From: Ashwin Ramaswami Date: Sun, 20 Jan 2019 17:22:50 -0800 Subject: [PATCH 33/90] chore: add a npm test-debug command for ease of use --- package.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/package.json b/package.json index 79ecf38dc3..6f39ce7d75 100644 --- a/package.json +++ b/package.json @@ -18,7 +18,8 @@ "preversion": "npm run build:playground && npm run dist && npm run build:readme && npm run cs-check && npm run lint", "start": "node devServer.js", "tdd": "cross-env NODE_ENV=test mocha --require babel-register --watch --require ./test/setup-jsdom.js test/**/*_test.js", - "test": "cross-env NODE_ENV=test mocha --require babel-register --require ./test/setup-jsdom.js test/**/*_test.js" + "test": "cross-env NODE_ENV=test mocha --require babel-register --require ./test/setup-jsdom.js test/**/*_test.js", + "test-debug": "cross-env NODE_ENV=test mocha --require babel-register --require ./test/setup-jsdom.js --debug-brk --inspect test/Form_test.js" }, "prettierOptions": "--jsx-bracket-same-line --trailing-comma es5 --semi --tab-width 2", "lint-staged": { From 3606c7c3602f8ad89ab085a5c6cd61f666e353d3 Mon Sep 17 00:00:00 2001 From: Ashwin Ramaswami Date: Sun, 20 Jan 2019 20:39:58 -0800 Subject: [PATCH 34/90] fix: fix build status travis ci badge --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 1ceaef7c86..13b28085f9 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ react-jsonschema-form ===================== -[![Build Status](https://travis-ci.org/mozilla-services/react-jsonschema-form.svg)](https://travis-ci.org/mozilla-services/react-jsonschema-form) +[![Build Status](https://travis-ci.org/mozilla-services/react-jsonschema-form.svg?branch=master)](https://travis-ci.org/mozilla-services/react-jsonschema-form) A simple [React](http://facebook.github.io/react/) component capable of building HTML forms out of a [JSON schema](http://json-schema.org/) and using [Bootstrap](http://getbootstrap.com/) semantics by default. From 0fd711e0aa582131e110900fd47e98de86de5727 Mon Sep 17 00:00:00 2001 From: Ashwin Ramaswami Date: Sun, 20 Jan 2019 21:43:27 -0800 Subject: [PATCH 35/90] docs: add docs using mkdocs --- docs/index.md | 1835 +++++++++++++++++++++++++++++++++++++++++++++++++ mkdocs.yml | 1 + 2 files changed, 1836 insertions(+) create mode 100644 docs/index.md create mode 100644 mkdocs.yml diff --git a/docs/index.md b/docs/index.md new file mode 100644 index 0000000000..91d6c61de2 --- /dev/null +++ b/docs/index.md @@ -0,0 +1,1835 @@ +react-jsonschema-form +===================== + +[![Build Status](https://travis-ci.org/mozilla-services/react-jsonschema-form.svg)](https://travis-ci.org/mozilla-services/react-jsonschema-form) + +A simple [React](http://facebook.github.io/react/) component capable of building HTML forms out of a [JSON schema](http://json-schema.org/) and using [Bootstrap](http://getbootstrap.com/) semantics by default. + +A [live playground](https://mozilla-services.github.io/react-jsonschema-form/) is hosted on gh-pages. + +![Image](https://i.imgur.com/M8ZCES5.gif) + +## Philosophy + +react-jsonschema-form is meant to automatically generate a React form based on a [JSON Schema](http://json-schema.org/). It is a major component in the [kinto-admin](https://github.com/Kinto/kinto-admin/) project. If you want to generate a form for any data, sight unseen, simply given a JSON schema, react-jsonschema-form may be for you. If you have _a priori_ knowledge of your data and want a toolkit for generating forms for it, you might look elsewhere. + +react-jsonschema-form validates that the data conforms to the given schema, but doesn't prevent the user from inputing data that doesn't fit (for example, stripping non-numbers from a number field, or adding values to an array that is already "full"). + +## Installation + +Requires React 15.0.0+. + +> Note: The `master` branch of the repository reflects ongoing development. Releases are published as [tags](https://github.com/mozilla-services/react-jsonschema-form/releases). You should never blindly install from `master`, but rather check what the available stable releases are. + + +### As a npm-based project dependency + +``` +$ npm install react-jsonschema-form --save +``` + +> Note: While the library renders [Bootstrap](http://getbootstrap.com/) HTML semantics, you have to build and load the Bootstrap styles on your own. + +### As a script served from a CDN + +```html + +``` + +Source maps are available at [this url](https://unpkg.com/react-jsonschema-form/dist/react-jsonschema-form.js.map). + +> Note: The CDN version **does not** embed `react` or `react-dom`. + +You'll also need to alias the default export property to use the Form component: + +```jsx +const Form = JSONSchemaForm.default; +// or +const {default: Form} = JSONSchemaForm; +``` + +## Usage + +```jsx +import React, { Component } from "react"; +import { render } from "react-dom"; + +import Form from "react-jsonschema-form"; + +const schema = { + title: "Todo", + type: "object", + required: ["title"], + properties: { + title: {type: "string", title: "Title", default: "A new task"}, + done: {type: "boolean", title: "Done?", default: false} + } +}; + +const log = (type) => console.log.bind(console, type); + +render(( + +), document.getElementById("app")); +``` + +This will generate a form like this (assuming you loaded the standard [Bootstrap](http://getbootstrap.com/) stylesheet): + +![](https://i.imgur.com/DZQYPyu.png) + +### Form initialization + +Often you'll want to prefill a form with existing data; this is done by passing a `formData` prop object matching the schema: + +```jsx +const formData = { + title: "First task", + done: true +}; + +render(( + +), document.getElementById("app")); +``` + +> Note: If your form has a single field, pass a single value to `formData`. ex: `formData='Charlie'` + +> WARNING: If you have situations where your parent component can re-render, make sure you listen to the `onChange` event and update the data you pass to the `formData` attribute. + +### Form event handlers + +#### Form submission + +You can pass a function as the `onSubmit` prop of your `Form` component to listen to when the form is submitted and its data are valid. It will be passed a result object having a `formData` attribute, which is the valid form data you're usually after: + +```js +const onSubmit = ({formData}) => console.log("Data submitted: ", formData); + +render(( + +), document.getElementById("app")); +``` + +#### Form error event handler + +To react when submitted form data are invalid, pass an `onError` handler. It will be passed the list of encountered errors: + +```js +const onError = (errors) => console.log("I have", errors.length, "errors to fix"); + +render(( + +), document.getElementById("app")); +``` + +#### Form data changes + +If you plan on being notified every time the form data are updated, you can pass an `onChange` handler, which will receive the same args as `onSubmit` any time a value is updated in the form. + +#### Form field blur events + +Sometimes you may want to trigger events or modify external state when a field has been touched, so you can pass an `onBlur` handler, which will receive the id of the input that was blurred and the field value. + +#### Form field focus events + +Sometimes you may want to trigger events or modify external state when a field has been focused, so you can pass an `onFocus` handler, which will receive the id of the input that is focused and the field value. + +### Submit form programmatically +You can use the reference to get your `Form` component and call the `submit` method to submit the form programmatically without a submit button. +This method will dispatch the `submit` event of the form, and the function, that is passed to `onSubmit` props, will be called. + +```js +const onSubmit = ({formData}) => console.log("Data submitted: ", formData); +let yourForm; + +render(( + {yourForm = form;}}/> +), document.getElementById("app")); + +yourForm.submit(); +``` + +## Form customization + +### The `uiSchema` object + +JSONSchema is limited for describing how a given data type should be rendered as a form input component. That's why this lib introduces the concept of *UI schema*. + +A UI schema is basically an object literal providing information on **how** the form should be rendered, while the JSON schema tells **what**. + +The uiSchema object follows the tree structure of the form field hierarchy, and defines how each property should be rendered: + +```js +const schema = { + type: "object", + properties: { + foo: { + type: "object", + properties: { + bar: {type: "string"} + } + }, + baz: { + type: "array", + items: { + type: "object", + properties: { + description: { + "type": "string" + } + } + } + } + } +} + +const uiSchema = { + foo: { + bar: { + "ui:widget": "textarea" + }, + }, + baz: { + // note the "items" for an array + items: { + description: { + "ui:widget": "textarea" + } + } + } +} + +render(( + +), document.getElementById("app")); +``` + +### Alternative widgets + +The uiSchema `ui:widget` property tells the form which UI widget should be used to render a field. + +Example: + +```jsx +const uiSchema =  { + done: { + "ui:widget": "radio" // could also be "select" + } +}; + +render(( + +), document.getElementById("app")); +``` + +Here's a list of supported alternative widgets for different JSONSchema data types: + +#### For `boolean` fields + + * `radio`: a radio button group with `true` and `false` as selectable values; + * `select`: a select box with `true` and `false` as options; + * by default, a checkbox is used + +> Note: To set the labels for a boolean field, instead of using `true` and `false` you can set `enumNames` in your schema. Note that `enumNames` belongs in your `schema`, not the `uiSchema`, and the order is always `[true, false]`. + +#### For `string` fields + + * `textarea`: a `textarea` element is used; + * `password`: an `input[type=password]` element is used; + * `color`: an `input[type=color]` element is used; + * by default, a regular `input[type=text]` element is used. + +##### String formats + +The built-in string field also supports the JSONSchema `format` property, and will render an appropriate widget by default for the following string formats: + +- `email`: An `input[type=email]` element is used; +- `uri`: An `input[type=url]` element is used; +- `data-url`: By default, an `input[type=file]` element is used; in case the string is part of an array, multiple files will be handled automatically (see [File widgets](#file-widgets)). +- `date`: By default, an `input[type=date]` element is used; +- `date-time`: By default, an `input[type=datetime-local]` element is used. + +![](https://i.imgur.com/xqu6Lcp.png) + +Please note that, even though they are standardized, `datetime-local` and `date` input elements are not yet supported by Firefox and IE. If you plan on targeting these platforms, two alternative widgets are available: + +- `alt-datetime`: Six `select` elements are used to select the year, the month, the day, the hour, the minute and the second; +- `alt-date`: Three `select` elements are used to select the year, month and the day. + +> **Firefox 57 - 66**: Firefox partially supporting `date` and `time` input types, but not `datetime-local`, `month` or `week` + +![](https://i.imgur.com/VF5tY60.png) + +You can customize the list of years displayed in the `year` dropdown by providing a ``yearsRange`` property to ``ui:options`` in your uiSchema. Its also possible to remove the `Now` and `Clear` buttons with the `hideNowButton` and `hideClearButton` options. + +```jsx +uiSchema: { + a_date: { + "alt-datetime": { + "ui:widget": "alt-datetime", + "ui:options": { + yearsRange: [1980, 2030], + hideNowButton: true, + hideClearButton: true, + }, + }, + }, +}, +``` + +#### For `number` and `integer` fields + + * `updown`: an `input[type=number]` updown selector; + * `range`: an `input[type=range]` slider; + * `radio`: a radio button group with enum values. This can only be used when `enum` values are specified for this input. + * By default, a regular `input[type=text]` element is used. + +> Note: If JSONSchema's `minimum`, `maximum` and `multipleOf` values are defined, the `min`, `max` and `step` input attributes values will take those values. + +#### Disabled fields + +The `ui:disabled` uiSchema directive will disable all child widgets from a given field. + +#### Read-only fields + +The `ui:readonly` uiSchema directive will mark all child widgets from a given field as read-only. + +> Note: If you're wondering about the difference between a `disabled` field and a `readonly` one: Marking a field as read-only will render it greyed out, but its text value will be selectable. Disabling it will prevent its value to be selected at all. + +#### Hidden widgets + +It's possible to use a hidden widget for a field by setting its `ui:widget` uiSchema directive to `hidden`: + +```js +const schema = { + type: "object", + properties: { + foo: {type: "boolean"} + } +}; + +const uiSchema = { + foo: {"ui:widget": "hidden"} +}; +``` + +Notes: + + - Hiding widgets is only supported for `boolean`, `string`, `number` and `integer` schema types; + - A hidden widget takes its value from the `formData` prop. + +#### File widgets + +This library supports a limited form of `input[type=file]` widgets, in the sense that it will propagate file contents to form data state as [data-url](http://dataurl.net/#about)s. + +There are two ways to use file widgets. + +1. By declaring a `string` json schema type along a `data-url` [format](#string-formats): +```js +const schema = { + type: "string", + format: "data-url", +}; +``` + +2. By specifying a `ui:widget` field uiSchema directive as `file`: +```js +const schema = { + type: "string", +}; + +const uiSchema = { + "ui:widget": "file", +}; +``` + +##### Multiple files + +Multiple files selectors are supported by defining an array of strings having `data-url` as a format: + +```js +const schema = { + type: "array", + items: { + type: "string", + format: "data-url", + } +}; +``` + +> Note that storing large dataURIs into form state might slow rendering. + +##### File widget input ref + +The included `FileWidget` exposes a reference to the `` element node as an `inputRef` component property. + +This allows you to programmatically trigger the browser's file selector, which can be used in a custom file widget. + +### Object fields ordering + +Since the order of object properties in Javascript and JSON is not guaranteed, the `uiSchema` object spec allows you to define the order in which properties are rendered using the `ui:order` property: + +```jsx +const schema = { + type: "object", + properties: { + foo: {type: "string"}, + bar: {type: "string"} + } +}; + +const uiSchema = { + "ui:order": ["bar", "foo"] +}; + +render(( + +), document.getElementById("app")); +``` + +If a guaranteed fixed order is only important for some fields, you can insert a wildcard `"*"` item in your `ui:order` definition. All fields that are not referenced explicitly anywhere in the list will be rendered at that point: + +```js +const uiSchema = { + "ui:order": ["bar", "*"] +}; +``` + +### Object item options + +#### `expandable` option + +If `additionalProperties` contains a schema object, an add button for new properties is shown by default. The UX for editing properties whose names are user-defined is still experimental. + +You can turn support for `additionalProperties` off with the `expandable` option in `uiSchema`: + +```jsx +const uiSchema = { + "ui:options": { + expandable: false + } +}; +``` + +### Array item options + +#### `orderable` option + +Array items are orderable by default, and react-jsonschema-form renders move up/down buttons alongside them. The `uiSchema` object spec allows you to disable ordering: + +```jsx +const schema = { + type: "array", + items: { + type: "string" + } +}; + +const uiSchema = { + "ui:options": { + orderable: false + } +}; +``` + +#### `addable` option + +If either `items` or `additionalItems` contains a schema object, an add button for new items is shown by default. You can turn this off with the `addable` option in `uiSchema`: + +```jsx +const uiSchema = { + "ui:options": { + addable: false + } +}; +``` + +#### `removable` option + +A remove button is shown by default for an item if `items` contains a schema object, or the item is an `additionalItems` instance. You can turn this off with the `removable` option in `uiSchema`: + +```jsx +const uiSchema = { + "ui:options": { + removable: false + } +}; +``` + +### Custom CSS class names + +The uiSchema object accepts a `classNames` property for each field of the schema: + +```jsx +const uiSchema = { + title: { + classNames: "task-title foo-bar" + } +}; +``` + +Will result in: + +```html +
    + +
    +``` + +### Custom labels for `enum` fields + +This library supports the [`enumNames`](https://github.com/json-schema/json-schema/wiki/enumNames-%28v5-proposal%29) property for `enum` fields, which allows defining custom labels for each option of an `enum`: + +```js +const schema = { + type: "number", + enum: [1, 2, 3], + enumNames: ["one", "two", "three"] +}; +``` + +This will be rendered using a select box like this: + +```html + +``` + +Note that string representations of numbers will be cast back and reflected as actual numbers into form state. + +#### Alternative JSON-Schema compliant approach + +JSON Schema has an alternative approach to enumerations; react-jsonschema-form supports it as well. + +```js +const schema = { + "type": "number", + "anyOf": [ + { + "type": "number", + "title": "one", + "enum": [ + 1 + ] + }, + { + "type": "number", + "title": "two", + "enum": [ + 2 + ] + }, + { + "type": "number", + "title": "three", + "enum": [ + 3 + ] + } + ] +}; +``` + +This will be rendered as follows: + +```html + +``` + +This also works for radio buttons: + +```js +const schema = { + "type": "boolean", + "oneOf": [ + { + "const": true, + "title": "Yes" + }, + { + "const": false, + "title": "No" + } + ] +}; + +const uiSchema = { + "ui:widget": "radio" +}; +``` + +This will be rendered as follows: + +```html +
    +
    + +
    +
    + +
    +
    +``` + +A live example of both approaches side-by-side can be found in the **Alternatives** tab of the [playground](https://mozilla-services.github.io/react-jsonschema-form/). + +### Disabled attribute for `enum` fields + +To disable an option, use the `enumDisabled` property in uiSchema. + +```js +const schema = { + type: "string", + enum: ["one", "two", "three"], +}; + +const uiSchema={ + "ui:enumDisabled": ['two'], +} +``` + +This will be rendered using a select box as follows: + +```html + +``` + +### Multiple-choice list + +The default behavior for array fields is a list of text inputs with add/remove buttons. There are two alternative widgets for picking multiple elements from a list of choices. Typically this applies when a schema has an `enum` list for the `items` property of an `array` field, and the `uniqueItems` property set to `true`. + +Example: + +```js +const schema = { + type: "array", + title: "A multiple-choice list", + items: { + type: "string", + enum: ["foo", "bar", "fuzz", "qux"], + }, + uniqueItems: true +}; +``` + +By default, this will render a multiple select box. If you prefer a list of checkboxes, just set the uiSchema `ui:widget` directive to `checkboxes` for that field: + +```js +const uiSchema = { + "ui:widget": "checkboxes" +}; +``` + +Note that when an array property is marked as `required`, an empty array is considered valid. If array needs to be populated, you can specify the minimum number of items using the `minItems` property. + +Example: + +```js +const schema = { + type: "array", + minItems: 2, + title: "A multiple-choice list", + items: { + type: "string", + enum: ["foo", "bar", "fuzz", "qux"], + }, + uniqueItems: true +}; +``` + +By default, checkboxes are stacked. If you prefer them inline, set the `inline` property to `true`: + +```js +const uiSchema = { + "ui:widget": "checkboxes", + "ui:options": { + inline: true + } +}; +``` + +See the "Arrays" section of the [playground](https://mozilla-services.github.io/react-jsonschema-form/) for cool demos. + +### Autogenerated widget ids + +By default, this library will generate ids unique to the form for all rendered widgets. If you plan on using multiple instances of the `Form` component in a same page, it's wise to declare a root prefix for these, using the `ui:rootFieldId` uiSchema directive: + +```js +const uiSchema = { + "ui:rootFieldId": "myform" +}; +``` + +So all widgets will have an id prefixed with `myform`. + +### Form action buttons + +You can provide custom buttons to your form via the `Form` component's `children`. Otherwise a default submit button will be rendered. + +```jsx +render(( + +
    + + +
    + +), document.getElementById("app")); +``` + +> **Warning:** There needs to be a button or an input with `type="submit"` to trigger the form submission (and then the form validation). + +### Help text + +Sometimes it's convenient to add text next to a field to guide the end user filling it. This is the purpose of the `ui:help` uiSchema directive: + +```js +const schema = {type: "string"}; +const uiSchema = { + "ui:widget": "password", + "ui:help": "Hint: Make it strong!" +}; +``` + +![](https://i.imgur.com/scJUuZo.png) + +Help texts work for any kind of field at any level, and will always be rendered immediately below the field component widget(s) (after contextualized errors, if any). + +### Title texts + +Sometimes it's convenient to change a field's title. this is the purpose of the `ui:title` uiSchema directive: + +```js +const schema = {type: "string"}; +const uiSchema = { + "ui:widget": "password", + "ui:title": "Your password" +}; +``` + +### Description texts + +Sometimes it's convenient to change description a field. This is the purpose of the `ui:description` uiSchema directive: + +```js +const schema = {type: "string"}; +const uiSchema = { + "ui:widget": "password", + "ui:description": "The best password" +}; +``` + +### Auto focus + +If you want to automatically focus on a text input or textarea input, set the `ui:autofocus` uiSchema directive to `true`. + +```js +const schema = {type: "string"}; +const uiSchema = { + "ui:widget": "textarea", + "ui:autofocus": true +} +``` + +### Textarea `rows` option + +You can set the initial height of a textarea widget by specifying `rows` option. + +```js +const schema = {type: "string"}; +const uiSchema = { + "ui:widget": "textarea", + "ui:options": { + rows: 15 + } +} +``` + +### Placeholders + +You can add placeholder text to an input by using the `ui:placeholder` uiSchema directive: + +```jsx +const schema = {type: "string", format: "uri"}; +const uiSchema = { + "ui:placeholder": "http://" +}; +``` + +![](https://i.imgur.com/MbHypKg.png) + +Fields using `enum` can also use `ui:placeholder`. The value will be used as the text for the empty option in the select widget. + +```jsx +const schema = {type: "string", enum: ["First", "Second"]}; +const uiSchema = { + "ui:placeholder": "Choose an option" +}; +``` + +### Field labels + +Field labels are rendered by default. Labels may be omitted by setting the `label` option to `false` in the `ui:options` uiSchema directive. + +```jsx +const schema = {type: "string"}; +const uiSchema = { + "ui:options": { + label: false + } +}; +``` + +### HTML5 Input Types + +To change the input type (for example, `tel` or `email`) you can specify the `inputType` in the `ui:options` uiSchema directive. + +```jsx +const schema = {type: "string"}; +const uiSchema = { + "ui:options": { + inputType: 'tel' + } +}; +``` + +### Form attributes + +The `Form` component supports the following html attributes: + +```jsx +
    +``` + +### Disabling a form + +It's possible to disable the whole form by setting the `disabled` prop. The `disabled` prop is then forwarded down to each field of the form. + +```jsx + +``` + +If you just want to disable some of the fields, see the [`ui:disabled`](#disabled-fields) parameter in the `uiSchema` directive. + +## Advanced customization + + +_ | Custom Field | Custom Template | Custom Widget +--|---------- | ------------- | ---- +What it does | Overrides all behaviour | Overrides just the layout | Overrides just the input box (not layout, labels, or help, or validation) +Usage | Global or per-field | Only global | Global or per-field +Global Example | `` | `` | `` +Per-Field Example | `"ui:field": MyField` | N/A | `"ui:widget":MyWidget` +Documentation | [Field](#field-props) | [Field Template](#field-template) - [Array Template](#array-field-template) - [Object Template](#object-field-template) - [Error List Template](#error-list-template) | [Custom Widgets](#custom-widget-components) + +### Field template + +To take control over the inner organization of each field (each form row), you can define a *field template* for your form. + +A field template is basically a React stateless component being passed field-related props, allowing you to structure your form row as you like. + +```jsx +function CustomFieldTemplate(props) { + const {id, classNames, label, help, required, description, errors, children} = props; + return ( +
    + + {description} + {children} + {errors} + {help} +
    + ); +} + +render(( + , +), document.getElementById("app")); +``` + +If you want to handle the rendering of each element yourself, you can use the props `rawHelp`, `rawDescription` and `rawErrors`. + +The following props are passed to a custom field template component: + +- `id`: The id of the field in the hierarchy. You can use it to render a label targeting the wrapped widget. +- `classNames`: A string containing the base Bootstrap CSS classes, merged with any [custom ones](#custom-css-class-names) defined in your uiSchema. +- `label`: The computed label for this field, as a string. +- `description`: A component instance rendering the field description, if one is defined (this will use any [custom `DescriptionField`](#custom-descriptions) defined). +- `rawDescription`: A string containing any `ui:description` uiSchema directive defined. +- `children`: The field or widget component instance for this field row. +- `errors`: A component instance listing any encountered errors for this field. +- `rawErrors`: An array of strings listing all generated error messages from encountered errors for this field. +- `help`: A component instance rendering any `ui:help` uiSchema directive defined. +- `rawHelp`: A string containing any `ui:help` uiSchema directive defined. **NOTE:** `rawHelp` will be `undefined` if passed `ui:help` is a React component instead of a string. +- `hidden`: A boolean value stating if the field should be hidden. +- `required`: A boolean value stating if the field is required. +- `readonly`: A boolean value stating if the field is read-only. +- `disabled`: A boolean value stating if the field is disabled. +- `displayLabel`: A boolean value stating if the label should be rendered or not. This is useful for nested fields in arrays where you don't want to clutter the UI. +- `fields`: An array containing all Form's fields including your [custom fields](#custom-field-components) and the built-in fields. +- `schema`: The schema object for this field. +- `uiSchema`: The uiSchema object for this field. +- `formContext`: The `formContext` object that you passed to Form. + +> Note: you can only define a single field template for a form. If you need many, it's probably time to look at [custom fields](#custom-field-components) instead. + +### Array Field Template + +Similarly to the `FieldTemplate` you can use an `ArrayFieldTemplate` to customize how your +arrays are rendered. This allows you to customize your array, and each element in the array. + +```jsx +function ArrayFieldTemplate(props) { + return ( +
    + {props.items.map(element => element.children)} + {props.canAdd && } +
    + ); +} + +render(( + , +), document.getElementById("app")); +``` + +Please see [customArray.js](https://github.com/mozilla-services/react-jsonschema-form/blob/master/playground/samples/customArray.js) for a better example. + +The following props are passed to each `ArrayFieldTemplate`: + +- `DescriptionField`: The `DescriptionField` from the registry (in case you wanted to utilize it) +- `TitleField`: The `TitleField` from the registry (in case you wanted to utilize it). +- `canAdd`: A boolean value stating whether new elements can be added to the array. +- `className`: The className string. +- `disabled`: A boolean value stating if the array is disabled. +- `idSchema`: Object +- `items`: An array of objects representing the items in the array. Each of the items represent a child with properties described below. +- `onAddClick: (event) => void`: A function that adds a new item to the array. +- `readonly`: A boolean value stating if the array is read-only. +- `required`: A boolean value stating if the array is required. +- `schema`: The schema object for this array. +- `uiSchema`: The uiSchema object for this array field. +- `title`: A string value containing the title for the array. +- `formContext`: The `formContext` object that you passed to Form. +- `formData`: The formData for this array. + +The following props are part of each element in `items`: + +- `children`: The html for the item's content. +- `className`: The className string. +- `disabled`: A boolean value stating if the array item is disabled. +- `hasMoveDown`: A boolean value stating whether the array item can be moved down. +- `hasMoveUp`: A boolean value stating whether the array item can be moved up. +- `hasRemove`: A boolean value stating whether the array item can be removed. +- `hasToolbar`: A boolean value stating whether the array item has a toolbar. +- `index`: A number stating the index the array item occurs in `items`. +- `onDropIndexClick: (index) => (event) => void`: Returns a function that removes the item at `index`. +- `onReorderClick: (index, newIndex) => (event) => void`: Returns a function that swaps the items at `index` with `newIndex`. +- `readonly`: A boolean value stating if the array item is read-only. + +### Object Field Template + +Similarly to the `FieldTemplate` you can use an `ObjectFieldTemplate` to customize how your +objects are rendered. + +```jsx +function ObjectFieldTemplate(props) { + return ( +
    + {props.title} + {props.description} + {props.properties.map(element =>
    {element.content}
    )} +
    + ); +} + +render(( + , +), document.getElementById("app")); +``` + +Please see [customObject.js](https://github.com/mozilla-services/react-jsonschema-form/blob/master/playground/samples/customObject.js) for a better example. + +The following props are passed to each `ObjectFieldTemplate`: + +- `DescriptionField`: The `DescriptionField` from the registry (in case you wanted to utilize it) +- `TitleField`: The `TitleField` from the registry (in case you wanted to utilize it). +- `title`: A string value containing the title for the object. +- `description`: A string value containing the description for the object. +- `disabled`: A boolean value stating if the object is disabled. +- `properties`: An array of object representing the properties in the array. Each of the properties represent a child with properties described below. +- `readonly`: A boolean value stating if the object is read-only. +- `required`: A boolean value stating if the object is required. +- `schema`: The schema object for this object. +- `uiSchema`: The uiSchema object for this object field. +- `idSchema`: An object containing the id for this object & ids for it's properties. +- `formData`: The form data for the object. +- `formContext`: The `formContext` object that you passed to Form. + +The following props are part of each element in `properties`: + +- `content`: The html for the property's content. +- `name`: A string representing the property name. +- `disabled`: A boolean value stating if the object property is disabled. +- `readonly`: A boolean value stating if the property is read-only. + +### Error List template + +To take control over how the form errors are displayed, you can define an *error list template* for your form. This list is the form global error list that appears at the top of your forms. + +An error list template is basically a React stateless component being passed errors as props so you can render them as you like: + +```jsx +function ErrorListTemplate(props) { + const {errors} = props; + return ( +
      + {errors.map(error => ( +
    • + {error.stack} +
    • + ))} +
    + ); +} + +render(( + , +), document.getElementById("app")); +``` + +> Note: Your custom `ErrorList` template will only render when `showErrorList` is `true`. + +The following props are passed to `ErrorList` + +- `errors`: An array of the errors. +- `errorSchema`: The errorSchema constructed by `Form`. +- `schema`: The schema that was passed to `Form`. +- `uiSchema`: The uiSchema that was passed to `Form`. +- `formContext`: The `formContext` object that you passed to Form. + +### Id prefix + +To avoid collisions with existing ids in the DOM, it is possible to change the prefix used for ids (the default is `root`). + +```jsx +render(( + , +), document.getElementById("app")); +``` + +This will render `` instead of `` + +### Custom widgets and fields + +The API allows to specify your own custom *widget* and *field* components: + +- A *widget* represents a HTML tag for the user to enter data, eg. `input`, `select`, etc. +- A *field* usually wraps one or more widgets and most often handles internal field state; think of a field as a form row, including the labels. + +### Custom widget components + +You can provide your own custom widgets to a uiSchema for the following json data types: + +- `string` +- `number` +- `integer` +- `boolean` + +```jsx +const schema = { + type: "string" +}; + +const uiSchema = { + "ui:widget": (props) => { + return ( + props.onChange(event.target.value)} /> + ); + } +}; + +render(( + , +), document.getElementById("app")); +``` + +The following props are passed to custom widget components: + +- `id`: The generated id for this field; +- `schema`: The JSONSchema subschema object for this field; +- `value`: The current value for this field; +- `required`: The required status of this field; +- `disabled`: `true` if the widget is disabled; +- `readonly`: `true` if the widget is read-only; +- `onChange`: The value change event handler; call it with the new value everytime it changes; +- `onBlur`: The input blur event handler; call it with the the widget id and value; +- `onFocus`: The input focus event handler; call it with the the widget id and value; +- `options`: A map of options passed as a prop to the component (see [Custom widget options](#custom-widget-options)). +- `formContext`: The `formContext` object that you passed to Form. + +> Note: Prior to v0.35.0, the `options` prop contained the list of options (`label` and `value`) for `enum` fields. Since v0.35.0, it now exposes this list as the `enumOptions` property within the `options` object. + +#### Custom component registration + +Alternatively, you can register them all at once by passing the `widgets` prop to the `Form` component, and reference their identifier from the `uiSchema`: + +```jsx +const MyCustomWidget = (props) => { + return ( + props.onChange(event.target.value)} /> + ); +}; + +const widgets = { + myCustomWidget: MyCustomWidget +}; + +const uiSchema = { + "ui:widget": "myCustomWidget" +} + +render(( + +), document.getElementById("app")); +``` + +This is useful if you expose the `uiSchema` as pure JSON, which can't carry functions. + +> Note: Until 0.40.0 it was possible to register a widget as object with shape `{ component: MyCustomWidget, options: {...} }`. This undocumented API has been removed. Instead, you can register a custom widget with a React `defaultProps` property. `defaultProps.options` can be an object containing your custom options. + +#### Custom widget options + +If you need to pass options to your custom widget, you can add a `ui:options` object containing those properties. If the widget has `defaultProps`, the options will be merged with the (optional) options object from `defaultProps`: + +```jsx +const schema = { + type: "string" +}; + +function MyCustomWidget(props) { + const {options} = props; + const {color, backgroundColor} = options; + return ; +} + +MyCustomWidget.defaultProps = { + options: { + color: "red" + } +}; + +const uiSchema = { + "ui:widget": MyCustomWidget, + "ui:options": { + backgroundColor: "yellow" + } +}; + +// renders red on yellow input +render(( + +), document.getElementById("app")); +``` + +> Note: This also applies to [registered custom components](#custom-component-registration). + +> Note: Since v0.41.0, the `ui:widget` object API, where a widget and options were specified with `"ui:widget": {component, options}` shape, is deprecated. It will be removed in a future release. + +#### Customizing widgets text input + +All the widgets that render a text input use the `BaseInput` component internally. If you need to customize all text inputs without customizing all widgets individually, you can provide a `BaseInput` component in the `widgets` property of `Form` (see [Custom component registration](#custom-component-registration). + +### Custom field components + +You can provide your own field components to a uiSchema for basically any json schema data type, by specifying a `ui:field` property. + +For example, let's create and register a dumb `geo` component handling a *latitude* and a *longitude*: + +```jsx +const schema = { + type: "object", + required: ["lat", "lon"], + properties: { + lat: {type: "number"}, + lon: {type: "number"} + } +}; + +// Define a custom component for handling the root position object +class GeoPosition extends React.Component { + constructor(props) { + super(props); + this.state = {...props.formData}; + } + + onChange(name) { + return (event) => { + this.setState({ + [name]: parseFloat(event.target.value) + }, () => this.props.onChange(this.state)); + }; + } + + render() { + const {lat, lon} = this.state; + return ( +
    + + +
    + ); + } +} + +// Define the custom field component to use for the root object +const uiSchema = {"ui:field": "geo"}; + +// Define the custom field components to register; here our "geo" +// custom field component +const fields = {geo: GeoPosition}; + +// Render the form with all the properties we just defined passed +// as props +render(( + +), document.getElementById("app")); +``` + +> Note: Registered fields can be reused across the entire schema. + +#### Field props + +A field component will always be passed the following props: + + - `schema`: The JSON schema for this field; + - `uiSchema`: The [uiSchema](#the-uischema-object) for this field; + - `idSchema`: The tree of unique ids for every child field; + - `formData`: The data for this field; + - `errorSchema`: The tree of errors for this field and its children; + - `registry`: A [registry](#the-registry-object) object (read next). + - `formContext`: A [formContext](#the-formcontext-object) object (read next). + +#### The `registry` object + +The `registry` is an object containing the registered custom fields and widgets as well as root schema definitions. + + - `fields`: The [custom registered fields](#custom-field-components). By default this object contains the standard `SchemaField`, `TitleField` and `DescriptionField` components; + - `widgets`: The [custom registered widgets](#custom-widget-components), if any; + - `definitions`: The root schema [definitions](#schema-definitions-and-references), if any. + - `formContext`: The [formContext](#the-formcontext-object) object. + +The registry is passed down the component tree, so you can access it from your custom field and `SchemaField` components. + +#### The `formContext` object + +You can provide a `formContext` object to the Form, which is passed down to all fields and widgets (including [TitleField](#custom-titles) and [DescriptionField](#custom-descriptions)). Useful for implementing context aware fields and widgets. + +### Custom array field buttons + +The `ArrayField` component provides a UI to add, remove and reorder array items, and these buttons use [Bootstrap glyphicons](http://getbootstrap.com/components/#glyphicons). If you don't use glyphicons but still want to provide your own icons or texts for these buttons, you can easily do so using CSS: + +```css +i.glyphicon { display: none; } +.btn-add::after { content: 'Add'; } +.array-item-move-up::after { content: 'Move Up'; } +.array-item-move-down::after { content: 'Move Down'; } +.array-item-remove::after { content: 'Remove'; } +``` + +### Custom SchemaField + +**Warning:** This is a powerful feature as you can override the whole form behavior and easily mess it up. Handle with care. + +You can provide your own implementation of the `SchemaField` base React component for rendering any JSONSchema field type, including objects and arrays. This is useful when you want to augment a given field type with supplementary powers. + +To proceed so, pass a `fields` object having a `SchemaField` property to your `Form` component; here's a rather silly example wrapping the standard `SchemaField` lib component: + +```jsx +import SchemaField from "react-jsonschema-form/lib/components/fields/SchemaField"; + +const CustomSchemaField = function(props) { + return ( +
    +

    Yeah, I'm pretty dumb.

    + +
    + ); +}; + +const fields = { + SchemaField: CustomSchemaField +}; + +render(( + +), document.getElementById("app")); +``` + +If you're curious how this could ever be useful, have a look at the [Kinto formbuilder](https://github.com/Kinto/formbuilder) repository to see how it's used to provide editing capabilities to any form field. + +Props passed to a custom SchemaField are the same as [the ones passed to a custom field](#field-props). + +### Customizing the default fields and widgets + +You can override any default field and widget, including the internal widgets like the `CheckboxWidget` that `ObjectField` renders for boolean values. You can override any field and widget just by providing the customized fields/widgets in the `fields` and `widgets` props: + +```jsx + +const CustomCheckbox = function(props) { + return ( + + ); +}; + +const widgets = { + CheckboxWidget: CustomCheckbox +}; + +render(( + +), document.getElementById("app")); +``` + +This allows you to create a reusable customized form class with your custom fields and widgets: + +```jsx +const customFields = {StringField: CustomString}; +const customWidgets = {CheckboxWidget: CustomCheckbox}; + +function MyForm(props) { + return ; +} + +render(( + +), document.getElementById("app")); +``` + +### Custom titles + +You can provide your own implementation of the `TitleField` base React component for rendering any title. This is useful when you want to augment how titles are handled. + +Simply pass a `fields` object having a `TitleField` property to your `Form` component: + +```jsx + +const CustomTitleField = ({title, required}) => { + const legend = required ? title + '*' : title; + return
    {legend}
    ; +}; + +const fields = { + TitleField: CustomTitleField +}; + +render(( + +), document.getElementById("app")); +``` + +### Custom descriptions + +You can provide your own implementation of the `DescriptionField` base React component for rendering any description. + +Simply pass a `fields` object having a `DescriptionField` property to your `Form` component: + +```jsx + +const CustomDescriptionField = ({id, description}) => { + return
    {description}
    ; +}; + +const fields = { + DescriptionField: CustomDescriptionField +}; + +render(( + +), document.getElementById("app")); +``` + +## Form data validation + +### Live validation + +By default, form data are only validated when the form is submitted or when a new `formData` prop is passed to the `Form` component. + +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. + +Be warned that this is an expensive strategy, with possibly strong impact on performances. + +To disable validation entirely, you can set Form's `noValidate` prop to `true`. + +### HTML5 Validation + +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. + +### Custom validation + +Form data is always validated against the JSON schema. + +But it is possible to define your own custom validation rules. This is especially useful when the validation depends on several interdependent fields. + +```js +function validate(formData, errors) { + if (formData.pass1 !== formData.pass2) { + errors.pass2.addError("Passwords don't match"); + } + return errors; +} + +const schema = { + type: "object", + properties: { + pass1: {type: "string", minLength: 3}, + pass2: {type: "string", minLength: 3}, + } +}; + +render(( + +), document.getElementById("app")); +``` + +> Notes: +> - The `validate()` function must **always** return the `errors` object +> received as second argument. +> - The `validate()` function is called **after** the JSON schema validation. + +### Custom error messages + +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. + +```js +function transformErrors(errors) { + return errors.map(error => { + if (error.name === "pattern") { + error.message = "Only digits are allowed" + } + return error; + }); +} + +const schema = { + type: "object", + properties: { + onlyNumbersString: {type: "string", pattern: "^\\d*$"}, + } +}; + +render(( + +), document.getElementById("app")); +``` + +> Notes: +> - The `transformErrors()` function must return the list of errors. Modifying the list in place without returning it will result in an error. + +### Error List Display + +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. + +```js +render(( + +), document.getElementById("app")); +``` + +> Note: you can also use your own [ErrorList](#error-list-template) + +### The case of empty strings + +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`. + +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. + +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. + +## Styling your forms + +This library renders form fields and widgets leveraging the [Bootstrap](http://getbootstrap.com/) semantics. That means your forms will be beautiful by default if you're loading its stylesheet in your page. + +You're not necessarily forced to use Bootstrap; while it uses its semantics, it also provides a bunch of other class names so you can bring new styles or override default ones quite easily in your own personalized stylesheet. That's just HTML after all :) + +If you're okay with using styles from the Bootstrap ecosystem though, then the good news is that you have access to many themes for it, which are compatible with our generated forms! + +Here are some examples from the [playground](http://mozilla-services.github.io/react-jsonschema-form/), using some of the [Bootswatch](http://bootswatch.com/) free themes: + +![](https://i.imgur.com/1Z5oUK3.png) +![](https://i.imgur.com/IMFqMwK.png) +![](https://i.imgur.com/HOACwt5.png) + +Last, if you really really want to override the semantics generated by the lib, you can always create and use your own custom [widget](#custom-widget-components), [field](#custom-field-components) and/or [schema field](#custom-schemafield) components. + +## Schema definitions and references + +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*: + +```json +{ + "definitions": { + "address": { + "type": "object", + "properties": { + "street_address": { "type": "string" }, + "city": { "type": "string" }, + "state": { "type": "string" } + }, + "required": ["street_address", "city", "state"] + } + }, + "type": "object", + "properties": { + "billing_address": { "$ref": "#/definitions/address" }, + "shipping_address": { "$ref": "#/definitions/address" } + } +} +``` + +*(Sample schema courtesy of the [Space Telescope Science Institute](http://spacetelescope.github.io/understanding-json-schema/structuring.html))* + +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. + +## Property dependencies + +This library supports conditionally making fields required based on the presence of other fields. + +### Unidirectional + +In the following example the `billing_address` field will be required if `credit_card` is defined. + +```json +{ + "type": "object", + + "properties": { + "name": { "type": "string" }, + "credit_card": { "type": "number" }, + "billing_address": { "type": "string" } + }, + + "required": ["name"], + + "dependencies": { + "credit_card": ["billing_address"] + } +} +``` + +### Bidirectional + +In the following example the `billing_address` field will be required if `credit_card` is defined and the `credit_card` +field will be required if `billing_address` is defined making them both required if either is defined. + +```json +{ + "type": "object", + + "properties": { + "name": { "type": "string" }, + "credit_card": { "type": "number" }, + "billing_address": { "type": "string" } + }, + + "required": ["name"], + + "dependencies": { + "credit_card": ["billing_address"], + "billing_address": ["credit_card"] + } +} +``` + +*(Sample schemas courtesy of the [Space Telescope Science Institute](https://spacetelescope.github.io/understanding-json-schema/reference/object.html#property-dependencies))* + +## Schema dependencies + +This library also supports modifying portions of a schema based on form data. + +### Conditional + +```json +{ + "type": "object", + + "properties": { + "name": { "type": "string" }, + "credit_card": { "type": "number" } + }, + + "required": ["name"], + + "dependencies": { + "credit_card": { + "properties": { + "billing_address": { "type": "string" } + }, + "required": ["billing_address"] + } + } +} +``` + +In this example the `billing_address` field will be displayed in the form if `credit_card` is defined. + +*(Sample schemas courtesy of the [Space Telescope Science Institute](https://spacetelescope.github.io/understanding-json-schema/reference/object.html#schema-dependencies))* + +### Dynamic + +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. + +```json +{ + "title": "Person", + "type": "object", + "properties": { + "Do you have any pets?": { + "type": "string", + "enum": [ + "No", + "Yes: One", + "Yes: More than one" + ], + "default": "No" + } + }, + "required": [ + "Do you have any pets?" + ], + "dependencies": { + "Do you have any pets?": { + "oneOf": [ + { + "properties": { + "Do you have any pets?": { + "enum": [ + "No" + ] + } + } + }, + { + "properties": { + "Do you have any pets?": { + "enum": [ + "Yes: One" + ] + }, + "How old is your pet?": { + "type": "number" + } + }, + "required": [ + "How old is your pet?" + ] + }, + { + "properties": { + "Do you have any pets?": { + "enum": [ + "Yes: More than one" + ] + }, + "Do you want to get rid of any?": { + "type": "boolean" + } + }, + "required": [ + "Do you want to get rid of any?" + ] + } + ] + } + } +} +``` + +In this example the user is prompted with different follow-up questions dynamically based on their answer to the first question. + +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. + +## JSON Schema supporting status + +This component follows [JSON Schema](http://json-schema.org/documentation.html) specs. Due to the limitation of form widgets, there are some exceptions as follows: + +* `additionalItems` keyword for arrays + + This keyword works when `items` is an array. `additionalItems: true` is not supported because there's no widget to represent an item of any type. In this case it will be treated as no additional items allowed. `additionalItems` being a valid schema is supported. + +* `anyOf`, `allOf`, and `oneOf`, or multiple `types` (i.e. `"type": ["string", "array"]` + + The `anyOf` and `oneOf` keywords are supported, however, properties declared inside the `anyOf/oneOf` should not overlap with properties "outside" of the `anyOf/oneOf`. + + You can also use `oneOf` with [schema dependencies](#schema-dependencies) to dynamically add schema properties based on input data. + +## Tips and tricks + + - Custom field template: https://jsfiddle.net/hdp1kgn6/1/ + - Multi-step wizard: https://jsfiddle.net/sn4bnw9h/1/ + - Using classNames with uiSchema: https://jsfiddle.net/gfwp25we/1/ + - Conditional fields: https://jsfiddle.net/69z2wepo/88541/ + - Advanced conditional fields: https://jsfiddle.net/cowbellerina/zbfh96b1/ + - Use radio list for enums: https://jsfiddle.net/f2y3fq7L/2/ + - Reading file input data: https://jsfiddle.net/f9vcb6pL/1/ + - Custom errors messages with transformErrors : https://jsfiddle.net/revolunet/5r3swnr4/ + - 2 columns form with CSS and FieldTemplate : https://jsfiddle.net/n1k0/bw0ffnz4/1/ + - Validate and submit form from external control : https://jsfiddle.net/spacebaboon/g5a1re63/ + +## Contributing + +### Coding style + +All the JavaScript code in this project conforms to the [prettier](https://github.com/prettier/prettier) coding style. A command is provided to ensure your code is always formatted accordingly: + +``` +$ npm run cs-format +``` + +The `cs-check` command ensures all files conform to that style: + +``` +$ npm run cs-check +``` + +### Development server + +``` +$ npm start +``` + +A live development server showcasing components with hot reload enabled is available at [localhost:8080](http://localhost:8080). + +If you want the development server to listen on another host or port, you can use the RJSF_DEV_SERVER env variable: + +``` +$ RJSF_DEV_SERVER=0.0.0.0:8000 npm start +``` + +### Tests + +``` +$ npm test +``` + +#### TDD + +``` +$ npm run tdd +``` + +### Releasing + +``` +$ edit package.json # update version number +$ git commit -m "Bump version $VERSION" +$ git tag v$VERSION +$ npm run dist +$ npm publish +$ git push --tags origin master +``` + +## FAQ + +### Q: Does rjsf support `oneOf`, `anyOf`, multiple types in an array, etc.? + +A: The `anyOf` and `oneOf` keywords are supported, however, properties declared inside the `anyOf/oneOf` should not overlap with properties "outside" of the `anyOf/oneOf`. +There is also special cased where you can use `oneOf` in [schema dependencies](#schema-dependencies), If you'd like to help improve support for these keywords, see the following issues for inspiration [#329](https://github.com/mozilla-services/react-jsonschema-form/pull/329) or [#417](https://github.com/mozilla-services/react-jsonschema-form/pull/417). See also: [#52](https://github.com/mozilla-services/react-jsonschema-form/issues/52), [#151](https://github.com/mozilla-services/react-jsonschema-form/issues/151), [#171](https://github.com/mozilla-services/react-jsonschema-form/issues/171), [#200](https://github.com/mozilla-services/react-jsonschema-form/issues/200), [#282](https://github.com/mozilla-services/react-jsonschema-form/issues/282), [#302](https://github.com/mozilla-services/react-jsonschema-form/pull/302), [#330](https://github.com/mozilla-services/react-jsonschema-form/issues/330), [#430](https://github.com/mozilla-services/react-jsonschema-form/issues/430), [#522](https://github.com/mozilla-services/react-jsonschema-form/issues/522), [#538](https://github.com/mozilla-services/react-jsonschema-form/issues/538), [#551](https://github.com/mozilla-services/react-jsonschema-form/issues/551), [#552](https://github.com/mozilla-services/react-jsonschema-form/issues/552), or [#648](https://github.com/mozilla-services/react-jsonschema-form/issues/648). + +### Q: Will react-jsonschema-form support Material, Ant-Design, Foundation, or [some other specific widget library or frontend style]? + +A: Probably not. We use Bootstrap v3 and it works fine for our needs. We would like for react-jsonschema-form to support other frameworks, we just don't want to support them ourselves. Ideally, these frontend styles could be added to react-jsonschema-form with a third-party library. If there is a technical limitation preventing this, please consider opening a PR. See also: [#91](https://github.com/mozilla-services/react-jsonschema-form/issues/91), [#99](https://github.com/mozilla-services/react-jsonschema-form/issues/99), [#125](https://github.com/mozilla-services/react-jsonschema-form/issues/125), [#237](https://github.com/mozilla-services/react-jsonschema-form/issues/237), [#287](https://github.com/mozilla-services/react-jsonschema-form/issues/287), [#299](https://github.com/mozilla-services/react-jsonschema-form/issues/299), [#440](https://github.com/mozilla-services/react-jsonschema-form/issues/440), [#461](https://github.com/mozilla-services/react-jsonschema-form/issues/461), [#546](https://github.com/mozilla-services/react-jsonschema-form/issues/546), [#555](https://github.com/mozilla-services/react-jsonschema-form/issues/555), [#626](https://github.com/mozilla-services/react-jsonschema-form/issues/626), and [#623](https://github.com/mozilla-services/react-jsonschema-form/pull/623). + +### Q: Is there a way to "collapse" fields, for instance to show/hide individual fields? + +A: There's no specific built-in way to do this, but you can write your own FieldTemplate that supports hiding/showing fields according to user input. We don't yet have an example of this use, but if you write one, please add it to the "tips and tricks" section, above. See also: [#268](https://github.com/mozilla-services/react-jsonschema-form/issues/268), [#304](https://github.com/mozilla-services/react-jsonschema-form/pull/304), [#598](https://github.com/mozilla-services/react-jsonschema-form/issues/598), [#920](https://github.com/mozilla-services/react-jsonschema-form/issues/920). + +## License + +Apache 2 diff --git a/mkdocs.yml b/mkdocs.yml new file mode 100644 index 0000000000..e334dbe4f3 --- /dev/null +++ b/mkdocs.yml @@ -0,0 +1 @@ +site_name: react-jsonschema-form documentation \ No newline at end of file From 7339a09739d4ccd23dcd4003cf598b3dd2748a9a Mon Sep 17 00:00:00 2001 From: Ashwin Ramaswami Date: Sun, 20 Jan 2019 21:52:35 -0800 Subject: [PATCH 36/90] docs: Add docs_dir --- mkdocs.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/mkdocs.yml b/mkdocs.yml index e334dbe4f3..0b28fda3dc 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -1 +1,2 @@ -site_name: react-jsonschema-form documentation \ No newline at end of file +site_name: react-jsonschema-form documentation +docs_dir: docs \ No newline at end of file From 251a274fe48e3421fa22d0ed67179c2d5cc82901 Mon Sep 17 00:00:00 2001 From: Lucian Buzzo Date: Mon, 21 Jan 2019 15:37:16 +0000 Subject: [PATCH 37/90] Add test and update documentation for using anyOf inside array items (#1131) * Add test and update documentation for using anyOf inside array items Signed-off-by: Lucian --- README.md | 5 +++-- docs/index.md | 2 +- playground/samples/anyOf.js | 22 +++++++++++++++++++ test/anyOf_test.js | 44 +++++++++++++++++++++++++++++++++++++ 4 files changed, 70 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 13b28085f9..e5cc458b2a 100644 --- a/README.md +++ b/README.md @@ -1845,9 +1845,9 @@ This component follows [JSON Schema](http://json-schema.org/documentation.html) This keyword works when `items` is an array. `additionalItems: true` is not supported because there's no widget to represent an item of any type. In this case it will be treated as no additional items allowed. `additionalItems` being a valid schema is supported. -* `anyOf`, `allOf`, and `oneOf`, or multiple `types` (i.e. `"type": ["string", "array"]` +* `anyOf`, `allOf`, and `oneOf`, or multiple `types` (i.e. `"type": ["string", "array"]`) - The `anyOf` and `oneOf` keywords are supported, however, properties declared inside the `anyOf/oneOf` should not overlap with properties "outside" of the `anyOf/oneOf`. + The `anyOf` and `oneOf` keywords are supported, however, properties declared inside the `anyOf/oneOf` should not overlap with properties "outside" of the `anyOf/oneOf`. You can also use `oneOf` with [schema dependencies](#schema-dependencies) to dynamically add schema properties based on input data. @@ -1922,6 +1922,7 @@ $ git push --tags origin master ### Q: Does rjsf support `oneOf`, `anyOf`, multiple types in an array, etc.? A: The `anyOf` and `oneOf` keywords are supported, however, properties declared inside the `anyOf/oneOf` should not overlap with properties "outside" of the `anyOf/oneOf`. + There is also special cased where you can use `oneOf` in [schema dependencies](#schema-dependencies), If you'd like to help improve support for these keywords, see the following issues for inspiration [#329](https://github.com/mozilla-services/react-jsonschema-form/pull/329) or [#417](https://github.com/mozilla-services/react-jsonschema-form/pull/417). See also: [#52](https://github.com/mozilla-services/react-jsonschema-form/issues/52), [#151](https://github.com/mozilla-services/react-jsonschema-form/issues/151), [#171](https://github.com/mozilla-services/react-jsonschema-form/issues/171), [#200](https://github.com/mozilla-services/react-jsonschema-form/issues/200), [#282](https://github.com/mozilla-services/react-jsonschema-form/issues/282), [#302](https://github.com/mozilla-services/react-jsonschema-form/pull/302), [#330](https://github.com/mozilla-services/react-jsonschema-form/issues/330), [#430](https://github.com/mozilla-services/react-jsonschema-form/issues/430), [#522](https://github.com/mozilla-services/react-jsonschema-form/issues/522), [#538](https://github.com/mozilla-services/react-jsonschema-form/issues/538), [#551](https://github.com/mozilla-services/react-jsonschema-form/issues/551), [#552](https://github.com/mozilla-services/react-jsonschema-form/issues/552), or [#648](https://github.com/mozilla-services/react-jsonschema-form/issues/648). ### Q: Will react-jsonschema-form support Material, Ant-Design, Foundation, or [some other specific widget library or frontend style]? diff --git a/docs/index.md b/docs/index.md index 91d6c61de2..6f8e489a13 100644 --- a/docs/index.md +++ b/docs/index.md @@ -1743,7 +1743,7 @@ This component follows [JSON Schema](http://json-schema.org/documentation.html) This keyword works when `items` is an array. `additionalItems: true` is not supported because there's no widget to represent an item of any type. In this case it will be treated as no additional items allowed. `additionalItems` being a valid schema is supported. -* `anyOf`, `allOf`, and `oneOf`, or multiple `types` (i.e. `"type": ["string", "array"]` +* `anyOf`, `allOf`, and `oneOf`, or multiple `types` (i.e. `"type": ["string", "array"]`) The `anyOf` and `oneOf` keywords are supported, however, properties declared inside the `anyOf/oneOf` should not overlap with properties "outside" of the `anyOf/oneOf`. diff --git a/playground/samples/anyOf.js b/playground/samples/anyOf.js index 432b0e0be1..8ff87f8523 100644 --- a/playground/samples/anyOf.js +++ b/playground/samples/anyOf.js @@ -6,6 +6,28 @@ module.exports = { type: "integer", title: "Age", }, + items: { + type: "array", + items: { + type: "object", + anyOf: [ + { + properties: { + foo: { + type: "string", + }, + }, + }, + { + properties: { + bar: { + type: "string", + }, + }, + }, + ], + }, + }, }, anyOf: [ { diff --git a/test/anyOf_test.js b/test/anyOf_test.js index ed5e67475b..cc845af6f7 100644 --- a/test/anyOf_test.js +++ b/test/anyOf_test.js @@ -299,4 +299,48 @@ describe("anyOf", () => { expect(node.querySelector("select").value).eql("1"); }); + + describe("Arrays", () => { + it("should correctly render form inputs for anyOf inside array items", () => { + const schema = { + type: "object", + properties: { + items: { + type: "array", + items: { + type: "object", + anyOf: [ + { + properties: { + foo: { + type: "string", + }, + }, + }, + { + properties: { + bar: { + type: "string", + }, + }, + }, + ], + }, + }, + }, + }; + + const { node } = createFormComponent({ + schema, + }); + + expect(node.querySelector(".array-item-add button")).not.eql(null); + + Simulate.click(node.querySelector(".array-item-add button")); + + expect(node.querySelectorAll("select")).to.have.length.of(1); + + expect(node.querySelectorAll("input#root_foo")).to.have.length.of(1); + }); + }); }); From 67ff7da0dc3fb6880fdb8220e8d05fb88528b1d4 Mon Sep 17 00:00:00 2001 From: Ashwin Ramaswami Date: Mon, 21 Jan 2019 08:37:58 -0800 Subject: [PATCH 38/90] doc: add browserstack logo #990 --- README.md | 3 +++ docs/index.md | 3 +++ 2 files changed, 6 insertions(+) diff --git a/README.md b/README.md index e5cc458b2a..3cd8bb163a 100644 --- a/README.md +++ b/README.md @@ -9,6 +9,9 @@ A [live playground](https://mozilla-services.github.io/react-jsonschema-form/) i ![](http://i.imgur.com/M8ZCES5.gif) +Testing powered by BrowserStack
    + + ## Table of Contents - [Philosophy](#philosophy) diff --git a/docs/index.md b/docs/index.md index 6f8e489a13..49569ab9f3 100644 --- a/docs/index.md +++ b/docs/index.md @@ -9,6 +9,9 @@ A [live playground](https://mozilla-services.github.io/react-jsonschema-form/) i ![Image](https://i.imgur.com/M8ZCES5.gif) +Testing powered by BrowserStack
    + + ## Philosophy react-jsonschema-form is meant to automatically generate a React form based on a [JSON Schema](http://json-schema.org/). It is a major component in the [kinto-admin](https://github.com/Kinto/kinto-admin/) project. If you want to generate a form for any data, sight unseen, simply given a JSON schema, react-jsonschema-form may be for you. If you have _a priori_ knowledge of your data and want a toolkit for generating forms for it, you might look elsewhere. From 7e74bb6e10eae4c0be798c82760136f945d9a184 Mon Sep 17 00:00:00 2001 From: Ashwin Ramaswami Date: Mon, 21 Jan 2019 08:54:10 -0800 Subject: [PATCH 39/90] doc: organize docs into separate files #1138 --- .gitignore | 2 + docs/advanced-customization.md | 575 ++++++++++++ docs/definitions.md | 29 + docs/dependencies.md | 157 ++++ docs/form-customization.md | 699 ++++++++++++++ docs/index.md | 1600 +------------------------------- docs/validation.md | 99 ++ mkdocs.yml | 12 +- 8 files changed, 1591 insertions(+), 1582 deletions(-) create mode 100644 docs/advanced-customization.md create mode 100644 docs/definitions.md create mode 100644 docs/dependencies.md create mode 100644 docs/form-customization.md create mode 100644 docs/validation.md diff --git a/.gitignore b/.gitignore index 03c1b53ecb..22092001c8 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,8 @@ npm-debug.log node_modules build +_build +site dist lib yarn.lock diff --git a/docs/advanced-customization.md b/docs/advanced-customization.md new file mode 100644 index 0000000000..e74fb9eaaa --- /dev/null +++ b/docs/advanced-customization.md @@ -0,0 +1,575 @@ +## Advanced customization + + +_ | Custom Field | Custom Template | Custom Widget +--|---------- | ------------- | ---- +What it does | Overrides all behaviour | Overrides just the layout | Overrides just the input box (not layout, labels, or help, or validation) +Usage | Global or per-field | Only global | Global or per-field +Global Example | `` | `` | `` +Per-Field Example | `"ui:field": MyField` | N/A | `"ui:widget":MyWidget` +Documentation | [Field](#field-props) | [Field Template](#field-template) - [Array Template](#array-field-template) - [Object Template](#object-field-template) - [Error List Template](#error-list-template) | [Custom Widgets](#custom-widget-components) + +### Field template + +To take control over the inner organization of each field (each form row), you can define a *field template* for your form. + +A field template is basically a React stateless component being passed field-related props, allowing you to structure your form row as you like. + +```jsx +function CustomFieldTemplate(props) { + const {id, classNames, label, help, required, description, errors, children} = props; + return ( +
    + + {description} + {children} + {errors} + {help} +
    + ); +} + +render(( + , +), document.getElementById("app")); +``` + +If you want to handle the rendering of each element yourself, you can use the props `rawHelp`, `rawDescription` and `rawErrors`. + +The following props are passed to a custom field template component: + +- `id`: The id of the field in the hierarchy. You can use it to render a label targeting the wrapped widget. +- `classNames`: A string containing the base Bootstrap CSS classes, merged with any [custom ones](#custom-css-class-names) defined in your uiSchema. +- `label`: The computed label for this field, as a string. +- `description`: A component instance rendering the field description, if one is defined (this will use any [custom `DescriptionField`](#custom-descriptions) defined). +- `rawDescription`: A string containing any `ui:description` uiSchema directive defined. +- `children`: The field or widget component instance for this field row. +- `errors`: A component instance listing any encountered errors for this field. +- `rawErrors`: An array of strings listing all generated error messages from encountered errors for this field. +- `help`: A component instance rendering any `ui:help` uiSchema directive defined. +- `rawHelp`: A string containing any `ui:help` uiSchema directive defined. **NOTE:** `rawHelp` will be `undefined` if passed `ui:help` is a React component instead of a string. +- `hidden`: A boolean value stating if the field should be hidden. +- `required`: A boolean value stating if the field is required. +- `readonly`: A boolean value stating if the field is read-only. +- `disabled`: A boolean value stating if the field is disabled. +- `displayLabel`: A boolean value stating if the label should be rendered or not. This is useful for nested fields in arrays where you don't want to clutter the UI. +- `fields`: An array containing all Form's fields including your [custom fields](#custom-field-components) and the built-in fields. +- `schema`: The schema object for this field. +- `uiSchema`: The uiSchema object for this field. +- `formContext`: The `formContext` object that you passed to Form. + +> Note: you can only define a single field template for a form. If you need many, it's probably time to look at [custom fields](#custom-field-components) instead. + +### Array Field Template + +Similarly to the `FieldTemplate` you can use an `ArrayFieldTemplate` to customize how your +arrays are rendered. This allows you to customize your array, and each element in the array. + +```jsx +function ArrayFieldTemplate(props) { + return ( +
    + {props.items.map(element => element.children)} + {props.canAdd && } +
    + ); +} + +render(( + , +), document.getElementById("app")); +``` + +Please see [customArray.js](https://github.com/mozilla-services/react-jsonschema-form/blob/master/playground/samples/customArray.js) for a better example. + +The following props are passed to each `ArrayFieldTemplate`: + +- `DescriptionField`: The `DescriptionField` from the registry (in case you wanted to utilize it) +- `TitleField`: The `TitleField` from the registry (in case you wanted to utilize it). +- `canAdd`: A boolean value stating whether new elements can be added to the array. +- `className`: The className string. +- `disabled`: A boolean value stating if the array is disabled. +- `idSchema`: Object +- `items`: An array of objects representing the items in the array. Each of the items represent a child with properties described below. +- `onAddClick: (event) => void`: A function that adds a new item to the array. +- `readonly`: A boolean value stating if the array is read-only. +- `required`: A boolean value stating if the array is required. +- `schema`: The schema object for this array. +- `uiSchema`: The uiSchema object for this array field. +- `title`: A string value containing the title for the array. +- `formContext`: The `formContext` object that you passed to Form. +- `formData`: The formData for this array. + +The following props are part of each element in `items`: + +- `children`: The html for the item's content. +- `className`: The className string. +- `disabled`: A boolean value stating if the array item is disabled. +- `hasMoveDown`: A boolean value stating whether the array item can be moved down. +- `hasMoveUp`: A boolean value stating whether the array item can be moved up. +- `hasRemove`: A boolean value stating whether the array item can be removed. +- `hasToolbar`: A boolean value stating whether the array item has a toolbar. +- `index`: A number stating the index the array item occurs in `items`. +- `onDropIndexClick: (index) => (event) => void`: Returns a function that removes the item at `index`. +- `onReorderClick: (index, newIndex) => (event) => void`: Returns a function that swaps the items at `index` with `newIndex`. +- `readonly`: A boolean value stating if the array item is read-only. + +### Object Field Template + +Similarly to the `FieldTemplate` you can use an `ObjectFieldTemplate` to customize how your +objects are rendered. + +```jsx +function ObjectFieldTemplate(props) { + return ( +
    + {props.title} + {props.description} + {props.properties.map(element =>
    {element.content}
    )} +
    + ); +} + +render(( + , +), document.getElementById("app")); +``` + +Please see [customObject.js](https://github.com/mozilla-services/react-jsonschema-form/blob/master/playground/samples/customObject.js) for a better example. + +The following props are passed to each `ObjectFieldTemplate`: + +- `DescriptionField`: The `DescriptionField` from the registry (in case you wanted to utilize it) +- `TitleField`: The `TitleField` from the registry (in case you wanted to utilize it). +- `title`: A string value containing the title for the object. +- `description`: A string value containing the description for the object. +- `disabled`: A boolean value stating if the object is disabled. +- `properties`: An array of object representing the properties in the array. Each of the properties represent a child with properties described below. +- `readonly`: A boolean value stating if the object is read-only. +- `required`: A boolean value stating if the object is required. +- `schema`: The schema object for this object. +- `uiSchema`: The uiSchema object for this object field. +- `idSchema`: An object containing the id for this object & ids for it's properties. +- `formData`: The form data for the object. +- `formContext`: The `formContext` object that you passed to Form. + +The following props are part of each element in `properties`: + +- `content`: The html for the property's content. +- `name`: A string representing the property name. +- `disabled`: A boolean value stating if the object property is disabled. +- `readonly`: A boolean value stating if the property is read-only. + +### Error List template + +To take control over how the form errors are displayed, you can define an *error list template* for your form. This list is the form global error list that appears at the top of your forms. + +An error list template is basically a React stateless component being passed errors as props so you can render them as you like: + +```jsx +function ErrorListTemplate(props) { + const {errors} = props; + return ( +
      + {errors.map(error => ( +
    • + {error.stack} +
    • + ))} +
    + ); +} + +render(( + , +), document.getElementById("app")); +``` + +> Note: Your custom `ErrorList` template will only render when `showErrorList` is `true`. + +The following props are passed to `ErrorList` + +- `errors`: An array of the errors. +- `errorSchema`: The errorSchema constructed by `Form`. +- `schema`: The schema that was passed to `Form`. +- `uiSchema`: The uiSchema that was passed to `Form`. +- `formContext`: The `formContext` object that you passed to Form. + +### Id prefix + +To avoid collisions with existing ids in the DOM, it is possible to change the prefix used for ids (the default is `root`). + +```jsx +render(( + , +), document.getElementById("app")); +``` + +This will render `` instead of `` + +### Custom widgets and fields + +The API allows to specify your own custom *widget* and *field* components: + +- A *widget* represents a HTML tag for the user to enter data, eg. `input`, `select`, etc. +- A *field* usually wraps one or more widgets and most often handles internal field state; think of a field as a form row, including the labels. + +### Custom widget components + +You can provide your own custom widgets to a uiSchema for the following json data types: + +- `string` +- `number` +- `integer` +- `boolean` + +```jsx +const schema = { + type: "string" +}; + +const uiSchema = { + "ui:widget": (props) => { + return ( + props.onChange(event.target.value)} /> + ); + } +}; + +render(( + , +), document.getElementById("app")); +``` + +The following props are passed to custom widget components: + +- `id`: The generated id for this field; +- `schema`: The JSONSchema subschema object for this field; +- `value`: The current value for this field; +- `required`: The required status of this field; +- `disabled`: `true` if the widget is disabled; +- `readonly`: `true` if the widget is read-only; +- `onChange`: The value change event handler; call it with the new value everytime it changes; +- `onBlur`: The input blur event handler; call it with the the widget id and value; +- `onFocus`: The input focus event handler; call it with the the widget id and value; +- `options`: A map of options passed as a prop to the component (see [Custom widget options](#custom-widget-options)). +- `formContext`: The `formContext` object that you passed to Form. + +> Note: Prior to v0.35.0, the `options` prop contained the list of options (`label` and `value`) for `enum` fields. Since v0.35.0, it now exposes this list as the `enumOptions` property within the `options` object. + +#### Custom component registration + +Alternatively, you can register them all at once by passing the `widgets` prop to the `Form` component, and reference their identifier from the `uiSchema`: + +```jsx +const MyCustomWidget = (props) => { + return ( + props.onChange(event.target.value)} /> + ); +}; + +const widgets = { + myCustomWidget: MyCustomWidget +}; + +const uiSchema = { + "ui:widget": "myCustomWidget" +} + +render(( + +), document.getElementById("app")); +``` + +This is useful if you expose the `uiSchema` as pure JSON, which can't carry functions. + +> Note: Until 0.40.0 it was possible to register a widget as object with shape `{ component: MyCustomWidget, options: {...} }`. This undocumented API has been removed. Instead, you can register a custom widget with a React `defaultProps` property. `defaultProps.options` can be an object containing your custom options. + +#### Custom widget options + +If you need to pass options to your custom widget, you can add a `ui:options` object containing those properties. If the widget has `defaultProps`, the options will be merged with the (optional) options object from `defaultProps`: + +```jsx +const schema = { + type: "string" +}; + +function MyCustomWidget(props) { + const {options} = props; + const {color, backgroundColor} = options; + return ; +} + +MyCustomWidget.defaultProps = { + options: { + color: "red" + } +}; + +const uiSchema = { + "ui:widget": MyCustomWidget, + "ui:options": { + backgroundColor: "yellow" + } +}; + +// renders red on yellow input +render(( + +), document.getElementById("app")); +``` + +> Note: This also applies to [registered custom components](#custom-component-registration). + +> Note: Since v0.41.0, the `ui:widget` object API, where a widget and options were specified with `"ui:widget": {component, options}` shape, is deprecated. It will be removed in a future release. + +#### Customizing widgets text input + +All the widgets that render a text input use the `BaseInput` component internally. If you need to customize all text inputs without customizing all widgets individually, you can provide a `BaseInput` component in the `widgets` property of `Form` (see [Custom component registration](#custom-component-registration). + +### Custom field components + +You can provide your own field components to a uiSchema for basically any json schema data type, by specifying a `ui:field` property. + +For example, let's create and register a dumb `geo` component handling a *latitude* and a *longitude*: + +```jsx +const schema = { + type: "object", + required: ["lat", "lon"], + properties: { + lat: {type: "number"}, + lon: {type: "number"} + } +}; + +// Define a custom component for handling the root position object +class GeoPosition extends React.Component { + constructor(props) { + super(props); + this.state = {...props.formData}; + } + + onChange(name) { + return (event) => { + this.setState({ + [name]: parseFloat(event.target.value) + }, () => this.props.onChange(this.state)); + }; + } + + render() { + const {lat, lon} = this.state; + return ( +
    + + +
    + ); + } +} + +// Define the custom field component to use for the root object +const uiSchema = {"ui:field": "geo"}; + +// Define the custom field components to register; here our "geo" +// custom field component +const fields = {geo: GeoPosition}; + +// Render the form with all the properties we just defined passed +// as props +render(( + +), document.getElementById("app")); +``` + +> Note: Registered fields can be reused across the entire schema. + +#### Field props + +A field component will always be passed the following props: + + - `schema`: The JSON schema for this field; + - `uiSchema`: The [uiSchema](#the-uischema-object) for this field; + - `idSchema`: The tree of unique ids for every child field; + - `formData`: The data for this field; + - `errorSchema`: The tree of errors for this field and its children; + - `registry`: A [registry](#the-registry-object) object (read next). + - `formContext`: A [formContext](#the-formcontext-object) object (read next). + +#### The `registry` object + +The `registry` is an object containing the registered custom fields and widgets as well as root schema definitions. + + - `fields`: The [custom registered fields](#custom-field-components). By default this object contains the standard `SchemaField`, `TitleField` and `DescriptionField` components; + - `widgets`: The [custom registered widgets](#custom-widget-components), if any; + - `definitions`: The root schema [definitions](#schema-definitions-and-references), if any. + - `formContext`: The [formContext](#the-formcontext-object) object. + +The registry is passed down the component tree, so you can access it from your custom field and `SchemaField` components. + +#### The `formContext` object + +You can provide a `formContext` object to the Form, which is passed down to all fields and widgets (including [TitleField](#custom-titles) and [DescriptionField](#custom-descriptions)). Useful for implementing context aware fields and widgets. + +### Custom array field buttons + +The `ArrayField` component provides a UI to add, remove and reorder array items, and these buttons use [Bootstrap glyphicons](http://getbootstrap.com/components/#glyphicons). If you don't use glyphicons but still want to provide your own icons or texts for these buttons, you can easily do so using CSS: + +```css +i.glyphicon { display: none; } +.btn-add::after { content: 'Add'; } +.array-item-move-up::after { content: 'Move Up'; } +.array-item-move-down::after { content: 'Move Down'; } +.array-item-remove::after { content: 'Remove'; } +``` + +### Custom SchemaField + +**Warning:** This is a powerful feature as you can override the whole form behavior and easily mess it up. Handle with care. + +You can provide your own implementation of the `SchemaField` base React component for rendering any JSONSchema field type, including objects and arrays. This is useful when you want to augment a given field type with supplementary powers. + +To proceed so, pass a `fields` object having a `SchemaField` property to your `Form` component; here's a rather silly example wrapping the standard `SchemaField` lib component: + +```jsx +import SchemaField from "react-jsonschema-form/lib/components/fields/SchemaField"; + +const CustomSchemaField = function(props) { + return ( +
    +

    Yeah, I'm pretty dumb.

    + +
    + ); +}; + +const fields = { + SchemaField: CustomSchemaField +}; + +render(( + +), document.getElementById("app")); +``` + +If you're curious how this could ever be useful, have a look at the [Kinto formbuilder](https://github.com/Kinto/formbuilder) repository to see how it's used to provide editing capabilities to any form field. + +Props passed to a custom SchemaField are the same as [the ones passed to a custom field](#field-props). + +### Customizing the default fields and widgets + +You can override any default field and widget, including the internal widgets like the `CheckboxWidget` that `ObjectField` renders for boolean values. You can override any field and widget just by providing the customized fields/widgets in the `fields` and `widgets` props: + +```jsx + +const CustomCheckbox = function(props) { + return ( + + ); +}; + +const widgets = { + CheckboxWidget: CustomCheckbox +}; + +render(( + +), document.getElementById("app")); +``` + +This allows you to create a reusable customized form class with your custom fields and widgets: + +```jsx +const customFields = {StringField: CustomString}; +const customWidgets = {CheckboxWidget: CustomCheckbox}; + +function MyForm(props) { + return ; +} + +render(( + +), document.getElementById("app")); +``` + +### Custom titles + +You can provide your own implementation of the `TitleField` base React component for rendering any title. This is useful when you want to augment how titles are handled. + +Simply pass a `fields` object having a `TitleField` property to your `Form` component: + +```jsx + +const CustomTitleField = ({title, required}) => { + const legend = required ? title + '*' : title; + return
    {legend}
    ; +}; + +const fields = { + TitleField: CustomTitleField +}; + +render(( + +), document.getElementById("app")); +``` + +### Custom descriptions + +You can provide your own implementation of the `DescriptionField` base React component for rendering any description. + +Simply pass a `fields` object having a `DescriptionField` property to your `Form` component: + +```jsx + +const CustomDescriptionField = ({id, description}) => { + return
    {description}
    ; +}; + +const fields = { + DescriptionField: CustomDescriptionField +}; + +render(( + +), document.getElementById("app")); +``` diff --git a/docs/definitions.md b/docs/definitions.md new file mode 100644 index 0000000000..65b89ed284 --- /dev/null +++ b/docs/definitions.md @@ -0,0 +1,29 @@ +## Schema definitions and references + +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*: + +```json +{ + "definitions": { + "address": { + "type": "object", + "properties": { + "street_address": { "type": "string" }, + "city": { "type": "string" }, + "state": { "type": "string" } + }, + "required": ["street_address", "city", "state"] + } + }, + "type": "object", + "properties": { + "billing_address": { "$ref": "#/definitions/address" }, + "shipping_address": { "$ref": "#/definitions/address" } + } +} +``` + +*(Sample schema courtesy of the [Space Telescope Science Institute](http://spacetelescope.github.io/understanding-json-schema/structuring.html))* + +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. + diff --git a/docs/dependencies.md b/docs/dependencies.md new file mode 100644 index 0000000000..9eb89cc412 --- /dev/null +++ b/docs/dependencies.md @@ -0,0 +1,157 @@ +## Property dependencies + +This library supports conditionally making fields required based on the presence of other fields. + +### Unidirectional + +In the following example the `billing_address` field will be required if `credit_card` is defined. + +```json +{ + "type": "object", + + "properties": { + "name": { "type": "string" }, + "credit_card": { "type": "number" }, + "billing_address": { "type": "string" } + }, + + "required": ["name"], + + "dependencies": { + "credit_card": ["billing_address"] + } +} +``` + +### Bidirectional + +In the following example the `billing_address` field will be required if `credit_card` is defined and the `credit_card` +field will be required if `billing_address` is defined making them both required if either is defined. + +```json +{ + "type": "object", + + "properties": { + "name": { "type": "string" }, + "credit_card": { "type": "number" }, + "billing_address": { "type": "string" } + }, + + "required": ["name"], + + "dependencies": { + "credit_card": ["billing_address"], + "billing_address": ["credit_card"] + } +} +``` + +*(Sample schemas courtesy of the [Space Telescope Science Institute](https://spacetelescope.github.io/understanding-json-schema/reference/object.html#property-dependencies))* + +## Schema dependencies + +This library also supports modifying portions of a schema based on form data. + +### Conditional + +```json +{ + "type": "object", + + "properties": { + "name": { "type": "string" }, + "credit_card": { "type": "number" } + }, + + "required": ["name"], + + "dependencies": { + "credit_card": { + "properties": { + "billing_address": { "type": "string" } + }, + "required": ["billing_address"] + } + } +} +``` + +In this example the `billing_address` field will be displayed in the form if `credit_card` is defined. + +*(Sample schemas courtesy of the [Space Telescope Science Institute](https://spacetelescope.github.io/understanding-json-schema/reference/object.html#schema-dependencies))* + +### Dynamic + +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. + +```json +{ + "title": "Person", + "type": "object", + "properties": { + "Do you have any pets?": { + "type": "string", + "enum": [ + "No", + "Yes: One", + "Yes: More than one" + ], + "default": "No" + } + }, + "required": [ + "Do you have any pets?" + ], + "dependencies": { + "Do you have any pets?": { + "oneOf": [ + { + "properties": { + "Do you have any pets?": { + "enum": [ + "No" + ] + } + } + }, + { + "properties": { + "Do you have any pets?": { + "enum": [ + "Yes: One" + ] + }, + "How old is your pet?": { + "type": "number" + } + }, + "required": [ + "How old is your pet?" + ] + }, + { + "properties": { + "Do you have any pets?": { + "enum": [ + "Yes: More than one" + ] + }, + "Do you want to get rid of any?": { + "type": "boolean" + } + }, + "required": [ + "Do you want to get rid of any?" + ] + } + ] + } + } +} +``` + +In this example the user is prompted with different follow-up questions dynamically based on their answer to the first question. + +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. diff --git a/docs/form-customization.md b/docs/form-customization.md new file mode 100644 index 0000000000..a6740a0dfa --- /dev/null +++ b/docs/form-customization.md @@ -0,0 +1,699 @@ +## Form customization + +### The `uiSchema` object + +JSONSchema is limited for describing how a given data type should be rendered as a form input component. That's why this lib introduces the concept of *UI schema*. + +A UI schema is basically an object literal providing information on **how** the form should be rendered, while the JSON schema tells **what**. + +The uiSchema object follows the tree structure of the form field hierarchy, and defines how each property should be rendered: + +```js +const schema = { + type: "object", + properties: { + foo: { + type: "object", + properties: { + bar: {type: "string"} + } + }, + baz: { + type: "array", + items: { + type: "object", + properties: { + description: { + "type": "string" + } + } + } + } + } +} + +const uiSchema = { + foo: { + bar: { + "ui:widget": "textarea" + }, + }, + baz: { + // note the "items" for an array + items: { + description: { + "ui:widget": "textarea" + } + } + } +} + +render(( + +), document.getElementById("app")); +``` + +### Alternative widgets + +The uiSchema `ui:widget` property tells the form which UI widget should be used to render a field. + +Example: + +```jsx +const uiSchema =  { + done: { + "ui:widget": "radio" // could also be "select" + } +}; + +render(( + +), document.getElementById("app")); +``` + +Here's a list of supported alternative widgets for different JSONSchema data types: + +#### For `boolean` fields + + * `radio`: a radio button group with `true` and `false` as selectable values; + * `select`: a select box with `true` and `false` as options; + * by default, a checkbox is used + +> Note: To set the labels for a boolean field, instead of using `true` and `false` you can set `enumNames` in your schema. Note that `enumNames` belongs in your `schema`, not the `uiSchema`, and the order is always `[true, false]`. + +#### For `string` fields + + * `textarea`: a `textarea` element is used; + * `password`: an `input[type=password]` element is used; + * `color`: an `input[type=color]` element is used; + * by default, a regular `input[type=text]` element is used. + +##### String formats + +The built-in string field also supports the JSONSchema `format` property, and will render an appropriate widget by default for the following string formats: + +- `email`: An `input[type=email]` element is used; +- `uri`: An `input[type=url]` element is used; +- `data-url`: By default, an `input[type=file]` element is used; in case the string is part of an array, multiple files will be handled automatically (see [File widgets](#file-widgets)). +- `date`: By default, an `input[type=date]` element is used; +- `date-time`: By default, an `input[type=datetime-local]` element is used. + +![](https://i.imgur.com/xqu6Lcp.png) + +Please note that, even though they are standardized, `datetime-local` and `date` input elements are not yet supported by Firefox and IE. If you plan on targeting these platforms, two alternative widgets are available: + +- `alt-datetime`: Six `select` elements are used to select the year, the month, the day, the hour, the minute and the second; +- `alt-date`: Three `select` elements are used to select the year, month and the day. + +> **Firefox 57 - 66**: Firefox partially supporting `date` and `time` input types, but not `datetime-local`, `month` or `week` + +![](https://i.imgur.com/VF5tY60.png) + +You can customize the list of years displayed in the `year` dropdown by providing a ``yearsRange`` property to ``ui:options`` in your uiSchema. Its also possible to remove the `Now` and `Clear` buttons with the `hideNowButton` and `hideClearButton` options. + +```jsx +uiSchema: { + a_date: { + "alt-datetime": { + "ui:widget": "alt-datetime", + "ui:options": { + yearsRange: [1980, 2030], + hideNowButton: true, + hideClearButton: true, + }, + }, + }, +}, +``` + +#### For `number` and `integer` fields + + * `updown`: an `input[type=number]` updown selector; + * `range`: an `input[type=range]` slider; + * `radio`: a radio button group with enum values. This can only be used when `enum` values are specified for this input. + * By default, a regular `input[type=text]` element is used. + +> Note: If JSONSchema's `minimum`, `maximum` and `multipleOf` values are defined, the `min`, `max` and `step` input attributes values will take those values. + +#### Disabled fields + +The `ui:disabled` uiSchema directive will disable all child widgets from a given field. + +#### Read-only fields + +The `ui:readonly` uiSchema directive will mark all child widgets from a given field as read-only. + +> Note: If you're wondering about the difference between a `disabled` field and a `readonly` one: Marking a field as read-only will render it greyed out, but its text value will be selectable. Disabling it will prevent its value to be selected at all. + +#### Hidden widgets + +It's possible to use a hidden widget for a field by setting its `ui:widget` uiSchema directive to `hidden`: + +```js +const schema = { + type: "object", + properties: { + foo: {type: "boolean"} + } +}; + +const uiSchema = { + foo: {"ui:widget": "hidden"} +}; +``` + +Notes: + + - Hiding widgets is only supported for `boolean`, `string`, `number` and `integer` schema types; + - A hidden widget takes its value from the `formData` prop. + +#### File widgets + +This library supports a limited form of `input[type=file]` widgets, in the sense that it will propagate file contents to form data state as [data-url](http://dataurl.net/#about)s. + +There are two ways to use file widgets. + +1. By declaring a `string` json schema type along a `data-url` [format](#string-formats): +```js +const schema = { + type: "string", + format: "data-url", +}; +``` + +2. By specifying a `ui:widget` field uiSchema directive as `file`: +```js +const schema = { + type: "string", +}; + +const uiSchema = { + "ui:widget": "file", +}; +``` + +##### Multiple files + +Multiple files selectors are supported by defining an array of strings having `data-url` as a format: + +```js +const schema = { + type: "array", + items: { + type: "string", + format: "data-url", + } +}; +``` + +> Note that storing large dataURIs into form state might slow rendering. + +##### File widget input ref + +The included `FileWidget` exposes a reference to the `` element node as an `inputRef` component property. + +This allows you to programmatically trigger the browser's file selector, which can be used in a custom file widget. + +### Object fields ordering + +Since the order of object properties in Javascript and JSON is not guaranteed, the `uiSchema` object spec allows you to define the order in which properties are rendered using the `ui:order` property: + +```jsx +const schema = { + type: "object", + properties: { + foo: {type: "string"}, + bar: {type: "string"} + } +}; + +const uiSchema = { + "ui:order": ["bar", "foo"] +}; + +render(( + +), document.getElementById("app")); +``` + +If a guaranteed fixed order is only important for some fields, you can insert a wildcard `"*"` item in your `ui:order` definition. All fields that are not referenced explicitly anywhere in the list will be rendered at that point: + +```js +const uiSchema = { + "ui:order": ["bar", "*"] +}; +``` + +### Object item options + +#### `expandable` option + +If `additionalProperties` contains a schema object, an add button for new properties is shown by default. The UX for editing properties whose names are user-defined is still experimental. + +You can turn support for `additionalProperties` off with the `expandable` option in `uiSchema`: + +```jsx +const uiSchema = { + "ui:options": { + expandable: false + } +}; +``` + +### Array item options + +#### `orderable` option + +Array items are orderable by default, and react-jsonschema-form renders move up/down buttons alongside them. The `uiSchema` object spec allows you to disable ordering: + +```jsx +const schema = { + type: "array", + items: { + type: "string" + } +}; + +const uiSchema = { + "ui:options": { + orderable: false + } +}; +``` + +#### `addable` option + +If either `items` or `additionalItems` contains a schema object, an add button for new items is shown by default. You can turn this off with the `addable` option in `uiSchema`: + +```jsx +const uiSchema = { + "ui:options": { + addable: false + } +}; +``` + +#### `removable` option + +A remove button is shown by default for an item if `items` contains a schema object, or the item is an `additionalItems` instance. You can turn this off with the `removable` option in `uiSchema`: + +```jsx +const uiSchema = { + "ui:options": { + removable: false + } +}; +``` + +### Custom CSS class names + +The uiSchema object accepts a `classNames` property for each field of the schema: + +```jsx +const uiSchema = { + title: { + classNames: "task-title foo-bar" + } +}; +``` + +Will result in: + +```html +
    + +
    +``` + +### Custom labels for `enum` fields + +This library supports the [`enumNames`](https://github.com/json-schema/json-schema/wiki/enumNames-%28v5-proposal%29) property for `enum` fields, which allows defining custom labels for each option of an `enum`: + +```js +const schema = { + type: "number", + enum: [1, 2, 3], + enumNames: ["one", "two", "three"] +}; +``` + +This will be rendered using a select box like this: + +```html + +``` + +Note that string representations of numbers will be cast back and reflected as actual numbers into form state. + +#### Alternative JSON-Schema compliant approach + +JSON Schema has an alternative approach to enumerations; react-jsonschema-form supports it as well. + +```js +const schema = { + "type": "number", + "anyOf": [ + { + "type": "number", + "title": "one", + "enum": [ + 1 + ] + }, + { + "type": "number", + "title": "two", + "enum": [ + 2 + ] + }, + { + "type": "number", + "title": "three", + "enum": [ + 3 + ] + } + ] +}; +``` + +This will be rendered as follows: + +```html + +``` + +This also works for radio buttons: + +```js +const schema = { + "type": "boolean", + "oneOf": [ + { + "const": true, + "title": "Yes" + }, + { + "const": false, + "title": "No" + } + ] +}; + +const uiSchema = { + "ui:widget": "radio" +}; +``` + +This will be rendered as follows: + +```html +
    +
    + +
    +
    + +
    +
    +``` + +A live example of both approaches side-by-side can be found in the **Alternatives** tab of the [playground](https://mozilla-services.github.io/react-jsonschema-form/). + +### Disabled attribute for `enum` fields + +To disable an option, use the `enumDisabled` property in uiSchema. + +```js +const schema = { + type: "string", + enum: ["one", "two", "three"], +}; + +const uiSchema={ + "ui:enumDisabled": ['two'], +} +``` + +This will be rendered using a select box as follows: + +```html + +``` + +### Multiple-choice list + +The default behavior for array fields is a list of text inputs with add/remove buttons. There are two alternative widgets for picking multiple elements from a list of choices. Typically this applies when a schema has an `enum` list for the `items` property of an `array` field, and the `uniqueItems` property set to `true`. + +Example: + +```js +const schema = { + type: "array", + title: "A multiple-choice list", + items: { + type: "string", + enum: ["foo", "bar", "fuzz", "qux"], + }, + uniqueItems: true +}; +``` + +By default, this will render a multiple select box. If you prefer a list of checkboxes, just set the uiSchema `ui:widget` directive to `checkboxes` for that field: + +```js +const uiSchema = { + "ui:widget": "checkboxes" +}; +``` + +Note that when an array property is marked as `required`, an empty array is considered valid. If array needs to be populated, you can specify the minimum number of items using the `minItems` property. + +Example: + +```js +const schema = { + type: "array", + minItems: 2, + title: "A multiple-choice list", + items: { + type: "string", + enum: ["foo", "bar", "fuzz", "qux"], + }, + uniqueItems: true +}; +``` + +By default, checkboxes are stacked. If you prefer them inline, set the `inline` property to `true`: + +```js +const uiSchema = { + "ui:widget": "checkboxes", + "ui:options": { + inline: true + } +}; +``` + +See the "Arrays" section of the [playground](https://mozilla-services.github.io/react-jsonschema-form/) for cool demos. + +### Autogenerated widget ids + +By default, this library will generate ids unique to the form for all rendered widgets. If you plan on using multiple instances of the `Form` component in a same page, it's wise to declare a root prefix for these, using the `ui:rootFieldId` uiSchema directive: + +```js +const uiSchema = { + "ui:rootFieldId": "myform" +}; +``` + +So all widgets will have an id prefixed with `myform`. + +### Form action buttons + +You can provide custom buttons to your form via the `Form` component's `children`. Otherwise a default submit button will be rendered. + +```jsx +render(( + +
    + + +
    + +), document.getElementById("app")); +``` + +> **Warning:** There needs to be a button or an input with `type="submit"` to trigger the form submission (and then the form validation). + +### Help text + +Sometimes it's convenient to add text next to a field to guide the end user filling it. This is the purpose of the `ui:help` uiSchema directive: + +```js +const schema = {type: "string"}; +const uiSchema = { + "ui:widget": "password", + "ui:help": "Hint: Make it strong!" +}; +``` + +![](https://i.imgur.com/scJUuZo.png) + +Help texts work for any kind of field at any level, and will always be rendered immediately below the field component widget(s) (after contextualized errors, if any). + +### Title texts + +Sometimes it's convenient to change a field's title. this is the purpose of the `ui:title` uiSchema directive: + +```js +const schema = {type: "string"}; +const uiSchema = { + "ui:widget": "password", + "ui:title": "Your password" +}; +``` + +### Description texts + +Sometimes it's convenient to change description a field. This is the purpose of the `ui:description` uiSchema directive: + +```js +const schema = {type: "string"}; +const uiSchema = { + "ui:widget": "password", + "ui:description": "The best password" +}; +``` + +### Auto focus + +If you want to automatically focus on a text input or textarea input, set the `ui:autofocus` uiSchema directive to `true`. + +```js +const schema = {type: "string"}; +const uiSchema = { + "ui:widget": "textarea", + "ui:autofocus": true +} +``` + +### Textarea `rows` option + +You can set the initial height of a textarea widget by specifying `rows` option. + +```js +const schema = {type: "string"}; +const uiSchema = { + "ui:widget": "textarea", + "ui:options": { + rows: 15 + } +} +``` + +### Placeholders + +You can add placeholder text to an input by using the `ui:placeholder` uiSchema directive: + +```jsx +const schema = {type: "string", format: "uri"}; +const uiSchema = { + "ui:placeholder": "http://" +}; +``` + +![](https://i.imgur.com/MbHypKg.png) + +Fields using `enum` can also use `ui:placeholder`. The value will be used as the text for the empty option in the select widget. + +```jsx +const schema = {type: "string", enum: ["First", "Second"]}; +const uiSchema = { + "ui:placeholder": "Choose an option" +}; +``` + +### Field labels + +Field labels are rendered by default. Labels may be omitted by setting the `label` option to `false` in the `ui:options` uiSchema directive. + +```jsx +const schema = {type: "string"}; +const uiSchema = { + "ui:options": { + label: false + } +}; +``` + +### HTML5 Input Types + +To change the input type (for example, `tel` or `email`) you can specify the `inputType` in the `ui:options` uiSchema directive. + +```jsx +const schema = {type: "string"}; +const uiSchema = { + "ui:options": { + inputType: 'tel' + } +}; +``` + +### Form attributes + +The `Form` component supports the following html attributes: + +```jsx +
    +``` + +### Disabling a form + +It's possible to disable the whole form by setting the `disabled` prop. The `disabled` prop is then forwarded down to each field of the form. + +```jsx + +``` + +If you just want to disable some of the fields, see the [`ui:disabled`](#disabled-fields) parameter in the `uiSchema` directive. diff --git a/docs/index.md b/docs/index.md index 49569ab9f3..a99ca20053 100644 --- a/docs/index.md +++ b/docs/index.md @@ -27,7 +27,7 @@ Requires React 15.0.0+. ### As a npm-based project dependency -``` +```bash $ npm install react-jsonschema-form --save ``` @@ -159,1584 +159,22 @@ render(( yourForm.submit(); ``` -## Form customization - -### The `uiSchema` object - -JSONSchema is limited for describing how a given data type should be rendered as a form input component. That's why this lib introduces the concept of *UI schema*. - -A UI schema is basically an object literal providing information on **how** the form should be rendered, while the JSON schema tells **what**. - -The uiSchema object follows the tree structure of the form field hierarchy, and defines how each property should be rendered: - -```js -const schema = { - type: "object", - properties: { - foo: { - type: "object", - properties: { - bar: {type: "string"} - } - }, - baz: { - type: "array", - items: { - type: "object", - properties: { - description: { - "type": "string" - } - } - } - } - } -} - -const uiSchema = { - foo: { - bar: { - "ui:widget": "textarea" - }, - }, - baz: { - // note the "items" for an array - items: { - description: { - "ui:widget": "textarea" - } - } - } -} - -render(( - -), document.getElementById("app")); -``` - -### Alternative widgets - -The uiSchema `ui:widget` property tells the form which UI widget should be used to render a field. - -Example: - -```jsx -const uiSchema =  { - done: { - "ui:widget": "radio" // could also be "select" - } -}; - -render(( - -), document.getElementById("app")); -``` - -Here's a list of supported alternative widgets for different JSONSchema data types: - -#### For `boolean` fields - - * `radio`: a radio button group with `true` and `false` as selectable values; - * `select`: a select box with `true` and `false` as options; - * by default, a checkbox is used - -> Note: To set the labels for a boolean field, instead of using `true` and `false` you can set `enumNames` in your schema. Note that `enumNames` belongs in your `schema`, not the `uiSchema`, and the order is always `[true, false]`. - -#### For `string` fields - - * `textarea`: a `textarea` element is used; - * `password`: an `input[type=password]` element is used; - * `color`: an `input[type=color]` element is used; - * by default, a regular `input[type=text]` element is used. - -##### String formats - -The built-in string field also supports the JSONSchema `format` property, and will render an appropriate widget by default for the following string formats: - -- `email`: An `input[type=email]` element is used; -- `uri`: An `input[type=url]` element is used; -- `data-url`: By default, an `input[type=file]` element is used; in case the string is part of an array, multiple files will be handled automatically (see [File widgets](#file-widgets)). -- `date`: By default, an `input[type=date]` element is used; -- `date-time`: By default, an `input[type=datetime-local]` element is used. - -![](https://i.imgur.com/xqu6Lcp.png) - -Please note that, even though they are standardized, `datetime-local` and `date` input elements are not yet supported by Firefox and IE. If you plan on targeting these platforms, two alternative widgets are available: - -- `alt-datetime`: Six `select` elements are used to select the year, the month, the day, the hour, the minute and the second; -- `alt-date`: Three `select` elements are used to select the year, month and the day. - -> **Firefox 57 - 66**: Firefox partially supporting `date` and `time` input types, but not `datetime-local`, `month` or `week` - -![](https://i.imgur.com/VF5tY60.png) - -You can customize the list of years displayed in the `year` dropdown by providing a ``yearsRange`` property to ``ui:options`` in your uiSchema. Its also possible to remove the `Now` and `Clear` buttons with the `hideNowButton` and `hideClearButton` options. - -```jsx -uiSchema: { - a_date: { - "alt-datetime": { - "ui:widget": "alt-datetime", - "ui:options": { - yearsRange: [1980, 2030], - hideNowButton: true, - hideClearButton: true, - }, - }, - }, -}, -``` - -#### For `number` and `integer` fields - - * `updown`: an `input[type=number]` updown selector; - * `range`: an `input[type=range]` slider; - * `radio`: a radio button group with enum values. This can only be used when `enum` values are specified for this input. - * By default, a regular `input[type=text]` element is used. - -> Note: If JSONSchema's `minimum`, `maximum` and `multipleOf` values are defined, the `min`, `max` and `step` input attributes values will take those values. - -#### Disabled fields - -The `ui:disabled` uiSchema directive will disable all child widgets from a given field. - -#### Read-only fields - -The `ui:readonly` uiSchema directive will mark all child widgets from a given field as read-only. - -> Note: If you're wondering about the difference between a `disabled` field and a `readonly` one: Marking a field as read-only will render it greyed out, but its text value will be selectable. Disabling it will prevent its value to be selected at all. - -#### Hidden widgets - -It's possible to use a hidden widget for a field by setting its `ui:widget` uiSchema directive to `hidden`: - -```js -const schema = { - type: "object", - properties: { - foo: {type: "boolean"} - } -}; - -const uiSchema = { - foo: {"ui:widget": "hidden"} -}; -``` - -Notes: - - - Hiding widgets is only supported for `boolean`, `string`, `number` and `integer` schema types; - - A hidden widget takes its value from the `formData` prop. - -#### File widgets - -This library supports a limited form of `input[type=file]` widgets, in the sense that it will propagate file contents to form data state as [data-url](http://dataurl.net/#about)s. - -There are two ways to use file widgets. - -1. By declaring a `string` json schema type along a `data-url` [format](#string-formats): -```js -const schema = { - type: "string", - format: "data-url", -}; -``` - -2. By specifying a `ui:widget` field uiSchema directive as `file`: -```js -const schema = { - type: "string", -}; - -const uiSchema = { - "ui:widget": "file", -}; -``` - -##### Multiple files - -Multiple files selectors are supported by defining an array of strings having `data-url` as a format: - -```js -const schema = { - type: "array", - items: { - type: "string", - format: "data-url", - } -}; -``` - -> Note that storing large dataURIs into form state might slow rendering. - -##### File widget input ref - -The included `FileWidget` exposes a reference to the `` element node as an `inputRef` component property. - -This allows you to programmatically trigger the browser's file selector, which can be used in a custom file widget. - -### Object fields ordering - -Since the order of object properties in Javascript and JSON is not guaranteed, the `uiSchema` object spec allows you to define the order in which properties are rendered using the `ui:order` property: - -```jsx -const schema = { - type: "object", - properties: { - foo: {type: "string"}, - bar: {type: "string"} - } -}; - -const uiSchema = { - "ui:order": ["bar", "foo"] -}; - -render(( - -), document.getElementById("app")); -``` - -If a guaranteed fixed order is only important for some fields, you can insert a wildcard `"*"` item in your `ui:order` definition. All fields that are not referenced explicitly anywhere in the list will be rendered at that point: - -```js -const uiSchema = { - "ui:order": ["bar", "*"] -}; -``` - -### Object item options - -#### `expandable` option - -If `additionalProperties` contains a schema object, an add button for new properties is shown by default. The UX for editing properties whose names are user-defined is still experimental. - -You can turn support for `additionalProperties` off with the `expandable` option in `uiSchema`: - -```jsx -const uiSchema = { - "ui:options": { - expandable: false - } -}; -``` - -### Array item options - -#### `orderable` option - -Array items are orderable by default, and react-jsonschema-form renders move up/down buttons alongside them. The `uiSchema` object spec allows you to disable ordering: - -```jsx -const schema = { - type: "array", - items: { - type: "string" - } -}; - -const uiSchema = { - "ui:options": { - orderable: false - } -}; -``` - -#### `addable` option - -If either `items` or `additionalItems` contains a schema object, an add button for new items is shown by default. You can turn this off with the `addable` option in `uiSchema`: - -```jsx -const uiSchema = { - "ui:options": { - addable: false - } -}; -``` - -#### `removable` option - -A remove button is shown by default for an item if `items` contains a schema object, or the item is an `additionalItems` instance. You can turn this off with the `removable` option in `uiSchema`: - -```jsx -const uiSchema = { - "ui:options": { - removable: false - } -}; -``` - -### Custom CSS class names - -The uiSchema object accepts a `classNames` property for each field of the schema: - -```jsx -const uiSchema = { - title: { - classNames: "task-title foo-bar" - } -}; -``` - -Will result in: - -```html -
    - -
    -``` - -### Custom labels for `enum` fields - -This library supports the [`enumNames`](https://github.com/json-schema/json-schema/wiki/enumNames-%28v5-proposal%29) property for `enum` fields, which allows defining custom labels for each option of an `enum`: - -```js -const schema = { - type: "number", - enum: [1, 2, 3], - enumNames: ["one", "two", "three"] -}; -``` - -This will be rendered using a select box like this: - -```html - -``` - -Note that string representations of numbers will be cast back and reflected as actual numbers into form state. - -#### Alternative JSON-Schema compliant approach - -JSON Schema has an alternative approach to enumerations; react-jsonschema-form supports it as well. - -```js -const schema = { - "type": "number", - "anyOf": [ - { - "type": "number", - "title": "one", - "enum": [ - 1 - ] - }, - { - "type": "number", - "title": "two", - "enum": [ - 2 - ] - }, - { - "type": "number", - "title": "three", - "enum": [ - 3 - ] - } - ] -}; -``` - -This will be rendered as follows: - -```html - -``` - -This also works for radio buttons: - -```js -const schema = { - "type": "boolean", - "oneOf": [ - { - "const": true, - "title": "Yes" - }, - { - "const": false, - "title": "No" - } - ] -}; - -const uiSchema = { - "ui:widget": "radio" -}; -``` - -This will be rendered as follows: - -```html -
    -
    - -
    -
    - -
    -
    -``` - -A live example of both approaches side-by-side can be found in the **Alternatives** tab of the [playground](https://mozilla-services.github.io/react-jsonschema-form/). - -### Disabled attribute for `enum` fields - -To disable an option, use the `enumDisabled` property in uiSchema. - -```js -const schema = { - type: "string", - enum: ["one", "two", "three"], -}; - -const uiSchema={ - "ui:enumDisabled": ['two'], -} -``` - -This will be rendered using a select box as follows: - -```html - -``` - -### Multiple-choice list - -The default behavior for array fields is a list of text inputs with add/remove buttons. There are two alternative widgets for picking multiple elements from a list of choices. Typically this applies when a schema has an `enum` list for the `items` property of an `array` field, and the `uniqueItems` property set to `true`. - -Example: - -```js -const schema = { - type: "array", - title: "A multiple-choice list", - items: { - type: "string", - enum: ["foo", "bar", "fuzz", "qux"], - }, - uniqueItems: true -}; -``` - -By default, this will render a multiple select box. If you prefer a list of checkboxes, just set the uiSchema `ui:widget` directive to `checkboxes` for that field: - -```js -const uiSchema = { - "ui:widget": "checkboxes" -}; -``` - -Note that when an array property is marked as `required`, an empty array is considered valid. If array needs to be populated, you can specify the minimum number of items using the `minItems` property. - -Example: - -```js -const schema = { - type: "array", - minItems: 2, - title: "A multiple-choice list", - items: { - type: "string", - enum: ["foo", "bar", "fuzz", "qux"], - }, - uniqueItems: true -}; -``` - -By default, checkboxes are stacked. If you prefer them inline, set the `inline` property to `true`: - -```js -const uiSchema = { - "ui:widget": "checkboxes", - "ui:options": { - inline: true - } -}; -``` - -See the "Arrays" section of the [playground](https://mozilla-services.github.io/react-jsonschema-form/) for cool demos. - -### Autogenerated widget ids - -By default, this library will generate ids unique to the form for all rendered widgets. If you plan on using multiple instances of the `Form` component in a same page, it's wise to declare a root prefix for these, using the `ui:rootFieldId` uiSchema directive: +## Styling your forms -```js -const uiSchema = { - "ui:rootFieldId": "myform" -}; -``` +This library renders form fields and widgets leveraging the [Bootstrap](http://getbootstrap.com/) semantics. That means your forms will be beautiful by default if you're loading its stylesheet in your page. -So all widgets will have an id prefixed with `myform`. +You're not necessarily forced to use Bootstrap; while it uses its semantics, it also provides a bunch of other class names so you can bring new styles or override default ones quite easily in your own personalized stylesheet. That's just HTML after all :) -### Form action buttons +If you're okay with using styles from the Bootstrap ecosystem though, then the good news is that you have access to many themes for it, which are compatible with our generated forms! -You can provide custom buttons to your form via the `Form` component's `children`. Otherwise a default submit button will be rendered. +Here are some examples from the [playground](http://mozilla-services.github.io/react-jsonschema-form/), using some of the [Bootswatch](http://bootswatch.com/) free themes: -```jsx -render(( - -
    - - -
    -
    -), document.getElementById("app")); -``` - -> **Warning:** There needs to be a button or an input with `type="submit"` to trigger the form submission (and then the form validation). - -### Help text - -Sometimes it's convenient to add text next to a field to guide the end user filling it. This is the purpose of the `ui:help` uiSchema directive: - -```js -const schema = {type: "string"}; -const uiSchema = { - "ui:widget": "password", - "ui:help": "Hint: Make it strong!" -}; -``` - -![](https://i.imgur.com/scJUuZo.png) - -Help texts work for any kind of field at any level, and will always be rendered immediately below the field component widget(s) (after contextualized errors, if any). - -### Title texts - -Sometimes it's convenient to change a field's title. this is the purpose of the `ui:title` uiSchema directive: - -```js -const schema = {type: "string"}; -const uiSchema = { - "ui:widget": "password", - "ui:title": "Your password" -}; -``` - -### Description texts - -Sometimes it's convenient to change description a field. This is the purpose of the `ui:description` uiSchema directive: - -```js -const schema = {type: "string"}; -const uiSchema = { - "ui:widget": "password", - "ui:description": "The best password" -}; -``` - -### Auto focus - -If you want to automatically focus on a text input or textarea input, set the `ui:autofocus` uiSchema directive to `true`. - -```js -const schema = {type: "string"}; -const uiSchema = { - "ui:widget": "textarea", - "ui:autofocus": true -} -``` - -### Textarea `rows` option - -You can set the initial height of a textarea widget by specifying `rows` option. - -```js -const schema = {type: "string"}; -const uiSchema = { - "ui:widget": "textarea", - "ui:options": { - rows: 15 - } -} -``` - -### Placeholders - -You can add placeholder text to an input by using the `ui:placeholder` uiSchema directive: - -```jsx -const schema = {type: "string", format: "uri"}; -const uiSchema = { - "ui:placeholder": "http://" -}; -``` - -![](https://i.imgur.com/MbHypKg.png) - -Fields using `enum` can also use `ui:placeholder`. The value will be used as the text for the empty option in the select widget. - -```jsx -const schema = {type: "string", enum: ["First", "Second"]}; -const uiSchema = { - "ui:placeholder": "Choose an option" -}; -``` - -### Field labels - -Field labels are rendered by default. Labels may be omitted by setting the `label` option to `false` in the `ui:options` uiSchema directive. - -```jsx -const schema = {type: "string"}; -const uiSchema = { - "ui:options": { - label: false - } -}; -``` - -### HTML5 Input Types - -To change the input type (for example, `tel` or `email`) you can specify the `inputType` in the `ui:options` uiSchema directive. - -```jsx -const schema = {type: "string"}; -const uiSchema = { - "ui:options": { - inputType: 'tel' - } -}; -``` - -### Form attributes - -The `Form` component supports the following html attributes: - -```jsx -
    -``` - -### Disabling a form - -It's possible to disable the whole form by setting the `disabled` prop. The `disabled` prop is then forwarded down to each field of the form. - -```jsx - -``` - -If you just want to disable some of the fields, see the [`ui:disabled`](#disabled-fields) parameter in the `uiSchema` directive. - -## Advanced customization - - -_ | Custom Field | Custom Template | Custom Widget ---|---------- | ------------- | ---- -What it does | Overrides all behaviour | Overrides just the layout | Overrides just the input box (not layout, labels, or help, or validation) -Usage | Global or per-field | Only global | Global or per-field -Global Example | `` | `` | `` -Per-Field Example | `"ui:field": MyField` | N/A | `"ui:widget":MyWidget` -Documentation | [Field](#field-props) | [Field Template](#field-template) - [Array Template](#array-field-template) - [Object Template](#object-field-template) - [Error List Template](#error-list-template) | [Custom Widgets](#custom-widget-components) - -### Field template - -To take control over the inner organization of each field (each form row), you can define a *field template* for your form. - -A field template is basically a React stateless component being passed field-related props, allowing you to structure your form row as you like. - -```jsx -function CustomFieldTemplate(props) { - const {id, classNames, label, help, required, description, errors, children} = props; - return ( -
    - - {description} - {children} - {errors} - {help} -
    - ); -} - -render(( - , -), document.getElementById("app")); -``` - -If you want to handle the rendering of each element yourself, you can use the props `rawHelp`, `rawDescription` and `rawErrors`. - -The following props are passed to a custom field template component: - -- `id`: The id of the field in the hierarchy. You can use it to render a label targeting the wrapped widget. -- `classNames`: A string containing the base Bootstrap CSS classes, merged with any [custom ones](#custom-css-class-names) defined in your uiSchema. -- `label`: The computed label for this field, as a string. -- `description`: A component instance rendering the field description, if one is defined (this will use any [custom `DescriptionField`](#custom-descriptions) defined). -- `rawDescription`: A string containing any `ui:description` uiSchema directive defined. -- `children`: The field or widget component instance for this field row. -- `errors`: A component instance listing any encountered errors for this field. -- `rawErrors`: An array of strings listing all generated error messages from encountered errors for this field. -- `help`: A component instance rendering any `ui:help` uiSchema directive defined. -- `rawHelp`: A string containing any `ui:help` uiSchema directive defined. **NOTE:** `rawHelp` will be `undefined` if passed `ui:help` is a React component instead of a string. -- `hidden`: A boolean value stating if the field should be hidden. -- `required`: A boolean value stating if the field is required. -- `readonly`: A boolean value stating if the field is read-only. -- `disabled`: A boolean value stating if the field is disabled. -- `displayLabel`: A boolean value stating if the label should be rendered or not. This is useful for nested fields in arrays where you don't want to clutter the UI. -- `fields`: An array containing all Form's fields including your [custom fields](#custom-field-components) and the built-in fields. -- `schema`: The schema object for this field. -- `uiSchema`: The uiSchema object for this field. -- `formContext`: The `formContext` object that you passed to Form. - -> Note: you can only define a single field template for a form. If you need many, it's probably time to look at [custom fields](#custom-field-components) instead. - -### Array Field Template - -Similarly to the `FieldTemplate` you can use an `ArrayFieldTemplate` to customize how your -arrays are rendered. This allows you to customize your array, and each element in the array. - -```jsx -function ArrayFieldTemplate(props) { - return ( -
    - {props.items.map(element => element.children)} - {props.canAdd && } -
    - ); -} - -render(( - , -), document.getElementById("app")); -``` - -Please see [customArray.js](https://github.com/mozilla-services/react-jsonschema-form/blob/master/playground/samples/customArray.js) for a better example. - -The following props are passed to each `ArrayFieldTemplate`: - -- `DescriptionField`: The `DescriptionField` from the registry (in case you wanted to utilize it) -- `TitleField`: The `TitleField` from the registry (in case you wanted to utilize it). -- `canAdd`: A boolean value stating whether new elements can be added to the array. -- `className`: The className string. -- `disabled`: A boolean value stating if the array is disabled. -- `idSchema`: Object -- `items`: An array of objects representing the items in the array. Each of the items represent a child with properties described below. -- `onAddClick: (event) => void`: A function that adds a new item to the array. -- `readonly`: A boolean value stating if the array is read-only. -- `required`: A boolean value stating if the array is required. -- `schema`: The schema object for this array. -- `uiSchema`: The uiSchema object for this array field. -- `title`: A string value containing the title for the array. -- `formContext`: The `formContext` object that you passed to Form. -- `formData`: The formData for this array. - -The following props are part of each element in `items`: - -- `children`: The html for the item's content. -- `className`: The className string. -- `disabled`: A boolean value stating if the array item is disabled. -- `hasMoveDown`: A boolean value stating whether the array item can be moved down. -- `hasMoveUp`: A boolean value stating whether the array item can be moved up. -- `hasRemove`: A boolean value stating whether the array item can be removed. -- `hasToolbar`: A boolean value stating whether the array item has a toolbar. -- `index`: A number stating the index the array item occurs in `items`. -- `onDropIndexClick: (index) => (event) => void`: Returns a function that removes the item at `index`. -- `onReorderClick: (index, newIndex) => (event) => void`: Returns a function that swaps the items at `index` with `newIndex`. -- `readonly`: A boolean value stating if the array item is read-only. - -### Object Field Template - -Similarly to the `FieldTemplate` you can use an `ObjectFieldTemplate` to customize how your -objects are rendered. - -```jsx -function ObjectFieldTemplate(props) { - return ( -
    - {props.title} - {props.description} - {props.properties.map(element =>
    {element.content}
    )} -
    - ); -} - -render(( - , -), document.getElementById("app")); -``` - -Please see [customObject.js](https://github.com/mozilla-services/react-jsonschema-form/blob/master/playground/samples/customObject.js) for a better example. - -The following props are passed to each `ObjectFieldTemplate`: - -- `DescriptionField`: The `DescriptionField` from the registry (in case you wanted to utilize it) -- `TitleField`: The `TitleField` from the registry (in case you wanted to utilize it). -- `title`: A string value containing the title for the object. -- `description`: A string value containing the description for the object. -- `disabled`: A boolean value stating if the object is disabled. -- `properties`: An array of object representing the properties in the array. Each of the properties represent a child with properties described below. -- `readonly`: A boolean value stating if the object is read-only. -- `required`: A boolean value stating if the object is required. -- `schema`: The schema object for this object. -- `uiSchema`: The uiSchema object for this object field. -- `idSchema`: An object containing the id for this object & ids for it's properties. -- `formData`: The form data for the object. -- `formContext`: The `formContext` object that you passed to Form. - -The following props are part of each element in `properties`: - -- `content`: The html for the property's content. -- `name`: A string representing the property name. -- `disabled`: A boolean value stating if the object property is disabled. -- `readonly`: A boolean value stating if the property is read-only. - -### Error List template - -To take control over how the form errors are displayed, you can define an *error list template* for your form. This list is the form global error list that appears at the top of your forms. - -An error list template is basically a React stateless component being passed errors as props so you can render them as you like: - -```jsx -function ErrorListTemplate(props) { - const {errors} = props; - return ( -
      - {errors.map(error => ( -
    • - {error.stack} -
    • - ))} -
    - ); -} - -render(( - , -), document.getElementById("app")); -``` - -> Note: Your custom `ErrorList` template will only render when `showErrorList` is `true`. - -The following props are passed to `ErrorList` - -- `errors`: An array of the errors. -- `errorSchema`: The errorSchema constructed by `Form`. -- `schema`: The schema that was passed to `Form`. -- `uiSchema`: The uiSchema that was passed to `Form`. -- `formContext`: The `formContext` object that you passed to Form. - -### Id prefix - -To avoid collisions with existing ids in the DOM, it is possible to change the prefix used for ids (the default is `root`). - -```jsx -render(( - , -), document.getElementById("app")); -``` - -This will render `` instead of `` - -### Custom widgets and fields - -The API allows to specify your own custom *widget* and *field* components: - -- A *widget* represents a HTML tag for the user to enter data, eg. `input`, `select`, etc. -- A *field* usually wraps one or more widgets and most often handles internal field state; think of a field as a form row, including the labels. - -### Custom widget components - -You can provide your own custom widgets to a uiSchema for the following json data types: - -- `string` -- `number` -- `integer` -- `boolean` - -```jsx -const schema = { - type: "string" -}; - -const uiSchema = { - "ui:widget": (props) => { - return ( - props.onChange(event.target.value)} /> - ); - } -}; - -render(( - , -), document.getElementById("app")); -``` - -The following props are passed to custom widget components: - -- `id`: The generated id for this field; -- `schema`: The JSONSchema subschema object for this field; -- `value`: The current value for this field; -- `required`: The required status of this field; -- `disabled`: `true` if the widget is disabled; -- `readonly`: `true` if the widget is read-only; -- `onChange`: The value change event handler; call it with the new value everytime it changes; -- `onBlur`: The input blur event handler; call it with the the widget id and value; -- `onFocus`: The input focus event handler; call it with the the widget id and value; -- `options`: A map of options passed as a prop to the component (see [Custom widget options](#custom-widget-options)). -- `formContext`: The `formContext` object that you passed to Form. - -> Note: Prior to v0.35.0, the `options` prop contained the list of options (`label` and `value`) for `enum` fields. Since v0.35.0, it now exposes this list as the `enumOptions` property within the `options` object. - -#### Custom component registration - -Alternatively, you can register them all at once by passing the `widgets` prop to the `Form` component, and reference their identifier from the `uiSchema`: - -```jsx -const MyCustomWidget = (props) => { - return ( - props.onChange(event.target.value)} /> - ); -}; - -const widgets = { - myCustomWidget: MyCustomWidget -}; - -const uiSchema = { - "ui:widget": "myCustomWidget" -} - -render(( - -), document.getElementById("app")); -``` - -This is useful if you expose the `uiSchema` as pure JSON, which can't carry functions. - -> Note: Until 0.40.0 it was possible to register a widget as object with shape `{ component: MyCustomWidget, options: {...} }`. This undocumented API has been removed. Instead, you can register a custom widget with a React `defaultProps` property. `defaultProps.options` can be an object containing your custom options. - -#### Custom widget options - -If you need to pass options to your custom widget, you can add a `ui:options` object containing those properties. If the widget has `defaultProps`, the options will be merged with the (optional) options object from `defaultProps`: - -```jsx -const schema = { - type: "string" -}; - -function MyCustomWidget(props) { - const {options} = props; - const {color, backgroundColor} = options; - return ; -} - -MyCustomWidget.defaultProps = { - options: { - color: "red" - } -}; - -const uiSchema = { - "ui:widget": MyCustomWidget, - "ui:options": { - backgroundColor: "yellow" - } -}; - -// renders red on yellow input -render(( - -), document.getElementById("app")); -``` - -> Note: This also applies to [registered custom components](#custom-component-registration). - -> Note: Since v0.41.0, the `ui:widget` object API, where a widget and options were specified with `"ui:widget": {component, options}` shape, is deprecated. It will be removed in a future release. - -#### Customizing widgets text input - -All the widgets that render a text input use the `BaseInput` component internally. If you need to customize all text inputs without customizing all widgets individually, you can provide a `BaseInput` component in the `widgets` property of `Form` (see [Custom component registration](#custom-component-registration). - -### Custom field components - -You can provide your own field components to a uiSchema for basically any json schema data type, by specifying a `ui:field` property. - -For example, let's create and register a dumb `geo` component handling a *latitude* and a *longitude*: - -```jsx -const schema = { - type: "object", - required: ["lat", "lon"], - properties: { - lat: {type: "number"}, - lon: {type: "number"} - } -}; - -// Define a custom component for handling the root position object -class GeoPosition extends React.Component { - constructor(props) { - super(props); - this.state = {...props.formData}; - } - - onChange(name) { - return (event) => { - this.setState({ - [name]: parseFloat(event.target.value) - }, () => this.props.onChange(this.state)); - }; - } - - render() { - const {lat, lon} = this.state; - return ( -
    - - -
    - ); - } -} - -// Define the custom field component to use for the root object -const uiSchema = {"ui:field": "geo"}; - -// Define the custom field components to register; here our "geo" -// custom field component -const fields = {geo: GeoPosition}; - -// Render the form with all the properties we just defined passed -// as props -render(( - -), document.getElementById("app")); -``` - -> Note: Registered fields can be reused across the entire schema. - -#### Field props - -A field component will always be passed the following props: - - - `schema`: The JSON schema for this field; - - `uiSchema`: The [uiSchema](#the-uischema-object) for this field; - - `idSchema`: The tree of unique ids for every child field; - - `formData`: The data for this field; - - `errorSchema`: The tree of errors for this field and its children; - - `registry`: A [registry](#the-registry-object) object (read next). - - `formContext`: A [formContext](#the-formcontext-object) object (read next). - -#### The `registry` object - -The `registry` is an object containing the registered custom fields and widgets as well as root schema definitions. - - - `fields`: The [custom registered fields](#custom-field-components). By default this object contains the standard `SchemaField`, `TitleField` and `DescriptionField` components; - - `widgets`: The [custom registered widgets](#custom-widget-components), if any; - - `definitions`: The root schema [definitions](#schema-definitions-and-references), if any. - - `formContext`: The [formContext](#the-formcontext-object) object. - -The registry is passed down the component tree, so you can access it from your custom field and `SchemaField` components. - -#### The `formContext` object - -You can provide a `formContext` object to the Form, which is passed down to all fields and widgets (including [TitleField](#custom-titles) and [DescriptionField](#custom-descriptions)). Useful for implementing context aware fields and widgets. - -### Custom array field buttons - -The `ArrayField` component provides a UI to add, remove and reorder array items, and these buttons use [Bootstrap glyphicons](http://getbootstrap.com/components/#glyphicons). If you don't use glyphicons but still want to provide your own icons or texts for these buttons, you can easily do so using CSS: - -```css -i.glyphicon { display: none; } -.btn-add::after { content: 'Add'; } -.array-item-move-up::after { content: 'Move Up'; } -.array-item-move-down::after { content: 'Move Down'; } -.array-item-remove::after { content: 'Remove'; } -``` - -### Custom SchemaField - -**Warning:** This is a powerful feature as you can override the whole form behavior and easily mess it up. Handle with care. - -You can provide your own implementation of the `SchemaField` base React component for rendering any JSONSchema field type, including objects and arrays. This is useful when you want to augment a given field type with supplementary powers. - -To proceed so, pass a `fields` object having a `SchemaField` property to your `Form` component; here's a rather silly example wrapping the standard `SchemaField` lib component: - -```jsx -import SchemaField from "react-jsonschema-form/lib/components/fields/SchemaField"; - -const CustomSchemaField = function(props) { - return ( -
    -

    Yeah, I'm pretty dumb.

    - -
    - ); -}; - -const fields = { - SchemaField: CustomSchemaField -}; - -render(( - -), document.getElementById("app")); -``` - -If you're curious how this could ever be useful, have a look at the [Kinto formbuilder](https://github.com/Kinto/formbuilder) repository to see how it's used to provide editing capabilities to any form field. - -Props passed to a custom SchemaField are the same as [the ones passed to a custom field](#field-props). - -### Customizing the default fields and widgets - -You can override any default field and widget, including the internal widgets like the `CheckboxWidget` that `ObjectField` renders for boolean values. You can override any field and widget just by providing the customized fields/widgets in the `fields` and `widgets` props: - -```jsx - -const CustomCheckbox = function(props) { - return ( - - ); -}; - -const widgets = { - CheckboxWidget: CustomCheckbox -}; - -render(( - -), document.getElementById("app")); -``` - -This allows you to create a reusable customized form class with your custom fields and widgets: - -```jsx -const customFields = {StringField: CustomString}; -const customWidgets = {CheckboxWidget: CustomCheckbox}; - -function MyForm(props) { - return ; -} - -render(( - -), document.getElementById("app")); -``` - -### Custom titles - -You can provide your own implementation of the `TitleField` base React component for rendering any title. This is useful when you want to augment how titles are handled. - -Simply pass a `fields` object having a `TitleField` property to your `Form` component: - -```jsx - -const CustomTitleField = ({title, required}) => { - const legend = required ? title + '*' : title; - return
    {legend}
    ; -}; - -const fields = { - TitleField: CustomTitleField -}; - -render(( - -), document.getElementById("app")); -``` - -### Custom descriptions - -You can provide your own implementation of the `DescriptionField` base React component for rendering any description. - -Simply pass a `fields` object having a `DescriptionField` property to your `Form` component: - -```jsx - -const CustomDescriptionField = ({id, description}) => { - return
    {description}
    ; -}; - -const fields = { - DescriptionField: CustomDescriptionField -}; - -render(( - -), document.getElementById("app")); -``` - -## Form data validation - -### Live validation - -By default, form data are only validated when the form is submitted or when a new `formData` prop is passed to the `Form` component. - -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. - -Be warned that this is an expensive strategy, with possibly strong impact on performances. - -To disable validation entirely, you can set Form's `noValidate` prop to `true`. - -### HTML5 Validation - -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. - -### Custom validation - -Form data is always validated against the JSON schema. - -But it is possible to define your own custom validation rules. This is especially useful when the validation depends on several interdependent fields. - -```js -function validate(formData, errors) { - if (formData.pass1 !== formData.pass2) { - errors.pass2.addError("Passwords don't match"); - } - return errors; -} - -const schema = { - type: "object", - properties: { - pass1: {type: "string", minLength: 3}, - pass2: {type: "string", minLength: 3}, - } -}; - -render(( - -), document.getElementById("app")); -``` - -> Notes: -> - The `validate()` function must **always** return the `errors` object -> received as second argument. -> - The `validate()` function is called **after** the JSON schema validation. - -### Custom error messages - -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. - -```js -function transformErrors(errors) { - return errors.map(error => { - if (error.name === "pattern") { - error.message = "Only digits are allowed" - } - return error; - }); -} - -const schema = { - type: "object", - properties: { - onlyNumbersString: {type: "string", pattern: "^\\d*$"}, - } -}; - -render(( - -), document.getElementById("app")); -``` - -> Notes: -> - The `transformErrors()` function must return the list of errors. Modifying the list in place without returning it will result in an error. - -### Error List Display - -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. - -```js -render(( - -), document.getElementById("app")); -``` - -> Note: you can also use your own [ErrorList](#error-list-template) - -### The case of empty strings - -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`. - -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. - -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. - -## Styling your forms - -This library renders form fields and widgets leveraging the [Bootstrap](http://getbootstrap.com/) semantics. That means your forms will be beautiful by default if you're loading its stylesheet in your page. - -You're not necessarily forced to use Bootstrap; while it uses its semantics, it also provides a bunch of other class names so you can bring new styles or override default ones quite easily in your own personalized stylesheet. That's just HTML after all :) - -If you're okay with using styles from the Bootstrap ecosystem though, then the good news is that you have access to many themes for it, which are compatible with our generated forms! - -Here are some examples from the [playground](http://mozilla-services.github.io/react-jsonschema-form/), using some of the [Bootswatch](http://bootswatch.com/) free themes: - -![](https://i.imgur.com/1Z5oUK3.png) -![](https://i.imgur.com/IMFqMwK.png) -![](https://i.imgur.com/HOACwt5.png) +![](https://i.imgur.com/1Z5oUK3.png) +![](https://i.imgur.com/IMFqMwK.png) +![](https://i.imgur.com/HOACwt5.png) Last, if you really really want to override the semantics generated by the lib, you can always create and use your own custom [widget](#custom-widget-components), [field](#custom-field-components) and/or [schema field](#custom-schemafield) components. -## Schema definitions and references - -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*: - -```json -{ - "definitions": { - "address": { - "type": "object", - "properties": { - "street_address": { "type": "string" }, - "city": { "type": "string" }, - "state": { "type": "string" } - }, - "required": ["street_address", "city", "state"] - } - }, - "type": "object", - "properties": { - "billing_address": { "$ref": "#/definitions/address" }, - "shipping_address": { "$ref": "#/definitions/address" } - } -} -``` - -*(Sample schema courtesy of the [Space Telescope Science Institute](http://spacetelescope.github.io/understanding-json-schema/structuring.html))* - -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. - -## Property dependencies - -This library supports conditionally making fields required based on the presence of other fields. - -### Unidirectional - -In the following example the `billing_address` field will be required if `credit_card` is defined. - -```json -{ - "type": "object", - - "properties": { - "name": { "type": "string" }, - "credit_card": { "type": "number" }, - "billing_address": { "type": "string" } - }, - - "required": ["name"], - - "dependencies": { - "credit_card": ["billing_address"] - } -} -``` - -### Bidirectional - -In the following example the `billing_address` field will be required if `credit_card` is defined and the `credit_card` -field will be required if `billing_address` is defined making them both required if either is defined. - -```json -{ - "type": "object", - - "properties": { - "name": { "type": "string" }, - "credit_card": { "type": "number" }, - "billing_address": { "type": "string" } - }, - - "required": ["name"], - - "dependencies": { - "credit_card": ["billing_address"], - "billing_address": ["credit_card"] - } -} -``` - -*(Sample schemas courtesy of the [Space Telescope Science Institute](https://spacetelescope.github.io/understanding-json-schema/reference/object.html#property-dependencies))* - -## Schema dependencies - -This library also supports modifying portions of a schema based on form data. - -### Conditional - -```json -{ - "type": "object", - - "properties": { - "name": { "type": "string" }, - "credit_card": { "type": "number" } - }, - - "required": ["name"], - - "dependencies": { - "credit_card": { - "properties": { - "billing_address": { "type": "string" } - }, - "required": ["billing_address"] - } - } -} -``` - -In this example the `billing_address` field will be displayed in the form if `credit_card` is defined. - -*(Sample schemas courtesy of the [Space Telescope Science Institute](https://spacetelescope.github.io/understanding-json-schema/reference/object.html#schema-dependencies))* - -### Dynamic - -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. - -```json -{ - "title": "Person", - "type": "object", - "properties": { - "Do you have any pets?": { - "type": "string", - "enum": [ - "No", - "Yes: One", - "Yes: More than one" - ], - "default": "No" - } - }, - "required": [ - "Do you have any pets?" - ], - "dependencies": { - "Do you have any pets?": { - "oneOf": [ - { - "properties": { - "Do you have any pets?": { - "enum": [ - "No" - ] - } - } - }, - { - "properties": { - "Do you have any pets?": { - "enum": [ - "Yes: One" - ] - }, - "How old is your pet?": { - "type": "number" - } - }, - "required": [ - "How old is your pet?" - ] - }, - { - "properties": { - "Do you have any pets?": { - "enum": [ - "Yes: More than one" - ] - }, - "Do you want to get rid of any?": { - "type": "boolean" - } - }, - "required": [ - "Do you want to get rid of any?" - ] - } - ] - } - } -} -``` - -In this example the user is prompted with different follow-up questions dynamically based on their answer to the first question. - -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. ## JSON Schema supporting status @@ -1754,16 +192,16 @@ This component follows [JSON Schema](http://json-schema.org/documentation.html) ## Tips and tricks - - Custom field template: https://jsfiddle.net/hdp1kgn6/1/ - - Multi-step wizard: https://jsfiddle.net/sn4bnw9h/1/ - - Using classNames with uiSchema: https://jsfiddle.net/gfwp25we/1/ - - Conditional fields: https://jsfiddle.net/69z2wepo/88541/ - - Advanced conditional fields: https://jsfiddle.net/cowbellerina/zbfh96b1/ - - Use radio list for enums: https://jsfiddle.net/f2y3fq7L/2/ - - Reading file input data: https://jsfiddle.net/f9vcb6pL/1/ - - Custom errors messages with transformErrors : https://jsfiddle.net/revolunet/5r3swnr4/ - - 2 columns form with CSS and FieldTemplate : https://jsfiddle.net/n1k0/bw0ffnz4/1/ - - Validate and submit form from external control : https://jsfiddle.net/spacebaboon/g5a1re63/ + - Custom field template: + - Multi-step wizard: + - Using classNames with uiSchema: + - Conditional fields: + - Advanced conditional fields: + - Use radio list for enums: + - Reading file input data: + - Custom errors messages with transformErrors: + - 2 columns form with CSS and FieldTemplate: + - Validate and submit form from external control: ## Contributing diff --git a/docs/validation.md b/docs/validation.md new file mode 100644 index 0000000000..8558174dad --- /dev/null +++ b/docs/validation.md @@ -0,0 +1,99 @@ +## Form data validation + +### Live validation + +By default, form data are only validated when the form is submitted or when a new `formData` prop is passed to the `Form` component. + +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. + +Be warned that this is an expensive strategy, with possibly strong impact on performances. + +To disable validation entirely, you can set Form's `noValidate` prop to `true`. + +### HTML5 Validation + +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. + +### Custom validation + +Form data is always validated against the JSON schema. + +But it is possible to define your own custom validation rules. This is especially useful when the validation depends on several interdependent fields. + +```js +function validate(formData, errors) { + if (formData.pass1 !== formData.pass2) { + errors.pass2.addError("Passwords don't match"); + } + return errors; +} + +const schema = { + type: "object", + properties: { + pass1: {type: "string", minLength: 3}, + pass2: {type: "string", minLength: 3}, + } +}; + +render(( + +), document.getElementById("app")); +``` + +> Notes: +> - The `validate()` function must **always** return the `errors` object +> received as second argument. +> - The `validate()` function is called **after** the JSON schema validation. + +### Custom error messages + +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. + +```js +function transformErrors(errors) { + return errors.map(error => { + if (error.name === "pattern") { + error.message = "Only digits are allowed" + } + return error; + }); +} + +const schema = { + type: "object", + properties: { + onlyNumbersString: {type: "string", pattern: "^\\d*$"}, + } +}; + +render(( + +), document.getElementById("app")); +``` + +> Notes: +> - The `transformErrors()` function must return the list of errors. Modifying the list in place without returning it will result in an error. + +### Error List Display + +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. + +```js +render(( + +), document.getElementById("app")); +``` + +> Note: you can also use your own [ErrorList](#error-list-template) + +### The case of empty strings + +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`. + +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. + +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. diff --git a/mkdocs.yml b/mkdocs.yml index 0b28fda3dc..83ce2c8c69 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -1,2 +1,12 @@ site_name: react-jsonschema-form documentation -docs_dir: docs \ No newline at end of file +docs_dir: docs +theme: readthedocs + +nav: + - Introduction: index.md + - Advanced Customization: advanced-customization.md + - Definitions: definitions.md + - Dependencies: dependencies.md + - Form Customization: form-customization.md + - Validation: validation.md + - Playground: https://mozilla-services.github.io/react-jsonschema-form/ \ No newline at end of file From f62cfc4fe172d778cd72a0e8d0c556ee6d62f6ce Mon Sep 17 00:00:00 2001 From: Ashwin Ramaswami Date: Mon, 21 Jan 2019 09:04:15 -0800 Subject: [PATCH 40/90] docs: remove docs from README and link to readthedocs #1138 --- README.md | 1929 +---------------------------------------------------- 1 file changed, 6 insertions(+), 1923 deletions(-) diff --git a/README.md b/README.md index 3cd8bb163a..0d9389cb45 100644 --- a/README.md +++ b/README.md @@ -5,1937 +5,20 @@ react-jsonschema-form A simple [React](http://facebook.github.io/react/) component capable of building HTML forms out of a [JSON schema](http://json-schema.org/) and using [Bootstrap](http://getbootstrap.com/) semantics by default. -A [live playground](https://mozilla-services.github.io/react-jsonschema-form/) is hosted on gh-pages. - ![](http://i.imgur.com/M8ZCES5.gif) Testing powered by BrowserStack
    -## Table of Contents - - - [Philosophy](#philosophy) - - [Installation](#installation) - - [As a npm-based project dependency](#as-a-npm-based-project-dependency) - - [As a script served from a CDN](#as-a-script-served-from-a-cdn) - - [Usage](#usage) - - [Form initialization](#form-initialization) - - [Form event handlers](#form-event-handlers) - - [Form submission](#form-submission) - - [Form error event handler](#form-error-event-handler) - - [Form data changes](#form-data-changes) - - [Form field blur events](#form-field-blur-events) - - [Form field focus events](#form-field-focus-events) - - [Submit form programmatically](#submit-form-programmatically) - - [Form customization](#form-customization) - - [The uiSchema object](#the-uischema-object) - - [Alternative widgets](#alternative-widgets) - - [For boolean fields](#for-boolean-fields) - - [For string fields](#for-string-fields) - - [String formats](#string-formats) - - [For number and integer fields](#for-number-and-integer-fields) - - [Disabled fields](#disabled-fields) - - [Read-only fields](#read-only-fields) - - [Hidden widgets](#hidden-widgets) - - [File widgets](#file-widgets) - - [Multiple files](#multiple-files) - - [File widget input ref](#file-widget-input-ref) - - [Object fields ordering](#object-fields-ordering) - - [Object item options](#object-item-options) - - [expandable option](#expandable-option) - - [Array item options](#array-item-options) - - [orderable option](#orderable-option) - - [addable option](#addable-option) - - [removable option](#removable-option) - - [Custom CSS class names](#custom-css-class-names) - - [Custom labels for enum fields](#custom-labels-for-enum-fields) - - [Alternative JSON-Schema compliant approach](#alternative-json-schema-compliant-approach) - - [Disabled attribute for enum fields](#disabled-attribute-for-enum-fields) - - [Multiple choices list](#multiple-choices-list) - - [Autogenerated widget ids](#autogenerated-widget-ids) - - [Form action buttons](#form-action-buttons) - - [Help texts](#help-texts) - - [Title texts](#title-texts) - - [Description texts](#description-texts) - - [Auto focus](#auto-focus) - - [Textarea rows option](#textarea-rows-option) - - [Placeholders](#placeholders) - - [Field labels](#field-labels) - - [HTML5 Input Types](#html5-input-types) - - [Form attributes](#form-attributes) - - [Disabling a form](#disabling-a-form) - - [Advanced customization](#advanced-customization) - - [Field template](#field-template) - - [Array Field Template](#array-field-template) - - [Object Field Template](#object-field-template) - - [Error List template](#error-list-template) - - [Id prefix](#id-prefix) - - [Custom widgets and fields](#custom-widgets-and-fields) - - [Custom widget components](#custom-widget-components) - - [Custom component registration](#custom-component-registration) - - [Custom widget options](#custom-widget-options) - - [Customizing widgets text input](#customizing-widgets-text-input) - - [Custom field components](#custom-field-components) - - [Field props](#field-props) - - [The registry object](#the-registry-object) - - [The formContext object](#the-formcontext-object) - - [Custom array field buttons](#custom-array-field-buttons) - - [Custom SchemaField](#custom-schemafield) - - [Customizing the default fields and widgets](#customizing-the-default-fields-and-widgets) - - [Custom titles](#custom-titles) - - [Custom descriptions](#custom-descriptions) - - [Form data validation](#form-data-validation) - - [Live validation](#live-validation) - - [HTML5 Validation](#html5-validation) - - [Custom validation](#custom-validation) - - [Custom error messages](#custom-error-messages) - - [Error List Display](#error-list-display) - - [The case of empty strings](#the-case-of-empty-strings) - - [Styling your forms](#styling-your-forms) - - [Schema definitions and references](#schema-definitions-and-references) - - [Property dependencies](#property-dependencies) - - [Unidirectional](#unidirectional) - - [Bidirectional](#bidirectional) - - [Schema dependencies](#schema-dependencies) - - [Conditional](#conditional) - - [Dynamic](#dynamic) - - [JSON Schema supporting status](#json-schema-supporting-status) - - [Tips and tricks](#tips-and-tricks) - - [Contributing](#contributing) - - [Coding style](#coding-style) - - [Development server](#development-server) - - [Tests](#tests) - - [TDD](#tdd) - - [Releasing](#releasing) - - [FAQ](#faq) - - [Q: Does rjsf support oneOf, anyOf, multiple types in an array, etc.?](#q-does-rjsf-support-oneof-anyof-multiple-types-in-an-array-etc) - - [Q: Will react-jsonschema-form support Material, Ant-Design, Foundation, or [some other specific widget library or frontend style]?](#q-will-react-jsonschema-form-support-material-ant-design-foundation-or-some-other-specific-widget-library-or-frontend-style) - - [License](#license) - ---- - -## Philosophy - -react-jsonschema-form is meant to automatically generate a React form based on a [JSON Schema](http://json-schema.org/). It is a major component in the [kinto-admin](https://github.com/Kinto/kinto-admin/) project. If you want to generate a form for any data, sight unseen, simply given a JSON schema, react-jsonschema-form may be for you. If you have _a priori_ knowledge of your data and want a toolkit for generating forms for it, you might look elsewhere. - -react-jsonschema-form validates that the data conforms to the given schema, but doesn't prevent the user from inputing data that doesn't fit (for example, stripping non-numbers from a number field, or adding values to an array that is already "full"). - -## Installation - -Requires React 15.0.0+. - -> Note: The `master` branch of the repository reflects ongoing development. Releases are published as [tags](https://github.com/mozilla-services/react-jsonschema-form/releases). You should never blindly install from `master`, but rather check what the available stable releases are. - - -### As a npm-based project dependency - -``` -$ npm install react-jsonschema-form --save -``` - -> Note: While the library renders [Bootstrap](http://getbootstrap.com/) HTML semantics, you have to build and load the Bootstrap styles on your own. - -### As a script served from a CDN - -```html - -``` - -Source maps are available at [this url](https://unpkg.com/react-jsonschema-form/dist/react-jsonschema-form.js.map). - -> Note: The CDN version **does not** embed `react` or `react-dom`. - -You'll also need to alias the default export property to use the Form component: - -```jsx -const Form = JSONSchemaForm.default; -// or -const {default: Form} = JSONSchemaForm; -``` - -## Usage - -```jsx -import React, { Component } from "react"; -import { render } from "react-dom"; - -import Form from "react-jsonschema-form"; - -const schema = { - title: "Todo", - type: "object", - required: ["title"], - properties: { - title: {type: "string", title: "Title", default: "A new task"}, - done: {type: "boolean", title: "Done?", default: false} - } -}; - -const log = (type) => console.log.bind(console, type); - -render(( - -), document.getElementById("app")); -``` - -This will generate a form like this (assuming you loaded the standard [Bootstrap](http://getbootstrap.com/) stylesheet): - -![](http://i.imgur.com/DZQYPyu.png) - -### Form initialization - -Often you'll want to prefill a form with existing data; this is done by passing a `formData` prop object matching the schema: - -```jsx -const formData = { - title: "First task", - done: true -}; - -render(( - -), document.getElementById("app")); -``` - -> Note: If your form has a single field, pass a single value to `formData`. ex: `formData='Charlie'` - -> WARNING: If you have situations where your parent component can re-render, make sure you listen to the `onChange` event and update the data you pass to the `formData` attribute. - -### Form event handlers - -#### Form submission - -You can pass a function as the `onSubmit` prop of your `Form` component to listen to when the form is submitted and its data are valid. It will be passed a result object having a `formData` attribute, which is the valid form data you're usually after: - -```js -const onSubmit = ({formData}) => console.log("Data submitted: ", formData); - -render(( - -), document.getElementById("app")); -``` - -#### Form error event handler - -To react when submitted form data are invalid, pass an `onError` handler. It will be passed the list of encountered errors: - -```js -const onError = (errors) => console.log("I have", errors.length, "errors to fix"); - -render(( - -), document.getElementById("app")); -``` - -#### Form data changes - -If you plan on being notified every time the form data are updated, you can pass an `onChange` handler, which will receive the same args as `onSubmit` any time a value is updated in the form. - -#### Form field blur events - -Sometimes you may want to trigger events or modify external state when a field has been touched, so you can pass an `onBlur` handler, which will receive the id of the input that was blurred and the field value. - -#### Form field focus events - -Sometimes you may want to trigger events or modify external state when a field has been focused, so you can pass an `onFocus` handler, which will receive the id of the input that is focused and the field value. - -### Submit form programmatically -You can use the reference to get your `Form` component and call the `submit` method to submit the form programmatically without a submit button. -This method will dispatch the `submit` event of the form, and the function, that is passed to `onSubmit` props, will be called. - -```js -const onSubmit = ({formData}) => console.log("Data submitted: ", formData); -let yourForm; - -render(( - {yourForm = form;}}/> -), document.getElementById("app")); - -yourForm.submit(); -``` - -## Form customization - -### The `uiSchema` object - -JSONSchema is limited for describing how a given data type should be rendered as a form input component. That's why this lib introduces the concept of *UI schema*. - -A UI schema is basically an object literal providing information on **how** the form should be rendered, while the JSON schema tells **what**. - -The uiSchema object follows the tree structure of the form field hierarchy, and defines how each property should be rendered: - -```js -const schema = { - type: "object", - properties: { - foo: { - type: "object", - properties: { - bar: {type: "string"} - } - }, - baz: { - type: "array", - items: { - type: "object", - properties: { - description: { - "type": "string" - } - } - } - } - } -} - -const uiSchema = { - foo: { - bar: { - "ui:widget": "textarea" - }, - }, - baz: { - // note the "items" for an array - items: { - description: { - "ui:widget": "textarea" - } - } - } -} - -render(( - -), document.getElementById("app")); -``` - -### Alternative widgets - -The uiSchema `ui:widget` property tells the form which UI widget should be used to render a field. - -Example: - -```jsx -const uiSchema =  { - done: { - "ui:widget": "radio" // could also be "select" - } -}; - -render(( - -), document.getElementById("app")); -``` - -Here's a list of supported alternative widgets for different JSONSchema data types: - -#### For `boolean` fields - - * `radio`: a radio button group with `true` and `false` as selectable values; - * `select`: a select box with `true` and `false` as options; - * by default, a checkbox is used - -> Note: To set the labels for a boolean field, instead of using `true` and `false` you can set `enumNames` in your schema. Note that `enumNames` belongs in your `schema`, not the `uiSchema`, and the order is always `[true, false]`. - -#### For `string` fields - - * `textarea`: a `textarea` element is used; - * `password`: an `input[type=password]` element is used; - * `color`: an `input[type=color]` element is used; - * by default, a regular `input[type=text]` element is used. - -##### String formats - -The built-in string field also supports the JSONSchema `format` property, and will render an appropriate widget by default for the following string formats: - -- `email`: An `input[type=email]` element is used; -- `uri`: An `input[type=url]` element is used; -- `data-url`: By default, an `input[type=file]` element is used; in case the string is part of an array, multiple files will be handled automatically (see [File widgets](#file-widgets)). -- `date`: By default, an `input[type=date]` element is used; -- `date-time`: By default, an `input[type=datetime-local]` element is used. - -![](http://i.imgur.com/xqu6Lcp.png) - -Please note that, even though they are standardized, `datetime-local` and `date` input elements are not yet supported by Firefox and IE. If you plan on targeting these platforms, two alternative widgets are available: - -- `alt-datetime`: Six `select` elements are used to select the year, the month, the day, the hour, the minute and the second; -- `alt-date`: Three `select` elements are used to select the year, month and the day. - -> **Firefox 57 - 66**: Firefox partially supporting `date` and `time` input types, but not `datetime-local`, `month` or `week` - -![](http://i.imgur.com/VF5tY60.png) - -You can customize the list of years displayed in the `year` dropdown by providing a ``yearsRange`` property to ``ui:options`` in your uiSchema. Its also possible to remove the `Now` and `Clear` buttons with the `hideNowButton` and `hideClearButton` options. - -```jsx -uiSchema: { - a_date: { - "alt-datetime": { - "ui:widget": "alt-datetime", - "ui:options": { - yearsRange: [1980, 2030], - hideNowButton: true, - hideClearButton: true, - }, - }, - }, -}, -``` - -#### For `number` and `integer` fields - - * `updown`: an `input[type=number]` updown selector; - * `range`: an `input[type=range]` slider; - * `radio`: a radio button group with enum values. This can only be used when `enum` values are specified for this input. - * By default, a regular `input[type=text]` element is used. - -> Note: If JSONSchema's `minimum`, `maximum` and `multipleOf` values are defined, the `min`, `max` and `step` input attributes values will take those values. - -#### Disabled fields - -The `ui:disabled` uiSchema directive will disable all child widgets from a given field. - -#### Read-only fields - -The `ui:readonly` uiSchema directive will mark all child widgets from a given field as read-only. - -> Note: If you're wondering about the difference between a `disabled` field and a `readonly` one: Marking a field as read-only will render it greyed out, but its text value will be selectable. Disabling it will prevent its value to be selected at all. - -#### Hidden widgets - -It's possible to use a hidden widget for a field by setting its `ui:widget` uiSchema directive to `hidden`: - -```js -const schema = { - type: "object", - properties: { - foo: {type: "boolean"} - } -}; - -const uiSchema = { - foo: {"ui:widget": "hidden"} -}; -``` - -Notes: - - - Hiding widgets is only supported for `boolean`, `string`, `number` and `integer` schema types; - - A hidden widget takes its value from the `formData` prop. - -#### File widgets - -This library supports a limited form of `input[type=file]` widgets, in the sense that it will propagate file contents to form data state as [data-url](http://dataurl.net/#about)s. - -There are two ways to use file widgets. - -1. By declaring a `string` json schema type along a `data-url` [format](#string-formats): -```js -const schema = { - type: "string", - format: "data-url", -}; -``` - -2. By specifying a `ui:widget` field uiSchema directive as `file`: -```js -const schema = { - type: "string", -}; - -const uiSchema = { - "ui:widget": "file", -}; -``` - -##### Multiple files - -Multiple files selectors are supported by defining an array of strings having `data-url` as a format: - -```js -const schema = { - type: "array", - items: { - type: "string", - format: "data-url", - } -}; -``` - -> Note that storing large dataURIs into form state might slow rendering. - -##### File widget input ref - -The included `FileWidget` exposes a reference to the `` element node as an `inputRef` component property. - -This allows you to programmatically trigger the browser's file selector, which can be used in a custom file widget. - -### Object fields ordering - -Since the order of object properties in Javascript and JSON is not guaranteed, the `uiSchema` object spec allows you to define the order in which properties are rendered using the `ui:order` property: - -```jsx -const schema = { - type: "object", - properties: { - foo: {type: "string"}, - bar: {type: "string"} - } -}; - -const uiSchema = { - "ui:order": ["bar", "foo"] -}; - -render(( - -), document.getElementById("app")); -``` - -If a guaranteed fixed order is only important for some fields, you can insert a wildcard `"*"` item in your `ui:order` definition. All fields that are not referenced explicitly anywhere in the list will be rendered at that point: - -```js -const uiSchema = { - "ui:order": ["bar", "*"] -}; -``` - -### Object item options - -#### `expandable` option - -If `additionalProperties` contains a schema object, an add button for new properties is shown by default. The UX for editing properties whose names are user-defined is still experimental. - -You can turn support for `additionalProperties` off with the `expandable` option in `uiSchema`: - -```jsx -const uiSchema = { - "ui:options": { - expandable: false - } -}; -``` - -### Array item options - -#### `orderable` option - -Array items are orderable by default, and react-jsonschema-form renders move up/down buttons alongside them. The `uiSchema` object spec allows you to disable ordering: - -```jsx -const schema = { - type: "array", - items: { - type: "string" - } -}; - -const uiSchema = { - "ui:options": { - orderable: false - } -}; -``` - -#### `addable` option - -If either `items` or `additionalItems` contains a schema object, an add button for new items is shown by default. You can turn this off with the `addable` option in `uiSchema`: - -```jsx -const uiSchema = { - "ui:options": { - addable: false - } -}; -``` - -#### `removable` option - -A remove button is shown by default for an item if `items` contains a schema object, or the item is an `additionalItems` instance. You can turn this off with the `removable` option in `uiSchema`: - -```jsx -const uiSchema = { - "ui:options": { - removable: false - } -}; -``` - -### Custom CSS class names - -The uiSchema object accepts a `classNames` property for each field of the schema: - -```jsx -const uiSchema = { - title: { - classNames: "task-title foo-bar" - } -}; -``` - -Will result in: - -```html -
    - -
    -``` - -### Custom labels for `enum` fields - -This library supports the [`enumNames`](https://github.com/json-schema/json-schema/wiki/enumNames-%28v5-proposal%29) property for `enum` fields, which allows defining custom labels for each option of an `enum`: - -```js -const schema = { - type: "number", - enum: [1, 2, 3], - enumNames: ["one", "two", "three"] -}; -``` - -This will be rendered using a select box like this: - -```html - -``` - -Note that string representations of numbers will be cast back and reflected as actual numbers into form state. - -#### Alternative JSON-Schema compliant approach - -JSON Schema has an alternative approach to enumerations; react-jsonschema-form supports it as well. - -```js -const schema = { - "type": "number", - "anyOf": [ - { - "type": "number", - "title": "one", - "enum": [ - 1 - ] - }, - { - "type": "number", - "title": "two", - "enum": [ - 2 - ] - }, - { - "type": "number", - "title": "three", - "enum": [ - 3 - ] - } - ] -}; -``` - -This will be rendered as follows: - -```html - -``` - -This also works for radio buttons: - -```js -const schema = { - "type": "boolean", - "oneOf": [ - { - "const": true, - "title": "Yes" - }, - { - "const": false, - "title": "No" - } - ] -}; - -const uiSchema = { - "ui:widget": "radio" -}; -``` - -This will be rendered as follows: - -```html -
    -
    - -
    -
    - -
    -
    -``` - -A live example of both approaches side-by-side can be found in the **Alternatives** tab of the [playground](https://mozilla-services.github.io/react-jsonschema-form/). - -### Disabled attribute for `enum` fields - -To disable an option, use the `enumDisabled` property in uiSchema. - -```js -const schema = { - type: "string", - enum: ["one", "two", "three"], -}; - -const uiSchema={ - "ui:enumDisabled": ['two'], -} -``` - -This will be rendered using a select box as follows: - -```html - -``` - -### Multiple-choice list - -The default behavior for array fields is a list of text inputs with add/remove buttons. There are two alternative widgets for picking multiple elements from a list of choices. Typically this applies when a schema has an `enum` list for the `items` property of an `array` field, and the `uniqueItems` property set to `true`. - -Example: - -```js -const schema = { - type: "array", - title: "A multiple-choice list", - items: { - type: "string", - enum: ["foo", "bar", "fuzz", "qux"], - }, - uniqueItems: true -}; -``` - -By default, this will render a multiple select box. If you prefer a list of checkboxes, just set the uiSchema `ui:widget` directive to `checkboxes` for that field: - -```js -const uiSchema = { - "ui:widget": "checkboxes" -}; -``` - -Note that when an array property is marked as `required`, an empty array is considered valid. If array needs to be populated, you can specify the minimum number of items using the `minItems` property. - -Example: - -```js -const schema = { - type: "array", - minItems: 2, - title: "A multiple-choice list", - items: { - type: "string", - enum: ["foo", "bar", "fuzz", "qux"], - }, - uniqueItems: true -}; -``` - -By default, checkboxes are stacked. If you prefer them inline, set the `inline` property to `true`: - -```js -const uiSchema = { - "ui:widget": "checkboxes", - "ui:options": { - inline: true - } -}; -``` - -See the "Arrays" section of the [playground](https://mozilla-services.github.io/react-jsonschema-form/) for cool demos. - -### Autogenerated widget ids - -By default, this library will generate ids unique to the form for all rendered widgets. If you plan on using multiple instances of the `Form` component in a same page, it's wise to declare a root prefix for these, using the `ui:rootFieldId` uiSchema directive: - -```js -const uiSchema = { - "ui:rootFieldId": "myform" -}; -``` - -So all widgets will have an id prefixed with `myform`. - -### Form action buttons - -You can provide custom buttons to your form via the `Form` component's `children`. Otherwise a default submit button will be rendered. - -```jsx -render(( - -
    - - -
    - -), document.getElementById("app")); -``` - -> **Warning:** There needs to be a button or an input with `type="submit"` to trigger the form submission (and then the form validation). - -### Help text - -Sometimes it's convenient to add text next to a field to guide the end user filling it. This is the purpose of the `ui:help` uiSchema directive: - -```js -const schema = {type: "string"}; -const uiSchema = { - "ui:widget": "password", - "ui:help": "Hint: Make it strong!" -}; -``` - -![](http://i.imgur.com/scJUuZo.png) - -Help texts work for any kind of field at any level, and will always be rendered immediately below the field component widget(s) (after contextualized errors, if any). - -### Title texts - -Sometimes it's convenient to change a field's title. this is the purpose of the `ui:title` uiSchema directive: - -```js -const schema = {type: "string"}; -const uiSchema = { - "ui:widget": "password", - "ui:title": "Your password" -}; -``` - -### Description texts - -Sometimes it's convenient to change description a field. This is the purpose of the `ui:description` uiSchema directive: - -```js -const schema = {type: "string"}; -const uiSchema = { - "ui:widget": "password", - "ui:description": "The best password" -}; -``` - -### Auto focus - -If you want to automatically focus on a text input or textarea input, set the `ui:autofocus` uiSchema directive to `true`. - -```js -const schema = {type: "string"}; -const uiSchema = { - "ui:widget": "textarea", - "ui:autofocus": true -} -``` - -### Textarea `rows` option - -You can set the initial height of a textarea widget by specifying `rows` option. - -```js -const schema = {type: "string"}; -const uiSchema = { - "ui:widget": "textarea", - "ui:options": { - rows: 15 - } -} -``` - -### Placeholders - -You can add placeholder text to an input by using the `ui:placeholder` uiSchema directive: - -```jsx -const schema = {type: "string", format: "uri"}; -const uiSchema = { - "ui:placeholder": "http://" -}; -``` - -![](http://i.imgur.com/MbHypKg.png) - -Fields using `enum` can also use `ui:placeholder`. The value will be used as the text for the empty option in the select widget. - -```jsx -const schema = {type: "string", enum: ["First", "Second"]}; -const uiSchema = { - "ui:placeholder": "Choose an option" -}; -``` - -### Field labels - -Field labels are rendered by default. Labels may be omitted by setting the `label` option to `false` in the `ui:options` uiSchema directive. - -```jsx -const schema = {type: "string"}; -const uiSchema = { - "ui:options": { - label: false - } -}; -``` - -### HTML5 Input Types - -To change the input type (for example, `tel` or `email`) you can specify the `inputType` in the `ui:options` uiSchema directive. - -```jsx -const schema = {type: "string"}; -const uiSchema = { - "ui:options": { - inputType: 'tel' - } -}; -``` - -### Form attributes - -The `Form` component supports the following html attributes: - -```jsx -
    -``` - -### Disabling a form - -It's possible to disable the whole form by setting the `disabled` prop. The `disabled` prop is then forwarded down to each field of the form. - -```jsx - -``` - -If you just want to disable some of the fields, see the [`ui:disabled`](#disabled-fields) parameter in the `uiSchema` directive. - -## Advanced customization - - -_ | Custom Field | Custom Template | Custom Widget ---|---------- | ------------- | ---- -What it does | Overrides all behaviour | Overrides just the layout | Overrides just the input box (not layout, labels, or help, or validation) -Usage | Global or per-field | Only global | Global or per-field -Global Example | `` | `` | `` -Per-Field Example | `"ui:field": MyField` | N/A | `"ui:widget":MyWidget` -Documentation | [Field](#field-props) | [Field Template](#field-template) - [Array Template](#array-field-template) - [Object Template](#object-field-template) - [Error List Template](#error-list-template) | [Custom Widgets](#custom-widget-components) - -### Field template - -To take control over the inner organization of each field (each form row), you can define a *field template* for your form. - -A field template is basically a React stateless component being passed field-related props, allowing you to structure your form row as you like. - -```jsx -function CustomFieldTemplate(props) { - const {id, classNames, label, help, required, description, errors, children} = props; - return ( -
    - - {description} - {children} - {errors} - {help} -
    - ); -} - -render(( - , -), document.getElementById("app")); -``` - -If you want to handle the rendering of each element yourself, you can use the props `rawHelp`, `rawDescription` and `rawErrors`. -The following props are passed to a custom field template component: +## Documentation +Documentation is hosted on: https://react-jsonschema-form.readthedocs.io/ -- `id`: The id of the field in the hierarchy. You can use it to render a label targeting the wrapped widget. -- `classNames`: A string containing the base Bootstrap CSS classes, merged with any [custom ones](#custom-css-class-names) defined in your uiSchema. -- `label`: The computed label for this field, as a string. -- `description`: A component instance rendering the field description, if one is defined (this will use any [custom `DescriptionField`](#custom-descriptions) defined). -- `rawDescription`: A string containing any `ui:description` uiSchema directive defined. -- `children`: The field or widget component instance for this field row. -- `errors`: A component instance listing any encountered errors for this field. -- `rawErrors`: An array of strings listing all generated error messages from encountered errors for this field. -- `help`: A component instance rendering any `ui:help` uiSchema directive defined. -- `rawHelp`: A string containing any `ui:help` uiSchema directive defined. **NOTE:** `rawHelp` will be `undefined` if passed `ui:help` is a React component instead of a string. -- `hidden`: A boolean value stating if the field should be hidden. -- `required`: A boolean value stating if the field is required. -- `readonly`: A boolean value stating if the field is read-only. -- `disabled`: A boolean value stating if the field is disabled. -- `displayLabel`: A boolean value stating if the label should be rendered or not. This is useful for nested fields in arrays where you don't want to clutter the UI. -- `fields`: An array containing all Form's fields including your [custom fields](#custom-field-components) and the built-in fields. -- `schema`: The schema object for this field. -- `uiSchema`: The uiSchema object for this field. -- `formContext`: The `formContext` object that you passed to Form. - -> Note: you can only define a single field template for a form. If you need many, it's probably time to look at [custom fields](#custom-field-components) instead. - -### Array Field Template - -Similarly to the `FieldTemplate` you can use an `ArrayFieldTemplate` to customize how your -arrays are rendered. This allows you to customize your array, and each element in the array. - -```jsx -function ArrayFieldTemplate(props) { - return ( -
    - {props.items.map(element => element.children)} - {props.canAdd && } -
    - ); -} - -render(( - , -), document.getElementById("app")); -``` - -Please see [customArray.js](https://github.com/mozilla-services/react-jsonschema-form/blob/master/playground/samples/customArray.js) for a better example. - -The following props are passed to each `ArrayFieldTemplate`: - -- `DescriptionField`: The `DescriptionField` from the registry (in case you wanted to utilize it) -- `TitleField`: The `TitleField` from the registry (in case you wanted to utilize it). -- `canAdd`: A boolean value stating whether new elements can be added to the array. -- `className`: The className string. -- `disabled`: A boolean value stating if the array is disabled. -- `idSchema`: Object -- `items`: An array of objects representing the items in the array. Each of the items represent a child with properties described below. -- `onAddClick: (event) => void`: A function that adds a new item to the array. -- `readonly`: A boolean value stating if the array is read-only. -- `required`: A boolean value stating if the array is required. -- `schema`: The schema object for this array. -- `uiSchema`: The uiSchema object for this array field. -- `title`: A string value containing the title for the array. -- `formContext`: The `formContext` object that you passed to Form. -- `formData`: The formData for this array. - -The following props are part of each element in `items`: - -- `children`: The html for the item's content. -- `className`: The className string. -- `disabled`: A boolean value stating if the array item is disabled. -- `hasMoveDown`: A boolean value stating whether the array item can be moved down. -- `hasMoveUp`: A boolean value stating whether the array item can be moved up. -- `hasRemove`: A boolean value stating whether the array item can be removed. -- `hasToolbar`: A boolean value stating whether the array item has a toolbar. -- `index`: A number stating the index the array item occurs in `items`. -- `onDropIndexClick: (index) => (event) => void`: Returns a function that removes the item at `index`. -- `onReorderClick: (index, newIndex) => (event) => void`: Returns a function that swaps the items at `index` with `newIndex`. -- `readonly`: A boolean value stating if the array item is read-only. - -### Object Field Template - -Similarly to the `FieldTemplate` you can use an `ObjectFieldTemplate` to customize how your -objects are rendered. - -```jsx -function ObjectFieldTemplate(props) { - return ( -
    - {props.title} - {props.description} - {props.properties.map(element =>
    {element.content}
    )} -
    - ); -} - -render(( - , -), document.getElementById("app")); -``` - -Please see [customObject.js](https://github.com/mozilla-services/react-jsonschema-form/blob/master/playground/samples/customObject.js) for a better example. - -The following props are passed to each `ObjectFieldTemplate`: - -- `DescriptionField`: The `DescriptionField` from the registry (in case you wanted to utilize it) -- `TitleField`: The `TitleField` from the registry (in case you wanted to utilize it). -- `title`: A string value containing the title for the object. -- `description`: A string value containing the description for the object. -- `disabled`: A boolean value stating if the object is disabled. -- `properties`: An array of object representing the properties in the array. Each of the properties represent a child with properties described below. -- `readonly`: A boolean value stating if the object is read-only. -- `required`: A boolean value stating if the object is required. -- `schema`: The schema object for this object. -- `uiSchema`: The uiSchema object for this object field. -- `idSchema`: An object containing the id for this object & ids for it's properties. -- `formData`: The form data for the object. -- `formContext`: The `formContext` object that you passed to Form. - -The following props are part of each element in `properties`: - -- `content`: The html for the property's content. -- `name`: A string representing the property name. -- `disabled`: A boolean value stating if the object property is disabled. -- `readonly`: A boolean value stating if the property is read-only. - -### Error List template - -To take control over how the form errors are displayed, you can define an *error list template* for your form. This list is the form global error list that appears at the top of your forms. - -An error list template is basically a React stateless component being passed errors as props so you can render them as you like: - -```jsx -function ErrorListTemplate(props) { - const {errors} = props; - return ( -
      - {errors.map(error => ( -
    • - {error.stack} -
    • - ))} -
    - ); -} - -render(( - , -), document.getElementById("app")); -``` - -> Note: Your custom `ErrorList` template will only render when `showErrorList` is `true`. - -The following props are passed to `ErrorList` - -- `errors`: An array of the errors. -- `errorSchema`: The errorSchema constructed by `Form`. -- `schema`: The schema that was passed to `Form`. -- `uiSchema`: The uiSchema that was passed to `Form`. -- `formContext`: The `formContext` object that you passed to Form. - -### Id prefix - -To avoid collisions with existing ids in the DOM, it is possible to change the prefix used for ids (the default is `root`). - -```jsx -render(( - , -), document.getElementById("app")); -``` - -This will render `` instead of `` - -### Custom widgets and fields - -The API allows to specify your own custom *widget* and *field* components: - -- A *widget* represents a HTML tag for the user to enter data, eg. `input`, `select`, etc. -- A *field* usually wraps one or more widgets and most often handles internal field state; think of a field as a form row, including the labels. - -### Custom widget components - -You can provide your own custom widgets to a uiSchema for the following json data types: - -- `string` -- `number` -- `integer` -- `boolean` - -```jsx -const schema = { - type: "string" -}; - -const uiSchema = { - "ui:widget": (props) => { - return ( - props.onChange(event.target.value)} /> - ); - } -}; - -render(( - , -), document.getElementById("app")); -``` - -The following props are passed to custom widget components: - -- `id`: The generated id for this field; -- `schema`: The JSONSchema subschema object for this field; -- `value`: The current value for this field; -- `required`: The required status of this field; -- `disabled`: `true` if the widget is disabled; -- `readonly`: `true` if the widget is read-only; -- `onChange`: The value change event handler; call it with the new value everytime it changes; -- `onBlur`: The input blur event handler; call it with the the widget id and value; -- `onFocus`: The input focus event handler; call it with the the widget id and value; -- `options`: A map of options passed as a prop to the component (see [Custom widget options](#custom-widget-options)). -- `formContext`: The `formContext` object that you passed to Form. - -> Note: Prior to v0.35.0, the `options` prop contained the list of options (`label` and `value`) for `enum` fields. Since v0.35.0, it now exposes this list as the `enumOptions` property within the `options` object. - -#### Custom component registration - -Alternatively, you can register them all at once by passing the `widgets` prop to the `Form` component, and reference their identifier from the `uiSchema`: - -```jsx -const MyCustomWidget = (props) => { - return ( - props.onChange(event.target.value)} /> - ); -}; - -const widgets = { - myCustomWidget: MyCustomWidget -}; - -const uiSchema = { - "ui:widget": "myCustomWidget" -} - -render(( - -), document.getElementById("app")); -``` - -This is useful if you expose the `uiSchema` as pure JSON, which can't carry functions. - -> Note: Until 0.40.0 it was possible to register a widget as object with shape `{ component: MyCustomWidget, options: {...} }`. This undocumented API has been removed. Instead, you can register a custom widget with a React `defaultProps` property. `defaultProps.options` can be an object containing your custom options. - -#### Custom widget options - -If you need to pass options to your custom widget, you can add a `ui:options` object containing those properties. If the widget has `defaultProps`, the options will be merged with the (optional) options object from `defaultProps`: - -```jsx -const schema = { - type: "string" -}; - -function MyCustomWidget(props) { - const {options} = props; - const {color, backgroundColor} = options; - return ; -} - -MyCustomWidget.defaultProps = { - options: { - color: "red" - } -}; - -const uiSchema = { - "ui:widget": MyCustomWidget, - "ui:options": { - backgroundColor: "yellow" - } -}; - -// renders red on yellow input -render(( - -), document.getElementById("app")); -``` - -> Note: This also applies to [registered custom components](#custom-component-registration). - -> Note: Since v0.41.0, the `ui:widget` object API, where a widget and options were specified with `"ui:widget": {component, options}` shape, is deprecated. It will be removed in a future release. - -#### Customizing widgets text input - -All the widgets that render a text input use the `BaseInput` component internally. If you need to customize all text inputs without customizing all widgets individually, you can provide a `BaseInput` component in the `widgets` property of `Form` (see [Custom component registration](#custom-component-registration). - -### Custom field components - -You can provide your own field components to a uiSchema for basically any json schema data type, by specifying a `ui:field` property. - -For example, let's create and register a dumb `geo` component handling a *latitude* and a *longitude*: - -```jsx -const schema = { - type: "object", - required: ["lat", "lon"], - properties: { - lat: {type: "number"}, - lon: {type: "number"} - } -}; - -// Define a custom component for handling the root position object -class GeoPosition extends React.Component { - constructor(props) { - super(props); - this.state = {...props.formData}; - } - - onChange(name) { - return (event) => { - this.setState({ - [name]: parseFloat(event.target.value) - }, () => this.props.onChange(this.state)); - }; - } - - render() { - const {lat, lon} = this.state; - return ( -
    - - -
    - ); - } -} - -// Define the custom field component to use for the root object -const uiSchema = {"ui:field": "geo"}; - -// Define the custom field components to register; here our "geo" -// custom field component -const fields = {geo: GeoPosition}; - -// Render the form with all the properties we just defined passed -// as props -render(( - -), document.getElementById("app")); -``` - -> Note: Registered fields can be reused across the entire schema. - -#### Field props - -A field component will always be passed the following props: - - - `schema`: The JSON schema for this field; - - `uiSchema`: The [uiSchema](#the-uischema-object) for this field; - - `idSchema`: The tree of unique ids for every child field; - - `formData`: The data for this field; - - `errorSchema`: The tree of errors for this field and its children; - - `registry`: A [registry](#the-registry-object) object (read next). - - `formContext`: A [formContext](#the-formcontext-object) object (read next). - -#### The `registry` object - -The `registry` is an object containing the registered custom fields and widgets as well as root schema definitions. - - - `fields`: The [custom registered fields](#custom-field-components). By default this object contains the standard `SchemaField`, `TitleField` and `DescriptionField` components; - - `widgets`: The [custom registered widgets](#custom-widget-components), if any; - - `definitions`: The root schema [definitions](#schema-definitions-and-references), if any. - - `formContext`: The [formContext](#the-formcontext-object) object. - -The registry is passed down the component tree, so you can access it from your custom field and `SchemaField` components. - -#### The `formContext` object - -You can provide a `formContext` object to the Form, which is passed down to all fields and widgets (including [TitleField](#custom-titles) and [DescriptionField](#custom-descriptions)). Useful for implementing context aware fields and widgets. - -### Custom array field buttons - -The `ArrayField` component provides a UI to add, remove and reorder array items, and these buttons use [Bootstrap glyphicons](http://getbootstrap.com/components/#glyphicons). If you don't use glyphicons but still want to provide your own icons or texts for these buttons, you can easily do so using CSS: - -```css -i.glyphicon { display: none; } -.btn-add::after { content: 'Add'; } -.array-item-move-up::after { content: 'Move Up'; } -.array-item-move-down::after { content: 'Move Down'; } -.array-item-remove::after { content: 'Remove'; } -``` - -### Custom SchemaField - -**Warning:** This is a powerful feature as you can override the whole form behavior and easily mess it up. Handle with care. - -You can provide your own implementation of the `SchemaField` base React component for rendering any JSONSchema field type, including objects and arrays. This is useful when you want to augment a given field type with supplementary powers. - -To proceed so, pass a `fields` object having a `SchemaField` property to your `Form` component; here's a rather silly example wrapping the standard `SchemaField` lib component: - -```jsx -import SchemaField from "react-jsonschema-form/lib/components/fields/SchemaField"; - -const CustomSchemaField = function(props) { - return ( -
    -

    Yeah, I'm pretty dumb.

    - -
    - ); -}; - -const fields = { - SchemaField: CustomSchemaField -}; - -render(( - -), document.getElementById("app")); -``` - -If you're curious how this could ever be useful, have a look at the [Kinto formbuilder](https://github.com/Kinto/formbuilder) repository to see how it's used to provide editing capabilities to any form field. - -Props passed to a custom SchemaField are the same as [the ones passed to a custom field](#field-props). - -### Customizing the default fields and widgets - -You can override any default field and widget, including the internal widgets like the `CheckboxWidget` that `ObjectField` renders for boolean values. You can override any field and widget just by providing the customized fields/widgets in the `fields` and `widgets` props: - -```jsx - -const CustomCheckbox = function(props) { - return ( - - ); -}; - -const widgets = { - CheckboxWidget: CustomCheckbox -}; - -render(( - -), document.getElementById("app")); -``` - -This allows you to create a reusable customized form class with your custom fields and widgets: - -```jsx -const customFields = {StringField: CustomString}; -const customWidgets = {CheckboxWidget: CustomCheckbox}; - -function MyForm(props) { - return ; -} - -render(( - -), document.getElementById("app")); -``` - -### Custom titles - -You can provide your own implementation of the `TitleField` base React component for rendering any title. This is useful when you want to augment how titles are handled. - -Simply pass a `fields` object having a `TitleField` property to your `Form` component: - -```jsx - -const CustomTitleField = ({title, required}) => { - const legend = required ? title + '*' : title; - return
    {legend}
    ; -}; - -const fields = { - TitleField: CustomTitleField -}; - -render(( - -), document.getElementById("app")); -``` - -### Custom descriptions - -You can provide your own implementation of the `DescriptionField` base React component for rendering any description. - -Simply pass a `fields` object having a `DescriptionField` property to your `Form` component: - -```jsx - -const CustomDescriptionField = ({id, description}) => { - return
    {description}
    ; -}; - -const fields = { - DescriptionField: CustomDescriptionField -}; - -render(( - -), document.getElementById("app")); -``` - -## Form data validation - -### Live validation - -By default, form data are only validated when the form is submitted or when a new `formData` prop is passed to the `Form` component. - -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. - -Be warned that this is an expensive strategy, with possibly strong impact on performances. - -To disable validation entirely, you can set Form's `noValidate` prop to `true`. - -### HTML5 Validation - -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. - -### Custom validation - -Form data is always validated against the JSON schema. - -But it is possible to define your own custom validation rules. This is especially useful when the validation depends on several interdependent fields. - -```js -function validate(formData, errors) { - if (formData.pass1 !== formData.pass2) { - errors.pass2.addError("Passwords don't match"); - } - return errors; -} - -const schema = { - type: "object", - properties: { - pass1: {type: "string", minLength: 3}, - pass2: {type: "string", minLength: 3}, - } -}; - -render(( - -), document.getElementById("app")); -``` - -> Notes: -> - The `validate()` function must **always** return the `errors` object -> received as second argument. -> - The `validate()` function is called **after** the JSON schema validation. - -### Custom error messages - -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. - -```js -function transformErrors(errors) { - return errors.map(error => { - if (error.name === "pattern") { - error.message = "Only digits are allowed" - } - return error; - }); -} - -const schema = { - type: "object", - properties: { - onlyNumbersString: {type: "string", pattern: "^\\d*$"}, - } -}; - -render(( - -), document.getElementById("app")); -``` - -> Notes: -> - The `transformErrors()` function must return the list of errors. Modifying the list in place without returning it will result in an error. - -### Error List Display - -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. - -```js -render(( - -), document.getElementById("app")); -``` - -> Note: you can also use your own [ErrorList](#error-list-template) - -### The case of empty strings - -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`. - -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. - -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. - -## Styling your forms - -This library renders form fields and widgets leveraging the [Bootstrap](http://getbootstrap.com/) semantics. That means your forms will be beautiful by default if you're loading its stylesheet in your page. - -You're not necessarily forced to use Bootstrap; while it uses its semantics, it also provides a bunch of other class names so you can bring new styles or override default ones quite easily in your own personalized stylesheet. That's just HTML after all :) - -If you're okay with using styles from the Bootstrap ecosystem though, then the good news is that you have access to many themes for it, which are compatible with our generated forms! - -Here are some examples from the [playground](http://mozilla-services.github.io/react-jsonschema-form/), using some of the [Bootswatch](http://bootswatch.com/) free themes: - -![](http://i.imgur.com/1Z5oUK3.png) -![](http://i.imgur.com/IMFqMwK.png) -![](http://i.imgur.com/HOACwt5.png) - -Last, if you really really want to override the semantics generated by the lib, you can always create and use your own custom [widget](#custom-widget-components), [field](#custom-field-components) and/or [schema field](#custom-schemafield) components. - -## Schema definitions and references - -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*: - -```json -{ - "definitions": { - "address": { - "type": "object", - "properties": { - "street_address": { "type": "string" }, - "city": { "type": "string" }, - "state": { "type": "string" } - }, - "required": ["street_address", "city", "state"] - } - }, - "type": "object", - "properties": { - "billing_address": { "$ref": "#/definitions/address" }, - "shipping_address": { "$ref": "#/definitions/address" } - } -} -``` - -*(Sample schema courtesy of the [Space Telescope Science Institute](http://spacetelescope.github.io/understanding-json-schema/structuring.html))* - -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. - -## Property dependencies - -This library supports conditionally making fields required based on the presence of other fields. - -### Unidirectional - -In the following example the `billing_address` field will be required if `credit_card` is defined. - -```json -{ - "type": "object", - - "properties": { - "name": { "type": "string" }, - "credit_card": { "type": "number" }, - "billing_address": { "type": "string" } - }, - - "required": ["name"], - - "dependencies": { - "credit_card": ["billing_address"] - } -} -``` - -### Bidirectional - -In the following example the `billing_address` field will be required if `credit_card` is defined and the `credit_card` -field will be required if `billing_address` is defined making them both required if either is defined. - -```json -{ - "type": "object", - - "properties": { - "name": { "type": "string" }, - "credit_card": { "type": "number" }, - "billing_address": { "type": "string" } - }, - - "required": ["name"], - - "dependencies": { - "credit_card": ["billing_address"], - "billing_address": ["credit_card"] - } -} -``` - -*(Sample schemas courtesy of the [Space Telescope Science Institute](https://spacetelescope.github.io/understanding-json-schema/reference/object.html#property-dependencies))* - -## Schema dependencies - -This library also supports modifying portions of a schema based on form data. - -### Conditional - -```json -{ - "type": "object", - - "properties": { - "name": { "type": "string" }, - "credit_card": { "type": "number" } - }, - - "required": ["name"], - - "dependencies": { - "credit_card": { - "properties": { - "billing_address": { "type": "string" } - }, - "required": ["billing_address"] - } - } -} -``` - -In this example the `billing_address` field will be displayed in the form if `credit_card` is defined. - -*(Sample schemas courtesy of the [Space Telescope Science Institute](https://spacetelescope.github.io/understanding-json-schema/reference/object.html#schema-dependencies))* - -### Dynamic - -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. - -```json -{ - "title": "Person", - "type": "object", - "properties": { - "Do you have any pets?": { - "type": "string", - "enum": [ - "No", - "Yes: One", - "Yes: More than one" - ], - "default": "No" - } - }, - "required": [ - "Do you have any pets?" - ], - "dependencies": { - "Do you have any pets?": { - "oneOf": [ - { - "properties": { - "Do you have any pets?": { - "enum": [ - "No" - ] - } - } - }, - { - "properties": { - "Do you have any pets?": { - "enum": [ - "Yes: One" - ] - }, - "How old is your pet?": { - "type": "number" - } - }, - "required": [ - "How old is your pet?" - ] - }, - { - "properties": { - "Do you have any pets?": { - "enum": [ - "Yes: More than one" - ] - }, - "Do you want to get rid of any?": { - "type": "boolean" - } - }, - "required": [ - "Do you want to get rid of any?" - ] - } - ] - } - } -} -``` - -In this example the user is prompted with different follow-up questions dynamically based on their answer to the first question. - -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. - -## JSON Schema supporting status - -This component follows [JSON Schema](http://json-schema.org/documentation.html) specs. Due to the limitation of form widgets, there are some exceptions as follows: - -* `additionalItems` keyword for arrays - - This keyword works when `items` is an array. `additionalItems: true` is not supported because there's no widget to represent an item of any type. In this case it will be treated as no additional items allowed. `additionalItems` being a valid schema is supported. - -* `anyOf`, `allOf`, and `oneOf`, or multiple `types` (i.e. `"type": ["string", "array"]`) - - The `anyOf` and `oneOf` keywords are supported, however, properties declared inside the `anyOf/oneOf` should not overlap with properties "outside" of the `anyOf/oneOf`. - - You can also use `oneOf` with [schema dependencies](#schema-dependencies) to dynamically add schema properties based on input data. - -## Tips and tricks - - - Custom field template: https://jsfiddle.net/hdp1kgn6/1/ - - Multi-step wizard: https://jsfiddle.net/sn4bnw9h/1/ - - Using classNames with uiSchema: https://jsfiddle.net/gfwp25we/1/ - - Conditional fields: https://jsfiddle.net/69z2wepo/88541/ - - Advanced conditional fields: https://jsfiddle.net/cowbellerina/zbfh96b1/ - - Use radio list for enums: https://jsfiddle.net/f2y3fq7L/2/ - - Reading file input data: https://jsfiddle.net/f9vcb6pL/1/ - - Custom errors messages with transformErrors : https://jsfiddle.net/revolunet/5r3swnr4/ - - 2 columns form with CSS and FieldTemplate : https://jsfiddle.net/n1k0/bw0ffnz4/1/ - - Validate and submit form from external control : https://jsfiddle.net/spacebaboon/g5a1re63/ +## Live Playground +A [live playground](https://mozilla-services.github.io/react-jsonschema-form/) is hosted on gh-pages. ## Contributing - -### Coding style - -All the JavaScript code in this project conforms to the [prettier](https://github.com/prettier/prettier) coding style. A command is provided to ensure your code is always formatted accordingly: - -``` -$ npm run cs-format -``` - -The `cs-check` command ensures all files conform to that style: - -``` -$ npm run cs-check -``` - -### Development server - -``` -$ npm start -``` - -A live development server showcasing components with hot reload enabled is available at [localhost:8080](http://localhost:8080). - -If you want the development server to listen on another host or port, you can use the RJSF_DEV_SERVER env variable: - -``` -$ RJSF_DEV_SERVER=0.0.0.0:8000 npm start -``` - -### Tests - -``` -$ npm test -``` - -#### TDD - -``` -$ npm run tdd -``` - -### Releasing - -``` -$ edit package.json # update version number -$ git commit -m "Bump version $VERSION" -$ git tag v$VERSION -$ npm run dist -$ npm publish -$ git push --tags origin master -``` - -## FAQ - -### Q: Does rjsf support `oneOf`, `anyOf`, multiple types in an array, etc.? - -A: The `anyOf` and `oneOf` keywords are supported, however, properties declared inside the `anyOf/oneOf` should not overlap with properties "outside" of the `anyOf/oneOf`. - -There is also special cased where you can use `oneOf` in [schema dependencies](#schema-dependencies), If you'd like to help improve support for these keywords, see the following issues for inspiration [#329](https://github.com/mozilla-services/react-jsonschema-form/pull/329) or [#417](https://github.com/mozilla-services/react-jsonschema-form/pull/417). See also: [#52](https://github.com/mozilla-services/react-jsonschema-form/issues/52), [#151](https://github.com/mozilla-services/react-jsonschema-form/issues/151), [#171](https://github.com/mozilla-services/react-jsonschema-form/issues/171), [#200](https://github.com/mozilla-services/react-jsonschema-form/issues/200), [#282](https://github.com/mozilla-services/react-jsonschema-form/issues/282), [#302](https://github.com/mozilla-services/react-jsonschema-form/pull/302), [#330](https://github.com/mozilla-services/react-jsonschema-form/issues/330), [#430](https://github.com/mozilla-services/react-jsonschema-form/issues/430), [#522](https://github.com/mozilla-services/react-jsonschema-form/issues/522), [#538](https://github.com/mozilla-services/react-jsonschema-form/issues/538), [#551](https://github.com/mozilla-services/react-jsonschema-form/issues/551), [#552](https://github.com/mozilla-services/react-jsonschema-form/issues/552), or [#648](https://github.com/mozilla-services/react-jsonschema-form/issues/648). - -### Q: Will react-jsonschema-form support Material, Ant-Design, Foundation, or [some other specific widget library or frontend style]? - -A: Probably not. We use Bootstrap v3 and it works fine for our needs. We would like for react-jsonschema-form to support other frameworks, we just don't want to support them ourselves. Ideally, these frontend styles could be added to react-jsonschema-form with a third-party library. If there is a technical limitation preventing this, please consider opening a PR. See also: [#91](https://github.com/mozilla-services/react-jsonschema-form/issues/91), [#99](https://github.com/mozilla-services/react-jsonschema-form/issues/99), [#125](https://github.com/mozilla-services/react-jsonschema-form/issues/125), [#237](https://github.com/mozilla-services/react-jsonschema-form/issues/237), [#287](https://github.com/mozilla-services/react-jsonschema-form/issues/287), [#299](https://github.com/mozilla-services/react-jsonschema-form/issues/299), [#440](https://github.com/mozilla-services/react-jsonschema-form/issues/440), [#461](https://github.com/mozilla-services/react-jsonschema-form/issues/461), [#546](https://github.com/mozilla-services/react-jsonschema-form/issues/546), [#555](https://github.com/mozilla-services/react-jsonschema-form/issues/555), [#626](https://github.com/mozilla-services/react-jsonschema-form/issues/626), and [#623](https://github.com/mozilla-services/react-jsonschema-form/pull/623). - -### Q: Is there a way to "collapse" fields, for instance to show/hide individual fields? - -A: There's no specific built-in way to do this, but you can write your own FieldTemplate that supports hiding/showing fields according to user input. We don't yet have an example of this use, but if you write one, please add it to the "tips and tricks" section, above. See also: [#268](https://github.com/mozilla-services/react-jsonschema-form/issues/268), [#304](https://github.com/mozilla-services/react-jsonschema-form/pull/304), [#598](https://github.com/mozilla-services/react-jsonschema-form/issues/598), [#920](https://github.com/mozilla-services/react-jsonschema-form/issues/920). +Read our [contributors' guide](https://react-jsonschema-form.readthedocs.io/en/latest/#contributing) to get started. ## License - -Apache 2 +Apache 2 \ No newline at end of file From bc15e23f5a146fdbfb823fffefc48b3e97659c18 Mon Sep 17 00:00:00 2001 From: Ashwin Ramaswami Date: Mon, 21 Jan 2019 09:05:27 -0800 Subject: [PATCH 41/90] doc: fix browserstack logo size --- README.md | 2 +- docs/index.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 0d9389cb45..1e7c658aa0 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@ A simple [React](http://facebook.github.io/react/) component capable of building ![](http://i.imgur.com/M8ZCES5.gif) Testing powered by BrowserStack
    - + ## Documentation diff --git a/docs/index.md b/docs/index.md index a99ca20053..916d425304 100644 --- a/docs/index.md +++ b/docs/index.md @@ -10,7 +10,7 @@ A [live playground](https://mozilla-services.github.io/react-jsonschema-form/) i ![Image](https://i.imgur.com/M8ZCES5.gif) Testing powered by BrowserStack
    - + ## Philosophy From a426124e1fa75a082c0ad4f5750dfdf92b26b70a Mon Sep 17 00:00:00 2001 From: Julien Bouquillon Date: Mon, 21 Jan 2019 18:09:48 +0100 Subject: [PATCH 42/90] add ui:help tips (#1145) * add ui:help tips from https://github.com/mozilla-services/react-jsonschema-form/issues/743#event-2084678361 --- docs/index.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/index.md b/docs/index.md index 916d425304..b90ad42e3d 100644 --- a/docs/index.md +++ b/docs/index.md @@ -202,6 +202,7 @@ This component follows [JSON Schema](http://json-schema.org/documentation.html) - Custom errors messages with transformErrors: - 2 columns form with CSS and FieldTemplate: - Validate and submit form from external control: + - Custom component for Help text with `ui:help`: ## Contributing From 3f35d788e8941b827f576d271671b6ffb84797c2 Mon Sep 17 00:00:00 2001 From: Ashwin Ramaswami Date: Mon, 21 Jan 2019 09:10:36 -0800 Subject: [PATCH 43/90] Add onBlur and onFocus events for radio and checkbox widgets (#1143) * fix: add onBlur and onFocus events for radio and checkbox widgets * fix: use event.target.checked instead of event.target.value for checkbox --- src/components/widgets/CheckboxWidget.js | 4 + src/components/widgets/RadioWidget.js | 4 + test/BooleanField_test.js | 132 +++++++++++++++++++++++ 3 files changed, 140 insertions(+) diff --git a/src/components/widgets/CheckboxWidget.js b/src/components/widgets/CheckboxWidget.js index 0a87532efe..f583f5e7a4 100644 --- a/src/components/widgets/CheckboxWidget.js +++ b/src/components/widgets/CheckboxWidget.js @@ -12,6 +12,8 @@ function CheckboxWidget(props) { readonly, label, autofocus, + onBlur, + onFocus, onChange, } = props; return ( @@ -28,6 +30,8 @@ function CheckboxWidget(props) { disabled={disabled || readonly} autoFocus={autofocus} onChange={event => onChange(event.target.checked)} + onBlur={onBlur && (event => onBlur(id, event.target.checked))} + onFocus={onFocus && (event => onFocus(id, event.target.checked))} /> {label} diff --git a/src/components/widgets/RadioWidget.js b/src/components/widgets/RadioWidget.js index 962fc069bb..8bd7dcf8c0 100644 --- a/src/components/widgets/RadioWidget.js +++ b/src/components/widgets/RadioWidget.js @@ -9,6 +9,8 @@ function RadioWidget(props) { disabled, readonly, autofocus, + onBlur, + onFocus, onChange, id, } = props; @@ -36,6 +38,8 @@ function RadioWidget(props) { disabled={disabled || itemDisabled || readonly} autoFocus={autofocus && i === 0} onChange={_ => onChange(option.value)} + onBlur={onBlur && (event => onBlur(id, event.target.value))} + onFocus={onFocus && (event => onFocus(id, event.target.value))} /> {option.label} diff --git a/test/BooleanField_test.js b/test/BooleanField_test.js index 57cfa11591..ea9d43b7a6 100644 --- a/test/BooleanField_test.js +++ b/test/BooleanField_test.js @@ -245,6 +245,50 @@ describe("BooleanField", () => { expect(node.querySelectorAll(".radio-inline")).to.have.length.of(2); }); + it("should handle a focus event for radio widgets", () => { + const onFocus = sandbox.spy(); + const { node } = createFormComponent({ + schema: { + type: "boolean", + default: false, + }, + uiSchema: { + "ui:widget": "radio", + }, + onFocus, + }); + + const element = node.querySelector(".field-radio-group"); + Simulate.focus(node.querySelector("input"), { + target: { + value: false, + }, + }); + expect(onFocus.calledWith(element.id, false)).to.be.true; + }); + + it("should handle a blur event for radio widgets", () => { + const onBlur = sandbox.spy(); + const { node } = createFormComponent({ + schema: { + type: "boolean", + default: false, + }, + uiSchema: { + "ui:widget": "radio", + }, + onBlur, + }); + + const element = node.querySelector(".field-radio-group"); + Simulate.blur(node.querySelector("input"), { + target: { + value: false, + }, + }); + expect(onBlur.calledWith(element.id, false)).to.be.true; + }); + it("should support enumNames for select", () => { const { node } = createFormComponent({ schema: { @@ -262,6 +306,50 @@ describe("BooleanField", () => { expect(labels).eql(["", "Yes", "No"]); }); + it("should handle a focus event with checkbox", () => { + const onFocus = sandbox.spy(); + const { node } = createFormComponent({ + schema: { + type: "boolean", + default: false, + }, + uiSchema: { + "ui:widget": "select", + }, + onFocus, + }); + + const element = node.querySelector("select"); + Simulate.focus(element, { + target: { + value: false, + }, + }); + expect(onFocus.calledWith(element.id, false)).to.be.true; + }); + + it("should handle a blur event with select", () => { + const onBlur = sandbox.spy(); + const { node } = createFormComponent({ + schema: { + type: "boolean", + default: false, + }, + uiSchema: { + "ui:widget": "select", + }, + onBlur, + }); + + const element = node.querySelector("select"); + Simulate.blur(element, { + target: { + value: false, + }, + }); + expect(onBlur.calledWith(element.id, false)).to.be.true; + }); + it("should render the widget with the expected id", () => { const { node } = createFormComponent({ schema: { @@ -285,6 +373,50 @@ describe("BooleanField", () => { expect(node.querySelector("#custom")).to.exist; }); + it("should handle a focus event with checkbox", () => { + const onFocus = sandbox.spy(); + const { node } = createFormComponent({ + schema: { + type: "boolean", + default: false, + }, + uiSchema: { + "ui:widget": "checkbox", + }, + onFocus, + }); + + const element = node.querySelector("input"); + Simulate.focus(element, { + target: { + checked: false, + }, + }); + expect(onFocus.calledWith(element.id, false)).to.be.true; + }); + + it("should handle a blur event with checkbox", () => { + const onBlur = sandbox.spy(); + const { node } = createFormComponent({ + schema: { + type: "boolean", + default: false, + }, + uiSchema: { + "ui:widget": "checkbox", + }, + onBlur, + }); + + const element = node.querySelector("input"); + Simulate.blur(element, { + target: { + checked: false, + }, + }); + expect(onBlur.calledWith(element.id, false)).to.be.true; + }); + describe("Label", () => { const Widget = props =>
    ; From 9fdab86d914e16f58fd42b71647d059860b18fd7 Mon Sep 17 00:00:00 2001 From: Ashwin Ramaswami Date: Mon, 21 Jan 2019 09:40:27 -0800 Subject: [PATCH 44/90] doc: add doc build instructions, update PR/issue templates with doc links --- ISSUE_TEMPLATE.md | 5 ++--- PULL_REQUEST_TEMPLATE.md | 5 ++--- docs/index.md | 10 ++++++++++ 3 files changed, 14 insertions(+), 6 deletions(-) diff --git a/ISSUE_TEMPLATE.md b/ISSUE_TEMPLATE.md index 91aa129f11..757feefc6b 100644 --- a/ISSUE_TEMPLATE.md +++ b/ISSUE_TEMPLATE.md @@ -1,7 +1,8 @@ ### Prerequisites -- [ ] I have read the [documentation](https://github.com/mozilla-services/react-jsonschema-form/blob/master/README.md#readme); +- [ ] I have read the [documentation](https://react-jsonschema-form.readthedocs.io/); - [ ] In the case of a bug report, I understand that providing a [SSCCE](http://sscce.org/) example is tremendously useful to the maintainers. +- [ ] 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. ### Description @@ -13,8 +14,6 @@ 2. [Second Step] 3. [and so on...] -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. - #### Expected behavior [What you expected to happen] diff --git a/PULL_REQUEST_TEMPLATE.md b/PULL_REQUEST_TEMPLATE.md index ff7da6a167..212fe4cd99 100644 --- a/PULL_REQUEST_TEMPLATE.md +++ b/PULL_REQUEST_TEMPLATE.md @@ -7,11 +7,10 @@ If this is related to existing tickets, include links to them as well. ### Checklist * [ ] **I'm updating documentation** - - [ ] I've checked the rendering of the Markdown text I've added - - [ ] If I'm adding a new section, I've updated the Table of Content + - [ ] I've [checked the rendering](https://react-jsonschema-form.readthedocs.io/en/latest/#contributing) of the Markdown text I've added * [ ] **I'm adding or updating code** - [ ] I've added and/or updated tests - - [ ] I've updated docs if needed + - [ ] I've updated [docs](https://react-jsonschema-form.readthedocs.io/) if needed - [ ] I've run `npm run cs-format` on my branch to conform my code to [prettier](https://github.com/prettier/prettier) coding style * [ ] **I'm adding a new feature** - [ ] I've updated the playground with an example use of the feature diff --git a/docs/index.md b/docs/index.md index 916d425304..9015794877 100644 --- a/docs/index.md +++ b/docs/index.md @@ -233,6 +233,16 @@ If you want the development server to listen on another host or port, you can us $ RJSF_DEV_SERVER=0.0.0.0:8000 npm start ``` +### Build documentation + +We use [mkdocs](https://www.mkdocs.org/) to build our documentation. To run documentation locally, run: +``` +$ pip install mkdocs==1.0.4 +$ mkdocs serve +``` + +Documentation will be served by [localhost:8000](http://localhost:8000). + ### Tests ``` From 15bc7516d43ac940748889f72afec49377893568 Mon Sep 17 00:00:00 2001 From: Ashwin Ramaswami Date: Mon, 21 Jan 2019 10:13:24 -0800 Subject: [PATCH 45/90] doc: add permalinks and fix internal hyperlinks in documentation --- docs/index.md | 6 +++--- docs/validation.md | 2 +- mkdocs.yml | 6 +++++- 3 files changed, 9 insertions(+), 5 deletions(-) diff --git a/docs/index.md b/docs/index.md index a844300b30..e730cb43e4 100644 --- a/docs/index.md +++ b/docs/index.md @@ -173,7 +173,7 @@ Here are some examples from the [playground](http://mozilla-services.github.io/r ![](https://i.imgur.com/IMFqMwK.png) ![](https://i.imgur.com/HOACwt5.png) -Last, if you really really want to override the semantics generated by the lib, you can always create and use your own custom [widget](#custom-widget-components), [field](#custom-field-components) and/or [schema field](#custom-schemafield) components. +Last, if you really really want to override the semantics generated by the lib, you can always create and use your own custom [widget](advanced-customization.md#custom-widget-components), [field](advanced-customization.md#custom-field-components) and/or [schema field](advanced-customization.md#custom-schemafield) components. ## JSON Schema supporting status @@ -188,7 +188,7 @@ This component follows [JSON Schema](http://json-schema.org/documentation.html) The `anyOf` and `oneOf` keywords are supported, however, properties declared inside the `anyOf/oneOf` should not overlap with properties "outside" of the `anyOf/oneOf`. - You can also use `oneOf` with [schema dependencies](#schema-dependencies) to dynamically add schema properties based on input data. + You can also use `oneOf` with [schema dependencies](dependencies.md#schema-dependencies) to dynamically add schema properties based on input data. ## Tips and tricks @@ -272,7 +272,7 @@ $ git push --tags origin master ### Q: Does rjsf support `oneOf`, `anyOf`, multiple types in an array, etc.? A: The `anyOf` and `oneOf` keywords are supported, however, properties declared inside the `anyOf/oneOf` should not overlap with properties "outside" of the `anyOf/oneOf`. -There is also special cased where you can use `oneOf` in [schema dependencies](#schema-dependencies), If you'd like to help improve support for these keywords, see the following issues for inspiration [#329](https://github.com/mozilla-services/react-jsonschema-form/pull/329) or [#417](https://github.com/mozilla-services/react-jsonschema-form/pull/417). See also: [#52](https://github.com/mozilla-services/react-jsonschema-form/issues/52), [#151](https://github.com/mozilla-services/react-jsonschema-form/issues/151), [#171](https://github.com/mozilla-services/react-jsonschema-form/issues/171), [#200](https://github.com/mozilla-services/react-jsonschema-form/issues/200), [#282](https://github.com/mozilla-services/react-jsonschema-form/issues/282), [#302](https://github.com/mozilla-services/react-jsonschema-form/pull/302), [#330](https://github.com/mozilla-services/react-jsonschema-form/issues/330), [#430](https://github.com/mozilla-services/react-jsonschema-form/issues/430), [#522](https://github.com/mozilla-services/react-jsonschema-form/issues/522), [#538](https://github.com/mozilla-services/react-jsonschema-form/issues/538), [#551](https://github.com/mozilla-services/react-jsonschema-form/issues/551), [#552](https://github.com/mozilla-services/react-jsonschema-form/issues/552), or [#648](https://github.com/mozilla-services/react-jsonschema-form/issues/648). +There is also special cased where you can use `oneOf` in [schema dependencies](dependencies.md#schema-dependencies), If you'd like to help improve support for these keywords, see the following issues for inspiration [#329](https://github.com/mozilla-services/react-jsonschema-form/pull/329) or [#417](https://github.com/mozilla-services/react-jsonschema-form/pull/417). See also: [#52](https://github.com/mozilla-services/react-jsonschema-form/issues/52), [#151](https://github.com/mozilla-services/react-jsonschema-form/issues/151), [#171](https://github.com/mozilla-services/react-jsonschema-form/issues/171), [#200](https://github.com/mozilla-services/react-jsonschema-form/issues/200), [#282](https://github.com/mozilla-services/react-jsonschema-form/issues/282), [#302](https://github.com/mozilla-services/react-jsonschema-form/pull/302), [#330](https://github.com/mozilla-services/react-jsonschema-form/issues/330), [#430](https://github.com/mozilla-services/react-jsonschema-form/issues/430), [#522](https://github.com/mozilla-services/react-jsonschema-form/issues/522), [#538](https://github.com/mozilla-services/react-jsonschema-form/issues/538), [#551](https://github.com/mozilla-services/react-jsonschema-form/issues/551), [#552](https://github.com/mozilla-services/react-jsonschema-form/issues/552), or [#648](https://github.com/mozilla-services/react-jsonschema-form/issues/648). ### Q: Will react-jsonschema-form support Material, Ant-Design, Foundation, or [some other specific widget library or frontend style]? diff --git a/docs/validation.md b/docs/validation.md index 8558174dad..4978ba7070 100644 --- a/docs/validation.md +++ b/docs/validation.md @@ -88,7 +88,7 @@ render(( ), document.getElementById("app")); ``` -> Note: you can also use your own [ErrorList](#error-list-template) +> Note: you can also use your own [ErrorList](advanced-customization.md#error-list-template) ### The case of empty strings diff --git a/mkdocs.yml b/mkdocs.yml index 83ce2c8c69..d3d8fcbdcc 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -9,4 +9,8 @@ nav: - Dependencies: dependencies.md - Form Customization: form-customization.md - Validation: validation.md - - Playground: https://mozilla-services.github.io/react-jsonschema-form/ \ No newline at end of file + - Playground: https://mozilla-services.github.io/react-jsonschema-form/ + +markdown_extensions: + - toc: + permalink: true \ No newline at end of file From 26850474e173d168c93e04fc625151a4b72c3507 Mon Sep 17 00:00:00 2001 From: Logan Volkers Date: Mon, 21 Jan 2019 15:53:53 -0800 Subject: [PATCH 46/90] Document a gotcha with `additionalProperties` (#1149) --- docs/index.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/index.md b/docs/index.md index e730cb43e4..e1550ef85d 100644 --- a/docs/index.md +++ b/docs/index.md @@ -190,6 +190,8 @@ This component follows [JSON Schema](http://json-schema.org/documentation.html) You can also use `oneOf` with [schema dependencies](dependencies.md#schema-dependencies) to dynamically add schema properties based on input data. +* `"additionalProperties":false` produces incorrect schemas when used with [schema dependencies](#schema-dependencies). This library does not remove extra properties, which causes validation to fail. It is recommended to avoid setting `"additionalProperties":false` when you use schema dependencies. See [#848](https://github.com/mozilla-services/react-jsonschema-form/issues/848) [#902](https://github.com/mozilla-services/react-jsonschema-form/issues/902) [#992](https://github.com/mozilla-services/react-jsonschema-form/issues/992) + ## Tips and tricks - Custom field template: From 2de683ebe828434efb5e5674b5776c5bfb68672f Mon Sep 17 00:00:00 2001 From: Lucian Buzzo Date: Tue, 22 Jan 2019 17:23:45 +0000 Subject: [PATCH 47/90] Fix bug where matching anyOf branch is not selected correctly (#1129) This change improves the logic that selects a matching anyOf branch based on form data. Previously the behaviour was to just check if the form data was valid against an anyOf branch, however due to the permissive nature of JSON schema this has unexpected behaviour. For example, given the following schema: ```json { "type": "object", "anyOf": [ { "properties": { "foo": { "type": "string" } } }, { "properties": { "bar": { "type": "string" } } } ] } ``` The form data `{ bar: 'baz' }` will actually match the first branch. To mitigate this, when doing the matching, the branch schema is augmented to require at least one of the keys in the branch. For example the schema above would become: ```json { "type": "object", "anyOf": [ { "properties": { "foo": { "type": "string" } }, "anyOf": [ { "required": [ "foo" ] } ] }, { "properties": { "bar": { "type": "string" } }, "anyOf": [ { "required": [ "bar" ] } ] } ] } ``` Signed-off-by: Lucian --- src/components/fields/MultiSchemaField.js | 44 ++++- test/anyOf_test.js | 229 ++++++++++++++++++++++ 2 files changed, 272 insertions(+), 1 deletion(-) diff --git a/src/components/fields/MultiSchemaField.js b/src/components/fields/MultiSchemaField.js index e11b9434a9..593590e6b8 100644 --- a/src/components/fields/MultiSchemaField.js +++ b/src/components/fields/MultiSchemaField.js @@ -30,7 +30,49 @@ class AnyOfField extends Component { getMatchingOption(formData, options) { for (let i = 0; i < options.length; i++) { - if (isValid(options[i], formData)) { + const option = options[i]; + + // If the schema describes an object then we need to add slightly more + // strict matching to the schema, because unless the schema uses the + // "requires" keyword, an object will match the schema as long as it + // doesn't have matching keys with a conflicting type. To do this we use an + // "anyOf" with an array of requires. This augmentation expresses that the + // schema should match if any of the keys in the schema are present on the + // object and pass validation. + if (option.properties) { + // Create an "anyOf" schema that requires at least one of the keys in the + // "properties" object + const requiresAnyOf = { + anyOf: Object.keys(option.properties).map(key => ({ + required: [key], + })), + }; + + let augmentedSchema; + + // If the "anyOf" keyword already exists, wrap the augmentation in an "allOf" + if (option.anyOf) { + // Create a shallow clone of the option + const { ...shallowClone } = option; + + if (!shallowClone.allOf) { + shallowClone.allOf = []; + } else { + // If "allOf" already exists, shallow clone the array + shallowClone.allOf = shallowClone.allOf.slice(); + } + + shallowClone.allOf.push(requiresAnyOf); + + augmentedSchema = shallowClone; + } else { + augmentedSchema = Object.assign({}, option, requiresAnyOf); + } + + if (isValid(augmentedSchema, formData)) { + return i; + } + } else if (isValid(options[i], formData)) { return i; } } diff --git a/test/anyOf_test.js b/test/anyOf_test.js index cc845af6f7..51566c3bf1 100644 --- a/test/anyOf_test.js +++ b/test/anyOf_test.js @@ -300,6 +300,235 @@ describe("anyOf", () => { expect(node.querySelector("select").value).eql("1"); }); + it("should not change the selected option when entering values", () => { + const schema = { + type: "object", + anyOf: [ + { + title: "First method of identification", + properties: { + firstName: { + type: "string", + }, + lastName: { + type: "string", + }, + }, + }, + { + title: "Second method of identification", + properties: { + idCode: { + type: "string", + }, + }, + }, + ], + }; + + const { node } = createFormComponent({ + schema, + }); + + const $select = node.querySelector("select"); + + expect($select.value).eql("0"); + + Simulate.change($select, { + target: { value: $select.options[1].value }, + }); + + expect($select.value).eql("1"); + + Simulate.change(node.querySelector("input#root_idCode"), { + target: { value: "Lorem ipsum dolor sit amet" }, + }); + + expect($select.value).eql("1"); + }); + + it("should not change the selected option when entering values and the subschema uses `anyOf`", () => { + const schema = { + type: "object", + anyOf: [ + { + title: "First method of identification", + properties: { + firstName: { + type: "string", + }, + lastName: { + type: "string", + }, + }, + }, + { + title: "Second method of identification", + properties: { + idCode: { + type: "string", + }, + }, + anyOf: [ + { + properties: { + foo: { + type: "string", + }, + }, + }, + { + properties: { + bar: { + type: "string", + }, + }, + }, + ], + }, + ], + }; + + const { node } = createFormComponent({ + schema, + }); + + const $select = node.querySelector("select"); + + expect($select.value).eql("0"); + + Simulate.change($select, { + target: { value: $select.options[1].value }, + }); + + expect($select.value).eql("1"); + + Simulate.change(node.querySelector("input#root_idCode"), { + target: { value: "Lorem ipsum dolor sit amet" }, + }); + + expect($select.value).eql("1"); + }); + + it("should not change the selected option when entering values and the subschema uses `allOf`", () => { + const schema = { + type: "object", + anyOf: [ + { + title: "First method of identification", + properties: { + firstName: { + type: "string", + }, + lastName: { + type: "string", + }, + }, + }, + { + title: "Second method of identification", + properties: { + idCode: { + type: "string", + }, + }, + allOf: [ + { + properties: { + foo: { + type: "string", + }, + }, + }, + { + properties: { + bar: { + type: "string", + }, + }, + }, + ], + }, + ], + }; + + const { node } = createFormComponent({ + schema, + }); + + const $select = node.querySelector("select"); + + expect($select.value).eql("0"); + + Simulate.change($select, { + target: { value: $select.options[1].value }, + }); + + expect($select.value).eql("1"); + + Simulate.change(node.querySelector("input#root_idCode"), { + target: { value: "Lorem ipsum dolor sit amet" }, + }); + + expect($select.value).eql("1"); + }); + + it("should not mutate a schema that contains nested anyOf and allOf", () => { + const schema = { + type: "object", + anyOf: [ + { + properties: { + foo: { type: "string" }, + }, + allOf: [ + { + properties: { + baz: { type: "string" }, + }, + }, + ], + anyOf: [ + { + properties: { + buzz: { type: "string" }, + }, + }, + ], + }, + ], + }; + + createFormComponent({ + schema, + }); + + expect(schema).to.eql({ + type: "object", + anyOf: [ + { + properties: { + foo: { type: "string" }, + }, + allOf: [ + { + properties: { + baz: { type: "string" }, + }, + }, + ], + anyOf: [ + { + properties: { + buzz: { type: "string" }, + }, + }, + ], + }, + ], + }); + }); + describe("Arrays", () => { it("should correctly render form inputs for anyOf inside array items", () => { const schema = { From 2ccfa7ecc04610a11da7cedad0f210cb8f2dfc57 Mon Sep 17 00:00:00 2001 From: jorgerobles Date: Tue, 22 Jan 2019 22:16:28 +0100 Subject: [PATCH 48/90] Fixes #824 (#1147) * Fixes #824 * Added test * Separated tests --- src/validate.js | 2 +- test/validate_test.js | 26 ++++++++++++++++++++++++++ 2 files changed, 27 insertions(+), 1 deletion(-) diff --git a/src/validate.js b/src/validate.js index 222b163287..80d599bd43 100644 --- a/src/validate.js +++ b/src/validate.js @@ -8,7 +8,7 @@ const ajv = new Ajv({ // add custom formats ajv.addFormat( "data-url", - /^data:([a-z]+\/[a-z0-9-+.]+)?;name=(.*);base64,(.*)$/ + /^data:([a-z]+\/[a-z0-9-+.]+)?;(?:name=(.*);)?base64,(.*)$/ ); ajv.addFormat( "color", diff --git a/test/validate_test.js b/test/validate_test.js index cb437e894e..072ed61ec2 100644 --- a/test/validate_test.js +++ b/test/validate_test.js @@ -134,6 +134,32 @@ describe("Validation", () => { }); }); + describe("Data-Url validation", () => { + const schema = { + type: "object", + properties: { + dataUrlWithName: { type: "string", format: "data-url" }, + dataUrlWithoutName: { type: "string", format: "data-url" }, + }, + }; + + it("Data-Url with name is accepted", () => { + const formData = { + dataUrlWithName: "data:text/plain;name=file1.txt;base64,x=", + }; + const result = validateFormData(formData, schema); + expect(result.errors).to.have.length.of(0); + }); + + it("Data-Url without name is accepted", () => { + const formData = { + dataUrlWithoutName: "data:text/plain;base64,x=", + }; + const result = validateFormData(formData, schema); + expect(result.errors).to.have.length.of(0); + }); + }); + describe("toErrorList()", () => { it("should convert an errorSchema into a flat list", () => { expect( From 23687402a395097e021aaaae8d931eb012e1b9d3 Mon Sep 17 00:00:00 2001 From: Tony Barnes Date: Tue, 22 Jan 2019 21:39:40 +0000 Subject: [PATCH 49/90] replace submit button paragraph tag with div (#766) --- src/components/Form.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/Form.js b/src/components/Form.js index 6863d74374..87b96c0e08 100644 --- a/src/components/Form.js +++ b/src/components/Form.js @@ -250,11 +250,11 @@ export default class Form extends Component { {children ? ( children ) : ( -

    +

    -

    +
    )} ); From 174e136af4fe728eb79e92594733ea729f3d659f Mon Sep 17 00:00:00 2001 From: Ashwin Ramaswami Date: Thu, 24 Jan 2019 08:37:53 -0800 Subject: [PATCH 50/90] Fix uiSchema for additionalProperties (#1144) * fix: uiSchema for additionalProperties #1132 * doc: update readme * doc: fix toc * doc: update docs --- docs/form-customization.md | 18 +++++++++++++++--- src/components/fields/ObjectField.js | 7 +++++-- test/ArrayField_test.js | 16 ++++++++++++++++ test/ObjectField_test.js | 17 +++++++++++++++++ 4 files changed, 53 insertions(+), 5 deletions(-) diff --git a/docs/form-customization.md b/docs/form-customization.md index a6740a0dfa..07f77d938a 100644 --- a/docs/form-customization.md +++ b/docs/form-customization.md @@ -248,11 +248,23 @@ const uiSchema = { }; ``` -### Object item options +### Object additional properties -#### `expandable` option +You can define `additionalProperties` by setting its value to a schema object, such as the following: -If `additionalProperties` contains a schema object, an add button for new properties is shown by default. The UX for editing properties whose names are user-defined is still experimental. +```js +const schema = { + "type": "object", + "properties": {"type": "string"}, + "additionalProperties": {"type": "number"} +} +``` + +In this way, an add button for new properties is shown by default. The UX for editing properties whose names are user-defined is still experimental. + +You can also define `uiSchema` options for `additionalProperties` by setting the `additionalProperties` attribute in the `uiSchema`. + +#### `expandable` option You can turn support for `additionalProperties` off with the `expandable` option in `uiSchema`: diff --git a/src/components/fields/ObjectField.js b/src/components/fields/ObjectField.js index 9eb1732d8c..b6514d82e0 100644 --- a/src/components/fields/ObjectField.js +++ b/src/components/fields/ObjectField.js @@ -213,7 +213,6 @@ class ObjectField extends Component { } const Template = registry.ObjectFieldTemplate || DefaultObjectFieldTemplate; - const templateProps = { title: uiSchema["ui:title"] || title, description, @@ -230,7 +229,11 @@ class ObjectField extends Component { name={name} required={this.isRequired(name)} schema={schema.properties[name]} - uiSchema={uiSchema[name]} + uiSchema={ + addedByAdditionalProperties + ? uiSchema.additionalProperties + : uiSchema[name] + } errorSchema={errorSchema[name]} idSchema={idSchema[name]} idPrefix={idPrefix} diff --git a/test/ArrayField_test.js b/test/ArrayField_test.js index d111af65af..da17bf630f 100644 --- a/test/ArrayField_test.js +++ b/test/ArrayField_test.js @@ -1162,6 +1162,22 @@ describe("ArrayField", () => { expect(addInput.value).eql("bar"); }); + it("should apply uiSchema to additionalItems", () => { + const { node } = createFormComponent({ + schema: schemaAdditional, + uiSchema: { + additionalItems: { + "ui:title": "Custom title", + }, + }, + formData: [1, 2, "bar"], + }); + const label = node.querySelector( + "fieldset .field-string label.control-label" + ); + expect(label.textContent).eql("Custom title*"); + }); + it("should have an add button if additionalItems is an object", () => { const { node } = createFormComponent({ schema: schemaAdditional }); expect(node.querySelector(".array-item-add button")).not.to.be.null; diff --git a/test/ObjectField_test.js b/test/ObjectField_test.js index c9e83611b3..afd1fb04cb 100644 --- a/test/ObjectField_test.js +++ b/test/ObjectField_test.js @@ -412,6 +412,23 @@ describe("ObjectField", () => { expect(node.querySelectorAll(".field-string")).to.have.length.of(1); }); + it("should apply uiSchema to additionalProperties", () => { + const { node } = createFormComponent({ + schema, + uiSchema: { + additionalProperties: { + "ui:title": "CustomName", + }, + }, + formData: { + property1: "test", + }, + }); + const labels = node.querySelectorAll("label.control-label"); + expect(labels[0].textContent).eql("CustomName Key"); + expect(labels[1].textContent).eql("CustomName"); + }); + it("should pass through non-schema properties and not throw validation errors if additionalProperties is undefined", () => { const undefinedAPSchema = { ...schema, From f99954713062f584984bd8a45059b542d8151e9a Mon Sep 17 00:00:00 2001 From: "kevinq.qk" Date: Sat, 2 Feb 2019 15:11:58 +0800 Subject: [PATCH 51/90] Fixed a bug.The selector have a empty option, when use enum and the default value is 0 or false or ''. --- src/components/widgets/SelectWidget.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/widgets/SelectWidget.js b/src/components/widgets/SelectWidget.js index a5ef160355..bb20ebb215 100644 --- a/src/components/widgets/SelectWidget.js +++ b/src/components/widgets/SelectWidget.js @@ -91,7 +91,7 @@ function SelectWidget(props) { const newValue = getValue(event, multiple); onChange(processValue(schema, newValue)); }}> - {!multiple && !schema.default && } + {!multiple && schema.default === undefined && } {enumOptions.map(({ value, label }, i) => { const disabled = enumDisabled && enumDisabled.indexOf(value) != -1; return ( From 54091b1641c5fb0634cd6de444a83d7103db5238 Mon Sep 17 00:00:00 2001 From: "kevinq.qk" Date: Sat, 2 Feb 2019 15:20:25 +0800 Subject: [PATCH 52/90] style: npm run cs-format --- src/components/widgets/SelectWidget.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/components/widgets/SelectWidget.js b/src/components/widgets/SelectWidget.js index bb20ebb215..e61139a4e2 100644 --- a/src/components/widgets/SelectWidget.js +++ b/src/components/widgets/SelectWidget.js @@ -91,7 +91,9 @@ function SelectWidget(props) { const newValue = getValue(event, multiple); onChange(processValue(schema, newValue)); }}> - {!multiple && schema.default === undefined && } + {!multiple && schema.default === undefined && ( + + )} {enumOptions.map(({ value, label }, i) => { const disabled = enumDisabled && enumDisabled.indexOf(value) != -1; return ( From a1eedfb55d6b8fe73cebd5577852899862981b2f Mon Sep 17 00:00:00 2001 From: Ethan Glasser-Camp Date: Mon, 4 Feb 2019 14:07:31 -0500 Subject: [PATCH 53/90] Bump v1.2.1 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 6f39ce7d75..a88cd449c3 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "react-jsonschema-form", - "version": "1.2.0", + "version": "1.2.1", "description": "A simple React component capable of building HTML forms out of a JSON schema.", "scripts": { "build:readme": "toctoc README.md -w", From b481f58dce20b31a7fdd6fda602d131d51c28e79 Mon Sep 17 00:00:00 2001 From: Ethan Glasser-Camp Date: Mon, 4 Feb 2019 14:22:53 -0500 Subject: [PATCH 54/90] Oops, bump version in package-lock.json too --- package-lock.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package-lock.json b/package-lock.json index 8396368a42..992862858a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "react-jsonschema-form", - "version": "1.2.0", + "version": "1.2.1", "lockfileVersion": 1, "requires": true, "dependencies": { From 6e79281a3c5ea99763cfce6ea78fcfe776824bad Mon Sep 17 00:00:00 2001 From: Lucian Buzzo Date: Fri, 8 Feb 2019 16:22:41 +0000 Subject: [PATCH 55/90] Infer field type from const value (#1174) This change adds support for schemas that use the `const` keyword without and adjacent `type` keyword. For example: ```json { "type": "object", "properties": { "firstName": { "const": "Chuck" } } } ``` Signed-off-by: Lucian --- src/utils.js | 5 ++++ test/const_test.js | 60 ++++++++++++++++++++++++++++++++++++++++++++++ test/utils_test.js | 48 +++++++++++++++++++++++++++++++++++++ 3 files changed, 113 insertions(+) create mode 100644 test/const_test.js diff --git a/src/utils.js b/src/utils.js index 8c69cccae8..ad92ee494b 100644 --- a/src/utils.js +++ b/src/utils.js @@ -67,6 +67,11 @@ export function getDefaultRegistry() { export function getSchemaType(schema) { let { type } = schema; + + if (!type && schema.const) { + return guessType(schema.const); + } + if (!type && schema.enum) { type = "string"; } diff --git a/test/const_test.js b/test/const_test.js new file mode 100644 index 0000000000..d47e012bd2 --- /dev/null +++ b/test/const_test.js @@ -0,0 +1,60 @@ +import { expect } from "chai"; + +import { createFormComponent, createSandbox } from "./test_utils"; + +describe("const", () => { + let sandbox; + + beforeEach(() => { + sandbox = createSandbox(); + }); + + afterEach(() => { + sandbox.restore(); + }); + + it("should render a schema that uses const with a string value", () => { + const schema = { + type: "object", + properties: { + foo: { const: "bar" }, + }, + }; + + const { node } = createFormComponent({ + schema, + }); + + expect(node.querySelector("input#root_foo")).not.eql(null); + }); + + it("should render a schema that uses const with a number value", () => { + const schema = { + type: "object", + properties: { + foo: { const: 123 }, + }, + }; + + const { node } = createFormComponent({ + schema, + }); + + expect(node.querySelector("input#root_foo")).not.eql(null); + }); + + it("should render a schema that uses const with a boolean value", () => { + const schema = { + type: "object", + properties: { + foo: { const: true }, + }, + }; + + const { node } = createFormComponent({ + schema, + }); + + expect(node.querySelector("input#root_foo[type='checkbox']")).not.eql(null); + }); +}); diff --git a/test/utils_test.js b/test/utils_test.js index 7ba1be7601..cb32727c06 100644 --- a/test/utils_test.js +++ b/test/utils_test.js @@ -5,6 +5,7 @@ import { dataURItoBlob, deepEquals, getDefaultFormState, + getSchemaType, isFilesArray, isConstant, toConstant, @@ -1369,4 +1370,51 @@ describe("utils", () => { expect(guessType({})).eql("object"); }); }); + + describe("getSchemaType()", () => { + const cases = [ + { + schema: { type: "string" }, + expected: "string", + }, + { + schema: { type: "number" }, + expected: "number", + }, + { + schema: { type: "integer" }, + expected: "integer", + }, + { + schema: { type: "object" }, + expected: "object", + }, + { + schema: { type: "array" }, + expected: "array", + }, + { + schema: { type: "boolean" }, + expected: "boolean", + }, + { + schema: { type: "null" }, + expected: "null", + }, + { + schema: { const: "foo" }, + expected: "string", + }, + { + schema: { const: 1 }, + expected: "number", + }, + ]; + + it("should correctly guess the type of a schema", () => { + for (const test of cases) { + expect(getSchemaType(test.schema)).eql(test.expected); + } + }); + }); }); From 919a16470ffd6f4493caa80000b8fe76c318cbda Mon Sep 17 00:00:00 2001 From: Lucian Buzzo Date: Fri, 8 Feb 2019 17:04:40 +0000 Subject: [PATCH 56/90] Add various always-ignore files and directories to .gitignore (#1171) Change-type: patch Signed-off-by: Lucian --- .gitignore | 119 ++++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 117 insertions(+), 2 deletions(-) diff --git a/.gitignore b/.gitignore index 22092001c8..b37f604b77 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,120 @@ -npm-debug.log -node_modules +# Created by https://www.gitignore.io/api/osx,node,linux,windows +# Edit at https://www.gitignore.io/?templates=osx,node,linux,windows + +### Linux ### +*~ + +# temporary files which can be created if a process still has a handle open of a deleted file +.fuse_hidden* + +# KDE directory preferences +.directory + +# Linux trash folder which might appear on any partition or disk +.Trash-* + +# .nfs files are created when an open file is removed but is still being accessed +.nfs* + +### Node ### +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +# Runtime data +pids +*.pid +*.seed +*.pid.lock + +# Coverage directory used by tools like istanbul +coverage + +# nyc test coverage +.nyc_output + +# Dependency directories +node_modules/ + +# Optional npm cache directory +.npm + +# Optional eslint cache +.eslintcache + +# Optional REPL history +.node_repl_history + +# Output of 'npm pack' +*.tgz + +# Yarn Integrity file +.yarn-integrity + +# dotenv environment variables file +.env +.env.test + +### OSX ### +# General +.DS_Store +.AppleDouble +.LSOverride + +# Icon must end with two \r +Icon + +# Thumbnails +._* + +# Files that might appear in the root of a volume +.DocumentRevisions-V100 +.fseventsd +.Spotlight-V100 +.TemporaryItems +.Trashes +.VolumeIcon.icns +.com.apple.timemachine.donotpresent + +# Directories potentially created on remote AFP share +.AppleDB +.AppleDesktop +Network Trash Folder +Temporary Items +.apdisk + +### Windows ### +# Windows thumbnail cache files +Thumbs.db +ehthumbs.db +ehthumbs_vista.db + +# Dump file +*.stackdump + +# Folder config file +[Dd]esktop.ini + +# Recycle Bin used on file shares +$RECYCLE.BIN/ + +# Windows Installer files +*.cab +*.msi +*.msix +*.msm +*.msp + +# Windows shortcuts +*.lnk + +# End of https://www.gitignore.io/api/osx,node,linux,windows + +# Package specific files + build _build site From 59038c0ce0f9f215e78e0f730d9e6138b5dcbf20 Mon Sep 17 00:00:00 2001 From: Travis Dahl Date: Fri, 8 Feb 2019 14:10:34 -0800 Subject: [PATCH 57/90] Submit event should return original event as second param (#1172) --- docs/index.md | 4 ++-- playground/app.js | 7 ++++--- src/components/Form.js | 3 ++- test/Form_test.js | 6 +++--- 4 files changed, 11 insertions(+), 9 deletions(-) diff --git a/docs/index.md b/docs/index.md index e1550ef85d..0887052f5e 100644 --- a/docs/index.md +++ b/docs/index.md @@ -107,10 +107,10 @@ render(( #### Form submission -You can pass a function as the `onSubmit` prop of your `Form` component to listen to when the form is submitted and its data are valid. It will be passed a result object having a `formData` attribute, which is the valid form data you're usually after: +You can pass a function as the `onSubmit` prop of your `Form` component to listen to when the form is submitted and its data are valid. It will be passed a result object having a `formData` attribute, which is the valid form data you're usually after. The original event will also be passed as a second parameter: ```js -const onSubmit = ({formData}) => console.log("Data submitted: ", formData); +const onSubmit = ({formData}, e) => console.log("Data submitted: ", formData); render((
    - console.log("submitted formData", formData) - } + onSubmit={({ formData }, e) => { + console.log("submitted formData", formData); + console.log("submit event", e); + }} fields={{ geo: GeoPosition }} validate={validate} onBlur={(id, value) => diff --git a/src/components/Form.js b/src/components/Form.js index 87b96c0e08..182f56af38 100644 --- a/src/components/Form.js +++ b/src/components/Form.js @@ -151,6 +151,7 @@ export default class Form extends Component { onSubmit = event => { event.preventDefault(); + event.persist(); if (!this.props.noValidate) { const { errors, errorSchema } = this.validate(this.state.formData); @@ -168,7 +169,7 @@ export default class Form extends Component { this.setState({ errors: [], errorSchema: {} }, () => { if (this.props.onSubmit) { - this.props.onSubmit({ ...this.state, status: "submitted" }); + this.props.onSubmit({ ...this.state, status: "submitted" }, event); } }); }; diff --git a/test/Form_test.js b/test/Form_test.js index 23b6ea8169..3933f570d3 100644 --- a/test/Form_test.js +++ b/test/Form_test.js @@ -774,15 +774,15 @@ describe("Form", () => { foo: "bar", }; const onSubmit = sandbox.spy(); + const event = { type: "submit" }; const { comp, node } = createFormComponent({ schema, formData, onSubmit, }); - Simulate.submit(node); - - sinon.assert.calledWithMatch(onSubmit, comp.state); + Simulate.submit(node, event); + sinon.assert.calledWithMatch(onSubmit, comp.state, event); }); it("should not call provided submit handler on validation errors", () => { From aa862abdbb4fd014b584c0cb267d46eec8473939 Mon Sep 17 00:00:00 2001 From: "kevinq.qk" Date: Tue, 12 Feb 2019 23:12:25 +0800 Subject: [PATCH 58/90] test: add enum test --- test/enum_test.js | 132 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 132 insertions(+) create mode 100644 test/enum_test.js diff --git a/test/enum_test.js b/test/enum_test.js new file mode 100644 index 0000000000..a9f335f6f7 --- /dev/null +++ b/test/enum_test.js @@ -0,0 +1,132 @@ +import { expect } from "chai"; + +import { createFormComponent, createSandbox } from "./test_utils"; + +describe("enum", () => { + let sandbox; + + beforeEach(() => { + sandbox = createSandbox(); + }); + + afterEach(() => { + sandbox.restore(); + }); + + it("should render a select element if set the enum.", () => { + const schema = { + type: "object", + properties: { + foo: { + type: "string", + enum: [0], + }, + }, + }; + + const { node } = createFormComponent({ + schema, + }); + + const selects = node.querySelectorAll("select"); + expect(selects).to.have.length.of(1); + }); + + it("should render a select element and it's value is empty, if set the enum and the default value is undefined.", () => { + const schema = { + type: "object", + properties: { + foo: { + type: "string", + enum: [0], + }, + }, + }; + + const { node } = createFormComponent({ + schema, + }); + + const selects = node.querySelectorAll("select"); + expect(selects[0].value).eql(""); + }); + + it("should render a select element and it's first option has an empty innerHTML, if set the enum and the default value is undefined.", () => { + const schema = { + type: "object", + properties: { + foo: { + type: "string", + enum: [0], + }, + }, + }; + + const { node } = createFormComponent({ + schema, + }); + + const options = node.querySelectorAll("option"); + expect(options[0].innerHTML).eql(""); + }); + + it("should render a select element and it's first option is '0', if set the enum and the default value is 0.", () => { + const schema = { + type: "object", + properties: { + foo: { + type: "string", + enum: [0], + default: 0, + }, + }, + }; + + const { node } = createFormComponent({ + schema, + }); + + const options = node.querySelectorAll("option"); + expect(options[0].innerHTML).eql("0"); + }); + + it("should render a select element and it's first option is 'false', if set the enum and the default value is false.", () => { + const schema = { + type: "object", + properties: { + foo: { + type: "string", + enum: [false, true], + default: false, + }, + }, + }; + + const { node } = createFormComponent({ + schema, + }); + + const options = node.querySelectorAll("option"); + expect(options[0].innerHTML).eql("false"); + }); + + it("should render a select element and the option's length is equal the enum's length, if set the enum.", () => { + const schema = { + type: "object", + properties: { + foo: { + type: "string", + enum: [false, true], + default: false, + }, + }, + }; + + const { node } = createFormComponent({ + schema, + }); + + const options = node.querySelectorAll("option"); + expect(options.length).eql(2); + }); +}); From bece2a5a847e216c2386d4f7f0ce9cf25b549562 Mon Sep 17 00:00:00 2001 From: Lucian Buzzo Date: Tue, 12 Feb 2019 16:57:47 +0000 Subject: [PATCH 59/90] Generate code coverage reports using nyc (#1170) Change-type: patch Signed-off-by: Lucian --- .gitignore | 4 + docs/index.md | 5 + package-lock.json | 1244 +++++++++++++++++++++++++++++++++++++++++++++ package.json | 2 + 4 files changed, 1255 insertions(+) diff --git a/.gitignore b/.gitignore index b37f604b77..7f2ca177f1 100644 --- a/.gitignore +++ b/.gitignore @@ -121,3 +121,7 @@ site dist lib yarn.lock + +# Code coverage +coverage +.nyc_output diff --git a/docs/index.md b/docs/index.md index 0887052f5e..4c26d86459 100644 --- a/docs/index.md +++ b/docs/index.md @@ -258,6 +258,11 @@ $ npm test $ npm run tdd ``` +#### Code coverage + +Code coverage reports are generated using [nyc](https://github.com/istanbuljs/nyc) each time the `npm test-coverage` script is run. +The full report can be seen by opening `./coverage/lcov-report/index.html`. + ### Releasing ``` diff --git a/package-lock.json b/package-lock.json index 992862858a..49350695a2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -4,6 +4,195 @@ "lockfileVersion": 1, "requires": true, "dependencies": { + "@babel/code-frame": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.0.0.tgz", + "integrity": "sha512-OfC2uemaknXr87bdLUkWog7nYuliM9Ij5HUcajsVcMCpQrcLmtxRbVFTIqmcSkSeYRBFBRxs2FiUqFJDLdiebA==", + "dev": true, + "requires": { + "@babel/highlight": "^7.0.0" + } + }, + "@babel/generator": { + "version": "7.3.2", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.3.2.tgz", + "integrity": "sha512-f3QCuPppXxtZOEm5GWPra/uYUjmNQlu9pbAD8D/9jze4pTY83rTtB1igTBSwvkeNlC5gR24zFFkz+2WHLFQhqQ==", + "dev": true, + "requires": { + "@babel/types": "^7.3.2", + "jsesc": "^2.5.1", + "lodash": "^4.17.10", + "source-map": "^0.5.0", + "trim-right": "^1.0.1" + }, + "dependencies": { + "jsesc": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz", + "integrity": "sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==", + "dev": true + } + } + }, + "@babel/helper-function-name": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.1.0.tgz", + "integrity": "sha512-A95XEoCpb3TO+KZzJ4S/5uW5fNe26DjBGqf1o9ucyLyCmi1dXq/B3c8iaWTfBk3VvetUxl16e8tIrd5teOCfGw==", + "dev": true, + "requires": { + "@babel/helper-get-function-arity": "^7.0.0", + "@babel/template": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "@babel/helper-get-function-arity": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/@babel/helper-get-function-arity/-/helper-get-function-arity-7.0.0.tgz", + "integrity": "sha512-r2DbJeg4svYvt3HOS74U4eWKsUAMRH01Z1ds1zx8KNTPtpTL5JAsdFv8BNyOpVqdFhHkkRDIg5B4AsxmkjAlmQ==", + "dev": true, + "requires": { + "@babel/types": "^7.0.0" + } + }, + "@babel/helper-split-export-declaration": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.0.0.tgz", + "integrity": "sha512-MXkOJqva62dfC0w85mEf/LucPPS/1+04nmmRMPEBUB++hiiThQ2zPtX/mEWQ3mtzCEjIJvPY8nuwxXtQeQwUag==", + "dev": true, + "requires": { + "@babel/types": "^7.0.0" + } + }, + "@babel/highlight": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.0.0.tgz", + "integrity": "sha512-UFMC4ZeFC48Tpvj7C8UgLvtkaUuovQX+5xNWrsIoMG8o2z+XFKjKaN9iVmS84dPwVN00W4wPmqvYoZF3EGAsfw==", + "dev": true, + "requires": { + "chalk": "^2.0.0", + "esutils": "^2.0.2", + "js-tokens": "^4.0.0" + }, + "dependencies": { + "ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dev": true, + "requires": { + "color-convert": "^1.9.0" + } + }, + "chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, + "requires": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + } + }, + "has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", + "dev": true + }, + "js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true + }, + "supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "requires": { + "has-flag": "^3.0.0" + } + } + } + }, + "@babel/parser": { + "version": "7.3.2", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.3.2.tgz", + "integrity": "sha512-QzNUC2RO1gadg+fs21fi0Uu0OuGNzRKEmgCxoLNzbCdoprLwjfmZwzUrpUNfJPaVRwBpDY47A17yYEGWyRelnQ==", + "dev": true + }, + "@babel/template": { + "version": "7.2.2", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.2.2.tgz", + "integrity": "sha512-zRL0IMM02AUDwghf5LMSSDEz7sBCO2YnNmpg3uWTZj/v1rcG2BmQUvaGU8GhU8BvfMh1k2KIAYZ7Ji9KXPUg7g==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.0.0", + "@babel/parser": "^7.2.2", + "@babel/types": "^7.2.2" + } + }, + "@babel/traverse": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.2.3.tgz", + "integrity": "sha512-Z31oUD/fJvEWVR0lNZtfgvVt512ForCTNKYcJBGbPb1QZfve4WGH8Wsy7+Mev33/45fhP/hwQtvgusNdcCMgSw==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.0.0", + "@babel/generator": "^7.2.2", + "@babel/helper-function-name": "^7.1.0", + "@babel/helper-split-export-declaration": "^7.0.0", + "@babel/parser": "^7.2.3", + "@babel/types": "^7.2.2", + "debug": "^4.1.0", + "globals": "^11.1.0", + "lodash": "^4.17.10" + }, + "dependencies": { + "debug": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", + "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", + "dev": true, + "requires": { + "ms": "^2.1.1" + } + }, + "globals": { + "version": "11.10.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-11.10.0.tgz", + "integrity": "sha512-0GZF1RiPKU97IHUO5TORo9w1PwrH/NBPl+fS7oMLdaTRiYmYbwK4NWoZWrAdd0/abG9R2BU+OiwyQpTpE6pdfQ==", + "dev": true + }, + "ms": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.1.tgz", + "integrity": "sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg==", + "dev": true + } + } + }, + "@babel/types": { + "version": "7.3.2", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.3.2.tgz", + "integrity": "sha512-3Y6H8xlUlpbGR+XvawiH0UXehqydTmNmEpozWcXymqwcrwYAl5KMvKtQ+TF6f6E08V6Jur7v/ykdDSF+WDEIXQ==", + "dev": true, + "requires": { + "esutils": "^2.0.2", + "lodash": "^4.17.10", + "to-fast-properties": "^2.0.0" + }, + "dependencies": { + "to-fast-properties": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", + "integrity": "sha1-3F5pjL0HkmW8c+A3doGk5Og/YW4=", + "dev": true + } + } + }, "@webassemblyjs/ast": { "version": "1.7.8", "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.7.8.tgz", @@ -5168,6 +5357,27 @@ "integrity": "sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo=", "dev": true }, + "istanbul-lib-coverage": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.3.tgz", + "integrity": "sha512-dKWuzRGCs4G+67VfW9pBFFz2Jpi4vSp/k7zBcJ888ofV5Mi1g5CUML5GvMvV6u9Cjybftu+E8Cgp+k0dI1E5lw==", + "dev": true + }, + "istanbul-lib-instrument": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-3.1.0.tgz", + "integrity": "sha512-ooVllVGT38HIk8MxDj/OIHXSYvH+1tq/Vb38s8ixt9GoJadXska4WkGY+0wkmtYCZNYtaARniH/DixUGGLZ0uA==", + "dev": true, + "requires": { + "@babel/generator": "^7.0.0", + "@babel/parser": "^7.0.0", + "@babel/template": "^7.0.0", + "@babel/traverse": "^7.0.0", + "@babel/types": "^7.0.0", + "istanbul-lib-coverage": "^2.0.3", + "semver": "^5.5.0" + } + }, "js-base64": { "version": "2.4.5", "resolved": "https://registry.npmjs.org/js-base64/-/js-base64-2.4.5.tgz", @@ -6349,6 +6559,1040 @@ "integrity": "sha512-3iuY4N5dhgMpCUrOVnuAdGrgxVqV2cJpM+XNccjR2DKOB1RUP0aA+wGXEiNziG/UKboFyGBIoKOaNlJxx8bciQ==", "dev": true }, + "nyc": { + "version": "13.2.0", + "resolved": "https://registry.npmjs.org/nyc/-/nyc-13.2.0.tgz", + "integrity": "sha512-gQBlOqvfpYt9b2PZ7qElrHWt8x4y8ApNfbMBoDPdl3sY4/4RJwCxDGTSqhA9RnaguZjS5nW7taW8oToe86JLgQ==", + "dev": true, + "requires": { + "archy": "^1.0.0", + "arrify": "^1.0.1", + "caching-transform": "^3.0.1", + "convert-source-map": "^1.6.0", + "find-cache-dir": "^2.0.0", + "find-up": "^3.0.0", + "foreground-child": "^1.5.6", + "glob": "^7.1.3", + "istanbul-lib-coverage": "^2.0.3", + "istanbul-lib-hook": "^2.0.3", + "istanbul-lib-instrument": "^3.0.1", + "istanbul-lib-report": "^2.0.4", + "istanbul-lib-source-maps": "^3.0.2", + "istanbul-reports": "^2.1.0", + "make-dir": "^1.3.0", + "merge-source-map": "^1.1.0", + "resolve-from": "^4.0.0", + "rimraf": "^2.6.3", + "signal-exit": "^3.0.2", + "spawn-wrap": "^1.4.2", + "test-exclude": "^5.1.0", + "uuid": "^3.3.2", + "yargs": "^12.0.5", + "yargs-parser": "^11.1.1" + }, + "dependencies": { + "ansi-regex": { + "version": "3.0.0", + "bundled": true, + "dev": true + }, + "append-transform": { + "version": "1.0.0", + "bundled": true, + "dev": true, + "requires": { + "default-require-extensions": "^2.0.0" + } + }, + "archy": { + "version": "1.0.0", + "bundled": true, + "dev": true + }, + "arrify": { + "version": "1.0.1", + "bundled": true, + "dev": true + }, + "async": { + "version": "2.6.1", + "bundled": true, + "dev": true, + "requires": { + "lodash": "^4.17.10" + } + }, + "balanced-match": { + "version": "1.0.0", + "bundled": true, + "dev": true + }, + "brace-expansion": { + "version": "1.1.11", + "bundled": true, + "dev": true, + "requires": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "builtin-modules": { + "version": "1.1.1", + "bundled": true, + "dev": true + }, + "caching-transform": { + "version": "3.0.1", + "bundled": true, + "dev": true, + "requires": { + "hasha": "^3.0.0", + "make-dir": "^1.3.0", + "package-hash": "^3.0.0", + "write-file-atomic": "^2.3.0" + } + }, + "camelcase": { + "version": "5.0.0", + "bundled": true, + "dev": true + }, + "cliui": { + "version": "4.1.0", + "bundled": true, + "dev": true, + "requires": { + "string-width": "^2.1.1", + "strip-ansi": "^4.0.0", + "wrap-ansi": "^2.0.0" + } + }, + "code-point-at": { + "version": "1.1.0", + "bundled": true, + "dev": true + }, + "commander": { + "version": "2.17.1", + "bundled": true, + "dev": true, + "optional": true + }, + "commondir": { + "version": "1.0.1", + "bundled": true, + "dev": true + }, + "concat-map": { + "version": "0.0.1", + "bundled": true, + "dev": true + }, + "convert-source-map": { + "version": "1.6.0", + "bundled": true, + "dev": true, + "requires": { + "safe-buffer": "~5.1.1" + } + }, + "cross-spawn": { + "version": "4.0.2", + "bundled": true, + "dev": true, + "requires": { + "lru-cache": "^4.0.1", + "which": "^1.2.9" + } + }, + "debug": { + "version": "4.1.1", + "bundled": true, + "dev": true, + "requires": { + "ms": "^2.1.1" + } + }, + "decamelize": { + "version": "1.2.0", + "bundled": true, + "dev": true + }, + "default-require-extensions": { + "version": "2.0.0", + "bundled": true, + "dev": true, + "requires": { + "strip-bom": "^3.0.0" + } + }, + "end-of-stream": { + "version": "1.4.1", + "bundled": true, + "dev": true, + "requires": { + "once": "^1.4.0" + } + }, + "error-ex": { + "version": "1.3.2", + "bundled": true, + "dev": true, + "requires": { + "is-arrayish": "^0.2.1" + } + }, + "es6-error": { + "version": "4.1.1", + "bundled": true, + "dev": true + }, + "execa": { + "version": "1.0.0", + "bundled": true, + "dev": true, + "requires": { + "cross-spawn": "^6.0.0", + "get-stream": "^4.0.0", + "is-stream": "^1.1.0", + "npm-run-path": "^2.0.0", + "p-finally": "^1.0.0", + "signal-exit": "^3.0.0", + "strip-eof": "^1.0.0" + }, + "dependencies": { + "cross-spawn": { + "version": "6.0.5", + "bundled": true, + "dev": true, + "requires": { + "nice-try": "^1.0.4", + "path-key": "^2.0.1", + "semver": "^5.5.0", + "shebang-command": "^1.2.0", + "which": "^1.2.9" + } + } + } + }, + "find-cache-dir": { + "version": "2.0.0", + "bundled": true, + "dev": true, + "requires": { + "commondir": "^1.0.1", + "make-dir": "^1.0.0", + "pkg-dir": "^3.0.0" + } + }, + "find-up": { + "version": "3.0.0", + "bundled": true, + "dev": true, + "requires": { + "locate-path": "^3.0.0" + } + }, + "foreground-child": { + "version": "1.5.6", + "bundled": true, + "dev": true, + "requires": { + "cross-spawn": "^4", + "signal-exit": "^3.0.0" + } + }, + "fs.realpath": { + "version": "1.0.0", + "bundled": true, + "dev": true + }, + "get-caller-file": { + "version": "1.0.3", + "bundled": true, + "dev": true + }, + "get-stream": { + "version": "4.1.0", + "bundled": true, + "dev": true, + "requires": { + "pump": "^3.0.0" + } + }, + "glob": { + "version": "7.1.3", + "bundled": true, + "dev": true, + "requires": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + } + }, + "graceful-fs": { + "version": "4.1.15", + "bundled": true, + "dev": true + }, + "handlebars": { + "version": "4.0.12", + "bundled": true, + "dev": true, + "requires": { + "async": "^2.5.0", + "optimist": "^0.6.1", + "source-map": "^0.6.1", + "uglify-js": "^3.1.4" + }, + "dependencies": { + "source-map": { + "version": "0.6.1", + "bundled": true, + "dev": true + } + } + }, + "has-flag": { + "version": "3.0.0", + "bundled": true, + "dev": true + }, + "hasha": { + "version": "3.0.0", + "bundled": true, + "dev": true, + "requires": { + "is-stream": "^1.0.1" + } + }, + "hosted-git-info": { + "version": "2.7.1", + "bundled": true, + "dev": true + }, + "imurmurhash": { + "version": "0.1.4", + "bundled": true, + "dev": true + }, + "inflight": { + "version": "1.0.6", + "bundled": true, + "dev": true, + "requires": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "inherits": { + "version": "2.0.3", + "bundled": true, + "dev": true + }, + "invert-kv": { + "version": "2.0.0", + "bundled": true, + "dev": true + }, + "is-arrayish": { + "version": "0.2.1", + "bundled": true, + "dev": true + }, + "is-builtin-module": { + "version": "1.0.0", + "bundled": true, + "dev": true, + "requires": { + "builtin-modules": "^1.0.0" + } + }, + "is-fullwidth-code-point": { + "version": "2.0.0", + "bundled": true, + "dev": true + }, + "is-stream": { + "version": "1.1.0", + "bundled": true, + "dev": true + }, + "isexe": { + "version": "2.0.0", + "bundled": true, + "dev": true + }, + "istanbul-lib-coverage": { + "version": "2.0.3", + "bundled": true, + "dev": true + }, + "istanbul-lib-hook": { + "version": "2.0.3", + "bundled": true, + "dev": true, + "requires": { + "append-transform": "^1.0.0" + } + }, + "istanbul-lib-report": { + "version": "2.0.4", + "bundled": true, + "dev": true, + "requires": { + "istanbul-lib-coverage": "^2.0.3", + "make-dir": "^1.3.0", + "supports-color": "^6.0.0" + }, + "dependencies": { + "supports-color": { + "version": "6.1.0", + "bundled": true, + "dev": true, + "requires": { + "has-flag": "^3.0.0" + } + } + } + }, + "istanbul-lib-source-maps": { + "version": "3.0.2", + "bundled": true, + "dev": true, + "requires": { + "debug": "^4.1.1", + "istanbul-lib-coverage": "^2.0.3", + "make-dir": "^1.3.0", + "rimraf": "^2.6.2", + "source-map": "^0.6.1" + }, + "dependencies": { + "source-map": { + "version": "0.6.1", + "bundled": true, + "dev": true + } + } + }, + "istanbul-reports": { + "version": "2.1.0", + "bundled": true, + "dev": true, + "requires": { + "handlebars": "^4.0.11" + } + }, + "json-parse-better-errors": { + "version": "1.0.2", + "bundled": true, + "dev": true + }, + "lcid": { + "version": "2.0.0", + "bundled": true, + "dev": true, + "requires": { + "invert-kv": "^2.0.0" + } + }, + "load-json-file": { + "version": "4.0.0", + "bundled": true, + "dev": true, + "requires": { + "graceful-fs": "^4.1.2", + "parse-json": "^4.0.0", + "pify": "^3.0.0", + "strip-bom": "^3.0.0" + } + }, + "locate-path": { + "version": "3.0.0", + "bundled": true, + "dev": true, + "requires": { + "p-locate": "^3.0.0", + "path-exists": "^3.0.0" + } + }, + "lodash": { + "version": "4.17.11", + "bundled": true, + "dev": true + }, + "lodash.flattendeep": { + "version": "4.4.0", + "bundled": true, + "dev": true + }, + "lru-cache": { + "version": "4.1.5", + "bundled": true, + "dev": true, + "requires": { + "pseudomap": "^1.0.2", + "yallist": "^2.1.2" + } + }, + "make-dir": { + "version": "1.3.0", + "bundled": true, + "dev": true, + "requires": { + "pify": "^3.0.0" + } + }, + "map-age-cleaner": { + "version": "0.1.3", + "bundled": true, + "dev": true, + "requires": { + "p-defer": "^1.0.0" + } + }, + "mem": { + "version": "4.0.0", + "bundled": true, + "dev": true, + "requires": { + "map-age-cleaner": "^0.1.1", + "mimic-fn": "^1.0.0", + "p-is-promise": "^1.1.0" + } + }, + "merge-source-map": { + "version": "1.1.0", + "bundled": true, + "dev": true, + "requires": { + "source-map": "^0.6.1" + }, + "dependencies": { + "source-map": { + "version": "0.6.1", + "bundled": true, + "dev": true + } + } + }, + "mimic-fn": { + "version": "1.2.0", + "bundled": true, + "dev": true + }, + "minimatch": { + "version": "3.0.4", + "bundled": true, + "dev": true, + "requires": { + "brace-expansion": "^1.1.7" + } + }, + "minimist": { + "version": "0.0.10", + "bundled": true, + "dev": true + }, + "mkdirp": { + "version": "0.5.1", + "bundled": true, + "dev": true, + "requires": { + "minimist": "0.0.8" + }, + "dependencies": { + "minimist": { + "version": "0.0.8", + "bundled": true, + "dev": true + } + } + }, + "ms": { + "version": "2.1.1", + "bundled": true, + "dev": true + }, + "nice-try": { + "version": "1.0.5", + "bundled": true, + "dev": true + }, + "normalize-package-data": { + "version": "2.4.0", + "bundled": true, + "dev": true, + "requires": { + "hosted-git-info": "^2.1.4", + "is-builtin-module": "^1.0.0", + "semver": "2 || 3 || 4 || 5", + "validate-npm-package-license": "^3.0.1" + } + }, + "npm-run-path": { + "version": "2.0.2", + "bundled": true, + "dev": true, + "requires": { + "path-key": "^2.0.0" + } + }, + "number-is-nan": { + "version": "1.0.1", + "bundled": true, + "dev": true + }, + "once": { + "version": "1.4.0", + "bundled": true, + "dev": true, + "requires": { + "wrappy": "1" + } + }, + "optimist": { + "version": "0.6.1", + "bundled": true, + "dev": true, + "requires": { + "minimist": "~0.0.1", + "wordwrap": "~0.0.2" + } + }, + "os-homedir": { + "version": "1.0.2", + "bundled": true, + "dev": true + }, + "os-locale": { + "version": "3.1.0", + "bundled": true, + "dev": true, + "requires": { + "execa": "^1.0.0", + "lcid": "^2.0.0", + "mem": "^4.0.0" + } + }, + "p-defer": { + "version": "1.0.0", + "bundled": true, + "dev": true + }, + "p-finally": { + "version": "1.0.0", + "bundled": true, + "dev": true + }, + "p-is-promise": { + "version": "1.1.0", + "bundled": true, + "dev": true + }, + "p-limit": { + "version": "2.1.0", + "bundled": true, + "dev": true, + "requires": { + "p-try": "^2.0.0" + } + }, + "p-locate": { + "version": "3.0.0", + "bundled": true, + "dev": true, + "requires": { + "p-limit": "^2.0.0" + } + }, + "p-try": { + "version": "2.0.0", + "bundled": true, + "dev": true + }, + "package-hash": { + "version": "3.0.0", + "bundled": true, + "dev": true, + "requires": { + "graceful-fs": "^4.1.15", + "hasha": "^3.0.0", + "lodash.flattendeep": "^4.4.0", + "release-zalgo": "^1.0.0" + } + }, + "parse-json": { + "version": "4.0.0", + "bundled": true, + "dev": true, + "requires": { + "error-ex": "^1.3.1", + "json-parse-better-errors": "^1.0.1" + } + }, + "path-exists": { + "version": "3.0.0", + "bundled": true, + "dev": true + }, + "path-is-absolute": { + "version": "1.0.1", + "bundled": true, + "dev": true + }, + "path-key": { + "version": "2.0.1", + "bundled": true, + "dev": true + }, + "path-type": { + "version": "3.0.0", + "bundled": true, + "dev": true, + "requires": { + "pify": "^3.0.0" + } + }, + "pify": { + "version": "3.0.0", + "bundled": true, + "dev": true + }, + "pkg-dir": { + "version": "3.0.0", + "bundled": true, + "dev": true, + "requires": { + "find-up": "^3.0.0" + } + }, + "pseudomap": { + "version": "1.0.2", + "bundled": true, + "dev": true + }, + "pump": { + "version": "3.0.0", + "bundled": true, + "dev": true, + "requires": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, + "read-pkg": { + "version": "3.0.0", + "bundled": true, + "dev": true, + "requires": { + "load-json-file": "^4.0.0", + "normalize-package-data": "^2.3.2", + "path-type": "^3.0.0" + } + }, + "read-pkg-up": { + "version": "4.0.0", + "bundled": true, + "dev": true, + "requires": { + "find-up": "^3.0.0", + "read-pkg": "^3.0.0" + } + }, + "release-zalgo": { + "version": "1.0.0", + "bundled": true, + "dev": true, + "requires": { + "es6-error": "^4.0.1" + } + }, + "require-directory": { + "version": "2.1.1", + "bundled": true, + "dev": true + }, + "require-main-filename": { + "version": "1.0.1", + "bundled": true, + "dev": true + }, + "resolve-from": { + "version": "4.0.0", + "bundled": true, + "dev": true + }, + "rimraf": { + "version": "2.6.3", + "bundled": true, + "dev": true, + "requires": { + "glob": "^7.1.3" + } + }, + "safe-buffer": { + "version": "5.1.2", + "bundled": true, + "dev": true + }, + "semver": { + "version": "5.6.0", + "bundled": true, + "dev": true + }, + "set-blocking": { + "version": "2.0.0", + "bundled": true, + "dev": true + }, + "shebang-command": { + "version": "1.2.0", + "bundled": true, + "dev": true, + "requires": { + "shebang-regex": "^1.0.0" + } + }, + "shebang-regex": { + "version": "1.0.0", + "bundled": true, + "dev": true + }, + "signal-exit": { + "version": "3.0.2", + "bundled": true, + "dev": true + }, + "spawn-wrap": { + "version": "1.4.2", + "bundled": true, + "dev": true, + "requires": { + "foreground-child": "^1.5.6", + "mkdirp": "^0.5.0", + "os-homedir": "^1.0.1", + "rimraf": "^2.6.2", + "signal-exit": "^3.0.2", + "which": "^1.3.0" + } + }, + "spdx-correct": { + "version": "3.1.0", + "bundled": true, + "dev": true, + "requires": { + "spdx-expression-parse": "^3.0.0", + "spdx-license-ids": "^3.0.0" + } + }, + "spdx-exceptions": { + "version": "2.2.0", + "bundled": true, + "dev": true + }, + "spdx-expression-parse": { + "version": "3.0.0", + "bundled": true, + "dev": true, + "requires": { + "spdx-exceptions": "^2.1.0", + "spdx-license-ids": "^3.0.0" + } + }, + "spdx-license-ids": { + "version": "3.0.3", + "bundled": true, + "dev": true + }, + "string-width": { + "version": "2.1.1", + "bundled": true, + "dev": true, + "requires": { + "is-fullwidth-code-point": "^2.0.0", + "strip-ansi": "^4.0.0" + } + }, + "strip-ansi": { + "version": "4.0.0", + "bundled": true, + "dev": true, + "requires": { + "ansi-regex": "^3.0.0" + } + }, + "strip-bom": { + "version": "3.0.0", + "bundled": true, + "dev": true + }, + "strip-eof": { + "version": "1.0.0", + "bundled": true, + "dev": true + }, + "test-exclude": { + "version": "5.1.0", + "bundled": true, + "dev": true, + "requires": { + "arrify": "^1.0.1", + "minimatch": "^3.0.4", + "read-pkg-up": "^4.0.0", + "require-main-filename": "^1.0.1" + } + }, + "uglify-js": { + "version": "3.4.9", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "commander": "~2.17.1", + "source-map": "~0.6.1" + }, + "dependencies": { + "source-map": { + "version": "0.6.1", + "bundled": true, + "dev": true, + "optional": true + } + } + }, + "uuid": { + "version": "3.3.2", + "bundled": true, + "dev": true + }, + "validate-npm-package-license": { + "version": "3.0.4", + "bundled": true, + "dev": true, + "requires": { + "spdx-correct": "^3.0.0", + "spdx-expression-parse": "^3.0.0" + } + }, + "which": { + "version": "1.3.1", + "bundled": true, + "dev": true, + "requires": { + "isexe": "^2.0.0" + } + }, + "which-module": { + "version": "2.0.0", + "bundled": true, + "dev": true + }, + "wordwrap": { + "version": "0.0.3", + "bundled": true, + "dev": true + }, + "wrap-ansi": { + "version": "2.1.0", + "bundled": true, + "dev": true, + "requires": { + "string-width": "^1.0.1", + "strip-ansi": "^3.0.1" + }, + "dependencies": { + "ansi-regex": { + "version": "2.1.1", + "bundled": true, + "dev": true + }, + "is-fullwidth-code-point": { + "version": "1.0.0", + "bundled": true, + "dev": true, + "requires": { + "number-is-nan": "^1.0.0" + } + }, + "string-width": { + "version": "1.0.2", + "bundled": true, + "dev": true, + "requires": { + "code-point-at": "^1.0.0", + "is-fullwidth-code-point": "^1.0.0", + "strip-ansi": "^3.0.0" + } + }, + "strip-ansi": { + "version": "3.0.1", + "bundled": true, + "dev": true, + "requires": { + "ansi-regex": "^2.0.0" + } + } + } + }, + "wrappy": { + "version": "1.0.2", + "bundled": true, + "dev": true + }, + "write-file-atomic": { + "version": "2.4.2", + "bundled": true, + "dev": true, + "requires": { + "graceful-fs": "^4.1.11", + "imurmurhash": "^0.1.4", + "signal-exit": "^3.0.2" + } + }, + "y18n": { + "version": "4.0.0", + "bundled": true, + "dev": true + }, + "yallist": { + "version": "2.1.2", + "bundled": true, + "dev": true + }, + "yargs": { + "version": "12.0.5", + "bundled": true, + "dev": true, + "requires": { + "cliui": "^4.0.0", + "decamelize": "^1.2.0", + "find-up": "^3.0.0", + "get-caller-file": "^1.0.1", + "os-locale": "^3.0.0", + "require-directory": "^2.1.1", + "require-main-filename": "^1.0.1", + "set-blocking": "^2.0.0", + "string-width": "^2.0.0", + "which-module": "^2.0.0", + "y18n": "^3.2.1 || ^4.0.0", + "yargs-parser": "^11.1.1" + } + }, + "yargs-parser": { + "version": "11.1.1", + "bundled": true, + "dev": true, + "requires": { + "camelcase": "^5.0.0", + "decamelize": "^1.2.0" + } + } + } + }, "oauth-sign": { "version": "0.8.2", "resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.8.2.tgz", diff --git a/package.json b/package.json index a88cd449c3..65330537e9 100644 --- a/package.json +++ b/package.json @@ -19,6 +19,7 @@ "start": "node devServer.js", "tdd": "cross-env NODE_ENV=test mocha --require babel-register --watch --require ./test/setup-jsdom.js test/**/*_test.js", "test": "cross-env NODE_ENV=test mocha --require babel-register --require ./test/setup-jsdom.js test/**/*_test.js", + "test-coverage": "cross-env NODE_ENV=test nyc --reporter=lcov mocha --require babel-register --require ./test/setup-jsdom.js test/**/*_test.js", "test-debug": "cross-env NODE_ENV=test mocha --require babel-register --require ./test/setup-jsdom.js --debug-brk --inspect test/Form_test.js" }, "prettierOptions": "--jsx-bracket-same-line --trailing-comma es5 --semi --tab-width 2", @@ -83,6 +84,7 @@ "lint-staged": "^3.3.1", "mini-css-extract-plugin": "^0.4.3", "mocha": "^5.2.0", + "nyc": "^13.2.0", "prettier": "^1.15.1", "react": "^15.5.0", "react-addons-test-utils": "^15.3.2", From 4d17bd86c361eddacfc73034fc905879aa660daf Mon Sep 17 00:00:00 2001 From: "kevinq.qk" Date: Wed, 13 Feb 2019 10:02:10 +0800 Subject: [PATCH 60/90] test: move the test to the right file --- test/NumberField_test.js | 77 +++++++++++++++++++++++ test/StringField_test.js | 80 ++++++++++++++++++++++++ test/enum_test.js | 132 --------------------------------------- 3 files changed, 157 insertions(+), 132 deletions(-) delete mode 100644 test/enum_test.js diff --git a/test/NumberField_test.js b/test/NumberField_test.js index b265e8e642..456dcd2bd9 100644 --- a/test/NumberField_test.js +++ b/test/NumberField_test.js @@ -290,5 +290,82 @@ describe("NumberField", () => { expect(node.querySelector("select").id).eql("root"); }); + + it("should render a select element if set the enum.", () => { + const schema = { + type: "object", + properties: { + foo: { + type: "number", + enum: [0], + }, + }, + }; + + const { node } = createFormComponent({ + schema, + }); + + const selects = node.querySelectorAll("select"); + expect(selects).to.have.length.of(1); + }); + + it("should render a select element and it's value is empty, if set the enum and the default value is undefined.", () => { + const schema = { + type: "object", + properties: { + foo: { + type: "number", + enum: [0], + }, + }, + }; + + const { node } = createFormComponent({ + schema, + }); + + const selects = node.querySelectorAll("select"); + expect(selects[0].value).eql(""); + }); + + it("should render a select element and it's first option has an empty innerHTML, if set the enum and the default value is undefined.", () => { + const schema = { + type: "object", + properties: { + foo: { + type: "number", + enum: [0], + }, + }, + }; + + const { node } = createFormComponent({ + schema, + }); + + const options = node.querySelectorAll("option"); + expect(options[0].innerHTML).eql(""); + }); + + it("should render a select element and it's first option is '0', if set the enum and the default value is 0.", () => { + const schema = { + type: "object", + properties: { + foo: { + type: "number", + enum: [0], + default: 0, + }, + }, + }; + + const { node } = createFormComponent({ + schema, + }); + + const options = node.querySelectorAll("option"); + expect(options[0].innerHTML).eql("0"); + }); }); }); diff --git a/test/StringField_test.js b/test/StringField_test.js index 2846031cfd..4382247b39 100644 --- a/test/StringField_test.js +++ b/test/StringField_test.js @@ -350,6 +350,86 @@ describe("StringField", () => { expect(node.querySelector("#custom")).to.exist; }); + + it("should render a select element and it's first option is 'false', if set the enum and the default value is false.", () => { + const schema = { + type: "object", + properties: { + foo: { + type: "string", + enum: [false, true], + default: false, + }, + }, + }; + + const { node } = createFormComponent({ + schema, + }); + + const options = node.querySelectorAll("option"); + expect(options[0].innerHTML).eql("false"); + }); + + it("should render a select element and the option's length is equal the enum's length, if set the enum.", () => { + const schema = { + type: "object", + properties: { + foo: { + type: "string", + enum: [false, true], + default: false, + }, + }, + }; + + const { node } = createFormComponent({ + schema, + }); + + const options = node.querySelectorAll("option"); + expect(options.length).eql(2); + }); + + it("should render a select element and the option's length is equal the enum's length, if set the enum and the default value is empty.", () => { + const schema = { + type: "object", + properties: { + foo: { + type: "string", + enum: ["", "1"], + default: "", + }, + }, + }; + + const { node } = createFormComponent({ + schema, + }); + + const options = node.querySelectorAll("option"); + expect(options.length).eql(2); + }); + + it("shouldn't render two empty options, when the default value is empty.", () => { + const schema = { + type: "object", + properties: { + foo: { + type: "string", + enum: [""], + default: "", + }, + }, + }; + + const { node } = createFormComponent({ + schema, + }); + + const options = node.querySelectorAll("option"); + expect(options.length).eql(1); + }); }); describe("TextareaWidget", () => { diff --git a/test/enum_test.js b/test/enum_test.js deleted file mode 100644 index a9f335f6f7..0000000000 --- a/test/enum_test.js +++ /dev/null @@ -1,132 +0,0 @@ -import { expect } from "chai"; - -import { createFormComponent, createSandbox } from "./test_utils"; - -describe("enum", () => { - let sandbox; - - beforeEach(() => { - sandbox = createSandbox(); - }); - - afterEach(() => { - sandbox.restore(); - }); - - it("should render a select element if set the enum.", () => { - const schema = { - type: "object", - properties: { - foo: { - type: "string", - enum: [0], - }, - }, - }; - - const { node } = createFormComponent({ - schema, - }); - - const selects = node.querySelectorAll("select"); - expect(selects).to.have.length.of(1); - }); - - it("should render a select element and it's value is empty, if set the enum and the default value is undefined.", () => { - const schema = { - type: "object", - properties: { - foo: { - type: "string", - enum: [0], - }, - }, - }; - - const { node } = createFormComponent({ - schema, - }); - - const selects = node.querySelectorAll("select"); - expect(selects[0].value).eql(""); - }); - - it("should render a select element and it's first option has an empty innerHTML, if set the enum and the default value is undefined.", () => { - const schema = { - type: "object", - properties: { - foo: { - type: "string", - enum: [0], - }, - }, - }; - - const { node } = createFormComponent({ - schema, - }); - - const options = node.querySelectorAll("option"); - expect(options[0].innerHTML).eql(""); - }); - - it("should render a select element and it's first option is '0', if set the enum and the default value is 0.", () => { - const schema = { - type: "object", - properties: { - foo: { - type: "string", - enum: [0], - default: 0, - }, - }, - }; - - const { node } = createFormComponent({ - schema, - }); - - const options = node.querySelectorAll("option"); - expect(options[0].innerHTML).eql("0"); - }); - - it("should render a select element and it's first option is 'false', if set the enum and the default value is false.", () => { - const schema = { - type: "object", - properties: { - foo: { - type: "string", - enum: [false, true], - default: false, - }, - }, - }; - - const { node } = createFormComponent({ - schema, - }); - - const options = node.querySelectorAll("option"); - expect(options[0].innerHTML).eql("false"); - }); - - it("should render a select element and the option's length is equal the enum's length, if set the enum.", () => { - const schema = { - type: "object", - properties: { - foo: { - type: "string", - enum: [false, true], - default: false, - }, - }, - }; - - const { node } = createFormComponent({ - schema, - }); - - const options = node.querySelectorAll("option"); - expect(options.length).eql(2); - }); -}); From f9d4c63cac24146e522528d33eb04f465464e052 Mon Sep 17 00:00:00 2001 From: Lucian Buzzo Date: Thu, 21 Feb 2019 00:48:04 +0000 Subject: [PATCH 61/90] Fix multiple bugs related to switching between anyOf/oneOf options (#1169) * Fix multiple bugs related to switching between anyOf/oneOf options Fixes #1168 - Fixed a bug that would prevent input fields from rendering when switching between a non-object type option and an object type option - Fixed a bug where options would incorrectly change when entering values if a subschema with multiple required fields is used - Fixed a bug where switching from an object tpye option to a non-object type option would result in an input field containing the value [Object object] Change-type: patch Signed-off-by: Lucian * Update src/utils.js * Update src/utils.js --- src/components/fields/MultiSchemaField.js | 13 ++- src/components/fields/ObjectField.js | 2 +- src/utils.js | 4 +- test/anyOf_test.js | 46 ++++++++ test/oneOf_test.js | 135 ++++++++++++++++++++++ test/utils_test.js | 76 ++++++++++++ 6 files changed, 273 insertions(+), 3 deletions(-) diff --git a/src/components/fields/MultiSchemaField.js b/src/components/fields/MultiSchemaField.js index 593590e6b8..325a1b9851 100644 --- a/src/components/fields/MultiSchemaField.js +++ b/src/components/fields/MultiSchemaField.js @@ -69,6 +69,10 @@ class AnyOfField extends Component { augmentedSchema = Object.assign({}, option, requiresAnyOf); } + // Remove the "required" field as it's likely that not all fields have + // been filled in yet, which will mean that the schema is not valid + delete augmentedSchema.required; + if (isValid(augmentedSchema, formData)) { return i; } @@ -85,7 +89,14 @@ class AnyOfField extends Component { const selectedOption = parseInt(event.target.value, 10); const { formData, onChange, options } = this.props; - if (guessType(formData) === "object") { + const newOption = options[selectedOption]; + + // If the new option is of type object and the current data is an object, + // discard properties added using the old option. + if ( + guessType(formData) === "object" && + (newOption.type === "object" || newOption.properties) + ) { const newFormData = Object.assign({}, formData); const optionsToDiscard = options.slice(); diff --git a/src/components/fields/ObjectField.js b/src/components/fields/ObjectField.js index b6514d82e0..8189536516 100644 --- a/src/components/fields/ObjectField.js +++ b/src/components/fields/ObjectField.js @@ -237,7 +237,7 @@ class ObjectField extends Component { errorSchema={errorSchema[name]} idSchema={idSchema[name]} idPrefix={idPrefix} - formData={formData[name]} + formData={(formData || {})[name]} onKeyChange={this.onKeyChange(name)} onChange={this.onPropertyChange( name, diff --git a/src/utils.js b/src/utils.js index ad92ee494b..d55386cf91 100644 --- a/src/utils.js +++ b/src/utils.js @@ -732,7 +732,9 @@ export function toIdSchema( field, fieldId, definitions, - formData[name], + // It's possible that formData is not an object -- this can happen if an + // array item has just been added, but not populated with data yet + (formData || {})[name], idPrefix ); } diff --git a/test/anyOf_test.js b/test/anyOf_test.js index 51566c3bf1..a2bd14b200 100644 --- a/test/anyOf_test.js +++ b/test/anyOf_test.js @@ -571,5 +571,51 @@ describe("anyOf", () => { expect(node.querySelectorAll("input#root_foo")).to.have.length.of(1); }); + + it("should correctly render mixed types for anyOf inside array items", () => { + const schema = { + type: "object", + properties: { + items: { + type: "array", + items: { + anyOf: [ + { + type: "string", + }, + { + type: "object", + properties: { + foo: { + type: "integer", + }, + bar: { + type: "string", + }, + }, + }, + ], + }, + }, + }, + }; + + const { node } = createFormComponent({ + schema, + }); + + expect(node.querySelector(".array-item-add button")).not.eql(null); + + Simulate.click(node.querySelector(".array-item-add button")); + + const $select = node.querySelector("select"); + expect($select).not.eql(null); + Simulate.change($select, { + target: { value: $select.options[1].value }, + }); + + expect(node.querySelectorAll("input#root_foo")).to.have.length.of(1); + expect(node.querySelectorAll("input#root_bar")).to.have.length.of(1); + }); }); }); diff --git a/test/oneOf_test.js b/test/oneOf_test.js index 25fe39f97d..a7589dae68 100644 --- a/test/oneOf_test.js +++ b/test/oneOf_test.js @@ -299,4 +299,139 @@ describe("oneOf", () => { expect(node.querySelector("select").value).eql("1"); }); + + it("should not change the selected option when entering values on a subschema with multiple required options", () => { + const schema = { + type: "object", + properties: { + items: { + oneOf: [ + { + type: "string", + }, + { + type: "object", + properties: { + foo: { + type: "integer", + }, + bar: { + type: "string", + }, + }, + required: ["foo", "bar"], + }, + ], + }, + }, + }; + + const { node } = createFormComponent({ + schema, + }); + + const $select = node.querySelector("select"); + + expect($select.value).eql("0"); + + Simulate.change($select, { + target: { value: $select.options[1].value }, + }); + + expect($select.value).eql("1"); + + Simulate.change(node.querySelector("input#root_bar"), { + target: { value: "Lorem ipsum dolor sit amet" }, + }); + + expect($select.value).eql("1"); + }); + + it("should empty the form data when switching from an option of type 'object'", () => { + const schema = { + oneOf: [ + { + type: "object", + properties: { + foo: { + type: "integer", + }, + bar: { + type: "string", + }, + }, + required: ["foo", "bar"], + }, + { + type: "string", + }, + ], + }; + + const { node } = createFormComponent({ + schema, + formData: { + foo: 1, + bar: "abc", + }, + }); + + const $select = node.querySelector("select"); + + Simulate.change($select, { + target: { value: $select.options[1].value }, + }); + + expect($select.value).eql("1"); + + expect(node.querySelector("input#root").value).eql(""); + }); + + describe("Arrays", () => { + it("should correctly render mixed types for oneOf inside array items", () => { + const schema = { + type: "object", + properties: { + items: { + type: "array", + items: { + oneOf: [ + { + type: "string", + }, + { + type: "object", + properties: { + foo: { + type: "integer", + }, + bar: { + type: "string", + }, + }, + }, + ], + }, + }, + }, + }; + + const { node } = createFormComponent({ + schema, + }); + + expect(node.querySelector(".array-item-add button")).not.eql(null); + + Simulate.click(node.querySelector(".array-item-add button")); + + const $select = node.querySelector("select"); + expect($select).not.eql(null); + Simulate.change($select, { + target: { value: $select.options[1].value }, + }); + + expect(node.querySelectorAll("input#root_foo")).to.have.length.of(1); + expect(node.querySelectorAll("input#root_bar")).to.have.length.of(1); + }); + }); }); diff --git a/test/utils_test.js b/test/utils_test.js index cb32727c06..5b84d0108b 100644 --- a/test/utils_test.js +++ b/test/utils_test.js @@ -1183,6 +1183,64 @@ describe("utils", () => { }); }); + it("should return an idSchema for nested property dependencies", () => { + const schema = { + type: "object", + properties: { + obj: { + type: "object", + properties: { + foo: { type: "string" }, + }, + dependencies: { + foo: { + properties: { + bar: { type: "string" }, + }, + }, + }, + }, + }, + }; + const formData = { + obj: { + foo: "test", + }, + }; + + expect(toIdSchema(schema, undefined, schema.definitions, formData)).eql({ + $id: "root", + obj: { + $id: "root_obj", + foo: { $id: "root_obj_foo" }, + bar: { $id: "root_obj_bar" }, + }, + }); + }); + + it("should return an idSchema for unmet property dependencies", () => { + const schema = { + type: "object", + properties: { + foo: { type: "string" }, + }, + dependencies: { + foo: { + properties: { + bar: { type: "string" }, + }, + }, + }, + }; + + const formData = {}; + + expect(toIdSchema(schema, undefined, schema.definitions, formData)).eql({ + $id: "root", + foo: { $id: "root_foo" }, + }); + }); + it("should handle idPrefix parameter", () => { const schema = { definitions: { @@ -1205,6 +1263,24 @@ describe("utils", () => { } ); }); + + it("should handle null form data for object schemas", () => { + const schema = { + type: "object", + properties: { + foo: { type: "string" }, + bar: { type: "string" }, + }, + }; + const formData = null; + const result = toIdSchema(schema, null, {}, formData, "rjsf"); + + expect(result).eql({ + $id: "rjsf", + foo: { $id: "rjsf_foo" }, + bar: { $id: "rjsf_bar" }, + }); + }); }); describe("parseDateString()", () => { From a7be6ee866f20a37b67c915c795ae82f327768ca Mon Sep 17 00:00:00 2001 From: Eric Lanehart Date: Fri, 22 Feb 2019 21:32:14 -0500 Subject: [PATCH 62/90] Remove build:readme script and toctoc dep (#1189) Followup to f62cfc4fe172d778cd72a0e8d0c556ee6d62f6ce which broke the build:readme script --- package-lock.json | 219 ---------------------------------------------- package.json | 6 +- 2 files changed, 2 insertions(+), 223 deletions(-) diff --git a/package-lock.json b/package-lock.json index 49350695a2..9beaddf6b4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1964,12 +1964,6 @@ "integrity": "sha1-r6uWJikQp/M8GaV3WCXGnzTjUMo=", "dev": true }, - "camelcase": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-3.0.0.tgz", - "integrity": "sha1-MvxLn82vhF/N9+c7uXysImHwqwo=", - "dev": true - }, "caniuse-api": { "version": "1.6.1", "resolved": "https://registry.npmjs.org/caniuse-api/-/caniuse-api-1.6.1.tgz", @@ -2182,39 +2176,6 @@ "integrity": "sha1-/xnt6Kml5XkyQUewwR8PvLq+1jk=", "dev": true }, - "cliui": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/cliui/-/cliui-3.2.0.tgz", - "integrity": "sha1-EgYBU3qRbSmUD5NNo7SNWFo5IT0=", - "dev": true, - "requires": { - "string-width": "^1.0.1", - "strip-ansi": "^3.0.1", - "wrap-ansi": "^2.0.0" - }, - "dependencies": { - "is-fullwidth-code-point": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz", - "integrity": "sha1-754xOG8DGn8NZDr4L95QxFfvAMs=", - "dev": true, - "requires": { - "number-is-nan": "^1.0.0" - } - }, - "string-width": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz", - "integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=", - "dev": true, - "requires": { - "code-point-at": "^1.0.0", - "is-fullwidth-code-point": "^1.0.0", - "strip-ansi": "^3.0.0" - } - } - } - }, "clone": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/clone/-/clone-1.0.4.tgz", @@ -5031,12 +4992,6 @@ "loose-envify": "^1.0.0" } }, - "invert-kv": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/invert-kv/-/invert-kv-1.0.0.tgz", - "integrity": "sha1-EEqOSqym09jNFXqO+L+rLXo//bY=", - "dev": true - }, "ipaddr.js": { "version": "1.6.0", "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.6.0.tgz", @@ -5308,12 +5263,6 @@ "integrity": "sha1-5HnICFjfDBsR3dppQPlgEfzaSpo=", "dev": true }, - "is-utf8": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/is-utf8/-/is-utf8-0.2.1.tgz", - "integrity": "sha1-Sw2hRCEE0bM2NA6AeX6GXPOffXI=", - "dev": true - }, "is-windows": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/is-windows/-/is-windows-1.0.2.tgz", @@ -5513,15 +5462,6 @@ "is-buffer": "^1.1.5" } }, - "lcid": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/lcid/-/lcid-1.0.0.tgz", - "integrity": "sha1-MIrMr6C8SDo4Z7S28rlQYlHRuDU=", - "dev": true, - "requires": { - "invert-kv": "^1.0.0" - } - }, "levn": { "version": "0.3.0", "resolved": "https://registry.npmjs.org/levn/-/levn-0.3.0.tgz", @@ -6002,12 +5942,6 @@ "object-visit": "^1.0.0" } }, - "marked": { - "version": "0.3.19", - "resolved": "https://registry.npmjs.org/marked/-/marked-0.3.19.tgz", - "integrity": "sha512-ea2eGWOqNxPcXv8dyERdSr/6FmzvWwzjMxpfGB/sbMccXoct+xY+YukPD+QTUZwyvK7BZwcr4m21WBOW41pAkg==", - "dev": true - }, "math-expression-evaluator": { "version": "1.2.17", "resolved": "https://registry.npmjs.org/math-expression-evaluator/-/math-expression-evaluator-1.2.17.tgz", @@ -7769,15 +7703,6 @@ "integrity": "sha1-/7xJiDNuDoM94MFox+8VISGqf7M=", "dev": true }, - "os-locale": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/os-locale/-/os-locale-1.4.0.tgz", - "integrity": "sha1-IPnxeuKe00XoveWDsT0gCYA8FNk=", - "dev": true, - "requires": { - "lcid": "^1.0.0" - } - }, "os-tmpdir": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz", @@ -10170,16 +10095,6 @@ } } }, - "toctoc": { - "version": "0.2.4", - "resolved": "https://registry.npmjs.org/toctoc/-/toctoc-0.2.4.tgz", - "integrity": "sha512-0m1SIvDrHAF/ujztVqmiqg147NwzBRag5Bdw0gCvVWWRCyA/w4sXdRqGoaTAF68hzOeXwgcKgi1BUcRJhrUZvw==", - "dev": true, - "requires": { - "marked": "^0.3.9", - "yargs": "^4.3.1" - } - }, "tough-cookie": { "version": "2.3.4", "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.3.4.tgz", @@ -11717,18 +11632,6 @@ "isexe": "^2.0.0" } }, - "which-module": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/which-module/-/which-module-1.0.0.tgz", - "integrity": "sha1-u6Y8qGGUiZT/MHc2CJ47lgJsKk8=", - "dev": true - }, - "window-size": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/window-size/-/window-size-0.2.0.tgz", - "integrity": "sha1-tDFbtCFKPXBY6+7okuE/ok2YsHU=", - "dev": true - }, "wordwrap": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz", @@ -11826,128 +11729,6 @@ "resolved": "https://registry.npmjs.org/yallist/-/yallist-2.1.2.tgz", "integrity": "sha1-HBH5IY8HYImkfdUS+TxmmaaoHVI=", "dev": true - }, - "yargs": { - "version": "4.8.1", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-4.8.1.tgz", - "integrity": "sha1-wMQpJMpKqmsObaFznfshZDn53cA=", - "dev": true, - "requires": { - "cliui": "^3.2.0", - "decamelize": "^1.1.1", - "get-caller-file": "^1.0.1", - "lodash.assign": "^4.0.3", - "os-locale": "^1.4.0", - "read-pkg-up": "^1.0.1", - "require-directory": "^2.1.1", - "require-main-filename": "^1.0.1", - "set-blocking": "^2.0.0", - "string-width": "^1.0.1", - "which-module": "^1.0.0", - "window-size": "^0.2.0", - "y18n": "^3.2.1", - "yargs-parser": "^2.4.1" - }, - "dependencies": { - "is-fullwidth-code-point": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz", - "integrity": "sha1-754xOG8DGn8NZDr4L95QxFfvAMs=", - "dev": true, - "requires": { - "number-is-nan": "^1.0.0" - } - }, - "load-json-file": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-1.1.0.tgz", - "integrity": "sha1-lWkFcI1YtLq0wiYbBPWfMcmTdMA=", - "dev": true, - "requires": { - "graceful-fs": "^4.1.2", - "parse-json": "^2.2.0", - "pify": "^2.0.0", - "pinkie-promise": "^2.0.0", - "strip-bom": "^2.0.0" - } - }, - "lodash.assign": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/lodash.assign/-/lodash.assign-4.2.0.tgz", - "integrity": "sha1-DZnzzNem0mHRm9rrkkUAXShYCOc=", - "dev": true - }, - "path-type": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/path-type/-/path-type-1.1.0.tgz", - "integrity": "sha1-WcRPfuSR2nBNpBXaWkBwuk+P5EE=", - "dev": true, - "requires": { - "graceful-fs": "^4.1.2", - "pify": "^2.0.0", - "pinkie-promise": "^2.0.0" - } - }, - "read-pkg": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-1.1.0.tgz", - "integrity": "sha1-9f+qXs0pyzHAR0vKfXVra7KePyg=", - "dev": true, - "requires": { - "load-json-file": "^1.0.0", - "normalize-package-data": "^2.3.2", - "path-type": "^1.0.0" - } - }, - "read-pkg-up": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-1.0.1.tgz", - "integrity": "sha1-nWPBMnbAZZGNV/ACpX9AobZD+wI=", - "dev": true, - "requires": { - "find-up": "^1.0.0", - "read-pkg": "^1.0.0" - } - }, - "string-width": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz", - "integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=", - "dev": true, - "requires": { - "code-point-at": "^1.0.0", - "is-fullwidth-code-point": "^1.0.0", - "strip-ansi": "^3.0.0" - } - }, - "strip-bom": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-2.0.0.tgz", - "integrity": "sha1-YhmoVhZSBJHzV4i9vxRHqZx+aw4=", - "dev": true, - "requires": { - "is-utf8": "^0.2.0" - } - } - } - }, - "yargs-parser": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-2.4.1.tgz", - "integrity": "sha1-hVaN488VD/SfpRgl8DqMiA3cxcQ=", - "dev": true, - "requires": { - "camelcase": "^3.0.0", - "lodash.assign": "^4.0.6" - }, - "dependencies": { - "lodash.assign": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/lodash.assign/-/lodash.assign-4.2.0.tgz", - "integrity": "sha1-DZnzzNem0mHRm9rrkkUAXShYCOc=", - "dev": true - } - } } } } diff --git a/package.json b/package.json index 65330537e9..a2636d3738 100644 --- a/package.json +++ b/package.json @@ -3,7 +3,6 @@ "version": "1.2.1", "description": "A simple React component capable of building HTML forms out of a JSON schema.", "scripts": { - "build:readme": "toctoc README.md -w", "build:lib": "rimraf lib && cross-env NODE_ENV=production babel -d lib/ src/", "build:dist": "rimraf dist && cross-env NODE_ENV=production webpack --config webpack.config.dist.js", "build:playground": "rimraf build && cross-env NODE_ENV=production webpack --config webpack.config.prod.js && cp playground/index.prod.html build/index.html", @@ -14,8 +13,8 @@ "prepare": "npm run dist", "precommit": "lint-staged", "publish-to-gh-pages": "npm run build:playground && gh-pages --dist build/", - "publish-to-npm": "npm run build:readme && npm run dist && npm publish", - "preversion": "npm run build:playground && npm run dist && npm run build:readme && npm run cs-check && npm run lint", + "publish-to-npm": "npm run dist && npm publish", + "preversion": "npm run build:playground && npm run dist && npm run cs-check && npm run lint", "start": "node devServer.js", "tdd": "cross-env NODE_ENV=test mocha --require babel-register --watch --require ./test/setup-jsdom.js test/**/*_test.js", "test": "cross-env NODE_ENV=test mocha --require babel-register --require ./test/setup-jsdom.js test/**/*_test.js", @@ -96,7 +95,6 @@ "rimraf": "^2.5.4", "sinon": "^1.17.6", "style-loader": "^0.13.1", - "toctoc": "^0.2.3", "webpack": "^4.20.2", "webpack-cli": "^3.1.2", "webpack-dev-middleware": "^3.4.0", From 5aaf8632a1785e9c1b6bfe4880d15af04c51665f Mon Sep 17 00:00:00 2001 From: Ashwin Ramaswami Date: Sat, 23 Feb 2019 09:34:13 -0800 Subject: [PATCH 63/90] test: fix tests --- test/NumberField_test.js | 40 +++++++++++++++++----------------------- test/StringField_test.js | 25 ++++--------------------- 2 files changed, 21 insertions(+), 44 deletions(-) diff --git a/test/NumberField_test.js b/test/NumberField_test.js index 456dcd2bd9..687fa65766 100644 --- a/test/NumberField_test.js +++ b/test/NumberField_test.js @@ -291,7 +291,7 @@ describe("NumberField", () => { expect(node.querySelector("select").id).eql("root"); }); - it("should render a select element if set the enum.", () => { + it("should render a select element with a blank option, when default value is not set.", () => { const schema = { type: "object", properties: { @@ -307,16 +307,21 @@ describe("NumberField", () => { }); const selects = node.querySelectorAll("select"); - expect(selects).to.have.length.of(1); + expect(selects[0].value).eql(""); + + const options = node.querySelectorAll("option"); + expect(options.length).eql(2); + expect(options[0].innerHTML).eql(""); }); - it("should render a select element and it's value is empty, if set the enum and the default value is undefined.", () => { + it("should render a select element without a blank option, if a default value is set.", () => { const schema = { type: "object", properties: { foo: { type: "number", - enum: [0], + enum: [2], + default: 2, }, }, }; @@ -326,29 +331,14 @@ describe("NumberField", () => { }); const selects = node.querySelectorAll("select"); - expect(selects[0].value).eql(""); - }); - - it("should render a select element and it's first option has an empty innerHTML, if set the enum and the default value is undefined.", () => { - const schema = { - type: "object", - properties: { - foo: { - type: "number", - enum: [0], - }, - }, - }; - - const { node } = createFormComponent({ - schema, - }); + expect(selects[0].value).eql("2"); const options = node.querySelectorAll("option"); - expect(options[0].innerHTML).eql(""); + expect(options.length).eql(1); + expect(options[0].innerHTML).eql("2"); }); - it("should render a select element and it's first option is '0', if set the enum and the default value is 0.", () => { + it("should render a select element without a blank option, if the default value is 0.", () => { const schema = { type: "object", properties: { @@ -364,7 +354,11 @@ describe("NumberField", () => { schema, }); + const selects = node.querySelectorAll("select"); + expect(selects[0].value).eql("0"); + const options = node.querySelectorAll("option"); + expect(options.length).eql(1); expect(options[0].innerHTML).eql("0"); }); }); diff --git a/test/StringField_test.js b/test/StringField_test.js index 4382247b39..9c34a783c9 100644 --- a/test/StringField_test.js +++ b/test/StringField_test.js @@ -351,7 +351,7 @@ describe("StringField", () => { expect(node.querySelector("#custom")).to.exist; }); - it("should render a select element and it's first option is 'false', if set the enum and the default value is false.", () => { + it("should render a select element with first option 'false' if the default value is false", () => { const schema = { type: "object", properties: { @@ -369,25 +369,6 @@ describe("StringField", () => { const options = node.querySelectorAll("option"); expect(options[0].innerHTML).eql("false"); - }); - - it("should render a select element and the option's length is equal the enum's length, if set the enum.", () => { - const schema = { - type: "object", - properties: { - foo: { - type: "string", - enum: [false, true], - default: false, - }, - }, - }; - - const { node } = createFormComponent({ - schema, - }); - - const options = node.querySelectorAll("option"); expect(options.length).eql(2); }); @@ -408,10 +389,11 @@ describe("StringField", () => { }); const options = node.querySelectorAll("option"); + expect(options[0].innerHTML).eql(""); expect(options.length).eql(2); }); - it("shouldn't render two empty options, when the default value is empty.", () => { + it("should render only one empty option when the default value is empty.", () => { const schema = { type: "object", properties: { @@ -428,6 +410,7 @@ describe("StringField", () => { }); const options = node.querySelectorAll("option"); + expect(options[0].innerHTML).eql(""); expect(options.length).eql(1); }); }); From aebfab9baa94d35176b7197ecf080e2fc1258dcf Mon Sep 17 00:00:00 2001 From: Lucian Buzzo Date: Tue, 26 Feb 2019 17:29:23 +0000 Subject: [PATCH 64/90] Improve handling of decimal points and trailing zeroes in numbers (#1183) Connects to #674 #958 Change-type: patch Signed-off-by: Lucian --- src/components/fields/NumberField.js | 91 +++++++++++++++++-- src/components/widgets/BaseInput.js | 28 +++++- test/ArrayField_test.js | 8 +- test/Form_test.js | 4 +- test/NumberField_test.js | 129 +++++++++++++++++++++++++-- test/uiSchema_test.js | 4 +- 6 files changed, 238 insertions(+), 26 deletions(-) diff --git a/src/components/fields/NumberField.js b/src/components/fields/NumberField.js index 1f7f7c112d..396da678ca 100644 --- a/src/components/fields/NumberField.js +++ b/src/components/fields/NumberField.js @@ -3,14 +3,89 @@ import React from "react"; import * as types from "../../types"; import { asNumber } from "../../utils"; -function NumberField(props) { - const { StringField } = props.registry.fields; - return ( - props.onChange(asNumber(value))} - /> - ); +// Matches a string that ends in a . character, optionally followed by a sequence of +// digits followed by any number of 0 characters up until the end of the line. +// Ensuring that there is at least one prefixed character is important so that +// you don't incorrectly match against "0". +const trailingCharMatcherWithPrefix = /\.([0-9]*0)*$/; + +// This is used for trimming the trailing 0 and . characters without affecting +// the rest of the string. Its possible to use one RegEx with groups for this +// functionality, but it is fairly complex compared to simply defining two +// different matchers. +const trailingCharMatcher = /[0.]0*$/; + +/** + * The NumberField class has some special handling for dealing with trailing + * decimal points and/or zeroes. This logic is designed to allow trailing values + * to be visible in the input element, but not be represented in the + * corresponding form data. + * + * The algorithm is as follows: + * + * 1. When the input value changes the value is cached in the component state + * + * 2. The value is then normalized, removing trailing decimal points and zeros, + * then passed to the "onChange" callback + * + * 3. When the component is rendered, the formData value is checked against the + * value cached in the state. If it matches the cached value, the cached + * value is passed to the input instead of the formData value + */ +class NumberField extends React.Component { + constructor(props) { + super(props); + + this.state = { + lastValue: props.value, + }; + } + + handleChange = value => { + // Cache the original value in component state + this.setState({ lastValue: value }); + + // Normalize decimals that don't start with a zero character in advance so + // that the rest of the normalization logic is simpler + if (`${value}`.charAt(0) === ".") { + value = `0${value}`; + } + + // Check that the value is a string (this can happen if the widget used is a + // - {options.map((option, index) => { - return ( - - ); - })} - + options={{ enumOptions }} + {...uiOptions} + />
    {option !== null && ( diff --git a/test/anyOf_test.js b/test/anyOf_test.js index a2bd14b200..9f7cbf2b06 100644 --- a/test/anyOf_test.js +++ b/test/anyOf_test.js @@ -54,6 +54,36 @@ describe("anyOf", () => { expect(node.querySelectorAll("select")).to.have.length.of(1); }); + it("should render a custom widget", () => { + const schema = { + type: "object", + anyOf: [ + { + properties: { + foo: { type: "string" }, + }, + }, + { + properties: { + bar: { type: "string" }, + }, + }, + ], + }; + const widgets = { + SelectWidget: () => { + return
    Custom Widget
    ; + }, + }; + + const { node } = createFormComponent({ + schema, + widgets, + }); + + expect(node.querySelector("#CustomSelect")).to.exist; + }); + it("should change the rendered form when the select value is changed", () => { const schema = { type: "object", diff --git a/test/oneOf_test.js b/test/oneOf_test.js index a7589dae68..98ba91ed6d 100644 --- a/test/oneOf_test.js +++ b/test/oneOf_test.js @@ -54,6 +54,36 @@ describe("oneOf", () => { expect(node.querySelectorAll("select")).to.have.length.of(1); }); + it("should render a custom widget", () => { + const schema = { + type: "object", + oneOf: [ + { + properties: { + foo: { type: "string" }, + }, + }, + { + properties: { + bar: { type: "string" }, + }, + }, + ], + }; + const widgets = { + SelectWidget: () => { + return
    Custom Widget
    ; + }, + }; + + const { node } = createFormComponent({ + schema, + widgets, + }); + + expect(node.querySelector("#CustomSelect")).to.exist; + }); + it("should change the rendered form when the select value is changed", () => { const schema = { type: "object", From e409be538892163dac7d2019fc435b2a20ba1e06 Mon Sep 17 00:00:00 2001 From: Eric Lanehart Date: Fri, 5 Apr 2019 13:19:47 -0400 Subject: [PATCH 81/90] Custom string formats (#1186) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Accept custom string formats via `additionalFormats` prop Follow AJV extension pattern established for meta schemas and passthrough to AJV's addFormat method * Wire additionalFormats prop * additionalFormats → customFormats * Fix and test custom format prop updates * Update docs/validation.md Co-Authored-By: pushred --- docs/validation.md | 24 ++++++++++++++++ src/components/Form.js | 10 +++++-- src/validate.js | 33 ++++++++++++++++++---- test/Form_test.js | 47 +++++++++++++++++++++++++++++++ test/validate_test.js | 63 ++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 169 insertions(+), 8 deletions(-) diff --git a/docs/validation.md b/docs/validation.md index 9f259729aa..3370a0ad11 100644 --- a/docs/validation.md +++ b/docs/validation.md @@ -47,6 +47,30 @@ render(( > received as second argument. > - The `validate()` function is called **after** the JSON schema validation. +### Custom string formats + +[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: + +```jsx +const schema = { + phoneNumber: { + type: 'string', + format: 'format-us' + } +}; + +const customFormats = { + 'phone-us': /\(?\d{3}\)?[\s-]?\d{3}[\s-]?\d{4}$/ +}; + +render(( + +), document.getElementById("app")); +``` + +Format values can be anything AJV’s [`addFormat` method](https://github.com/epoberezkin/ajv#addformatstring-name-stringregexpfunctionobject-format---ajv) accepts. + ### Custom schema validation 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: diff --git a/src/components/Form.js b/src/components/Form.js index 96f2fed955..bded378062 100644 --- a/src/components/Form.js +++ b/src/components/Form.js @@ -58,9 +58,10 @@ export default class Form extends Component { const { definitions } = schema; const formData = getDefaultFormState(schema, props.formData, definitions); const retrievedSchema = retrieveSchema(schema, definitions, formData); + const customFormats = props.customFormats; const additionalMetaSchemas = props.additionalMetaSchemas; const { errors, errorSchema } = mustValidate - ? this.validate(formData, schema, additionalMetaSchemas) + ? this.validate(formData, schema, additionalMetaSchemas, customFormats) : { errors: state.errors || [], errorSchema: state.errorSchema || {}, @@ -91,7 +92,8 @@ export default class Form extends Component { validate( formData, schema = this.props.schema, - additionalMetaSchemas = this.props.additionalMetaSchemas + additionalMetaSchemas = this.props.additionalMetaSchemas, + customFormats = this.props.customFormats ) { const { validate, transformErrors } = this.props; const { definitions } = this.getRegistry(); @@ -101,7 +103,8 @@ export default class Form extends Component { resolvedSchema, validate, transformErrors, - additionalMetaSchemas + additionalMetaSchemas, + customFormats ); } @@ -301,6 +304,7 @@ if (process.env.NODE_ENV !== "production") { transformErrors: PropTypes.func, safeRenderCompletion: PropTypes.bool, formContext: PropTypes.object, + customFormats: PropTypes.object, additionalMetaSchemas: PropTypes.arrayOf(PropTypes.object), }; } diff --git a/src/validate.js b/src/validate.js index 9e8d34f114..f20afc79e2 100644 --- a/src/validate.js +++ b/src/validate.js @@ -3,6 +3,7 @@ import Ajv from "ajv"; let ajv = createAjvInstance(); import { deepEquals } from "./utils"; +let formerCustomFormats = null; let formerMetaSchema = null; import { isObject, mergeObjects } from "./utils"; @@ -167,19 +168,35 @@ export default function validateFormData( schema, customValidate, transformErrors, - additionalMetaSchemas = [] + additionalMetaSchemas = [], + customFormats = {} ) { + const newMetaSchemas = !deepEquals(formerMetaSchema, additionalMetaSchemas); + const newFormats = !deepEquals(formerCustomFormats, customFormats); + + if (newMetaSchemas || newFormats) { + ajv = createAjvInstance(); + } + // add more schemas to validate against if ( additionalMetaSchemas && - !deepEquals(formerMetaSchema, additionalMetaSchemas) && + newMetaSchemas && Array.isArray(additionalMetaSchemas) ) { - ajv = createAjvInstance(); ajv.addMetaSchema(additionalMetaSchemas); formerMetaSchema = additionalMetaSchemas; } + // add more custom formats to validate against + if (customFormats && newFormats && isObject(customFormats)) { + Object.keys(customFormats).forEach(formatName => { + ajv.addFormat(formatName, customFormats[formatName]); + }); + + formerCustomFormats = customFormats; + } + let validationError = null; try { ajv.validate(schema, formData); @@ -198,7 +215,13 @@ export default function validateFormData( typeof validationError.message === "string" && validationError.message.includes("no schema with key or ref "); - if (noProperMetaSchema) { + const unknownFormat = + validationError && + validationError.message && + typeof validationError.message === "string" && + validationError.message.includes("unknown format"); + + if (noProperMetaSchema || unknownFormat) { errors = [ ...errors, { @@ -212,7 +235,7 @@ export default function validateFormData( let errorSchema = toErrorSchema(errors); - if (noProperMetaSchema) { + if (noProperMetaSchema || unknownFormat) { errorSchema = { ...errorSchema, ...{ diff --git a/test/Form_test.js b/test/Form_test.js index ff1fb240d4..388ed1e665 100644 --- a/test/Form_test.js +++ b/test/Form_test.js @@ -1979,6 +1979,53 @@ describe("Form", () => { }); }); + describe("Custom format updates", () => { + it("Should update custom formats when customFormats is changed", () => { + const formProps = { + liveValidate: true, + formData: { + areaCode: 123, + }, + schema: { + type: "object", + properties: { + areaCode: { + type: "number", + format: "area-code", + }, + }, + }, + uiSchema: { + areaCode: { + "ui:widget": "area-code", + }, + }, + widgets: { + "area-code": () =>
    , + }, + }; + + const { comp } = createFormComponent(formProps); + + expect(comp.state.errorSchema).eql({ + $schema: { + __errors: [ + 'unknown format "area-code" is used in schema at path "#/properties/areaCode"', + ], + }, + }); + + setProps(comp, { + ...formProps, + customFormats: { + "area-code": /\d{3}/, + }, + }); + + expect(comp.state.errorSchema).eql({}); + }); + }); + describe("Meta schema updates", () => { it("Should update allowed meta schemas when additionalMetaSchemas is changed", () => { const formProps = { diff --git a/test/validate_test.js b/test/validate_test.js index b881a8e396..3ea9d40fef 100644 --- a/test/validate_test.js +++ b/test/validate_test.js @@ -163,6 +163,69 @@ describe("Validation", () => { }); }); + describe("validating using custom string formats", () => { + const schema = { + type: "object", + properties: { + phone: { + type: "string", + format: "phone-us", + }, + }, + }; + + it("should return a validation error if unknown string format is used", () => { + const result = validateFormData({ phone: "800.555.2368" }, schema); + const errMessage = + 'unknown format "phone-us" is used in schema at path "#/properties/phone"'; + + expect(result.errors[0].stack).include(errMessage); + expect(result.errorSchema).to.eql({ + $schema: { __errors: [errMessage] }, + }); + }); + + it("should return a validation error about formData", () => { + const result = validateFormData( + { phone: "800.555.2368" }, + schema, + null, + null, + null, + { "phone-us": /\(?\d{3}\)?[\s-]?\d{3}[\s-]?\d{4}$/ } + ); + + expect(result.errors).to.have.lengthOf(1); + expect(result.errors[0].stack).to.equal( + '.phone should match format "phone-us"' + ); + }); + + it("prop updates with new custom formats are accepted", () => { + const result = validateFormData( + { phone: "abc" }, + { + type: "object", + properties: { + phone: { + type: "string", + format: "area-code", + }, + }, + }, + null, + null, + null, + { "area-code": /\d{3}/ } + ); + + expect(result.errors).to.have.lengthOf(1); + expect(result.errors[0].stack).to.equal( + '.phone should match format "area-code"' + ); + }); + }); + describe("Custom validate function", () => { let errors, errorSchema; From 06b5e2f6a1a4e198074d6b010e8eb5470ccc05f1 Mon Sep 17 00:00:00 2001 From: Laney Smith Date: Mon, 8 Apr 2019 18:46:28 -0600 Subject: [PATCH 82/90] fix mistake in validation docs (#1251) --- docs/validation.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/validation.md b/docs/validation.md index 3370a0ad11..0f1d3c2daa 100644 --- a/docs/validation.md +++ b/docs/validation.md @@ -55,7 +55,7 @@ render(( const schema = { phoneNumber: { type: 'string', - format: 'format-us' + format: 'phone-us' } }; From ac397cae49a9a8e4bd78617eb70e563e21dc7a4f Mon Sep 17 00:00:00 2001 From: Ashwin Ramaswami Date: Fri, 12 Apr 2019 08:43:01 -0700 Subject: [PATCH 83/90] fix: do not error when File is not defined (#1253) --- src/utils.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/utils.js b/src/utils.js index ce13634b5d..a4b2c8af15 100644 --- a/src/utils.js +++ b/src/utils.js @@ -229,7 +229,7 @@ export function getUiOptions(uiSchema) { } export function isObject(thing) { - if (thing instanceof File) { + if (typeof File !== "undefined" && thing instanceof File) { return false; } return typeof thing === "object" && thing !== null && !Array.isArray(thing); From c5e8899701279a2ab1ac75551824ff24b8d187a4 Mon Sep 17 00:00:00 2001 From: faissalMT <41294831+faissalMT@users.noreply.github.com> Date: Fri, 12 Apr 2019 21:35:05 +0100 Subject: [PATCH 84/90] Add NullField (#1238) * Add NullField * Always set the formData to null for a null field * Don't overwrite existing data * Use central fields propTypes --- package-lock.json | 7 +--- playground/samples/index.js | 2 + playground/samples/null.js | 28 +++++++++++++ src/components/fields/NullField.js | 20 ++++++++++ src/components/fields/SchemaField.js | 1 + src/components/fields/index.js | 2 + test/NullField_test.js | 60 ++++++++++++++++++++++++++++ 7 files changed, 115 insertions(+), 5 deletions(-) create mode 100644 playground/samples/null.js create mode 100644 src/components/fields/NullField.js create mode 100644 test/NullField_test.js diff --git a/package-lock.json b/package-lock.json index 60dfbdb9e0..f1bcbef82b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "react-jsonschema-form", - "version": "1.2.1", + "version": "1.3.0", "lockfileVersion": 1, "requires": true, "dependencies": { @@ -4599,7 +4599,6 @@ "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-2.0.0.tgz", "integrity": "sha1-gTg9ctsFT8zPUzbaqQLxgvbtuyg=", "dev": true, - "optional": true, "requires": { "is-glob": "^2.0.0" } @@ -5268,8 +5267,7 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-1.0.0.tgz", "integrity": "sha1-rEaBd8SUNAWgkvyPKXYMb/xiBsA=", - "dev": true, - "optional": true + "dev": true }, "is-finite": { "version": "1.0.2", @@ -5291,7 +5289,6 @@ "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-2.0.1.tgz", "integrity": "sha1-0Jb5JqPe1WAPP9/ZEZjLCIjC2GM=", "dev": true, - "optional": true, "requires": { "is-extglob": "^1.0.0" } diff --git a/playground/samples/index.js b/playground/samples/index.js index 2abe944523..4e6a4fa031 100644 --- a/playground/samples/index.js +++ b/playground/samples/index.js @@ -21,6 +21,7 @@ import propertyDependencies from "./propertyDependencies"; import schemaDependencies from "./schemaDependencies"; import additionalProperties from "./additionalProperties"; import nullable from "./nullable"; +import nullField from "./null"; export const samples = { Simple: simple, @@ -45,5 +46,6 @@ export const samples = { "Additional Properties": additionalProperties, "Any Of": anyOf, "One Of": oneOf, + "Null fields": nullField, Nullable: nullable, }; diff --git a/playground/samples/null.js b/playground/samples/null.js new file mode 100644 index 0000000000..e6154e6a1e --- /dev/null +++ b/playground/samples/null.js @@ -0,0 +1,28 @@ +module.exports = { + schema: { + title: "Null field example", + description: "A short form with a null field", + type: "object", + required: ["firstName"], + properties: { + helpText: { + title: "A null field", + description: + "Null fields like this are great for adding extra information", + type: "null", + }, + firstName: { + type: "string", + title: "A regular string field", + default: "Chuck", + }, + }, + }, + uiSchema: { + firstName: { + "ui:autofocus": true, + "ui:emptyValue": "", + }, + }, + formData: {}, +}; diff --git a/src/components/fields/NullField.js b/src/components/fields/NullField.js new file mode 100644 index 0000000000..f82e436626 --- /dev/null +++ b/src/components/fields/NullField.js @@ -0,0 +1,20 @@ +import { Component } from "react"; +import * as types from "../../types"; + +class NullField extends Component { + componentDidMount() { + if (this.props.formData === undefined) { + this.props.onChange(null); + } + } + + render() { + return null; + } +} + +if (process.env.NODE_ENV !== "production") { + NullField.propTypes = types.fieldProps; +} + +export default NullField; diff --git a/src/components/fields/SchemaField.js b/src/components/fields/SchemaField.js index a48523d866..7090ee5d91 100644 --- a/src/components/fields/SchemaField.js +++ b/src/components/fields/SchemaField.js @@ -26,6 +26,7 @@ const COMPONENT_TYPES = { number: "NumberField", object: "ObjectField", string: "StringField", + null: "NullField", }; function getFieldComponent(schema, uiSchema, idSchema, fields) { diff --git a/src/components/fields/index.js b/src/components/fields/index.js index 758cb322ab..3ec27d44f8 100644 --- a/src/components/fields/index.js +++ b/src/components/fields/index.js @@ -7,6 +7,7 @@ import ObjectField from "./ObjectField"; import SchemaField from "./SchemaField"; import StringField from "./StringField"; import TitleField from "./TitleField"; +import NullField from "./NullField"; import UnsupportedField from "./UnsupportedField"; export default { @@ -20,5 +21,6 @@ export default { SchemaField, StringField, TitleField, + NullField, UnsupportedField, }; diff --git a/test/NullField_test.js b/test/NullField_test.js new file mode 100644 index 0000000000..edb72b3281 --- /dev/null +++ b/test/NullField_test.js @@ -0,0 +1,60 @@ +import { expect } from "chai"; + +import { createFormComponent, createSandbox } from "./test_utils"; + +describe("NullField", () => { + let sandbox; + + beforeEach(() => { + sandbox = createSandbox(); + }); + + afterEach(() => { + sandbox.restore(); + }); + + describe("No widget", () => { + it("should render a null field", () => { + const { node } = createFormComponent({ + schema: { + type: "null", + }, + }); + + expect(node.querySelectorAll(".field")).to.have.length.of(1); + }); + + it("should render a null field with a label", () => { + const { node } = createFormComponent({ + schema: { + type: "null", + title: "foo", + }, + }); + + expect(node.querySelector(".field label").textContent).eql("foo"); + }); + + it("should assign a default value", () => { + const { comp } = createFormComponent({ + schema: { + type: "null", + default: null, + }, + }); + + expect(comp.state.formData).eql(null); + }); + + it("should not overwrite existing data", () => { + const { comp } = createFormComponent({ + schema: { + type: "null", + }, + formData: 3, + }); + + expect(comp.state.formData).eql(3); + }); + }); +}); From 45ff77f60308c77fcf2e193c6c4b2d5740965aa6 Mon Sep 17 00:00:00 2001 From: Ethan Glasser-Camp Date: Wed, 17 Apr 2019 14:35:11 -0400 Subject: [PATCH 85/90] Bump version 1.5.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 46349d1133..8ab9fbbab9 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "react-jsonschema-form", - "version": "1.4.0", + "version": "1.5.0", "description": "A simple React component capable of building HTML forms out of a JSON schema.", "scripts": { "build:lib": "rimraf lib && cross-env NODE_ENV=production babel -d lib/ src/", From 2d63594e075bba3b134ef09709c773a9ea037923 Mon Sep 17 00:00:00 2001 From: fsteger <43000896+fsteger@users.noreply.github.com> Date: Wed, 24 Apr 2019 08:07:16 -0500 Subject: [PATCH 86/90] Pass along required for custom checkboxes widgets (#1259) --- src/components/fields/ArrayField.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/components/fields/ArrayField.js b/src/components/fields/ArrayField.js index 58d321f712..4f0d5781ae 100644 --- a/src/components/fields/ArrayField.js +++ b/src/components/fields/ArrayField.js @@ -421,6 +421,7 @@ class ArrayField extends Component { formData, disabled, readonly, + required, autofocus, onBlur, onFocus, @@ -448,6 +449,7 @@ class ArrayField extends Component { value={items} disabled={disabled} readonly={readonly} + required={required} formContext={formContext} autofocus={autofocus} rawErrors={rawErrors} From c4322bfab641e4a4020afe5308662e01a673c964 Mon Sep 17 00:00:00 2001 From: Drew Brigham Date: Thu, 25 Apr 2019 12:37:34 -0400 Subject: [PATCH 87/90] Update wording for description text (#1266) --- docs/form-customization.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/form-customization.md b/docs/form-customization.md index a42c0f6fa0..8603d991ed 100644 --- a/docs/form-customization.md +++ b/docs/form-customization.md @@ -596,7 +596,7 @@ const uiSchema = { ### Description texts -Sometimes it's convenient to change description a field. This is the purpose of the `ui:description` uiSchema directive: +Sometimes it's convenient to change the description of a field. This is the purpose of the `ui:description` uiSchema directive: ```js const schema = {type: "string"}; From fba5bee2ba4dae0b057a1a6f19c408ef2af427fb Mon Sep 17 00:00:00 2001 From: Ashwin Ramaswami Date: Tue, 7 May 2019 07:00:29 -0700 Subject: [PATCH 88/90] Fix/warn unknown formats (#1277) * fix: console warn on unknown format errors instead of showing an error * fix: default to default widget if string format is not found * test: update tests --- src/components/fields/StringField.js | 6 +++++- src/utils.js | 16 ++++++++++++++++ src/validate.js | 11 +++-------- test/Form_test.js | 20 +++++++++----------- test/validate_test.js | 10 ++-------- 5 files changed, 35 insertions(+), 28 deletions(-) diff --git a/src/components/fields/StringField.js b/src/components/fields/StringField.js index 31dc31580b..6f583ba9be 100644 --- a/src/components/fields/StringField.js +++ b/src/components/fields/StringField.js @@ -7,6 +7,7 @@ import { isSelect, optionsList, getDefaultRegistry, + hasWidget, } from "../../utils"; function StringField(props) { @@ -29,7 +30,10 @@ function StringField(props) { const { title, format } = schema; const { widgets, formContext } = registry; const enumOptions = isSelect(schema) && optionsList(schema); - const defaultWidget = format || (enumOptions ? "select" : "text"); + let defaultWidget = enumOptions ? "select" : "text"; + if (format && hasWidget(schema, format, widgets)) { + defaultWidget = format; + } const { widget = defaultWidget, placeholder = "", ...options } = getUiOptions( uiSchema ); diff --git a/src/utils.js b/src/utils.js index a4b2c8af15..8088bc5fa3 100644 --- a/src/utils.js +++ b/src/utils.js @@ -123,6 +123,22 @@ export function getWidget(schema, widget, registeredWidgets = {}) { throw new Error(`No widget "${widget}" for type "${type}"`); } +export function hasWidget(schema, widget, registeredWidgets = {}) { + try { + getWidget(schema, widget, registeredWidgets); + return true; + } catch (e) { + if ( + e.message && + (e.message.startsWith("No widget") || + e.message.startsWith("Unsupported widget")) + ) { + return false; + } + throw e; + } +} + function computeDefaults(schema, parentDefaults, definitions = {}) { // Compute the defaults recursively: give highest priority to deepest nodes. let defaults = parentDefaults; diff --git a/src/validate.js b/src/validate.js index f20afc79e2..526bceef93 100644 --- a/src/validate.js +++ b/src/validate.js @@ -14,6 +14,7 @@ function createAjvInstance() { allErrors: true, multipleOfPrecision: 8, schemaId: "auto", + unknownFormats: "ignore", }); // add custom formats @@ -215,13 +216,7 @@ export default function validateFormData( typeof validationError.message === "string" && validationError.message.includes("no schema with key or ref "); - const unknownFormat = - validationError && - validationError.message && - typeof validationError.message === "string" && - validationError.message.includes("unknown format"); - - if (noProperMetaSchema || unknownFormat) { + if (noProperMetaSchema) { errors = [ ...errors, { @@ -235,7 +230,7 @@ export default function validateFormData( let errorSchema = toErrorSchema(errors); - if (noProperMetaSchema || unknownFormat) { + if (noProperMetaSchema) { errorSchema = { ...errorSchema, ...{ diff --git a/test/Form_test.js b/test/Form_test.js index 388ed1e665..45cc89e7e7 100644 --- a/test/Form_test.js +++ b/test/Form_test.js @@ -1984,13 +1984,13 @@ describe("Form", () => { const formProps = { liveValidate: true, formData: { - areaCode: 123, + areaCode: "123455", }, schema: { type: "object", properties: { areaCode: { - type: "number", + type: "string", format: "area-code", }, }, @@ -2007,22 +2007,20 @@ describe("Form", () => { const { comp } = createFormComponent(formProps); - expect(comp.state.errorSchema).eql({ - $schema: { - __errors: [ - 'unknown format "area-code" is used in schema at path "#/properties/areaCode"', - ], - }, - }); + expect(comp.state.errorSchema).eql({}); setProps(comp, { ...formProps, customFormats: { - "area-code": /\d{3}/, + "area-code": /^\d{3}$/, }, }); - expect(comp.state.errorSchema).eql({}); + expect(comp.state.errorSchema).eql({ + areaCode: { + __errors: ['should match format "area-code"'], + }, + }); }); }); diff --git a/test/validate_test.js b/test/validate_test.js index 3ea9d40fef..696ed06311 100644 --- a/test/validate_test.js +++ b/test/validate_test.js @@ -174,15 +174,9 @@ describe("Validation", () => { }, }; - it("should return a validation error if unknown string format is used", () => { + it("should not return a validation error if unknown string format is used", () => { const result = validateFormData({ phone: "800.555.2368" }, schema); - const errMessage = - 'unknown format "phone-us" is used in schema at path "#/properties/phone"'; - - expect(result.errors[0].stack).include(errMessage); - expect(result.errorSchema).to.eql({ - $schema: { __errors: [errMessage] }, - }); + expect(result.errors.length).eql(0); }); it("should return a validation error about formData", () => { From 1ee343d4654a6d98ac08ece97330ce2a3f9f1de3 Mon Sep 17 00:00:00 2001 From: Samantha Branham Date: Tue, 7 May 2019 07:01:32 -0700 Subject: [PATCH 89/90] Make numbers nullable by allowing null values to be echoed from asNumber() (#1269) * Make numbers nullable by allowing null values to be echoed from asNumber() * fix: revert package-lock changes --- playground/samples/nullable.js | 3 ++- src/utils.js | 3 +++ test/utils_test.js | 4 ++++ 3 files changed, 9 insertions(+), 1 deletion(-) diff --git a/playground/samples/nullable.js b/playground/samples/nullable.js index 1068ac9741..2ccc62de9b 100644 --- a/playground/samples/nullable.js +++ b/playground/samples/nullable.js @@ -15,7 +15,7 @@ module.exports = { title: "Last name", }, age: { - type: "integer", + type: ["integer", "null"], title: "Age", }, bio: { @@ -43,6 +43,7 @@ module.exports = { "ui:widget": "updown", "ui:title": "Age of person", "ui:description": "(earthian year)", + "ui:emptyValue": null, }, bio: { "ui:widget": "textarea", diff --git a/src/utils.js b/src/utils.js index 8088bc5fa3..22f08e1863 100644 --- a/src/utils.js +++ b/src/utils.js @@ -272,6 +272,9 @@ export function asNumber(value) { if (value === "") { return undefined; } + if (value === null) { + return null; + } if (/\.$/.test(value)) { // "3." can't really be considered a number even if it parses in js. The // user is most likely entering a float. diff --git a/test/utils_test.js b/test/utils_test.js index 2f1ecc3696..5acfcc229d 100644 --- a/test/utils_test.js +++ b/test/utils_test.js @@ -342,6 +342,10 @@ describe("utils", () => { it("should return undefined if the input is empty", () => { expect(asNumber("")).eql(undefined); }); + + it("should return null if the input is null", () => { + expect(asNumber(null)).eql(null); + }); }); describe("orderProperties()", () => { From 98952b7f2a6add8909a2c7209797f3770d4a04f7 Mon Sep 17 00:00:00 2001 From: Patrik Henningsson Date: Tue, 14 May 2019 20:25:16 +0200 Subject: [PATCH 90/90] Support `readOnly` property from latest validation draft (#1282) * Support `readOnly` property from draft-handrews-json-schema-validation-01 * Fix failing test * Add note in docs about using the readOnly property * Update to be consistent with the changes done in commit 497620f --- docs/form-customization.md | 14 ++ playground/samples/widgets.js | 6 + src/components/fields/SchemaField.js | 7 +- test/uiSchema_test.js | 268 +++++++++++++++++++++++++++ 4 files changed, 294 insertions(+), 1 deletion(-) diff --git a/docs/form-customization.md b/docs/form-customization.md index 8603d991ed..6310c0f1d6 100644 --- a/docs/form-customization.md +++ b/docs/form-customization.md @@ -146,6 +146,20 @@ The `ui:disabled` uiSchema directive will disable all child widgets from a given The `ui:readonly` uiSchema directive will mark all child widgets from a given field as read-only. +You can also set specific fields to read-only by setting the `readOnly` property in the schema. + +```js +const schema = { + type: "object", + properties: { + foo: { + type: "string", + readOnly: true + } + } +}; +``` + > Note: If you're wondering about the difference between a `disabled` field and a `readonly` one: Marking a field as read-only will render it greyed out, but its text value will be selectable. Disabling it will prevent its value to be selected at all. #### Hidden widgets diff --git a/playground/samples/widgets.js b/playground/samples/widgets.js index 9a7d575d13..3530334440 100644 --- a/playground/samples/widgets.js +++ b/playground/samples/widgets.js @@ -73,6 +73,12 @@ module.exports = { title: "A readonly field", default: "I am read-only.", }, + readonly2: { + type: "string", + title: "Another readonly field", + default: "I am also read-only.", + readOnly: true, + }, widgetOptions: { title: "Custom widget with options", type: "string", diff --git a/src/components/fields/SchemaField.js b/src/components/fields/SchemaField.js index 7090ee5d91..3fc14f582e 100644 --- a/src/components/fields/SchemaField.js +++ b/src/components/fields/SchemaField.js @@ -251,7 +251,12 @@ function SchemaFieldRender(props) { const FieldComponent = getFieldComponent(schema, uiSchema, idSchema, fields); const { DescriptionField } = fields; const disabled = Boolean(props.disabled || uiSchema["ui:disabled"]); - const readonly = Boolean(props.readonly || uiSchema["ui:readonly"]); + const readonly = Boolean( + props.readonly || + uiSchema["ui:readonly"] || + props.schema.readOnly || + schema.readOnly + ); const autofocus = Boolean(props.autofocus || uiSchema["ui:autofocus"]); if (Object.keys(schema).length === 0) { return null; diff --git a/test/uiSchema_test.js b/test/uiSchema_test.js index a964cdada5..adbe9fc255 100644 --- a/test/uiSchema_test.js +++ b/test/uiSchema_test.js @@ -2376,4 +2376,272 @@ describe("uiSchema", () => { }); }); }); + + describe("Readonly in schema", () => { + describe("Fields", () => { + describe("ArrayField", () => { + let node; + + beforeEach(() => { + const schema = { + type: "array", + items: { + type: "string", + }, + readOnly: true, + }; + const uiSchema = {}; + const formData = ["a", "b"]; + + let rendered = createFormComponent({ schema, uiSchema, formData }); + node = rendered.node; + }); + + it("should mark as readonly an ArrayField", () => { + const disabled = [].map.call( + node.querySelectorAll("[type=text]"), + node => node.hasAttribute("readonly") + ); + expect(disabled).eql([true, true]); + }); + + it("should disable the Add button", () => { + expect(node.querySelector(".array-item-add button").disabled).eql( + true + ); + }); + + it("should disable the Delete button", () => { + expect(node.querySelector(".array-item-remove").disabled).eql(true); + }); + }); + + describe("ObjectField", () => { + let node; + + beforeEach(() => { + const schema = { + type: "object", + properties: { + foo: { + type: "string", + }, + bar: { + type: "string", + }, + }, + readOnly: true, + }; + const uiSchema = {}; + + let rendered = createFormComponent({ schema, uiSchema }); + node = rendered.node; + }); + + it("should mark as readonly an ObjectField", () => { + const disabled = [].map.call( + node.querySelectorAll("[type=text]"), + node => node.hasAttribute("readonly") + ); + expect(disabled).eql([true, true]); + }); + }); + }); + + describe("Widgets", () => { + function shouldBeReadonly(selector, schema, uiSchema) { + const { node } = createFormComponent({ schema, uiSchema }); + expect(node.querySelector(selector).hasAttribute("readonly")).eql(true); + } + function shouldBeDisabled(selector, schema, uiSchema) { + const { node } = createFormComponent({ schema, uiSchema }); + expect(node.querySelector(selector).disabled).eql(true); + } + + it("should mark as readonly a text widget", () => { + shouldBeReadonly( + "input[type=text]", + { + type: "string", + readOnly: true, + }, + {} + ); + }); + + it("should mark as readonly a file widget", () => { + // We mark a file widget as readonly by disabling it. + const { node } = createFormComponent({ + schema: { + type: "string", + format: "data-url", + readOnly: true, + }, + uiSchema: {}, + }); + expect( + node.querySelector("input[type=file]").hasAttribute("disabled") + ).eql(true); + }); + + it("should mark as readonly a textarea widget", () => { + shouldBeReadonly( + "textarea", + { + type: "string", + readOnly: true, + }, + { + "ui:widget": "textarea", + } + ); + }); + + it("should mark as readonly a number text widget", () => { + shouldBeReadonly( + "input[type=number]", + { + type: "number", + readOnly: true, + }, + {} + ); + }); + + it("should mark as readonly a number widget", () => { + shouldBeReadonly( + "input[type=number]", + { + type: "number", + readOnly: true, + }, + { + "ui:widget": "updown", + } + ); + }); + + it("should mark as readonly a range widget", () => { + shouldBeReadonly( + "input[type=range]", + { + type: "number", + readOnly: true, + }, + { + "ui:widget": "range", + } + ); + }); + + it("should mark readonly as disabled on a select widget", () => { + shouldBeDisabled( + "select", + { + type: "string", + enum: ["a", "b"], + readOnly: true, + }, + {} + ); + }); + + it("should mark as readonly a color widget", () => { + shouldBeReadonly( + "input[type=color]", + { + type: "string", + format: "color", + readOnly: true, + }, + {} + ); + }); + + it("should mark as readonly a password widget", () => { + shouldBeReadonly( + "input[type=password]", + { + type: "string", + readOnly: true, + }, + { + "ui:widget": "password", + } + ); + }); + + it("should mark as readonly a url widget", () => { + shouldBeReadonly( + "input[type=url]", + { + type: "string", + format: "uri", + readOnly: true, + }, + {} + ); + }); + + it("should mark as readonly an email widget", () => { + shouldBeReadonly("input[type=email]", { + type: "string", + format: "email", + readOnly: true, + }); + }); + + it("should mark as readonly a date widget", () => { + shouldBeReadonly("input[type=date]", { + type: "string", + format: "date", + readOnly: true, + }); + }); + + it("should mark as readonly a datetime widget", () => { + shouldBeReadonly("input[type=datetime-local]", { + type: "string", + format: "date-time", + readOnly: true, + }); + }); + + it("should mark readonly as disabled on an alternative date widget", () => { + const { node } = createFormComponent({ + schema: { + type: "string", + format: "date", + readOnly: true, + }, + uiSchema: { + "ui:widget": "alt-date", + }, + }); + + const readonly = [].map.call(node.querySelectorAll("select"), node => + node.hasAttribute("disabled") + ); + expect(readonly).eql([true, true, true]); + }); + + it("should mark readonly as disabled on an alternative datetime widget", () => { + const { node } = createFormComponent({ + schema: { + type: "string", + format: "date-time", + readOnly: true, + }, + uiSchema: { + "ui:widget": "alt-datetime", + }, + }); + + const readonly = [].map.call(node.querySelectorAll("select"), node => + node.hasAttribute("disabled") + ); + expect(readonly).eql([true, true, true, true, true, true]); + }); + }); + }); });