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:
- A
FileReader
method (here,FileReader.readAsText()
) is invoked - When the read operation is finished, the
load
event is fired. At this point, the file’s contents are in itsFileReader.result
property. - The property
FileReader.onload
is a handler for theload
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’stiddlers
array.
Breakdown
Therefore, the tasks involved in writing my handleFile()
method are:
- Use the
FileReader()
constructor to create a newFileReader
object - 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 - Invoke the
FileReader.readAsText()
method, which, when it finishes, will fire theload
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.