Build a breadcrumb using SPFx extensions

A few months ago, I’ve built a solution to add the breadcrumb navigation to all SharePoint sites and pages. While that solution still works fine on SharePoint 2013, 2016 and on the classic experience of SharePoint online it no longer works for the modern sites.

In the previous solution I used a script link to add the breadcrumb globally to the site collection, this functionality was disabled by Microsoft in the modern experience and it was replaced by the SPFx Extensions.

The SPFx Extensions are client-side components that run inside the context of the SharePoint and provide the possibility to manipulate the page DOM to add or modify components.

In this article I’ll provide you the final solution but I’ll also explain step by step how I migrated the code from the old solution to the SPFx.

If you are new to the SPFx development you will need to setup your machine first, all the instructions can be found in this link.

Create the extension project

  1. Create a folder with the name of the project e.g. breadcrumb-extension
  2. Open the console window in the new directory
  3. Type the command yo @microsoft/sharepoint
  4. When prompted:
    • Accept the default app-extension as your solution name, and press Enter.
    • Choose SharePoint Online only (latest), and press Enter.
    • Choose Use the current folder, and press Enter.
    • Choose Y to make the extension available to be added without activating any features.
    • Choose Extension as the client-side component type to be created.
    • Choose Application Customizer as the extension type to be created.
    • Provide a name to the extension. e.g. breadcrumb
    • Provide a description to the extension. e.g. breadcrumb for modern pages
  5. The download of all the requirements might take a few minutes, once it’s done you will see the message bellow indicating the success of the operation.
  6. Type code . to open the project (this will open visual studio code but you can use another editor). The structure of the extension project is like the SPFx web parts projects.

Build the extension

In this project I’ll reuse and adapt the code from my previous article, it was modified from the original version that is available in the PNP git hub repository.

The breadcrumb is built using JSOM but the dependencies to use it are not loaded by default on modern pages. To use JSOM with SPFx solutions you need to explicitly load it first. You can find detailed information about this process in this link.

In this article I’ll follow the declarative approach which requires the absolute path to the JS files.

Note: JavaScript Object Model (JSOM) is no longer the recommended path to develop SharePoint solutions, but there are still valid use cases such code migrations where it can be used. If you are building a solution from scratch consider the use of SharePoint REST API or the PNP JavaScript Core Library.

Reference JSOM Declaratively

To reference JSOM declarative, the first step is to register all the JSOM API resources as external scripts. On your solution go to the folder config and open the file config.json, add the following code to the externals section.

Note: This method requires the absolute path to the.
  "externals": {
    "sp-init": {
      "path": "https://yourtenant.sharepoint.com/_layouts/15/init.js",
      "globalName": "$_global_init"
    },
    "microsoft-ajax": {
      "path": "https://yourtenant.sharepoint.com/_layouts/15/MicrosoftAjax.js",
      "globalName": "Sys",
      "globalDependencies": [
        "sp-init"
      ]
    },
    "sp-runtime": {
      "path": "https://yourtenant.sharepoint.com/_layouts/15/SP.Runtime.js",
      "globalName": "SP",
      "globalDependencies": [
        "microsoft-ajax"
      ]
    },
    "sharepoint": {
      "path": "https://yourtenant.sharepoint.com/_layouts/15/SP.js",
      "globalName": "SP",
      "globalDependencies": [
        "sp-runtime"
      ]
    }
  }

Install TypeScript Typings for SharePoint JSOM

The next step is to install and configure TypeScript typings for SharePoint JSOM. This allows you to benefit from TypeScript’s type safety features when working with SharePoint JSOM.

  1. From the console, execute the following command within your project directory
    npm install @types/microsoft-ajax @types/sharepoint --save-dev
  2. On your solution open the tsconfig.json file, and in the types property, right after the webpack-env entry, add references to microsoft-ajax and sharepoint.
      "compilerOptions": {
        "types": [
          "es6-promise",
          "es6-collections",
          "webpack-env",
          "microsoft-ajax",
          "sharepoint"
        ]
      }
    

Reference SharePoint JSOM Scripts in the solution code

To load the SharePoint JSOM scripts in your SPFx component, you must reference them in the component’s code.

On your solution go to src/extensions/breadcrumb/ and open the file BreadcrumbApplicationCustomizer.ts, after the last import add the following code:

require('sp-init');
require('microsoft-ajax');
require('sp-runtime');
require('sharepoint');

