Submit FormData object using Fetch API

created: 9/12/2019

updated: 7/17/2020

The JavaScript Fetch API provides a utility to make AJAX requests. This post will show how ES6 syntax can be used with Typescript and the Fetch API to submit an HTML form. Using the Fetch API in conjunction with other Web API's a post request can be sent, containing FormData Objects in the body of the request. If you would like more infomration about Typescript please read my post describing how to compile TypeScript with npm.

HTML Form

First we need to create an html file, let's call it index.html, with a form element to capture the input values we are going to submit using JavaScript.

    
    <!-- index.html -->
    <!DOCTYPE html>
    <html lang="en">
    <head>
    <meta charset="utf-8">
    <title>Example Form</title>
    </head>
    <body>
      <form id="myForm" action="myFormAction" method="post">

        <!-- this input is hidden with a value already set -->
        <input type="hidden" id="userId" name="userId" value="3"/>  

        <label for="firstName">first name</label>
        <input type="text" id="firstName" name="firstName"/>

        <button type="submit">Submit</button>
      </form>
      <script src="form.js"></script>
    </body>    
    </html>
    
  

Take note that the value for the form action attribute is a placeholder. In real usage this would be replaced with url that you would like to submit the form to. Just for fun, one of the inputs is type=hidden to show that we can submit hidden elements using this technique. Additionally there is one input to capture first name and button to submit the form using the HTTP post method.

Typescript Form Submit

Next we'll need to write form.ts so the TypeScript compiler can generate the JavaScript file, form.js, referenced in index.html. The code in form.ts will handle the form submit by making an AJAX request. If you haven't already now is a good time to read my other post Compile Typescript with npm. There you will find instructions on how to install and configure TypeScript to accomodate the usage below.

*NOTE*: The code below assumes the endpoint we are submitting the form to, in the sample HTML action="myFormAction", will be returning a response that has the Content-Type header set to application/json.

To begin an event listener is created to listen for all form submit events. Notice that the callback function is marked as an async function. Using the async modifier allows for the use of the await keyword when executing the asynchronous Fetch request.

    
    // form.ts

    // listen for any form submit event 
    document.body.addEventListener("submit", async function(event) {
          
    });      
    
  

Inside the callback the first line of code prevents the default action from occuring. Without preventing the default, the browser would attempt to navigate to the URL of the form action attribute when the form is submitted.

    
    // form.ts

    // listen for any form submit event 
    document.body.addEventListener("submit", async function(event) {
      event.preventDefault();
    });      
    
  

Next a variable for the form element is created and cast to an HTMLFormElement to allow access to the action and method properties.

    
    // form.ts

    // listen for any form submit event 
    document.body.addEventListener("submit", async function(event) {
      event.preventDefault();
      
      const form = event.target as HTMLFormElement;
    });      
    
  

Fetch API Post Form Data

Then the result variable is created and it is used to store the response sent following the Fetch request. The Fetch request returns a promise and must be awaited so that the result can be obtained. The URL passed into the Fetch method is set to the action of the form, and the options contains keys for method and body values. The form method, like the action, is available from HTMLFormElement.

    
    // listen for any form submit event 
    document.body.addEventListener("submit", async function(event) {
      event.preventDefault();

      const form = event.target as HTMLFormElement;
      
      // casting to any here to satisfy tsc
      // sending body as x-www-form-url-encoded
      const result = await fetch(form.action, {
        method: form.method,
        body: new URLSearchParams([...(new FormData(form) as any)])
      })        
      .then((response: Response) => response.json())
      .then(json => json)
      .catch(error => console.log(error));        
    });      
    
  

Notice the use of spread syntax to transform the FormData into an array of key-value pairs. This may seem redudant, but the Edge browser cannot iterate over FormData objects. By transforming the object into an array, Edge is able to successfully construct the URLSearchParams object.

Interestingly the current TypeScript definition for URLSearchParams does not permit a FormData object to be passed into the constructor, however this is valid JavaScript. To satisfy the TypeScript compiler the FormData object is cast to any. This allows a URLSearchParams object to be constructed from the FormData object which itself is constructed from the HTMLFormElement. Since the body of the Fetch request is of the type URLSearchParams (hint: it looks like a ?query=string) the resulting Content-Type of the request body will be x-www-form-url-encoded. This allows for the server to parse it as it would a normal form submission.

Now when the form in the index.html file is submitted the submit event listener will override the default browser behavior and submit the form using an AJAX request. Even without an actual endpoint to receive the request you should still be able to verify the code is working because the resulting error Failed to fetch will be logged to the console.

Comments

  • Josh Habdas

    Thanks for the post, James. It was very helpful and highly ranked in search. A couple of suggestions for improvement: (1) Handle the form submit event directly as it's valid HTML to have multiple forms on a page and (2) make a note about security somewhere with regard to how query strings attached to URLs can leak data if not sent encrypted. One more thing to note, I ended up simply passing a FormData instance without using URLSearchParams for my use case. This is worth covering with an example and it cleans up the code. I also think you might be able to do new URLSearchParams(new FormData(event.currentTarget)) and pass that straight into the body. Regardless, this was just what I was looking for. Many thanks for posting.

Leave a Reply