A getter and a setter for managing our digitalData object. Has the ability to get and set a value within the digitalData object based on the path. This is to allow developers to work with the data layer when the entire schema may not already be implemented on the page.
I appreciate feedback on best practices and performance improvements. Style comments are always considered as-well. :-)
Example usage:
// Get window.digitalData.page.pageInfo[0].pageID, return
// a blank string if you value is currently set.
var pageId = ddo.get('page.pageInfo[0].pageID', '');
// Get window.digitalData.page.attributes.source.campaignId
var campaignId = ddo.get('page.attributes.source.campaignId');
// Set the pageID at the zero index.
ddo.set('page.pageInfo[0].pageID', 'TEST::Page');
Implementation:
var ddo = {};
/**
* Returns the value within the digitalData object. If a portion of the path doesn't exist,
* return the defaultValue.
*
* @public
* @param {string} key - The path of the property to return.
* @param {*} (defaultValue=null) - The value to return if the property isn't set.
* @returns {*} value.
*/
ddo.get = function(key, defaultValue) {
if(typeof key !== 'string') throw new TypeError('Key must be a string.');
defaultValue = typeof defaultValue === 'undefined' ? null : defaultValue;
var tree = key.split('.');
var base = window.digitalData || {};
for(var i = 0, len = tree.length; i < len; i++) {
var propKey = tree[i];
// See if we are referencing an array in the path.
var arrInd = propKey.indexOf('[');
if(arrInd > -1) {
// Get the name portion of the array we are referencing.
var arrPropKey = propKey.substring(0, arrInd);
if(!Array.isArray(base[arrPropKey]) || base[arrPropKey].length === 0)
return defaultValue;
// Get the index value of the array we are referencing.
var index = propKey.substring(arrInd + 1, propKey.length - 1);
if(isNaN(index)) return defaultValue;
// If the index it outside the bounds of the array, return the defaultValue.
if(index < 0 || index > base[arrPropKey].length) return defaultValue;
// If this is the last property in the path, return the value.
if(i === len - 1) return base[arrPropKey][index];
// Set our base and continue on in the path.
base = base[arrPropKey][index];
continue;
}
// Check if the property exists.
var baseHasProperty = base.hasOwnProperty(propKey);
// If the property in the path exists, and we have further to go
// in our path, set the base and continue on in the path.
if(baseHasProperty && i < len - 1) {
base = base[propKey];
continue;
}
// If the property exists, return the value. Otherwise return the defaultValue.
return baseHasProperty ? base[propKey] : defaultValue;
}
}
/**
* Sets the value within the digitalData object. If a portion of the path doesn't exist,
* it's created.
*
* @public
* @param {string} key - The path of the property to set.
* @param {*} value - The value to set.
* @returns {*} value.
*/
ddo.set = function(key, value) {
if(typeof key !== 'string') throw new TypeError('Key must be a dot seperated string.');
if(typeof value === 'undefined') throw new TypeError('A value must be provided.');
var tree = key.split('.');
var base = window.digitalData || {};
for(var i = 0, len = tree.length; i < len; i++) {
var propKey = tree[i];
// See if we are referencing an array in the path.
var arrInd = propKey.indexOf('[');
if(arrInd > -1) {
// Get the name portion of the array we are referencing.
var arrPropKey = propKey.substring(0, arrInd);
if(!Array.isArray(base[arrPropKey]) && typeof base[arrPropKey] !== 'undefined')
throw new TypeError('You referenced an array index but the property ' +
arrPropKey + ' is not an array.');
// Get the index value of the array we are referencing.
var index = parseInt(propKey.substring(arrInd + 1, propKey.length - 1));
if(isNaN(index)) throw new TypeError('The array index must be a number.');
// Get the array and check its length.
base[arrPropKey] = base[arrPropKey] || [];
var baseLen = base[arrPropKey].length;
// If the index it outside the bounds of the array, throw an error.
if(index !== 0 && (index < 0 || index > baseLen))
throw new RangeError('Trying to save to an index outside the range of the array.');
// Check if we are at the end of the path.
if(i === len - 1) {
// The index requested is one greater than the current length.
// In this case, we can go ahead and push the value onto the end.
if(index === baseLen) {
base[arrPropKey].push(value);
return value;
}
// We are within the bounds of the array and at the end of the
// path, so set the value against the array index provided.
base[arrPropKey][index] = value;
return value;
};
// The index requested is one greater than the current length.
// In this case, we can go ahead and push a blank object onto the end.
if(index === baseLen) base[arrPropKey].push({});
// We aren't at the end of the path, so we know to set the a property
// on the index provided, there needs to be an object there.
if(typeof base[arrPropKey][index] !== 'object')
throw new TypeError('Cannot write to a non-object property.');
// Set our base and continue on in the path.
base = base[arrPropKey][index];
continue;
}
// Check if we are at the end of the path.
if(i === len - 1) {
// We are at the end of the path, but we cannot set a property on a non-object.
if(typeof base !== 'object')
throw new TypeError('Cannot write to a non-object property.');
// We can set the value and get out of here.
base[propKey] = value;
return value;
}
// Set our base and continue on in the path.
base[propKey] = base[propKey] || {};
base = base[propKey];
}
}
Additional Background:
I'd like to provide some additional background and a little bit about the why. First, the unique situation I am in is that I have to write code that is able to read from our data layer across an entire enterprise. We have thousands of pages developed on at least 7-10 different platforms by multiple organizations within our enterprise and at least 5 external development firms.
While the W3C standard for the data layer is fairly specific, it is also meant to be extensible. We've extended it for our use and made some modifications for our needs. While we have a schema that I maintain, there is really no guarantee that every development team has properly implemented our schema. We also can't control that they've upgraded to the latest version of our schema. For this reason, among others, when we read and write to and from the data layer, our code has to be extremely fault tolerant.
Using Mike's example from his answer below, if I want to write to an object sitting in an array I can't just write digitalData.page.pageInfo[0].pageID
because I have no way to ensure each of those properties is available or that pageInfo
is even an array. Therefore, I have to write something more like this:
var dd = window.digitalData = window.digitalData || {};
dd.page = dd.page || {};
dd.page.pageInfo = dd.page.pageInfo || [];
if(Array.isArray(dd.page.pageInfo)) {
if(dd.page.pageInfo.length > 0)
{
var pi = dd.page.pageInfo[0];
if(!Array.isArray(pi) && typeof pi === 'object') {
pi.pageID = 'TEST::TEST-ID';
}
} else {
var pi = {
pageID: 'TEST::TEST-ID'
}
dd.page.pageInfo.push(pi);
}
}
Then to read such value I need something like:
var dd = window.digitalData = window.digitalData || {};
dd.page = dd.page || {};
dd.page.pageInfo = dd.page.pageInfo || [];
var pageID = dd.page.pageInfo[0] ? dd.page.pageInfo[0].pageID || '' : '';
This is what I am trying to clean up. I'd like to have a single fault tolerant way to read and write to and from the data layer, no matter what the current structure of the digitalData
object currently is.