Render Placeholder

  1. Create a new _renderPlaceHolders private method, all the HTML code for the breadcrumb will be generated by this method.
  2. On the _renderPlaceHolders method paste the code bellow:
    // Handling the top placeholder
    if (!this._topPlaceholder) {
      this._topPlaceholder =
    	this.context.placeholderProvider.tryCreateContent(
    	  PlaceholderName.Top,
    	  { onDispose: this._onDispose });
    
      // The extension should not assume that the expected placeholder is available.
      if (!this._topPlaceholder) {
    	console.error('The expected placeholder (Top) was not found.');
    	return;
      }
    
      if (this.properties) {    
    	if (this._topPlaceholder.domElement) {
    	  this._topPlaceholder.domElement.innerHTML = `
    		<div id="breadcrumbWrapper" class="ms-FocusZone">
    		  <ul id="breadcrumbSite" class="ms-Breadcrumb-list"></ul>
    		</div>
    		`;
    		this.LoadSiteBreadcrumb(this);
    	}
      }
    }
  3. Add the functions bellow to the extension class, this is the code from the previous version, it was slightly modified to be compliant with TypeScript. If you used the previous version you will notice that the functions declaration is now different and receive 2 arguments. Since the previous version was written in plain JavaScript the code doesn’t need any other modification, all JavaScript code is valid TypeScript code.
    private LoadSiteBreadcrumb(context): void {
        var breadCrumbNode;
        var clientcontext = SP.ClientContext.get_current();
        var site = clientcontext.get_site();
        var currentWeb = clientcontext.get_web();
        clientcontext.load(currentWeb, 'ServerRelativeUrl', 'Title', 'ParentWeb', 'Url');
        clientcontext.load(site, 'ServerRelativeUrl');
        clientcontext.executeQueryAsync(
        function () {
            var breadcrumbWrapper = document.createElement('div');
            breadcrumbWrapper.className = "ms-Breadcrumb";
            breadcrumbWrapper.innerHTML = '<div class="ms-FocusZone"><ul id="breadcrumbSite" class="ms-Breadcrumb-list"></ul></div>';
    
            var breadCrumbNode = document.getElementById('breadcrumbSite');
            var Custombreadcrumb = document.getElementById('DeltaPlaceHolderMain');
            var breadCrumbNode = document.getElementById('breadcrumbSite');
            if (document.location.pathname.indexOf('SitePages') != -1 || document.location.pathname.indexOf('Pages') != -1) {
                var li = document.createElement('li');
                li.className = "ms-Breadcrumb-listItem";
                if (document.title.split('-').length > 1) {
                    li.innerHTML = '<span class="ms-Breadcrumb-itemLink">' + document.title.split('-')[1].trim() + '</span><i class="ms-Breadcrumb-chevron ms-Icon ms-Icon--ChevronRight"></i>'
                } else {
                    li.innerHTML = '<span class="ms-Breadcrumb-itemLink">' + document.title.trim() + '</span><i class="ms-Breadcrumb-chevron ms-Icon ms-Icon--ChevronRight"></i>'
                }
                breadCrumbNode.insertBefore(li, breadCrumbNode.childNodes[0]);
            }
            else if (document.location.pathname.indexOf('_layouts/15/') != -1) {
                var li = document.createElement('li');
                li.className = "ms-Breadcrumb-listItem";
                li.innerHTML = '<span class="ms-Breadcrumb-itemLink">' + document.title + '</span><i class="ms-Breadcrumb-chevron ms-Icon ms-Icon--ChevronRight"></i>'
                breadCrumbNode.insertBefore(li, breadCrumbNode.childNodes[0]);
            }
    
            var li = document.createElement('li');
            li.className = "ms-Breadcrumb-listItem";
            li.innerHTML = '<a class="ms-Breadcrumb-itemLink" href="' + currentWeb.get_url() + '">' + currentWeb.get_title() + '</a><i class="ms-Breadcrumb-chevron ms-Icon ms-Icon--ChevronRight"></i>'
            if (Custombreadcrumb != null) {
                breadCrumbNode.insertBefore(li, breadCrumbNode.childNodes[0]);
            }
            if (site.get_serverRelativeUrl() !== currentWeb.get_serverRelativeUrl()) {
                context.RecursiveWebBreadcrumb(context, currentWeb.get_serverRelativeUrl());
            }
        }, this.fail);
    }
            
    public RecursiveWebBreadcrumb(context, siteUrl): void {
      var Custombreadcrumb = document.getElementById('contentBox');
      var breadCrumbNode = document.getElementById('breadcrumbSite');
      var clientcontext = new SP.ClientContext(siteUrl);
      var site = clientcontext.get_site();
      var currentWeb = clientcontext.get_web();
      clientcontext.load(currentWeb, 'ServerRelativeUrl', 'Title', 'ParentWeb', 'Url');
      clientcontext.load(site, 'ServerRelativeUrl');
      clientcontext.executeQueryAsync(function () {
    	  if (site.get_serverRelativeUrl() !== currentWeb.get_serverRelativeUrl()) {
    		  var li = document.createElement('li');
    		  li.className = "ms-Breadcrumb-listItem";
    		  li.innerHTML = '<a class="ms-Breadcrumb-itemLink" href="' + currentWeb.get_url() + '">' + currentWeb.get_title() + '</a><i class="ms-Breadcrumb-chevron ms-Icon ms-Icon--ChevronRight"></i>'
    		  var Custombreadcrumb = document.getElementById('contentBox');
    		  breadCrumbNode.insertBefore(li, breadCrumbNode.childNodes[0]);             
    		  context.RecursiveWebBreadcrumb(context, currentWeb.get_parentWeb().get_serverRelativeUrl());
    	  } else {
    		  var li = document.createElement('li');
    		  li.className = "ms-Breadcrumb-listItem";
    		  li.innerHTML = '<a class="ms-Breadcrumb-itemLink" href="' + currentWeb.get_url() + '">' + currentWeb.get_title() + '</a><i class="ms-Breadcrumb-chevron ms-Icon ms-Icon--ChevronRight"></i>'
    		  breadCrumbNode.insertBefore(li, breadCrumbNode.childNodes[0]);
    	  }
      }, this.fail);
    }
        
    private fail(): void {
    	console.log('Unable to load SharePoint BreadCrumb');
    }

