Form Validation Patterns
Completing forms can be challenging, especially on mobile devices. We, therefore, want to be as clear as possible when requesting data from our users and, if there is an error, present feedback to the user in the context of the error. A number, perhaps even most, backend framework comes with a form of form validation that makes it easy to accomplish this. One does not always want to reach for a big framework simply to have this capability.
Personally, I have run into this a number of times over the last couple of months with a stack that is typically,
- Expressjs on the back-end
- HTML(Pug), CSS(Sass) and minimal flavorless JavaScript on the front-end
Uncertainty and frustration are two experiences we want to avoid for our users. Whenever we ask a user to provide us with data the onus is on us to ensure that we provide consistent and clear feedback to the user in the context of the problem. After some experimentation, I have come to a pattern that I feel works really well and thought I would share my pattern and see what the wider community thinks, and what others have come up with to solve the same problem.
tl;dr - It’s all about naming conventions
When this aspect of the user experience is not addressed, it is generally not done out of malice or simply because developers and designers do not care about the user experience. It is more often than not, because of time pressure and, sometimes, it is done because: "We will come back and do it better later". We are all guilty of that last one.
As with accessibility and writing tests, if we can make this something that simply slots into our workflow, everybody wins.
The pieces of the puzzle
Let us look at what we want to achieve then.
[[screenshot of final form]]
The above screenshot then shows the form with:
- The errors in the context of the field or fields they apply to
- A clear and concise message
The front end
This is where we start our journey and where solid naming conventions are going to make all the difference. For our example we will use the following form:
Next, we need to add the elements that will display our error messages. Seeing that we want to present this information in context, we will add our container elements just below the relevant input.
The first field to address is the title
field. We are using the id
attribute of our element here to do double duty.
First of all, it provides an easy way to get to the element from JavaScript using document.getElementById
, and
secondly, it gives us some basic information with regards to the field. We will also always append -error
to all of
these for consistency. Lastly, we add some classes we can use to style our messages.
Let’s proceed to the remaining fields of the form.
I want to take a moment to highlight a difference with the id
and name
attributes of the last field here. Here we
have a name that is not a single word but two words separated by a hyphen. In JavaScript, the convention is to use
camelCase
for variable names that are a combination of two or more words. We, therefore, do the same here and use
contactNumber-error
as the value for our id
and contactNumber
as the value for the name
attribute.
Now our form is set-up and ready. Our next step is to handle the submit
event of our form.
Submitting the Form
Go ahead and open the main.js
file located at /static/js/main.js
. We are going to start this off by wrapping all the
code we will need inside an IIFE and indicate that we opt into
JavaScript’s strict mode.
Next, we need to get out form DOM node and add an event listener so we can override the browser’s default behavior and handle form submission ourselves.
... "use strict"; const contactUsForm = document.getElementById("contact-us"); contactUsForm.addEventListener("submit", (event) => { event.preventDefault(); }); ...If you try to submit the form at this point, absolutely nothing will happen other than the built-in browser validation for the fields marked as required, as well as any fields where the content does not match the type of content we specified. For example the email field.
Assuming the user has entered all of the data correctly and proceeds to submit the form, we need a way to capture all of the data and then send it off to the server for processing.
Getting the FormData
There is a variety of ways to get the data that has been entered into the form but, for the purposes of this post and
because it is honestly the cleanest and simplest way to accomplish this, I am going to use the FormData
JavaScript
interface. Even though I have only
relatively recently discovered this interface, it has been available for a surprisingly long time with support as far
back as Internet Explorer 10.
This, combined with one more interfaceURLSearchParams, makes it very simple to mimic what a plain form submit would do in terms of the format of the data sent to the server. This is great because it means whether the form was submitted through JavaScript, or directly should JavaScript be disabled in the browser, our server code will happily handle both scenarios.
Add the following code to the event listener:
... event.preventDefauly(); let formData = new URLSearchParams(new FormData(contactUsForm)).toString();That is it. We can see what our data will look like by logging out the value of formData
. Add the following just after
the last line from above:
Startup the bundled server with npm start
and open up http://localhost:3000
in your browser. Also, open the
dev-tools(Cmd+Option+i/Ctrl+Alt+i) and switch to the console
tab so you can see the output of our log
statement. You
should see a pretty ugly but fully functional form. To ease the testing of forms and save you a lot of typing I would
recommend installing the Form Filler extension by Hussein Shabbir.
With all of this in place, go ahead and use the extension to fill in all of the form fields and click Submit
. In your
dev-tools console you should see something like the following:
Nice! Now let’s get this sent over to the server.
Posting the data to the server
Now, we could use the Fetch
API here but, support is not quite as far back as for the other APIs and interfaces we
have used so far, and it is still marked as experimental on MDN Web
Docs so, we are to stick with good
old Ajax for this one.
To help you along and save some typing, add the following two helper functions just above the addEventListener
line:
Replace our console
statement with the following:
Next we need to set the content type appropriately so that the server knows how to handle and parse the post:
ajaxRequest.setRequestHeader( "Content-Type", "application/x-www-form-urlencoded" );And now we are ready to send the data:
ajaxRequest.send(formData);Now that the data has been sent, we need to get the response:
getAjaxResponse(ajaxRequest).then(response => { console.log(data); }At this point we will get an error when we try to submit the form as nothing has been set-up on the server-side. You can confirm this by filling in the form and clicking on submit. This will result in a 404 error such as:
uncaught exception: Ajax error: 404...Time to handle our post data.
Handling the post on the server
Open up the Routes file at routes/router.js
. Currently we have only one route configured which is the one that serves
up the landing page with the form. We next need to add a route to handle the form post. If you look back at the
initAjaxRequest
you will notice that the url
portion was set to /contactus
and the method set to post
. Let’s add
a route to handle this:
The above route will handle post requests sent to /contactus
and respond with JSON
, in this case an empty object.
Simple right? Save the file, stop the server and start it up again to pick up the route change. Reload the form, fill
the field and click submit. This time, you should not get an error but instead, see the following:
It Works! Let’s log out our form data and have a look at what we get. As POST
data is added to the body
of the
request, we can get it from our request
object via req.body
. Inside our new route, before the res
add the
following:
Stop and start the server and click submit on the form. In your terminal you should now see your form data:
{ title: 'Quo amet quis dolor', surname: 'Fitzpatrick', name: 'Jillian Hopkins', email: 'zofeqigim@mailinator.com', contactNumber: '+1 (276) 129-1368', address: 'Est elit veniam of' }And it is already formatted as an object which means, you can get at the individual properties using dot notation, for example:
console.log(req.body.title); console.log(req.body.contactNumber);Which would output something like:
Quo amet quis dolor +1 (276) 129-1368Let’s do some validation.
At the root of the project create a new folder and call it utils
. Inside this new folder create a new file called
validate.js
and add the following to get us started:
Here we create and export a validate
object that we can then require
when we want to do some validation. Next, we
add a function to handle the contact form as follows:
Before we go any further, let’s call this function in our form route and confirm that it works as expected. Open up
router.js
again and add the following line after the line that requires express:
Instead of returning the empty object, let’s return the result of calling our validator.
res.json({ valid: validate.contactUs(req.body) });Stop and start your server and resubmit the form. This time you should see the following in your console:
XHR POSThttp://localhost:3000/contactus [HTTP/1.1 200 OK 16ms] {"valid":true} main.js:52:15Now that we know that all the pieces work together we can implement the actual validation.
We will start by simply ensuring that none of our fields are empty. Add the following to the contactUs
function inside
validate.js
:
We use Object.keys
to turn our object’s properties, our form field names, into an Array
and then loop over them
using forEach
. For each of our form fields we get the input, strip of any white space, and then confirm that what the
user submitted is not an empty string.
If it is, we push the field name onto the Array
of invalid fields, and mark the form is invalid by setting valid
to
false. Once the loop is complete, we return our results in the following form:
Now, back in our router, we need to change what we pass to res.json
as follows:
As we are returning an Object from our validator, we can simply return its return value. To test our validation we are
going to tell the browser to not validate on submit by adding the novalidate
attribute to the form element as follows: