1

I am working on a sub-site of type Team Site. And inside this sub-site I have two lists:

  • Departments
  • Assets

Where inside the Assets list I have a lookup column (which is single value + Required) referencing the Departments list. so in other way each Department can have zero to many Assets. and each asset much exists under single Department.

Now I want a way to dynamically list my Department and Asset as follow, inside the left side navigation links (quick launch):-

enter image description here

And as users add/edit/delete new departments and new assets, the left side navigation links should be updated accordingly.

I am on SharePoint 2013 on-premise ,, so I have full control over my farm . So can anyone advice on this please? i know i can manually modify the links, but i want my links to be dynamically build.

Thanks

4 Answers 4

2
+100

Here's a small JS solution I created:

enter image description here

Simply modify the settings object to your needs. It does not come with any url's, since you did not specify if you need one, but this can be added without much trouble.

(function () {
    'use strict';

    var settings = {
        listUrl: 'https://sharepoint/Lists/Assets/',
        lookupFieldInternalName: 'Department',
        targetControlSelector: '#sideNavBox .ms-core-listMenu-verticalBox',
        replaceContent: true
    };

    var template = {
        Container:
            '<ul id="zz15_RootAspMenu" class="root ms-core-listMenu-root static">{Departments}</ul>',
        Department:
            '<li class="static">' +
                '<a class="static menu-item ms-core-listMenu-item ms-displayInline ms-navedit-linkNode" tabindex="0" href="#">' +
                    '<span class="additional-background ms-navedit-flyoutArrow">' +
                        '<span class="menu-item-text">{Department}</span>' +
                    '</span>' +
                '</a>' +
                '<ul class="static">' +
                    '{Assets}' +
                '</ul>' +
            '</li>',
        Asset:
            '<li class="static">' +
                '<a class="static menu-item ms-core-listMenu-item ms-displayInline ms-navedit-linkNode" tabindex="0" title="Wiki" href="#">' +
                    '<span class="additional-background ms-navedit-flyoutArrow">' +
                        '<span class="menu-item-text">{Asset}</span>' +
                    '</span>' +
                '</a>' +
            '</li>'
    };

    //some utilities for working with dom elements
    var utilities = {
        focusNextElement: function () {
            //add all elements we want to include in our selection
            var focussableElements = 'a:not([disabled]), button:not([disabled]), input[type=text]:not([disabled]), [tabindex]:not([disabled])';
            if (document.activeElement && document.activeElement.form) {
                var focussable = Array.prototype.filter.call(document.activeElement.form.querySelectorAll(focussableElements),
                function (element) {
                    //check for visibility while always inlcude the current activeElement
                    return element.offsetWidth > 0 || element.offsetHeight > 0 || element === document.activeElement
                });
                var index = focussable.indexOf(document.activeElement);
                if (focussable[index + 1]) {
                    focussable[index + 1].focus();
                }
            }
        },
        getElementByHTML: function (html) {
            var elem = document.createElement('div');
            elem.innerHTML = html;
            return elem.firstChild;
        },
        hasClass: function (el, className) {
            if (el.classList)
                return el.classList.contains(className);
            else
                return !!el.className.match(new RegExp('(\\s|^)' + className + '(\\s|$)'));
        },
        addClass: function (el, className) {
            if (el.classList)
                el.classList.add(className);
            else if (!this.hasClass(el, className)) el.className += ' ' + className;
        },
        removeClass: function (el, className) {
            if (el.classList)
                el.classList.remove(className);
            else if (this.hasClass(el, className)) {
                var reg = new RegExp('(\\s|^)' + className + '(\\s|$)');
                el.className = el.className.replace(reg, ' ');
            }
        },
        guid: function () {
            function s4() {
                return Math.floor((1 + Math.random()) * 0x10000)
                    .toString(16)
                    .substring(1);
            }
            return s4() + s4() + '-' + s4() + '-' + s4() + '-' +
                s4() + '-' + s4() + s4() + s4();
        },
        isNullOrUndefined: function (obj) {
            return typeof obj === 'undefined' || obj === null;
        },
        getTableRowByHTML: function (html) {
            //fix MS Internet Exploder’s lameness
            var temp = document.createElement('div');
            temp.innerHTML = '<table>' + html + '</table>';
            return temp.firstChild;
        },
        log: function (message) {
            if (settings.debugging && window.console && window.console.log) {
                // console is available
                console.log(message);
            }
        },
        runAsync: function (functionToRun, callback) {
            setTimeout(function () {
                functionToRun();
                if (callback) { callback(); }
            }, 0);
        },
        format: function (str, replaceWith) {
            for (var key in replaceWith) { // jshint ignore:line
                str = utilities.replaceAll(str, '{' + key + '}', replaceWith[key]);
            }
            return str;
        },
        replaceAll: function (str, find, replace) {
            return str.replace(new RegExp(find.replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&'), 'g'), replace);
        }
    };

    //helper to get all listItems even when the list contains > 5000 items
    var listUtility = {
        batch: {
            getListItemsByBatch: function (batchSuccess, success, error) {
                var camlQuery = new SP.CamlQuery();
                camlQuery.set_viewXml(
                '<View Scope="RecursiveAll">' +
                    '<ViewFields>' +
                        '<FieldRef Name="ID"/>' +
                        '<FieldRef Name="Title"/>' +
                        '<FieldRef Name="' + settings.lookupFieldInternalName + '"/>' +
                    '</ViewFields>' +
                    '<QueryOptions>' +
                        '<QueryThrottleMode>Override</QueryThrottleMode>' +
                    '</QueryOptions>' +
                    '<RowLimit>5000</RowLimit>' +
                    '<OrderBy Override="true" UseIndexForOrderBy="true" />' +
                '</View>');
                listUtility.batch.executeItemsRequest(camlQuery, batchSuccess, success, error);
            },
            executeItemsRequest: function (camlQuery, batchSuccess, success, error) {
                var ctx = SP.ClientContext.get_current();
                var web = ctx.get_web();
                var lookupList = web.getList(settings.listUrl);
                var itemCollection = lookupList.getItems(camlQuery);

                ctx.load(itemCollection, 'ListItemCollectionPosition', 'Include(ID, Title, ' + settings.lookupFieldInternalName + ')');
                ctx.executeQueryAsync(
                        function () {
                            batchSuccess(itemCollection);

                            //Iterate if more items needs to be fetched
                            if (itemCollection.get_listItemCollectionPosition()) {
                                camlQuery.set_listItemCollectionPosition(itemCollection.get_listItemCollectionPosition());
                                listUtility.batch.executeItemsRequest(camlQuery, batchSuccess, success, error);
                            } else {
                                success();
                            }
                        },
                        function (sender, args) {
                            error(args);
                        });
            }
        }
    };

    var init = function () {
        SP.SOD.executeFunc('sp.js', 'SP.ClientContext', function () {
            var allItems = [];
            listUtility.batch.getListItemsByBatch(
                function (items) {
                    //got batch
                    allItems = allItems.concat(items.get_data());
                },
                function () {
                    //got all batches
                    //--> reformat data
                    var formatted = {};
                    for (var i = 0; i < allItems.length; i++) {
                        var itemTitle = allItems[i].get_fieldValues().Title;
                        var itemDepartment = allItems[i].get_fieldValues().Department.get_lookupValue();
                        formatted[itemDepartment] = formatted[itemDepartment] || [];
                        formatted[itemDepartment].push({
                            title: itemTitle
                        });
                    }

                    var departmentsHtml = '';
                    var departments = Object.keys(formatted);
                    for (var i = 0; i < departments.length; i++) {
                        var department = departments[i];

                        var assetHtml = '';
                        for (var j = 0; j < formatted[department].length; j++) {
                            var asset = formatted[department][j];
                            assetHtml += utilities.format(template.Asset, { Asset: asset.title });
                        }

                        departmentsHtml += utilities.format(template.Department, { Department: department, Assets: assetHtml });
                    }

                    var container = document.createElement('div');
                    container.innerHTML = utilities.format(template.Container, { Departments: departmentsHtml });

                    var target = document.querySelector(settings.targetControlSelector);
                    if (settings.replaceContent) {
                        target.innerHTML = '';
                    }
                    target.appendChild(container);
                },
                function (a) {
                    //error
                    console.log('error while getting the list items - ', a.get_message());
                });
        });
    };

    init();
})();
11
  • Thanks alot Your solution looks promising, as i do not have to write custom actions each time a Dept or Asset get modified. but one question i have before start investigating this .. seems inside your js file,,, you are hard coding the css classes names such as "class="static menu-item ms-core-listMenu-item ms-displayInline" so could these classes got changed in the future by a sharepoint update for example,, and as a results the js will not be working as expected ,, or it might render the links correctly but the link will be on old format settings ?? or this is not the case?
    – John John
    Commented Nov 23, 2016 at 15:35
  • It's unlikely that the classes will change for 2013. If they change you may have to change the template but that should not be a problem.
    – Mx.
    Commented Nov 23, 2016 at 15:41
  • 1
    FYI - If you want to use my solution since you use a teamsite you either have to disable mds or add a few lines to support it. Here's a example for that sharepoint.stackexchange.com/a/193009/10271
    – Mx.
    Commented Nov 23, 2016 at 15:51
  • 1
    You still have the option to either build your own CSS or use the OOTB one like I did. Of course there is a risk that sharepoint changes it's way of doing things but this risk is present in most of the customization's. If you want and advice from me go with the OOTB classnames. The big plus here is that it's the least amount of work while also getting updates (since the classes likely wont change). For now this solution will work in 2016 and o365 when you aren't using the new UI feature.
    – Mx.
    Commented Nov 28, 2016 at 9:01
  • 1
    Now I get you - this surely is another option but as you stated you rely on a live updating UI. In the end it's your choice - you either use a simple solution with a small footprint with the described risk or you use the approach with a construct of eventlisteners (or timerjobs / scheduled tasks) with diffrent risks (such as events not fireing, no live data, api deprecation, etc.).
    – Mx.
    Commented Nov 28, 2016 at 14:04
2

Options 1

  • Custom SharePoint web parts using VB/ C#

    Create a Custom SharePoint web part which is deployed as a feature and overrides the default Quick Launch.

    One way to achieve is it create a layout page and override quick launch section by a webpart zone and add the custom web part to it.

    Second way is add a custom master page and add a delegate control The following link will provide more detail about the approach quick launch control.

Event receivers- It is one good approach to update the quick launch. The quick launch on a page is rendered when the page is rendered. When an item is changed the page is not re rendered so on paper there are no advantages to this approach.

Also look at the following links

a) link1 It is sp2007 but the concept still remains the same.

b) link2

