Skip to content

Instantly share code, notes, and snippets.

@apsolut
Last active April 19, 2026 03:44
Show Gist options
  • Select an option

  • Save apsolut/c270f4c74a1c28b42a3b619ac9d90d14 to your computer and use it in GitHub Desktop.

Select an option

Save apsolut/c270f4c74a1c28b42a3b619ac9d90d14 to your computer and use it in GitHub Desktop.
HubSpot Multi-step form validation , if required field not filled cant go to next step
/****
*
* @LINK https://github.com/apsolut-public/hubspot/tree/main/modules/multistep-form-validation.module
*
*/
(function () {
'use strict';
if (window.__hscvCleanup) { window.__hscvCleanup(); }
if (!document.getElementById('__hscv_styles')) {
var s = document.createElement('style');
s.id = '__hscv_styles';
s.textContent =
'.hscv-err-field .hsfc-TextInput:not([type=hidden]):not([aria-hidden=true]),' +
'.hscv-err-field .hsfc-TextareaInput{border-color:#e51520!important;background:#fdf3f4!important}' +
'.hscv-err-field .hsfc-TextInput--button{border-color:#e51520!important;background:#fdf3f4!important}' +
'.hscv-err-msg{display:block;font-size:13px;color:#e51520;margin-top:4px;font-family:Helvetica,sans-serif}';
document.head.appendChild(s);
}
var form = document.querySelector('form.hsfc-Form');
if (!form) { console.warn('[HSCV] ❌ form.hsfc-Form not found'); return; }
console.log('[HSCV] ✅ Form:', form.id);
/* ── helpers ── */
function getSteps() { return Array.from(form.querySelectorAll('[data-hsfc-id="Step"]')); }
function getActiveIndex(steps) {
for (var i = 0; i < steps.length; i++) {
if (steps[i].style.display !== 'none') return i;
}
return 0;
}
function isRequired(w) {
/* Source of truth: aria-required="true" on the input,
OR the RequiredIndicator span in the label.
Both are set by HubSpot when a field is marked
required in the form editor — no hardcoding needed. */
if (w.querySelector('.hsfc-FieldLabel__RequiredIndicator')) return true;
var inputs = w.querySelectorAll(
'input:not([type=hidden]):not([aria-hidden=true]), textarea'
);
for (var i = 0; i < inputs.length; i++) {
if (inputs[i].getAttribute('aria-required') === 'true') return true;
}
return false;
}
function getValue(w) {
var dd = w.querySelector('.hsfc-DropdownInput input[type="hidden"][aria-hidden="true"]');
if (dd) return dd.value.trim();
var inp = w.querySelector(
'input.hsfc-TextInput:not([type=hidden]):not([aria-hidden=true]):not([role=searchbox]),' +
'textarea.hsfc-TextareaInput'
);
return inp ? inp.value.trim() : '';
}
function getType(w) {
if (w.querySelector('.hsfc-DropdownInput')) return 'dropdown';
if (w.querySelector('input[type="email"]')) return 'email';
return 'text';
}
function emailOk(v) { return /^[^\s@]+@[^\s@]+\.[^\s@]{2,}$/.test(v); }
function getLabelText(w) {
var l = w.querySelector('.hsfc-FieldLabel');
return l ? l.textContent.replace('*', '').trim() : '(field)';
}
function showError(w, msg) {
w.classList.add('hscv-err-field');
var ex = w.querySelector('.hscv-err-msg');
if (ex) { ex.textContent = msg; return; }
var span = document.createElement('span');
span.className = 'hscv-err-msg';
span.textContent = msg;
var footer = w.querySelector('.hsfc-FieldFooter');
footer ? w.insertBefore(span, footer) : w.appendChild(span);
}
function clearError(w) {
w.classList.remove('hscv-err-field');
var m = w.querySelector('.hscv-err-msg'); if (m) m.remove();
}
function validateStep(step) {
var wrappers = step.querySelectorAll('[data-hsfc-id$="Field"]');
console.log('[HSCV] Validating', wrappers.length, 'fields on this step...');
var valid = true, firstBad = null;
for (var i = 0; i < wrappers.length; i++) {
var w = wrappers[i];
var label = getLabelText(w);
var req = isRequired(w);
console.log('[HSCV] ', label, '| aria-required:', req, '| val:', JSON.stringify(getValue(w)));
if (!req) { clearError(w); continue; }
var val = getValue(w);
var type = getType(w);
var ok = val !== '';
if (ok && type === 'email') ok = emailOk(val);
if (!ok) {
var msg = type === 'dropdown' ? 'Please make a selection.'
: type === 'email' ? 'Email must be formatted correctly.'
: 'Please complete this required field.';
showError(w, msg);
valid = false;
if (!firstBad) firstBad = w;
console.warn('[HSCV] ❌', label);
} else {
clearError(w);
console.log('[HSCV] ✅', label);
}
}
if (!valid && firstBad) firstBad.scrollIntoView({ behavior: 'smooth', block: 'center' });
console.log('[HSCV] Step valid:', valid);
return valid;
}
/* ── MutationObserver with re-entry guard ── */
var currentIndex = getActiveIndex(getSteps());
var _processing = false;
console.log('[HSCV] Starting on step:', currentIndex);
function reobserve() {
getSteps().forEach(function (step) {
observer.observe(step, { attributes: true, attributeFilter: ['style'] });
});
}
var observer = new MutationObserver(function () {
if (_processing) return;
observer.disconnect();
var steps = getSteps();
var newIndex = getActiveIndex(steps);
if (newIndex === currentIndex) { reobserve(); return; }
if (newIndex > currentIndex) {
var leaving = steps[currentIndex];
var arriving = steps[newIndex];
_processing = true;
leaving.style.display = '';
arriving.style.display = 'none';
_processing = false;
if (!validateStep(leaving)) {
console.warn('[HSCV] 🚫 Blocked on step', currentIndex);
} else {
_processing = true;
leaving.style.display = 'none';
arriving.style.display = '';
_processing = false;
currentIndex = newIndex;
console.log('[HSCV] ✅ Advanced to step', currentIndex);
attachDropdownClearers();
}
} else {
currentIndex = newIndex;
console.log('[HSCV] ← Back to step', currentIndex);
}
reobserve();
});
reobserve();
/* ── live clearing ── */
form.addEventListener('input', function (e) {
var w = e.target.closest('[data-hsfc-id$="Field"]');
if (w && e.target.value.trim()) clearError(w);
});
function attachDropdownClearers() {
form.querySelectorAll('.hsfc-DropdownInput').forEach(function (dd) {
if (dd._hscvWired) return;
dd._hscvWired = true;
var hidden = dd.querySelector('input[type="hidden"][aria-hidden="true"]');
var list = dd.querySelector('[role="listbox"]');
if (!hidden || !list) return;
list.addEventListener('click', function () {
setTimeout(function () {
var w = dd.closest('[data-hsfc-id$="Field"]');
if (w && hidden.value) clearError(w);
}, 50);
});
});
}
attachDropdownClearers();
window.__hscvCleanup = function () {
observer.disconnect();
console.log('[HSCV] Cleaned up');
};
console.log('[HSCV] 🎉 Active — validating aria-required="true" fields only.');
})();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment