Emtiaz
ServicesCase StudiesTestimonialsAboutFAQBlogs
Advanced Tracking8 min read

The Complete Guide to Form Tracking with Google Tag Manager

Emtiaz Hossain

Author Name

Emtiaz Hossain

Last Edit : Jul 19, 2026, 10:47:00 AM
The Complete Guide to Form Tracking with Google Tag Manager

TABLE OF CONTENTS▼
Why a Universal Pattern, Not a Plugin PluginThe Core PatternWiring It Into GTMWhere the Plugin-Specific Guides DifferKey Takeaways

Contents

0%
Why a Universal Pattern, Not a Plugin PluginThe Core PatternWiring It Into GTMWhere the Plugin-Specific Guides DifferKey Takeaways

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.

Why a Universal Pattern, Not a Plugin Plugin

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.

The Core Pattern

javascript
<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.

Wiring It Into GTM

  1. Add the script above as a Custom HTML tag, adjusting the selectors to match the actual form's field names (view page source or inspect the form to get the real name attributes).
  2. Set the trigger to the form's submit event, either a page load on the confirmation URL if the form redirects, or a DOM-ready/timer-based check if it doesn't.
  3. Build a GA4 Event tag (or your CAPI/enhanced-conversions tag) listening for contact_submit_v2, mapping {{DLV - user_data.email}}, {{DLV - user_data.phone}}, etc. as event parameters or user-provided data.
  4. Preview and submit a real test entry, confirm the full user_data object appears in the GTM debugger with every field populated.

Where the Plugin-Specific Guides Differ

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.

Key Takeaways

  • ▸A universal document.querySelector-based pattern works for any form and is the right starting point before reaching for a plugin-specific hook
  • ▸Always guard the dataLayer push with a completeness check, a submit event firing does not guarantee valid data was entered
  • ▸Nest field data under a consistent user_data object so the same event can feed GA4, Meta CAPI, and Google Ads enhanced conversions without rework
  • ▸Plugin-specific forms need a different success signal (jQuery event, AJAX response, postMessage), the field-reading and structuring logic stays the same
#GTM#CustomTracking#JavaScript#conversion tracking

Discussed this on LinkedIn

Found this helpful?

Drop a comment on the LinkedIn post – I read every reply and love connecting with tracking folks.

View on LinkedIn

Not Sure If Your Tracking Is Costing You?

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.

More Blog Posts

View all
Tracking WPForms Submissions with GTM
Advanced Tracking

Tracking WPForms Submissions with GTM

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.

Tracking Ninja Forms Submissions with GTM
Advanced Tracking

Tracking Ninja Forms Submissions with GTM

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.

Tracking Contact Form 7 Submissions with GTM
Advanced Tracking

Tracking Contact Form 7 Submissions with 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.

Emtiaz
EmailLinkedInPrivacyTerms

© 2026 Emtiaz Hossain