Option 2

  • using REST or JSOM

Using JavaScript to render the assets for a department.

In this approach we create custom Navigation HTML using JS and replace the Quick launch Section.

The JavaScript file can be saved to either Site assets or document library. The page can access the JavaScript by using have reference to the JavaScript file. There are many ways to insert the JavaScript file ref.

a) Add the JS ref directly to a Custom master (i.e. copy of the original master plus JS ref Tag)

b) Add the JS reference to a Layout page

c) Add a Content editor page on the desired home page and add reference. This approach is not recommended as you would need to add it all the pages wherever you need the quick launch html to be replaced

  • When you insert the JavaScript file ref in the master page all the pages in the site will have the custom quick launch.
  • If you are using the same master page as the parent and don't want to render it on the parent site

    There are two ways to do it

a) In the script first check if it the parent site or sub site. If it the parent site do not replace the quick launch else replace the quick launch.

b) Add the Javascript ref to the layout pages

https://www.dynamics101.com/customize-sharepoint-quick-launch-menu-using-javascript-client-object-model/

The sample quick Launch HTML is as follows

<div id=zz14_V4QuickLaunchMenu>
    <ul class="root ms-core-listMenu-root static" id="zz15_RootAspMenu">
      <li class="static">
        <a href="url">
         <span class="menu-item-text">Documents</span>
       </a>
      </li>
      <li class="static">
         <a href="url">
            <span class="menu-item-text">Documents</span>
         </a>
      </li>
    </ul>
  </div>

