Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add user journeys and additional info to survey link #892

Merged
merged 13 commits into from
Nov 14, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .tool-versions
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
nodejs 18.18.2
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
# History

## 1.1.0 (2024-11-10)
* Appends "userJourneys" and "additionalInfo" parameters to the generated survey URL if not empty.

## 1.0.3 (2023-09-18)
* Fixes issue with visually hidden elements causing an overflow due to absolute positioning.

Expand All @@ -14,14 +17,14 @@
* BREAKING
* Brings component up to date with v32.0.0 of Brand Context
* Note: Please ensure your application also uses v32.0.0 of Brand Context
* Removes dependency on Global Forms component.
* Removes dependency on Global Forms component.
* Note: Please uninstall Global Forms if you do not use elsewhere in your application.
* Note: The data model needed for this component has been greatly simplified. Please consider updating your application view model where needed.
* Introduces new data field: id.
* Introduces new data field: id.
* Note: Please refer to docs for more info. Please consider updating your application view model if needed.
* Removes CSAT component design tokens.
* Removes CSAT component design tokens.
* Note: Please update your import path from "00-tokens/default.tokens.scss" to "10-settings/default.scss"
* Major refactor of component's HTML and Sass.
* Major refactor of component's HTML and Sass.
* Note: If you do not consume the HTML directly from the installed package please update the HTML in your application.
* New functionality: now appends responseRating URL param to survey question link href. Refer to the docs for more info.

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ The Global Customer Satisfaction Input component is the frontend for an overall

The component comprises a set of 5 pictographic radio inputs used to collect a satisfaction score from our customers. The user is able to submit one of the following scores: Awful, Bad, OK, Good, Great.

It is a JavaScript dependent component. When a user interacts with the component an event is dispatched to `window.dataLayer` allowing customer satisfaction score data and context data scraped from the page to be sent to Google Tag Manager and Google Cloud Big Query.
It is a JavaScript dependent component. When a user interacts with the component an event is dispatched to `window.dataLayer` allowing customer satisfaction score data and context data scraped from the page to be sent to Google Tag Manager and Google Cloud Big Query.

## Usage
This documents how to implement this component on your website. If your project does not use Elements you can still use this component, see refer to the `Projects that do not use Elements` section below.
Expand Down Expand Up @@ -42,7 +42,7 @@ Consume the component's Handlebars view template found in the `/view/` directory

If you are unable to consume view templates in your project you can still use this component. Use the component demo to generate the HTML. This can be done as follows:

1. Edit the JSON data file found at `/demo/context.json` and define the data you need for your instance (see below Data section)
1. Edit the JSON data file found at `/demo/context.json` and define the data you need for your instance (see below Data section)
2. Run `npm run demo -- -p global-customer-satisfaction-input`
3. Copy the `<aside>` and its contents from the generated demo file at `/demo/dist/index.html` and paste it into the relevant location in your project

Expand Down Expand Up @@ -97,10 +97,6 @@ A string that defines the link text for any survey you wish to link to after a u

A string that can be used to define additional data that you would like to be captured with the user's customer satisfaction score. This property aims to future-proof this component to ensure we can capture additional meaning if needed.

**additionalInfo** (Optional)

A string that can be used to define additional data that you would like to be captured with the user's customer satisfaction score. This property aims to future-proof this component to ensure we can capture additional meaning if needed.

**id** (Optional)

Please define a unique value here if you are going to implement more than one CSAT component on the same page. This will ensure there are no duplicate id values in the page HTML.
Expand All @@ -117,6 +113,10 @@ This is the url of the page the user is on.

This is the rating that the user chose when they submitted the form.

**userJourneys** and **additionalInfo**

These parameters will each be appended only if you set them when using the Handlebars template or the HTML generated by this component. They will contain the values you provide for each.

These data can be used in your reporting system to filter your survey responses. For example, in Survey Monkey you can configure custom variables with your survey so that these can be used in reporting analysis and queries.

