Validating a Single Page Multi-Step HTML Form

by Michael Szul on

No ads, no tracking, and no data collection. Enjoy this article? Buy us a ☕.

I was having a discussion over Skype with Bill the other day about validation form input when the form in question has multiple steps. I've built an insurmountable number of forms over the last two decades, and validation is always something where you keep searching to find a better solution. Bill works at Peirce College, and I consulted with them for a time when I was with Barbella Digital, Inc. (BDI). At BDI, we refreshed Peirce's admissions forms on several occasions. These were multi-step forms that didn't just capture potential student information, but also connected to Peirce's Talisma CRM, Jenzabar ICS and ERP systems, sent backup email notifications, and recorded application submissions as terminations in Peirce's lead tracking system (something else that I had a hand in building).

Needless to say, forms can get rather complicated, but validation shouldn't have to be. Peirce's forms exist as a single HTML page, but various parts are hidden as separate steps. Submission of forms such as those require validation for the current step before moving on to the next step (or finally submitting the form).

After some back and forth on best practices, I decided to throw together a sample application to see how concise I could make such a solution. I uploaded the example to GitHub in case anyone wants to follow along.

The first thing to notice is that I use the HTML5 data-* attributes for using custom attributes. This allows for valid HTML5, while offering the customization necessary to simplify the form validation. You'll see data-step as an attribute in both the form and the section elements:

<form method="POST" action="." data-step="1">
                      <section data-step="1">
                          <fieldset>
                              <legend>Personal Information</legend>
                              <label for="name">Name</label>
                              <input id="name" name="name" type="text" required="required" />
                              <label for="email">Email</label>
                              <input id="email" name="email" type="email" required="required" />
                          </fieldset>
                      </section>
                      <section data-step="2">
                          <fieldset>
                              <legend>Company Information</legend>
                              <label for="cname">Company Name</label>
                              <input id="cname" name="cname" type="text" required="required" />
                              <label for="phone">Phone Number</label>
                              <input id="phone" name="phone" type="tel" />
                          </fieldset>
                      </section>
                      <section data-step="3">
                          <fieldset>
                              <legend>Optional Information</legend>
                              <label for="reference">Reference</label>
                              <input id="reference" name="reference" type="text" />
                              <label for="feedback">Feedback</label>
                              <textarea id="feedback" name="feedback"></textarea>
                          </fieldset>
                      </section>
                      <button class="button button-outline">Back</button>
                      <input class="button button-primary" type="submit" value="Send" />
                  </form>
      

The data-step attribute in the form element is used to keep track of the current step, while the data-step attributes in the section elements are used to identify the various steps.

At this point, it's just a matter of using jQuery to hide the necessary elements, and then provide the click events to validate and navigate:

$(document).ready(function() {
          $("section[data-step]").hide();
          $("button.button-outline").hide();
          $("section[data-step=1]").show();
          $("input[type='submit']").click(function(e) {
              e.preventDefault();
              var step = $("form").data("step");
              var isValid = true;
              $("section[data-step='" + step + "'] input[required='required']").each(function(idx, elem) {
                  $(elem).removeClass("error");
                  if($(elem).val().trim() === "") {
                      isValid = false;
                      $(elem).addClass("error");
                  }
              });
              if(isValid) {
                  step += 1;
                  if(step > $("section[data-step]").length) {
                      $("form").submit(); //Submit the form to the URL in the action attribute, or you could always do something else.
                  }
                  $("form").data("step", step);
                  $("section[data-step]").hide();
                  $("section[data-step='" + step + "']").show();
                  $("button.button-outline").show();
              }
          });
          $("button.button-outline").click(function(e) {
              e.preventDefault();
              var step = $("form").data("step");
              step -= 1;
              $("form").data("step", step);
              $("section[data-step]").hide();
              $("section[data-step='" + step + "']").show();
              if(step === 1) {
                  $("button.button-outline").hide();
              }
          });
      });
      

We start by hiding the back button and all the steps except the first. The click on the submit button gets the current step, uses jQuery to gather all the form elements that have the required attribute, validates them, and then appropriately navigates to the next step, while showing the back button. Clicking the back button does the reverse, only without the validation.

You might be wondering why I'm not using the HTML5 pre-built validation. Not all browsers support the recommended functionality at this time, nor the hooks into the validation API. Also, you'll note that I'm using the XHTML version (hence required="required") of HTML5--mostly because I like well-formed code.

Simple. Clean.