Use developer tools to confirm the HTML. The best practice is to use Template engine like Mustache to create HTML.

******Option1***

  • Replace Quick Launch with HTML

a)Check if the Id exists then create the HTML with links to the correct Assets URL b)Replace the HTML inside the Div Tag

****** Option 2***

The following quick links will give you an idea how to add or remove quick links.

Add Quick Launch Links

Remove Quick Links

6
  • thanks for your great reply.. now i think i find the path i need to follow. first using event receiver is just a headache in my case, because there are a lot of scenarios i need to take care of , for example renaming an Asset, or rename Department, change the Asset's Department, etc... it is not just add and delete links.. so i think using JSOM will be more flexible in my case, so what ever happen on the 2 lists (Assets & Departments) i do not care, as the JavaScript will get the latest info regardless of what had done...
    – John John
    Commented Nov 19, 2016 at 17:50
  • now in my case i am already using a custom master page, and i can add a reference to the JSOM script.. also i used some JSOM script before to get items from a list,display them inside some custom pages, and reports.. but the thing i am trying to figure out is how to access the left side navigation and build the list inside the JSOM script? can you provide me some links or sample code ? again thanks for your great reply and details description
    – John John
    Commented Nov 19, 2016 at 17:51
  • 1
    I have modified the answer Commented Nov 19, 2016 at 20:06
  • i think using JSOM as mentioned on this link "olafd.wordpress.com/2015/10/06/…" is the way to go.. although the link did not show how to add a link as a child inside another link (seems someone already add a comment on the link asking about this), and sill i am not sure how i can get the data from the Assets list and show the assets under their Departments... would you adivce more on these ? thanks
    – John John
    Commented Nov 20, 2016 at 1:24
  • i tried copying the script as mentioned on this link link inside a script editor, where i got two buttons , now when i click on the "Set Navigation" link , i will get a confirmation alert ("Nodes are added to the navigation.") but the quick lunch links will not be updated.. do you know what can be the reasons ?
    – John John
    Commented Nov 20, 2016 at 13:14
