In my article “Embrace the Platform” over at CSS-Tricks I mentioned this experience by Drew Devault:
My web browser has been perfectly competent at submitting HTML forms for the past 28 years, but for some stupid reason some developer decided to reimplement all of the form semantics in JavaScript, and now I can’t pay my electricity bill without opening up the dev tools.
While the article hints at solving the situation using Progressive Enhancement, it doesn’t cover the practical side of things. Let’s change that with this post.
~
Enhance!
To apply Progressive Enhancement on HTML forms, there’s this little JavaScript gem named FormData
that one can use:
The
FormData
interface provides a way to easily construct a set of key/value pairs representing form fields and their values, which can then be easily sent using thefetch()
orXMLHttpRequest.send()
method. It uses the same format a form would use if the encoding type were set to"multipart/form-data"
.
To use FormData
, create a new instance of it and pass in a form element as its first argument. It will automagically do its thing.
const form = document.querySelector('form');
const data = new FormData(form); // 👈 Captures all the form’s key-value pairs
As FormData
captures all the form’s key-value pairs, applying Progressive Enhancement on forms becomes pretty easy:
- Build a regular HTML form that submits its data to somewhere
- Make it visually interesting with CSS
- Using JavaScript, hijack the form’s
submit
event and, instead, send its contents — captured throughFormData
— usingfetch()
to the defined endpoint.
A first iteration of the JavaScript code for step 3 would look like this:
document.querySelector("form").addEventListener("submit", async (event) => {
event.preventDefault();
const form = event.currentTarget;
const resource = form.action;
const options = {
method: form.method,
body: new FormData(form) // 👈 Magic!
};
const r = await fetch(resource, options);
if (!r.ok) {
// @TODO: Show an error message
return;
}
// @TODO: handle the response by showing a message, redirecting, etc.
});
Congratulations, you’ve just progressively enhanced your form!
~
Dealing with JSON and GET
While this basic approach already works, it doesn’t cover all scenarios, mainly due to the way how FormData
encodes the data:
The
FormData
interface […] uses the same format a form would use if the encoding type were set to"multipart/form-data"
.
So if you want to send the data as JSON, you will need to convert the formData
to it yourself. Thankfully, using Modern JavaScript, that’s only a one-liner nowadays. Furthermore the code also doesn’t properly handle GET
requests. To cater for those one doesn’t need to send the formData
as the request body
, but alter the resource
’s query string parameters instead.
Expressed in code, we need these adjustments:
const resource = new URL(form.action || window.location.href);
// …
if (options.method === "get") {
resource.search = new URLSearchParams(formData);
} else {
if (form.enctype === "multipart/form-data") {
options.body = formData;
} else {
options.body = JSON.stringify(Object.fromEntries(formData));
options.headers['Content-Type'] = 'application/json';
}
}
~
Demo
Embedded below is a CodePen demo that submits the data to a dummy API and then handles its response:
See the Pen
Progressive Enhancement and <form>: Use FormData by Bramus (@bramus)
on CodePen.
🥳
~
Next steps
The core of this post evolves around using FormData
+ fetch()
to capture and send the data. As for next steps, the experience can still further be improved by – for example – preventing double form submissions and showing a loading spinner.
Example of a loading indicator while the form is submitting
~
🔥 Like what you see? Want to stay in the loop? Here's how:
Thank you for these tips, but the only way we can get forms to comply with WCAG is to use JavaScript as the error handling in HTML forms is broken. That is the reason people hack forms to add some functionality in there.