Returning a SharePoint 2013 termset in a tree structure using JavaScript

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

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);

    Stories say it best.

    Are you ready to make your workplace awesome? We're keen to hear what you have in mind.

    Interested in learning more about the work we do?

    Explore our culture and transformation services.

    Our commitment to reconciliation

    Learn how Habanero is responding to the Truth and Reconciliation Calls to Action as a settler-owned company operating on Indigenous territories across what is now called Canada.

    Read about our commitment