Hooking into Sitecore WFFM Validation Rules Using JavaScript

Introduction

When it comes to validating a form on a website, we need to think about its application in multiple places. It's favoured to have form validation on both the client and the server as to enforce data integrity and security. It's likely that the validation rules applied in the frontend will differ to those applied in the backend. Reasons for this could be:

  • The frontend/backend developers will apply their own rules.
  • The frontend/backend make use of a 3rd party library with its own rules.
  • Native validation support provided with DOM elements.

We want to satisfy uniformity in our apps to keep code clean and easily maintainable - so we need to get creative when it comes to form validation. It's possible to tap into the form validation - which Sitecore's WFFM module exposes - via some creative JavaScript code.

Setup

For this tutorial, I'll be using Sitecore 8.2 rev. 171121 with an installation of Web Forms For Marketers 8.2 Update-6. Go ahead and setup your local development site with these prerequisites.

We now need to configure a form on a page for us to noodle around with. Go to the home item in Sitecore (/sitecore/content/Home) and add a Form Sublayout to the page with the DataSource set to /sitecore/system/Modules/Web Forms for Marketers/Sample forms/Create an Account, the Presentation Details should look similar to the below image. Publish the Home item and check the frontend to verify that the form has been added to the page.

Image of the Form's Sublayout Configuration
Image of the Form's Sublayout Configuration

Image of the WFFM Form in the frontend
Image of the WFFM Form in the frontend

The Approach

Now that we have the form on the page, we can now get stuck in with some JavaScript. Right-click the Email input field and click on Inspect Element. You will be directed to the below HTML in dev tools.

<div class="scfEmailGeneralPanel">
    <input name="main_0$centercolumn_0$form_0E8906B399BA421EA74D277ED17A32FD$field_4FB90F035B614A54A1B8275D7C698726" type="text" id="main_0_centercolumn_0_form_0E8906B399BA421EA74D277ED17A32FD_field_4FB90F035B614A54A1B8275D7C698726" class="scfEmailTextBox">
    <span class="scfEmailUsefulInfo">Should be a real e-mail.</span>
    <span
        data-val-controltovalidate="main_0_centercolumn_0_form_0E8906B399BA421EA74D277ED17A32FD_field_4FB90F035B614A54A1B8275D7C698726" data-val-errormessage="E-mail contains an invalid address."
        data-val-display="Dynamic"
        data-val-validationgroup="form_0E8906B399BA421EA74D277ED17A32FD_submit" id="main_0_centercolumn_0_form_0E8906B399BA421EA74D277ED17A32FD_field_4FB90F035B614A54A1B8275D7C6987265D10AF7533054C39908EB25E8CB4ABDC_validator" class="scfValidator trackevent.%7b844BBD40-91F6-42CE-8823-5EA4D089ECA2%7d fieldid.%7b4FB90F03-5B61-4A54-A1B8-275D7C698726%7d inner.1"
        data-val="true"
        data-val-evaluationfunction="RegularExpressionValidatorEvaluateIsValid"
        data-val-validationexpression="^[A-Za-z0-9\.\_%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,4}$"
        style="display:none;">
            Enter a valid email address.
    </span>
</div>

That's quite a barrage on the eyes. Those attribute values are generated in the backend so we don't need to worry about where they come from. We're more concerned with a handful of the attributes on those elements which we can access using JS. There are three elements within the panel:

  • The input field itself.
  • Information Message (if any).
  • The element containing the validation rules/messages.

These elements can be accessed via JavaScript Window variables. To test this, ensure that the input element is highlighted in the Elements pane, open the JavaScript console and type:

window[$0.id].Validators[0]

This line is doing the following:

  1. window[$0.id]: Returns the node element
  2. window[$0.id].Validators: Returns an array of all the validators currently assigned to the node element
  3. window[$0.id].Validators[0]: We access the first (in this case, the only) validator bound to this element.

