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

MWPW-160954 Marketo Multi-step #3671

Open
wants to merge 3 commits into
base: stage
Choose a base branch
from
Open
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
100 changes: 100 additions & 0 deletions libs/blocks/marketo/marketo-multi.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
import { createTag } from '../../utils/utils.js';
import { debounce } from '../../utils/action.js';

const VALIDATION_STEP = {
name: '2',
phone: '2',
mktoFormsJobTitle: '2',
mktoFormsFunctionalArea: '2',
company: '3',
state: '3',
postcode: '3',
mktoFormsPrimaryProductInterest: '3',
mktoFormsCompanyType: '3',
};

function updateStepDetails(formEl, step, totalSteps) {
formEl.classList.add('hide-errors');
formEl.classList.remove('show-warnings');
formEl.dataset.step = step;
formEl.querySelector('.step-details .step').textContent = `Step ${step} of ${totalSteps}`;
formEl.querySelector('#mktoButton_new').textContent = step === totalSteps ? 'Submit' : 'Next';
formEl.querySelector(`.mktoFormRowTop[data-validate="${step}"]:not(.mktoHidden) input`)?.focus();
}

function showPreviousStep(formEl, totalSteps) {
const currentStep = parseInt(formEl.dataset.step, 10);
const previousStep = currentStep - 1;
const backBtn = formEl.querySelector('.back-btn');

updateStepDetails(formEl, previousStep, totalSteps);
if (previousStep === 1) backBtn?.remove();
}

const showNextStep = (formEl, currentStep, totalSteps) => {
if (currentStep === totalSteps) return;
const nextStep = currentStep + 1;
const stepDetails = formEl.querySelector('.step-details');

if (!stepDetails.querySelector('.back-btn')) {
const backBtn = createTag('button', { class: 'back-btn', type: 'button' }, 'Back');
backBtn.addEventListener('click', () => showPreviousStep(formEl, totalSteps));
stepDetails.prepend(backBtn);
}

updateStepDetails(formEl, nextStep, totalSteps);
};

export const formValidate = (formEl) => {
const currentStep = parseInt(formEl.dataset.step, 10) || 1;

if (formEl.querySelector(`.mktoFormRowTop[data-validate="${currentStep}"] .mktoInvalid`)) {
return false;
}

const totalSteps = formEl.closest('.marketo').classList.contains('multi-3') ? 3 : 2;
showNextStep(formEl, currentStep, totalSteps);

return currentStep === totalSteps;
};

function setValidationSteps(formEl, totalSteps) {
formEl.querySelectorAll('.mktoFormRowTop').forEach((row) => {
const rowAttr = row.getAttribute('data-mktofield') || row.getAttribute('data-mkto_vis_src');
const step = VALIDATION_STEP[rowAttr] ? Math.min(VALIDATION_STEP[rowAttr], totalSteps) : 1;
row.dataset.validate = rowAttr?.startsWith('adobe-privacy') ? totalSteps : step;
});
}

function onRender(formEl, totalSteps) {
const currentStep = parseInt(formEl.dataset.step, 10);
const submitButton = formEl.querySelector('#mktoButton_new');
if (submitButton) submitButton.textContent = currentStep === totalSteps ? 'Submit' : 'Next';
formEl.querySelector('.step-details .step').textContent = `Step ${currentStep} of ${totalSteps}`;

setValidationSteps(formEl, totalSteps);
}

const readyForm = (form, totalSteps) => {
const formEl = form.getFormElem().get(0);
form.onValidate(() => formValidate(formEl));

const stepEl = createTag('p', { class: 'step' }, `Step 1 of ${totalSteps}`);
const stepDetails = createTag('div', { class: 'step-details' }, stepEl);
formEl.append(stepDetails);

const debouncedOnRender = debounce(() => onRender(formEl, totalSteps), 10);
const observer = new MutationObserver(debouncedOnRender);
observer.observe(formEl, { childList: true, subtree: true });
debouncedOnRender();
};

