A small Vue.js app

Selecting a local file with <input type="file">

In my previous post, I used Eli Grey’s FileSaver.js library to allow my Vue app’s data (an array called tiddlers) to be downloaded to a local file. The next obvious thing to do is load it back from the file.

I had planned to do that in a single post, but it turns out that selecting a file and reading its contents are each interesting enough to warrant thinking about separately.

Goal

Here I will add an interface that uses an <input type="file"> element to let the user choose a local file to load.

Breakdown

  1. Add an <input type="file"> element to the template
  2. Apply consistent styling to the interface by hiding the <input> element and adding a button to activate it
  3. Add a handler that watches for changes in the file list (i.e. when the user picks a file) and invokes a method, handleFile(), which will be written in the next post

1. Add an <input type="file"> element to choose a file

This element is a bit strange, UI-wise. The whole element is clickable, even though it looks like it has a button. Browsers choose the text on the “button” part. Firefox puts “Browse…” on it. I can add a <label> element to clarify what the “button” is for. The <label> is clickable too. The "main-buttons" part of the template now looks like this:

<div id="main-buttons">
  <div id="file-load-input">
    <label for="load-tiddlers">Load tiddlers from file:</label>
    <input type="file" id="load-tiddlers" name="load-tiddlers" accept="application/JSON">
  </div>
  <button id="save-tiddlers" @click="saveFile">Save tiddlers to file</button>
</div>

I’ve specified with the accept attribute that the <input> element should only accept JSON files.

2. Style the file-opening interface

The <input type="file"> element interface is really inconsistent with the rest of the app, so I want to customize it.

I tried the MDN suggested solution of visually hiding the input element, and using CSS to put a dotted line around the label element when the <input> element has focus, but the dotted line didn’t show up in Firefox. It showed up in Chrome, but Chrome’s default indicator of focus seems to be a thick blue glowing outline, which (aside from looking goofy), is inconsistent with the dotted outline.

So I’ll go with the gist of the other MDN suggestion of activating the <input> element with a click from a real <button> element.

Write a method of the Vue instance that clicks the "load-tiddlers" <input> element:

fileSelect() {
  document.getElementById("load-tiddlers").click()
}

Add the button in the template, with a click handler pointing to the fileSelect() method.

<button id="file-select" @click="fileSelect">Load tiddlers from file</button>

In CSS, make the <input type="file"> element hidden with display: none;. I’ve also taken out the <label> element, and to fit the “Load” and “Save” buttons side-by-side, I’ve shortened their text.

Now the user can select a file. The app does nothing with it, yet.

Current code: minapp_v17b.js

3. Add an event handler to the <input> element in the template

When the user chooses a file, this commits a change to the value of the <input> element, and a change event is emitted. I’ll attach a handler for that event in the template:

<input type="file" id="load-tiddlers" name="load-tiddlers" @change="handleFile" accept="application/JSON">

Now the app is a bit broken, because it’s invoking handleFile() when the user selects a file, but handleFile() does not exist yet.