From 37864eb127855f4731345d5ca9f3e4e8fc896370 Mon Sep 17 00:00:00 2001 From: Naeem Baghi Date: Tue, 27 Nov 2018 20:49:41 +0330 Subject: [PATCH 1/6] support localized strings, dates and numbers --- src/Array.js | 18 +++++++++-- src/Checkbox.js | 53 +++++++++++++++---------------- src/FieldSet.js | 11 +++++-- src/MultiSelect.js | 18 +++++++---- src/NativeDateField.js | 41 ++++++++++++------------ src/Number.js | 65 +++++++++++++++++++++++++++------------ src/Radios.js | 26 ++++++++++++---- src/SchemaForm.js | 28 ++++++++++++++--- src/Select.js | 15 ++++++--- src/Text.js | 18 ++++++++--- src/TextArea.js | 18 ++++++++--- src/TripleBoolean.js | 28 ++++++++++++----- src/types/index.js | 3 ++ src/types/localization.js | 7 +++++ 14 files changed, 235 insertions(+), 114 deletions(-) create mode 100644 src/types/index.js create mode 100644 src/types/localization.js 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/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 +}; From bb3155486527fd7ff870acd1acc2ed9dc0aa0437 Mon Sep 17 00:00:00 2001 From: Naeem Baghi Date: Wed, 28 Nov 2018 18:23:04 +0330 Subject: [PATCH 2/6] SchemaForm: fix bug in partial localization --- src/SchemaForm.js | 28 +++++++++++++++++++++------- 1 file changed, 21 insertions(+), 7 deletions(-) diff --git a/src/SchemaForm.js b/src/SchemaForm.js index acaa07a0..21048827 100644 --- a/src/SchemaForm.js +++ b/src/SchemaForm.js @@ -45,11 +45,7 @@ const formatDate = (date: string | Date) => { class SchemaForm extends Component { static defaultProps = { - localization: { - getLocalizedString: value => value, - getLocalizedNumber: value => value, - getLocalizedDate: formatDate - } + localization: undefined }; mapper = { @@ -91,8 +87,26 @@ class SchemaForm extends Component { onModelChange(key, value, form.type, form); }; + getLocalization = () => { + const { localization } = this.props; + return { + getLocalizedString: + localization && localization.getLocalizedString + ? localization.getLocalizedString + : value => value, + getLocalizedNumber: + localization && localization.getLocalizedNumber + ? localization.getLocalizedNumber + : value => value, + getLocalizedDate: + localization && localization.getLocalizedDate + ? localization.getLocalizedDate + : formatDate + }; + }; + builder(form, model, index, mapper, onChange, builder) { - const { errors, localization } = this.props; + const { errors } = this.props; const Field = this.mapper[form.type]; if (!Field) { return null; @@ -120,7 +134,7 @@ class SchemaForm extends Component { mapper={mapper} builder={builder} errorText={error} - localization={localization} + localization={this.getLocalization()} /> ); } From 359cc703cefbbe798aa4d5e5f441a444f89062b3 Mon Sep 17 00:00:00 2001 From: Naeem Baghi Date: Sun, 23 Dec 2018 14:24:30 +0330 Subject: [PATCH 3/6] ExamplePage: add example for using localizer --- example/ExamplePage.js | 52 ++++++++++++++++++++++++--------- example/data/tests/localizer.js | 21 +++++++++++++ 2 files changed, 59 insertions(+), 14 deletions(-) create mode 100644 example/data/tests/localizer.js diff --git a/example/ExamplePage.js b/example/ExamplePage.js index ce2fd6b7..3ea391ea 100644 --- a/example/ExamplePage.js +++ b/example/ExamplePage.js @@ -9,9 +9,13 @@ 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 { tempModel = { @@ -33,7 +37,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 +50,10 @@ class ExamplePage extends React.Component { { label: "Test - Date Capture", value: "data/tests/datecapture.json" + }, + { + label: "Test - Localizer", + value: "localizer" } ], validationResult: {}, @@ -54,7 +62,8 @@ class ExamplePage extends React.Component { model: {}, schemaJson: "", formJson: "", - selected: "" + selected: "", + localization: undefined }; setStateDefault = () => this.setState({ model: this.tempModel }); @@ -71,18 +80,31 @@ class ExamplePage extends React.Component { }); } - 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 }); + } 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 + }); + }); + } }; onModelChange = (key, val, type) => { @@ -125,7 +147,8 @@ class ExamplePage extends React.Component { selected, tests, formJson, - schemaJson + schemaJson, + localization } = this.state; const mapper = { // 'rc-select': RcSelect @@ -142,6 +165,7 @@ class ExamplePage extends React.Component { onModelChange={this.onModelChange} mapper={mapper} model={model} + localization={localization} /> ); 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 + } +}; From db2f972d82b268c9ac3ea2744ba46df2452c0837 Mon Sep 17 00:00:00 2001 From: Naeem Baghi Date: Sun, 23 Dec 2018 14:24:45 +0330 Subject: [PATCH 4/6] Localizer-test.js: add tests for localizer --- src/__tests__/Localizer-test.js | 138 ++++++++++++++++++++++++++++++++ 1 file changed, 138 insertions(+) create mode 100644 src/__tests__/Localizer-test.js 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" + ); + }); + }); +}); From 9d4bd48b3a50b05aa22bf89123c8eb2079c56976 Mon Sep 17 00:00:00 2001 From: Naeem Baghi Date: Sun, 23 Dec 2018 15:55:53 +0330 Subject: [PATCH 5/6] Fix validation behavior on load --- example/ExamplePage.js | 24 +++++++++++++++++------- src/ComposedComponent.js | 34 +++++++++++++++++++++++++--------- src/SchemaForm.js | 9 ++++++--- 3 files changed, 48 insertions(+), 19 deletions(-) diff --git a/example/ExamplePage.js b/example/ExamplePage.js index 3ea391ea..c99a0e98 100644 --- a/example/ExamplePage.js +++ b/example/ExamplePage.js @@ -17,7 +17,11 @@ const examples = { localizer: Localizer }; -class ExamplePage extends React.Component { +type State = { + showError: boolean +}; + +class ExamplePage extends React.Component { tempModel = { comments: [ { @@ -63,7 +67,8 @@ class ExamplePage extends React.Component { schemaJson: "", formJson: "", selected: "", - localization: undefined + localization: undefined, + showError: false }; setStateDefault = () => this.setState({ model: this.tempModel }); @@ -76,7 +81,8 @@ class ExamplePage extends React.Component { selected: "", schema: {}, model: {}, - form: [] + form: [], + showError: false }); } @@ -89,7 +95,8 @@ class ExamplePage extends React.Component { schema: elem.schema, model: elem.model || {}, form: elem.form, - localization: elem.localization + localization: elem.localization, + showError: false }); } else { fetch(value) @@ -101,7 +108,8 @@ class ExamplePage extends React.Component { selected: value, schema, model: model || {}, - form + form, + showError: false }); }); } @@ -117,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, showError: true }); }; onFormChange = val => { @@ -148,7 +156,8 @@ class ExamplePage extends React.Component { tests, formJson, schemaJson, - localization + localization, + showError } = this.state; const mapper = { // 'rc-select': RcSelect @@ -166,6 +175,7 @@ class ExamplePage extends React.Component { mapper={mapper} model={model} localization={localization} + showError={showError} /> ); diff --git a/src/ComposedComponent.js b/src/ComposedComponent.js index fb5f923f..b9377771 100755 --- a/src/ComposedComponent.js +++ b/src/ComposedComponent.js @@ -24,23 +24,39 @@ export default (ComposedComponent, defaultProps = {}) => class Composed extends React.Component { constructor(props) { super(props); - const { errorText, form } = this.props; + const { errorText, form, showError } = this.props; this.onChangeValidate = this.onChangeValidate.bind(this); const value = defaultValue(this.props); const validationResult = utils.validate(form, value); - this.state = { - value, - valid: !!(validationResult.valid || !value), - error: - (!validationResult.valid && - (value ? validationResult.error.message : null)) || - errorText - }; + if (!showError) { + this.state = { + value, + valid: true, + error: "" + }; + } else { + this.state = { + value, + valid: !!(validationResult.valid || !value), + error: + (!validationResult.valid && + (value ? validationResult.error.message : null)) || + errorText + }; + } } static getDerivedStateFromProps(nextProps) { const value = defaultValue(nextProps); + const { showError } = nextProps; const validationResult = utils.validate(nextProps.form, value); + if (!showError) { + return { + value, + valid: true, + error: "" + }; + } return { value, valid: validationResult.valid, diff --git a/src/SchemaForm.js b/src/SchemaForm.js index 21048827..7a663b42 100644 --- a/src/SchemaForm.js +++ b/src/SchemaForm.js @@ -31,7 +31,8 @@ type Props = { model: any, className: any, mapper: any, - localization?: Localization + localization?: Localization, + showError?: boolean }; const formatDate = (date: string | Date) => { @@ -45,7 +46,8 @@ const formatDate = (date: string | Date) => { class SchemaForm extends Component { static defaultProps = { - localization: undefined + localization: undefined, + showError: false }; mapper = { @@ -106,7 +108,7 @@ class SchemaForm extends Component { }; builder(form, model, index, mapper, onChange, builder) { - const { errors } = this.props; + const { errors, showError } = this.props; const Field = this.mapper[form.type]; if (!Field) { return null; @@ -135,6 +137,7 @@ class SchemaForm extends Component { builder={builder} errorText={error} localization={this.getLocalization()} + showError={showError} /> ); } From c867e78eaa9106d82780aca0be1a4fc2187e6d16 Mon Sep 17 00:00:00 2001 From: Naeem Baghi Date: Sun, 23 Dec 2018 16:09:58 +0330 Subject: [PATCH 6/6] Fix validation behavior on load --- README.md | 28 +++++++++++++++++++++++++ example/ExamplePage.js | 16 +++++++------- src/ComposedComponent.js | 8 +++---- src/SchemaForm.js | 8 +++---- src/__tests__/ComposedComponent-test.js | 7 ++++++- 5 files changed, 50 insertions(+), 17 deletions(-) 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 c99a0e98..6f1a4f0c 100644 --- a/example/ExamplePage.js +++ b/example/ExamplePage.js @@ -18,7 +18,7 @@ const examples = { }; type State = { - showError: boolean + showErrors: boolean }; class ExamplePage extends React.Component { @@ -68,7 +68,7 @@ class ExamplePage extends React.Component { formJson: "", selected: "", localization: undefined, - showError: false + showErrors: false }; setStateDefault = () => this.setState({ model: this.tempModel }); @@ -82,7 +82,7 @@ class ExamplePage extends React.Component { schema: {}, model: {}, form: [], - showError: false + showErrors: false }); } @@ -96,7 +96,7 @@ class ExamplePage extends React.Component { model: elem.model || {}, form: elem.form, localization: elem.localization, - showError: false + showErrors: false }); } else { fetch(value) @@ -109,7 +109,7 @@ class ExamplePage extends React.Component { schema, model: model || {}, form, - showError: false + showErrors: false }); }); } @@ -125,7 +125,7 @@ class ExamplePage extends React.Component { onValidate = () => { const { schema, model } = this.state; const result = utils.validateBySchema(schema, model); - this.setState({ validationResult: result, showError: true }); + this.setState({ validationResult: result, showErrors: true }); }; onFormChange = val => { @@ -157,7 +157,7 @@ class ExamplePage extends React.Component { formJson, schemaJson, localization, - showError + showErrors } = this.state; const mapper = { // 'rc-select': RcSelect @@ -175,7 +175,7 @@ class ExamplePage extends React.Component { mapper={mapper} model={model} localization={localization} - showError={showError} + showErrors={showErrors} /> ); diff --git a/src/ComposedComponent.js b/src/ComposedComponent.js index b9377771..0ed0fa02 100755 --- a/src/ComposedComponent.js +++ b/src/ComposedComponent.js @@ -24,11 +24,11 @@ export default (ComposedComponent, defaultProps = {}) => class Composed extends React.Component { constructor(props) { super(props); - const { errorText, form, showError } = this.props; + const { errorText, form, showErrors } = this.props; this.onChangeValidate = this.onChangeValidate.bind(this); const value = defaultValue(this.props); const validationResult = utils.validate(form, value); - if (!showError) { + if (!showErrors) { this.state = { value, valid: true, @@ -48,9 +48,9 @@ export default (ComposedComponent, defaultProps = {}) => static getDerivedStateFromProps(nextProps) { const value = defaultValue(nextProps); - const { showError } = nextProps; + const { showErrors } = nextProps; const validationResult = utils.validate(nextProps.form, value); - if (!showError) { + if (!showErrors) { return { value, valid: true, diff --git a/src/SchemaForm.js b/src/SchemaForm.js index 7a663b42..23bc74a2 100644 --- a/src/SchemaForm.js +++ b/src/SchemaForm.js @@ -32,7 +32,7 @@ type Props = { className: any, mapper: any, localization?: Localization, - showError?: boolean + showErrors?: boolean }; const formatDate = (date: string | Date) => { @@ -47,7 +47,7 @@ const formatDate = (date: string | Date) => { class SchemaForm extends Component { static defaultProps = { localization: undefined, - showError: false + showErrors: false }; mapper = { @@ -108,7 +108,7 @@ class SchemaForm extends Component { }; builder(form, model, index, mapper, onChange, builder) { - const { errors, showError } = this.props; + const { errors, showErrors } = this.props; const Field = this.mapper[form.type]; if (!Field) { return null; @@ -137,7 +137,7 @@ class SchemaForm extends Component { builder={builder} errorText={error} localization={this.getLocalization()} - showError={showError} + showErrors={showErrors} /> ); } 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();