Skip to content

Commit e6de1e6

Browse files
committed
Fixes rjsf-team#142: Added DateWidget. r=@leplatrem
1 parent 6536bca commit e6de1e6

File tree

7 files changed

+239
-85
lines changed

7 files changed

+239
-85
lines changed

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -218,6 +218,7 @@ Here's a list of supported alternative widgets for different JSONSchema data typ
218218
The built-in string field also supports the JSONSchema `format` property, and will render an appropriate widget by default for the following formats:
219219

220220
- `date-time`: Six `select` elements are used to select the year, the month, the day, the hour, the minute and the second;
221+
* If you don't want to deal with time, a `"ui:widget": "date"` uiSchema widget is alternatively available, exposing three selects for year, month and day only;
221222
- `email`: An `input[type=email]` element is used;
222223
- `uri`: An `input[type=url]` element is used;
223224
- More formats could be supported in a near future, feel free to help us going faster!

playground/samples/widgets.js

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,10 @@ module.exports = {
5050
datetime: {
5151
type: "string",
5252
format: "date-time"
53+
},
54+
date: {
55+
type: "string",
56+
format: "date-time"
5357
}
5458
}
5559
},
@@ -73,6 +77,11 @@ module.exports = {
7377
"ui:widget": "textarea"
7478
}
7579
},
80+
stringFormats: {
81+
date: {
82+
"ui:widget": "date"
83+
}
84+
},
7685
secret: {
7786
"ui:widget": "hidden"
7887
}
@@ -90,7 +99,6 @@ module.exports = {
9099
stringFormats: {
91100
email: "chuck@norris.net",
92101
uri: "http://chucknorris.com/",
93-
datetime: new Date().toJSON(),
94102
},
95103
secret: "I'm a hidden string."
96104
}

src/components/widgets/DateTimeWidget.js

Lines changed: 4 additions & 76 deletions
Original file line numberDiff line numberDiff line change
@@ -1,82 +1,10 @@
1-
import React, { Component, PropTypes } from "react";
1+
import React, { PropTypes } from "react";
22

3-
import { shouldRender, parseDateString, toDateString, pad } from "../../utils";
4-
import SelectWidget from "../widgets/SelectWidget";
3+
import DateWidget from "./DateWidget";
54

