Skip to Content

cf7Functions()

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

JavaScript July 2, 2019

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.

Source File

Located in /assets/js/nebula.js on line 2403.

Note: This function contains 4 to-do comments.

JavaScript
nebula.cf7Functions = function(){
    if ( !jQuery('.wpcf7-form').length ){
        return false;
    }

    jQuery('.wpcf7-form p:empty').remove(); //Remove empty <p> tags within CF7 forms

    var 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
        }
    });

    //Track CF7 forms when they scroll into view (Autotrack). Currently not possible to change category/action/label for just these impressions.
    jQuery('.wpcf7-form').each(function(){
        var thisEvent = {
            category: 'CF7 Form',
            action: 'Impression', //GA4 Name: "form_impression"?
            formID: jQuery(this).closest('.wpcf7').attr('id') || jQuery(this).attr('id'),
        };

        ga('impressionTracker:observeElements', [{
            'id': thisEvent.formID,
            'threshold': 0.25,
            'fieldsObj': { //@todo "Nebula" 0: The fieldsObj doesn't appear to be supported in programmatic impression tracking via Autotrack
                'eventCategory': thisEvent.category, //This doesn't do anything right now. There is a task that is modifying the category in inc/analytics.php (but I'd prefer it be here instead)
            },
        }]);
    });

    //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 button, .wpcf7-form textarea', function(e){
        var formID = jQuery(this).closest('div.wpcf7').attr('id');

        var thisField = e.target.name || jQuery(this).closest('.form-group').find('label').text() || e.target.id || 'Unknown';
        var 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 ){
            var thisEvent = {
                event: e,
                category: 'CF7 Form',
                action: 'Started Form (Focus)',  //GA4 Name: "form_start"?
                formID: formID,
                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({'event': 'nebula-form-started', 'nebula-event': thisEvent});
            }
        }

        nebula.timer(formID, 'start', thisField);

        //Individual form field timings
        if ( nebula && nebula.timings && typeof nebula.timings[formID] !== 'undefined' && typeof nebula.timings[formID].lap[nebula.timings[formID].laps-1] !== 'undefined' ){ //@todo "Nebula" 0: Use optional chaining
            var 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 before submission
    nebula.dom.document.on('wpcf7beforesubmit', function(e){
        jQuery(e.target).find('button#submit').addClass('active');
    });

    //CF7 Invalid (CF7 AJAX response after invalid form)
    nebula.dom.document.on('wpcf7invalid', function(e){
        var thisEvent = {
            event: e,
            category: 'CF7 Form',
            action: 'Submit (Invalid)', //GA4 Name: "form_invalid"?
            formID: e.detail.id,
        };

        //If timing data exists
        if ( nebula && nebula.timings && typeof nebula.timings[e.detail.id] !== 'undefined' ){ //@todo "Nebula" 0: Use optional chaining
            thisEvent.formTime = nebula.timer(e.detail.id, 'lap', 'wpcf7-submit-invalid');
            thisEvent.inputs = nebula.timings[e.detail.id].laps + ' inputs';
        }

        thisEvent.label = 'Form validation errors occurred on form ID: ' + thisEvent.formID;

        //Apply Bootstrap validation classes to invalid fields
        jQuery('.wpcf7-not-valid').each(function(){
            jQuery(this).addClass('is-invalid');
        });

        nebula.updateFormFlow(thisEvent.formID, '[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({'event': 'nebula-form-invalid', 'nebula-event': thisEvent});
        ga('send', 'exception', {'exDescription': '(JS) Invalid form submission for form ID ' + thisEvent.formID, 'exFatal': false});
        nebula.scrollTo(jQuery(".wpcf7-not-valid").first()); //Scroll to the first invalid input
        nebula.crm('identify', {'form_contacted': 'CF7 (' + thisEvent.formID + ') Invalid'}, false);
        nebula.crm('event', 'Contact Form (' + thisEvent.formID + ') Invalid');
    });

    //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(){
            var thisEvent = {
                event: e,
                category: 'CF7 Form',
                action: 'Submit (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({'event': 'nebula-form-invalid', 'nebula-event': thisEvent});
            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){
        var thisEvent = {
            event: e,
            category: 'CF7 Form',
            action: 'Submit (Spam)', //GA4 Name: "form_spam"?
            formID: e.detail.id,
            formTime: nebula.timer(e.detail.id, 'end'),
            inputs: nebula.timings[e.detail.id].laps + ' inputs'
        };

        thisEvent.label = 'Form submission failed spam tests on form ID: ' + thisEvent.formID;

        nebula.updateFormFlow(thisEvent.formID, '[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({'event': 'nebula-form-spam', 'nebula-event': thisEvent});
        ga('send', 'exception', {'exDescription': '(JS) Spam form submission for form ID ' + thisEvent.formID, 'exFatal': false});
        nebula.crm('identify', {'form_contacted': 'CF7 (' + thisEvent.formID + ') Submit Spam'}, false);
        nebula.crm('event', 'Contact Form (' + thisEvent.formID + ') Spam');
    });

    //CF7 Mail Send Failure (CF7 AJAX response after mail failure)
    nebula.dom.document.on('wpcf7mailfailed', function(e){
        var thisEvent = {
            event: e,
            category: 'CF7 Form',
            action: 'Submit (Mail Failed)', //GA4 Name: "form_failed"?
            formID: e.detail.id,
            formTime: nebula.timer(e.detail.id, 'end'),
            inputs: nebula.timings[e.detail.id].laps + ' inputs'
        };

        thisEvent.label = 'Form submission email send failed for form ID: ' + thisEvent.formID;

        nebula.updateFormFlow(thisEvent.formID, '[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({'event': 'nebula-form-failed', 'nebula-event': thisEvent});
        ga('send', 'exception', {'exDescription': '(JS) Mail failed to send for form ID ' + thisEvent.formID, 'exFatal': true});
        nebula.crm('identify', {'form_contacted': 'CF7 (' + thisEvent.formID + ') Submit Failed'}, false);
        nebula.crm('event', 'Contact Form (' + thisEvent.formID + ') Failed');
    });

    //CF7 Mail Sent Success (CF7 AJAX response after submit success)
    nebula.dom.document.on('wpcf7mailsent', function(e){
        formStarted[e.detail.id] = false; //Reset abandonment tracker for this form.

        //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?)

        var thisEvent = {
            event: e,
            category: 'CF7 Form',
            action: 'Submit (Success)', //GA4 Name: "form_submit" (and also somehow "generate_lead"?)
            formID: e.detail.id,
            formTime: nebula.timer(e.detail.id, 'end'),
            inputs: nebula.timings[e.detail.id].laps + ' inputs'
        };

        thisEvent.label = 'Form ID: ' + thisEvent.formID;

        nebula.updateFormFlow(thisEvent.formID, '[Success]');
        if ( !jQuery('#' + e.detail.id).hasClass('.ignore-form') && !jQuery('#' + e.detail.id).find('.ignore-form').length && !jQuery('#' + e.detail.id).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.formID + ')', 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({'event': 'nebula-form-submit-success', 'nebula-event': thisEvent});
        if ( typeof fbq === 'function' ){fbq('track', 'Lead', {content_name: 'Form Submit (Success)'});}
        if ( typeof clarity === 'function' ){clarity('set', thisEvent.category, thisEvent.action);}
        nebula.crm('identify', {'form_contacted': 'CF7 (' + thisEvent.formID + ') Submit Success'}, false);
        nebula.crm('event', 'Contact Form (' + thisEvent.formID + ') Submit Success');

        //Clear localstorage on submit success
        jQuery('#' + e.detail.id + ' .wpcf7-textarea, #' + e.detail.id + ' .wpcf7-text').each(function(){
            jQuery(this).trigger('keyup'); //Clear validation
            localStorage.removeItem('cf7_' + jQuery(this).attr('name'));
        });

        jQuery('#' + e.detail.id).find('.is-valid, .is-invalid').removeClass('is-valid is-invalid'); //Clear all validation classes
    });

    //CF7 Submit (CF7 AJAX response after any submit attempt). This triggers after the other submit triggers.
    nebula.dom.document.on('wpcf7submit', function(e){
        var thisEvent = {
            event: e,
            category: 'CF7 Form',
            action: 'Submit (Attempt)', //GA4 Name: "form_attempt"?
            formID: e.detail.id,
        };

        //If timing data exists
        if ( nebula && nebula.timings && typeof nebula.timings[e.detail.id] !== 'undefined' ){ //@todo "Nebula" 0: Use optional chaining
            thisEvent.formTime = nebula.timer(e.detail.id, 'lap', 'wpcf7-submit-attempt');
            thisEvent.inputs = nebula.timings[e.detail.id].laps + ' inputs';
        }

        thisEvent.label = 'Submission attempt for form ID: ' + thisEvent.formID;

        nebula.crmForm(thisEvent.formID); //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 (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({'event': 'nebula-form-submit-attempt', 'nebula-event': thisEvent});
        if ( typeof fbq === 'function' ){fbq('track', 'Lead', {content_name: 'Form Submit (Attempt)'});}
        if ( typeof clarity === 'function' ){clarity('set', thisEvent.category, thisEvent.action);}

        jQuery('#' + e.detail.id).find('button#submit').removeClass('active');
        jQuery('.invalid-feedback').addClass('hidden');
    });
};

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