Pages

Thursday, November 4, 2021

Validation Summary for Optimizely Forms

I have seen requests for a method to implement a Validation Summary for Optimizely Forms, and I dealt with one myself. If you're not familiar with the idea, a Validation Summary includes all the validation messages for form fields in a summary section for easy review. This is typically found at the top of the form after you try to submit. There is no option for this functionality out of the box with Optimizely so I put it together.

Start by listening to the formsStepValidating event

Optimizely Forms has client side events that you can subscribe to. You can read the full list of the events available on the Forms - Handling Events documentation. For the purpose of creating a Validation Summary, the event to listen to is "formsStepValidating". Optimizely includes their own flavour of jQuery with the Forms functionality, and they provide guidance on using that to subscribe to the client events in that same documentation. 

This approach to create the validation summary uses the Optimizely (Episerver) Forms jQuery and vanilla Javascript. The validation messages are collected and added to the top of the form in a summary container. The out-of-box Form Container Block includes a Form__Status field at the top, and the created summary container is added to that element. Because of the way this script works, the JS can be placed in a separate file and added to the main layout template. This approach does not require a custom Form Container Block or anything special.

The full script is in the following block. 

The rest of the article offers a little explanation to what it's doing.

[pre class="brush:js;class-name:collapse-box;"]
<script type="text/javascript">
    // vanilla js - not jQuery dependent so it works with noJS mode for Forms
    function addValidationSummary(form) {
        if (typeof form === 'undefined') { return; }
        // utilize the status message field
        var statusField = form.querySelector('.Form__Status'); // only one should exist
        var validationWrapper = statusField.querySelector('.Form__Validation__Summary') ?? document.createElement('div');
        validationWrapper.innerHTML = '';
        validationWrapper.classList.add('Form__Validation__Summary');
        var validFails = form.querySelectorAll('.ValidationFail');    // loop through failed items
        validFails.forEach(function (ele, idx) {
            // use the error field to reference the form element
            // - it's not always an input so this is more accurate
            var error = ele.querySelector('.Form__Element__ValidationError');
            var fieldName = error.dataset.fLinkedName; // use es6 dataset to access data attribute
            if (fieldName == null || fieldName.trim() == '') { return; }
            var linkedField = ele.querySelector(`[name='${fieldName}']`);
            var label = ele.querySelector('label'); // label is easy inside validation fail
            // assemble summary elements
            // using a label lets us click to go to the failed element
            var errorLabel = document.createElement('label');
            errorLabel.setAttribute('for', linkedField.id);
            errorLabel.textContent = `${label.textContent}:`;
            var errorText = document.createElement('span');
            errorText.classList.add('Form__Element__ValidationError');
            errorText.textContent = error.textContent;
            var summaryError = document.createElement('div');
            summaryError.classList.add('validation-summary-error');
            summaryError.append(errorLabel, errorText);
            validationWrapper.appendChild(summaryError);
        });
        statusField.appendChild(validationWrapper);
    }
    if (typeof $$epiforms !== 'undefined') {
        $$epiforms(function () {
            $$epiforms('.EPiServerForms').on('formsStepValidating', function (event) {
                if (!event.isValid) {
                    addValidationSummary(event.target);
                }
            });
        });
    }
</script>
[/pre]

The last part of the code adds the event listener

The form being validated is the target property of the event arg passed to the event handler. That form reference is passed as a parameter to the addValidationSummary function to constrain the summary to that form. This allows multiple forms to exist on a page with their own summaries.

[pre class="brush:js"]
if (typeof $$epiforms !== 'undefined') {
    $$epiforms(function () {
        $$epiforms('.EPiServerForms').on('formsStepValidating', function (event) {
            if (!event.isValid) {
                addValidationSummary(event.target);
            }
        });
    });
}
[/pre]

The addValidationSummary function builds the summary using Javascript

All the summary elements are built in this method using JS so no additional modification to the Form Container Block view is necessary. As mentioned, the form DOM element is a parameter to constrain the validation summary to the specific form. Also, because the validation messages can be for a variety of field types, not just inputs, the data attribute "f-linked-name" is used to get the field related to the validation message.

[pre class="brush:js;class-name:collapse-box;"]
// vanilla js - not jQuery dependent so it works with noJS mode for Forms
function addValidationSummary(form) {
    if (typeof form === 'undefined') { return; }
    // utilize the status message field
    var statusField = form.querySelector('.Form__Status'); // only one should exist
    var validationWrapper = statusField.querySelector('.Form__Validation__Summary') ?? document.createElement('div');
    validationWrapper.innerHTML = '';
    validationWrapper.classList.add('Form__Validation__Summary');
    var validFails = form.querySelectorAll('.ValidationFail');    // loop through failed items
    validFails.forEach(function (ele, idx) {
        // use the error field to reference the form element
        // - it's not always an input so this is more accurate
        var error = ele.querySelector('.Form__Element__ValidationError');
        var fieldName = error.dataset.fLinkedName; // es6 - use dataset to access data attribute
        if (fieldName == null || fieldName.trim() == '') { return; } // this probably means something went wrong
        var linkedField = ele.querySelector(`[name='${fieldName}']`);
        var label = ele.querySelector('label'); // label is easy inside validation fail
        // create summary elements
        // using a label lets us click to go to the failed element
        var errorLabel = document.createElement('label');
        errorLabel.setAttribute('for', linkedField.id);
        errorLabel.textContent = `${label.textContent}:`;
        var errorText = document.createElement('span');
        errorText.classList.add('Form__Element__ValidationError');
        errorText.textContent = error.textContent;
        var summaryError = document.createElement('div');
        summaryError.classList.add('validation-summary-error');
        summaryError.append(errorLabel, errorText);
        validationWrapper.appendChild(summaryError);
    });
    statusField.appendChild(validationWrapper);
}
[/pre]

A few additional notes

  • You can add this script to the bottom of your custom Form Container Block rendering if you already use one. The obvious advantage of that over including it in your global JS file is the script is only included when a form renders.
  • This functionality requires the form to run in JS mode. When Optimizely Forms are set to run in NonJS mode they post back to the server for validation and don't trigger the client events.
  • You must be using Optimizely Forms jQuery to use this exact code example. However, I wrote the addValidationSummary method in plain JS because I have used it like this without jQuery. 
  • I used a Label field for the summary so you could click and focus on the related element. I find this technique especially useful for long forms. 
  • You can modify this code and move the summary box wherever you'd like by changing the line for the statusField reference to use a different element.

See it in action


 

No comments:

Post a Comment

Share your thoughts or ask questions...