Skip to content

Commit b8bd94d

Browse files
committed
Made form tracking customizable (closes snowplow#287)
1 parent 9febb84 commit b8bd94d

4 files changed

Lines changed: 113 additions & 18 deletions

File tree

Gruntfile.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,7 @@ module.exports = function(grunt) {
8282
dest: 'src/js/lib_managed/lodash.js',
8383
options: {
8484
exports: 'node',
85-
include: 'isArray, isFunction, isString, isObject, isDate, isUndefined, isNull, map, filter, find, compact, isEmpty, clone',
85+
include: 'isArray, isFunction, isString, isObject, isDate, isUndefined, isNull, map, forEach, filter, find, compact, isEmpty, clone',
8686
flags: ['debug']
8787
}
8888
}

examples/web/async-large.html

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,14 @@
3737
window.snowplow_1('setCustomUrl', '/overridden-url/'); // Override the page URL
3838
window.snowplow_1('enableActivityTracking', 10, 10); // Ping every 10 seconds after 10 seconds
3939
window.snowplow_1('enableLinkClickTracking', {blacklist: ['barred']}, true); // Track clicks on links whose class is not "barred"
40-
window.snowplow_1('enableFormTracking'); // Track changes to form fields and form submissions
40+
window.snowplow_1('enableFormTracking', {
41+
forms: {
42+
whitelist: ['formclass']
43+
},
44+
fields: {
45+
blacklist: ['comments']
46+
}
47+
}); // Track changes to form fields and form submissions
4148
// window.snowplow_1('trackPageView', 'Async Test'); // Track the page view with custom title
4249
window.snowplow_1('trackPageView', null , false, [ // Auto-set page title; add page context
4350
{
@@ -235,7 +242,7 @@ <h2>This shows how two users can simultaneously use Snowplow on the same page wi
235242

236243
<p>
237244
Fill in the form fields and click the button to trigger form tracking:
238-
<form id="signup" onsubmit="return false">
245+
<form id="signup" onsubmit="return false" class="formclass">
239246
<fieldset>
240247
<input name="gender" type="radio" value="Male"></input>Male<br>
241248
<input name="gender" type="radio" value="Female"></input>Female<br>
@@ -246,6 +253,7 @@ <h2>This shows how two users can simultaneously use Snowplow on the same page wi
246253
</select><br>
247254
<input name="name" type="text" placeholder="Name"></input><br>
248255
<textarea name="other" placeholder="Other information"></textarea><br>
256+
<textarea name="comments" placeholder="Comments"></textarea><br>
249257
<input type="checkbox" name="email" value="on">Add me to the mailing list
250258
</fieldset>
251259
<input type="submit" value="Submit"></input>

src/js/forms.js

Lines changed: 97 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,15 @@ object.getFormTrackingManager = function (core, trackerId) {
4848
// Tag names of mutable elements inside a form
4949
var innerElementTags = ['textarea', 'input', 'select'];
5050

51+
// Used to mark elements with event listeners
52+
var trackingMarker = trackerId + 'form';
53+
54+
// Filter to determine which forms should be tracked
55+
var formFilter = function (e) {return true;}
56+
57+
// Filter to determine which form fields should be tracked
58+
var fieldFilter = function (e) {return true;}
59+
5160
/*
5261
* Get an identifier for a form, input, textarea, or select element
5362
*/
@@ -77,8 +86,13 @@ object.getFormTrackingManager = function (core, trackerId) {
7786
*/
7887
function getInnerFormElements(elt) {
7988
var innerElements = [];
80-
lodash.map(innerElementTags, function (tagname) {
81-
lodash.map(elt.getElementsByTagName(tagname), function (child) {
89+
lodash.forEach(innerElementTags, function (tagname) {
90+
91+
var trackedChildren = lodash.filter(elt.getElementsByTagName(tagname), function (child) {
92+
return child.hasOwnProperty(trackingMarker);
93+
});
94+
95+
lodash.forEach(trackedChildren, function (child) {
8296
if (child.type === 'submit') {
8397
return;
8498
}
@@ -124,26 +138,95 @@ object.getFormTrackingManager = function (core, trackerId) {
124138
};
125139
}
126140

141+
/**
142+
* Check whether an element has at least one class from a given list
143+
*/
144+
function checkClass(elt, classList) {
145+
var classes = lodash.map(elt.classList),
146+
i;
147+
148+
for (i = 0; i < classes.length; i++) {
149+
if (classList[classes[i]]) {
150+
return true;
151+
}
152+
}
153+
return false;
154+
}
155+
156+
/*
157+
* Convert a criterion object to a filter function
158+
*
159+
* @param object criterion Either {whitelist: [array of allowable strings]}
160+
* or {blacklist: [array of allowable strings]}
161+
* or {filter: function (elt) {return whether to track the element}}
162+
* @param boolean byClass Whether to whitelist/blacklist based on an element's classes (for forms)
163+
* or name attribute (for fields)
164+
*/
165+
function getFilter(criterion, byClass) {
166+
167+
// If the criterion argument is not an object, add listeners to all elements
168+
if (lodash.isArray(criterion) || !lodash.isObject(criterion)) {
169+
return function (elt) {
170+
return true;
171+
};
172+
}
173+
174+
if (criterion.hasOwnProperty('filter')) {
175+
return criterion.filter;
176+
} else {
177+
var inclusive = criterion.hasOwnProperty('whitelist');
178+
var specifiedClasses = criterion.whitelist || criterion.blacklist;
179+
if (!lodash.isArray(specifiedClasses)) {
180+
specifiedClasses = [specifiedClasses];
181+
}
182+
183+
// Convert the array of classes to an object of the form {class1: true, class2: true, ...}
184+
var specifiedClassesSet = {};
185+
for (var i=0; i<specifiedClasses.length; i++) {
186+
specifiedClassesSet[specifiedClasses[i]] = true;
187+
}
188+
189+
if (byClass) {
190+
return function (elt) {
191+
return checkClass(elt, specifiedClassesSet) === inclusive;
192+
};
193+
} else {
194+
return function (elt) {
195+
return elt.name in specifiedClassesSet === inclusive;
196+
};
197+
}
198+
}
199+
}
200+
127201
return {
128202

203+
/*
204+
* Configures form tracking: which forms and fields will be tracked, and the context to attach
205+
*/
206+
configureFormTracking: function (config, context) {
207+
if (config) {
208+
formFilter = getFilter(config.forms, true);
209+
fieldFilter = getFilter(config.fields, false);
210+
}
211+
},
212+
129213
/*
130214
* Add submission event listeners to all form elements
131215
* Add value change event listeners to all mutable inner form elements
132216
*/
133217
addFormListeners: function (context) {
134-
var trackingMarker = trackerId + 'form';
135-
136-
lodash.map(innerElementTags, function (tagname) {
137-
lodash.map(document.getElementsByTagName(tagname), function (innerElement) {
138-
if (!innerElement[trackingMarker]) {
139-
helpers.addEventListener(innerElement, 'change', getFormChangeListener(context), false);
140-
innerElement[trackingMarker] = true;
141-
}
142-
});
143-
});
218+
lodash.forEach(document.getElementsByTagName('form'), function (form) {
219+
if (formFilter(form) && !form[trackingMarker]) {
220+
221+
lodash.forEach(innerElementTags, function (tagname) {
222+
lodash.forEach(form.getElementsByTagName(tagname), function (innerElement) {
223+
if (fieldFilter(innerElement) && !innerElement[trackingMarker]) {
224+
helpers.addEventListener(innerElement, 'change', getFormChangeListener(context), false);
225+
innerElement[trackingMarker] = true;
226+
}
227+
});
228+
});
144229

145-
lodash.map(document.getElementsByTagName('form'), function (form) {
146-
if (!form[trackingMarker]) {
147230
helpers.addEventListener(form, 'submit', getFormSubmissionListener(context));
148231
form[trackingMarker] = true;
149232
}

src/js/tracker.js

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1021,13 +1021,17 @@
10211021
* An event will be fired when a form field is changed or a form submitted.
10221022
* This can be called multiple times: only forms not already tracked will be tracked.
10231023
*
1024+
* @param object config Configuration object determining which forms and fields to track.
1025+
* Has two properties: "forms" and "fields"
10241026
* @param array context Context for all form tracking events
10251027
*/
1026-
enableFormTracking: function (context) {
1028+
enableFormTracking: function (config, context) {
10271029
if (mutSnowplowState.hasLoaded) {
1030+
formTrackingManager.configureFormTracking(config);
10281031
formTrackingManager.addFormListeners(context);
10291032
} else {
10301033
mutSnowplowState.registeredOnLoadHandlers.push(function () {
1034+
formTrackingManager.configureFormTracking(config);
10311035
formTrackingManager.addFormListeners(context);
10321036
});
10331037
}

0 commit comments

Comments
 (0)