Folders in SharePoint are as necessary as evil. Make the best of it using jQuery and SPServices.

by Patrick Penn, SharePoint Architect, Enthusiast and Entrepreneur.

[UPDATE] Please visit FriendlyFolders on GitHub to retrieve the latest updates of the completely revised solution.

A big thanks goes to our team member Benjamin Linder for developing the main code of this solution and to Marc D. Anderson for dedicating this topic a place in his famous SPServices Stories (SPServices Stories #19).

Let me say right upfront, this post is not about Folders vs Metadata. If you're searching for that, you will find a rather good one here.

Hopefully you know about the benefits of SharePoint and its features like, enterprise keywords, taxonomy and metadata navigation. But sometimes you or your client need a good old folder hierarchy. If you're a valuable consultant you will neither roll your eyes nor surrender but assure him with a smile that you will build a solution that will absolutely meet his needs.

Some Reasons Why Folders are Necessary

Some logical arguments for using folders in SharePoint are:

  • If you have many document types which need different permissions within a single document library. Sure, you can configure dedicated permissions for each single document, but this may be hard to maintain.

  • If your customer needs a quick solution to share documents without manually setting managed metadata for each single document, because they often upload documents in a bulk.

  • If you need to logically group different document types and provide a dynamically generated status based on documents or its metadata, which needs to be displayed on a higher hierarchy level to provide an consolidated overview about the content. Sounds complicated? Practically speaking it may be needed to show a completeness status about documents to deliver, which brought me to the solution dealt with in this post.

  • Another reason is to simply not to overstrain the users, if they're new to SharePoint. They quite likely know the folder structure and you as a consultant have the possibility to provide a solution which include the best of both worlds. To work future-oriented, be creative, for example you're able to automatically define metadata predefined by a documents name or its parent folder or even better by its content. There's an outdated solution on Codeplex, that may give you an idea. Maybe I dedicate a post to this topic in the near future.

While you're reading this, you may think: "Why the hell didn't he use Document Sets?".
We involved this feature in our planning, but there is no metadata navigation within Document Sets, which could have been an advantage. Furthermore the customer needed a multi-level hierarchical structure. As we had no significant arguments for it, the latter was a show stopper for Document Sets.

How to Pep Up Folders

As you can imagine, you have many possibilities to get more out of old and boring folders. For example JSLink may be your favorite approach, but I didn't use it, because this is a migrated solution.

Something like the following is a simple approach to provide much more value to folders.

The presumably simplest way may be consulting SharePoint Designer and add conditional formatting to change the presentation of a document library view.
But if you think further and consider a more professional deployment you may realize, that there must be a better way. Also using XSLT will not be fun to handle the content of multiple subfolders and I'm sure the result will not be a smooth experience either.

To keep things simple and manageable I decided to use jQuery and a powerful javascript library from SharePoint MVP Marc D. Anderson, author of SPServices.

I'm not allowed to publish the whole source code regarding this solution, due to the NDA with our client, but I will hopefully give you enough information to build a solution like this by yourself. Sorry for that!

Let's Begin

I used the following javascript libraries for SharePoint 2013:
jQuery 1.10.2 and SPServices 2013.02a

Because I use jQuery and SPServices in multiple places I decided to place the script references within the custom masterpage. You can do it manually or use a more professional approach like this, by using a custom delegate control.

For testing purpose you can simply add a reference within the content editor webpart. But be sure to implement this before you call the functions.

<script src="/Style%20Library/scripts/jquery/jquery-1.10.2.min.js" type=text/javascript></script>  
<script src="/Style%20Library/scripts/jquery/jquery.SPServices-201302a.js" type=text/javascript></script>  

Congrats! Now you're able to use the full power of jQuery and SPServices!

I want to keep things as simple as possible. So to implement the pepped up folder (PUF) functionality within a document library, I just added a content editor webpart (CEWP) below the list view, which is invisible for users.

Then I added a reference (Content Link) for the PUF-script.

To load a .js file as Content Link you should put the whole code between these tags. The alternative is to use a simple .txt file.

<script type="text/javascript">  
<!--  
  //your code here
-->
</script>  

I like the way using .js files, because of reusability outside a CEWP Content Link.

Logic

  • default folder: Containing files within the folder or in any of its subfolders. The user knows that there will be content and the click will not be for nothing
  • empty folder: No files are contained within the folder and each of its subfolders. So the user doesn't have to look for any content and knows that there is still something to deliver.
  • not reviewed folder: No files are contained within the folder and each of its subfolders and the status is "not reviewed".
  • reviewed folder: The folder status is set to "reviewed" or all child folders are set to "reviewed". Sometimes there are no files to deliver for a specific folder, so it can directly be marked as "reviewed".

Optionally you can notify the user that pepping up begins and load the pepUpFolders function with a short delay.

$(document).ready(function(){
  var loadingNotifyId = SP.UI.Notify.addNotification('Pepping up folders ...', false);
  setTimeout('pepUpFolders();',1100);
});

It may happen, that folders are already updated before the user is able to read the notification, so the delay helps.
You can decide which approach is the best for your users. Our customer wanted to notify users about what's going to happen.

Use this function to retrieve the document libraries root folder from the url parameter if you're currently in any subfolder.

function getQueryStrings() {  
  var assoc  = {};
  var decode = function (s) { return decodeURIComponent(s.replace(/\+/g, " ")); };
  var queryString = location.search.substring(1); 
  var keyValues = queryString.split('&'); 

  for(var i in keyValues) { 
    var key = keyValues[i].split('=');
    if (key.length > 1) {
      assoc[decode(key[0])] = decode(key[1]);
    }
  } 

  return assoc; 
} 

Last but not least the more exciting part. This is where the magic happens.
As I mentioned before this function is a bit truncated, but for the result you'll see a difference regarding folders with and without content. The nice part is, that the most functionality loads asynchronously so the user feels nearly no delay, when navigating through the folder structure.

<script type="text/javascript">  
function pepUpFolders() {  
  $(document).ready(function () {
    var sitecollectionUrl = _spPageContextInfo.siteServerRelativeUrl;
    if (sitecollectionUrl == "/") {
      sitecollectionUrl = "";
    }
    var emptyFolderIconPath = sitecollectionUrl + "/Style Library/scripts/images/folder_empty.gif";
    var folderIconPath = sitecollectionUrl + "/_layouts/15/images/folder.gif?rev=23";
    var loaderIconPath = sitecollectionUrl + "/Style Library/scripts/images/loading.gif";
    var folderPrefix = "";

    /* You need this prefix for SharePoint 2010 to replace folder icons

    switch(_spPageContextInfo.currentLanguage)
    {
      case 1031:
      folderPrefix = "";//"Ordner: ";
      break;

      case 1033:
        folderPrefix = "";//"Folder: ";
        break;
    }*/

    var folderStatusReviewedString = "reviewed";

    var listName = $().SPServices.SPListNameFromUrl();
    var siteUrl = $().SPServices.SPGetCurrentSite();

    //Parsing the server relative url of the current web
    if (siteUrl.startsWith("http")) {
      var siteServerRelativeUrl = siteUrl.match(/\/[^\/]+(.+)?/)[1] + "/";
    }
    else {
      var siteServerRelativeUrl = siteUrl;
    }

    var currentFolderPath;
    var qs = getQueryStrings();
    var rootFolder = decodeURI(qs["RootFolder"]);

    //If the rootFolder is not set, then we are currently in the root folder
    if (rootFolder != null) {
        rootFolder = rootFolder.replace(siteServerRelativeUrl, "");
        currentFolderPath = rootFolder;
    } else {
        //The root folder is not set, then we need to get the rootFolder from list
        $().SPServices({
          operation: "GetList",
          async: false,
          listName: listName,
          completefunc: function (xData, Status) {
            $(xData.responseXML).find("List").each(function () {
              currentFolderPath = $(this).attr("RootFolder");
            });
          }
        });
    }

    currentFolderPath = currentFolderPath.replace(siteServerRelativeUrl, "");
    parentFolderPath = currentFolderPath.substring(0, currentFolderPath.lastIndexOf('/'));
    parentFolderName = currentFolderPath.substring(currentFolderPath.lastIndexOf('/') + 1);

    //We request only a list of folders as we don't need to inspect files from current folder
    var query = "<Query><Where><Eq><FieldRef Name='FSObjType'></FieldRef><Value Type='Lookup'>1</Value></Eq></Where></Query>";
    var queryOptions = '<QueryOptions><Folder><![CDATA[' + currentFolderPath.substring(1) + ']]></Folder></QueryOptions>';
    var viewFields = "<ViewFields Properties='true'><FieldRef Name='Level' /></ViewFields>";

    var promFolders = [];
    promFolders[0] = $().SPServices({
      operation: "GetListItems",
      listName: listName,
      CAMLQuery: query,
      CAMLQueryOptions: queryOptions,
      CAMLViewFields: viewFields
    });

    var promSubFolders = [];
    $.when.apply($, promFolders).done(function(){
      $(promFolders[0].responseXML).SPFilterNode("z:row").each(function () {
        var subFolderPath = $(this).attr("ows_FileRef").split(";#")[1];
        subFolderPath = subFolderPath.replace(siteServerRelativeUrl.substring(1), "").substring(1);
        var subFolderName = $(this).attr("ows_FileLeafRef").split(";#")[1];
        $("[title='" + folderPrefix + subFolderName + "']").attr("src", loaderIconPath);

        //Search for Files in any subfolder and return 1 item per Folder max.
        var query = "<Query><Where><Eq><FieldRef Name='FSObjType'></FieldRef><Value Type='Lookup'>0</Value></Eq></Where></Query>";
        var queryOptions = "<QueryOptions><Folder><![CDATA[" + subFolderPath + "]]></Folder><ViewAttributes Scope='Recursive' /></QueryOptions>";

        $().SPServices({
          operation: "GetListItems",
          async: true,
          listName: listName,
          CAMLRowLimit: 1,
          CAMLQuery: query,
          CAMLQueryOptions: queryOptions,
          completefunc: function (xData, Status) {
            //Replace Folder Icon with empty Folder icon
            if ($(xData.responseXML).find("z\\:row, row").length == 0) {
              $("[title='" + folderPrefix + subFolderName + "']").attr("src", emptyFolderIconPath);
            }
            else {
              $("[title='" + folderPrefix + subFolderName + "']").attr("src", folderIconPath);
            }
          }
        });
      });
    });
  });
}

function getQueryStrings() {  
  var assoc  = {};
  var decode = function (s) { return decodeURIComponent(s.replace(/\+/g, " ")); };
  var queryString = location.search.substring(1); 
  var keyValues = queryString.split('&'); 

  for(var i in keyValues) { 
    var key = keyValues[i].split('=');
    if (key.length > 1) {
      assoc[decode(key[0])] = decode(key[1]);
    }
  } 

  return assoc; 
} 

I'm sure there is something to optimize. But the intention of this post was to show a solution to pep up boring folders and make them more valuable.

I was struggeling with async calls somehow, so Marc D. Anderson recommended me to use promises instead of regular async calls, because of a better controllable program flow.
So, I updated the code and combined both approaches, because of depending calls. Now everything seems to work as expected and it's clear what javascript promises are and how they can help to improve program flow.

I hope this post gives you some fresh ideas how to gain more value out of folders, because they are still necessary, albeit evil.

If you need some advice feel free to contact me or let us know if you found this post useful.