65

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;
17-
return (
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>
30-
);
31-
}
32-
33-
class DateTimeWidget extends Component {
34-
constructor(props) {
35-
super(props);
36-
this.state = parseDateString(props.value);
37-
}
38-
39-
componentWillReceiveProps(nextProps) {
40-
this.setState(parseDateString(nextProps.value));
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-
}
6+
function DateTimeWidget(props) {
7+
return <DateWidget time {...props} />;
808
}
819

8210
if (process.env.NODE_ENV !== "production") {
Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
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;
17+
return (
18+
<span>
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={(value) => select(type, value)} />
26+
<p className="text-center help-block">
27+
<label htmlFor={id}><small>{type}</small></label>
28+
</p>
29+
</span>
30+
);
31+
}
32+
33+
class DateWidget extends Component {
34+
defaultProps = {
35+
time: false
36+
};
37+
38+
constructor(props) {
39+
super(props);
40+
this.state = parseDateString(props.value, !!props.time);
41+
}
42+
43+
componentWillReceiveProps(nextProps) {
44+
this.setState(parseDateString(nextProps.value, !!nextProps.time));
45+
}
46+
47+
shouldComponentUpdate(nextProps, nextState) {
48+
return shouldRender(this, nextProps, nextState);
49+
}
50+
51+
onChange = (property, value) => {
52+
this.setState({[property]: value}, () => {
53+
this.props.onChange(toDateString(this.state));
54+
});
55+
};
56+
57+
get dateElements() {
58+
const {id, time} = this.props;
59+
const {year, month, day, hour, minute, second} = this.state;
60+
const data = [
61+
{type: "year", range: [1900, 2020], value: year},
62+
{type: "month", range: [1, 12], value: month},
63+
{type: "day", range: [1, 31], value: day},
64+
];
65+
if (time) {
66+
data.push(
67+
{type: "hour", range: [0, 23], value: hour},
68+
{type: "minute", range: [0, 59], value: minute},
69+
{type: "second", range: [0, 59], value: second}
70+
);
71+
}
72+
return data.map(props => {
73+
return <DateElement rootId={id} select={this.onChange} {...props} />;
74+
});
75+
}
76+
77+
render() {
78+
return (
79+
<ul className="list-inline">{
80+
this.dateElements.map((elem, i) => <li key={i}>{elem}</li>)
81+
}</ul>
82+
);
83+
}
84+
}
85+
86+
if (process.env.NODE_ENV !== "production") {
87+
DateWidget.propTypes = {
88+
schema: PropTypes.object.isRequired,
89+
id: PropTypes.string.isRequired,
90+
placeholder: PropTypes.string,
91+
value: React.PropTypes.string,
92+
required: PropTypes.bool,
93+
onChange: PropTypes.func,
94+
time: PropTypes.bool,
95+
};
96+
}
97+
98+
export default DateWidget;

src/utils.js

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import UpDownWidget from "./components/widgets/UpDownWidget";
77
import RangeWidget from "./components/widgets/RangeWidget";
88
import SelectWidget from "./components/widgets/SelectWidget";
99
import TextWidget from "./components/widgets/TextWidget";
10+
import DateWidget from "./components/widgets/DateWidget";
1011
import DateTimeWidget from "./components/widgets/DateTimeWidget";
1112
import EmailWidget from "./components/widgets/EmailWidget";
1213
import URLWidget from "./components/widgets/URLWidget";
@@ -27,6 +28,7 @@ const altWidgetMap = {
2728
select: SelectWidget,
2829
textarea: TextareaWidget,
2930
hidden: HiddenWidget,
31+
date: DateWidget,
3032
},
3133
number: {
3234
updown: UpDownWidget,
@@ -244,7 +246,7 @@ function errorPropertyToPath(property) {
244246
return property.split(".").reduce((path, node) => {
245247
const match = node.match(RE_ERROR_ARRAY_PATH);
246248
if (match) {
247-
const nodeName = node.slice(0, node.indexOf('['));
249+
const nodeName = node.slice(0, node.indexOf("["));
248250
const indices = match.map(str => parseInt(str.slice(1, -1), 10));
249251
path = path.concat(nodeName, indices);
250252
} else {
@@ -312,7 +314,7 @@ export function toIdSchema(schema, id, definitions) {
312314
return idSchema;
313315
}
314316

315-
export function parseDateString(dateString) {
317+
export function parseDateString(dateString, includeTime = true) {
316318
if (!dateString) {
317319
dateString = new Date().toJSON();
318320
}
@@ -324,9 +326,9 @@ export function parseDateString(dateString) {
324326
year: date.getUTCFullYear(),
325327
month: date.getUTCMonth() + 1, // oh you, javascript.
326328
day: date.getUTCDate(),
327-
hour: date.getUTCHours(),
328-
minute: date.getUTCMinutes(),
329-
second: date.getUTCSeconds(),
329+
hour: includeTime ? date.getUTCHours() : 0,
330+
minute: includeTime ? date.getUTCMinutes() : 0,
331+
second: includeTime ? date.getUTCSeconds() : 0,
330332
};
331333
}
332334

test/StringField_test.js

Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -293,6 +293,111 @@ describe("StringField", () => {
293293
});
294294
});
295295

296+
describe("DateWidget", () => {
297+
const uiSchema = {"ui:widget": "date"};
298+
299+
it("should render a date field", () => {
300+
const {node} = createFormComponent({schema: {
301+
type: "string",
302+
format: "date-time",
303+
}, uiSchema});
304+
305+
expect(node.querySelectorAll(".field select"))
306+
.to.have.length.of(3);
307+
});
308+
309+
it("should render a string field with a main label", () => {
310+
const {node} = createFormComponent({schema: {
311+
type: "string",
312+
format: "date-time",
313+
title: "foo",
314+
}, uiSchema});
315+
316+
expect(node.querySelector(".field label").textContent)
317+
.eql("foo");
318+
});
319+
320+
it("should assign a default value", () => {
321+
const datetime = new Date().toJSON();
322+
const {comp} = createFormComponent({schema: {
323+
type: "string",
324+
format: "date-time",
325+
default: datetime,
326+
}, uiSchema});
327+
328+
expect(comp.state.formData).eql(datetime);
329+
});
330+
331+
it("should reflect the change into the dom", () => {
332+
const {comp, node} = createFormComponent({schema: {
333+
type: "string",
334+
format: "date-time",
335+
}, uiSchema});
336+
337+
Simulate.change(node.querySelector("#root_year"), {target: {value: "2012"}});
338+
Simulate.change(node.querySelector("#root_month"), {target: {value: "10"}});
339+
Simulate.change(node.querySelector("#root_day"), {target: {value: "2"}});
340+
341+
expect(comp.state.formData).eql("2012-10-02T00:00:00.000Z");
342+
});
343+
344+
it("should fill field with data", () => {
345+
const datetime = new Date().toJSON();
346+
const {comp} = createFormComponent({schema: {
347+
type: "string",
348+
format: "date-time",
349+
}, uiSchema, formData: datetime});
350+
351+
expect(comp.state.formData).eql(datetime);
352+
});
353+
354+
it("should render the widgets with the expected ids", () => {
355+
const {node} = createFormComponent({schema: {
356+
type: "string",
357+
format: "date-time",
358+
}, uiSchema});
359+
360+
const ids = [].map.call(node.querySelectorAll("select"), node => node.id);
361+
362+
expect(ids).eql([
363+
"root_year",
364+
"root_month",
365+
"root_day",
366+
]);
367+
});
368+
369+
it("should render the widgets with the expected options' values", () => {
370+
const {node} = createFormComponent({schema: {
371+
type: "string",
372+
format: "date-time",
373+
}, uiSchema});
374+
375+
const lengths = [].map.call(node.querySelectorAll("select"), node => node.length);
376+
377+
expect(lengths).eql([
378+
121, // from 1900 to 2020
379+
12,
380+
31,
381+
]);
382+
const monthOptions = node.querySelectorAll("select#root_month option");
383+
const monthOptionsValues = [].map.call(monthOptions, option => option.value);
384+
expect(monthOptionsValues).eql([
385+
"1", "2", "3", "4", "5", "6", "7", "8", "9", "10", "11", "12"]);
386+
});
387+
388+
it("should render the widgets with the expected options' labels", () => {
389+
const {node} = createFormComponent({schema: {
390+
type: "string",
391+
format: "date-time",
392+
}, uiSchema});
393+
394+
const monthOptions = node.querySelectorAll("select#root_month option");
395+
const monthOptionsLabels = [].map.call(monthOptions, option => option.text);
396+
expect(monthOptionsLabels).eql([
397+
"01", "02", "03", "04", "05", "06", "07", "08", "09", "10", "11", "12"]);
398+
});
399+
});
400+
296401
describe("EmailWidget", () => {
297402
it("should render an email field", () => {
298403
const {node} = createFormComponent({schema: {

0 commit comments

Comments
 (0)