1

You may want to look at the CSOM in sharepoint, query your lists using javascript, and then build your links from there. The only downside is that it won't be instantaneous (ajax) unless you develop it server side (C#/VB). You can always use a mini preloader though to make it fluid. There may also be a workflow based solution but it would be much more complicated to figure out.

With the javascript approach, you can query your list, group by the department as the "key" and then use that to build a <ul><li></li></ul> structure. You probably will only need to query your assets list as it has a column which has the department in it.

https://msdn.microsoft.com/en-us/library/office/hh185007(v=office.14).aspx

4
  • thanks for the reply. but i still need to know the following before i can test your appraoch; 1) where i need to add this script ? so that it will take effect inside the whole subsite. now in my case i am working on a subsite which inherit its master page from its parent. 2) second question, is there a way to access the left-side navigation links inside the javascript so i can build my list and add it there as shown in my question (using ajax based approach is not a requirement at this stage).
    – John John
    Commented Nov 18, 2016 at 23:35
  • 3) third question i was thinking of writting an event receviers or remote event receivers , while will fire when item is added/edit/deleted and each time to add, edit delete the realted navigation link accornidgly ,, so seem it is more comlicated but not sure if it worth the try ? .. can u please advice on these 3 points ?thanks
    – John John
    Commented Nov 18, 2016 at 23:36
  • furth question, now inside this link which you provide "msdn.microsoft.com/en-us/library/office/…" is this consider under JSOM ?? as this link is for SP 2010,, and not sure which approach it is using CSOM ,JSOM ??or what exactly ?
    – John John
    Commented Nov 19, 2016 at 17:42
  • can you adivce further on my comments please?
    – John John
    Commented Nov 21, 2016 at 12:50