If you compare the element returned in Point 3 with the last <span /> in the first code snippet in this section, you will notice that they are both the same. So now we're starting to piece together a path of how these elements are linked.

Now let's add a dot (.) to the end of the line that we've typed in the console to see what else we can access (by looking through the autocomplete menu):

Accessor Result
validationexpression ^[A-Za-z0-9.\_%+-]+@[A-Za-z0-9.-]+.[A-Za-z]{2,4}$
controltovalidate main_0_centercolumn_0_form_0E8906B399BA421EA74D277ED17A32FD_field_4FB90F035B614A54A1B8275D7C698726
errormessage E-mail contains an invalid address.
validationGroup form_0E8906B399BA421EA74D277ED17A32FD_submit

We are therefore able to access the Regular Expression and the error message defined for this field (in Sitecore) in the frontend.

Let's go to the email field's respective Sitecore item to confirm this. This item can be found in the following location at /sitecore/system/Modules/Web Forms for Marketers/Sample forms/Create an Account/Necessary information/E-mail. This item references a Simple Field setting in the following location sitecore/system/Modules/Web Forms for Marketers/Settings/Field Types/Simple Types/E-mail - let's navigate to that item. You will now be able to see the Regular Expression defined in this field and compare it to that in the table above to see that they both match.

The Code

So let's write some JavaScript to validate the email field on blur. Paste and run the following in the console:

var emailField = document.getElementsByClassName("scfEmailTextBox")[0]
emailField.addEventListener("blur", function() {
  // If we call the function with this method, we are able to access the input
  // through the `this` keyword.
  var hasPassedValidation = sitecoreValidation.call(this)

  // Run some other business logic (if needed).

  // Incorporate `hasPassedValidation` with any other business logic to
  // determine the field's ultimate validation.-
})

var sitecoreValidation = function() {
  // Create an array which we will push the validation results onto.
  // The items will either be `true` or `false`.
  var validityTable = []

  // Find all validators next to the email input field.
  var sitecoreValidationEls = this.parentElement.getElementsByClassName(
    "scfValidator"
  )

  // Cast the sitecoreValidationEls HTMLCollection to an Array (to be iterable).
  var validationArr = [].slice.call(sitecoreValidationEls)

  if (validationArr.length) {
    validationArr.forEach(function(validationEl) {
      // Get the element's respective JavaScript reference on the window.
      var valItem = window[validationEl.id]

      if (valItem && valItem.validationexpression) {
        // Pass the validation expression into a RegExp object.
        var valExpression = new RegExp(valItem.validationexpression, "g")

        // Run the input field's value against the regular expression.
        var isValid = this.value.match(valExpression) != null

        validityTable.push(isValid)
      }
    }, this)
  }

  // If the array doesn't contain any falsey values, then the element has
  // passed validation, so return `true`.
  return validityTable.indexOf(false) < 0
}

We're doing a couple of things here:

  1. For the sake of this example, we're just targeting the email field directly as opposed to targeting all input elements in a generic way (which will be the preferred way, in reality).
  2. On input blur, the function sitecoreValidation will be invoked with the this keyword assigned to the input field itself.
  3. We then do a bit of DOM traversal to access any sibling elements with the CSS class name .scfValidator and cast them to the array validationArr (to be iterated).
  4. Using the validationArr elements array, we run some queries on the window to see if any validation expressions are assigned to that element. If there are any validation expressions available to us, we then: 1. Initialise a RegExp object, passing the expression into the constructor. 2. We run the current input fields' value against the RegExp object created above. 3. We push the result into the validationArr array.
  5. After all validations have been queried, we then check if there are any falsey values in the array and consequently return the result to the invoker.
  6. We can then run additional business logic, such as custom UI updates or query a service which can be combined with the input's final validation result.

Conclusion

Using this method, we are able to centralise rules and messages in one place for easier development and maintenance as there is one single point of truth.

Comments