Skip to main content

Your privacy settings

We use cookies to help you with journey planning and relevant disruptions, remember your login and show you content you might be interested in. If you’re happy with the use of cookies by West Midlands Combined Authority and our selected partners, click ‘Accept all cookies’. Or click ‘Manage cookies’ to learn more.

Manage Cookies
Beta

This is a new service - your feedback will help us to improve it.

Patterns

Feedback loop

About

What does it do?

  • Collects user feedback to allow measure user satisfaction, effectiveness of the page both in-terms of content and usability and capture bugs.

When to use it?

  • Every page must have feedback loop.
  • It must be placed directly above the footer.



Is this page useful? Thank you for letting us know
Thank you for letting us know. If you require urgent help, please contact our Customer Service team.

Help us improve Transport for West Midlands






HTML markup
<div class="wmnds-feedback-loop">
  <input type="hidden" name="contentType" value="content" />
  <input type="hidden" name="contentName" value="PageTitle" />
  <input type="hidden" name="contentLastEditor" value="Editor name" />
  <input type="hidden" name="serviceName" value="Transport for West Midlands Design system" />
  <input type="hidden" name="contentLastUpdated" value="2021-03-19T11:18:15.532Z" />
  <div class="wmnds-container wmnds-feedback-loop__questions">
    <div class="wmnds-feedback-loop__useful">
      <span>Is this page useful?</span>
      <button class="wmnds-btn wmnds-btn--link wmnds-m-l-sm">Yes</button>
      <button class="wmnds-btn wmnds-btn--link wmnds-m-l-sm">No</button>
      <span class="wmnds-feedback-loop__sent-msg wmnds-m-b-none">Thank you for letting us know</span>
    </div>
    <div class="wmnds-feedback-loop__wrong">
      <button class="wmnds-btn wmnds-btn--link">Is there anything wrong with this page?</button>
    </div>
    <div class="wmnds-feedback-loop__sent-msg">
      <span>Thank you for letting us know. If you require urgent help, please contact our <a class="wmnds-link" href="#">Customer Service</a> team.</span>
    </div>
  </div>
  <form class="wmnds-container wmnds-feedback-loop__form wmnds-grid">
    <input type="hidden" name="contentWrong" value="true">
    <div class="wmnds-grid wmnds-grid--align-center wmnds-grid--justify-between wmnds-m-b-md">
      <legend class="wmnds-col-auto wmnds-m-none">
        <h2 class="wmnds-h3">Help us improve Transport for West Midlands</h2>
      </legend>
      <button class="wmnds-btn wmnds-btn--link wmnds-col-auto wmnds-m-l-md">Close</button>
    </div>
    <div class="wmnds-col-1 wmnds-col-md-1-2">
      <div class="wmnds-fe-group wmnds-m-b-md">
        <label class="wmnds-fe-label" for="fb-textarea-1">What is wrong with this page?</label>
        <textarea class="wmnds-fe-textarea" id="fb-textarea-1" name="contentWrongComment" rows="3" placeholder="Text" required="true"></textarea>
      </div>
    </div>
    <br>
    <div class="wmnds-col-1 wmnds-col-md-1-3">
      <div class="wmnds-fe-group wmnds-m-b-md">
        <label class="wmnds-fe-label" for="fb-input-2">Email address (optional)</label>
        <input class="wmnds-fe-input" id="fb-input-2" name="emailAddress" type="text" value="" placeholder="Text" />
      </div>
    </div>
    <br>
    <div class="wmnds-col-1">
      <div class="wmnds-fe-group wmnds-m-b-none">
        <div class="wmnds-fe-checkboxes">
          <span class="wmnds-fe-checkboxes__desc">
          </span>
          <label class="wmnds-fe-checkboxes__container">
            <div class="wmnds-m-b-lg">I consent to being contacted about the issue I am reporting (optional)
            </div>
            <input id="fb-checkbox-consent-3" class="wmnds-fe-checkboxes__input" value="" name="consent_null" type="checkbox" />
            <span class="wmnds-fe-checkboxes__checkmark">
              <svg class="wmnds-fe-checkboxes__icon">
                <use xlink:href="#wmnds-general-checkmark" href="#wmnds-general-checkmark"></use>
              </svg>
            </span>
          </label>
        </div>
      </div>
    </div>
    <br>
    <div class="wmnds-col-1">
      <div class="wmnds-fe-group wmnds-m-b-none">
        <div class="wmnds-fe-checkboxes">
          <span class="wmnds-fe-checkboxes__desc">
          </span>
          <label class="wmnds-fe-checkboxes__container">
            <div class="wmnds-m-b-lg">I agree to <a href="#">WMCA’s Privacy Policy</a>
            </div>
            <input id="fb-checkbox-consent-4" class="wmnds-fe-checkboxes__input" value="" name="privacy_null" type="checkbox" required="true" />
            <span class="wmnds-fe-checkboxes__checkmark">
              <svg class="wmnds-fe-checkboxes__icon">
                <use xlink:href="#wmnds-general-checkmark" href="#wmnds-general-checkmark"></use>
              </svg>
            </span>
          </label>
        </div>
      </div>
    </div>
    <br>
    <button class="wmnds-btn wmnds-btn--primary wmnds-m-t-md" type="submit">
      Submit
    </button>
  </form>
</div>

Nunjucks markup
{% from "wmnds/patterns/feedback-loop/_feedback-loop.njk" import wmndsFeedbackLoop %}

