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.
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
Name | Type | Description |
---|---|---|
id | string | The id of the feedback loop container. |
isOpen | boolean | If true , the feedback loop form will be in the open state. Defaults to false . |
isUsefulSent | boolean | If true , the feedback loop will show the usefulSentMessage as if the 'Is this page useful?' feedback has been submitted. Defaults to false . |
usefulSentMessage | object | The message shown when the 'Is this page useful?' feedback had been submitted. This message is also shown when isUsefulSent is true . |
isFormSent | boolean | If true , the feedback loop will show the formSentMessage as if the feedback form had been submitted. Defaults to false . |
formSentMessage | object | The message shown when the feedback form has been submitted. This message is also shown when isFormSent is true . |
metaFields | array | Required Data used to populate the hidden fields that are included when both the 'Is this page useful?' feedback and the form is submitted. |
formFields | array | Required Fields to include in the feedback form. |
Options for usefulSentMessage
Name | Type | Description |
---|---|---|
contentText | string | Text shown once 'Is this page useful?' feedback is submitted. If contentHTML is provided, this argument will be ignored. |
contentHTML | string | HTML shown once 'Is this page useful?' feedback is submitted. If contentHTML is provided, the contentText argument will be ignored. |
Options for formSentMessage
Name | Type | Description |
---|---|---|
contentText | string | Text shown once feedback form is submitted. If contentText is provided, the contentText argument will be ignored. |
contentHTML | string | HTML shown once feedback form is submitted. If contentText is provided, this argument will be ignored. |
Options for metaFields
Name | Type | Description |
---|---|---|
name | string | Required The key of the data to be submitted. One of contentType , contentName , contentLastEditor , serviceName or contentLastUpdated . contentType is required. |
value | string | Required The value of the data to be submitted. |
Options for formFields
Name | Type | Description |
---|---|---|
type | string | Required Type of form input. One of textarea , input or checkbox . |
name | string | Required The key for the data that will submitted form the input. |
labelText | string | Required Text label to be used for the input. If labelHTML is provided, this argument will be ignored. |
labelHTML | string | Required HTML to be used in the input label. If labelHTML is provided, labelText will be ignored. |
required | boolean | Whether the input is required or not. Defaults to false . |
placeholder | string | Placeholder 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.
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
Name | Type | Description |
---|---|---|
id | string | The id of the feedback loop container. |
isOpen | boolean | If true , the feedback loop form will be in the open state. Defaults to false . |
isUsefulSent | boolean | If true , the feedback loop will show the usefulSentMessage as if the 'Is this page useful?' feedback has been submitted. Defaults to false . |
usefulSentMessage | object | The message shown when the 'Is this page useful?' feedback had been submitted. This message is also shown when isUsefulSent is true . |
isFormSent | boolean | If true , the feedback loop will show the formSentMessage as if the feedback form had been submitted. Defaults to false . |
formSentMessage | object | The message shown when the feedback form has been submitted. This message is also shown when isFormSent is true . |
metaFields | array | Required Data used to populate the hidden fields that are included when both the 'Is this page useful?' feedback and the form is submitted. |
formFields | array | Required Fields to include in the feedback form. |
Options for usefulSentMessage
Name | Type | Description |
---|---|---|
contentText | string | Text shown once 'Is this page useful?' feedback is submitted. If contentHTML is provided, this argument will be ignored. |
contentHTML | string | HTML shown once 'Is this page useful?' feedback is submitted. If contentHTML is provided, the contentText argument will be ignored. |
Options for formSentMessage
Name | Type | Description |
---|---|---|
contentText | string | Text shown once feedback form is submitted. If contentText is provided, the contentText argument will be ignored. |
contentHTML | string | HTML shown once feedback form is submitted. If contentText is provided, this argument will be ignored. |
Options for metaFields
Name | Type | Description |
---|---|---|
name | string | Required The key of the data to be submitted. One of contentType , contentName , contentLastEditor , serviceName or contentLastUpdated . contentType is required. |
value | string | Required The value of the data to be submitted. |
Options for formFields
Name | Type | Description |
---|---|---|
type | string | Required Type of form input. One of textarea , input or checkbox . |
name | string | Required The key for the data that will submitted form the input. |
labelText | string | Required Text label to be used for the input. If labelHTML is provided, this argument will be ignored. |
labelHTML | string | Required HTML to be used in the input label. If labelHTML is provided, labelText will be ignored. |
required | boolean | Whether the input is required or not. Defaults to false . |
placeholder | string | Placeholder 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;