Skip to content

Commit d88eb99

Browse files
committed
Allow opt-in bubble-phase form tracking
1 parent e79e83c commit d88eb99

File tree

4 files changed

+46
-15
lines changed

4 files changed

+46
-15
lines changed
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
{
2+
"changes": [
3+
{
4+
"packageName": "@snowplow/browser-plugin-form-tracking",
5+
"comment": "Allow opt-in bubble-phase listeners for change/submit",
6+
"type": "none"
7+
}
8+
],
9+
"packageName": "@snowplow/browser-plugin-form-tracking"
10+
}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
{
2+
"changes": [
3+
{
4+
"packageName": "@snowplow/javascript-tracker",
5+
"comment": "",
6+
"type": "none"
7+
}
8+
],
9+
"packageName": "@snowplow/javascript-tracker"
10+
}

plugins/browser-plugin-form-tracking/src/helpers.ts

Lines changed: 18 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,8 @@ const defaultFormTrackingEvents = [
4040

4141
/** Form tracking plugin options to determine which events to fire and the elements to listen for */
4242
interface FormTrackingOptions {
43+
/** Whether to handle events in the capture phase or the bubbling phase. Capture is usually more reliable, but may trigger early if you need changes from other submit handlers in your transforms, filters, or context generators. Defaults to true. */
44+
useCapture?: boolean;
4345
/** List of `form` elements that are allowed to generate events, or criteria for deciding that when the event listener handles the event */
4446
forms?:
4547
| FilterCriterion<HTMLElement>
@@ -86,6 +88,7 @@ const _focusListeners: Record<string, EventListener> = {};
8688
const _changeListeners: Record<string, EventListener> = {};
8789
const _submitListeners: Record<string, EventListener> = {};
8890
const _targets: Record<string, EventTarget[]> = {};
91+
const _captures: Record<string, boolean> = {};
8992

9093
/**
9194
* Add submission/focus/change event listeners to page for forms and elements according to `configuration`
@@ -99,19 +102,21 @@ export function addFormListeners(tracker: BrowserTracker, configuration: FormTra
99102

100103
const events = options?.events ?? defaultFormTrackingEvents;
101104

105+
const useCapture = (_captures[tracker.id] = options?.useCapture ?? true);
106+
102107
const targets = (_targets[tracker.id] = getTargetList(options?.targets, config.forms));
103108

104109
if (events.indexOf(FormTrackingEvent.FOCUS_FORM) !== -1) {
105110
_focusListeners[tracker.id] = getFormChangeListener(tracker, config, FormTrackingEvent.FOCUS_FORM, context);
106-
targets.forEach((target) => addEventListener(target, 'focus', _focusListeners[tracker.id], true));
111+
targets.forEach((target) => addEventListener(target, 'focus', _focusListeners[tracker.id], true)); // focus does not bubble
107112
}
108113
if (events.indexOf(FormTrackingEvent.CHANGE_FORM) !== -1) {
109114
_changeListeners[tracker.id] = getFormChangeListener(tracker, config, FormTrackingEvent.CHANGE_FORM, context);
110-
targets.forEach((target) => addEventListener(target, 'change', _changeListeners[tracker.id], true));
115+
targets.forEach((target) => addEventListener(target, 'change', _changeListeners[tracker.id], useCapture));
111116
}
112117
if (events.indexOf(FormTrackingEvent.SUBMIT_FORM) !== -1) {
113118
_submitListeners[tracker.id] = getFormSubmissionListener(tracker, config, context);
114-
targets.forEach((target) => addEventListener(target, 'submit', _submitListeners[tracker.id], true));
119+
targets.forEach((target) => addEventListener(target, 'submit', _submitListeners[tracker.id], useCapture));
115120
}
116121
}
117122

@@ -145,10 +150,11 @@ function getTargetList(configTargets: EventTarget[] | undefined, forms: FormConf
145150
*/
146151
export function removeFormListeners(tracker: BrowserTracker) {
147152
const targets = _targets[tracker.id] ?? [document];
153+
const useCapture = _captures[tracker.id] ?? true;
148154
targets.forEach((target) => {
149-
if (_focusListeners[tracker.id]) target.removeEventListener('focus', _focusListeners[tracker.id], true);
150-
if (_changeListeners[tracker.id]) target.removeEventListener('change', _changeListeners[tracker.id], true);
151-
if (_submitListeners[tracker.id]) target.removeEventListener('submit', _submitListeners[tracker.id], true);
155+
if (_focusListeners[tracker.id]) target.removeEventListener('focus', _focusListeners[tracker.id], true); // focus does not bubble
156+
if (_changeListeners[tracker.id]) target.removeEventListener('change', _changeListeners[tracker.id], useCapture);
157+
if (_submitListeners[tracker.id]) target.removeEventListener('submit', _submitListeners[tracker.id], useCapture);
152158
});
153159
}
154160

@@ -363,10 +369,13 @@ function getFormChangeListener(
363369
// bind late to the forms/field directly on field focus in this case
364370
if (target !== e.target && e.composed && isTrackableElement(target)) {
365371
if (target.form) {
366-
if (_changeListeners[tracker.id]) addEventListener(target.form, 'change', _changeListeners[tracker.id], true);
367-
if (_submitListeners[tracker.id]) addEventListener(target.form, 'submit', _submitListeners[tracker.id], true);
372+
if (_changeListeners[tracker.id])
373+
addEventListener(target.form, 'change', _changeListeners[tracker.id], _captures[tracker.id]);
374+
if (_submitListeners[tracker.id])
375+
addEventListener(target.form, 'submit', _submitListeners[tracker.id], _captures[tracker.id]);
368376
} else {
369-
if (_changeListeners[tracker.id]) addEventListener(target, 'change', _changeListeners[tracker.id], true);
377+
if (_changeListeners[tracker.id])
378+
addEventListener(target, 'change', _changeListeners[tracker.id], _captures[tracker.id]);
370379
}
371380
}
372381

trackers/javascript-tracker/test/pages/form-tracking.html

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -144,14 +144,15 @@
144144

145145
switch (parseQuery().filter) {
146146
case 'exclude':
147-
snowplow('enableFormTracking', { options: { fields: { denylist: ['fname'] } } });
147+
snowplow('enableFormTracking', { options: { useCapture: true, fields: { denylist: ['fname'] } } });
148148
break;
149149
case 'include':
150-
snowplow('enableFormTracking', { options: { fields: { allowlist: ['lname'] } } });
150+
snowplow('enableFormTracking', { options: { useCapture: false, fields: { allowlist: ['lname'] } } });
151151
break;
152152
case 'filter':
153153
snowplow('enableFormTracking', {
154154
options: {
155+
useCapture: undefined,
155156
forms: { allowlist: ['formy-mcformface'] },
156157
fields: { filter: formFilter },
157158
},
@@ -160,22 +161,23 @@
160161
case 'transform':
161162
snowplow('enableFormTracking', {
162163
options: {
164+
useCapture: true,
163165
fields: { transform: redactPII },
164166
},
165167
});
166168
break;
167169
case 'excludedForm':
168-
snowplow('enableFormTracking', { options: { forms: { denylist: ['excluded-form'] } } });
170+
snowplow('enableFormTracking', { options: { useCapture: false, forms: { denylist: ['excluded-form'] } } });
169171
break;
170172
case 'onlyFocus':
171-
snowplow('enableFormTracking', { options: { events: ['focus_form'] } });
173+
snowplow('enableFormTracking', { options: { useCapture: undefined, events: ['focus_form'] } });
172174
break;
173175
case 'iframeForm':
174176
var forms = iframe.contentWindow.document.getElementsByTagName('form');
175-
snowplow('enableFormTracking', { options: { forms: forms } });
177+
snowplow('enableFormTracking', { options: { useCapture: true, forms: forms } });
176178
break;
177179
case 'shadow':
178-
snowplow('enableFormTracking', { options: { forms: { allowlist: ['shadow-form'] } } });
180+
snowplow('enableFormTracking', { options: { useCapture: false, forms: { allowlist: ['shadow-form'] } } });
179181
break;
180182
default:
181183
snowplow('enableFormTracking', {

0 commit comments

Comments
 (0)