The Trouble With Editing And Uploading Files In The Browser

With the introduction of the File API we gained the ability to edit files in the browser. We could finally resize images, unzip files, and generate new files based on interactions in the browser. One caveat though, you couldn’t upload these files.

Well you could, but you had to resort to either XMLHttpRequest or fetch, see, it’s not possible to set the value of a file input element. This means you can’t submit a custom file along with a classic form submit, you have to asynchronously upload the file. This really put the brakes on any progressive enhancement solutions to file editing. If you decide to modify a file on the client, then you also have to make sure you make changes on the server so you can receive the modified file.

As a product developer building image editing products this really grinds my gears. I’d love to offer my products as client-side only solutions. But that’s impossible because asynchronous file uploads require server-side modifications. WordPress, Netlify, Shopify, Bubble.io, they all offer default file input elements, but there’s no straightforward way to support them without writing a client-side and server-side plugin. In the case of WordPress this means offering a plugin for each and every form builder out there. Not very realistic.

But a couple months ago something changed.

Setting a custom file to a file input

It’s really quite logical that we can’t set the value of the file input element. Doing so would allow us to point it at files on the visitors file system.

<input type="file" />

<script>
    document.querySelector('input').value = 'some/file/i/want/to/have';
</script>

Obviously this would be a huge security risk.

Setting the file input value property is off the table.

What about the file input files property? If we could somehow update the files property or update the files in it, that would solve the issue.

The files property holds a reference to a FileList. Great! Let’s create a new FileList() and overwrite the one on the file input. Unfortunately there’s no FileList constructor. There is also no “add a file” method exposed on the FileList instance. On top of that the File object doesn’t have a method to update the file data in place, so we can’t update the individual file objects in the files list.

Well that’s it then.

And it was untill a couple months a go Hidde de Vries pointed me to this issue on the WHATWG, Turns out there’s a different API we can use to achieve our goal.

Firefox, Chrome, and Safari have recently added support for the DataTransfer constructor. The DataTransfer Class is most commonly used when dragging and dropping files from the user device to the webpage.

It has a files property of type FileList 🎉

It also has an items.add method for adding items to this list 🎉

Oh la la!

<input type="file" />

<script>
    // Create a DataTransfer instance and add a newly created file
    const dataTransfer = new DataTransfer();
    dataTransfer.items.add(new File(['hello world'], 'This_Works.txt'));

    // Assign the DataTransfer files list to the file input
    document.querySelector('input').files = dataTransfer.files;
</script>

If you’re browser can run the code above the file input below will show “This_Works.txt” as the input value. Starting from version 14.1 Safari also supports setting the files property in this manner but it won’t update the label next to the input.

It just works. Fantastic! We now have a method to send files created on the client to the server without having to make any changes to the server-side API.

This doesn’t work on IE, non Chromium Edge, and Safari versions before 14.1.

Alternatives for other browsers

If we want to submit our file data along with the form post, what can we offer users on these other browsers? There are currently two alternate solutions I can think of. One requires changes on the server, the other might be buggy depending on your use case.

Let’s take alook.

Encode the file data

We can encode the file data as a base64 string or dataURL, store the resulting string in a hidden input element and then send it on its way when the form is submitted. This will require changes to the server, the server will have to be aware an encoded file might be submitted as well. The server will also have to decode the dataURL and turn it back into a File object.

We can use the FileReader API to turn a File into a dataURL.

<input type="file" />
<input type="hidden" />

<script>
    document.querySelector('input[type="file"]').onchange = (e) => {
        const reader = new FileReader();
        reader.onloadend = () => {
            document.querySelector('input[type="hidden"]').value =
                reader.result;
        };
        reader.readAsDataURL(e.target.files[0]);
    };
</script>

A couple of issues my clients reported when using this method.

Encoding files is a fine solution if you’re dealing with small images, anything bigger than 1MB and I’d steer clear.

Capture the form submit

We can add custom files when submitting a form asynchronously. So another solution is capturing the entire form submit and asynchronously submitting the form to the same end point (action attribute) using XMLHttpRequest or fetch.

This is what I’ve tried to do with Poost (this is very much a prototype, also I’m bad at naming things on the spot). Poost captures the form submit, and then posts the form asynchronously instead. This allows us to build a custom FormData object, adding our custom file (stored in the _value property) instead of the files in the files property.

<input type="file" />

<script>
    // Create a new File object
    const myFile = new File(['Hello World!'], 'myFile.txt', {
        type: 'text/plain',
        lastModified: new Date(),
    });

    // Assign File to _value property
    const target = document.querySelector('input[type="file"]');
    target._value = [myFile];
</script>

This actually works quite well. We post the same data to the same end point. Things start to get tricky when you realise that the returned page also needs to be rendered to the screen (normally the browser navigates to it). Where are we going to render it, what to do with the navigator history, how to deal with script tags on the page, what about IE (no surprise there).

Still, for very basic forms this works nicely. You don’t have to modify the server and it could even be loaded conditionally as a fallback for when a browser doesn’t support new DataTransfer().

The state of things

So our file upload situation, while it has improved, is still all but fantastic.

We’re still stuck with these bandaid solutions because of IE, Edge, and Safari. If you have the luxury it’s probably easier to make changes on the server to facilitate async transfers. If you’re in a situation where that’s impossible I hope the solutions offered above might just fit your situation perfectly and help you out.

I share web dev tips on Twitter, if you found this interesting and want to learn more, follow me there

Or join my newsletter

More articles More articles