0

Background/Problem

Our customer recently switched from a choice column to using a managed metadata field to capture data in a list. They really love the ability to add new values on the fly, as well as the ability to merge terms together when they refer to the same underlying value.

However, they're not thrilled with the interface for filtering on the field in a regular list view (using the column header). The big problem is that the column header shows every term in the term set, regardless of whether any items in the current view use those terms.

End Goal

Ideally, the filter dropdown would only show values that appear in the view.

What are our options for providing an interface for the user to filter a list view by a managed metadata column?

Constraints

This is for SharePoint 2016 on-premises (thus with "classic" views of lists and libraries, not the modern UX).

I'm open to injecting JavaScript onto the page via Content Editor/Script Editor web parts, client side rendering, or through a SPFx web part if it makes sense.

1 Answer 1

0

Using REST to get a subset of Managed Metadata Field Values

The solution I ended up using was to inject JavaScript onto the page to populate an HTML dropdown (<select>) element with all the managed metadata field values available in the current view. Choosing an option from the dropdown refreshes the page with query string parameters applied to automatically filter the list view by the chosen managed metadata term.

1. Get necessary information/parameters

Querying a Managed Metadata field through REST can be frustrating because the REST interface only gives you numbers and GUIDs, without the meaningful labels. However, Managed Metadata columns also have a related hidden text field which you can access through REST. The text field itself is named with a random string but you can perform a couple preliminary queries to determine its internal name.

  1. Get the internal name of your managed metadata column. For example, Supplier_x0020_Name
  2. Plug https://[your url here]/_api/lists/getByTitle('[ListName]')/fields?$filter=internalname eq 'Supplier_x0020_Name' in your browser (replacing Supplier_x0020_Name with the internal name of your managed metadata column
  3. Look for the <TextField> element and grab its inner value, which will be a beautiful GUID. This is the ID of the text field that we're going to query to get the Managed Metadata labels.screenshot of TextField XML element
  4. To get the internal name of that field, plug https://[your url here]/_api/lists/getByTitle('[ListName]')/fields(guid'your-beautiful-guid-here') into your browser, replacing your-beautiful-guid-here with the GUID from the previous step. Now you can grab the internal name of the text field from the <InternalName> element. screenshot of InternalName XML element
  5. Next, determine the REST-like filter string that matches the filter settings of your list view. In my case, my list view was filtered to only show items where a Review Status column was equal to "Active", so my REST filter string looked like this: $filter=Review_x0020_Status eq 'Active'

Now you've got the internal name of your managed metadata field, the internal name of the related text field, and the filter string you want to use to limit the results. This is everything you need to programmatically build your own interface for filtering on the managed metadata field.

2. Build the interface

