DEV Community

Cover image for The Trouble with Editing and Uploading Files in the Browser
Rik Schennink for PQINA

Posted on • Originally published at pqina.nl

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 in the browser, 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 something changed a couple months ago.

This article was originally published on my personal blog

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>
Enter fullscreen mode Exit fullscreen mode

Obviously this is a huge security risk.

Setting the file input value property is of 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.

Both Firefox and Chrome 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>
Enter fullscreen mode Exit fullscreen mode

Live Demo on CodePend

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.

However. Sad Trombone. This doesn't work on IE, Edge, and Safari 13.

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>
Enter fullscreen mode Exit fullscreen mode

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

  • Security related scripts running on the server that monitor traffic might flag the form post as suspicious as it contains a lot of string based data.
  • When submitting large files, that means files above 1MB, it's highly likely the browser will crash with a "ran out of memory" error. This differs per browser, but I've seen it happen on both mobile and desktop browsers.
  • You don't see a change in the file input. So it's a good idea to reset, disable, or hide it when submitting the form.

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>
Enter fullscreen mode Exit fullscreen mode

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).

  • Again, when setting _value you don't see a change in the file input. So it's a good idea to reset, disable, or hide it when submitting the form.
  • We're taking over a lot of default browser behavior, that is always a recipe for disaster.

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.

If you have anything to add, do share below.

Top comments (0)