{{
   wmndsFeedbackLoop({
      id: 'fb',
      formTitle: 'Help us improve Transport for West Midlands',
      isOpen: false,
      metaFields: [
        {
          name: 'contentType', 
          value: 'content'
        }, 
        {
          name: 'contentName', 
          value: 'PageTitle'
        }, 
        {
          name: 'contentLastEditor', 
          value: 'Editor name'
        }, 
        {
          name: 'serviceName', 
          value: 'Transport for West Midlands Design system'
        }, 
        {
          name: 'contentLastUpdated', 
          value: '2021-03-19T11:18:15.532Z'
        }
      ],
      formFields: [
        {
          type: 'textarea',
          name: 'contentWrongComment',
          labelText: 'What is wrong with this page?',
          placeholder: 'Text',
          required: true
        },
        {
          type: 'input',
          name: 'emailAddress',
          labelText: 'Email address (optional)',
          placeholder: 'Text'
        },
        {
          type: 'checkbox',
          name: 'consent',
          labelText: 'I consent to being contacted about the issue I am reporting (optional)'
        },
        {
          type: 'checkbox',
          name: 'privacy',
          labelHTML: 'I agree to <a href="#">WMCA’s Privacy Policy</a>',
          required: true
        }
      ]
    })
}}

Nunjucks properties
NameTypeDescription
idstringThe id of the feedback loop container.
isOpenbooleanIf true, the feedback loop form will be in the open state. Defaults to false.
isUsefulSentbooleanIf true, the feedback loop will show the usefulSentMessage as if the 'Is this page useful?' feedback has been submitted. Defaults to false.
usefulSentMessageobjectThe message shown when the 'Is this page useful?' feedback had been submitted. This message is also shown when isUsefulSent is true.
isFormSentbooleanIf true, the feedback loop will show the formSentMessage as if the feedback form had been submitted. Defaults to false.
formSentMessageobjectThe message shown when the feedback form has been submitted. This message is also shown when isFormSent is true.
metaFieldsarrayRequired Data used to populate the hidden fields that are included when both the 'Is this page useful?' feedback and the form is submitted.
formFieldsarrayRequired Fields to include in the feedback form.

Options for usefulSentMessage

NameTypeDescription
contentTextstringText shown once 'Is this page useful?' feedback is submitted. If contentHTML is provided, this argument will be ignored.
contentHTMLstringHTML shown once 'Is this page useful?' feedback is submitted. If contentHTML is provided, the contentText argument will be ignored.

Options for formSentMessage

NameTypeDescription
contentTextstringText shown once feedback form is submitted. If contentText is provided, the contentText argument will be ignored.
contentHTMLstringHTML shown once feedback form is submitted. If contentText is provided, this argument will be ignored.

Options for metaFields

NameTypeDescription
namestringRequired The key of the data to be submitted. One of contentType, contentName, contentLastEditor, serviceName or contentLastUpdated. contentType is required.
valuestringRequired The value of the data to be submitted.

Options for formFields

NameTypeDescription
typestringRequired Type of form input. One of textarea, input or checkbox.
namestringRequired The key for the data that will submitted form the input.
labelTextstringRequired Text label to be used for the input. If labelHTML is provided, this argument will be ignored.
labelHTMLstringRequired HTML to be used in the input label. If labelHTML is provided, labelText will be ignored.
requiredbooleanWhether the input is required or not. Defaults to false.
placeholderstringPlaceholder text for input. Only applies to textarea and input.

Recommended javascript (browser compatible)
"use strict";