### Projects that do not use Elements
Expand All @@ -128,5 +128,5 @@ or
2. You can generate a component demo using the editing `/demo/context.json` and using the command `npm run demo -- -p global-customer-satisfaction-input`. Then, copy the compiled code from `/demo/dist/index.html` in to your project.

> **Warning**
> These approaches are **not** preferred and should only be used in exceptional circumstances where Elements cannot be used in your application. If you use either of these approaches you **must** ensure you have adequate means of staying up to date with any new version releases for this component.
> These approaches are **not** preferred and should only be used in exceptional circumstances where Elements cannot be used in your application. If you use either of these approaches you **must** ensure you have adequate means of staying up to date with any new version releases for this component.

Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,11 @@
import {customerSatisfactionInput} from '../js/index.js';
import * as qs from 'querystring';

const additionalInfo = 'some additional info';
const userJourneys = 'get prepared to publish';

const fixture = `
<aside class="u-hide u-js-show" data-customer-satisfaction-input="" data-customer-satisfaction-input-user-journeys="get prepared to publish">
<aside class="u-hide u-js-show" data-customer-satisfaction-input="" data-customer-satisfaction-input-additional-info="${additionalInfo}" data-customer-satisfaction-input-user-journeys="${userJourneys}">
<form>
<fieldset>
<div>
Expand Down Expand Up @@ -114,42 +117,42 @@ describe('Global Customer Satisfaction Input', () => {
button.click();
const expectedValue =
[{
additionalInfo: null,
additionalInfo,
event: 'survey.track',
radioValue: '1',
userJourneys: 'get prepared to publish'
userJourneys
}];
expect(window.dataLayer).toEqual(expectedValue);
});

test('Should trim and lowercase user journey values before dispatching them in a dataLayer event', () => {
expect(window.dataLayer).toEqual([]);
aside.dataset.customerSatisfactionInputUserJourneys = ' GET PREPARED TO PUBLISH ';
aside.dataset.customerSatisfactionInputUserJourneys = ` ${userJourneys.toUpperCase()} `;
customerSatisfactionInput();
label.click();
button.click();
const expectedValue =
[{
additionalInfo: null,
additionalInfo,
event: 'survey.track',
radioValue: '1',
userJourneys: 'get prepared to publish'
userJourneys
}];
expect(window.dataLayer).toEqual(expectedValue);
});

test('Should process comma separated user journey values correctly before dispatching them in a dataLayer event', () => {
expect(window.dataLayer).toEqual([]);
aside.dataset.customerSatisfactionInputUserJourneys = 'get prepared to publish, get published';
aside.dataset.customerSatisfactionInputUserJourneys = `${userJourneys}, get published`;
customerSatisfactionInput();
label.click();
button.click();
const expectedValue =
[{
additionalInfo: null,
additionalInfo,
event: 'survey.track',
radioValue: '1',
userJourneys: 'get prepared to publish,get published'
userJourneys: `${userJourneys},get published`
}];
expect(window.dataLayer).toEqual(expectedValue);
});
Expand All @@ -161,7 +164,7 @@ describe('Global Customer Satisfaction Input', () => {
customerSatisfactionInput();
label.click();
button.click();
expect(console.error).toBeCalledTimes(1);
expect(console.error).toBeCalledTimes(2);
expect(console.error).toBeCalledWith('Attempt to send Global Customer Satisfaction Input event failed. Value not found for User Journeys.');
// Also asserting that, from the user's perspective, it fails silently
expect(fieldset.classList.contains('u-hide')).toBe(true);
Expand All @@ -175,7 +178,7 @@ describe('Global Customer Satisfaction Input', () => {
customerSatisfactionInput();
label.click();
button.click();
expect(console.error).toBeCalledTimes(1);
expect(console.error).toBeCalledTimes(2);
expect(console.error).toBeCalledWith('Attempt to send Global Customer Satisfaction Input event failed. One or more of the user journeys provided are not permissible values.');
// Also asserting that, from the user's perspective, it fails silently
expect(fieldset.classList.contains('u-hide')).toBe(true);
Expand All @@ -198,12 +201,14 @@ describe('Global Customer Satisfaction Input', () => {

test('Should get the current location and add it as a query parameter to the survey link', () => {
expect(surveyLink.href === 'https://www.surveymonkey.com/1').toBe(true);
window.location.href = 'http://localhost/?shafkjsahfh'
const location = 'http://localhost/';
window.location.href = `${location}?shafkjsahfh`;
customerSatisfactionInput();
expect(surveyLink.href).toEqual('https://www.surveymonkey.com/1?location=http://localhost/');
const url = new URL(surveyLink.href);
expect(url.searchParams.get('location')).toEqual(location);
})

test('Should get the selected and add it as a query parameter to the survey link', () => {
test('Should get the selected rating and add it as a query parameter to the survey link', () => {
const rating = 3
customerSatisfactionInput();

Expand All @@ -212,6 +217,23 @@ describe('Global Customer Satisfaction Input', () => {
expect(surveyLink.href).toContain(`responseRating=${rating}`);
})


test('Should get the additional info and add it as a query parameter to the survey link', () => {
customerSatisfactionInput();
label.click();
button.click();
const url = new URL(surveyLink.href);
expect(url.searchParams.get('additionalInfo')).toEqual(additionalInfo);
})

test('Should get the user journeys and add it as a query parameter to the survey link', () => {
customerSatisfactionInput();
label.click();
button.click();
const url = new URL(surveyLink.href);
expect(url.searchParams.get('userJourneys')).toEqual(userJourneys);
})

function clickOnRating(rating) {
document.querySelectorAll('label')[rating - 1].click();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,32 @@ class CustomerSatisfactionInput {
this._aside = aside;
this._form = this._aside.querySelector('form');
this._formRadioFieldset = this._form.querySelector('fieldset');
this._formRadios = Array.from(this._form.querySelectorAll('[data-customer-satisfaction-input="radio"]'));
this._surveyLink = this._form.querySelector('[data-customer-satisfaction-input="survey-link"]');
this._formRadios = Array.from(
this._form.querySelectorAll('[data-customer-satisfaction-input="radio"]')
);
this._surveyLink = this._form.querySelector(
'[data-customer-satisfaction-input="survey-link"]'
);
this._setSurveyLinkHref();
this._submitButton = this._form.querySelector('button[type="submit"]');
this._submitMessage = this._form.querySelector('[data-customer-satisfaction-input="submit-message"]');
this._permissibleUserJourneys = ['get prepared to publish', 'get published', 'discover relevant scholarly content', 'manage my editorial work', 'manage my peer reviews', 'promote my work', 'evaluate the performance of scholarly work', 'manage an apc', 'buy something', 'access what i am entitled to', 'librarian get the information i need', 'librarian assess the performance and use of my portfolio', 'librarian buy something'];
this._submitMessage = this._form.querySelector(
'[data-customer-satisfaction-input="submit-message"]'
);
this._permissibleUserJourneys = [
'get prepared to publish',
'get published',
'discover relevant scholarly content',
'manage my editorial work',
'manage my peer reviews',
'promote my work',
'evaluate the performance of scholarly work',
'manage an apc',
'buy something',
'access what i am entitled to',
'librarian get the information i need',
'librarian assess the performance and use of my portfolio',
'librarian buy something'
];
this._bindEvents();
}

Expand All @@ -28,44 +48,77 @@ class CustomerSatisfactionInput {

_setSurveyLinkHref() {
if (this._surveyLink) {
this._surveyLink.href = this._surveyLink.href + '?location=' + window.location.href.split('?')[0];
this._surveyLink.href =
this._surveyLink.href +
'?location=' +
window.location.href.split('?')[0];
}
}

_setSurveyLinkResponseRating(rating) {
this._updateSurveyLinkQueryString('responseRating', rating);
}

_setSurveyLinkUserJourneys(userJourneys) {
if (userJourneys) {
this._updateSurveyLinkQueryString('userJourneys', userJourneys);
}
}

_setSurveyLinkAdditionalInfo(additionalInfo) {
if (additionalInfo) {
this._updateSurveyLinkQueryString('additionalInfo', additionalInfo);
}
}

_getUserJourneys() {
if (!this._aside.dataset.customerSatisfactionInputUserJourneys) {
console.error('Attempt to send Global Customer Satisfaction Input event failed. Value not found for User Journeys.');
console.error(
'Attempt to send Global Customer Satisfaction Input event failed. Value not found for User Journeys.'
);
return;
}
const userJourneyStrings = this._aside.dataset.customerSatisfactionInputUserJourneys.split(',');
const sanitisedUserJourneyStrings = userJourneyStrings.map(string => string.trim().toLowerCase());
const containsPermissibleUserJourneys = sanitisedUserJourneyStrings.every(string => {
return this._permissibleUserJourneys.includes(string);
});

const userJourneyStrings =
this._aside.dataset.customerSatisfactionInputUserJourneys.split(',');
const sanitisedUserJourneyStrings = userJourneyStrings.map(string =>
string.trim().toLowerCase()
);
const containsPermissibleUserJourneys = sanitisedUserJourneyStrings.every(
string => {
return this._permissibleUserJourneys.includes(string);
}
);

if (containsPermissibleUserJourneys) {
return sanitisedUserJourneyStrings.join(',');
}
console.error('Attempt to send Global Customer Satisfaction Input event failed. One or more of the user journeys provided are not permissible values.');

console.error(
'Attempt to send Global Customer Satisfaction Input event failed. One or more of the user journeys provided are not permissible values.'
);

return false;
}

_dispatchDataLayerEvent(radioValue) {
if (!window.dataLayer) {
console.error('Attempt to send Global Customer Satisfaction Input event failed. window.dataLayer does not exist.');
console.error(
'Attempt to send Global Customer Satisfaction Input event failed. window.dataLayer does not exist.'
);

return;
}

const userJourneys = this._getUserJourneys();
if (userJourneys && radioValue) {
window.dataLayer.push({
// this event name corresponds to the GTM trigger setup for this solution
event: 'survey.track',
userJourneys: userJourneys,
radioValue: radioValue,
additionalInfo: this._aside.dataset.customerSatisfactionInputAdditionalInfo || null
additionalInfo:
this._aside.dataset.customerSatisfactionInputAdditionalInfo || null
});
}
}
Expand All @@ -81,22 +134,32 @@ class CustomerSatisfactionInput {
event.preventDefault();
event.stopPropagation();
});
['click', 'keydown'].forEach(eventType => {

const eventTypes = ['click', 'keydown'];

for (const eventType of eventTypes) {
this._submitButton.addEventListener(eventType, event => {
if (/Enter|Space/.test(event.key) || event.type === 'click') {
event.preventDefault();
event.stopPropagation();
const validForm = this._validateForm();

if (validForm) {
this._setSurveyLinkResponseRating((this._getCheckedRadioValue()));
this._setSurveyLinkResponseRating(this._getCheckedRadioValue());
this._setSurveyLinkUserJourneys(this._getUserJourneys());
this._setSurveyLinkAdditionalInfo(
this._aside.dataset.customerSatisfactionInputAdditionalInfo
);
this._dispatchDataLayerEvent(this._getCheckedRadioValue());
this._displayMessage();

return;
}

this._displayError();
}
});
});
}
}

_validateForm() {
Expand All @@ -106,7 +169,9 @@ class CustomerSatisfactionInput {
}

_displayError() {
if (this._form.classList.contains('c-customer-satisfaction-input--show-error')) {
if (
this._form.classList.contains('c-customer-satisfaction-input--show-error')
) {
return;
}
this._form.classList.add('c-customer-satisfaction-input--show-error');
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
{
"name": "@springernature/global-customer-satisfaction-input",
"version": "1.0.3",
"version": "1.1.0",
"license": "MIT",
"description": "HTML form based component for gathering customer satisfaction data",
"keywords": [],
"keywords": ["survey"],
"author": "Springer Nature",
"brandContext": "^32.0.0"
}
}