Handling Form Submission and Sending Files to SvelteKit Endpoints

In the last blog post I touched on setting up a Google Drive folder to serve as the location of our uploads as well as creating the necessary endpoints for uploading and renaming the files.

In this post I'll set up functions to send any necessary files to these endpoints to upload. Then in the last post I'll add the markup to handle file acquisition.

SvelteKit Component Script

The functionality for this script is fairly complex, so most of this post will handle this logic.

Variable Instantiation

The only thing we'll really need at this point in time is an object to hold accepted and rejected files. I'll be using Svelte File Dropzone for file acquisition and the setup used in the docs is as follows

<script>
  let files = {
    accepted: [],
    rejected: []
  }
</script>

That's it. From the perspective of basic functionality this is all we'll need. When I add in Dropzone-specific stuff I'll add more variable instantiation, but I'll leave that for the next post.

Handling Form Submission

While I was working on this for my client, this was the feature on which I spent the most time. I'm not terribly familiar with handling readable streams, transmitting them to an endpoint as the correct type, etc. On top of that, I was trying to make the submit button disabled and add a loading spinner and then once all of the files were uploaded I would then add a notification and stop the loading spinner. This would have been less of an issue with a single file, but I wanted to allow for multiple file submissions at the same time. I found a great Stack Overflow answer about how to handle this, and it effectively wraps the reader.onload functionality in a Promise and you can just await a Promise.all for all of the necessary files.

File Reader

This will be broken up into two smaller sections:

  • The file reader
  • The rest of handleSubmit

Following along a bit of the aforementioned Stack Overflow answer, I'm going to share the code in its entirety and discuss after

const filePromises = files.accepted.map((file) => {
    return new Promise((resolve, reject) => {
        const reader = new FileReader();
        reader.onloadend = async () => {
            try {
                const response = await fetch('/upload-files', {
                    method: 'POST',
                    body: reader.result,
                    headers: { 'Content-Type': 'application/pdf' }
                });
                const { id } = await response.json();
                const body = JSON.stringify({ name: file.name, fileId: id });
                await fetch('/rename-file', {
                    method: 'POST',
                    body
                });
                resolve();
            } catch (err) {
                reject(err);
            }
        };
        reader.onerror = (error) => reject(error);
        reader.readAsArrayBuffer(file);
    });
});

The general flow of this method is as follows (for each file in the files.accepted array)

  • Create a new FileReader
  • When the file is fully loaded, try sending the reader.result element to the previously-created endpoint with the correct Content-Type header
  • Pull the id value from the endpoint that we'll use to rename the file
  • Create and stringify the submission body to rename the file
  • Send the data to rename the file using that previously-created endpoint

Handle Submission

I'm going to add more functionality than is necessary to the handleSubmit method and will explain why after

const handleSubmit = async () => {
    loading = true;
    const filePromises = files.accepted.map((file) => {
        return new Promise((resolve, reject) => {
            const reader = new FileReader();
            reader.onloadend = async () => {
                try {
                    const response = await fetch('/upload-files', {
                        method: 'POST',
                        body: reader.result,
                        headers: { 'Content-Type': 'application/pdf' }
                    });
                    const { id } = await response.json();
                    const body = JSON.stringify({ name: file.name, fileId: id });
                    await fetch('/api/rename-file', {
                        method: 'POST',
                        body
                    });
                    resolve();
                } catch (err) {
                    reject(err);
                }
            };
            reader.onerror = (error) => reject(error);
            reader.readAsArrayBuffer(file);
        });
    });
    try {
        await Promise.all(filePromises);
        files.accepted = [];

        /* Extra functionality for fun */
        notification = true;
        setTimeout(() => {
            notification = false;
        }, 10000);
        fetch('/api/send-uploaded-message', {
            method: 'POST',
            body: JSON.stringify({ messageType: 'success' })
        });
        /***************************/
    } catch (err) {
        console.log('err', err);
    } finally {
        loading = false;
    }
};

I've added in a bit of extra functionality for demonstration purposes.

  • If we want to have a loading spinner that is displayed based on a variable called something like loading then we can instantiate it when the function is triggered and then stop it in the finally block of the try/catch/finally group
  • If we want to include a notification on the page then we can do that after the Promise.all evaluation
    • I set up a setTimeout to close the notification automatically after 10 seconds
  • We can also have a separate endpoint to notify whomever it's worth notifying upon successful completion of the file upload, which I've added as well

Summary

These last two posts have been a bit shorter, but I wanted to keep them to the point and not clutter with a lot of information. In the next (last) blog post I'll add functionality to handle for file management within the component itself.