Validation

Should prototypes have validation?

This has long been a debate among interaction designers and other DDaT staff in government. My personal opinion is that at least, the prototype should be as close to the real service as possible.

Yes, there are cases where such high fidelity is not required; having said that if your plan is to add usability testing to your user research you have to include different levels of fidelity.

Branching should be the first level, passing data should be the second, conditional logic should be the third and finally error validation should be the last.

Working on the NCTS phase 5 prototype we have used the HTML form validation for empty fields.

 <form class="form" method="post">

      <span class="govuk-caption-l"></span>
      <fieldset class="govuk-fieldset" data-required data-error=" Enter the Guarantee Reference Number" aria-required="true">
      <div class="govuk-form-group">
        <h1 class="govuk-label-wrapper">
          <label class="govuk-label govuk-label--l" for="guaranteeReference">
            What is the Guarantee Reference Number (GRN)?
          </label>
        </h1>
        <div id="guarantee-hint" class="govuk-hint">
          This can be up to 24 characters long and include both letters and numbers. For example, 01GB01234567890123A1B2C3.
        </div>
        <p>You must make sure this reference is up to date.</p>
        <input class="govuk-input govuk-input--width-20" id="guaranteeReference" name="guaranteeReference" type="text" aria-describedby="guaranteeReference-hint">
      </div>
      </fieldset>

      <div class="govuk-button-group govuk-!-margin-top-3">
        <button class="govuk-button" data-module="govuk-button">
          Save and continue
        </button>
      </div>

    </form>

Simply put, if the user does not enter or select a radio gets an error message.

A screenshot

Now even thought this approach is okay, it requires javaScript injected in the /javascripts folder of the project, named validation.js. You can copy the code below, or download it via the link on the related content.

// Config
const defaultErrorHeading = "There is a problem";
const defaultErrorDescription = "Check the following";
const defaultErrorMessage = "There is an error";

function clearValidation() {
  $(".govuk-error-summary").remove();

  $(".govuk-form-group--error").each(function () {
    $(this).removeClass("govuk-form-group--error");
  });

  $(".govuk-error-message").each(function () {
    $(this).remove();
  });

  $(".form-group-error").each(function () {
    $(this).removeClass("form-group-error");
  });
}

function checkTextFields(errors) {
  $(document)
    .find('input[type="text"],input[type="password"], textarea')
    .each(function () {
      var $fieldset = $(this).parents("fieldset");
      var label = $(this)
        .parent()
        .find("label")
        .clone()
        .children()
        .remove()
        .end()
        .text();

      if (
        $fieldset.attr("data-required") !== undefined &&
        $(this).val() === "" &&
        !$(this).parent().hasClass("js-hidden")
      ) {
        if ($(this).attr("id") === undefined) {
          $(this).attr("id", $(this).attr("name"));
        }

        errors.push({
          id: $(this).attr("id"),
          name: $(this).attr("name"),
          errorMessage: $fieldset.attr("data-error") || defaultErrorMessage,
          label: label,
          type: "text, password",
        });
      }
    });
  return;
}

function checkSelectors(errors) {
  var checked = [];

  $(document)
    .find('input[type="radio"], input[type="checkbox"]')
    .each(function () {
      var $fieldset = $(this).parents("fieldset");
      var label = $fieldset
        .find("legend")
        .clone()
        .children()
        .remove()
        .end()
        .text();

      if (
        $fieldset.attr("data-required") !== undefined &&
        $fieldset.find(":checked").length === 0
      ) {
        if ($(this).attr("id") === undefined) {
          $(this).attr("id", $(this).attr("name"));
        }

        if (checked.indexOf($(this).attr("name")) < 0) {
          checked.push($(this).attr("name"));
          errors.push({
            id: $(this).attr("id"),
            name: $(this).attr("name"),
            errorMessage: $fieldset.attr("data-error"),
            label: label,
            type: "text, password",
          });
        }
      }
    });
}

function appendErrorSummary(errors) {
  console.log(errors);
  var summaryNotPresent = $(document).find(".govuk-error-summary").length === 0;
  var errorHTML = errors.reduce(
    (res, error) =>
      res + `<li><a href="#${error.id}">${error.errorMessage}</a></li>`,
    ""
  );
  var summary =
    '<div class="govuk-error-summary" role="alert" aria-labelledby="error-summary-title" tabindex="-1" data-module="govuk-error-summary">' +
    '<h2 class="govuk-error-summary__title" id="error-summary-title">' +
    defaultErrorHeading +
    "</h2>" +
    '<div class="govuk-error-summary__body">' +
    '<ul class="govuk-list govuk-error-summary__list">' +
    errorHTML +
    "</ul>" +
    "</div>" +
    "</div>";
  if (summaryNotPresent) {
    $("form").before(summary);
  }
}

function appendErrorMessages(errors) {
  for (var i = 0; i < errors.length; i++) {
    const error = errors[i];
    const $input = $(document).find(`#${error.id}`);
    var $formgroup = $input.parents(".govuk-form-group");
    console.log($input);
    console.log($formgroup);
    if ($formgroup.hasClass("govuk-form-group--error")) {
      break;
    }

    $formgroup.addClass("govuk-form-group--error");

    if (
      $formgroup.find('input[type="text"], input[type="password"]').length >
        0 ||
      $formgroup.find("textarea").length > 0
    ) {
      if ($formgroup.find(".form-date").length > 0) {
        $formgroup
          .find(".form-date")
          .before(
            '<span class="gov-uk-error-message">' +
              error.errorMessage +
              "</span>"
          );
      } else {
        $input.before(
          '<span class="govuk-error-message">' + error.errorMessage + "</span>"
        );
      }
    } else if (
      $formgroup.find('input[type="radio"]').length > 0 ||
      $formgroup.find('input[type="checkbox"]')
    ) {
      $formgroup
        .find("legend")
        .append(
          '<span class="govuk-error-message">' + error.errorMessage + "</span>"
        );
    }
  }
}

$(document).on("submit", "form", function (e) {
  const requiredFieldsPresent = $(document).find("[data-required]").length > 0;

  clearValidation();

  if (requiredFieldsPresent) {
    const errors = [];

    checkTextFields(errors);
    checkSelectors(errors);
    console.log(errors);

    if (errors.length > 0) {
      e.preventDefault();
      appendErrorSummary(errors);
      appendErrorMessages(errors);
      $(document).scrollTop(0);
    }
  }
});

That’s all well and good if you want to use HTML but what if you are using nunjucks? Well, here comes the ExpressJS validator.

A new approach we are going to implement going forward is using the Express.js validator and the nunjucks syntax. We have already put this into practice in the Routes section, this is still a work in progress at the time of this post.

This new approach requires that you add the following requirement into your dependencies package.json file "express-validator": "^6.14.2", it rearranges alphabetically after you save the file.