export default (el) => {
if (!el.classList.contains('multi-step')) return;
const formEl = el.querySelector('form');
const totalSteps = el.classList.contains('multi-3') ? 3 : 2;
formEl.dataset.step = 1;

const { MktoForms2 } = window;
MktoForms2.whenReady((form) => { readyForm(form, totalSteps); });
};
74 changes: 71 additions & 3 deletions libs/blocks/marketo/marketo.css
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@
--marketo-form-focus: #147AF3;
--marketo-form-placeholder-height: calc(78px * 7 + 57px); /* 7 rows + submit */
--marketo-form-placeholder-height-desktop: calc(78px * 4 + 57px); /* 4 rows + submit */
--marketo-form-placeholder-height-multi: calc(78px * 2 + 57px); /* 2 rows + submit */
--marketo-form-placeholder-height-multi-desktop: calc(78px + 57px); /* 1 row + submit */
--marketo-form-min-height: 215px;
--marketo-form-max-height: 10000px;
}
Expand All @@ -25,6 +27,10 @@
min-height: var(--marketo-form-placeholder-height);
}

.marketo.marketo.multi-step.loading form {
min-height: var(--marketo-form-placeholder-height-multi);
}

.marketo .marketo-title {
font-size: 28px;
font-weight: bold;
Expand Down Expand Up @@ -87,6 +93,50 @@
display: contents;
}

.marketo.multi-step .mktoFormRow.mktoFormRowTop[data-validate="2"],
.marketo.multi-step .mktoFormRow.mktoFormRowTop[data-validate="3"] {
display: none;
}

.marketo.multi-step .mktoForm[data-step="2"] .mktoFormRow.mktoFormRowTop[data-validate="2"],
.marketo.multi-step .mktoForm[data-step="3"] .mktoFormRow.mktoFormRowTop[data-validate="3"] {
display: contents;
}

.marketo.multi-step .mktoForm[data-step="2"] .mktoFormRow.mktoFormRowTop[data-validate="1"],
.marketo.multi-step .mktoForm[data-step="3"] .mktoFormRow.mktoFormRowTop[data-validate="1"] {
display: none;
}

.marketo.multi-step .mktoForm[data-step="1"] .mktoFormRow.mktoFormRowTop.adobe-privacy,
.marketo.multi-step .mktoForm[data-step="2"] .mktoFormRow.mktoFormRowTop.adobe-privacy {
display: none;
}

.marketo.multi-step .mktoForm[data-step="3"] .mktoFormRow.mktoFormRowTop.adobe-privacy,
.marketo.multi-step.multi-2 .mktoForm[data-step="2"] .mktoFormRow.mktoFormRowTop.adobe-privacy {
display: grid;
}

.marketo.multi-step .step-details {
display: flex;
justify-content: center;
font-size: var(--type-body-xs-size);
font-weight: normal;
line-height: var(--type-body-xs-lh);
grid-column: span 2;
}

.marketo.multi-step .step-details button {
background: none;
border: none;
padding: 0;
text-decoration: underline;
cursor: pointer;
color: var(--link-color-dark);
margin-right: 8px;
}

.marketo .mktoFormCol.mktoVisible {
width: 100%;
margin-bottom: 11px;
Expand Down Expand Up @@ -322,12 +372,12 @@
outline: 2px solid var(--color-accent-focus-ring);
}