Object.defineProperty(exports, "__esModule", {
  value: true
});
exports["default"] = void 0;
var feedbackLoopJS = function feedbackLoopJS() {
  // CONSTANTS
  var feedbackLoopClass = 'wmnds-feedback-loop';
  var isOpenClass = 'wmnds-is--open';
  var isSentClass = 'wmnds-is--sent';
  var formGroupClass = 'wmnds-fe-group';
  var formInputClass = 'wmnds-fe-input';
  var formCheckBoxesClass = 'wmnds-fe-checkboxes';
  var formTextareaClass = 'wmnds-fe-textarea';
  var formGroupErrorClass = "".concat(formGroupClass, "--error");
  var inputErrorClass = "".concat(formInputClass, "--error");
  var errorMessageClass = 'wmnds-fe-error-message';

  // Helper functions
  var nodeListToArray = function nodeListToArray(nodeList) {
    return Array.prototype.slice.call(nodeList);
  };
  var createErrorMessage = function createErrorMessage(textContent) {
    var element = document.createElement('span');
    element.classList.add(errorMessageClass);
    element.textContent = textContent;
    return element;
  };

  // Form submission functions
  var sendFeedback = function sendFeedback(data) {
    // Send data
    var url = 'https://prod-10.uksouth.logic.azure.com/workflows/8bf9c75f399246de96cf505a1331e738/triggers/manual/paths/invoke?api-version=2016-10-01&sp=%2Ftriggers%2Fmanual%2Frun&sv=1.0&sig=qSXrjDmMgHoPORSob4-uqonyJyF8mB8addKURnOhL1E';
    fetch(url, {
      method: 'POST',
      headers: {
        'content-type': 'application/json'
      },
      body: JSON.stringify(data)
    }).then(function(response) {
      return response.text();
    }).then(function(text) {
      // eslint-disable-next-line no-console
      console.log(text);
    });
  };
  var parseFormData = function parseFormData(nodeList) {
    var formData = {};
    // Add data from DOM
    formData.contentPath = document.location.pathname;
    // Add data from form fields
    var formFields = nodeListToArray(nodeList);
    formFields.forEach(function(field) {
      if (!field.value) return;
      if (field.type === 'checkbox') return;
      if (field.type === 'radio' && !field.checked) return;
      formData[field.name] = field.value;
    });
    return formData;
  };
  var sendPageUsefulFeedback = function sendPageUsefulFeedback(inputElements, isPageUseful) {
    var formData = parseFormData(inputElements);
    formData.contentHelpful = isPageUseful;
    sendFeedback(formData);
  };
  var sendSomethingWrongFeedback = function sendSomethingWrongFeedback(inputElements) {
    var formData = parseFormData(inputElements);
    sendFeedback(formData);
  };

  // Form validation functions
  var getFormGroupToValidate = function getFormGroupToValidate(form) {
    var formGroups = nodeListToArray(form.querySelectorAll(".".concat(formGroupClass)));
    var filterFunc = function filterFunc(group) {
      return group.querySelectorAll('[required="true"]').length;
    };
    var formGroupsToValidate = formGroups.filter(filterFunc);
    return formGroupsToValidate;
  };
  var showErrorState = function showErrorState(formGroup, type) {
    if (formGroup.classList.contains(formGroupErrorClass)) return;
    formGroup.classList.add(formGroupErrorClass);
    switch (type) {
      case 'checkbox': {
        var container = formGroup.querySelector(".".concat(formCheckBoxesClass));
        var errorMsg = createErrorMessage('This field is required');
        container.prepend(errorMsg);
        break;
      }
      case 'textarea': {
        var textareaElement = formGroup.querySelector(".".concat(formTextareaClass));
        var parentElement = textareaElement.parentElement;
        var _errorMsg = createErrorMessage('This field is required');
        textareaElement.classList.add(inputErrorClass);
        parentElement.insertBefore(_errorMsg, textareaElement);
        break;
      }
      case 'text': {
        var inputElement = formGroup.querySelector(".".concat(formInputClass));
        var _parentElement = inputElement.parentElement;
        var _errorMsg2 = createErrorMessage('This field is required');
        inputElement.classList.add(inputErrorClass);
        _parentElement.insertBefore(_errorMsg2, inputElement);
        break;
      }
      default:
        break;
    }
  };
  var clearErrorState = function clearErrorState(formGroup) {
    formGroup.classList.remove(formGroupErrorClass);
    // Remove error messages
    var errorMsg = formGroup.querySelector(".".concat(errorMessageClass));
    if (errorMsg) errorMsg.remove();
    // Remove error classes
    var erroredInputs = formGroup.querySelectorAll(".".concat(inputErrorClass));
    erroredInputs.forEach(function(input) {
      return input.classList.remove(inputErrorClass);
    });
  };
  var addValidationEvents = function addValidationEvents(form) {
    var formGroups = getFormGroupToValidate(form);
    // Loop through and add invalid validation
    formGroups.forEach(function(group) {
      var formElements = group.querySelectorAll('[required="true"]');
      formElements.forEach(function(element) {
        element.addEventListener('input', function() {
          return clearErrorState(group);
        });
        element.addEventListener('invalid', function(event) {
          event.preventDefault();
          showErrorState(group, element.type);
        });
      });
    });
  };

  // Set up all the feedback loops
  var feedbackLoops = document.querySelectorAll(".".concat(feedbackLoopClass));
  feedbackLoops.forEach(function(feedbackLoop) {
    var somethingWrongBtn = feedbackLoop.querySelector(".".concat(feedbackLoopClass, "__wrong > button"));
    var closeBtn = feedbackLoop.querySelector(".".concat(feedbackLoopClass, "__form legend + button"));
    var questionsRef = feedbackLoop.querySelector(".".concat(feedbackLoopClass, "__questions"));
    var contentWrongFormRef = feedbackLoop.querySelector(".".concat(feedbackLoopClass, "__form"));
    var pageUsefulRef = feedbackLoop.querySelector(".".concat(feedbackLoopClass, "__useful"));
    var pageUsefulBtns = nodeListToArray(pageUsefulRef.querySelectorAll('button'));

    // Form functions
    var clearFormErrors = function clearFormErrors() {
      var formGroups = nodeListToArray(contentWrongFormRef.querySelectorAll(".".concat(formGroupClass)));
      formGroups.forEach(function(group) {
        return clearErrorState(group);
      });
    };
    var toggleFeedbackForm = function toggleFeedbackForm() {
      var event = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : false;
      if (event) event.preventDefault();
      feedbackLoop.classList.toggle(isOpenClass);
      clearFormErrors();
    };
    var submitIsPageUseful = function submitIsPageUseful(event) {
      var inputElements = feedbackLoop.querySelectorAll('[type="hidden"]');
      sendPageUsefulFeedback(inputElements, event.target.textContent);
      pageUsefulRef.classList.add(isSentClass);
    };
    var submitContentWrongForm = function submitContentWrongForm(event) {
      event.preventDefault();
      var inputElements = feedbackLoop.querySelectorAll('textarea, input');
      sendSomethingWrongFeedback(inputElements);
      questionsRef.classList.add(isSentClass);
      toggleFeedbackForm();
    };

    // Attach form event listeners
    somethingWrongBtn.addEventListener('click', toggleFeedbackForm);
    closeBtn.addEventListener('click', toggleFeedbackForm);
    addValidationEvents(contentWrongFormRef);
    pageUsefulBtns.forEach(function(btn) {
      return btn.addEventListener('click', submitIsPageUseful);
    });
    contentWrongFormRef.addEventListener('submit', submitContentWrongForm);
  });
};
var _default = feedbackLoopJS;
exports["default"] = _default;