Format the breadcrumb

Since this project is focused in the code migration I’ll will the original css file instead of the scss used by the SPFx solutions.

  1. Create a new Breadcrumb.css file inside the src folder of your project
  2. Add the code bellow to the css file
    .ms-dialog .ms-Breadcrumb {
        display: none;
    }
    
    .ms-Breadcrumb {
        margin: 0 0 10px 0;
    }
    
    li.ms-Breadcrumb-listItem:first-child a {
        padding-left: 0;
    }
    
    .ms-Breadcrumb.is-overflow .ms-Breadcrumb-overflow {
        display: inline
    }
    
    .ms-Breadcrumb-chevron {
        font-size: 17px;
        color: #666;
        vertical-align: top;
        margin: 10px 0
    }
    
    .ms-Breadcrumb-list {
        display: inline;
        white-space: nowrap;
        padding: 0;
        margin: 0
    }
    
    .ms-Breadcrumb-list .ms-Breadcrumb-listItem {
        list-style-type: none;
        vertical-align: top;
        margin: 0;
        padding: 0;
        display: inline-block
    }
    
    .ms-Breadcrumb-list .ms-Breadcrumb-listItem:last-of-type .ms-Breadcrumb-chevron {
        display: none
    }
    
    .ms-Breadcrumb-overflow {
        display: none;
        position: relative;
        margin-right: -4px
    }
    
    .ms-Breadcrumb-overflow .ms-Breadcrumb-overflowButton {
        font-size: 12px;
        display: inline-block;
        color: #0078d7;
        margin-right: -4px;
        padding: 12px 8px 3px;
        cursor: pointer
    }
    
    .ms-Breadcrumb-overflowMenu {
        display: none;
        position: absolute
    }
    
    .ms-Breadcrumb-overflowMenu.is-open {
        display: block;
        top: 36px;
        left: 0;
        box-shadow: 0 0 5px 0 rgba(0,0,0,.4);
        background-color: #fff;
        border: 1px solid #c8c8c8;
        z-index: 5
    }
    
    .ms-Breadcrumb-overflowMenu:before {
        position: absolute;
        box-shadow: 0 0 5px 0 rgba(0,0,0,.4);
        top: -6px;
        left: 6px;
        content: ' ';
        width: 16px;
        height: 16px;
        -webkit-transform: rotate(45deg);
        transform: rotate(45deg);
        background-color: #fff
    }
    
    .ms-Breadcrumb-overflowMenu .ms-ContextualMenu {
        border: none;
        box-shadow: none;
        position: relative;
        width: 190px
    }
    
    .ms-Breadcrumb-overflowMenu .ms-ContextualMenu.is-open {
        margin-bottom: 0
    }
    
    .ms-Breadcrumb-itemLink,.ms-Breadcrumb-overflowButton {
        text-decoration: none;
        outline: transparent
    }
    
    .ms-Breadcrumb-itemLink:hover,.ms-Breadcrumb-overflowButton:hover {
        background-color: #f4f4f4;
        cursor: pointer
    }
    
    .ms-Breadcrumb-itemLink:focus,.ms-Breadcrumb-overflowButton:focus {
        outline: 1px solid #767676;
        color: #000
    }
    
    .ms-Breadcrumb-itemLink:active,.ms-Breadcrumb-overflowButton:active {
        outline: transparent;
        background-color: #c8c8c8
    }
    
    .ms-Breadcrumb-itemLink {
        color: #333;
        font-family: Segoe UI Light WestEuropean,Segoe UI Light,Segoe UI,Tahoma,Arial,sans-serif;
        font-size: 14px;
        font-weight: 400;
        display: inline-block;
        padding: 0 4px;
        max-width: 160px;
        white-space: nowrap;
        text-overflow: ellipsis;
        overflow: hidden
    }
    
    .ms-Breadcrumb span:hover{
        cursor: default;
        background:none;
    }
    
    .ms-Breadcrumb a:hover{
        text-decoration: none;
    }
    
    .ms-Breadcrumb a, .ms-Breadcrumb a:visited{
        color: #23527c;
    }
    
    .ms-Breadcrumb-itemLink {
        font-size: 14px;
    }
    
    li.ms-Breadcrumb-listItem i {
        font-size: 14px;
        margin: 4px 0;
    }
    
    #breadcrumbWrapper{
        padding: 5px 18px 2px 18px;
        background-color: #f4f4f4;
    }
    
  3. Open the BreadcrumbApplicationCustomizer.ts file and add the import of the CSS file to the import section
    import './Breadcrumb.css';

