Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,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)
- [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)
Expand Down Expand Up @@ -654,6 +655,15 @@ 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
const schema = {type: "string", enum: ["First", "Second"]};
const uiSchema = {
"ui:placeholder": "Choose an option"
};
```

### Form attributes

Form component supports the following html attributes:
Expand Down Expand Up @@ -1224,6 +1234,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.
Expand Down
6 changes: 5 additions & 1 deletion playground/samples/large.js
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,10 @@ module.exports = {
choice10: {$ref: "#/definitions/largeEnum"},
}
},
uiSchema: {},
uiSchema: {
choice1: {
"ui:placeholder": "Choose one"
}
},
formData: {}
};
9 changes: 5 additions & 4 deletions src/components/widgets/AltDateWidget.js
Original file line number Diff line number Diff line change
Expand Up @@ -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)});
}
Expand All @@ -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}
Expand Down Expand Up @@ -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));
Expand Down
19 changes: 12 additions & 7 deletions src/components/widgets/SelectWidget.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We need to add a warning somewhere in the docs, because if one adds "" as an enum choice in their schema, it will be automatically converted to undefined and drop the property from any parent object - which is now consistent with our text inputs but may be surprising to users.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I added a note, but I'm not sure how well I explained it or if there's a better spot.

} else if (type === "array" && items && ["number", "integer"].includes(items.type)) {
return value.map(asNumber);
} else if (type === "boolean") {
return value === "true";
Expand Down Expand Up @@ -38,15 +40,17 @@ function SelectWidget({
multiple,
autofocus,
onChange,
onBlur
onBlur,
placeholder
}) {
const {enumOptions} = options;
const emptyValue = multiple ? [] : "";
return (
<select
id={id}
multiple={multiple}
className="form-control"
value={value}
value={typeof value === "undefined" ? emptyValue : value}
required={required}
disabled={disabled}
readOnly={readonly}
Expand All @@ -58,11 +62,12 @@ function SelectWidget({
onChange={(event) => {
const newValue = getValue(event, multiple);
onChange(processValue(schema, newValue));
}}>{
enumOptions.map(({value, label}, i) => {
}}>
{!multiple && !schema.default && <option value="">{placeholder}</option>}
{enumOptions.map(({value, label}, i) => {
return <option key={i} value={value}>{label}</option>;
})
}</select>
})}
</select>
);
}

Expand Down
3 changes: 0 additions & 3 deletions src/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
2 changes: 1 addition & 1 deletion test/BooleanField_test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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", () => {
Expand Down
2 changes: 1 addition & 1 deletion test/Form_test.js
Original file line number Diff line number Diff line change
Expand Up @@ -381,7 +381,7 @@ describe("Form", () => {
const {node} = createFormComponent({schema});

expect(node.querySelectorAll("option"))
.to.have.length.of(2);
.to.have.length.of(3);
});
});

Expand Down
55 changes: 53 additions & 2 deletions test/StringField_test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -181,6 +206,19 @@ describe("StringField", () => {
expect(comp.state.formData).eql("foo");
});

it("should reflect undefined into form state if 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",
Expand All @@ -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",
Expand Down Expand Up @@ -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"]);
});

Expand Down Expand Up @@ -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", () => {
Expand Down
6 changes: 3 additions & 3 deletions test/uiSchema_test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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");
});

Expand Down
14 changes: 0 additions & 14 deletions test/utils_test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down