Skip to content

Commit

Permalink
Fixes #114: Added a widget for datetime string format.
Browse files Browse the repository at this point in the history
Fixes #114: Added a widget for date-time string format. r=@leplatrem
  • Loading branch information
n1k0 committed Apr 4, 2016
1 parent c062b1b commit 7f2518d
Show file tree
Hide file tree
Showing 10 changed files with 153 additions and 59 deletions.
8 changes: 6 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -182,6 +182,11 @@ Here's a list of supported alternative widgets for different JSONSchema data typ
* `password`: an `input[type=password]` element;
* by default, a regular `input[type=text]` element is used.

The built-in string field also supports the JSONSchema `format` property, and will render an appropriate widget by default for the following formats:

- `date-time`: An `input[type=datetime-local]` will be rendered.
- More formats will be supported in a near future, feel free to help us going faster!

#### For `number` and `integer` fields

* `updown`: an `input[type=number]` updown selector;
Expand Down Expand Up @@ -209,7 +214,7 @@ const uiSchema = {

> Notes
>
> - Hiding widgets is only supported for `boolean`, `string`, `number`, `integer` and `date-time` schema types;
> - Hiding widgets is only supported for `boolean`, `string`, `number` and `integer` schema types;
> - An hidden widget takes its value from the `formData` prop.
### Object fields ordering
Expand Down Expand Up @@ -330,7 +335,6 @@ You can provide your own custom widgets to a uiSchema for the following json dat
- `number`
- `integer`
- `boolean`
- `date-time`

```jsx
const schema = {
Expand Down
5 changes: 5 additions & 0 deletions playground/samples/widgets.js
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,10 @@ module.exports = {
}
}
},
datetime: {
type: "string",
format: "date-time"
},
secret: {
type: "string",
default: "I'm a hidden string."
Expand Down Expand Up @@ -69,6 +73,7 @@ module.exports = {
default: "Hello...",
textarea: "... World"
},
datetime: new Date().toJSON(),
secret: "I'm a hidden string."
}
};
2 changes: 1 addition & 1 deletion src/components/fields/BooleanField.js
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ function BooleanField(props) {
required,
};
if (widget) {
const Widget = getAlternativeWidget(schema.type, widget, widgets);
const Widget = getAlternativeWidget(schema, widget, widgets);
return <Widget options={buildOptions(schema)} {... commonProps} />;
}
return <CheckboxWidget {...commonProps} />;
Expand Down
1 change: 0 additions & 1 deletion src/components/fields/SchemaField.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@ const REQUIRED_FIELD_SYMBOL = "*";
const COMPONENT_TYPES = {
"array": ArrayField,
"boolean": BooleanField,
"date-time": StringField,
"integer": NumberField,
"number": NumberField,
"object": ObjectField,
Expand Down
6 changes: 3 additions & 3 deletions src/components/fields/StringField.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ function StringField(props) {
onChange
} = props;
const {title, description} = schema;
const widget = uiSchema["ui:widget"];
const widget = uiSchema["ui:widget"] || schema.format;
const commonProps = {
schema,
id: idSchema && idSchema.id,
Expand All @@ -30,13 +30,13 @@ function StringField(props) {
};
if (Array.isArray(schema.enum)) {
if (widget) {
const Widget = getAlternativeWidget(schema.type, widget, widgets);
const Widget = getAlternativeWidget(schema, widget, widgets);
return <Widget options={optionsList(schema)} {...commonProps} />;
}
return <SelectWidget options={optionsList(schema)} {...commonProps} />;
}
if (widget) {
const Widget = getAlternativeWidget(schema.type, widget, widgets);
const Widget = getAlternativeWidget(schema, widget, widgets);
return <Widget {...commonProps} />;
}
return <TextWidget {...commonProps} />;
Expand Down
37 changes: 37 additions & 0 deletions src/components/widgets/DateTimeWidget.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import React, { PropTypes } from "react";


function DateTimeWidget({
schema,
id,
placeholder,
value,
defaultValue,
required,
onChange
}) {
return (
<input type="datetime-local"
id={id}
className="form-control"
value={value}
defaultValue={defaultValue}
placeholder={placeholder}
required={required}
onChange={(event) => onChange(event.target.value)} />
);
}

if (process.env.NODE_ENV !== "production") {
DateTimeWidget.propTypes = {
schema: PropTypes.object.isRequired,
id: PropTypes.string.isRequired,
placeholder: PropTypes.string,
value: React.PropTypes.string,
defaultValue: React.PropTypes.string,
required: PropTypes.bool,
onChange: PropTypes.func,
};
}

export default DateTimeWidget;
2 changes: 2 additions & 0 deletions src/components/widgets/HiddenWidget.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,12 @@ if (process.env.NODE_ENV !== "production") {
value: PropTypes.oneOfType([
React.PropTypes.string,
React.PropTypes.number,
React.PropTypes.bool,
]),
defaultValue: PropTypes.oneOfType([
React.PropTypes.string,
React.PropTypes.number,
React.PropTypes.bool,
])
};
}
Expand Down
28 changes: 20 additions & 8 deletions src/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ import RadioWidget from "./components/widgets/RadioWidget";
import UpDownWidget from "./components/widgets/UpDownWidget";
import RangeWidget from "./components/widgets/RangeWidget";
import SelectWidget from "./components/widgets/SelectWidget";
import TextWidget from "./components/widgets/TextWidget";
import DateTimeWidget from "./components/widgets/DateTimeWidget";
import TextareaWidget from "./components/widgets/TextareaWidget";
import HiddenWidget from "./components/widgets/HiddenWidget";

Expand Down Expand Up @@ -33,17 +35,22 @@ const altWidgetMap = {
updown: UpDownWidget,
range: RangeWidget,
hidden: HiddenWidget,
},
"date-time": {
hidden: HiddenWidget,
}
};

const stringFormatWidgets = {
"date-time": DateTimeWidget,
"email": TextWidget, // XXX: to customize appropriately
"hostname": TextWidget,
"ipv4": TextWidget,
"ipv6": TextWidget,
"uri": TextWidget, // XXX: to customize appropriately
};

export function defaultTypeValue(type) {
switch (type) {
case "array": return [];
case "boolean": return false;
case "date-time": return "";
case "number": return 0;
case "object": return {};
case "string": return "";
Expand All @@ -55,7 +62,8 @@ export function defaultFieldValue(formData, schema) {
return formData === null ? defaultTypeValue(schema.type) : formData;
}

export function getAlternativeWidget(type, widget, registeredWidgets={}) {
export function getAlternativeWidget(schema, widget, registeredWidgets={}) {
const {type, format} = schema;
if (typeof widget === "function") {
return widget;
}
Expand All @@ -68,10 +76,14 @@ export function getAlternativeWidget(type, widget, registeredWidgets={}) {
if (!altWidgetMap.hasOwnProperty(type)) {
throw new Error(`No alternative widget for type ${type}`);
}
if (!altWidgetMap[type].hasOwnProperty(widget)) {
throw new Error(`No alternative widget "${widget}" for type ${type}`);
if (altWidgetMap[type].hasOwnProperty(widget)) {
return altWidgetMap[type][widget];
}
if (type === "string" && stringFormatWidgets.hasOwnProperty(format)) {
return stringFormatWidgets[format];
}
return altWidgetMap[type][widget];
const info = type === "string" && format ? `/${format}` : "";
throw new Error(`No alternative widget "${widget}" for type ${type}${info}`);
}

function computeDefaults(schema, parentDefaults, definitions={}) {
Expand Down
79 changes: 79 additions & 0 deletions test/StringField_test.js
Original file line number Diff line number Diff line change
Expand Up @@ -163,4 +163,83 @@ describe("StringField", () => {
.eql("root");
});
});

describe("DateTimeWidget", () => {
it("should render a datetime field", () => {
const {node} = createFormComponent({schema: {
type: "string",
format: "date-time",
}});

expect(node.querySelectorAll(".field [type=datetime-local]"))
.to.have.length.of(1);
});

it("should render a string field with a label", () => {
const {node} = createFormComponent({schema: {
type: "string",
format: "date-time",
title: "foo",
}});

expect(node.querySelector(".field label").textContent)
.eql("foo");
});

it("should render a select field with a placeholder", () => {
const {node} = createFormComponent({schema: {
type: "string",
format: "date-time",
description: "baz",
}});

expect(node.querySelector(".field [type=datetime-local]").getAttribute("placeholder"))
.eql("baz");
});

it("should assign a default value", () => {
const datetime = new Date().toJSON();
const {comp} = createFormComponent({schema: {
type: "string",
format: "date-time",
default: datetime,
}});

expect(comp.state.formData).eql(datetime);
});

it("should reflect the change into the dom", () => {
const {node} = createFormComponent({schema: {
type: "string",
format: "date-time",
}});

const newDatetime = new Date().toJSON();
Simulate.change(node.querySelector("[type=datetime-local]"), {
target: {value: newDatetime}
});

expect(node.querySelector("[type=datetime-local]").value).eql(newDatetime);
});

it("should fill field with data", () => {
const datetime = new Date().toJSON();
const {comp} = createFormComponent({schema: {
type: "string",
format: "date-time",
}, formData: datetime});

expect(comp.state.formData).eql(datetime);
});

it("should render the widget with the expected id", () => {
const {node} = createFormComponent({schema: {
type: "string",
format: "date-time",
}});

expect(node.querySelector("[type=datetime-local]").id)
.eql("root");
});
});
});
44 changes: 0 additions & 44 deletions test/uiSchema_test.js
Original file line number Diff line number Diff line change
Expand Up @@ -54,50 +54,6 @@ describe("uiSchema", () => {
});
});

describe("date-time", () => {
const schema = {
type: "object",
properties: {
foo: {
type: "date-time",
}
}
};

describe("hidden", () => {
const uiSchema = {
foo: {
"ui:widget": "hidden"
}
};
const datetime = new Date().toJSON();

it("should accept a uiSchema object", () => {
const {node} = createFormComponent({schema, uiSchema});

expect(node.querySelectorAll("[type=hidden]"))
.to.have.length.of(1);
});

it("should support formData", () => {
const {node} = createFormComponent({schema, uiSchema, formData: {
foo: datetime
}});

expect(node.querySelector("[type=hidden]").value)
.eql(datetime);
});

it("should map widget value to a typed state one", () => {
const {comp} = createFormComponent({schema, uiSchema, formData: {
foo: datetime
}});

expect(comp.state.formData.foo).eql(datetime);
});
});
});

describe("string", () => {
const schema = {
type: "object",
Expand Down

0 comments on commit 7f2518d

Please sign in to comment.