Recommended javascript (ES6)
const feedbackLoopJS = () => {
  // CONSTANTS
  const feedbackLoopClass = 'wmnds-feedback-loop';
  const isOpenClass = 'wmnds-is--open';
  const isSentClass = 'wmnds-is--sent';

  const formGroupClass = 'wmnds-fe-group';
  const formInputClass = 'wmnds-fe-input';
  const formCheckBoxesClass = 'wmnds-fe-checkboxes';
  const formTextareaClass = 'wmnds-fe-textarea';
  const formGroupErrorClass = `${formGroupClass}--error`;
  const inputErrorClass = `${formInputClass}--error`;
  const errorMessageClass = 'wmnds-fe-error-message';

  // Helper functions
  const nodeListToArray = nodeList => Array.prototype.slice.call(nodeList);
  const createErrorMessage = textContent => {
    const element = document.createElement('span');
    element.classList.add(errorMessageClass);
    element.textContent = textContent;
    return element;
  };

  // Form submission functions
  const sendFeedback = data => {
    // Send data
    const url =
      'https://prod-10.uksouth.logic.azure.com/workflows/8bf9c75f399246de96cf505a1331e738/triggers/manual/paths/invoke?api-version=2016-10-01&sp=%2Ftriggers%2Fmanual%2Frun&sv=1.0&sig=qSXrjDmMgHoPORSob4-uqonyJyF8mB8addKURnOhL1E';

    fetch(url, {
        method: 'POST',
        headers: {
          'content-type': 'application/json'
        },
        body: JSON.stringify(data)
      })
      .then(response => {
        return response.text();
      })
      .then(text => {
        // eslint-disable-next-line no-console
        console.log(text);
      });
  };

  const parseFormData = nodeList => {
    const formData = {};
    // Add data from DOM
    formData.contentPath = document.location.pathname;
    // Add data from form fields
    const formFields = nodeListToArray(nodeList);
    formFields.forEach(field => {
      if (!field.value) return;
      if (field.type === 'checkbox') return;
      if (field.type === 'radio' && !field.checked) return;
      formData[field.name] = field.value;
    });
    return formData;
  };

  const sendPageUsefulFeedback = (inputElements, isPageUseful) => {
    const formData = parseFormData(inputElements);
    formData.contentHelpful = isPageUseful;
    sendFeedback(formData);
  };

  const sendSomethingWrongFeedback = inputElements => {
    const formData = parseFormData(inputElements);
    sendFeedback(formData);
  };

  // Form validation functions
  const getFormGroupToValidate = form => {
    const formGroups = nodeListToArray(form.querySelectorAll(`.${formGroupClass}`));
    const filterFunc = group => group.querySelectorAll('[required="true"]').length;
    const formGroupsToValidate = formGroups.filter(filterFunc);
    return formGroupsToValidate;
  };

  const showErrorState = (formGroup, type) => {
    if (formGroup.classList.contains(formGroupErrorClass)) return;
    formGroup.classList.add(formGroupErrorClass);

    switch (type) {
      case 'checkbox': {
        const container = formGroup.querySelector(`.${formCheckBoxesClass}`);
        const errorMsg = createErrorMessage('This field is required');
        container.prepend(errorMsg);
        break;
      }

      case 'textarea': {
        const textareaElement = formGroup.querySelector(`.${formTextareaClass}`);
        const {
          parentElement
        } = textareaElement;
        const errorMsg = createErrorMessage('This field is required');
        textareaElement.classList.add(inputErrorClass);
        parentElement.insertBefore(errorMsg, textareaElement);
        break;
      }

      case 'text': {
        const inputElement = formGroup.querySelector(`.${formInputClass}`);
        const {
          parentElement
        } = inputElement;
        const errorMsg = createErrorMessage('This field is required');
        inputElement.classList.add(inputErrorClass);
        parentElement.insertBefore(errorMsg, inputElement);
        break;
      }

      default:
        break;
    }
  };

  const clearErrorState = formGroup => {
    formGroup.classList.remove(formGroupErrorClass);
    // Remove error messages
    const errorMsg = formGroup.querySelector(`.${errorMessageClass}`);
    if (errorMsg) errorMsg.remove();
    // Remove error classes
    const erroredInputs = formGroup.querySelectorAll(`.${inputErrorClass}`);
    erroredInputs.forEach(input => input.classList.remove(inputErrorClass));
  };

  const addValidationEvents = form => {
    const formGroups = getFormGroupToValidate(form);
    // Loop through and add invalid validation
    formGroups.forEach(group => {
      const formElements = group.querySelectorAll('[required="true"]');
      formElements.forEach(element => {
        element.addEventListener('input', () => clearErrorState(group));
        element.addEventListener('invalid', event => {
          event.preventDefault();
          showErrorState(group, element.type);
        });
      });
    });
  };

  // Set up all the feedback loops
  const feedbackLoops = document.querySelectorAll(`.${feedbackLoopClass}`);
  feedbackLoops.forEach(feedbackLoop => {
    const somethingWrongBtn = feedbackLoop.querySelector(`.${feedbackLoopClass}__wrong > button`);
    const closeBtn = feedbackLoop.querySelector(`.${feedbackLoopClass}__form legend + button`);
    const questionsRef = feedbackLoop.querySelector(`.${feedbackLoopClass}__questions`);
    const contentWrongFormRef = feedbackLoop.querySelector(`.${feedbackLoopClass}__form`);
    const pageUsefulRef = feedbackLoop.querySelector(`.${feedbackLoopClass}__useful`);
    const pageUsefulBtns = nodeListToArray(pageUsefulRef.querySelectorAll('button'));

    // Form functions
    const clearFormErrors = () => {
      const formGroups = nodeListToArray(
        contentWrongFormRef.querySelectorAll(`.${formGroupClass}`)
      );
      formGroups.forEach(group => clearErrorState(group));
    };

    const toggleFeedbackForm = (event = false) => {
      if (event) event.preventDefault();
      feedbackLoop.classList.toggle(isOpenClass);
      clearFormErrors();
    };

    const submitIsPageUseful = event => {
      const inputElements = feedbackLoop.querySelectorAll('[type="hidden"]');
      sendPageUsefulFeedback(inputElements, event.target.textContent);
      pageUsefulRef.classList.add(isSentClass);
    };

    const submitContentWrongForm = event => {
      event.preventDefault();
      const inputElements = feedbackLoop.querySelectorAll('textarea, input');
      sendSomethingWrongFeedback(inputElements);
      questionsRef.classList.add(isSentClass);
      toggleFeedbackForm();
    };

    // Attach form event listeners
    somethingWrongBtn.addEventListener('click', toggleFeedbackForm);
    closeBtn.addEventListener('click', toggleFeedbackForm);
    addValidationEvents(contentWrongFormRef);
    pageUsefulBtns.forEach(btn => btn.addEventListener('click', submitIsPageUseful));
    contentWrongFormRef.addEventListener('submit', submitContentWrongForm);
  });
};

