A small Vue.js app

Loading from a file

My app can save its data to a local file. In the previous post, in preparation for loading the data back into the app, I added an <input> element with type="file", which allows the user to select a file from the local filesystem.

Goal

Next, I’ll write a handleFile() method to load the selected file’s contents into the app’s tiddlers data object, using the FileReader Web API.

Planning

Up until now, everything I’ve done has been synchronous. The app finishes one task, then it starts and finishes the next, then the next… I’ve been able to remain blissfully ignorant of asynchronous operations. But now, I want to use the FileReader Web API to read local files, and FileReader works asynchronously.

This means that I’ll tell my app to read the file, and it will start work on that, but it won’t wait until it’s done before starting the next thing I told it to do. Such behaviour has the general effect that an app or page won’t freeze up waiting, e.g., for a response from a server.

It also means that an instruction that uses the result of a FileReader method can be invoked before the read operation has finished – in which case the result will be null.

Obviously there is a standard way to deal with this, because the whole point of using FileReader is to get the result:

The FileReader object has a built-in handler for the load event, which is fired when the read operation is done. The function that needs the file contents can be written as a callback to be invoked by this handler. The app can get on with other business, if there is any, while the read operation is working, and the callback is put back on the call stack at the appropriate time.

Let me sum up the sequence:

  1. A FileReader method (here, FileReader.readAsText()) is invoked
  2. When the read operation is finished, the load event is fired. At this point, the file’s contents are in its FileReader.result property.
  3. The property FileReader.onload is a handler for the load event. It invokes whatever callback function is defined for it – in my case, this will be the function that puts the file data into the app’s tiddlers array.

Breakdown

Therefore, the tasks involved in writing my handleFile() method are:

  1. Use the FileReader() constructor to create a new FileReader object
  2. Write a callback function for the FileReader.onload handler to invoke, containing the code I want executed when the file contents are available to use
  3. Invoke the FileReader.readAsText() method, which, when it finishes, will fire the load event and trigger the callback

1. Construct a new FileReader object

I’ll call it reader.

var reader = new FileReader()

2. Write the callback for FileReader.onload

It’s set up like this:

reader.onload = function() {
}

All the callback function has to do is parse the result property of reader and store it in the app’s data object, in the tiddlers array. I was tempted to write:

// This does not work (wrong "this")!
reader.onload = function(e) {
  this.tiddlers = JSON.parse(reader.result)
}

But that will not work, because this inside a callback isn’t the same as this just outside the callback.

To access the tiddlers object that belongs to the Vue instance (of which handleFile() is a method), I can point a normal variable at this outside the callback, and refer to that normal variable inside the callback, like so:

var self = this
reader.onload = function(e) {
  self.tiddlers = JSON.parse(reader.result)
}

Interestingly, ES6 arrow functions don’t have their own this. If I write the callback as an arrow function, this is the context of the enclosing function, so I can refer to it. Translating the above:

reader.onload = e => {this.tiddlers = JSON.parse(reader.result)}

Even more solutions to the this situation are discussed in this Stack Overflow answer.

3. Invoke FileReader.readAsText()

My handleFile() method is invoked by the handler for the change event emitted by the <input type="file"> element when the user selects a file. So the event target (ev.target) is the <input> element.

The <input> element has a property, files, which returns an object of type FileList. Even if (as in my app) only a single file is selected, I need to refer to the file by its index in this list, so: ev.target.files[0].

Thus, to invoke the readAsText method:

reader.readAsText(ev.target.files[0])

Finally the method looks like:

handleFile(ev) {
  var reader = new FileReader()
  reader.onload = e => {this.tiddlers = JSON.parse(reader.result)}
  reader.readAsText(ev.target.files[0])
}

Here I used ev and e to refer to different events (the change event of the <input> element and the FileReader load event), although perhaps it’s better form to stick to one way of denoting an event.

Current code: minapp_v18.js

At this point I’ve got a sort-of working app written in a single JS file (plus a CSS file for styling). There are obvious improvements to make, and features to add. I have somehow avoided topics like watched and computed properties, so it hasn’t been a complete primer exercise, but it allowed me to bootstrap into some functionality and enough understanding that I can carry on if I want to, so I’m going to call this series of posts complete. Edit 5 March 2019: Except for the next post, in which I tidy up the display of paragraphs retrieved from stored values.