
Author Name
Emtiaz Hossain
Most "form tracking" advice online stops at "add a click trigger on the submit button." That works until the form has validation, an AJAX submission with no page reload, or fields you actually need in your CRM, not just a binary "someone submitted a form" event. This is the pattern used across dozens of client implementations: a plain JavaScript listener that reads real field values at the moment of submission and pushes a structured object to the dataLayer, no plugin-specific dependency required.
Every form builder (Contact Form 7, WPForms, Ninja Forms, JotForm, a raw HTML form) eventually does the same two things: it collects field values, and it confirms success somehow, either a page reload, an AJAX response, or a DOM change. The universal pattern below covers the raw-HTML case directly and is the base every plugin-specific post in this series adapts.
<script>
(function() {
var fullName = document.querySelector('input[name="name"]').value;
var email = document.querySelector('input[name="email"]').value;
var phone_code = document.querySelector("select[name='phone_code']").value;
var phone = document.querySelector('input[name="phone"]').value;
var country = document.querySelector('input[name="country"]').value;
var service_type = document.querySelector("select[name='service_type']").value;
var start_date = document.querySelector('input[name="start_date"]').value;
var end_date = document.querySelector('input[name="end_date"]').value;
var message = document.querySelector("textarea").value;
var nameParts = fullName.trim().split(' ');
var firstName = nameParts[0] || '';
var lastName = nameParts.slice(1).join(' ') || '';
var phone_no = phone_code + phone;
if (fullName && email && phone && country && service_type) {
window.dataLayer = window.dataLayer || [];
window.dataLayer.push({
event: 'contact_submit_v2',
user_data: {
first_name: firstName,
last_name: lastName,
email: email,
phone: phone_no,
country: country,
service_type: service_type,
message: message
}
});
}
})();
</script>Four things worth calling out in this specific structure, since they're what separate a pattern that survives real-world forms from one that breaks the first time a field is empty:
1. Read values, don't guess at them. Every field is read directly from the DOM at submission time via document.querySelector, not inferred from a form library's internal state. This means the exact same approach works regardless of what's rendering the form underneath.
2. Name splitting is explicit, not assumed. Most CRMs and ad platforms want first_name and last_name separately, but most forms just collect one "name" field. Splitting on the first space and treating everything after it as the last name handles the vast majority of real names correctly, better than assuming exactly two words.
3. A completeness check before firing. The if (fullName && email && phone && country && service_type) guard exists because a submit event alone doesn't guarantee the form's required fields were actually filled in, particularly on multi-step or client-validated forms where the submit handler can fire before validation blocks it. Firing a conversion event with empty required fields pollutes reporting and any list you build in a CRM.
4. Everything nests under user_data. This isn't arbitrary, it matches the object shape most enhanced conversion and CAPI implementations expect, so the same dataLayer push can feed both a GA4 event tag and a hashed user-data variable for Meta CAPI or Google Ads enhanced conversions without restructuring.
name attributes).contact_submit_v2, mapping {{DLV - user_data.email}}, {{DLV - user_data.phone}}, etc. as event parameters or user-provided data.user_data object appears in the GTM debugger with every field populated.The pattern above is the foundation, but most sites don't use raw HTML forms, they use a plugin, and each plugin exposes its own submission signal instead of a plain DOM submit event: Contact Form 7 fires a jQuery event, WPForms behaves differently for AJAX versus non-AJAX forms, JotForm posts a message event across an iframe boundary, and Ninja Forms exposes structured field data directly on its own submit response. The follow-up guides in this series cover each of those signals specifically, while reusing this same "read fields, guard for completeness, push a structured object" core.
document.querySelector-based pattern works for any form and is the right starting point before reaching for a plugin-specific hookuser_data object so the same event can feed GA4, Meta CAPI, and Google Ads enhanced conversions without reworkDiscussed this on LinkedIn
Found this helpful?
Drop a comment on the LinkedIn post – I read every reply and love connecting with tracking folks.
Book a free 30-minute call. I will review your current tracking setup, show you where conversions are leaking, and tell you exactly what I would fix.
WPForms has two submission modes, AJAX and non-AJAX, that need completely different GTM tracking patterns. Here's the real code for both, plus the validation logic non-AJAX forms need.
Ninja Forms fires its own jQuery success event with structured field data attached, making it the simplest of the major form plugins to track accurately in GTM.
How to track Contact Form 7 submissions accurately in GTM by confirming CF7's own success message instead of trusting a click event that fires even on failed validation.