Introduction

Recently, I’ve been working on a file upload from a web app, which needed to report the upload progress to the end user. I typically use the browsers’ native fetch function to perform API calls to the server. It supports promises and does not require external dependencies. For the few browsers, which do not implement fetch, there are convenient polyfills.

However, I couldn’t make fetch report the progress of a server call. I could await for a call’s promise to finish, but couldn’t get intermediate progress updates. Thus, I had to resolve to a native browser primitive called XMLHttpRequest. Unfortunately, it’s quite old school and did not fit well with the rest of project’s code which uses promises. In this article, I’ll show how to wrap the XMLHttpRequest primitives in a simple promise-based function for arbitrary form submission.

The Solution

The following function implements programmatic form submission with fields and files. I’ve used TypeScript to show the types of the parameters. If you are using plain JavaScript, you’ll need to remove the type definitions. The function takes a few params:

  • url - the server endpoint.
  • fields - a dictionary/map whose keys are the form fields. The values are either strings, files (e.g. from a file selection event), or blobs.
  • headers - a dictionary/map whose keys and values represent the headers. This is typically used to authenticate to the server.
  • onprogress - a callback function called by the browser every now and then to report the upload progress.

The result of the function call is a promise which resolves with the server response if the form submission was successful. On failure the promise rejects/fails. Check out the comments in the code below for more details on the implementation:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
export function submitForm(
  url: string,
  fields: Map<string, string | File | Blob>,
  headers: Map<string, string>,
  onprogress: (pe: ProgressEvent) => any) {
  // FormData represents the payload of the form
  const form = new FormData();

  // Attach all files and field values
  fields.forEach((val, field) => form.append(field, val));

  // The XHR request/call
  const xhrRequest = new XMLHttpRequest();

  // The "true" parametes makes the request asynchronous
  xhrRequest.open("POST", url, true);

  // Set up the the request headers.
  // Skip "content-type" - it messes up xhr. See - http://www.olioapps.com/blog/formdata-fetch-gotchas/
  headers.forEach((val, h) => h.toLowerCase() !== "content-type" && xhrRequest.setRequestHeader(h, val));

  // Set up the callback for the upload progress
  xhrRequest.upload.onprogress = onprogress;

  // Wrap the result callback in a promise
  const responsePromise = new Promise((resolve, reject) => {
    xhrRequest.onreadystatechange = () => {
      // State 4 means "Done" - https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest/readyState
      if (xhrRequest.readyState === 4) {
        if (xhrRequest.status >= 200 && xhrRequest.status <= 299) {
          resolve(xhrRequest.responseText);
        } else {
          reject(xhrRequest.statusText);
        }
      }
    };
  });

  // Kick off the actual form submission
  xhrRequest.send(form);

  // Return the promise so the caller can await
  return responsePromise;
}

Sample Usage

Let’s demonstrate how to call the form upload function:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// Assuming we have an "onChange" listener on a file input in the HTML
// we can extract the selected File from the event object
const file = e.target.files[0] as File;

// Prepare the headers for authentication
const headers = new Map([['Authorization', 'Bearer secret-token']]);
const fields = new Map([
  ['user', 'john'],
  ['upload', file]
]);

// A callback for the upload progress
const onprogress = (pe: ProgressEvent) => {
  const percent = Math.floor(pe.loaded / pe.total * 100);
  console.log(`${percent} % finished`);
}

// Call the function ..
const submissionPromise = submitForm('http://example.com', fields, headers, onprogress);

// Wait for server call to succeed or fail
submissionPromise
  .then(resp => console.log(resp))
  .catch((e: any) => console.log(e));