diff --git a/README.md b/README.md index b597906077..d79335db57 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 ### 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 a85bd6fa0f..9a022fd1d8 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 { @@ -661,34 +661,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 4b7edb41f3..4e186f9509 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, @@ -178,7 +178,7 @@ class ObjectField extends Component { let orderedProperties; try { - const properties = Object.keys(schema.properties); + const properties = Object.keys(schema.properties || {}); orderedProperties = orderProperties(properties, uiSchema["ui:order"]); } catch (err) { return ( @@ -239,25 +239,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 fd7ff9a795..498c51172c 100644 --- a/src/components/fields/SchemaField.js +++ b/src/components/fields/SchemaField.js @@ -1,9 +1,11 @@ import { ADDITIONAL_PROPERTY_FLAG } from "../../utils"; import React from "react"; import PropTypes from "prop-types"; +import * as types from "../../types"; import { isMultiSelect, + isSelect, retrieveSchema, toIdSchema, getDefaultRegistry, @@ -35,6 +37,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] : () => { @@ -123,7 +132,7 @@ function DefaultTemplate(props) { onKeyChange, } = props; if (hidden) { - return children; + return
{children}
; } const additional = props.schema.hasOwnProperty(ADDITIONAL_PROPERTY_FLAG); const keyLabel = `${label} Key`; @@ -296,7 +305,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 { @@ -330,17 +367,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 91fcf6911e..4d3d2ad52e 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, @@ -58,33 +58,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 3c8273e7a3..521ace6440 100644 --- a/src/validate.js +++ b/src/validate.js @@ -182,3 +182,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.'\"[]()=+*&^%$#@!";