Form Data - My 2nd Favourite Web API
Of the hundreds of Web APIs available in browsers and some server-side runtimes, the FormData class is one of my favourites.
tl;dr
- Use a
<form>
. - Add
names
s to all your inputs - most inputs should have unique names. All inputs withtype="checkbox"
ortype="radio"
should have the same name for logical groups, and should havevalue
s. - Construct a
FormData
object with the form as an argument:new FormData(form)
. - Use the
get
andgetAll
methods to access any values you need!
Using it
At first blush, the
methods available
see kinda useless - why use FormData.get
and FormData.set
when Map
s do the
exact same thing, and plain old objects can do basically the same thing with an
arguable simpler interface? The real power of FormData
comes when used with
the <form>
element. Take the simple register form below:
<form id="register-form">
<!-- Email Input -->
<div>
<label for="register-email">Email</label>
<input type="email" id="register-email" required />
</div>
<!-- Password Input -->
<div>
<label for="register-password">Password</label>
<input type="password" id="register-password" required />
</div>
<!-- Mailing Lists -->
<fieldset>
<legend>
<p>Select the email lists you want to subscribe to</p>
</legend>
<label>
<input type="checkbox" id="register-mailing-marketing" />
Marketing Emails
</label>
<label>
<input type="checkbox" id="register-mailing-blog" />
Blog Notifications
</label>
<label>
<input type="checkbox" id="register-mailing-spam" />
Constant Spam
</label>
</fieldset>
<button type="submit">Register</button>
</form>
When the form is submitted you'll need to get the values with JavaScript, which you might do like this:
const registerForm: HTMLFormElement = document.getElementById('register-form')
const emailInput: HTMLInputElement = document.getElementById('register-email')
const passwordInput: HTMLInputElement =
document.getElementById('register-password')
const marketingMailingInput: HTMLInputElement = document.getElementById(
'register-mailing-marketing'
)
const blogMailingInput: HTMLInputElement = document.getElementById(
'register-mailing-blog'
)
const spamMailingInput: HTMLInputElement = document.getElementById(
'register-mailing-spam'
)
registerForm.addEventListener('submit', event => {
// Remember to prevent the default <form> submission behaviour
// if you're handling submissions with JavaScript!
event.preventDefault()
const email = emailInput.value
const password = passwordInput.value
const acceptedMailingLists = []
if (marketingMailingInput.checked) {
acceptedMailingLists.push('marketing')
}
if (blogMailingInput.checked) {
acceptedMailingLists.push('blog')
}
if (spamMailingInput.checked) {
acceptedMailingLists.push('spam')
}
doRegister(email, password, acceptedMailingLists)
})
const registerForm = document.getElementById('register-form')
const emailInput = document.getElementById('register-email')
const passwordInput = document.getElementById('register-password')
const marketingMailingInput = document.getElementById(
'register-mailing-marketing'
)
const blogMailingInput = document.getElementById('register-mailing-blog')
const spamMailingInput = document.getElementById('register-mailing-spam')
registerForm.addEventListener('submit', event => {
// Remember to prevent the default <form> submission behaviour
// if you're handling submissions with JavaScript!
event.preventDefault()
const email = emailInput.value
const password = passwordInput.value
const acceptedMailingLists = []
if (marketingMailingInput.checked) {
acceptedMailingLists.push('marketing')
}
if (blogMailingInput.checked) {
acceptedMailingLists.push('blog')
}
if (spamMailingInput.checked) {
acceptedMailingLists.push('spam')
}
doRegister(email, password, acceptedMailingLists)
})
This doesn't look that bad, but it's a lot of work that we could avoid.
Consider (almost) the same script but now using FormData
:
const registerForm: HTMLFormElement = document.getElementById('register-form')
registerForm.addEventListener('submit', event => {
// Remember to prevent the default <form> submission behaviour
// if you're handling submissions with JavaScript!
event.preventDefault()
const formData = new FormData(registerForm)
const email = formData.get('email')
const password = formData.get('password')
const acceptedMailingLists = formData.getAll('mailing-lists')
doRegister(email, password, acceptedMailingLists)
})
const registerForm = document.getElementById('register-form')
registerForm.addEventListener('submit', event => {
// Remember to prevent the default <form> submission behaviour
// if you're handling submissions with JavaScript!
event.preventDefault()
const formData = new FormData(registerForm)
const email = formData.get('email')
const password = formData.get('password')
const acceptedMailingLists = formData.getAll('mailing-lists')
doRegister(email, password, acceptedMailingLists)
})
This is good, but there's one pretty big issue with this that I haven't
mentioned yet, which leads into the a (kind of) downside with using FormData
.
The code above doesn't work - not because I suck at JavaScript or because
FormData is too bleeding edge, but because we need to update the origina HTML to
work with FormData
, and be more correct.
In the code snippet above, we used a few strings as arguments to formData.get
and formData.getAll
, but where do these come from? When you use create a
FormData
object with a form
, the keys we use in get
and getAll
map to
the name
attribute for any <input>
s, <select>
s, and <textarea>
s inside
the <form>
. We can fix this issue by adding name
s to the inputs and adding
value
s to the <input>
s with type="checkbox"
:
<form id="register-form">
<!-- Email Input -->
<div>
<label for="register-email">Email</label>
<input name="email" type="email" id="register-email" required />
</div>
<!-- Password Input -->
<div>
<label for="register-password">Password</label>
<input name="password" type="password" id="register-password" required />
</div>
<!-- Mailing Lists -->
<fieldset>
<legend>
<p>Select the email lists you want to subscribe to</p>
</legend>
<label>
<input
name="mailing-lists"
value="marketing"
type="checkbox"
id="register-mailing-marketing"
/>
Marketing Emails
</label>
<label>
<input
name="mailing-lists"
value="blog"
type="checkbox"
id="register-mailing-blog"
/>
Blog Notifications
</label>
<label>
<input
name="mailing-lists"
value="spam"
type="checkbox"
id="register-mailing-spam"
/>
Constant Spam
</label>
</fieldset>
<button type="submit">Register</button>
</form>
If you scroll all the way to the bottom, you'll notice that all the checkbox
<input>
s all have the same name
, and they each have a value
. As mentioned
before, the name
is how FormData
knows what to associate the input's value
with, but when there are multiple inputs with the same name
, we can get the
values for all of them with getAll
. Now, to know which checkbox have been
selected, we give them unique value
s.
After making the changes above, everything works! Here's a demo of the complete code in a CodeSandbox:
React and Other Frameworks
If you've used React, you might notice a little issue with the using the code
above - React doesn't (directly) give you access to the DOM elements created.
You could use this weird hacky hook called useRef
, but there's simpler
solution. In fact, you can use this approach in any framework where you have
access to the <form>
s submission event. We can use the currentTarget
property on methods to get access to the element that we added an event listener
to, so currentTarget
for a submit event will be the form! Here's what a
simplified React component using FormData
would look like:
import type React from 'react'
export const ExampleForm: React.FC = () => {
const handleSubmit: FormEventHandler<HTMLFormElement> = event => {
// Remember to prevent the default <form> submission behaviour
// if you're handling submissions with JavaScript!
event.preventDefault()
const formData = new FormData(event.currentTarget)
const email = formData.get('email')
const password = formData.get('password')
const acceptedMailingLists = formData.getAll('mailing-lists')
doRegister(email, password, acceptedMailingLists)
}
return (
<form onSubmit={handleSubmit}>
{/* Same form contents as the HTML form above */}
</form>
)
}
export const ExampleForm = () => {
const handleSubmit = event => {
// Remember to prevent the default <form> submission behaviour
// if you're handling submissions with JavaScript!
event.preventDefault()
const formData = new FormData(event.currentTarget)
const email = formData.get('email')
const password = formData.get('password')
const acceptedMailingLists = formData.getAll('mailing-lists')
doRegister(email, password, acceptedMailingLists)
}
return (
<form onSubmit={handleSubmit}>
{/* Same form contents as the HTML form above */}
</form>
)
}
Notice how there aren't any useState
s and you don't need to manually wire up
value
and onChange
to control and get values for all of the inputs!
fetch
ing with FormData
When you need to make a fetch
request the values from form, you'll often get
the specific values you need (as above), and make a request using them. However,
if your API accepts application/x-www-form-urlencoded
or multipart/form-data
mime types for the request body (i.e. accepts those values for Content-Type
),
you can set the body
of a fetch
request to the FormData directly! Doing this
also automatically adds the correct Content-Type
header to your request.
const formData = new FormData(document.getElementById('register-form'))
fetch('/your-endpoint', {
method: 'POST',
body: formData,
})
const formData = new FormData(document.getElementById('register-form'))
fetch('/your-endpoint', {
method: 'POST',
body: formData,
})
This is also the only way I know of right now that allows you to send or upload multiple files in a network request!
On the Server
Warning
The following will work only if the HTTP request has the Content-Type
header
set to application/x-www-form-urlencoded
or multipart/form-data
. If the
header has a different value, or is unset an error will be thrown.
If you use a server-side framework / environment that uses Web API Request
s
(such as
Next.js' middleware,
Remix and Astro), you can use
FormData
on there too! Request
s have a formData
method that parses the
request body as a FormData
object, similar to how Response.json()
parses a
response's body as JSON.
For example, in Remix your action
s have access to the request and you can
access its body as a FormData
object:
import type { ActionArgs } from 'remix'
// This runs on the server!
export const action = async ({ request }: ActionArgs) => {
const formData = await request.formData()
const email = formData.get('email')
const password = formData.get('password')
const acceptedMailingLists = formData.getAll('mailing-lists')
db.createAccount(email, password, acceptedMailingLists)
}
// This runs on the server!
export const action = async ({ request }) => {
const formData = await request.formData()
const email = formData.get('email')
const password = formData.get('password')
const acceptedMailingLists = formData.getAll('mailing-lists')
db.createAccount(email, password, acceptedMailingLists)
}
A (Somewhat) Nice Hack
Warning
The following will only works well if every input in your form has different
name
attributes, and so doesn't really support inputs with
type="checkbox"
.
FormData
's interface is great, but sometimes you just need object to do the
job. The following snippet converts a FormData
to a plain JavaScript object
where the keys and values correspond to those in your FormData
.
const formData = new FormData(document.getElementById('register-form'))
const dataAsObject = Object.fromEntries(formData as any)
const formData = new FormData(document.getElementById('register-form'))
const dataAsObject = Object.fromEntries(formData)
Warning
For at least version 4.8.4
and earlier, TypeScript doesn't recognise the
required properties on FormData
even though they're widely implemented so
you need to cast the FormData
object with as any
.
Conclusion
FormData
is great. Use it if you can.