Run SPFx extensions

The SharePoint Framework Extensions cannot be tested on the local workbench, you’ll need to test them against a live SharePoint Online site.

  1. Compile your code running the command gulp serve –nobrowser
  2. On your project go to the src folder and copy the id value
  3. To test your extension go to a modern list view page in your SharePoint environment and add the following query string to the URL, replade the ##id## by the id of your solution
  4. ?loadSPFX=true&debugManifestsFile=https://localhost:4321/temp/manifests.js&customActions={"##id##":{"location":"ClientSideExtension.ApplicationCustomizer","properties":{"testMessage":"Hello as property!"}}}

  5. Choose load debug scripts

  6. At the top of your site you should see the breadcrumb

Make the breadcrumb available to all the users

To get the breadcrumb available to all the users it needs to be packaged and installed in the SharePoint Online, to accomplish this follow the steps bellow.

Create CDN to host your extension files

The script files need to be hosted in a CDN, in this article I’ll create a public CDN on SharePoint.

The steps bellow requires the use of SharePoint Management Shell, you can get the latest version from this link.

  1. On your console execute the command Connect-SPOService -Url https://yourtenant-admin.sharepoint.com
  2. Enable the public CDN by running this command Set-SPOTenantCdnEnabled -CdnType Public -Enable $true
  3. On your SharePoint create a new library to host the files e.g. cdn
  4. Make the library a CDN by running the command Add-SPOTenantCdnOrigin -CdnType Public -OriginUrl */cdn
  5. The creation of the CDN can take up to 15 minutes to check the status of the process run the command Get-SPOTenantCdnOrigins -CdnType Public

Package and install solution

  1. On your project go to the config folder and open the package-solution.json and confirm if the property skipFeatureDeployment exists in the solution, if it doesn’t exist add it with the value true.
  2. Open the write-manifest.json and update the value of the cdnBasePath, the URL use the publiccdn.sharepointonline.com as prefix followed by the path to the library https://publiccdn.sharepointonline.com/tenant host name/sites/site/library/folder
  3. To get the basic structure for the packaging run the command gulp bundle
  4. To get the installation package run the command gulp package-solution
  5. On your project structure navigate to sharepoint/solution, in this folder you will find the *.sppkg file
  6. Open your App Catalog and upload the sppkg file
  7. Check the box Make this solution available to all sites in the organization

  8. Navigate to the temp/Deploy folder and drag the files to the CDN library

Make the solution available to all sites of the site collection

To make the extension visible to all the sites it needs to be deployed at the Site Collection scope. Unfortunately to the date I’m writing this article the scope property is not supported by the SPFx feature so I’ll use a workaround to make it available for the entire site collection.

There are a few alternatives to make the extension globally available to the site collection, in this article I’ll use the SPFx extensions CLI from Vardhaman Deshpande.

  1. On your command window run
    npm install spfx-extensions-cli -g
  2. Autenticate on your SharePoint by running the command
    spfx-ext --connect https://yourtenant.sharepoint.com/
  3. To make the custom action available to the web run the command
    spfx-ext add "Breadcrumb" ApplicationCustomizer web <clientSideComponentId>
    • To get the clientSideComponentId expand the dist directory, open the manifest.json and copy the id value

Conclusion

In this article you learned how to create a breadcrumb navigation to the modern SharePoint pages, ate the same time you learned a possible path to migrate code from the classic pages to the SharePoint framework.

This breadcrumb is not the only solution available and while writing this article I found out that there is already a React version available on the PNP GitHub.


No comments yet

Leave a Reply


Web developer focused on SharePoint branding, blogger, tech enthusiast. Travelling and sports are my addictions, knowledge and success are my daily motivations.