1
\$\begingroup\$

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.

\$\endgroup\$

1 Answer 1

3
\$\begingroup\$

I guess I don't really see what value you are getting from these functions.

Isn't the following in essence the same thing without ~100 extra lines of code that don't really do much other than provide some error handling.

var dd = window.digitalData
var someValue = dd.page.pageInfo[0].pageID || '';
dd.page.pageInfo[0].pageID = 'TEST::Page';

With this approach you eliminate the need to parse the string value as what the string really represents - a data structure.


Do you really need to be arbitrarily creating objects/arrays in your data structure the way you are doing with arbitrary levels of nesting?

Taking this arbitrary retrieval/assignment approach to this problem of working with the desired data structure seems like a problem in how you have conceived your implementation.

By my brief perusal of the specification you linked, it would appear that you are dealing with a much more specific data structure than this arbitrary get/set approach conveys.

For example, I am guessing that you might need individual methods/functions managing the different nodes on your data structure (page, product, user, cart, etc.) so that you can actually enforce proper behaviors on those subelements on the data structure.


if(typeof key !== 'string') throw new TypeError('Key must be a dot seperated string.');

The error message here seems incorrect. What does this check have to do with presence of dots in the string? Why not key must be string which actually matches what you are checking for.


Updated comments

So it sounds like your real problem is one of dependency management. As you are developing different versions of your digitalData schema, you currently have no reliable way to ensure that interacting code adheres to your standards.

It seems that you don't really care that each area of the site be on your most recent schema, but rather that each area of the site at least work with a schema that works with your backend data collection system. This is actually OK and a reasonable way to think about working with a large-scale application/site.

To this end, you might consider treating your digitalData code as a version-controlled dependency. If you release version 2.0.0 of the dependency, each different team that develops a different part of the site can then control whether they want to take the new version of the dependency or stick with their 1.X.X version. It would conceivably be OK if portions of the site are on different versions of your dependency as all of those versions have been built to properly interact with the backend data collection mechanism.

With your current approach, you don't know this to be true. Teams could potentially introduce schema changes on the fly and break the data ingestion process.

By taking this approach, you enforce that every development team that works with this dependency must declare which version of the dependency there are going to load into their area of the site. Meanwhile, as the team developing the dependency, you can begin to enforce specific behaviors against the data structure, as I have mentioned, without concern that your changes are going to break portions of the site, as when you release a new version, nothing on the site would actually be using it until those areas update their dependencies to work with your latest release. You push the testing burden onto these individual development teams to make sure they are working with your dependency in an appropriate manner in their portion of the site.

\$\endgroup\$
3
  • \$\begingroup\$ Thank you for your comments Mike. I have added in some additional information to the question to hopefully better communicate why I've developed this getter and setter in the first place. \$\endgroup\$
    – Jeremy H.
    Commented Mar 8, 2017 at 14:19
  • \$\begingroup\$ @JeremyH. I added some updated thoughts with regards to your updated post. \$\endgroup\$
    – Mike Brant
    Commented Mar 8, 2017 at 14:58
  • \$\begingroup\$ Thanks Mike. I appreciate the additional comments. It's difficult to go into all the enterprise-wide difficulties we run into and I do think that goes outside the scope of the programmatic question and review of the code. We'll certainly use your comments to ponder some of our implementation decisions. Always looking to improve. \$\endgroup\$
    – Jeremy H.
    Commented Mar 8, 2017 at 19:29

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.