Using KendoReact Upload component to convert files to base64 and asynchronous state updates

1.5k views Asked by At

I'm trying to make a reusable multiple file upload component using the KendoReact Upload component. What I have is functional but I'm fairly new to React so was hoping there is a more concise way to achieve this.

By default this component takes a URL of the endpoint for the upload / remove request, but the saveUrl / removeUrl props can also take a function to allow manipulation of uploaded files. I am using FileReader to convert the Files to base64 and taking a function that can be used for external state updates once files are uploaded.

I have the following implementation but it feels heavy as I need

  • a collection of UploadFileInfo in state for files used by the Kendo control
  • a collection of IFileUploadViewModel in state for the files including base64 content to be passed out
  • a useEffect call to trigger external state updates to ensure updated state is passed out
  • function state updates in the saveRequestPromise and removeRequestPromise to keep the control and external file collections in sync
import React, { useEffect, useState } from "react";
import { Upload, UploadFileInfo, UploadOnAddEvent, UploadOnProgressEvent, UploadOnRemoveEvent, UploadOnStatusChangeEvent } from "@progress/kendo-react-upload";

export interface IFileUploadViewModel {
    uid: string;
    fileName: string;
    fileSize: number;
    content?: string | undefined;
}

export function KendoMultipleSmallFileUpload(props: {
    allowedExtensions?: string[] | undefined;
    minFileSizeMb?: number | undefined;
    maxFileSizeMb?: number | undefined;
    onFileChange: (files: Array<IFileUploadViewModel>) => void;
}) {
    // The Upload component accepts files from users only, an initial 'system' state e.g. pre-selected file determined by a viewmodel is not a valid use case.
    const [files, setFiles] = useState(new Array<UploadFileInfo>());
    const [fileUploads, setFileUploads] = useState(new Array<IFileUploadViewModel>());

    useEffect(() => {
        console.log(fileUploads);
        props.onFileChange(fileUploads);
    }, [fileUploads]);

    const convertMbToBytes = (mb: number) => mb * 1024 * 1024;
    // The Upload component does not allow empty files, convert file size restrictions to bytes and set a minimum value if not provided.
    const minFileSizeBytes = props.minFileSizeMb ? convertMbToBytes(props.minFileSizeMb) : 1;
    const maxFileSizeBytes = props.maxFileSizeMb ? convertMbToBytes(props.maxFileSizeMb) : undefined;

    // Fires when user clicks on the Remove button while the file upload is in progress. Can be used when the saveUrl option is set to a function that cancels custom requests.
    // For small files, conversion to a byte array is so quick it is impossible to Cancel through the UI and in any case is automatically followed by Remove so no implementation is required.
    // For large files we may need to consider aborting the upload process through the FileReader API.
    function onCancel(event: any) {}

    function onAdd(event: UploadOnAddEvent) {
        setFiles(event.newState);
    }

    function onRemove(event: UploadOnRemoveEvent) {
        setFiles(event.newState);
    }

    function onProgress(event: UploadOnProgressEvent) {
        setFiles(event.newState);
    }

    function onStatusChange(event: UploadOnStatusChangeEvent) {
        setFiles(event.newState);
    }

    function onSaveRequest(
        files: UploadFileInfo[],
        options: { formData: FormData; requestOptions: any },
        onProgress: (uid: string, event: ProgressEvent<EventTarget>) => void
    ): Promise<{ uid: string }> {
        const currentFile = files[0] as UploadFileInfo;
        const uid = currentFile.uid;
        console.log("onSaveRequest for " + currentFile.name);

        const saveRequestPromise = new Promise<{ uid: string }>(async (resolve, reject) => {
            // as currently configured it is impossible to request a save from the UI if there are validation errors, but that can be changed so keeping this check in place
            if (currentFile.validationErrors && currentFile.validationErrors.length > 0) {
                reject({ uid: uid });
            } else {
                const reader = new FileReader();
                // onload is executed when the load even is fired i.e. when content read with readAsArrayBuffer, readAsBinaryString, readAsDataURL or readAsText is available
                reader.onload = () => {
                    if (reader.result && typeof reader.result === "string") {
                        // stripping the data-url declaration as per https://developer.mozilla.org/en-US/docs/Web/API/FileReader/readAsDataURL
                        const base64Result = reader.result.split(",")[1];
                        // update viewModel and resolve
                        const fileUpload: IFileUploadViewModel = { uid: uid, fileName: currentFile.name, fileSize: currentFile.size!, content: base64Result };
                        setFileUploads((state) => [...state, fileUpload]);

                        resolve({ uid: uid });
                    } else {
                        reject({ uid: uid });
                    }
                };
                // onprogress is fired periodically as the FileReader reads data and the ProgressEvent can be passed directly to the Upload control, handy!
                reader.onprogress = (data) => {
                    onProgress(uid, data);
                };
                // if the read is not completed due to error or user intervention, reject
                reader.onabort = () => {
                    reject({ uid: uid });
                };
                reader.onerror = () => {
                    reject({ uid: uid });
                };

                reader.readAsDataURL(currentFile.getRawFile!());
            }
        });

        return saveRequestPromise;
    }

    function onRemoveRequest(files: UploadFileInfo[], options: { formData: FormData; requestOptions: any }): Promise<{ uid: string }> {
        const currentFile = files[0] as UploadFileInfo;
        const uid = currentFile.uid;

        const removeRequestPromise = new Promise<{ uid: string }>((resolve) => {
            const updatedFileUploads = fileUploads.filter((f) => f.uid !== uid);
            setFileUploads(updatedFileUploads);
            props.onFileChange(updatedFileUploads);
            resolve({ uid: uid });
        });

        return removeRequestPromise;
    }

    return (
        <>
            <Upload
                className="mb-2"
                autoUpload={true}
                batch={false}
                files={files}
                multiple={true}
                onAdd={onAdd}
                onCancel={onCancel}
                onProgress={onProgress}
                onRemove={onRemove}
                onStatusChange={onStatusChange}
                withCredentials={false}
                removeUrl={onRemoveRequest}
                saveUrl={onSaveRequest}
                restrictions={{ allowedExtensions: props.allowedExtensions, minFileSize: minFileSizeBytes, maxFileSize: maxFileSizeBytes }}
            />
            <div className="d-flex flex-row flex-wrap small">
                {props.minFileSizeMb && (
                    <div className="d-flex flex-row mr-2 mb-2">
                        <span className="label font-weight-bold mr-1">Minimum File Size: </span>
                        <span>{props.minFileSizeMb} MB.</span>
                    </div>
                )}
                {props.maxFileSizeMb && (
                    <div className="d-flex flex-row mr-2 mb-2">
                        <span className="label font-weight-bold mr-1">Maximum File Size: </span>
                        <span>{props.maxFileSizeMb} MB.</span>
                    </div>
                )}
                {props.allowedExtensions && (
                    <div className="d-flex flex-row mr-2 mb-2">
                        <span className="label font-weight-bold mr-1">Supported File Types: </span>
                        <span>{props.allowedExtensions.join(", ")}.</span>
                    </div>
                )}
            </div>
        </>
    );
}

Any suggestions would be appreciated.

0

There are 0 answers