Skip to content

Commit 9a4b624

Browse files
authored
Migrate altdatewidget to functional component (rjsf-team#3013)
* Migrate core widget folder to typescript * Adressed PR feedback * Addressed additional PR comments * Migrate AltDateWidget to functional component * Add types to DateElement
1 parent 1d79bee commit 9a4b624

File tree

3 files changed

+159
-150
lines changed

3 files changed

+159
-150
lines changed

packages/core/src/components/widgets/AltDateTimeWidget.tsx

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -11,12 +11,7 @@ function AltDateTimeWidget<T = any, F = any>({
1111
...props
1212
}: WidgetProps<T, F>) {
1313
const { AltDateWidget } = props.registry.widgets;
14-
const options = {
15-
...(AltDateWidget?.defaultProps?.options ?? {}),
16-
...props.options,
17-
};
18-
19-
return <AltDateWidget time={time} {...props} options={options} />;
14+
return <AltDateWidget time={time} {...props} />;
2015
}
2116

2217
export default AltDateTimeWidget;
Lines changed: 144 additions & 138 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
1-
import React, { Component, MouseEvent } from "react";
1+
import React, { MouseEvent, useEffect, useReducer } from "react";
22

33
import {
4-
shouldRender,
54
parseDateString,
65
toDateString,
76
pad,
@@ -17,23 +16,64 @@ function rangeOptions(start: number, stop: number) {
1716
return options;
1817
}
1918

20-
function readyForChange(state: any) {
21-
return Object.keys(state).every((key) => state[key] !== -1);
19+
function readyForChange(state: DateObject) {
20+
return Object.values(state).every((value) => value !== -1);
2221
}
2322

24-
function DateElement(props: any) {
25-
const {
26-
type,
27-
range,
28-
value,
29-
select,
30-
rootId,
31-
disabled,
32-
readonly,
33-
autofocus,
34-
registry,
35-
onBlur,
36-
} = props;
23+
function dateElementProps(
24+
state: DateObject,
25+
time: boolean,
26+
yearsRange: [number, number] = [1900, new Date().getFullYear() + 2]
27+
) {
28+
const { year, month, day, hour, minute, second } = state;
29+
const data = [
30+
{
31+
type: "year",
32+
range: yearsRange,
33+
value: year,
34+
},
35+
{ type: "month", range: [1, 12], value: month },
36+
{ type: "day", range: [1, 31], value: day },
37+
] as { type: string; range: [number, number]; value: number | undefined }[];
38+
if (time) {
39+
data.push(
40+
{ type: "hour", range: [0, 23], value: hour },
41+
{ type: "minute", range: [0, 59], value: minute },
42+
{ type: "second", range: [0, 59], value: second }
43+
);
44+
}
45+
return data;
46+
}
47+
48+
type DateElementProps<T, F> = Pick<
49+
WidgetProps<T, F>,
50+
| "value"
51+
| "disabled"
52+
| "readonly"
53+
| "autofocus"
54+
| "registry"
55+
| "onBlur"
56+
| "onFocus"
57+
> & {
58+
rootId: string;
59+
select: (property: keyof DateObject, value: any) => void;
60+
type: string;
61+
range: [number, number];
62+
};
63+
64+
function DateElement<T, F>({
65+
type,
66+
range,
67+
value,
68+
select,
69+
rootId,
70+
disabled,
71+
readonly,
72+
autofocus,
73+
registry,
74+
onBlur,
75+
onFocus,
76+
}: DateElementProps<T, F>) {
3777
const id = rootId + "_" + type;
3878
const { SelectWidget } = registry.widgets;
3979
return (
@@ -47,152 +87,118 @@ function DateElement(props: any) {
4787
disabled={disabled}
4888
readonly={readonly}
4989
autofocus={autofocus}
50-
onChange={(value: any) => select(type, value)}
90+
onChange={(value: any) => select(type as keyof DateObject, value)}
5191
onBlur={onBlur}
92+
onFocus={onFocus}
93+
registry={registry}
94+
label=""
5295
/>
5396
);
5497
}
5598

56-
/** The `AltDateWidget` is an alternative widget for rendering date properties. */
57-
class AltDateWidget<T = any, F = any> extends Component<
58-
WidgetProps<T, F>,
59-
DateObject
60-
> {
61-
static defaultProps = {
62-
time: false,
63-
disabled: false,
64-
readonly: false,
65-
autofocus: false,
66-
options: {
67-
yearsRange: [1900, new Date().getFullYear() + 2],
99+
/** The `AltDateWidget` is an alternative widget for rendering date properties.
100+
* @param props - The `WidgetProps` for this component
101+
*/
102+
function AltDateWidget<T = any, F = any>({
103+
time = false,
104+
disabled = false,
105+
readonly = false,
106+
autofocus = false,
107+
options,
108+
id,
109+
registry,
110+
onBlur,
111+
onFocus,
112+
onChange,
113+
value,
114+
}: WidgetProps<T, F>) {
115+
const [state, setState] = useReducer(
116+
(state: DateObject, action: Partial<DateObject>) => {
117+
return { ...state, ...action };
68118
},
69-
};
70-
71-
/**
72-
*
73-
* @param props - The `WidgetProps` for this component
74-
*/
75-
constructor(props: WidgetProps<T, F>) {
76-
super(props);
77-
this.state = parseDateString(props.value, props.time);
78-
}
119+
parseDateString(value, time)
120+
);
79121

80-
componentDidUpdate(prevProps: WidgetProps<T, F>) {
81-
if (
82-
prevProps.value &&
83-
prevProps.value !== parseDateString(this.props.value, this.props.time)
84-
) {
85-
this.setState(parseDateString(this.props.value, this.props.time));
122+
useEffect(() => {
123+
if (value && value !== toDateString(state, time)) {
124+
setState(parseDateString(value, time));
86125
}
87-
}
126+
}, [value]);
88127

89-
shouldComponentUpdate(
90-
nextProps: WidgetProps<T, F>,
91-
nextState: DateObject
92-
): boolean {
93-
return shouldRender(this, nextProps, nextState);
94-
}
128+
useEffect(() => {
129+
if (readyForChange(state)) {
130+
// Only propagate to parent state if we have a complete date{time}
131+
onChange(toDateString(state, time));
132+
}
133+
}, [state, time]);
95134

96-
onChange = (property: keyof DateObject, value: any) => {
97-
this.setState(
98-
{ [property]: typeof value === "undefined" ? -1 : value } as Pick<
99-
DateObject,
100-
keyof DateObject
101-
>,
102-
() => {
103-
// Only propagate to parent state if we have a complete date{time}
104-
if (readyForChange(this.state)) {
105-
this.props.onChange(toDateString(this.state, this.props.time));
106-
}
107-
}
108-
);
135+
const handleChange = (property: keyof DateObject, value: string) => {
136+
setState({ [property]: value });
109137
};
110138

111-
setNow = (event: MouseEvent<HTMLAnchorElement>) => {
139+
const handleSetNow = (event: MouseEvent<HTMLAnchorElement>) => {
112140
event.preventDefault();
113-
const { time, disabled, readonly, onChange } = this.props;
114141
if (disabled || readonly) {
115142
return;
116143
}
117144
const nowDateObj = parseDateString(new Date().toJSON(), time);
118-
this.setState(nowDateObj, () => onChange(toDateString(this.state, time)));
145+
setState(nowDateObj);
119146
};
120147

121-
clear = (event: MouseEvent<HTMLAnchorElement>) => {
148+
const handleClear = (event: MouseEvent<HTMLAnchorElement>) => {
122149
event.preventDefault();
123-
const { time, disabled, readonly, onChange } = this.props;
124150
if (disabled || readonly) {
125151
return;
126152
}
127-
this.setState(parseDateString("", time), () => onChange(undefined));
153+
setState(parseDateString("", time));
154+
onChange(undefined);
128155
};
129156

130-
get dateElementProps() {
131-
const { time, options } = this.props;
132-
const { year, month, day, hour, minute, second } = this.state;
133-
const data = [
134-
{
135-
type: "year",
136-
range: options.yearsRange,
137-
value: year,
138-
},
139-
{ type: "month", range: [1, 12], value: month },
140-
{ type: "day", range: [1, 31], value: day },
141-
] as { type: string; range: [number, number]; value: number | undefined }[];
142-
if (time) {
143-
data.push(
144-
{ type: "hour", range: [0, 23], value: hour },
145-
{ type: "minute", range: [0, 59], value: minute },
146-
{ type: "second", range: [0, 59], value: second }
147-
);
148-
}
149-
return data;
150-
}
151-
152-
render() {
153-
const { id, disabled, readonly, autofocus, registry, onBlur, options } =
154-
this.props;
155-
return (
156-
<ul className="list-inline">
157-
{this.dateElementProps.map((elemProps, i) => (
158-
<li key={i}>
159-
<DateElement
160-
rootId={id}
161-
select={this.onChange}
162-
{...elemProps}
163-
disabled={disabled}
164-
readonly={readonly}
165-
registry={registry}
166-
onBlur={onBlur}
167-
autofocus={autofocus && i === 0}
168-
/>
169-
</li>
170-
))}
171-
{(options.hideNowButton !== "undefined"
172-
? !options.hideNowButton
173-
: true) && (
174-
<li>
175-
<a href="#" className="btn btn-info btn-now" onClick={this.setNow}>
176-
Now
177-
</a>
178-
</li>
179-
)}
180-
{(options.hideClearButton !== "undefined"
181-
? !options.hideClearButton
182-
: true) && (
183-
<li>
184-
<a
185-
href="#"
186-
className="btn btn-warning btn-clear"
187-
onClick={this.clear}
188-
>
189-
Clear
190-
</a>
191-
</li>
192-
)}
193-
</ul>
194-
);
195-
}
157+
return (
158+
<ul className="list-inline">
159+
{dateElementProps(
160+
state,
161+
time,
162+
options.yearsRange as [number, number] | undefined
163+
).map((elemProps, i) => (
164+
<li key={i}>
165+
<DateElement
166+
rootId={id}
167+
select={handleChange}
168+
{...elemProps}
169+
disabled={disabled}
170+
readonly={readonly}
171+
registry={registry}
172+
onBlur={onBlur}
173+
onFocus={onFocus}
174+
autofocus={autofocus && i === 0}
175+
/>
176+
</li>
177+
))}
178+
{(options.hideNowButton !== "undefined"
179+
? !options.hideNowButton
180+
: true) && (
181+
<li>
182+
<a href="#" className="btn btn-info btn-now" onClick={handleSetNow}>
183+
Now
184+
</a>
185+
</li>
186+
)}
187+
{(options.hideClearButton !== "undefined"
188+
? !options.hideClearButton
189+
: true) && (
190+
<li>
191+
<a
192+
href="#"
193+
className="btn btn-warning btn-clear"
194+
onClick={handleClear}
195+
>
196+
Clear
197+
</a>
198+
</li>
199+
)}
200+
</ul>
201+
);
196202
}
197203

198204
export default AltDateWidget;

packages/core/test/StringField_test.js

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1069,7 +1069,9 @@ describe("StringField", () => {
10691069
uiSchema,
10701070
});
10711071

1072-
Simulate.click(node.querySelector("a.btn-now"));
1072+
act(() => {
1073+
Simulate.click(node.querySelector("a.btn-now"));
1074+
});
10731075
const formValue = onChange.lastCall.args[0].formData;
10741076
// Test that the two DATETIMEs are within 5 seconds of each other.
10751077
const now = new Date().getTime();
@@ -1086,8 +1088,10 @@ describe("StringField", () => {
10861088
uiSchema,
10871089
});
10881090

1089-
Simulate.click(node.querySelector("a.btn-now"));
1090-
Simulate.click(node.querySelector("a.btn-clear"));
1091+
act(() => {
1092+
Simulate.click(node.querySelector("a.btn-now"));
1093+
Simulate.click(node.querySelector("a.btn-clear"));
1094+
});
10911095

10921096
sinon.assert.calledWithMatch(onChange.lastCall, {
10931097
formData: undefined,
@@ -1378,7 +1382,9 @@ describe("StringField", () => {
13781382
uiSchema,
13791383
});
13801384

1381-
Simulate.click(node.querySelector("a.btn-now"));
1385+
act(() => {
1386+
Simulate.click(node.querySelector("a.btn-now"));
1387+
});
13821388

13831389
const expected = toDateString(
13841390
parseDateString(new Date().toJSON()),
@@ -1399,8 +1405,10 @@ describe("StringField", () => {
13991405
uiSchema,
14001406
});
14011407

1402-
Simulate.click(node.querySelector("a.btn-now"));
1403-
Simulate.click(node.querySelector("a.btn-clear"));
1408+
act(() => {
1409+
Simulate.click(node.querySelector("a.btn-now"));
1410+
Simulate.click(node.querySelector("a.btn-clear"));
1411+
});
14041412

14051413
sinon.assert.calledWithMatch(onChange.lastCall, {
14061414
formData: undefined,

0 commit comments

Comments
 (0)