Validation

Should prototypes have validation?

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

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

I recommend the following approach:

• Level 1 – Branching
• Level 2 – Passing data
• Level 3 – Conditional logic
• Level 4 – Error validation

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 information in the input field or select a radio they get an error message.

A screenshot

However, this approach 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.

Back