diff --git a/README.md b/README.md index ed31dfe9..9d44d22c 100644 --- a/README.md +++ b/README.md @@ -102,6 +102,34 @@ import RcSelect from 'react-schema-form-rc-select/lib/RcSelect'; ``` +# Error Handler + +The error handler is disabled by default but you can enable it by using showErrors prop on `SchemaForm`. + +```js +... +onValidate = () => { + if (form is valid) { + ... + } else { + this.setState({ showErrors: true }); + } +} + +... + <> + + + +``` + # Contributing diff --git a/example/ExamplePage.js b/example/ExamplePage.js index ce2fd6b7..6f1a4f0c 100644 --- a/example/ExamplePage.js +++ b/example/ExamplePage.js @@ -9,11 +9,19 @@ import { MenuItem, Select } from "@material-ui/core"; +import Localizer from "./data/tests/localizer"; import ErrorBoundary from "./ErrorBoundary"; // RcSelect is still in migrating process so it's excluded for now // import RcSelect from 'react-schema-form-rc-select/lib/RcSelect'; +const examples = { + localizer: Localizer +}; -class ExamplePage extends React.Component { +type State = { + showErrors: boolean +}; + +class ExamplePage extends React.Component { tempModel = { comments: [ { @@ -33,7 +41,7 @@ class ExamplePage extends React.Component { { label: "Basic JSON Schema Type", value: "data/types.json" }, { label: "Basic Radios", value: "data/radio.json" }, { label: "Condition", value: "data/condition.json" }, - { label: "Help", value: "data/help.json"}, + { label: "Help", value: "data/help.json" }, { label: "Kitchen Sink", value: "data/kitchenSink.json" }, { label: "Login", value: "data/login.json" }, { label: "Date", value: "data/date.json" }, @@ -46,6 +54,10 @@ class ExamplePage extends React.Component { { label: "Test - Date Capture", value: "data/tests/datecapture.json" + }, + { + label: "Test - Localizer", + value: "localizer" } ], validationResult: {}, @@ -54,7 +66,9 @@ class ExamplePage extends React.Component { model: {}, schemaJson: "", formJson: "", - selected: "" + selected: "", + localization: undefined, + showErrors: false }; setStateDefault = () => this.setState({ model: this.tempModel }); @@ -67,22 +81,38 @@ class ExamplePage extends React.Component { selected: "", schema: {}, model: {}, - form: [] + form: [], + showErrors: false }); } - fetch(value) - .then(x => x.json()) - .then(({ form, schema, model }) => { - this.setState({ - schemaJson: JSON.stringify(schema, undefined, 2), - formJson: JSON.stringify(form, undefined, 2), - selected: value, - schema, - model: model || {}, - form - }); + if (!value.endsWith("json")) { + const elem = examples[value]; + this.setState({ + schemaJson: JSON.stringify(elem.schema, undefined, 2), + formJson: JSON.stringify(elem.form, undefined, 2), + selected: value, + schema: elem.schema, + model: elem.model || {}, + form: elem.form, + localization: elem.localization, + showErrors: false }); + } else { + fetch(value) + .then(x => x.json()) + .then(({ form, schema, model }) => { + this.setState({ + schemaJson: JSON.stringify(schema, undefined, 2), + formJson: JSON.stringify(form, undefined, 2), + selected: value, + schema, + model: model || {}, + form, + showErrors: false + }); + }); + } }; onModelChange = (key, val, type) => { @@ -95,7 +125,7 @@ class ExamplePage extends React.Component { onValidate = () => { const { schema, model } = this.state; const result = utils.validateBySchema(schema, model); - this.setState({ validationResult: result }); + this.setState({ validationResult: result, showErrors: true }); }; onFormChange = val => { @@ -125,7 +155,9 @@ class ExamplePage extends React.Component { selected, tests, formJson, - schemaJson + schemaJson, + localization, + showErrors } = this.state; const mapper = { // 'rc-select': RcSelect @@ -142,6 +174,8 @@ class ExamplePage extends React.Component { onModelChange={this.onModelChange} mapper={mapper} model={model} + localization={localization} + showErrors={showErrors} /> ); diff --git a/example/data/tests/localizer.js b/example/data/tests/localizer.js new file mode 100644 index 00000000..fa5c56bc --- /dev/null +++ b/example/data/tests/localizer.js @@ -0,0 +1,21 @@ +export default { + form: ["firstName", "date"], + schema: { + type: "object", + title: "Title", + properties: { + firstName: { + title: "first.name", + type: "string" + }, + date: { + title: "first.name", + type: "date" + } + } + }, + localization: { + getLocalizedString: value => + value === "first.name" ? "First Name" : value + } +}; diff --git a/src/Array.js b/src/Array.js index e2911094..2b9de922 100644 --- a/src/Array.js +++ b/src/Array.js @@ -12,6 +12,7 @@ import Typography from "@material-ui/core/Typography"; import cloneDeep from "lodash/cloneDeep"; import utils from "./utils"; import ComposedComponent from "./ComposedComponent"; +import type { Localization } from "./types"; const styles = theme => ({ arrayItem: { @@ -36,7 +37,8 @@ type Props = { mapper: any, options: any, onChangeValidate: any, - onChange: any + onChange: any, + localization: Localization }; type State = { @@ -171,7 +173,15 @@ class Array extends Component { }; render() { - const { classes, form, builder, model, mapper, onChange } = this.props; + const { + classes, + form, + builder, + model, + mapper, + onChange, + localization: { getLocalizedString } + } = this.props; const { model: stateModel } = this.state; const arrays = []; for (let i = 0; i < stateModel.length; i += 1) { @@ -198,7 +208,9 @@ class Array extends Component { return (
- {form.title} + + {getLocalizedString(form.title)} +
{arrays}
) : ( "" diff --git a/src/__tests__/ComposedComponent-test.js b/src/__tests__/ComposedComponent-test.js index 59bcba72..9f7ac39f 100644 --- a/src/__tests__/ComposedComponent-test.js +++ b/src/__tests__/ComposedComponent-test.js @@ -56,7 +56,12 @@ describe("ComposedComponent", () => { const Composed = ComposedComponent(Text); renderer.render( - + ); const result = renderer.getRenderOutput(); diff --git a/src/__tests__/Localizer-test.js b/src/__tests__/Localizer-test.js new file mode 100644 index 00000000..afe66bff --- /dev/null +++ b/src/__tests__/Localizer-test.js @@ -0,0 +1,138 @@ +// @flow +import React from "react"; +import { mount, configure } from "enzyme"; +import Adapter from "enzyme-adapter-react-16"; +import SchemaForm from "../SchemaForm"; + +configure({ adapter: new Adapter() }); + +jest.dontMock("../ComposedComponent"); +jest.dontMock("../utils"); +jest.dontMock("lodash"); + +const getLocalizedString = value => { + switch (value) { + case "first.name": + return "First Name"; + case "last.name": + return "Last Name"; + case "number.field": + return "Number Field"; + case "integer.field": + return "Integer Field"; + case "boolean.field": + return "Boolean Field"; + case "array.field": + return "Array Field"; + case "date.field": + return "Date Field"; + case "tBoolean.field": + return "T Boolean Field"; + default: + return value; + } +}; + +const localization = { + getLocalizedString +}; + +const config = { + form: [ + "firstName", + "lastName", + "numberField", + "integerField", + "booleanField", + "dateField", + "tBooleanField", + "arrayField" + ], + schema: { + type: "object", + title: "Title", + properties: { + firstName: { + title: "first.name", + type: "string" + }, + lastName: { + title: "last.name", + type: "string" + }, + numberField: { + title: "number.field", + type: "string" + }, + integerField: { + title: "integer.field", + type: "integer" + }, + booleanField: { + title: "boolean.field", + type: "boolean" + }, + dateField: { + title: "date.field", + type: "date" + }, + tBooleanField: { + title: "tBoolean.field", + type: "date" + }, + arrayField: { + title: "array.field", + type: "boolean", + items: ["test1", "test2"] + } + } + } +}; + +describe("Localizer test", () => { + it("Schema that has localizer should render labels properly", () => { + const wrapper = mount( + + ); + const labels = wrapper.find("label"); + expect(wrapper.find("label").length).toBe(8); + labels.forEach((each, index) => { + if (index === 0) expect(each.text()).toBe("First Name"); + if (index === 1) expect(each.text()).toBe("Last Name"); + if (index === 2) expect(each.text()).toBe("Number Field"); + if (index === 3) expect(each.text()).toBe("Integer Field"); + if (index === 4) expect(each.text()).toBe("Boolean Field"); + if (index === 5) expect(each.text()).toBe("Date Field"); + if (index === 6) expect(each.text()).toBe("T Boolean Field"); + if (index === 7) + expect(each.find("Typography > span").text()).toBe( + "Array Field" + ); + }); + }); + + it("Schema that has not localizer should render labels properly", () => { + const wrapper = mount( + + ); + const labels = wrapper.find("label"); + expect(wrapper.find("label").length).toBe(8); + labels.forEach((each, index) => { + if (index === 0) expect(each.text()).toBe("first.name"); + if (index === 1) expect(each.text()).toBe("last.name"); + if (index === 2) expect(each.text()).toBe("number.field"); + if (index === 3) expect(each.text()).toBe("integer.field"); + if (index === 4) expect(each.text()).toBe("boolean.field"); + if (index === 5) expect(each.text()).toBe("date.field"); + if (index === 6) expect(each.text()).toBe("tBoolean.field"); + if (index === 7) + expect(each.find("Typography > span").text()).toBe( + "array.field" + ); + }); + }); +}); diff --git a/src/types/index.js b/src/types/index.js new file mode 100644 index 00000000..0ecd0381 --- /dev/null +++ b/src/types/index.js @@ -0,0 +1,3 @@ +// @flow +export type { Localization } from "./localization"; +export type { Localization as ss2 } from "./localization"; diff --git a/src/types/localization.js b/src/types/localization.js new file mode 100644 index 00000000..7646dbe8 --- /dev/null +++ b/src/types/localization.js @@ -0,0 +1,7 @@ +// @flow + +export type Localization = { + getLocalizedDate: (string | Date) => string, + getLocalizedString: (string, ...params: Array) => string, + getLocalizedNumber: (number | string) => string +};