Skip to main content
Habanero logo

Returning a SharePoint 2013 termset in a tree structure using JavaScript

Christopher Parsons

Recently, we needed to render out a large drop-down list of navigation items stored in our SharePoint 2013 metadata termset.

Related content

{
  "linkText": "Read more",
  "_key": "6c399f7a9725",
  "headline": "We're Hiring!",
  "content": [
    {
      "children": [
        {
          "_type": "span",
          "marks": [],
          "text": "We're on a mission to change the world of work. Come join us!",
          "_key": "d511880f83ef0"
        }
      ],
      "_type": "block",
      "style": "normal",
      "_key": "d511880f83ef",
      "markDefs": []
    }
  ],
  "_type": "relatedContent",
  "link": "/careers"
}

We created a termset, added terms, then added additional terms within each parent term. But when it came to rendering all of these terms out using JSOM and the methods in the SP.Taxonomy namespace, we realized that there was no way to get the data back structured in the same hierarchy that we inputted.

To fix this issue, we wrote utility methods that get the terms from the term store, create a hierarchical tree of terms based on their path, then sorted the terms if there is custom sorting involved.

To use the following: call Hcf.Util.Termset.getTermSetAsTree() and use the GUID from your termset and a callback function to be run afterwards as your parameters.

/*!
* Termset utilities
*/

var Hcf = Hcf || {};
Hcf.Util = Hcf.Util || {};
Hcf.Util.Termset = Hcf.Util.Termset || {};

(function(module) {

/**
* Returns a termset, based on ID
*
* @param {string} id - Termset ID
* @param {object} callback - Callback function to call upon completion and pass termset into
*/
module.getTermSet = function (id, callback) {
SP.SOD.loadMultiple(['sp.js'], function () {
// Make sure taxonomy library is registered
SP.SOD.registerSod('sp.taxonomy.js', SP.Utilities.Utility.getLayoutsPageUrl('sp.taxonomy.js'));

SP.SOD.loadMultiple(['sp.taxonomy.js'], function () {
var ctx = SP.ClientContext.get_current(),
taxonomySession = SP.Taxonomy.TaxonomySession.getTaxonomySession(ctx),
termStore = taxonomySession.getDefaultSiteCollectionTermStore(),
termSet = termStore.getTermSet(id),
terms = termSet.getAllTerms();

ctx.load(terms);

ctx.executeQueryAsync(Function.createDelegate(this, function (sender, args) {
callback(terms);
}),

Function.createDelegate(this, function (sender, args) { }));
});
});
};


/**
* Returns an array object of terms as a tree
*
* @param {string} id - Termset ID
* @param {object} callback - Callback function to call upon completion and pass termset into
*/
module.getTermSetAsTree = function (id, callback) {
module.getTermSet(id, function (terms) {
var termsEnumerator = terms.getEnumerator(),
tree = {
term: terms,
children: []
};

// Loop through each term
while (termsEnumerator.moveNext()) {
var currentTerm = termsEnumerator.get_current();
var currentTermPath = currentTerm.get_pathOfTerm().split(';');
var children = tree.children;

// Loop through each part of the path
for (var i = 0; i < currentTermPath.length; i++) {
var foundNode = false;

for (var j = 0; j < children.length; j++) {
if (children[j].name === currentTermPath[i]) {
foundNode = true;
break;
}
}

// Select the node, otherwise create a new one
var term = foundNode ? children[j] : { name: currentTermPath[i], children: [] };

// If we're a child element, add the term properties
if (i === currentTermPath.length - 1) {
term.term = currentTerm;
term.title = currentTerm.get_name();
term.guid = currentTerm.get_id().toString();
}

// If the node did exist, let's look there next iteration
if (foundNode) {
children = term.children;
}
// If the segment of path does not exist, create it
else {
children.push(term);

// Reset the children pointer to add there next iteration
if (i !== currentTermPath.length - 1) {
children = term.children;
}
}
}
}

tree = module.sortTermsFromTree(tree);

callback(tree);
});
};


/**
* Sort children array of a term tree by a sort order
*
* @param {obj} tree The term tree
* @return {obj} A sorted term tree
*/
module.sortTermsFromTree = function (tree) {
// Check to see if the get_customSortOrder function is defined. If the term is actually a term collection,
// there is nothing to sort.
if (tree.children.length && tree.term.get_customSortOrder) {
var sortOrder = null;

if (tree.term.get_customSortOrder()) {
sortOrder = tree.term.get_customSortOrder();
}

// If not null, the custom sort order is a string of GUIDs, delimited by a :
if (sortOrder) {
sortOrder = sortOrder.split(':');

tree.children.sort(function (a, b) {
var indexA = sortOrder.indexOf(a.guid);
var indexB = sortOrder.indexOf(b.guid);

if (indexA > indexB) {
return 1;
} else if (indexA < indexB) {
return -1;
}

return 0;
});
}
// If null, terms are just sorted alphabetically
else {
tree.children.sort(function (a, b) {
if (a.title > b.title) {
return 1;
} else if (a.title < b.title) {
return -1;
}

return 0;
});
}
}

for (var i = 0; i < tree.children.length; i++) {
tree.children[i] = module.sortTermsFromTree(tree.children[i]);
}

return tree;
};

})(Hcf.Util.Termset);

And as an example of how you might use getTermSetAsTree:

(function (module) {
// Recursively loop through the termset and create list items
function renderTerm(term) {
var html = '' + term.title + ''; if (term.children && term.children.length) { html += ''; for (var i = 0; i < term.children.length; i++) { html += renderTerm(term.children[i]); } html += ''; } return html + '';
}

module.getTermSetAsTree('d8e8eb27-898f-48b4-ac14-ffac05cd19e0', function (terms) {
var html = '';

// Kick off the term rendering
for (var i = 0; i < terms.children.length; i++) {
html += renderTerm(terms.children[i]);
}

// Append the create HTML to the bottom of the page
var list = document.createElement('ul');
list.innerHTML = html;
document.body.appendChild(list);
});
})(Hcf.Util.Termset);