export default feedbackLoopJS;


Open


Is this page useful? Thank you for letting us know
Thank you for letting us know. If you require urgent help, please contact our Customer Service team.

Help us improve Transport for West Midlands



HTML markup
<div class="wmnds-feedback-loop wmnds-is--open">
  <input type="hidden" name="contentType" value="content" />
  <input type="hidden" name="contentName" value="PageTitle" />
  <input type="hidden" name="contentLastEditor" value="Editor name" />
  <input type="hidden" name="serviceName" value="Transport for West Midlands Design system" />
  <input type="hidden" name="contentLastUpdated" value="2021-03-19T11:18:15.532Z" />
  <div class="wmnds-container wmnds-feedback-loop__questions">
    <div class="wmnds-feedback-loop__useful">
      <span>Is this page useful?</span>
      <button class="wmnds-btn wmnds-btn--link wmnds-m-l-sm">Yes</button>
      <button class="wmnds-btn wmnds-btn--link wmnds-m-l-sm">No</button>
      <span class="wmnds-feedback-loop__sent-msg wmnds-m-b-none">Thank you for letting us know</span>
    </div>
    <div class="wmnds-feedback-loop__wrong">
      <button class="wmnds-btn wmnds-btn--link">Is there anything wrong with this page?</button>
    </div>
    <div class="wmnds-feedback-loop__sent-msg">
      <span>Thank you for letting us know. If you require urgent help, please contact our <a class="wmnds-link" href="#">Customer Service</a> team.</span>
    </div>
  </div>
  <form class="wmnds-container wmnds-feedback-loop__form wmnds-grid">
    <input type="hidden" name="contentWrong" value="true">
    <div class="wmnds-grid wmnds-grid--align-center wmnds-grid--justify-between wmnds-m-b-md">
      <legend class="wmnds-col-auto wmnds-m-none">
        <h2 class="wmnds-h3">Help us improve Transport for West Midlands</h2>
      </legend>
      <button class="wmnds-btn wmnds-btn--link wmnds-col-auto wmnds-m-l-md">Close</button>
    </div>
    <div class="wmnds-col-1 wmnds-col-md-1-2">
      <div class="wmnds-fe-group wmnds-m-b-md">
        <label class="wmnds-fe-label" for="fb-open-textarea-1">What would make this page more useful?</label>
        <textarea class="wmnds-fe-textarea" id="fb-open-textarea-1" name="contentWrongComment" rows="3" placeholder="Text" required="true"></textarea>
      </div>
    </div>
    <br>
    <button class="wmnds-btn wmnds-btn--primary wmnds-m-t-md" type="submit">
      Submit
    </button>
  </form>
</div>

Nunjucks properties
NameTypeDescription
idstringThe id of the feedback loop container.
isOpenbooleanIf true, the feedback loop form will be in the open state. Defaults to false.
isUsefulSentbooleanIf true, the feedback loop will show the usefulSentMessage as if the 'Is this page useful?' feedback has been submitted. Defaults to false.
usefulSentMessageobjectThe message shown when the 'Is this page useful?' feedback had been submitted. This message is also shown when isUsefulSent is true.
isFormSentbooleanIf true, the feedback loop will show the formSentMessage as if the feedback form had been submitted. Defaults to false.
formSentMessageobjectThe message shown when the feedback form has been submitted. This message is also shown when isFormSent is true.
metaFieldsarrayRequired Data used to populate the hidden fields that are included when both the 'Is this page useful?' feedback and the form is submitted.
formFieldsarrayRequired Fields to include in the feedback form.

