Skip to content

Commit 87517f9

Browse files
Fix 3146 by implementing useFileWidgetProps hook (#4818)
* Fix 3146 by implementing useFileWidgetProps hook Fixes #3146 by refactoring `FileWidget` in `core` to make the `useFileWidgetProps()` hook Fixed #4803 by deleting the `FileWidget` in `daisyui`, moving the key "type-file" elements into `BaseInputTemplate` - In `@rjsf/core` refactored the file extraction and callbacks into the new `useFileWidgetProps()` hook in `@rjsf/utils` - In `@rjsf/daisyui` refactored the key `type=file` elements from `FileWidget` into `BaseInputTemplate`, deleting the `FileWidget` entirely in favor of the `core` one - In `@rjsf/mantine` updated the `FileWidget` to use the `useFileWidgetProps()` hook - In `@rjsf/utils` exported the `useFileWidgetProps()` hook and its associates types - Also added 100% unit tests for the new hook - Updated the `utility-functions.md` and `v6.x upgrade guide.md` to document the new hook - Updated the `CHANGELOG.md` accordingly # Conflicts: # CHANGELOG.md * - Responded to reviewer feedback * - Updated docs
1 parent da381db commit 87517f9

File tree

13 files changed

+456
-321
lines changed

13 files changed

+456
-321
lines changed

CHANGELOG.md

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,18 +23,28 @@ should change the heading of the (upcoming) version to include a major version b
2323
- Updated `Form` to support the new feature to do `onBlur` handling of `liveValidate` and `liveOmit`
2424
- Updated `FormProps` to add the new `initialFormData` prop
2525
- Updated `Form` so that is behaves as a "controlled" form when `formData` is passed and uncontrolled when `initialFormData` is passed, fixing [#391](https://github.com/rjsf-team/react-jsonschema-form/issues/391)
26-
- Also fixed an issue where live validation was called on the initial form render, causing errors to show immediately, partially fixing [#512](https://github.com/rjsf-team/react-jsonschema-form/issues/512)
26+
- Also fixed an issue where live validation was called on the initial form render, causing errors to show immediately, partially fixing [#512](https://github.com/rjsf-team/react-jsonschema-form/issues/512)
2727
- Updated `Form` to add a new programmatic function, `setFieldValue(fieldPath: string | FieldPathList, newValue?: T): void`, fixing [#2099](https://github.com/rjsf-team/react-jsonschema-form/issues/2099)
2828
- Added new `FallbackField` to add opt-in functionality to control form data that is of an unsupported or unknown type ([#4736](https://github.com/rjsf-team/react-jsonschema-form/issues/4736)).
29+
- Refactored much of the `FileWidget` implementation into a new `useFileWidgetProps()` hook, fixing [#3146](https://github.com/rjsf-team/react-jsonschema-form/issues/3146)
30+
31+
## @rjsf/daisyui
32+
33+
- Deleted the `FileWidget` component, moving the className and isMulti logic directly into the `BaseInputTemplate` so that the `@rjsf/core`'s `FileWidget` works properly for the theme, fixing [#4803](https://github.com/rjsf-team/react-jsonschema-form/issues/4803)
2934

3035
## @rjsf/mantine
3136

3237
- Updated `FieldHelpTemplate` to avoid issue when `help` `and `fieldPathId` are undefined
38+
- Updated `FileWidget` to use the `useFileWidgetProps()` hook
39+
40+
## @rjsf/utils
41+
42+
- Added the `useFileWidgetProps()` hook implementation, refactored from `@rjsf/core`
3343

3444
## Dev / docs / playground
3545

3646
- Updated the playground to switch `liveValidate` and `liveOmit` from checkboxes to radio buttons for the new options
37-
- Updated `internals.md`, `form-props.md` and `v6x upgrade guide.md` to document the new features, potential breaking changes and deprecations
47+
- Updated `internals.md`, `form-props.md`, `utility-functions.md` and `v6x upgrade guide.md` to document the new features, potential breaking changes and deprecations
3848

3949
# 6.0.0-beta.22
4050

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

Lines changed: 11 additions & 102 deletions
Original file line numberDiff line numberDiff line change
@@ -1,61 +1,18 @@
1-
import { ChangeEvent, useCallback, useMemo } from 'react';
1+
import { ChangeEvent } from 'react';
22
import {
3-
dataURItoBlob,
3+
FileInfoType,
44
FormContextType,
55
getTemplate,
66
Registry,
77
RJSFSchema,
88
StrictRJSFSchema,
99
TranslatableString,
1010
UIOptionsType,
11+
useFileWidgetProps,
1112
WidgetProps,
1213
} from '@rjsf/utils';
1314
import Markdown from 'markdown-to-jsx';
1415

15-
function addNameToDataURL(dataURL: string, name: string) {
16-
if (dataURL === null) {
17-
return null;
18-
}
19-
return dataURL.replace(';base64', `;name=${encodeURIComponent(name)};base64`);
20-
}
21-
22-
type FileInfoType = {
23-
dataURL?: string | null;
24-
name: string;
25-
size: number;
26-
type: string;
27-
};
28-
29-
function processFile(file: File): Promise<FileInfoType> {
30-
const { name, size, type } = file;
31-
return new Promise((resolve, reject) => {
32-
const reader = new window.FileReader();
33-
reader.onerror = reject;
34-
reader.onload = (event) => {
35-
if (typeof event.target?.result === 'string') {
36-
resolve({
37-
dataURL: addNameToDataURL(event.target.result, name),
38-
name,
39-
size,
40-
type,
41-
});
42-
} else {
43-
resolve({
44-
dataURL: null,
45-
name,
46-
size,
47-
type,
48-
});
49-
}
50-
};
51-
reader.readAsDataURL(file);
52-
});
53-
}
54-
55-
function processFiles(files: FileList) {
56-
return Promise.all(Array.from(files).map(processFile));
57-
}
58-
5916
function FileInfoPreview<T = any, S extends StrictRJSFSchema = RJSFSchema, F extends FormContextType = any>({
6017
fileInfo,
6118
registry,
@@ -125,29 +82,6 @@ function FilesInfo<T = any, S extends StrictRJSFSchema = RJSFSchema, F extends F
12582
);
12683
}
12784

128-
function extractFileInfo(dataURLs: string[]): FileInfoType[] {
129-
return dataURLs.reduce((acc, dataURL) => {
130-
if (!dataURL) {
131-
return acc;
132-
}
133-
try {
134-
const { blob, name } = dataURItoBlob(dataURL);
135-
return [
136-
...acc,
137-
{
138-
dataURL,
139-
name: name,
140-
size: blob.size,
141-
type: blob.type,
142-
},
143-
];
144-
} catch {
145-
// Invalid dataURI, so just ignore it.
146-
return acc;
147-
}
148-
}, [] as FileInfoType[]);
149-
}
150-
15185
/**
15286
* The `FileWidget` is a widget for rendering file upload fields.
15387
* It is typically used with a string property with data-url format.
@@ -156,54 +90,29 @@ function FileWidget<T = any, S extends StrictRJSFSchema = RJSFSchema, F extends
15690
props: WidgetProps<T, S, F>,
15791
) {
15892
const { disabled, readonly, required, multiple, onChange, value, options, registry } = props;
93+
const { filesInfo, handleChange, handleRemove } = useFileWidgetProps(value, onChange, multiple);
15994
const BaseInputTemplate = getTemplate<'BaseInputTemplate', T, S, F>('BaseInputTemplate', registry, options);
16095

161-
const handleChange = useCallback(
162-
(event: ChangeEvent<HTMLInputElement>) => {
163-
if (!event.target.files) {
164-
return;
165-
}
166-
// Due to variances in themes, dealing with multiple files for the array case now happens one file at a time.
167-
// This is because we don't pass `multiple` into the `BaseInputTemplate` anymore. Instead, we deal with the single
168-
// file in each event and concatenate them together ourselves
169-
processFiles(event.target.files).then((filesInfoEvent) => {
170-
const newValue = filesInfoEvent.map((fileInfo) => fileInfo.dataURL);
171-
if (multiple) {
172-
onChange(value.concat(newValue));
173-
} else {
174-
onChange(newValue[0]);
175-
}
176-
});
177-
},
178-
[multiple, value, onChange],
179-
);
96+
const handleOnChangeEvent = (event: ChangeEvent<HTMLInputElement>) => {
97+
if (event.target.files) {
98+
handleChange(event.target.files);
99+
}
100+
};
180101

181-
const filesInfo = useMemo(() => extractFileInfo(Array.isArray(value) ? value : [value]), [value]);
182-
const rmFile = useCallback(
183-
(index: number) => {
184-
if (multiple) {
185-
const newValue = value.filter((_: any, i: number) => i !== index);
186-
onChange(newValue);
187-
} else {
188-
onChange(undefined);
189-
}
190-
},
191-
[multiple, value, onChange],
192-
);
193102
return (
194103
<div>
195104
<BaseInputTemplate
196105
{...props}
197106
disabled={disabled || readonly}
198107
type='file'
199108
required={value ? false : required} // this turns off HTML required validation when a value exists
200-
onChangeOverride={handleChange}
109+
onChangeOverride={handleOnChangeEvent}
201110
value=''
202111
accept={options.accept ? String(options.accept) : undefined}
203112
/>
204113
<FilesInfo<T, S, F>
205114
filesInfo={filesInfo}
206-
onRemove={rmFile}
115+
onRemove={handleRemove}
207116
registry={registry}
208117
preview={options.filePreview}
209118
options={options}

packages/daisyui/src/templates/BaseInputTemplate/BaseInputTemplate.tsx

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ export default function BaseInputTemplate<
3131
const {
3232
id,
3333
htmlName,
34+
multiple,
3435
value,
3536
required,
3637
disabled,
@@ -48,7 +49,12 @@ export default function BaseInputTemplate<
4849
} = props;
4950

5051
const inputProps = getInputProps<T, S, F>(schema, type, options);
51-
52+
let className = 'input input-bordered';
53+
let isMulti = multiple;
54+
if (type === 'file') {
55+
isMulti = schema.type === 'array' || Boolean(options.multiple);
56+
className = 'file-input w-full';
57+
}
5258
// Extract step, min, max, accept from inputProps
5359
const { step, min, max, accept, ...rest } = inputProps;
5460
const htmlInputProps = { step, min, max, accept, ...(schema.examples ? { list: examplesId(id) } : undefined) };
@@ -82,7 +88,8 @@ export default function BaseInputTemplate<
8288
required={required}
8389
disabled={disabled || readonly}
8490
autoFocus={autofocus}
85-
className='input input-bordered'
91+
className={className}
92+
multiple={isMulti}
8693
{...rest}
8794
{...htmlInputProps}
8895
onChange={onChangeOverride || _onChange}

packages/daisyui/src/widgets/FileWidget/FileWidget.tsx

Lines changed: 0 additions & 86 deletions
This file was deleted.

packages/daisyui/src/widgets/FileWidget/index.ts

Lines changed: 0 additions & 2 deletions
This file was deleted.

packages/daisyui/src/widgets/Widgets.tsx

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@ import CheckboxWidget from './CheckboxWidget/CheckboxWidget';
66
import CheckboxesWidget from './CheckboxesWidget/CheckboxesWidget';
77
import DateTimeWidget from './DateTimeWidget/DateTimeWidget';
88
import DateWidget from './DateWidget/DateWidget';
9-
import FileWidget from './FileWidget/FileWidget';
109
import RadioWidget from './RadioWidget/RadioWidget';
1110
import RangeWidget from './RangeWidget/RangeWidget';
1211
import RatingWidget from './RatingWidget/RatingWidget';
@@ -22,7 +21,6 @@ export {
2221
CheckboxWidget,
2322
DateTimeWidget,
2423
DateWidget,
25-
FileWidget,
2624
RadioWidget,
2725
RangeWidget,
2826
RatingWidget,
@@ -44,7 +42,6 @@ export function generateWidgets<
4442
CheckboxWidget,
4543
DateTimeWidget,
4644
DateWidget,
47-
FileWidget,
4845
RadioWidget,
4946
RangeWidget,
5047
RatingWidget,

packages/daisyui/test/__snapshots__/Form.test.tsx.snap

Lines changed: 34 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -13852,16 +13852,40 @@ exports[`single fields string field format data-url 1`] = `
1385213852
className="label-text font-medium"
1385313853
/>
1385413854
</label>
13855-
<input
13856-
className="file-input w-full"
13857-
disabled={false}
13858-
id="root"
13859-
multiple={false}
13860-
onBlur={[Function]}
13861-
onChange={[Function]}
13862-
onFocus={[Function]}
13863-
type="file"
13864-
/>
13855+
<div>
13856+
<div
13857+
className="form-control"
13858+
>
13859+
<label
13860+
className="label hidden"
13861+
htmlFor="root"
13862+
style={
13863+
{
13864+
"display": "none",
13865+
}
13866+
}
13867+
>
13868+
<span
13869+
className="label-text"
13870+
/>
13871+
</label>
13872+
<input
13873+
aria-describedby="root__error root__description root__help"
13874+
autoFocus={false}
13875+
className="file-input w-full"
13876+
disabled={false}
13877+
id="root"
13878+
multiple={false}
13879+
name="root"
13880+
onBlur={[Function]}
13881+
onChange={[Function]}
13882+
onFocus={[Function]}
13883+
placeholder=""
13884+
type="file"
13885+
value=""
13886+
/>
13887+
</div>
13888+
</div>
1386513889
<div
1386613890
className="rjsf-field-error-template text-red-600"
1386713891
>

0 commit comments

Comments
 (0)