Skip to Content
Menu

cf7Functions()

Capture timings and events from Contact Form 7 forms for Google Analytics.

JavaScript February 7, 2021

Usage

This function runs automatically, so it is not called manually. Is this incorrect?

Additional Notes

This function handles the timing of individual form fields as well as overall forms.

It also handles the Google Analytics event tracking for form submission funnels including starting forms, submit attempt, invalid, spam, mail failure, and successful submissions.

Was this page helpful? Yes No


    A feedback message is required to submit this form.

    Please check that you have entered a valid email address.

    Enter your email address if you would like a response.

    Thank you for your feedback!

    Source File

    Located in /assets/js/modules/forms.js on line 3.

    No Hooks

    This function does not have any filters or actions available. Request one?
    JavaScript
    nebula.cf7Functions = async function(){
        if ( !jQuery('.wpcf7-form').length ){
            return false;
        }
    
        jQuery('.wpcf7-form p:empty').remove(); //Remove empty <p> tags within CF7 forms
    
        let formStarted = {};
    
        //Replace submit input with a button so a spinner icon can be used instead of the CF7 spin gif (unless it has the class "no-button")
        jQuery('.wpcf7-form input[type=submit]').each(function(){
            if ( !jQuery(this).hasClass('no-button') ){
                jQuery(this).replaceWith('<button id="submit" type="submit" class="' + nebula.sanitize(jQuery(this).attr('class')) + '">' + nebula.sanitize(jQuery(this).val()) + '</button>'); //Sanitized to prevent XSS
            }
        });
    
        //Observe CF7 Forms when they scroll into the viewport
        try {
            //Observe the entries that are identified and added later (below)
            let cf7Observer = new IntersectionObserver(function(entries){
                entries.forEach(function(entry){
                    if ( entry.intersectionRatio > 0 ){
                        let thisEvent = {
                            category: 'CF7 Form',
                            action: 'Impression', //GA4 Name: "form_impression"?
                            formID: jQuery(entry.target).closest('.wpcf7').attr('id') || jQuery(entry.target).attr('id'),
                        };
    
                        nebula.dom.document.trigger('nebula_event', thisEvent);
                        ga('send', 'event', thisEvent.category, thisEvent.action, thisEvent.formID, {'nonInteraction': true});
                        window.dataLayer.push(Object.assign(thisEvent, {'event': 'nebula-cf7-impression'}));
    
                        cf7Observer.unobserve(entry.target); //Stop observing the element
                    }
                });
            }, {
                rootMargin: '0px',
                threshold: 0.10
            });
    
            //Create the entries and add them to the observer
            jQuery('.wpcf7-form').each(function(){
                cf7Observer.observe(jQuery(this)[0]); //Observe the element
            });
        } catch(error){
            nebula.help('CF7 Impression Observer: ' + error.message, '/functions/cf7Functions/', true);
        }
    
        //Re-init forms inside Bootstrap modals (to enable AJAX submission) when needed
        nebula.dom.document.on('shown.bs.modal', function(e){
            if ( typeof wpcf7.initForm === 'function' && jQuery(e.target).find('.wpcf7-form').length && !jQuery(e.target).find('.ajax-loader').length ){ //If initForm function exists, and a form is inside the modal, and if it has not yet been initialized (The initForm function adds the ".ajax-loader" span)
                wpcf7.initForm(jQuery(e.target).find('.wpcf7-form'));
            }
        });
    
        //Form starts and field focuses
        nebula.dom.document.on('focus', '.wpcf7-form input, .wpcf7-form select, .wpcf7-form button, .wpcf7-form textarea', function(e){
            let formID = jQuery(this).closest('div.wpcf7').attr('id');
    
            let thisField = e.target.name || jQuery(this).closest('.form-group').find('label').text() || e.target.id || 'Unknown';
            let fieldInfo = '';
            if ( jQuery(this).attr('type') === 'checkbox' || jQuery(this).attr('type') === 'radio' ){
                fieldInfo = jQuery(this).attr('value');
            }
    
            if ( !jQuery(this).hasClass('.ignore-form') && !jQuery(this).find('.ignore-form').length && !jQuery(this).parents('.ignore-form').length ){
                let thisEvent = {
                    event: e,
                    category: 'CF7 Form',
                    action: 'Started Form (Focus)', //GA4 Name: "form_start"?
                    formID: formID, //Actual ID (not Unit Tag)
                    field: thisField,
                    fieldInfo: fieldInfo
                };
    
                //Form starts
                if ( typeof formStarted[formID] === 'undefined' || !formStarted[formID] ){
                    thisEvent.label = 'Began filling out form ID: ' + thisEvent.formID + ' (' + thisEvent.field + ')';
    
                    ga('set', nebula.analytics.metrics.formStarts, 1);
                    nebula.dom.document.trigger('nebula_event', thisEvent);
                    ga('send', 'event', thisEvent.category, thisEvent.action, thisEvent.label);
                    nebula.crm('identify', {'form_contacted': 'CF7 (' + thisEvent.formID + ') Started'}, false);
                    nebula.crm('event', 'Contact Form (' + thisEvent.formID + ') Started (' + thisEvent.field + ')');
                    formStarted[formID] = true;
                }
    
                nebula.updateFormFlow(thisEvent.formID, thisEvent.field, thisEvent.fieldInfo);
    
                //Track each individual field focuses
                if ( !jQuery(this).is('button') ){
                    thisEvent.action = 'Individual Field Focused';
                    thisEvent.label = 'Focus into ' + thisEvent.field + ' (Form ID: ' + thisEvent.formID + ')';
    
                    nebula.dom.document.trigger('nebula_event', thisEvent);
                    ga('send', 'event', thisEvent.category, thisEvent.action, thisEvent.label);
                    window.dataLayer.push(Object.assign(thisEvent, {'event': 'nebula-form-started'}));
                }
            }
    
            nebula.timer(formID, 'start', thisField);
    
            //Individual form field timings
            if ( nebula.timings && typeof nebula.timings[formID] !== 'undefined' && typeof nebula.timings[formID].lap[nebula.timings[formID].laps-1] !== 'undefined' ){
                let labelText = '';
                if ( jQuery(this).parent('.label') ){
                    labelText = jQuery(this).parent('.label').text();
                } else if ( jQuery('label[for="' + jQuery(this).attr('id') + '"]').length ){
                    labelText = jQuery('label[for="' + jQuery(this).attr('id') + '"]').text();
                } else if ( jQuery(this).attr('placeholder').length ){
                    labelText = ' "' + jQuery(this).attr('placeholder') + '"';
                }
                ga('send', 'timing', 'CF7 Form', nebula.timings[formID].lap[nebula.timings[formID].laps-1].name + labelText + ' (Form ID: ' + formID + ')', Math.round(nebula.timings[formID].lap[nebula.timings[formID].laps-1].duration), 'Amount of time on this input field (until next focus or submit).');
            }
        });
    
        //CF7 Submit "Attempts" (submissions of any CF7 form on the HTML-side: before REST API)
        //This metric should always match the "Submit (Processing)" metric or else something is wrong!
        nebula.dom.document.on('wpcf7beforesubmit', function(e){
            try {
                jQuery(e.target).find('button#submit').addClass('active');
    
                let thisEvent = {
                    event: e,
                    category: 'CF7 Form',
                    action: 'Submit (Attempt)', //GA4 Name: "form_attempt"?
                    formID: e.detail.contactFormId, //CF7 Form ID
                    postID: e.detail.containerPostId, //Post/Page ID
                    unitTag: e.detail.unitTag, //CF7 Unit Tag
                };
    
                //If timing data exists
                if ( nebula.timings && typeof nebula.timings[e.detail.unitTag] !== 'undefined' ){
                    thisEvent.formTime = nebula.timer(e.detail.unitTag, 'lap', 'wpcf7-submit-attempt');
                    thisEvent.inputs = nebula.timings[e.detail.unitTag].laps + ' inputs';
                }
    
                thisEvent.label = 'HTML submission attempt for form ID: ' + thisEvent.unitTag;
    
                ga('set', nebula.analytics.dimensions.contactMethod, 'CF7 Form (Attempt)');
                ga('set', nebula.analytics.dimensions.formTiming, nebula.millisecondsToString(thisEvent.formTime) + 'ms (' + thisEvent.inputs + ')');
                nebula.dom.document.trigger('nebula_event', thisEvent);
                ga('send', 'event', thisEvent.category, thisEvent.action, thisEvent.label); //This event is required for the notable form metric!
                window.dataLayer.push(Object.assign(thisEvent, {'event': 'nebula-form-submit-attempt'}));
                nebula.fbq('track', 'Lead', {content_name: 'Form Submit (Attempt)'});
                nebula.clarity('set', thisEvent.category, thisEvent.action);
            } catch {
                ga('send', 'exception', {'exDescription': '(JS) CF7 Catch (cf7 HTML form submit): ' + error, 'exFatal': false});
                nebula.usage('CF7 (HTML) Catch: ' + error);
            }
        });
    
        //CF7 Submit "Processing" (CF7 AJAX response after any submit attempt). This triggers after the other submit triggers.
        //This metric should always match the "Submit (Attempt)" metric or else something is wrong!
        nebula.dom.document.on('wpcf7submit', function(e){
            try {
                let thisEvent = {
                    event: e,
                    category: 'CF7 Form',
                    action: 'Submit (Processing)', //GA4 Name: "form_processing"?
                    formID: e.detail.contactFormId, //CF7 Form ID
                    postID: e.detail.containerPostId, //Post/Page ID
                    unitTag: e.detail.unitTag, //CF7 Unit Tag
                };
    
                thisEvent.label = 'Submission processing for form ID: ' + thisEvent.unitTag;
    
                nebula.crmForm(thisEvent.unitTag); //nebula.crmForm() here because it triggers after all others. No nebula.crm() here so it doesn't overwrite the other (more valuable) data.
    
                ga('set', nebula.analytics.dimensions.contactMethod, 'CF7 Form (Processing)');
                ga('set', nebula.analytics.dimensions.formTiming, nebula.millisecondsToString(thisEvent.formTime) + 'ms (' + thisEvent.inputs + ')'); //This is a backup for the HTML form listener
                nebula.dom.document.trigger('nebula_event', thisEvent);
                ga('send', 'event', thisEvent.category, thisEvent.action, thisEvent.label); //This event is required for the notable form metric!
                window.dataLayer.push(Object.assign(thisEvent, {'event': 'nebula-form-submit-processing'}));
                nebula.fbq('track', 'Lead', {content_name: 'Form Submit (Processing)'});
                nebula.clarity('track', 'Lead', {content_name: 'Form Submit (Processing)'});
    
                jQuery('#' + e.detail.unitTag).find('button#submit').removeClass('active');
                jQuery('.invalid-feedback').addClass('hidden'); //Reset all of the "live" feedback to let CF7 handle its feedback
                jQuery('#cf7-privacy-acceptance').trigger('change'); //Until CF7 has a native invalid indicator for the privacy acceptance checkbox, force the Nebula validator here
            } catch(error){
                ga('send', 'exception', {'exDescription': '(JS) CF7 Catch (wpcf7submit): ' + error, 'exFatal': false});
                nebula.usage('CF7 Catch: ' + error);
            }
        });
    
        //CF7 Invalid (CF7 AJAX response after invalid form)
        nebula.dom.document.on('wpcf7invalid', function(e){
            try {
                let thisEvent = {
                    event: e,
                    category: 'CF7 Form',
                    action: 'Submit (CF7 Invalid)', //GA4 Name: "form_invalid"?
                    formID: e.detail.contactFormId, //CF7 Form ID
                    postID: e.detail.containerPostId, //Post/Page ID
                    unitTag: e.detail.unitTag, //CF7 Unit Tag
                };
    
                //If timing data exists
                if ( nebula.timings && typeof nebula.timings[e.detail.unitTag] !== 'undefined' ){
                    thisEvent.formTime = nebula.timer(e.detail.unitTag, 'lap', 'wpcf7-submit-invalid');
                    thisEvent.inputs = nebula.timings[e.detail.unitTag].laps + ' inputs';
                }
    
                thisEvent.label = 'Form validation errors occurred on form ID: ' + thisEvent.unitTag;
    
                //Apply Bootstrap validation classes to invalid fields
                jQuery('.wpcf7-not-valid').each(function(){
                    jQuery(this).addClass('is-invalid');
                });
    
                nebula.updateFormFlow(thisEvent.unitTag, '[Invalid]');
                ga('set', nebula.analytics.dimensions.contactMethod, 'CF7 Form (Invalid)');
                ga('set', nebula.analytics.dimensions.formTiming, nebula.millisecondsToString(thisEvent.formTime) + 'ms (' + thisEvent.inputs + ')');
                nebula.dom.document.trigger('nebula_event', thisEvent);
                ga('send', 'event', thisEvent.category, thisEvent.action, thisEvent.label);
                window.dataLayer.push(Object.assign(thisEvent, {'event': 'nebula-form-invalid'}));
                ga('send', 'exception', {'exDescription': '(JS) Invalid form submission for form ID ' + thisEvent.unitTag, 'exFatal': false});
                nebula.scrollTo(jQuery('.wpcf7-not-valid').first(), 35); //Scroll to the first invalid input
                nebula.crm('identify', {'form_contacted': 'CF7 (' + thisEvent.unitTag + ') Invalid'}, false);
                nebula.crm('event', 'Contact Form (' + thisEvent.unitTag + ') Invalid');
            } catch(error){
                ga('send', 'exception', {'exDescription': '(JS) CF7 Catch (wpcf7invalid): ' + error, 'exFatal': false});
                nebula.usage('CF7 Catch: ' + error);
            }
        });
    
        //General HTML5 validation errors
        jQuery('.wpcf7-form input').on('invalid', function(e){ //Would it be more useful to capture all inputs (rather than just CF7)? How would we categorize this in GA?
            nebula.debounce(function(){
                let thisEvent = {
                    event: e,
                    category: 'CF7 Form',
                    action: 'Submit (HTML5 Invalid)', //GA4 Name: "form_invalid"?
                    label: 'General HTML5 validation error',
                };
    
                nebula.dom.document.trigger('nebula_event', thisEvent);
                ga('send', 'event', thisEvent.category, thisEvent.action, thisEvent.label);
                window.dataLayer.push(Object.assign(thisEvent, {'event': 'nebula-form-invalid'}));
                nebula.crm('identify', {'form_contacted': 'CF7 HTML5 Validation Error'});
            }, 50, 'invalid form');
        });
    
        //CF7 Spam (CF7 AJAX response after spam detection)
        nebula.dom.document.on('wpcf7spam', function(e){
            try {
                let formInputs = 'Unknown';
                if ( nebula.timings[e.detail.unitTag] && nebula.timings[e.detail.unitTag].laps ){
                    formInputs = nebula.timings[e.detail.unitTag].laps + ' inputs';
                }
    
                let thisEvent = {
                    event: e,
                    category: 'CF7 Form',
                    action: 'Submit (Spam)', //GA4 Name: "form_spam"?
                    formID: e.detail.contactFormId, //CF7 Form ID
                    postID: e.detail.containerPostId, //Post/Page ID
                    unitTag: e.detail.unitTag, //CF7 Unit Tag
                    formTime: nebula.timer(e.detail.unitTag, 'end'),
                    inputs: formInputs
                };
    
                thisEvent.label = 'Form submission failed spam tests on form ID: ' + thisEvent.unitTag;
    
                nebula.updateFormFlow(thisEvent.unitTag, '[Spam]');
                ga('set', nebula.analytics.dimensions.contactMethod, 'CF7 Form (Spam)');
                ga('set', nebula.analytics.dimensions.formTiming, nebula.millisecondsToString(thisEvent.formTime) + 'ms (' + thisEvent.inputs + ')');
                nebula.dom.document.trigger('nebula_event', thisEvent);
                ga('send', 'event', thisEvent.category, thisEvent.action, thisEvent.label);
                window.dataLayer.push(Object.assign(thisEvent, {'event': 'nebula-form-spam'}));
                ga('send', 'exception', {'exDescription': '(JS) Spam form submission for form ID ' + thisEvent.unitTag, 'exFatal': false});
                nebula.crm('identify', {'form_contacted': 'CF7 (' + thisEvent.unitTag + ') Submit Spam'}, false);
                nebula.crm('event', 'Contact Form (' + thisEvent.unitTag + ') Spam');
            } catch(error){
                ga('send', 'exception', {'exDescription': '(JS) CF7 Catch (wpcf7spam): ' + error, 'exFatal': false});
                nebula.usage('CF7 Catch: ' + error);
            }
        });
    
        //CF7 Mail Send Failure (CF7 AJAX response after mail failure)
        nebula.dom.document.on('wpcf7mailfailed', function(e){
            try {
                let formInputs = 'Unknown';
                if ( nebula.timings[e.detail.unitTag] && nebula.timings[e.detail.unitTag].laps ){
                    formInputs = nebula.timings[e.detail.unitTag].laps + ' inputs';
                }
    
                let thisEvent = {
                    event: e,
                    category: 'CF7 Form',
                    action: 'Submit (Mail Failed)', //GA4 Name: "form_failed"?
                    formID: e.detail.contactFormId, //CF7 Form ID
                    postID: e.detail.containerPostId, //Post/Page ID
                    unitTag: e.detail.unitTag, //CF7 Unit Tag
                    formTime: nebula.timer(e.detail.unitTag, 'end'),
                    inputs: formInputs
                };
    
                thisEvent.label = 'Form submission email send failed for form ID: ' + thisEvent.unitTag;
    
                nebula.updateFormFlow(thisEvent.unitTag, '[Failed]');
                ga('set', nebula.analytics.dimensions.contactMethod, 'CF7 Form (Failed)');
                ga('set', nebula.analytics.dimensions.formTiming, nebula.millisecondsToString(thisEvent.formTime) + 'ms (' + thisEvent.inputs + ')');
                nebula.dom.document.trigger('nebula_event', thisEvent);
                ga('send', 'event', thisEvent.category, thisEvent.action, thisEvent.label);
                window.dataLayer.push(Object.assign(thisEvent, {'event': 'nebula-form-failed'}));
                ga('send', 'exception', {'exDescription': '(JS) Mail failed to send for form ID ' + thisEvent.unitTag, 'exFatal': true});
                nebula.crm('identify', {'form_contacted': 'CF7 (' + thisEvent.unitTag + ') Submit Failed'}, false);
                nebula.crm('event', 'Contact Form (' + thisEvent.unitTag + ') Failed');
            } catch(error){
                ga('send', 'exception', {'exDescription': '(JS) CF7 Catch (wpcf7mailfailed): ' + error, 'exFatal': false});
                nebula.usage('CF7 Catch: ' + error);
            }
        });
    
        //CF7 Mail Sent Success (CF7 AJAX response after submit success)
        nebula.dom.document.on('wpcf7mailsent', function(e){
            try {
                formStarted[e.detail.unitTag] = false; //Reset abandonment tracker for this form.
    
                let formInputs = 'Unknown';
                if ( nebula.timings[e.detail.unitTag] && nebula.timings[e.detail.unitTag].laps ){
                    formInputs = nebula.timings[e.detail.unitTag].laps + ' inputs';
                }
    
                //These event may want to correspond to the GA4 event name "generate_lead" and use "value" and "currency" as parameters: https://support.google.com/analytics/answer/9267735 (or consider multiple events?)
    
                let thisEvent = {
                    event: e,
                    category: 'CF7 Form',
                    action: 'Submit (Success)', //GA4 Name: "form_submit" (and also somehow "generate_lead"?)
                    formID: e.detail.contactFormId, //CF7 Form ID
                    postID: e.detail.containerPostId, //Post/Page ID
                    unitTag: e.detail.unitTag, //CF7 Unit Tag ("f" is CF7 form ID, "p" is WP post ID, and "o" is the count if there are multiple per page)
                    formTime: nebula.timer(e.detail.unitTag, 'end'),
                    inputs: formInputs
                };
    
                thisEvent.label = 'Form ID: ' + thisEvent.unitTag;
    
                nebula.updateFormFlow(thisEvent.unitTag, '[Success]');
                if ( !jQuery('#' + e.detail.unitTag).hasClass('.ignore-form') && !jQuery('#' + e.detail.unitTag).find('.ignore-form').length && !jQuery('#' + e.detail.unitTag).parents('.ignore-form').length ){
                    ga('set', nebula.analytics.metrics.formSubmissions, 1);
                }
                ga('set', nebula.analytics.dimensions.contactMethod, 'CF7 Form (Success)');
                ga('set', nebula.analytics.dimensions.formTiming, nebula.millisecondsToString(thisEvent.formTime) + 'ms (' + thisEvent.inputs + ')');
                ga('send', 'timing', thisEvent.category, 'Form Completion (ID: ' + thisEvent.unitTag + ')', Math.round(thisEvent.formTime), 'Initial form focus until valid submit');
                nebula.dom.document.trigger('nebula_event', thisEvent);
                ga('send', 'event', thisEvent.category, thisEvent.action, thisEvent.label);
                window.dataLayer.push(Object.assign(thisEvent, {'event': 'nebula-form-submit-success'}));
                nebula.fbq('track', 'Lead', {content_name: 'Form Submit (Success)'});
                nebula.clarity('set', thisEvent.category, thisEvent.action);
                nebula.crm('identify', {'form_contacted': 'CF7 (' + thisEvent.unitTag + ') Submit Success'}, false);
                nebula.crm('event', 'Contact Form (' + thisEvent.unitTag + ') Submit Success');
    
                //Clear localstorage on submit success on non-persistent forms
                if ( !jQuery('#' + e.detail.unitTag).hasClass('nebula-persistent') && !jQuery('#' + e.detail.unitTag).parents('.nebula-persistent').length ){
                    jQuery('#' + e.detail.unitTag + ' .wpcf7-textarea, #' + e.detail.unitTag + ' .wpcf7-text').each(function(){
                        jQuery(this).trigger('keyup'); //Clear validation
                        localStorage.removeItem('cf7_' + jQuery(this).attr('name'));
                    });
                }
    
                jQuery('#' + e.detail.unitTag).find('.is-valid, .is-invalid').removeClass('is-valid is-invalid'); //Clear all validation classes
            } catch(error){
                ga('send', 'exception', {'exDescription': '(JS) CF7 Catch (wpcf7mailsent): ' + error, 'exFatal': false});
                nebula.usage('CF7 Catch: ' + error);
            }
        });
    };
    

    Override

    To override or disable this JavaScript function, simply redeclare it with the exact same function name.

    JavaScript
    nebula.cf7Functions = function(){
        //Write your own code here, leave it blank, or return false.
    }