Options for usefulSentMessage

NameTypeDescription
contentTextstringText shown once 'Is this page useful?' feedback is submitted. If contentHTML is provided, this argument will be ignored.
contentHTMLstringHTML shown once 'Is this page useful?' feedback is submitted. If contentHTML is provided, the contentText argument will be ignored.

Options for formSentMessage

NameTypeDescription
contentTextstringText shown once feedback form is submitted. If contentText is provided, the contentText argument will be ignored.
contentHTMLstringHTML shown once feedback form is submitted. If contentText is provided, this argument will be ignored.

Options for metaFields

NameTypeDescription
namestringRequired The key of the data to be submitted. One of contentType, contentName, contentLastEditor, serviceName or contentLastUpdated. contentType is required.
valuestringRequired The value of the data to be submitted.

Options for formFields

NameTypeDescription
typestringRequired Type of form input. One of textarea, input or checkbox.
namestringRequired The key for the data that will submitted form the input.
labelTextstringRequired Text label to be used for the input. If labelHTML is provided, this argument will be ignored.
labelHTMLstringRequired HTML to be used in the input label. If labelHTML is provided, labelText will be ignored.
requiredbooleanWhether the input is required or not. Defaults to false.
placeholderstringPlaceholder text for input. Only applies to textarea and input.

Recommended javascript (browser compatible)
"use strict";

Object.defineProperty(exports, "__esModule", {
  value: true
});
exports["default"] = void 0;
var feedbackLoopJS = function feedbackLoopJS() {
  // CONSTANTS
  var feedbackLoopClass = 'wmnds-feedback-loop';
  var isOpenClass = 'wmnds-is--open';
  var isSentClass = 'wmnds-is--sent';
  var formGroupClass = 'wmnds-fe-group';
  var formInputClass = 'wmnds-fe-input';
  var formCheckBoxesClass = 'wmnds-fe-checkboxes';
  var formTextareaClass = 'wmnds-fe-textarea';
  var formGroupErrorClass = "".concat(formGroupClass, "--error");
  var inputErrorClass = "".concat(formInputClass, "--error");
  var errorMessageClass = 'wmnds-fe-error-message';

  // Helper functions
  var nodeListToArray = function nodeListToArray(nodeList) {
    return Array.prototype.slice.call(nodeList);
  };
  var createErrorMessage = function createErrorMessage(textContent) {
    var element = document.createElement('span');
    element.classList.add(errorMessageClass);
    element.textContent = textContent;
    return element;
  };

  // Form submission functions
  var sendFeedback = function sendFeedback(data) {
    // Send data
    var url = 'https://prod-10.uksouth.logic.azure.com/workflows/8bf9c75f399246de96cf505a1331e738/triggers/manual/paths/invoke?api-version=2016-10-01&sp=%2Ftriggers%2Fmanual%2Frun&sv=1.0&sig=qSXrjDmMgHoPORSob4-uqonyJyF8mB8addKURnOhL1E';
    fetch(url, {
      method: 'POST',
      headers: {
        'content-type': 'application/json'
      },
      body: JSON.stringify(data)
    }).then(function(response) {
      return response.text();
    }).then(function(text) {
      // eslint-disable-next-line no-console
      console.log(text);
    });
  };
  var parseFormData = function parseFormData(nodeList) {
    var formData = {};
    // Add data from DOM
    formData.contentPath = document.location.pathname;
    // Add data from form fields
    var formFields = nodeListToArray(nodeList);
    formFields.forEach(function(field) {
      if (!field.value) return;
      if (field.type === 'checkbox') return;
      if (field.type === 'radio' && !field.checked) return;
      formData[field.name] = field.value;
    });
    return formData;
  };
  var sendPageUsefulFeedback = function sendPageUsefulFeedback(inputElements, isPageUseful) {
    var formData = parseFormData(inputElements);
    formData.contentHelpful = isPageUseful;
    sendFeedback(formData);
  };
  var sendSomethingWrongFeedback = function sendSomethingWrongFeedback(inputElements) {
    var formData = parseFormData(inputElements);
    sendFeedback(formData);
  };

  // Form validation functions
  var getFormGroupToValidate = function getFormGroupToValidate(form) {
    var formGroups = nodeListToArray(form.querySelectorAll(".".concat(formGroupClass)));
    var filterFunc = function filterFunc(group) {
      return group.querySelectorAll('[required="true"]').length;
    };
    var formGroupsToValidate = formGroups.filter(filterFunc);
    return formGroupsToValidate;
  };
  var showErrorState = function showErrorState(formGroup, type) {
    if (formGroup.classList.contains(formGroupErrorClass)) return;
    formGroup.classList.add(formGroupErrorClass);
    switch (type) {
      case 'checkbox': {
        var container = formGroup.querySelector(".".concat(formCheckBoxesClass));
        var errorMsg = createErrorMessage('This field is required');
        container.prepend(errorMsg);
        break;
      }
      case 'textarea': {
        var textareaElement = formGroup.querySelector(".".concat(formTextareaClass));
        var parentElement = textareaElement.parentElement;
        var _errorMsg = createErrorMessage('This field is required');
        textareaElement.classList.add(inputErrorClass);
        parentElement.insertBefore(_errorMsg, textareaElement);
        break;
      }
      case 'text': {
        var inputElement = formGroup.querySelector(".".concat(formInputClass));
        var _parentElement = inputElement.parentElement;
        var _errorMsg2 = createErrorMessage('This field is required');
        inputElement.classList.add(inputErrorClass);
        _parentElement.insertBefore(_errorMsg2, inputElement);
        break;
      }
      default:
        break;
    }
  };
  var clearErrorState = function clearErrorState(formGroup) {
    formGroup.classList.remove(formGroupErrorClass);
    // Remove error messages
    var errorMsg = formGroup.querySelector(".".concat(errorMessageClass));
    if (errorMsg) errorMsg.remove();
    // Remove error classes
    var erroredInputs = formGroup.querySelectorAll(".".concat(inputErrorClass));
    erroredInputs.forEach(function(input) {
      return input.classList.remove(inputErrorClass);
    });
  };
  var addValidationEvents = function addValidationEvents(form) {
    var formGroups = getFormGroupToValidate(form);
    // Loop through and add invalid validation
    formGroups.forEach(function(group) {
      var formElements = group.querySelectorAll('[required="true"]');
      formElements.forEach(function(element) {
        element.addEventListener('input', function() {
          return clearErrorState(group);
        });
        element.addEventListener('invalid', function(event) {
          event.preventDefault();
          showErrorState(group, element.type);
        });
      });
    });
  };

  // Set up all the feedback loops
  var feedbackLoops = document.querySelectorAll(".".concat(feedbackLoopClass));
  feedbackLoops.forEach(function(feedbackLoop) {
    var somethingWrongBtn = feedbackLoop.querySelector(".".concat(feedbackLoopClass, "__wrong > button"));
    var closeBtn = feedbackLoop.querySelector(".".concat(feedbackLoopClass, "__form legend + button"));
    var questionsRef = feedbackLoop.querySelector(".".concat(feedbackLoopClass, "__questions"));
    var contentWrongFormRef = feedbackLoop.querySelector(".".concat(feedbackLoopClass, "__form"));
    var pageUsefulRef = feedbackLoop.querySelector(".".concat(feedbackLoopClass, "__useful"));
    var pageUsefulBtns = nodeListToArray(pageUsefulRef.querySelectorAll('button'));

    // Form functions
    var clearFormErrors = function clearFormErrors() {
      var formGroups = nodeListToArray(contentWrongFormRef.querySelectorAll(".".concat(formGroupClass)));
      formGroups.forEach(function(group) {
        return clearErrorState(group);
      });
    };
    var toggleFeedbackForm = function toggleFeedbackForm() {
      var event = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : false;
      if (event) event.preventDefault();
      feedbackLoop.classList.toggle(isOpenClass);
      clearFormErrors();
    };
    var submitIsPageUseful = function submitIsPageUseful(event) {
      var inputElements = feedbackLoop.querySelectorAll('[type="hidden"]');
      sendPageUsefulFeedback(inputElements, event.target.textContent);
      pageUsefulRef.classList.add(isSentClass);
    };
    var submitContentWrongForm = function submitContentWrongForm(event) {
      event.preventDefault();
      var inputElements = feedbackLoop.querySelectorAll('textarea, input');
      sendSomethingWrongFeedback(inputElements);
      questionsRef.classList.add(isSentClass);
      toggleFeedbackForm();
    };

    // Attach form event listeners
    somethingWrongBtn.addEventListener('click', toggleFeedbackForm);
    closeBtn.addEventListener('click', toggleFeedbackForm);
    addValidationEvents(contentWrongFormRef);
    pageUsefulBtns.forEach(function(btn) {
      return btn.addEventListener('click', submitIsPageUseful);
    });
    contentWrongFormRef.addEventListener('submit', submitContentWrongForm);
  });
};
var _default = feedbackLoopJS;
exports["default"] = _default;