In my case, I'm going to add an HTML <select> dropdown that contains the list of managed metadata values. This will appear on the same page as my list view, in a content editor web part above it. Selecting an option from the dropdown will refresh the page with the filter applied.

  1. Create an HTML file in Visual Studio Code or your favorite text editor. We'll upload this file to a library on the site, then embed it on a page through the "Content Link" property of a Content Editor web part.
  2. We'll start by adding the HTML dropdown that we'll use to filter:

     <div class="CustomFilterSection">Filter by Supplier Name: 
          <select id="CustomFilter">
               <option value="">(Select an option...)</option>
          </select>
     </div>
    
  3. Then add a script block to contain the code we want to execute when the page loads.

    <script></script>
    
  4. Inside the script block, set variables to store all the environment-specific info for your scenario, such as the web url, list name, and the information we determined above with the managed metadata text field.

    var webUrl = "http://your_url_here",
        listName = "Invoices",
        mgdMetadataField = "Supplier_x0020_Name",
        textField = "b47a28aba1e54c0ba495efe1c92ec811",
        filterString = "Invoice_x0020_Review_x0020_Status eq 'Active'",
        selectDropdown =  document.getElementById("CustomFilter");
    
  5. Define a general function for retrieving items through REST

    function getListItems(webUrl, listTitle, options, onSuccess, onFailure){
        var xhr = new XMLHttpRequest();
        var verb = "GET";
        var url = webUrl + "/_api/web/lists/GetByTitle('"+listTitle+"')/items" + (options ? "?"+options : "") ;
        xhr.open(verb,url,true);
        xhr.setRequestHeader("Content-Type","application/json; odata=verbose");
        xhr.setRequestHeader("Accept","application/json; odata=verbose");
        xhr.onreadystatechange = function(){
            if(xhr.readyState == 4){
                if(xhr.status == 200){
                    onSuccess(JSON.parse(xhr.responseText).d.results);
                }else{
                    onFailure(xhr.status + ":\n " + xhr.responseText);
                }
            }
        };
        xhr.send();
    }
    
  6. Now define a function that uses that to get a filtered subset of items from our list.

    function getFilteredResultSet(onSuccess, onFailure){
        getListItems(webUrl, listName,"$top=5000&$filter="+filterString+"&$select="+textField+","+mgdMetadataField+"/Label",onSuccess,onFailure);
    }    
    
  7. Now we need to define a function that takes the results and populates our HTML <select> with options for us.

    function populateFilterDropdown(selectElement, results){
    }
    
  8. In the populateFilterDropdown function, create an empty object var termDictionary = {} to store a map of all the term GUIDs we encounter so that we can ignore the ones we've already captured.

  9. In the populateFilterDropdown function, create an empty array var terms = [] to capture all the managed metadata field values. Technically these will already be captured in the dictionary from the previous step, but putting them in an array allows us to sort them pretty effortlessly.
  10. In the populateFilterDropdown function, loop through the results and populate our array.

    for(var i = 0, len = results.length; i < len; i++){ // loop through the items to build array of terms
        var item = results[i];
        if(item[textField]){
            var labelGuid = item[textField].split("|");
            var label = labelGuid[0], guid = labelGuid[1];
            if(termDictionary[guid]){
            }else{
                var id = item[mgdMetadataField].Label;
                termDictionary[guid] = {label:label,id:id};
                terms.push({label:label,id:id,guid:guid});
            }
        }
    }
    
  11. Still in the populateFilterDropdown function, sort our terms array alphabetically: terms.sort(function(a,b){return a.label.toUpperCase() < b.label.toUpperCase() ? -1 : 1});

  12. Now that the terms are sorted, add them to our <select> dropdown.

    for(var i = 0, len = terms.length; i < len; i++){ // add the terms to the filter dropdown
        var term = terms[i];
        var option = selectElement.appendChild(document.createElement("option"));
        option.innerHTML = term.label;
        option.value = i;
    }
    
  13. Now we just need to add an event handler to our <select> element and have it apply the filter when we make a selection. In this case, I'm cheating and using the built-in list view behavior in which it filters based on query string parameters.

    selectElement.onchange = function(){
        var selectedTerm = terms[selectElement.value];
        window.location.href = window.location.href.replace(window.location.search,"")+"?FilterField1="+escapeProperly(mgdMetadataField)+"&FilterValue1="+selectedTerm.id+"&FilterOp1=In&FilterLookupId1=1&FilterData1=0%2C"+selectedTerm.guid;
    };
    
  14. That wraps up the populateFilterDropdown function. Now tie it all together by calling getFilteredResultSet and invoking populateFilterDropdown in the onSuccess callback.

    getFilteredResultSet( 
        function(results){populateFilterDropdown(selectDropdown,results); }, // on success
        function(message){alert(message);} // on failure
    );
    

The final code looks like this:

<div class="CustomFilterSection">Filter by Supplier Name: 
    <select id="CustomFilter">
        <option value="">(Select an option...)</option>
    </select>
