Last active
April 19, 2026 03:44
-
-
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
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| /**** | |
| * | |
| * @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