Module

Basic UI and File Uploads

Progress 2/8
2/8

Create a basic UI

Let’s improve the popup so users can upload their syllabus file for us to use!

Within our <body> element, we’ll add an <input> element with the type set to "file" — this allows users to select and submit their syllabus and add a respective id which we will use for our event listeners. Just below the input, we’ll include a <p> tag to let users know where they’ll be able to click and download their CSV file, which can contain text like Click here to download your file!.

Tip: Make sure to properly close both the <input> and <p> tags.

Next, we need to connect our JavaScript to this HTML. To do that, we’ll add a <script> tag right before the closing </body> tag. The script should have type="module" and src="popup.js".

Using type="module" allows us to use modern JavaScript features such as import and export statements. The src attribute tells the browser to load the logic from our popup.js file — the place where all our “behind-the-scenes” functionality will live. Later, we’ll also update the innerHTML of the <p> tag to contain a downloadable link once the file has been processed.

Make sure to include the script tag at the bottom of the body or else your JavaScript will not work on your HTML elements.

Your index.html should finally look like this:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Syllabus to CSV</title>
</head>
<body>
    <h1>MasterList</h1>
    <input type="file" id="file-upload" name="filename">
    <p>Click on the File to download it:<p>

    <script type="module" src="popup.js"></script>
</body>
</html>

Handle file uploads

Now that we’ve set up a file input, let’s write the JavaScript needed to handle uploaded files and prepare them for processing. Create a file in the root directoy named popup.js.

We’ll be using document.getElementById() to grab our file input and attach an event listener that runs every time a user selects a file.

1
2
3
document.getElementById('file-upload').addEventListener('change', async () => {
    // all the lines below will go inside here
});

Here, we’re listening for the “change” event on the file input id (or whatever id you used) — this fires whenever a user picks a file.

1
async () => {}

Async allows us to use await inside the function. Since we’ll likely send the file to an external API (which takes time), we want to pause and wait for the response before moving on — this keeps the code clean and readable.

Without async, we’d have to use .then() chains, which are harder to manage.

Arrow function syntax (() => {}) is a modern way to write functions in JavaScript. It’s short, clean, and avoids creating its own this context — which works well here since we don’t need to refer to the event handler’s context directly.

Tip: We use async to write cleaner code that lets us easily work with an API. If you’d like to learn more about why async & await is the best practice, watch this video

1
const fileUploaded = this.files.item(0);

This line attempts to grab the first file the user selected.

BUG HUNT: If you test this event handler by selecting a file and opening the DevTools Console (right-click the extension popup and click Inspect), you will see TypeError: Cannot read properties of undefined (reading 'item')! Why is this.files undefined inside an arrow function async () => {}, and how can we use the event parameter instead?

Fixing the this Context

Unlike traditional function() declarations, arrow functions (() => {}) do not bind their own this context — this refers to the global window object. To access the uploaded file safely inside an arrow event listener, pass the event parameter and use event.target.files[0]:

1
2
3
4
5
document.getElementById('file-upload').addEventListener('change', async (event) => {
    const fileUploaded = event.target.files[0];
    if (!fileUploaded) {
        return;
    }
1
2
3
const form = new FormData();
form.append('purpose', 'ocr');
form.append('file', new File([fileUploaded], `${fileUploaded.name}`));

Here, we create a FormData object to prepare for sending the file to an external API.

FormData works like a key-value map that you can send with fetch() for things like file uploads.

We add two things:

A purpose field — this is useful if your API requires it (in this case, to label it for OCR processing).

The actual uploaded file, wrapped in a new File object.

Note: Wrapping the file again with new File([…]) is optional but helpful if you want to manipulate the name or metadata before sending.

This is the foundation of getting the syllabus file from the user and preparing it for conversion.

At this point your code should look similar to this:

1
2
3
4
5
6
7
8
document.getElementById('file-upload').addEventListener('change', async (event) => {
    const fileUploaded = event.target.files[0];
    if (!fileUploaded) {
        return;
    }
    const form = new FormData();
    form.append('purpose', 'ocr');
    form.append('file', new File([fileUploaded], `${fileUploaded.name}`));

In the next step, we’ll send this FormData to an OCR model for parsing and response.