Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 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
14 changes: 14 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
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 All @@ -672,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`.

@n1k0 n1k0 Jan 27, 2017

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

I think I'd move this in new section under Form data validation, eg. The case of empty strings.


## Advanced customization

### Field template
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
Copy Markdown
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
Copy Markdown
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