From f2d9af8721c0411b9c1f27417ec064d204c1581f Mon Sep 17 00:00:00 2001 From: Jeff Balboni Date: Tue, 24 Jan 2017 16:47:51 -0500 Subject: [PATCH 1/6] Allow empty option for enum fields --- README.md | 9 ++++ playground/samples/large.js | 6 ++- src/components/widgets/AltDateWidget.js | 9 ++-- src/components/widgets/SelectWidget.js | 19 +++++---- src/utils.js | 3 -- test/BooleanField_test.js | 2 +- test/Form_test.js | 2 +- test/StringField_test.js | 55 ++++++++++++++++++++++++- test/uiSchema_test.js | 6 +-- test/utils_test.js | 14 ------- 10 files changed, 89 insertions(+), 36 deletions(-) diff --git a/README.md b/README.md index f4cb9bfc20..6c9c9ce1ef 100644 --- a/README.md +++ b/README.md @@ -652,6 +652,15 @@ const uiSchema = { }; ``` +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" +}; +``` + ![](http://i.imgur.com/MbHypKg.png) ### Form attributes diff --git a/playground/samples/large.js b/playground/samples/large.js index 8e5150d6c3..de6240e956 100644 --- a/playground/samples/large.js +++ b/playground/samples/large.js @@ -30,6 +30,10 @@ module.exports = { choice10: {$ref: "#/definitions/largeEnum"}, } }, - uiSchema: {}, + uiSchema: { + choice1: { + "ui:placeholder": "Choose one" + } + }, formData: {} }; diff --git a/src/components/widgets/AltDateWidget.js b/src/components/widgets/AltDateWidget.js index ddbcfa90cb..b977f7c9bd 100644 --- a/src/components/widgets/AltDateWidget.js +++ b/src/components/widgets/AltDateWidget.js @@ -3,8 +3,8 @@ import React, {Component, PropTypes} from "react"; import {shouldRender, parseDateString, toDateString, pad} from "../../utils"; -function rangeOptions(type, start, stop) { - let options = [{value: -1, label: type}]; +function rangeOptions(start, stop) { + let options = []; for (let i=start; i<= stop; i++) { options.push({value: i, label: pad(i, 2)}); } @@ -24,7 +24,8 @@ function DateElement(props) { schema={{type: "integer"}} id={id} className="form-control" - options={{enumOptions: rangeOptions(type, range[0], range[1])}} + options={{enumOptions: rangeOptions(range[0], range[1])}} + placeholder={type} value={value} disabled={disabled} readonly={readonly} @@ -56,7 +57,7 @@ class AltDateWidget extends Component { } onChange = (property, value) => { - this.setState({[property]: value}, () => { + this.setState({[property]: typeof value === "undefined" ? -1 : value}, () => { // Only propagate to parent state if we have a complete date{time} if (readyForChange(this.state)) { this.props.onChange(toDateString(this.state, this.props.time)); diff --git a/src/components/widgets/SelectWidget.js b/src/components/widgets/SelectWidget.js index 41cc8ca669..0595e5dff2 100644 --- a/src/components/widgets/SelectWidget.js +++ b/src/components/widgets/SelectWidget.js @@ -7,7 +7,9 @@ import {asNumber} from "../../utils"; * always retrieved as strings. */ function processValue({type, items}, value) { - if (type === "array" && items && ["number", "integer"].includes(items.type)) { + if (value === "") { + return undefined; + } else if (type === "array" && items && ["number", "integer"].includes(items.type)) { return value.map(asNumber); } else if (type === "boolean") { return value === "true"; @@ -38,15 +40,17 @@ function SelectWidget({ multiple, autofocus, onChange, - onBlur + onBlur, + placeholder }) { const {enumOptions} = options; + const emptyValue = multiple ? [] : ""; return ( + })} + ); } diff --git a/src/utils.js b/src/utils.js index 96a94db26a..cb927c8a87 100644 --- a/src/utils.js +++ b/src/utils.js @@ -116,9 +116,6 @@ function computeDefaults(schema, parentDefaults, definitions={}) { } else if ("default" in schema) { // Use schema defaults for this node. defaults = schema.default; - } else if ("enum" in schema && Array.isArray(schema.enum)) { - // For enum with no defined default, select the first entry. - defaults = schema.enum[0]; } else if ("$ref" in schema) { // Use referenced schema defaults for this node. const refSchema = findSchemaDefinition(schema.$ref, definitions); diff --git a/test/BooleanField_test.js b/test/BooleanField_test.js index 6d3be909cc..54831dc95b 100644 --- a/test/BooleanField_test.js +++ b/test/BooleanField_test.js @@ -120,7 +120,7 @@ describe("BooleanField", () => { const labels = [].map.call(node.querySelectorAll(".field option"), label => label.textContent); - expect(labels).eql(["Yes", "No"]); + expect(labels).eql(["", "Yes", "No"]); }); it("should render the widget with the expected id", () => { diff --git a/test/Form_test.js b/test/Form_test.js index 10bb712c7a..7aff92cdff 100644 --- a/test/Form_test.js +++ b/test/Form_test.js @@ -381,7 +381,7 @@ describe("Form", () => { const {node} = createFormComponent({schema}); expect(node.querySelectorAll("option")) - .to.have.length.of(2); + .to.have.length.of(3); }); }); diff --git a/test/StringField_test.js b/test/StringField_test.js index 1dac260406..5c7cb531a3 100644 --- a/test/StringField_test.js +++ b/test/StringField_test.js @@ -158,6 +158,31 @@ describe("StringField", () => { .eql("foo"); }); + it("should render empty option", () => { + const {node} = createFormComponent({schema: { + type: "string", + enum: ["foo", "bar"], + }}); + + expect(node.querySelectorAll(".field option")[0].value) + .eql(""); + }); + + it("should render empty option with placeholder text", () => { + const {node} = createFormComponent({schema: { + type: "string", + enum: ["foo", "bar"], + }, uiSchema: { + "ui:options": { + placeholder: "Test" + } + }}); + + console.log(node.querySelectorAll(".field option")[0].innerHTML); + expect(node.querySelectorAll(".field option")[0].textContent) + .eql("Test"); + }); + it("should assign a default value", () => { const {comp} = createFormComponent({schema: { type: "string", @@ -181,6 +206,19 @@ describe("StringField", () => { expect(comp.state.formData).eql("foo"); }); + it("should reflect undefined into form state is empty option selected", () => { + const {comp, node} = createFormComponent({schema: { + type: "string", + enum: ["foo", "bar"], + }}); + + Simulate.change(node.querySelector("select"), { + target: {value: ""} + }); + + expect(comp.state.formData).to.be.undefined; + }); + it("should reflect the change into the dom", () => { const {node} = createFormComponent({schema: { type: "string", @@ -194,6 +232,19 @@ describe("StringField", () => { expect(node.querySelector("select").value).eql("foo"); }); + it("should reflect undefined value into the dom as empty option", () => { + const {node} = createFormComponent({schema: { + type: "string", + enum: ["foo", "bar"], + }}); + + Simulate.change(node.querySelector("select"), { + target: {value: ""} + }); + + expect(node.querySelector("select").value).eql(""); + }); + it("should fill field with data", () => { const {comp} = createFormComponent({schema: { type: "string", @@ -550,7 +601,7 @@ describe("StringField", () => { const monthOptions = node.querySelectorAll("select#root_month option"); const monthOptionsValues = [].map.call(monthOptions, o => o.value); expect(monthOptionsValues).eql([ - "-1", "1", "2", "3", "4", "5", "6", + "", "1", "2", "3", "4", "5", "6", "7", "8", "9", "10", "11", "12"]); }); @@ -734,7 +785,7 @@ describe("StringField", () => { const monthOptions = node.querySelectorAll("select#root_month option"); const monthOptionsValues = [].map.call(monthOptions, o => o.value); expect(monthOptionsValues).eql([ - "-1", "1", "2", "3", "4", "5", "6", "7", "8", "9", "10", "11", "12"]); + "", "1", "2", "3", "4", "5", "6", "7", "8", "9", "10", "11", "12"]); }); it("should render the widgets with the expected options' labels", () => { diff --git a/test/uiSchema_test.js b/test/uiSchema_test.js index f248137f61..3d33899efa 100644 --- a/test/uiSchema_test.js +++ b/test/uiSchema_test.js @@ -1158,15 +1158,15 @@ describe("uiSchema", () => { const {node} = createFormComponent({schema, uiSchema}); expect(node.querySelectorAll("select option")) - .to.have.length.of(2); + .to.have.length.of(3); }); it("should render boolean option labels", () => { const {node} = createFormComponent({schema, uiSchema}); - expect(node.querySelectorAll("option")[0].textContent) - .eql("yes"); expect(node.querySelectorAll("option")[1].textContent) + .eql("yes"); + expect(node.querySelectorAll("option")[2].textContent) .eql("no"); }); diff --git a/test/utils_test.js b/test/utils_test.js index 912b964c3e..97fbfc07cc 100644 --- a/test/utils_test.js +++ b/test/utils_test.js @@ -178,20 +178,6 @@ describe("utils", () => { .eql({level1: [1, 2, 3]}); }); - it("should use first enum value when no default is specified", () => { - const schema = { - type: "object", - properties: { - foo: { - type: "string", - enum: ["a", "b", "c"], - } - } - }; - expect(getDefaultFormState(schema, {})) - .eql({foo: "a"}); - }); - it("should map item defaults to fixed array default", () => { const schema = { type: "object", From a8955925e1f81c3c418d1518c29db40287a9ece5 Mon Sep 17 00:00:00 2001 From: Jeff Balboni Date: Tue, 24 Jan 2017 16:52:55 -0500 Subject: [PATCH 2/6] Fix readme --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 6c9c9ce1ef..7edc2d1ab6 100644 --- a/README.md +++ b/README.md @@ -652,6 +652,8 @@ const uiSchema = { }; ``` +![](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 @@ -661,8 +663,6 @@ const uiSchema = { }; ``` -![](http://i.imgur.com/MbHypKg.png) - ### Form attributes Form component supports the following html attributes: From 5daf19763cd4e45ed102e20aa9121025ce77dd17 Mon Sep 17 00:00:00 2001 From: Jeff Balboni Date: Thu, 26 Jan 2017 14:16:08 -0500 Subject: [PATCH 3/6] PR comments --- README.md | 5 +++++ test/StringField_test.js | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 7edc2d1ab6..5fb957b51f 100644 --- a/README.md +++ b/README.md @@ -48,6 +48,7 @@ A [live playground](https://mozilla-services.github.io/react-jsonschema-form/) i - [Auto focus](#auto-focus) - [Placeholders](#placeholders) - [Form attributes](#form-attributes) + - [Enum fields](#enum-fields) - [Advanced customization](#advanced-customization) - [Field template](#field-template) - [Array field template](#array-field-template) @@ -681,6 +682,10 @@ Form component supports the following html attributes: schema={} /> ``` +### Enum fields + +String fields that use `enum` and a `select` widget will have an empty option in the options list. When a user selects that option, the field will be set to `undefined` (similar to how regular `string` fields work if the field is empty). This also means that if you have an empty string in your `enum` array, selecting that option will cause the field to be set to `undefined`. + ## Advanced customization ### Field template diff --git a/test/StringField_test.js b/test/StringField_test.js index 5c7cb531a3..92b94a9de6 100644 --- a/test/StringField_test.js +++ b/test/StringField_test.js @@ -206,7 +206,7 @@ describe("StringField", () => { expect(comp.state.formData).eql("foo"); }); - it("should reflect undefined into form state is empty option selected", () => { + it("should reflect undefined into form state if empty option selected", () => { const {comp, node} = createFormComponent({schema: { type: "string", enum: ["foo", "bar"], From b1e50b2490622e12c42b001a1ae5f3e6e73b7510 Mon Sep 17 00:00:00 2001 From: Jeff Balboni Date: Thu, 26 Jan 2017 22:27:13 -0500 Subject: [PATCH 4/6] Updated doc --- README.md | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 5fb957b51f..d60bfa421e 100644 --- a/README.md +++ b/README.md @@ -71,6 +71,7 @@ A [live playground](https://mozilla-services.github.io/react-jsonschema-form/) i - [Custom validation](#custom-validation) - [Custom error messages](#custom-error-messages) - [Error List Display](#error-list-display) + - [Empty strings](#empty-strings) - [Styling your forms](#styling-your-forms) - [Schema definitions and references](#schema-definitions-and-references) - [JSON Schema supporting status](#json-schema-supporting-status) @@ -682,10 +683,6 @@ Form component supports the following html attributes: schema={} /> ``` -### Enum fields - -String fields that use `enum` and a `select` widget will have an empty option in the options list. When a user selects that option, the field will be set to `undefined` (similar to how regular `string` fields work if the field is empty). This also means that if you have an empty string in your `enum` array, selecting that option will cause the field to be set to `undefined`. - ## Advanced customization ### Field template @@ -1238,6 +1235,12 @@ render(( ), document.getElementById("app")); ``` +### 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 work similarly and 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. + ## 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. From 82ede03cc0f74b72c6b52e81ef334217b563e5c1 Mon Sep 17 00:00:00 2001 From: Jeff Balboni Date: Thu, 26 Jan 2017 22:27:52 -0500 Subject: [PATCH 5/6] Fix toc --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index d60bfa421e..d84e994143 100644 --- a/README.md +++ b/README.md @@ -71,7 +71,7 @@ A [live playground](https://mozilla-services.github.io/react-jsonschema-form/) i - [Custom validation](#custom-validation) - [Custom error messages](#custom-error-messages) - [Error List Display](#error-list-display) - - [Empty strings](#empty-strings) + - [The case of empty strings](#the-case-of-empty-strings) - [Styling your forms](#styling-your-forms) - [Schema definitions and references](#schema-definitions-and-references) - [JSON Schema supporting status](#json-schema-supporting-status) From 2fceb00a00e86c491516cfc41ac16f0b79fcffca Mon Sep 17 00:00:00 2001 From: Jeff Balboni Date: Thu, 26 Jan 2017 22:28:52 -0500 Subject: [PATCH 6/6] Fix toc --- README.md | 1 - 1 file changed, 1 deletion(-) diff --git a/README.md b/README.md index d84e994143..60f1852434 100644 --- a/README.md +++ b/README.md @@ -48,7 +48,6 @@ A [live playground](https://mozilla-services.github.io/react-jsonschema-form/) i - [Auto focus](#auto-focus) - [Placeholders](#placeholders) - [Form attributes](#form-attributes) - - [Enum fields](#enum-fields) - [Advanced customization](#advanced-customization) - [Field template](#field-template) - [Array field template](#array-field-template)