</div>
<script>
    if(!escapeProperly){
        var escapeProperly = function a(f,g,h,i){var c="",b,d=0,k=" \"%<>'&";if(typeof f=="undefined")return"";for(d=0;d<f.length;d++){var a=f.charCodeAt(d),e=f.charAt(d);if(g&&(e=="#"||e=="?")){c+=f.substr(d);break}if(h&&e=="&"){c+=e;continue}if(a<=127){if(i)c+=e;else if(a>=97&&a<=122||a>=65&&a<=90||a>=48&&a<=57||g&&a>=32&&a<=95&&k.indexOf(e)<0)c+=e;else if(a<=15)c+="%0"+a.toString(16).toUpperCase();else if(a<=127)c+="%"+a.toString(16).toUpperCase()}else if(a<=2047){b=192|a>>6;c+="%"+b.toString(16).toUpperCase();b=128|a&63;c+="%"+b.toString(16).toUpperCase()}else if((a&64512)!=55296){b=224|a>>12;c+="%"+b.toString(16).toUpperCase();b=128|(a&4032)>>6;c+="%"+b.toString(16).toUpperCase();b=128|a&63;c+="%"+b.toString(16).toUpperCase()}else if(d<f.length-1){a=(a&1023)<<10;d++;var j=f.charCodeAt(d);a|=j&1023;a+=65536;b=240|a>>18;c+="%"+b.toString(16).toUpperCase();b=128|(a&258048)>>12;c+="%"+b.toString(16).toUpperCase();b=128|(a&4032)>>6;c+="%"+b.toString(16).toUpperCase();b=128|a&63;c+="%"+b.toString(16).toUpperCase()}}return c}
    }
    // Replace these with your own values:
    var webUrl = "https://yoursitehere/site/web",
        listName = "your list name here",
        mgdMetadataField = "Supplier_x0020_Name",
        textField = "b47a28aba1e54c0ba495efe1c92ec811",
        filterString = "Review_x0020_Status eq 'Active'",
        selectDropdown =  document.getElementById("CustomFilter");

    getFilteredResultSet( 
        function(results){populateFilterDropdown(selectDropdown,results); }, // on success
        function(message){alert(message);} // on failure
    );

    function getFilteredResultSet(onSuccess, onFailure){
        getListItems(webUrl, listName,"$top=5000&$filter="+filterString+"&$select="+textField+","+mgdMetadataField+"/Label",onSuccess,onFailure);
    }

    /**
    * Loop through an array of items and populate an HTML select with an option for each managed metadata field value discovered
    * @param {HTMLElement} selectElement A reference to the HTML select element to populate
    * @param {Array} results The array of SharePoint list items to process
    **/
    function populateFilterDropdown(selectElement, results){
        var termDictionary = {}, terms = [];
        for(var i = 0, len = results.length; i < len; i++){ // loop through the items to build array of terms
            var item = results[i];
            if(item[textField]){
                var labelGuid = item[textField].split("|");
                var label = labelGuid[0], guid = labelGuid[1];
                if(termDictionary[guid]){
                }else{
                    var id = item[mgdMetadataField].Label;
                    termDictionary[guid] = {label:label,id:id};
                    terms.push({label:label,id:id,guid:guid});
                }
            }
        }
        terms.sort(function(a,b){return a.label.toUpperCase() < b.label.toUpperCase() ? -1 : 1}); //sort the terms alphabetically
        for(var i = 0, len = terms.length; i < len; i++){ // add the terms to the filter dropdown
            var term = terms[i];
            var option = selectElement.appendChild(document.createElement("option"));
            option.innerHTML = term.label;
            option.value = i;
        }
        selectElement.onchange = function(){
            var selectedTerm = terms[selectElement.value];
            window.location.href = window.location.href.replace(window.location.search,"")+"?FilterField1="+escapeProperly(mgdMetadataField)+"&FilterValue1="+selectedTerm.id+"&FilterOp1=In&FilterLookupId1=1&FilterData1=0%2C"+selectedTerm.guid;
        };
    }  

    /**
    * Retrieve list items from a list
    * @param {string} webUrl The full path of the website, including http/s and excluding any trailing slash
    * @param {string} listTitle The name of the list on the given website
    * @param {string} options Any query string parameters to append to the GET request, such as filter and orderby values
    * @param {function} onSuccess Function to execute upon successfully retrieving the items. Accepts an array of list items as a parameter.
    * @param {function} onFailure Function to execute upon failure to retrieve the items. Accepts a string with the response status and message.
    */
    function getListItems(webUrl, listTitle, options, onSuccess, onFailure){
        var xhr = new XMLHttpRequest();
        var verb = "GET";
        var url = webUrl + "/_api/web/lists/GetByTitle('"+listTitle+"')/items" + (options ? "?"+options : "") ;
        xhr.open(verb,url,true);
        xhr.setRequestHeader("Content-Type","application/json; odata=verbose");
        xhr.setRequestHeader("Accept","application/json; odata=verbose");
        xhr.onreadystatechange = function(){
            if(xhr.readyState == 4){
                if(xhr.status == 200){
                    onSuccess(JSON.parse(xhr.responseText).d.results);
                }else{
                    onFailure(xhr.status + ":\n " + xhr.responseText);
                }
            }
        };
        xhr.send();
    }

</script>

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.