Skip to content

Commit 53b1a0d

Browse files
committed
Fixes rjsf-team#122: Better DateTimeWidget.
1 parent ecdd43e commit 53b1a0d

File tree

6 files changed

+168
-61
lines changed

6 files changed

+168
-61
lines changed

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -212,7 +212,7 @@ Here's a list of supported alternative widgets for different JSONSchema data typ
212212

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

215-
- `date-time`: An `input[type=datetime-local]` element is used;
215+
- `date-time`: Six `select` elements are used to select the year, the month, the day, the hour, the minute and the second;
216216
- `email`: An `input[type=email]` element is used;
217217
- `uri`: An `input[type=url]` element is used;
218218
- More formats could be supported in a near future, feel free to help us going faster!

playground/samples/simple.js

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,11 @@ module.exports = {
2424
type: "string",
2525
title: "Password",
2626
minLength: 3
27+
},
28+
date: {
29+
type:"string",
30+
format: "date-time",
31+
title: "Subscription date"
2732
}
2833
}
2934
},
@@ -45,5 +50,6 @@ module.exports = {
4550
age: 75,
4651
bio: "Roundhouse kicking asses since 1940",
4752
password: "noneed",
53+
date: new Date().toJSON()
4854
}
4955
};

src/components/widgets/DateTimeWidget.js

