Skip to content

Commit de9dbec

Browse files
authored
Migrate filewidget to functional component (rjsf-team#3017)
1 parent 9a4b624 commit de9dbec

File tree

2 files changed

+63
-77
lines changed

2 files changed

+63
-77
lines changed

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

Lines changed: 49 additions & 67 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
1-
import React, { ChangeEvent, Component } from "react";
1+
import React, { ChangeEvent, useCallback, useMemo, useState } from "react";
22

3-
import { dataURItoBlob, shouldRender, WidgetProps } from "@rjsf/utils";
3+
import { dataURItoBlob, WidgetProps } from "@rjsf/utils";
44

55
function addNameToDataURL(dataURL: string, name: string) {
66
if (dataURL === null) {
@@ -81,81 +81,63 @@ function extractFileInfo(dataURLs: string[]) {
8181
});
8282
}
8383

84-
type FileWidgetStateType = {
85-
values: any[];
86-
filesInfo: FileInfoType[];
87-
};
88-
8984
/**
9085
* The `FileWidget` is a widget for rendering file upload fields.
9186
* It is typically used with a string property with data-url format.
9287
*/
93-
class FileWidget<T, F> extends Component<
94-
WidgetProps<T, F>,
95-
FileWidgetStateType
96-
> {
97-
constructor(props: WidgetProps<T, F>) {
98-
super(props);
99-
const { value } = props;
100-
const values = Array.isArray(value) ? value : [value];
101-
this.state = { values, filesInfo: extractFileInfo(values) };
102-
}
103-
104-
shouldComponentUpdate(
105-
nextProps: WidgetProps<T, F>,
106-
nextState: FileWidgetStateType
107-
): boolean {
108-
return shouldRender(this, nextProps, nextState);
109-
}
88+
function FileWidget<T, F>({
89+
multiple,
90+
id,
91+
readonly,
92+
disabled,
93+
onChange,
94+
value,
95+
autofocus = false,
96+
options,
97+
}: WidgetProps<T, F>) {
98+
const extractedFilesInfo = useMemo(
99+
() =>
100+
Array.isArray(value) ? extractFileInfo(value) : extractFileInfo([value]),
101+
[value]
102+
);
103+
const [filesInfo, setFilesInfo] =
104+
useState<FileInfoType[]>(extractedFilesInfo);
110105

111-
onChange = (event: ChangeEvent<HTMLInputElement>) => {
112-
const { multiple, onChange } = this.props;
113-
if (!event.target.files) {
114-
return;
115-
}
116-
processFiles(event.target.files).then((filesInfo) => {
117-
const state = {
118-
values: filesInfo.map((fileInfo) => fileInfo.dataURL),
119-
filesInfo,
120-
};
121-
this.setState(state, () => {
106+
const handleChange = useCallback(
107+
(event: ChangeEvent<HTMLInputElement>) => {
108+
if (!event.target.files) {
109+
return;
110+
}
111+
processFiles(event.target.files).then((filesInfoEvent) => {
112+
setFilesInfo(filesInfoEvent);
113+
const newValue = filesInfoEvent.map((fileInfo) => fileInfo.dataURL);
122114
if (multiple) {
123-
onChange(state.values);
115+
onChange(newValue);
124116
} else {
125-
onChange(state.values[0]);
117+
onChange(newValue[0]);
126118
}
127119
});
128-
});
129-
};
120+
},
121+
[multiple, onChange]
122+
);
130123

131-
render() {
132-
const {
133-
multiple,
134-
id,
135-
readonly,
136-
disabled,
137-
autofocus = false,
138-
options,
139-
} = this.props;
140-
const { filesInfo } = this.state;
141-
return (
142-
<div>
143-
<p>
144-
<input
145-
id={id}
146-
type="file"
147-
disabled={readonly || disabled}
148-
onChange={this.onChange}
149-
defaultValue=""
150-
autoFocus={autofocus}
151-
multiple={multiple}
152-
accept={options.accept ? String(options.accept) : undefined}
153-
/>
154-
</p>
155-
<FilesInfo filesInfo={filesInfo} />
156-
</div>
157-
);
158-
}
124+
return (
125+
<div>
126+
<p>
127+
<input
128+
id={id}
129+
type="file"
130+
disabled={readonly || disabled}
131+
onChange={handleChange}
132+
defaultValue=""
133+
autoFocus={autofocus}
134+
multiple={multiple}
135+
accept={options.accept ? String(options.accept) : undefined}
136+
/>
137+
</p>
138+
<FilesInfo filesInfo={filesInfo} />
139+
</div>
140+
);
159141
}
160142

161143
export default FileWidget;

packages/core/test/StringField_test.js

Lines changed: 14 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -138,8 +138,10 @@ describe("StringField", () => {
138138
},
139139
});
140140

141-
Simulate.change(node.querySelector("input"), {
142-
target: { value: "yo" },
141+
act(() => {
142+
Simulate.change(node.querySelector("input"), {
143+
target: { value: "yo" },
144+
});
143145
});
144146

145147
sinon.assert.calledWithMatch(onChange.lastCall, {
@@ -356,9 +358,10 @@ describe("StringField", () => {
356358
enum: ["foo", "bar"],
357359
},
358360
});
359-
360-
Simulate.change(node.querySelector("select"), {
361-
target: { value: "foo" },
361+
act(() => {
362+
Simulate.change(node.querySelector("select"), {
363+
target: { value: "foo" },
364+
});
362365
});
363366
sinon.assert.calledWithMatch(onChange.lastCall, {
364367
formData: "foo",
@@ -1906,12 +1909,13 @@ describe("StringField", () => {
19061909
},
19071910
});
19081911

1909-
Simulate.change(node.querySelector("[type=file]"), {
1910-
target: {
1911-
files: [{ name: nonUriEncodedValue, size: 1, type: "type" }],
1912-
},
1912+
act(() => {
1913+
Simulate.change(node.querySelector("[type=file]"), {
1914+
target: {
1915+
files: [{ name: nonUriEncodedValue, size: 1, type: "type" }],
1916+
},
1917+
});
19131918
});
1914-
19151919
await new Promise(setImmediate);
19161920

19171921
sinon.assert.calledWithMatch(onChange.lastCall, {

0 commit comments

Comments
 (0)