Recommended javascript (ES6)
const feedbackLoopJS = () => {
  // CONSTANTS
  const feedbackLoopClass = 'wmnds-feedback-loop';
  const isOpenClass = 'wmnds-is--open';
  const isSentClass = 'wmnds-is--sent';

  const formGroupClass = 'wmnds-fe-group';
  const formInputClass = 'wmnds-fe-input';
  const formCheckBoxesClass = 'wmnds-fe-checkboxes';
  const formTextareaClass = 'wmnds-fe-textarea';
  const formGroupErrorClass = `${formGroupClass}--error`;
  const inputErrorClass = `${formInputClass}--error`;
  const errorMessageClass = 'wmnds-fe-error-message';

  // Helper functions
  const nodeListToArray = nodeList => Array.prototype.slice.call(nodeList);
  const createErrorMessage = textContent => {
    const element = document.createElement('span');
    element.classList.add(errorMessageClass);
    element.textContent = textContent;
    return element;
  };

  // Form submission functions
  const sendFeedback = data => {
    // Send data
    const url =
      'https://prod-10.uksouth.logic.azure.com/workflows/8bf9c75f399246de96cf505a1331e738/triggers/manual/paths/invoke?api-version=2016-10-01&sp=%2Ftriggers%2Fmanual%2Frun&sv=1.0&sig=qSXrjDmMgHoPORSob4-uqonyJyF8mB8addKURnOhL1E';

    fetch(url, {
        method: 'POST',
        headers: {
          'content-type': 'application/json'
        },
        body: JSON.stringify(data)
      })
      .then(response => {
        return response.text();
      })
      .then(text => {
        // eslint-disable-next-line no-console
        console.log(text);
      });
  };

  const parseFormData = nodeList => {
    const formData = {};
    // Add data from DOM
    formData.contentPath = document.location.pathname;
    // Add data from form fields
    const formFields = nodeListToArray(nodeList);
    formFields.forEach(field => {
      if (!field.value) return;
      if (field.type === 'checkbox') return;
      if (field.type === 'radio' && !field.checked) return;
      formData[field.name] = field.value;
    });
    return formData;
  };

  const sendPageUsefulFeedback = (inputElements, isPageUseful) => {
    const formData = parseFormData(inputElements);
    formData.contentHelpful = isPageUseful;
    sendFeedback(formData);
  };

  const sendSomethingWrongFeedback = inputElements => {
    const formData = parseFormData(inputElements);
    sendFeedback(formData);
  };

  // Form validation functions
  const getFormGroupToValidate = form => {
    const formGroups = nodeListToArray(form.querySelectorAll(`.${formGroupClass}`));
    const filterFunc = group => group.querySelectorAll('[required="true"]').length;
    const formGroupsToValidate = formGroups.filter(filterFunc);
    return formGroupsToValidate;
  };

  const showErrorState = (formGroup, type) => {
    if (formGroup.classList.contains(formGroupErrorClass)) return;
    formGroup.classList.add(formGroupErrorClass);

    switch (type) {
      case 'checkbox': {
        const container = formGroup.querySelector(`.${formCheckBoxesClass}`);
        const errorMsg = createErrorMessage('This field is required');
        container.prepend(errorMsg);
        break;
      }

      case 'textarea': {
        const textareaElement = formGroup.querySelector(`.${formTextareaClass}`);
        const {
          parentElement
        } = textareaElement;
        const errorMsg = createErrorMessage('This field is required');
        textareaElement.classList.add(inputErrorClass);
        parentElement.insertBefore(errorMsg, textareaElement);
        break;
      }

      case 'text': {
        const inputElement = formGroup.querySelector(`.${formInputClass}`);
        const {
          parentElement
        } = inputElement;
        const errorMsg = createErrorMessage('This field is required');
        inputElement.classList.add(inputErrorClass);
        parentElement.insertBefore(errorMsg, inputElement);
        break;
      }

      default:
        break;
    }
  };

  const clearErrorState = formGroup => {
    formGroup.classList.remove(formGroupErrorClass);
    // Remove error messages
    const errorMsg = formGroup.querySelector(`.${errorMessageClass}`);
    if (errorMsg) errorMsg.remove();
    // Remove error classes
    const erroredInputs = formGroup.querySelectorAll(`.${inputErrorClass}`);
    erroredInputs.forEach(input => input.classList.remove(inputErrorClass));
  };

  const addValidationEvents = form => {
    const formGroups = getFormGroupToValidate(form);
    // Loop through and add invalid validation
    formGroups.forEach(group => {
      const formElements = group.querySelectorAll('[required="true"]');
      formElements.forEach(element => {
        element.addEventListener('input', () => clearErrorState(group));
        element.addEventListener('invalid', event => {
          event.preventDefault();
          showErrorState(group, element.type);
        });
      });
    });
  };

  // Set up all the feedback loops
  const feedbackLoops = document.querySelectorAll(`.${feedbackLoopClass}`);
  feedbackLoops.forEach(feedbackLoop => {
    const somethingWrongBtn = feedbackLoop.querySelector(`.${feedbackLoopClass}__wrong > button`);
    const closeBtn = feedbackLoop.querySelector(`.${feedbackLoopClass}__form legend + button`);
    const questionsRef = feedbackLoop.querySelector(`.${feedbackLoopClass}__questions`);
    const contentWrongFormRef = feedbackLoop.querySelector(`.${feedbackLoopClass}__form`);
    const pageUsefulRef = feedbackLoop.querySelector(`.${feedbackLoopClass}__useful`);
    const pageUsefulBtns = nodeListToArray(pageUsefulRef.querySelectorAll('button'));

    // Form functions
    const clearFormErrors = () => {
      const formGroups = nodeListToArray(
        contentWrongFormRef.querySelectorAll(`.${formGroupClass}`)
      );
      formGroups.forEach(group => clearErrorState(group));
    };

    const toggleFeedbackForm = (event = false) => {
      if (event) event.preventDefault();
      feedbackLoop.classList.toggle(isOpenClass);
      clearFormErrors();
    };

    const submitIsPageUseful = event => {
      const inputElements = feedbackLoop.querySelectorAll('[type="hidden"]');
      sendPageUsefulFeedback(inputElements, event.target.textContent);
      pageUsefulRef.classList.add(isSentClass);
    };

    const submitContentWrongForm = event => {
      event.preventDefault();
      const inputElements = feedbackLoop.querySelectorAll('textarea, input');
      sendSomethingWrongFeedback(inputElements);
      questionsRef.classList.add(isSentClass);
      toggleFeedbackForm();
    };

    // Attach form event listeners
    somethingWrongBtn.addEventListener('click', toggleFeedbackForm);
    closeBtn.addEventListener('click', toggleFeedbackForm);
    addValidationEvents(contentWrongFormRef);
    pageUsefulBtns.forEach(btn => btn.addEventListener('click', submitIsPageUseful));
    contentWrongFormRef.addEventListener('submit', submitContentWrongForm);
  });
};

export default feedbackLoopJS;