Lines changed: 77 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,84 @@
1-
import React, { PropTypes } from "react";
2-
3-
4-
function DateTimeWidget({
5-
schema,
6-
id,
7-
placeholder,
8-
value,
9-
defaultValue,
10-
required,
11-
onChange
12-
}) {
1+
import React, { Component, PropTypes } from "react";
2+
3+
import { shouldRender, parseDateString, toDateString, pad } from "../../utils";
4+
import SelectWidget from "../widgets/SelectWidget";
5+
6+
7+
function rangeOptions(start, stop) {
8+
let options = [];
9+
for (let i=start; i<= stop; i++) {
10+
options.push({value: i, label: pad(i, 2)});
11+
}
12+
return options;
13+
}
14+
15+
function DateElement({type, range, value, select, rootId}) {
16+
const id = rootId + "_" + type;
1317
return (
14-
<input type="datetime-local"
15-
id={id}
16-
className="form-control"
17-
value={value}
18-
defaultValue={defaultValue}
19-
placeholder={placeholder}
20-
required={required}
21-
onChange={(event) => onChange(event.target.value)} />
18+
<li>
19+
<SelectWidget
20+
schema={{type: "integer"}}
21+
id={id}
22+
className="form-control"
23+
options={rangeOptions(range[0], range[1])}
24+
value={value}
25+
onChange={select} />
26+
<p className="text-center help-block">
27+
<label htmlFor={id}><small>{type}</small></label>
28+
</p>
29+
</li>
2230
);
2331
}
2432

33+
class DateTimeWidget extends Component {
34+
constructor(props) {
35+
super(props);
36+
this.state = parseDateString(props.value || props.defaultValue);
37+
}
38+
39+
componentWillReceiveProps(nextProps) {
40+
this.setState(parseDateString(nextProps.value || nextProps.defaultValue));
41+
}
42+
43+
shouldComponentUpdate(nextProps, nextState) {
44+
return shouldRender(this, nextProps, nextState);
45+
}
46+
47+
onChange = (property, value) => {
48+
this.setState({[property]: value}, () => {
49+
this.props.onChange(toDateString(this.state));
50+
});
51+
};
52+
53+
onYearChange = (value) => this.onChange("year", value);
54+
onMonthChange = (value) => this.onChange("month", value);
55+
onDayChange = (value) => this.onChange("day", value);
56+
onHourChange = (value) => this.onChange("hour", value);
57+
onMinuteChange = (value) => this.onChange("minute", value);
58+
onSecondChange = (value) => this.onChange("second", value);
59+
60+
render() {
61+
const {id} = this.props;
62+
const {year, month, day, hour, minute, second} = this.state;
63+
return (
64+
<ul className="list-inline">
65+
<DateElement type="year" rootId={id} range={[1900, 2020]}
66+
value={year} select={this.onYearChange} />
67+
<DateElement type="month" rootId={id} range={[1, 12]}
68+
value={month} select={this.onMonthChange} />
69+
<DateElement type="day" rootId={id} range={[1, 31]}
70+
value={day} select={this.onDayChange} />
71+
<DateElement type="hour" rootId={id} range={[0, 23]}
72+
value={hour} select={this.onHourChange} />
73+
<DateElement type="minute" rootId={id} range={[0, 59]}
74+
value={minute} select={this.onMinuteChange} />
75+
<DateElement type="second" rootId={id} range={[0, 59]}
76+
value={second} select={this.onSecondChange} />
77+
</ul>
78+
);
79+
}
80+
}
81+
2582
if (process.env.NODE_ENV !== "production") {
2683
DateTimeWidget.propTypes = {
2784
schema: PropTypes.object.isRequired,

src/utils.js

Lines changed: 42 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -49,19 +49,25 @@ const stringFormatWidgets = {
4949
"uri": URLWidget,
5050
};
5151

52-
export function defaultTypeValue(type) {
52+
export function defaultTypeValue(schema) {
53+
const {type} = schema;
5354
switch (type) {
5455
case "array": return [];
5556
case "boolean": return false;
5657
case "number": return 0;
5758
case "object": return {};
58-
case "string": return "";
59+
case "string": {
60+
if (schema.format === "date-time") {
61+
return new Date().toJSON();
62+
}
63+
return "";
64+
}
5965
default: return undefined;
6066
}
6167
}
6268

6369
export function defaultFieldValue(formData, schema) {
64-
return formData === null ? defaultTypeValue(schema.type) : formData;
70+
return formData === null ? defaultTypeValue(schema) : formData;
6571
}
6672

6773
export function getAlternativeWidget(schema, widget, registeredWidgets={}) {
@@ -108,7 +114,7 @@ function computeDefaults(schema, parentDefaults, definitions={}) {
108114
}
109115
// Not defaults defined for this node, fallback to generic typed ones.
110116
if (typeof(defaults) === "undefined") {
111-
defaults = defaultTypeValue(schema.type);
117+
defaults = defaultTypeValue(schema);
112118
}
113119
// We need to recur for object schema inner default values.
114120
if (schema.type === "object") {
@@ -288,3 +294,35 @@ export function toIdSchema(schema, id, definitions) {
288294
}
289295
return idSchema;
290296
}
297+
298+
export function parseDateString(dateString) {
299+
if (!dateString) {
300+
dateString = new Date().toJSON();
301+
}
302+
const date = new Date(dateString);
303+
if (Number.isNaN(date.getTime())) {
304+
throw new Error("Unable to parse date " + dateString);
305+
}
306+
return {
307+
year: date.getUTCFullYear(),
308+
month: date.getUTCMonth() + 1, // oh you, javascript.
309+
day: date.getUTCDate(),
310+
hour: date.getUTCHours(),
311+
minute: date.getUTCMinutes(),
312+
second: date.getUTCSeconds(),
313+
};
314+
}
315+
316+
export function toDateString(dateObj) {
317+
const {year, month, day, hour, minute, second} = dateObj;
318+
const utcTime = Date.UTC(year, month - 1, day, hour, minute, second);
319+
return new Date(utcTime).toJSON();
320+
}
321+
322+
export function pad(num, size) {
323+
let s = String(num);
324+
while (s.length < size) {
325+
s = "0" + s;
326+
}
327+
return s;
328+
}

test/StringField_test.js

Lines changed: 21 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -171,11 +171,11 @@ describe("StringField", () => {
171171
format: "date-time",
172172
}});
173173

174-
expect(node.querySelectorAll(".field [type=datetime-local]"))
175-
.to.have.length.of(1);
174+
expect(node.querySelectorAll(".field select"))
175+
.to.have.length.of(6);
176176
});
177177

178-
it("should render a string field with a label", () => {
178+
it("should render a string field with a main label", () => {
179179
const {node} = createFormComponent({schema: {
180180
type: "string",
181181
format: "date-time",
@@ -186,17 +186,6 @@ describe("StringField", () => {
186186
.eql("foo");
187187
});
188188

189-
it("should render a select field with a placeholder", () => {
190-
const {node} = createFormComponent({schema: {
191-
type: "string",
192-
format: "date-time",
193-
description: "baz",
194-
}});
195-
196-
expect(node.querySelector(".field [type=datetime-local]").getAttribute("placeholder"))
197-
.eql("baz");
198-
});
199-
200189
it("should assign a default value", () => {
201190
const datetime = new Date().toJSON();
202191
const {comp} = createFormComponent({schema: {
@@ -209,17 +198,19 @@ describe("StringField", () => {
209198
});
210199

211200
it("should reflect the change into the dom", () => {
212-
const {node} = createFormComponent({schema: {
201+
const {comp, node} = createFormComponent({schema: {
213202
type: "string",
214203
format: "date-time",
215204
}});
216205

217-
const newDatetime = new Date().toJSON();
218-
Simulate.change(node.querySelector("[type=datetime-local]"), {
219-
target: {value: newDatetime}
220-
});
206+
Simulate.change(node.querySelector("#root_year"), {target: {value: "2010"}});
207+
Simulate.change(node.querySelector("#root_month"), {target: {value: "12"}});
208+
Simulate.change(node.querySelector("#root_day"), {target: {value: "1"}});
209+
Simulate.change(node.querySelector("#root_hour"), {target: {value: "0"}});
210+
Simulate.change(node.querySelector("#root_minute"), {target: {value: "0"}});
211+
Simulate.change(node.querySelector("#root_second"), {target: {value: "0"}});
221212

222-
expect(node.querySelector("[type=datetime-local]").value).eql(newDatetime);
213+
expect(comp.state.formData).eql("2010-12-01T00:00:00.000Z");
223214
});
224215

225216
it("should fill field with data", () => {
@@ -232,27 +223,22 @@ describe("StringField", () => {
232223
expect(comp.state.formData).eql(datetime);
233224
});
234225

235-
it("should render the widget with the expected id", () => {
226+
it("should render the widgets with the expected ids", () => {
236227
const {node} = createFormComponent({schema: {
237228
type: "string",
238229
format: "date-time",
239230
}});
240231

241-
expect(node.querySelector("[type=datetime-local]").id)
242-
.eql("root");
243-
});
232+
const ids = [].map.call(node.querySelectorAll("select"), node => node.id);
244233

245-
it("should reject an invalid entered datetime", () => {
246-
const {comp, node} = createFormComponent({schema: {
247-
type: "string",
248-
format: "date-time",
249-
}, liveValidate: true});
250-
251-
Simulate.change(node.querySelector("[type=datetime-local]"), {
252-
target: {value: "invalid"}
253-
});
254-
255-
expect(comp.state.errors).to.have.length.of(1);
234+
expect(ids).eql([
235+
"root_year",
236+
"root_month",
237+
"root_day",
238+
"root_hour",
239+
"root_minute",
240+
"root_second",
241+
]);
256242
});
257243
});
258244

test/utils_test.js

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,8 @@ import {
77
mergeObjects,
88
retrieveSchema,
99
shouldRender,
10-
toIdSchema
10+
toIdSchema,
11+
parseDateString
1112
} from "../src/utils";
1213

1314

@@ -470,4 +471,23 @@ describe("utils", () => {
470471
});
471472
});
472473
});
474+
475+
describe("parseDateString()", () => {
476+
it("should parse a valid JSON datetime string", () => {
477+
expect(parseDateString("2016-04-05T14:01:30.182Z"))
478+
.eql({
479+
"year": 2016,
480+
"month": 4,
481+
"day": 5,
482+
"hour": 14,
483+
"minute": 1,
484+
"second": 30,
485+
});
486+
});
487+
488+
it("should raise on invalid JSON datetime", () => {
489+
expect(() => parseDateString("plop"))
490+
.to.Throw(Error, "Unable to parse");
491+
});
492+
});
473493
});

0 commit comments

Comments
 (0)