Forms in HTML are an odd thing. HTML was initially designed to be an index to other non-HTML resources (that's why index.html is the default). In fact the hyper part of hypertext was primarily all about links; if you needed to find a user in a contact list, for example, the original design was to click links, building up a series of filters, to narrow down your search.
Of course, as the Web grew, it wasn't too long until the cracks in this system started to show. Both developers and users wanted to interact with Web servers at a deeper level. They wanted to be able to send text, make selections, and toggle options. The initial idea of “you should use a separate application for that” met resistance, and so was born the concept for Web forms. First prototyped by Lynx browser, Web forms were introduced to the masses by 1993's Mosaic 2.0 as fill-out forms (http://marc.merlins.org/htmlearn/forms/forms.html), and before long, they were a must-have for every browser on the market.
Forms are a powerful yet simple and easy to use solution. Even complete novices can write them.
Forms are a powerful yet simple and easy-to-use solution. Even complete novices can write them. You create the inputs you want and put them in a form tag and...well that's really all there is to it! Here's a sample:
<form action="/contact-us" method="POST">
<label>
<span class="caption">Your Email</span>
<input name="email" />
</label>
<label>
<span class="caption">Message</span>
<textarea name="message"></textarea>
</label>
<button type="submit">Send</button>
</form>
And on the server? That's simple enough as well. You could read the submitted data out of Request.Form
or $_POST
or even use fancy-pantsy model-binding.
Form-free Coding
Fast-forward to 2016 and Web developers have undergone a revolution. Devs simply don't use forms much anymore. We use model binding with Knockout observables, or with AngularJs and its concept of cascading scopes. We create components with isolated state in React, or bring in Redux, CycleJs, or Reflux for unidirectional data-flow. We invent things like Backbone and do just about everything under the sun to avoid that form tag. And that's fine. After all, there was a reason we all stopped using forms in the first place.
Specifically, there were three big reasons.
The Need for Validation
Developers needed the ability to do client-side validations. This is a need that was identified as early as 1993 (http://1997.webhistory.org/www.lists/www-talk.1993q2/0023.html). Tasks like ensuring that an entry is numeric or matches a certain pattern, or contains certain terms are important. It seems like the only way to do these in the browser is with JavaScript. But to run JavaScript code on a value, having the value in a variable and doing some sort of model binding to objects seems inescapable.
The Need for REST
When forms were first introduced in the early 90s, HTTP was still new and the term REST would not be coined for another eight years. The GET verb was the only one in the initial specification, quickly followed by POST when the need for a request body to supplant the URL was identified. Additional verbs were not defined until 1996 and even then, were deemed of secondary importance (https://tools.ietf.org/html/rfc1945#appendix-D.1). Forms never received the upgrade and, despite efforts throughout the years to support the full range of HTTP verbs, forms still lag behind, supporting only old-school GET and POST.
Today is the age of REST architectures, where the verb used is as important as the data being sent. It seems, therefore, a terrifying oversight that forms can't submit PUTs or DELETEs. Unfortunately, the only way to do this is with the XMLHttpRequest
or fetch
APIs. Both require you to provide the form data as JavaScript objects. So you're back to custom JavaScript code and model binding.
Asynchronous Form Submission
Possibly the biggest reason developers stopped using forms has to do with the fact that the early browsers didn't have the concept of tabs or of things happening in the background. Many barely had GUIs. They were essentially big modal windows and submitting a form was a synchronous operation during which your data was sent to the server and a new page returned in its stead. This made perfect sense when pages were simple; why would you need to remain on the page? To revel in how good a job you did filling it out? No, your form was sent and you awaited the next step.
As Web pages grew more complex browsers started supporting alternate possibilities. Microsoft introduced the IXMLHTTPRequest
interface in 1999 and when eventually the world figured out that it was useful, Ajax was born. Web pages could send and receive data in the background while users continued interacting with their complex and meticulously rendered pages. This was all due to JavaScript. Forms offered no support.
Modern Alternatives
I want to just say that forms are nice. It's hard to think of a simpler mechanism for submitting data to the server, but, for all of the reasons above, the development community stopped using them.
Meanwhile, browsers have been evolving at a breakneck pace. It's now fair to stop and ask again: Do the above reasons against forms still hold or are we all sprinting along from one great-idea to another, leaving a trail of bathwater and babies in our path?
It's now fair to stop and ask again: Do the above reasons still hold?
Let's take stock.
Client-side Validation
First, there is the question of validation. For several years now, HTML5 has provided a perfectly workable mark-up solution for input validation. As a sampling, you can:
- Use the required attribute to specify that an input should not be left blank.
- Use the email input type to get an email format enforced.
- Use a number input type to force numbers. Only use this where incrementing makes sense and no special formatting is required - in other words, credit card numbers and time components inputs are not numbers (https://www.filamentgroup.com/lab/type-number.html).
- Use the datalist attribute to provide “soft” suggestions on values.
- Use the pattern attribute to specify that an input should match any regular expression you might want.
The built-in attributes cover 99% of validations that you'll want. For the remainder, you can implement your own validations by using the JavaScript setCustomValidation
API.
If you chose to use the HTML5 validation attributes, you get the following for free:
- Forms cannot be submitted while invalid.
- Invalid fields can be styled however you want with the
:invalid
css pseudoselector. - Help text on why a specific field fails validation can be provided via the title attribute and appears in a nice pop-up on most browsers.
Although there can be some rough edges, for most use cases, the ease of using HTML5 form validators blows anything else away.
Asynchronous Submission
Now that you've accepted modern HTML form validation into your life, the question is begged: exactly how hard is it to submit a form asynchronously anyway?
It turns out that it's easy. Really really easy.
// Find all forms on the page
for (let f of document.querySelectorAll('form'))
// do the below whenever any of them submit
f.addEventListener('submit', submitViaAjax);
function submitViaAjax(e) {
// don't do the full-page submit
e.preventDefault();
// async request with config from the form
const xhr = new XMLHttpRequest();
xhr.open(e.target.method, e.target.action);
// submit form using the FromData object
xhr.send(new FormData(e.target));
}
That's it. That's all you've got to do for Ajax form submission when using the little known FormData
object (https://xhr.spec.whatwg.org/#interface-formdata). And yes, support for this approach on all modern browsers is excellent.
A fantastic benefit of this approach is that you are now able to submit forms with the full range of input types. That's right; this approach allows you to easily submit binary data using input type="file"
. The browser even properly handles multiple simultaneous uploads!
And if you're cutting-edge and prefer the newer fetch
API over XmlHttpRequest
? No sweat. You can submit FormData
as the fetch body. Easy.
That's it: Ajax submission of a form in a few simple lines of code.
Now for Some REST
It might seem like the code above will work with the form
method set to PUT or DELETE verbs out-of-the-box. Unfortunately, it won't. The reason is that e.target.method
validates values, and won't return anything but “GET” or “POST”.
The offending code is this:
xhr.open(e.target.method, e.target.action);
Fortunately, it's easy to get around this limitation by using the raw attribute value.
const { method } = e.target.attributes;
xhr.open(method.value, e.target.action);
Just like that, REST is back on the table.
Back to Reality
Okay, so now that you're nice and excited about the ease and effectiveness of this technique, let's bring things back down to earth for a bit as there are still some problems.
The Encoding Issue
When you attempt this technique for the first time, you might be surprised to see your server not accepting your sent input. This frustrating state of affairs is because FormData
always encodes submissions as multipart/form-data (https://www.w3.org/TR/html401/interact/forms.html#h-17.13.4.2) as opposed to the application/x-www-form-urlencoded format that's the default for HTML forms. This, in itself, is not a problem. It can be argued that multipart/from-data is a more appropriate format for forms to begin with. This very thing was argued relatively early in the evolution of forms and the current default seems, like many things, to be a result of historical coincidence. In fact, if your form includes an input with type="file"
, it encodes via multipart/form-data anyway. That's right, standards can really be unnecessarily confusing.
That's right, standards can really be unnecessarily confusing.
The problem is that despite its obvious usefulness, many server-side Web frameworks don't have multipart/form-data model binding built in by default. Chief among these are NodeJs's Express server and ASP.NET MVC/Web API (others, like Ruby on Rails and Elixir's Phoenix, work just fine). In both cases, it's a fairly straightforward affair to drop down a level and read the request directly, for example using ReadAsMultipartAsync
, or to install a third-party ModelBinder
, such as NodeJs's Multer (https://www.npmjs.com/package/multer):
const upload = require('multer')({})
const app = express();
app.post('/register', upload.array(), req =>
console.log("response", req.body);
)
Or you can use the .NET MultipartDataMediaFormatter
(https://www.nuget.org/packages/MultipartDataMediaFormatter). This is available on NuGet and can be wired up globally by simply adding the following during application startup:
var cfg = GlobalConfiguration.Configuration;
cfg.Formatters.Add(
new FormMultipartEncodedMediaTypeFormatter(
new MultipartFormatterSettings()
)
);
Working It into Your Architecture
Of course, now that you're submitting forms asynchronously, it's necessary to figure out how to work it into application architectures. For example, submitting data to the server isn't usually enough; you need to know when and if the request succeeds. You can use Fetch API or a slightly more mature XMLHttpRequest
implementation to convert things to a promise.
const r = new XMLHttpRequest();
const { attributes, action } = e.target;
r.open(attributes.method.value, action);
r.send(new FormData(e.target));
const isJson = /^application\/json;/
const sending =
new Promise((resolve, reject) => {
const ct = r.getResponseHeader('Content-Type')
const result = r.response && isJson.test(ct)
? JSON.parse(r.response) : r.response;
const response = () => ({ xhr: r, result });
r.addEventListener('load', () =>
r.status < 200 || r.status >= 300
? reject(response())
: resolve(response())
);
r.addEventListener('error', () =>
reject(response())
)
})
Now that you have a promise in the sending
variable, you need to get this to the rest of your application. The specifics depend on how your application is structured. You could use a ReactJs higher-order component, or an AngularJs directive could trigger an event on the scope or invoke a callback in a property, or you could do nothing at all - simply wire up some global way to handle errors and successes.
Although this technique is simple, natural, and can replace a significant amount of boilerplate, it's not something that you drop in and that starts working out-of-the-box in a real situation. You have to put in the effort and figure out how it might best fit into your specific application. And ultimately, that's what most programming techniques should be.
You have to put in the effort to figure out how it might best fit into your specific application.
Cautions
This brings me to final notes and cautions.
First, there's browser support for FromData
. It's quite excellent but, as always, make sure to check. Don't wait until the final months of your project to verify browser requirements with clients; discuss requirements up front to know what techniques are available to you.
Next, you might notice that this technique relies fairly heavily on HTML5 validation. HTML5 validation attributes are powerful and robust, but their abilities are ultimately limited to what is implemented natively. It's conceivable that you might want validations that are more complex, for example, having different required fields depending on the selected country. One or two such instances may be reasonably hacked around with the helpful setCustomValidation
backdoor, but at some point, the simplicity of using forms is lost to too many custom validation hooks. At that point, you might consider falling back to the various more involved techniques, such as model-binding, Redux, or JavaScript validation frameworks.
A related issue; because there's no model-binding with forms, this technique is not ideal for situations where that would be a good idea. As-you-type calculators, forms with a lot of areas that expand and contract based on selections, or anything with a decent amount of business logic in the form's user interface, are likely to see problems with the FormData
approach. Again, it's perfectly possible to hack around, but you're losing hard-won simplicity. If your business requirements are best satisfied with JavaScript managing user inputs, then by all means do that!
Wrapping Up
Finally, if you feel as if you're unable or unwilling to make the requisite decisions, to look at your requirements and find a way to get HTML forms working with the framework of your choice, it might not be for you. There's good guidance available for Angular, Knockout, React, and others of their ilk, and although the FormData
technique can work perfectly well with or without any of these, it does necessarily violate some of their most promoted patterns.
Ultimately, forms are a very natural fit to the Web and have been a part of it since nearly the very beginning. They're excellent for their simplicity, and they're straightforward and well understood. However, the simplicity that is their strong suit is also their limitation. Forms should always be considered and, like all programming techniques, decided upon a case-by-case basis.