Let’s state that, without any further ado, dealing with HTML forms is boring. For some time, when a full-page refresh was still acceptable to end users, forms were relatively simple to work with: just collect data and post. Today, validation is required on the client and the server, and posting should happen via Ajax which requires some additional script. As a result, many repetitive and boilerplate tasks such as layout, validation, submission and display of the response are on the list for each and every form your application may need.
Today, it’s a point of honor for most web frameworks to streamline the process by handling many of the inevitable chores. Angular, for example, does a good job with forms through the FormsModule and the NgForm directive. In this article, I’ll focus on what you can do to enforce the logic behind an HTML form before it can be submitted to the server. There are many little things that can make the user experience a bit smoother and even more pleasant. I’ll incorporate some of them into a small vanilla-JS framework you are welcome to use as-is or, if you like it, integrate into your own solutions.
Improving the Usability of HTML Forms
Whatever you can do through the sapient use of JavaScript and CSS styles will improve the experience of HTML forms for the user. CSS styles have only a cosmetic effect on the view being displayed to users—but graphics are quite important. JavaScript, adds additional behavior that the default implementation of HTML forms in browsers doesn’t provide. Some aspects of HTML forms that can be improved to smooth the friction of input fields and submit buttons are:
- A coherent experience across browsers for common input fields such as those that accept dates and numbers
- Prompt notification of when the form’s status has changed, i.e., when submit is valid
- An easy to express validation layer that runs on the client and occasionally interacts with the server to perform remote validation of some fields being accepted
- A compact and effective way to post the content of the form to some remote endpoint and an equally simple way for the developers to display the response of the post
In this article, I’m going to cover the first two aspects leaving the other two for a successive article. It is worth noting that the Angular FormsModule addresses the same concerns through the use of an advanced syntax backend supported by a gigantic and comprehensive framework.
Coherent Experience for Input Fields
HTML5 comes with a long list of new input types, but the implementation that browsers provide for all of them is not coherent and is the subject of endless debates between developers and users. Consider the following piece of HTML markup:
1 |
<input type="date" /> |
In the vision of the HTML5 committee, that should give developers a universal way to accept a date within their forms. It’s only a spec, though, that browsers rendered in different graphical ways and some old browsers (most notably Internet Explorer) didn’t transpose at all. In addition, even when the specs are implemented in full compliance, it might not be the ideal way that errors are rendered and configurability is supported. Finally, there’s the point that some developers reckon that users should get the user interface of the agent they’re used to, and others that would maintain that a uniform experience is always preferable.
As far as dates are concerned, if your stand is the former then all you do is use the date
input type. Otherwise, you pick up your favorite calendar plugin and attach it to a plain text
input field. For what it’s worth, my personal stand is the latter. Here’s the code I love to use when I need to grab a date from within a form.
1 2 3 4 5 |
if (isMobile) { $("#checkin").attr("type", "date"); } else { $("#checkin").datepicker(); } |
I tend to distinguish the behavior between truly mobile devices and desktop browsers. On mobile devices, I’d go for the native calendar control. On laptops, instead, I’d go for a unified experience through some calendar plugin. To do mobile detection, I suggest you look into WURFL.JS, a free service that does a good job of detecting mobile devices. For more information, see http://wurfl.io.
A point to keep in mind is that no validation is applied consistently to entered values. While the browser’s user interface generally doesn’t let you enter patently invalid dates, it is still possible for the user to type or paste text into the field that doesn’t match a valid date and in the range of acceptable dates, you may have configured through the min and max attributes. In other words, you may get browser-led error messages (outside your programmatic control), but you might want to check that provided values are those you expect. To make a long story short, you can’t completely trust the browser’s implementation of date inputs. With a calendar plugin, instead, you have a lot more control.
A similar story can be told for numbers. In fact, I suggest using a similar script validation for numbers to guarantee that nothing but digits are entered and that they are within the given range of values.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
$("input[data-digits-only]") .on("keypress", function(event) { if (event.charCode < 48 || event.charCode > 57) { event.preventDefault(); return false; }}) .on("keyup", function () { var buffer = $(this).val(); var maxLength = parseInt($(this).attr("maxlength")); if (buffer.length > maxLength) { $(this).val(""); return false; } var minVal = parseInt($(this).attr("min")); var maxVal = parseInt($(this).attr("max")); var number = parseInt(buffer); if (number < minVal || number > maxVal) { $(this).val(""); return false; } return true; }); |
The input field can be either of type number or text as it’s the attached JavaScript role to control what happens with entered data. If you make it a number, though, you have an ad hoc keyboard on mobile devices and some additional UI support on desktop browsers. The code above is attached (via jQuery in the demo) to all INPUT
elements with a custom data attribute and ensure that min and max are honored as well as the maxlength attribute (not covered by the HTML5 standard). Any non-digit character is just refused, and if more than the expected characters are typed, the buffer is automatically emptied. Users are forced to enter a valid number.
The lesson we learn from this is that beyond built-in form validation, it is only via a script that you can ensure that entered data is in the proper format.
Let’s say that you decide to go with the above approach for all your date and number input fields. How would you silently attach those handlers to all such fields in all HTML forms? Some preliminary work should be orchestrated before a form is displayed. Believe it or not, this is just what all libraries do, including the Angular form module.
Silent Configuration of the HTML Form
Let’s start from a canonical form just made of a few different input types. I’ll use Bootstrap 4 to make it look nicer. (See Figure 1.)
Here is the code for the form:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 |
<<strong>form</strong> method="post" action="..."> <div class="form-group"> <label for="username">Username</label> <input type="text" class="form-control" id="username" placeholder="User name" value="Dino"> </div> <div class="form-group"> <label for="password">Password</label> <input type="password" class="form-control" id="password" placeholder="Password"> </div> <div class="form-group"> <label for="email">Email</label> <input type="email" class="form-control" id="email" placeholder="Email address" value="user@server.com"> <small class="form-text text-muted"> We'll never share this with anyone else. </small> </div> <div class="form-group form-check"> <input type="checkbox" class="form-check-input" id="rememberme" checked> <label class="form-check-label" for="rememberme"> Remember me</label> </div> <div class="form-group"> <label for="age">My age</label> <input type="range" class="form-control-range" id="age"> </div> <button type="button" class="btn btn-primary">Submit</button> </<strong>form</strong>> |
Whether you want to attach some ad hoc configuration to a form or some individual input elements, you must first find a way to easily select them. CSS selectors are an excellent approach. By adding a custom, even empty, CSS class to the form, you make it simpler and, more importantly, general for developers to attach some script code to initialize the form in the page.
1 |
<<strong>form</strong> class="ybq-form"> |
Now you can append a script file at the bottom of the file, or bound to document.ready
if you use jQuery, that hooks up the form and manipulates it in a way that is completely transparent to users and even other developers. Let’s say you create a ybq-forms.js file with the following content:
1 2 3 |
$(".ybq-form").each(function () { ... }); |
The code in the each
repeater depends on the custom behavior you want to enable on the form. For example, you can add a mechanism to detect whether the original content of the form has been changed by the user. The first thing to do is store the current value of each input field. Angular and other MVVM frameworks do this through the JavaScript artifact (or TypeScript class) they use to bind data to the form. In a vanilla-JS solution, you can use a custom data
attribute on each input field.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
$(".ybq-form").each(function () { var form = $(this); form.find("input").each(function () { __preserveOriginalValue($(this)); }); }) // Add data-attributes with original values function __preserveOriginalValue(field) { field.data("orig", __getCurrentValue(field)); } function __getCurrentValue(field) { if (field.prop("checked")) return true; else return field.val(); } |
After finding all INPUT
elements within the form, the code iterates and adds a data-orig
custom attribute set to the current value of the field. Note that the jQuery val()
function doesn’t return Boolean, so some additional work is required to support checkboxes and possibly other specific types.
Adding a User Interface to Show Pending Changes
Another necessary step consists of adding some user interface elements that will be responsible for showing the current status of form, in other words, whether it has pending changes. The structure of this user interface is entirely up to you, but in some way, a reference to it must be communicated to, or discoverable by, the script. A simple way to achieve this is by assigning a CSS class to the container of the user interface. The DIV
below is then the container of any user interface message from the form.
1 2 3 4 |
<<strong>form</strong> class="ybq-form"> <div class="ybq-form-header"></div> ... </<strong>form</strong>> |
What you put in the DIV
depends on how sophisticated you want the form to be. At the very minimum, the DIV
will show a message that tells about the changed or pristine state of the form. However, you can add a timer that counts the time the user spends on the form and even a button to reset the state of the input fields to the original values. The changed/pristine state of the form will reasonably affect the state of the submit button(s). In the end, the script will be extended with a new function.
1 2 3 4 5 6 7 8 9 10 |
function __stateHasChanged(form, state) { var header = form.find(".ybq-form-header"); if (state) { header.html("CHANGED").addClass("bg-warning"); form.find(".ybq-form-submit").removeAttr("disabled"); } else { header.html("NO CHANGES PENDING").removeClass("bg-warning"); form.find(".ybq-form-submit").attr("disabled", "disabled"); } } |
The function is invoked as first thing in the initialization script.
1 2 3 4 5 6 7 8 9 10 |
$(".ybq-form").each(function () { var form = $(this); __stateHasChanged(form, false); form.find("input").each(function () { __preserveOriginalValue($(this)); }); // Timer to detect changes here ... }) |
The header bar at the top of the form will be styled as dictated by the custom ybq-form-header
class. However, the style changes to Bootstrap’s bg-warning
state when the state of the form becomes changed and will be restored if the state returns to pristine. The final step is finding a way to detect changes. There’s not just one way to do it, but the one I’ve chosen here is adding a timer. Fired every one or two seconds, the timer may serve two purposes. One is checking that the values in the various input fields are different from the original values. The other is calculating the time the user has spent on the form.
1 2 3 4 5 6 7 8 9 10 |
window.setInterval(function() { __stateHasChanged(form, false); form.find("input").each(function() { if (__isChanged($(this))) { __stateHasChanged(form, true); return false; } }); }, 1000); |
Take a quick look at how the __isChanged
helper function being used. It invokes the __getCurrentValue
defined earlier and checks its value against the pristine value stored in the data-orig attribute.
1 2 3 4 |
function __isChanged(elem) { var outcome = __getCurrentValue(elem) != elem.data("orig"); return outcome; } |
Enriched with the above (and silently attached) script, the relatively scanty original form will now look like the one in Figure 2.
If you compare Figure 1 and Figure 2, you will see that state of the button is different if the form contains changes to be posted to the server.
Summary
This article identified four aspects that, if improved, would produce more user-friendly HTML forms, even in ASP.NET or any vanilla-JS programming environments. Type-specific input fields and detection of changes in the form are covered in the article while form validation and posting will be covered in a future column.
Related to the problem of detecting changes in the form, is the problem of validation that also the Forms module of Angular addresses. If there’s a way to validate the content being posted, in fact, then the form should not post any invalid content. Validation, though, is a more delicate matter that is easier to deal with in an overall MVVM model. Some good results, however, can be obtained even with vanilla-JS within ASP.NET or maybe PHP web pages, but you’ll read about it in the next article.
Load comments