1

You can create Event Receiver on both Departments and Assets lists. On ItemAdded events you can create link to Left Navigation programmatically. It will create Left Navigation menu.

On Departments list event receiver:

 public override void ItemAdded(SPItemEventProperties properties)
            {            
                this.EventFiringEnabled = false;
                using (SPWeb web = site.OpenWeb(properties.RelativeWebUrl))
                {
                  web.AllowUnsafeUpdates = true;
                string headingTitle = DepartmentListItem["Title"]; // Head Navigation Title
                string headingUrl = DepartmentListItem["URL"]; // Head Navigation URL 

           // Get the Quick Launch headings.
                SPNavigationNodeCollection ql = web.Navigation.QuickLaunch;

              // If a Resources heading exists, get it.
              SPNavigationNode heading = ql.Cast<SPNavigationNode>().FirstOrDefault(n => n.Title == headingTitle);

              // If the Resources heading does not exist, create it.
              if (heading == null)
              {
                   heading = new SPNavigationNode(headingTitle, headingUrl);
                   heading = ql.AddAsLast(heading);
              }
                web.Update();
                web.AllowUnsafeUpdates = false; 
                }
                base.ItemAdded(properties);
                this.EventFiringEnabled = false;
            } 

On Assets list event receiver:

string headingTitle = AssetsListItem["DepartmentLookup"]; // **Your department lookup field**

// Get the Quick Launch headings.
SPNavigationNodeCollection ql = web.Navigation.QuickLaunch;

// If a Resources heading exists, get it.
SPNavigationNode heading = ql.Cast<SPNavigationNode>().FirstOrDefault(n => n.Title == headingTitle);                     

string itemUrl = AssetsListItem["AssetURL"]; // **Your sublink of Asset**

// If the heading has a SharePoint Dev Center item, get it.
SPNavigationNode item = heading.Children.Cast<SPNavigationNode>().FirstOrDefault(n => n.Url == itemUrl);

// If the item does not exist, create it.
if (item == null)
{
  item = new SPNavigationNode(itemTitle, itemUrl, true);
  item = heading.Children.AddAsLast(item);
}

Same you can also update the left navigation links on ItemUpdated events.

Also see my answer here: Add Link to the Quick Launch Navigation while deploying a webpart page in the module

5
  • i was thinking on such an option, but one problem i will be facing, is that i need to cover many scenarios in my case to get it working correctly. such as rename an item, change the asset's dept, etc... while using js or other client side rendering ,, then i do not have to worry about these scenarios,, as the js will just render the latest data... does this sound valid ?
    – John John
    Commented Nov 23, 2016 at 15:38
  • can you adivce on my above comment please?
    – John John
    Commented Nov 25, 2016 at 0:50
  • So, you are saying that using JS, render the navigation links & display in your on menu ? The code will be on Master Page, right ? Then it will load everytime. While in my solution, once item will be created/updated in Departments or Assets then navgation link will be created/updated. Then done. Enjoy default sharepoint navigation. Commented Nov 28, 2016 at 3:54
  • @Ronal Patel, yes i will add the code to the master page ,, but will check that the JS code will not run unless the users are inside the related subsite.. again your appraoch is good and should work,, but it will require lot of work specifically i need to show the Assets inside the Dept, so what about a scenario that a user edit an Asset and change its Dept, rename a Dept, etc... i feel that it is a lot of work, while using JS is less work, as i do not have to cover each scenario the user will do on the Dept and on the Assets list..
    – John John
    Commented Nov 28, 2016 at 13:46
  • Yeah,. It's good .. Do what you find better & easy.. Commented Nov 29, 2016 at 4:00

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.