Async Form Posts With a Couple Lines of Vanilla JavaScript
Tags: html, javascript, a11y, webdev
Published: 2019-01-24 12:52:45

---
title: Async Form Posts With a Couple Lines of Vanilla JavaScript
published: true
canonical_url: https://pqina.nl/blog/async-form-posts-with-a-couple-lines-of-vanilla-javascript/
cover_image: https://thepracticaldev.s3.amazonaws.com/i/mpr01lf33qt1mzgd9axj.jpg
description: How to improve UX and transform a classic synchronous form post into an asynchronous one with a couple lines of JavaScript
tags: html, javascript, accessibility, webdev
---

In this tutorial, we will write a tiny JavaScript event handler that will post our HTML forms using `fetch` instead of the classic synchronous redirect form post. We're building a solution based on the Progressive Enhancement strategy, if JavaScript fails to load, users will still be able to submit our forms but if JavaScript is available the form submit will be a lot more smooth. While building this solution we'll explore JavaScript DOM APIs, handy HTML structures, and accessibility related topics.

Let's start by setting up a form.

> This article was originally published on [my personal blog](https://pqina.nl/blog/async-form-posts-with-a-couple-lines-of-vanilla-javascript/)


## Setting up the HTML

Let's build a newsletter subscription form. 

Our form will have an optional **name** field and an **email** field that we'll mark as required. We assign the `required` attribute to our email field so the form can't be posted if this field is empty. Also, we set the field type to `email` which triggers email validation and shows a nice email keyboard layout on mobile devices.

```html
<form action="subscribe.php" method="POST">

  Name
  <input type="text" name="name"/>

  Email
  <input type="email" name="email" required/>
  
  <button type="submit">Submit</button>

</form>
```

Our form will post to a `subscribe.php` page, which in our situation is nothing more than a page with a paragraph that confirms to the user that she has subscribed to the newsletter.

```html
<!doctype html>
<html lang="en">
  <head>
    <meta charset="UTF-8">
    <title>Successfully subscribed!</title>
  </head>
  <body>
    <p>Successfully subscribed!</p>
  </body>
</html>
```

Let's quickly move back to our `<form>` tag to make some tiny improvements.

If our stylesheet somehow fails to load it currently renders like this:

![All fields render in a single line](https://thepracticaldev.s3.amazonaws.com/i/997ymo8wel3vox5h249b.png)

This isn't horribly bad for our tiny form, but imagine this being a bigger form, and it'll be quite messy as every field will be on the same line. Let's wrap each label and field combo in a `<div>`.

```html
<form action="subscribe.php" method="POST">

  <div>
    Name
    <input type="text" name="name"/>
  </div>

  <div>
    Email
    <input type="email" name="email" required/>
  </div>

  <button type="submit">Submit</button>

</form>
```

Now each field is rendered on a new line.

![All fields are rendered on separate lines](https://thepracticaldev.s3.amazonaws.com/i/bca1b37gcabmnbtoqdy4.png)

Another improvement would be to wrap the field names in a `<label>` element so we can explicitly link each label to its sibling input field. This allows users to click on the label to focus the field but also triggers assistive technology like screen readers to read out the label of the field when the field receives focus.

```html
<form action="subscribe.php" method="POST">

  <div>
    <label for="name">Name</label>
    <input type="text" name="name" id="name"/>
  </div>

  <div>
    <label for="email">Email</label>
    <input type="email" name="email" id="email" required/>
  </div>

  <button type="submit">Submit</button>

</form>
```

A tiny effort resulting in big UX and accessibility gains. Wonderful!

With our form finished, let's write some JavaScript.



## Writing the Form Submit Handler

We'll write a script that turns all forms on the page into asynchronous forms.

We don't need access to all forms on the page to set this up, we can simply listen to the `'submit'` event on the `document` and handle all form posts in a single event handler. The event target will always be the form that was submitted so we can access the form element using `e.target`

To prevent the classic form submit from happening we can use the `preventDefault` method on the `event` object, this will prevent default actions performed by the browser.

*If you only want to handle a single form, you can do so by attaching the event listener to that specific form element.*

```js
document.addEventListener('submit', e => {
   
  // Store reference to form to make later code easier to read
  const form = e.target;

  // Prevent the default form submit
  e.preventDefault();

});
```

Okay, we're now ready to send our form data. 

This action is two-part, the sending part and the data part.

For sending the data we can use the [`fetch`](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API) API, for gathering the form data we can use a super handy API called [`FormData`](https://developer.mozilla.org/en-US/docs/Web/API/FormData).

```js
document.addEventListener('submit', e => {
   
  // Store reference to form to make later code easier to read
  const form = e.target;

  // Post data using the Fetch API
  fetch(form.action, {
    method: form.method,
    body: new FormData(form)
  })

  // Prevent the default form submit
  e.preventDefault();

});
```

**Yes, I kid you not, it's this straightforward.**

The first argument to `fetch` is a URL, so we pass the `form.action` property, which contains `subscribe.php`. Then we pass a configuration object, which contains the `method` to use, which we get from the `form.method` property (`POST`). Lastly, we need to pass the data in the `body` property. We can blatantly pass the `form` element as a parameter to the `FormData` constructor and it'll create an object for us that resembles the classic form post and is posted as `multipart/form-data`.

[Michael Scharnagl](https://dev.to/justmarkup) suggested moving the `preventDefault()` call to the end, this makes sure the classic submit is only prevented if all our JavaScript runs.


**We're done! To the pub!** 

![Let's go to the Winchester, have a nice cold pint, and wait for all of this to blow over](https://thepracticaldev.s3.amazonaws.com/i/czf30mf2tyfhisref0c1.gif)

Of course, there are a couple of things we forgot, this basically was the extremely happy flow, so hold those horses and put down that pint. How do we handle connection errors? What about notifying the user of a successful subscription? And what happens while the subscribe page is being requested?



## The Edge Cases

Let's first handle notifying the user of a successful newsletter subscription.



### Showing the Success State

We can do this by pulling in the message on the subscribe.php page and showing that instead of the form element. Let's continue right after the `fetch` statement and handle the resolve case of the `fetch` call.

First, we need to turn the response into a `text` based response. Then we can turn this text-based response in an actual HTML document using the [`DOMParser`](https://developer.mozilla.org/en-US/docs/Web/API/DOMParser) API, we tell it to parse our text and regard it as `text/html`, we return this result so it's available in the next `then`

Now we have an HTML document to work with (`doc`) we can finally replace our form with the success status. We'll copy the `body.innerHTML` to our `result.innerHTML`, then we replace our form with the newly created result element. Last but not least we move focus to the result element so it's read to screen reader users and keyboard users can resume navigation from that point in the page.

```js
document.addEventListener('submit', e => {
   
  // Store reference to form to make later code easier to read
  const form = e.target;

  // Post data using the Fetch API
  fetch(form.action, {
      method: form.method,
      body: new FormData(form)
    })
    // We turn the response into text as we expect HTML
    .then(res => res.text())

    // Let's turn it into an HTML document
    .then(text => new DOMParser().parseFromString(text, 'text/html'))

    // Now we have a document to work with let's replace the <form>
    .then(doc => {

      // Create result message container and copy HTML from doc
      const result = document.createElement('div');
      result.innerHTML = doc.body.innerHTML;

      // Allow focussing this element with JavaScript
      result.tabIndex = -1;

      // And replace the form with the response children
      form.parentNode.replaceChild(result, form);

      // Move focus to the status message
      result.focus();

    });

  // Prevent the default form submit
  e.preventDefault();

});
```



### Connection Troubles

If our connection fails the `fetch` call will be rejected which we can handle with a `catch`

First, we extend our HTML form with a message to show when the connection fails, let's place it above the submit button so it's clearly visible when things go wrong.

```html
<form action="subscribe.php" method="POST">

  <div>
    <label for="name">Name</label>
    <input type="text" name="name" id="name"/>
  </div>

  <div>
    <label for="email">Email</label>
    <input type="email" name="email" id="email" required/>
  </div>

  <p role="alert" hidden>Connection failure, please try again.</p>

  <button type="submit">Submit</button>

</form>
```

By using the `hidden` attribute, we've hidden the `<p>` from everyone. We've added a `role="alert"` to the paragraph, this triggers screen readers to read out loud the contents of the paragraph once it becomes visible.

Now let's handle the JavaScript side of things.

The code we put in the `fetch` rejection handler (`catch`) will select our alert paragraph and show it to the user.

```js
document.addEventListener('submit', e => {
   
  // Store reference to form to make later code easier to read
  const form = e.target;

  // Post data using the Fetch API
  fetch(form.action, {
      method: form.method,
      body: new FormData(form)
    })
    // We turn the response into text as we expect HTML
    .then(res => res.text())

    // Let's turn it into an HTML document
    .then(text => new DOMParser().parseFromString(text, 'text/html'))

    // Now we have a document to work with let's replace the <form>
    .then(doc => {

      // Create result message container and copy HTML from doc
      const result = document.createElement('div');
      result.innerHTML = doc.body.innerHTML;

      // Allow focussing this element with JavaScript
      result.tabIndex = -1;

      // And replace the form with the response children
      form.parentNode.replaceChild(result, form);

      // Move focus to the status message
      result.focus();

    })
    .catch(err => {

      // Some form of connection failure
      form.querySelector('[role=alert]').hidden = false;

    });

  // Make sure connection failure message is hidden
  form.querySelector('[role=alert]').hidden = true;

  // Prevent the default form submit
  e.preventDefault();

});
```

We select our alert paragraph with the CSS attribute selector `[role=alert]`. No need for a class name. Not saying we might not need one in the future, but sometimes selecting by attribute is fine.

I think we got our edge cases covered, let's polish this up a bit.



## Locking Fields While Loading

It would be nice if the form locked all input fields while it's being sent to the server. This prevents the user from clicking the submit button multiple times, and also from editing the fields while waiting for the process to finish.

We can use the `form.elements` property to select all form fields and then disable each field.

*If you have a `<fieldset>` in your form, you can disable the fieldset and that will disable all fields inside it*


```js
document.addEventListener('submit', e => {
   
  // Store reference to form to make later code easier to read
  const form = e.target;

  // Post data using the Fetch API
  fetch(form.action, {
      method: form.method,
      body: new FormData(form)
    })
    // We turn the response into text as we expect HTML
    .then(res => res.text())

    // Let's turn it into an HTML document
    .then(text => new DOMParser().parseFromString(text, 'text/html'))

    // Now we have a document to work with let's replace the <form>
    .then(doc => {

      // Create result message container and copy HTML from doc
      const result = document.createElement('div');
      result.innerHTML = doc.body.innerHTML;

      // Allow focussing this element with JavaScript
      result.tabIndex = -1;

      // And replace the form with the response children
      form.parentNode.replaceChild(result, form);

      // Move focus to the status message
      result.focus();

    })
    .catch(err => {

      // Show error message
      form.querySelector('[role=alert]').hidden = false;

    });

  // Disable all form elements to prevent further input
  Array.from(form.elements).forEach(field => field.disabled = true);

  // Make sure connection failure message is hidden
  form.querySelector('[role=alert]').hidden = true;

  // Prevent the default form submit
  e.preventDefault();

});
```

`form.elements` needs to be turned into an array using `Array.from` for us to loop over it with `forEach` and set the `disable` attribute on `true` for each field.

Now we got ourselves into a sticky situation because if `fetch` fails and we end up in our `catch` all form fields are disabled and we can no longer use our form. Let's resolve that by adding the same statement to the `catch` handler but instead of disabling the fields we'll enable the fields.

```js
.catch(err => {

  // Unlock form elements
  Array.from(form.elements).forEach(field => field.disabled = false);

  // Show error message
  form.querySelector('[role=alert]').hidden = false;

});
```

Believe it or not, we're still not out of the woods. Because we've disabled all elements the browser has moved focus to the `<body>` element. If the `fetch` fails we end up in the `catch` handler, enable our form elements, but the user has already lost her location on the page (this is especially useful for users navigating with a keyboard, or, again, users that have to rely on a screen reader).

We can store the current focussed element `document.activeElement` and then restore the focus with `element.focus()` later on when we enable all the fields in the `catch` handler. While we wait for a response we'll move focus to the form element itself.

```js
document.addEventListener('submit', e => {
   
  // Store reference to form to make later code easier to read
  const form = e.target;

  // Post data using the Fetch API
  fetch(form.action, {
      method: form.method,
      body: new FormData(form)
    })
    // We turn the response into text as we expect HTML
    .then(res => res.text())

    // Let's turn it into an HTML document
    .then(text => new DOMParser().parseFromString(text, 'text/html'))

    // Now we have a document to work with let's replace the <form>
    .then(doc => {

      // Create result message container and copy HTML from doc
      const result = document.createElement('div');
      result.innerHTML = doc.body.innerHTML;

      // Allow focussing this element with JavaScript
      result.tabIndex = -1;

      // And replace the form with the response children
      form.parentNode.replaceChild(result, form);

      // Move focus to the status message
      result.focus();

    })
    .catch(err => {

      // Unlock form elements
      Array.from(form.elements).forEach(field => field.disabled = false);

      // Return focus to active element
      lastActive.focus();

      // Show error message
      form.querySelector('[role=alert]').hidden = false;

    });

  // Before we disable all the fields, remember the last active field
  const lastActive = document.activeElement;

  // Move focus to form while we wait for a response from the server
  form.tabIndex = -1;
  form.focus();

  // Disable all form elements to prevent further input
  Array.from(form.elements).forEach(field => field.disabled = true);

  // Make sure connection failure message is hidden
  form.querySelector('[role=alert]').hidden = true;

  // Prevent the default form submit
  e.preventDefault();

});
```

I admit it's not a few lines of JavaScript, but honestly, there are a lot of comments in there.

### Showing a Busy State

To finish up it would be nice to show a busy state so the user knows something is going on. 

*Please note that while `fetch` is fancy, it currently doesn't support setting a timeout and it also doesn't support progress events, so for busy states that might take a while there would be no shame in using `XMLHttpRequest`, it would be a good idea even.*

With that said the time has come to add a class to that alert message of ours (DAMN YOU PAST ME!). We'll name it `status-failure` and add our busy paragraph right next to it.

```html
<form action="subscribe.php" method="POST">

  <div>
    <label for="name">Name</label>
    <input type="text" name="name" id="name"/>
  </div>

  <div>
    <label for="email">Email</label>
    <input type="email" name="email" id="email" required/>
  </div>

  <p role="alert" class="status-failure" hidden>Connection failure, please try again.</p>

  <p role="alert" class="status-busy" hidden>Busy sending data, please wait.</p>

  <button type="submit">Submit</button>

</form>
```

We'll reveal the busy state once the form is submitted, and hide it whenever we end up in `catch`. When data is submitted correctly the entire form is replaced, so no need to hide it again in the success flow.

When the busy state is revealed, instead of moving focus to the form, we move it to the busy state. This triggers the screen reader to read it out loud so the user knows the form is busy.

*We've stored references to the two status messages at the start of the event handler, this makes the code later on a bit easier to read.*

```js
document.addEventListener('submit', e => {
   
  // Store reference to form to make later code easier to read
  const form = e.target;

  // get status message references
  const statusBusy = form.querySelector('.status-busy');
  const statusFailure = form.querySelector('.status-failure');

  // Post data using the Fetch API
  fetch(form.action, {
      method: form.method,
      body: new FormData(form)
    })
    // We turn the response into text as we expect HTML
    .then(res => res.text())

    // Let's turn it into an HTML document
    .then(text => new DOMParser().parseFromString(text, 'text/html'))

    // Now we have a document to work with let's replace the <form>
    .then(doc => {

      // Create result message container and copy HTML from doc
      const result = document.createElement('div');
      result.innerHTML = doc.body.innerHTML;

      // Allow focussing this element with JavaScript
      result.tabIndex = -1;

      // And replace the form with the response children
      form.parentNode.replaceChild(result, form);

      // Move focus to the status message
      result.focus();

    })
    .catch(err => {

      // Unlock form elements
      Array.from(form.elements).forEach(field => field.disabled = false);

      // Return focus to active element
      lastActive.focus();

      // Hide the busy state
      statusBusy.hidden = false;

      // Show error message
      statusFailure.hidden = false;

    });

  // Before we disable all the fields, remember the last active field
  const lastActive = document.activeElement;

  // Show busy state and move focus to it
  statusBusy.hidden = false;
  statusBusy.tabIndex = -1;
  statusBusy.focus();

  // Disable all form elements to prevent further input
  Array.from(form.elements).forEach(field => field.disabled = true);

  // Make sure connection failure message is hidden
  statusFailure.hidden = true;

  // Prevent the default form submit
  e.preventDefault();

});
```


**That's it!**

We skipped over the CSS part of front-end development, you can either use a CSS framework or apply your own custom styles. The example as it is should give an excellent starting point for further customization. 

One final thing. [Don't remove the focus outline](http://www.outlinenone.com).



## Conclusion

We've written a semantic HTML structure for our form and then built from there to deliver an asynchronous upload experience using plain JavaScript. We've made sure our form is accessible to users with keyboards and users who rely on assistive technology like screen readers. And because we've followed a Progressive Enhancement strategy the form will still work even if our JavaScript fails.

I hope we've touched upon a couple new APIs and methodologies for you to use, let me know if you have any questions!