In my last post I showed how to create file downloads with JavaScript.
That's only part of the picture. If we want our users to be able to continue working with their data after they've downloaded it, we also need to support file uploads.
Why not use another storage option?
The specific use case we are talking about is a project where we don't have access to any sort of backend server or database, but we still need a way for our users to save their work. One option is to just store the data in the browser, but all of our in-browser options have significant limitations:
- If we used session storage, the data would disappear as soon as the browser was closed. In most cases, we need the data to persist between browser sessions.
- We could use HTTP cookies to store data, but cookies have a maximum size of 4096 bytes per domain. When it comes to storing data, that's probably not enough. Also, since cookies are transferred to the server with every request, we would be bogging down our interactions with the server. Wait! There is no server! In that case, our users are likely viewing our HTML file using the file:// protocol, which means cookies can't be set in the first place.
- We could step up our game with local storage or IndexedDB, which both have reasonable size limits and can store data across browser sessions. The major problem here is that they are browser-specific. Our users can't use their data in other browsers on other computers. They also can't export their data to be used in other programs.
If we allow users to just save data to the filesystem, none of the above drawbacks apply. Files don't disappear when the browser is closed or the user clears their cookies. There is practically no size limit. Most importantly, files are portable and can be used in other browsers, on other computers, and even in other applications.
Reading an uploaded file
In order to read an uploaded file, we will first need an HTML form to which we can attach our "upload." Of course, we aren't really uploading anything since we don't have a server. We just need a file <input>
element to let our JavaScript read a file from the local filesystem.
<input type="file" id="file-to-read">
Well that was about as easy as it gets. That one-liner will allow us to select a file on our computer to "upload." Now let's add a button that we can click to initiate reading the selected file.
<button onclick="readFileAsText()">Load Selected File</button>
Piece of cake. This is just a regular button with an event listener. When we click it, the readFileAsText
function is called.
Let's finish up by defining that function:
<script>
const readFileAsText = function() {
const fileToRead = document.getElementById('file-to-read').files[0]
const fileReader = new FileReader()
fileReader.addEventListener('load', function(fileLoadedEvent) {
const textFromFileLoaded = fileLoadedEvent.target.result
console.log(textFromFileLoaded)
})
fileReader.readAsText(fileToRead, 'UTF-8')
}
</script>
Step by step, in English
- The first thing we need to do is get a reference to the file that the user selected. We do that by finding the file input, in this case by its ID. Then we access its files property. Since a file input can allow multiple upload files to be selected, the files property is an array. We only care about the first item, so we will grab the item at index zero.
- We will then create a FileReader, which allows us to asynchronously read data from a File or Blob.
- It is important to realize what "asynchronous" means here. It means the FileReader will mind its own business while it is reading and won't block the rest of our code from running. We need to add an event listener for its load event so we can work with the result when it is finished reading.
- In our event listener we need to reference the FileReader because it now has a populated result property. We can always do this by using event.target. (Since this particular example has a traditional function and not an arrow function we could also use this to refer to the FileReader.)
- We are now free to do whatever we want with the contents of the file, which are a string. In this example, the file contents are logged to the console.
- Remember that we haven't actually told the FileReader to read the file yet – so far we have only told it what to do when it finishes reading a file. It is important to attach event listeners first. However, in order to get the FileReader to read a file in the first place, we need to call its readAsText method on the file from our
<input>
. This method causes the result to be a string. The FileReader class has other methods that cause result to be different types of data.
Managing the data
If you are going to allow your users to save and load data from your app, I recommend serializing that data in JSON form. If you do, the data will have a clear structure and will be easy to deserialize and load back into your application. It will also be easier for other applications and custom scripts to consume the data.