.marketo .mktoForm .mktoFormRow.msg-error {
.marketo .mktoFormRow.mktoFormRowTop.msg-error .mktoFormCol {
display: none;
}

.marketo .mktoForm.show-warnings .mktoFormRow.msg-error {
display: contents;
.marketo .show-warnings .mktoFormRow.mktoFormRowTop.msg-error .mktoFormCol {
display: block;
}

.marketo .mktoForm .mktoFormRow.msg-error .mktoHtmlText {
Expand Down Expand Up @@ -513,6 +563,16 @@
grid-column: span 2;
}

.marketo.multi-step .mktoForm[data-step="2"] .mktoFormRow.mktoFormRowTop[data-validate="2"].comments,
.marketo.multi-step .mktoForm[data-step="2"] .mktoFormRow.mktoFormRowTop[data-validate="2"].demo,
.marketo.multi-step .mktoForm[data-step="2"] .mktoFormRow.mktoFormRowTop[data-validate="2"].name,
.marketo.multi-step .mktoForm[data-step="3"] .mktoFormRow.mktoFormRowTop[data-validate="3"].comments,
.marketo.multi-step .mktoForm[data-step="3"] .mktoFormRow.mktoFormRowTop[data-validate="3"].demo,
.marketo.multi-step .mktoForm[data-step="3"] .mktoFormRow.mktoFormRowTop[data-validate="3"].name {
display: block;
grid-column: span 2;
}

.marketo .mktoFormRow.mktoFormRowTop.name .mktoFormRow[data-mktofield="Salutation"] {
grid-area: salutation;
}
Expand Down Expand Up @@ -585,9 +645,17 @@
min-height: var(--marketo-form-placeholder-height-desktop);
}

.marketo.multi-step.loading form {
min-height: var(--marketo-form-placeholder-height-multi-desktop);
}

.marketo .mktoForm {
max-height: var(--marketo-form-placeholder-height-desktop);
}

.marketo.multi-step .mktoForm {
max-height: var(--marketo-form-placeholder-height-multi-desktop);
}

.resource-form.section.two-up {
grid-template-columns: repeat(2, 1fr);
Expand Down
8 changes: 8 additions & 0 deletions libs/blocks/marketo/marketo.js
Original file line number Diff line number Diff line change
Expand Up @@ -180,6 +180,10 @@ export const loadMarketo = (el, formData) => {

MktoForms2.loadForm(`//${baseURL}`, munchkinID, formID);
MktoForms2.whenReady((form) => { readyForm(form, formData); });
/* c8 ignore next 3 */
if (el.classList.contains('multi-step')) {
import('./marketo-multi.js').then(({ default: multiStep }) => multiStep(el));
}
})
.catch(() => {
/* c8 ignore next 2 */
Expand Down Expand Up @@ -261,6 +265,10 @@ export default function init(el) {
fragment.append(formWrapper);
el.replaceChildren(fragment);
el.classList.add('loading');
/* c8 ignore next 3 */
if (el.classList.contains('multi-2') || el.classList.contains('multi-3')) {
el.classList.add('multi-step');
}

loadLink(`https://${baseURL}`, { rel: 'dns-prefetch' });

Expand Down
78 changes: 78 additions & 0 deletions test/blocks/marketo/marketo-multi.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
import { readFile } from '@web/test-runner-commands';
import { expect } from '@esm-bundle/chai';
import sinon, { stub } from 'sinon';
import init, { formValidate } from '../../../libs/blocks/marketo/marketo-multi.js';

const innerHTML = await readFile({ path: './mocks/multi-step-2.html' });

describe('marketo multi-step', () => {
let clock;

beforeEach(() => {
document.body.innerHTML = innerHTML;
clock = sinon.useFakeTimers();
window.MktoForms2 = { whenReady: stub().callsFake((callback) => callback({ onValidate: () => {}, getFormElem: () => ({ get: () => document.querySelector('form') }) })) };

const el = document.querySelector('.marketo');
init(el);
clock.tick(300);
});

afterEach(() => {
window.MktoForms2 = undefined;
clock.restore();
});

it('initializes multi-step form', () => {
const el = document.querySelector('.marketo');
const stepDetails = el.querySelector('.step-details .step');

expect(stepDetails).to.exist;
expect(stepDetails.textContent).to.equal('Step 1 of 2');
expect(window.MktoForms2.whenReady.calledOnce).to.be.true;

const step1 = el.querySelector('.mktoFormRowTop[data-validate="1"]');
const step2 = el.querySelector('.mktoFormRowTop[data-validate="2"]');
expect(step1).to.exist;
expect(step2).to.exist;
});

it('shows next step on valid form step', () => {
const formEl = document.querySelector('form');

formValidate(formEl);
clock.tick(200);

expect(formEl.dataset.step).to.equal('2');
expect(formEl.querySelector('#mktoButton_new').textContent).to.equal('Submit');
expect(formEl.querySelector('.back-btn')).to.exist;
});

it('does not show next step on invalid form submission', () => {
const formEl = document.querySelector('form');
formEl.querySelector('.mktoFormRowTop[data-validate="1"] input').classList.add('mktoInvalid');

const result = formValidate(formEl);
clock.tick(200);

expect(result).to.be.false;
expect(formEl.dataset.step).to.equal('1');
});

it('shows previous step on back button click', () => {
const formEl = document.querySelector('form');

formValidate(formEl);
clock.tick(200);

expect(formEl.dataset.step).to.equal('2');
const backBtn = formEl.querySelector('.back-btn');

backBtn.click();
clock.tick(200);

expect(formEl.dataset.step).to.equal('1');
expect(formEl.querySelector('.back-btn')).to.be.null;
expect(formEl.querySelector('.step-details .step').textContent).to.equal('Step 1 of 2');
});
});
49 changes: 49 additions & 0 deletions test/blocks/marketo/mocks/multi-step-2.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
<style>
.marketo.multi-step .mktoFormRow.mktoFormRowTop[data-validate="2"] {
display: none;
}

.marketo.multi-step[data-step="2"] .mktoFormRow.mktoFormRowTop[data-validate="2"] {
display: block;
}

.marketo.multi-step[data-step="2"] .mktoFormRow.mktoFormRowTop[data-validate="1"] {
display: none;
}
</style>
<div class="marketo multi-step">
<div class="marketo-form-wrapper">
<h3 class="marketo-title">Form template: Full</h3>
<p class="marketo-description">Marketo Template: flex_contact</p>
<form id="mktoForm_2277"
class="hide-errors mktoForm mktoHasWidth mktoLayoutLeft starting_fieldset mktoWhenRendered mktoForm--styles-added observMKTO focusReady mktoForm--fade-in mktoVisible"
novalidate="novalidate" autocomplete="off">
<div class="mktoFormRow mktoFormRowTop" data-mktofield="email">
<div class="mktoFieldDescriptor mktoFormCol mktoVisible">
<div class="mktoFieldWrap mktoRequiredField">
<label for="Email" id="LblEmail" class="mktoLabel mktoVisible">Business email</label>
<input id="Email" name="Email" maxlength="255" aria-labelledby="LblEmail InstructEmail" type="email"
class="mktoField mktoEmailField mktoRequired mktoVisible mktofield_anchor" aria-required="true"
autocomplete="email" placeholder="Business email*">
</div>
</div>
</div>
<div class="mktoFormRow mktoFormRowTop mktoCleaned phone" data-mkto_vis_src="phone">
<fieldset class="mktoFormCol mktoVisible">
<legend class="phone mktoLegend">phone</legend>
<div class="mktoFormRow mktoRequiredField" data-mktofield="Phone">
<div class="mktoFieldDescriptor mktoFormCol mktoVisible">
<div class="mktoFieldWrap mktoRequiredField">
<label for="Phone" id="LblPhone" class="mktoLabel mktoVisible">Business phone</label>
<input id="Phone" name="Phone" maxlength="255" aria-labelledby="LblPhone InstructPhone" type="tel"
class="mktoField mktoTelField mktoVisible mktofield_anchor mktoRequired mkto_toggle mktoRequiredVis"
autocomplete="tel" placeholder="Business phone*" aria-required="true" required="true">
</div>
</div>
</div>
</fieldset>
</div>
<button type="submit" id="mktoButton_new">Submit</button>
</form>
</div>
</div>
Loading