SPFx Top & Footer Navigation from TermSet (Application Customizer)

SPFx (SharePoint Framework) is the way to go in SharePoint development future and we can daily do more and more with it.

Here is another example how you can create Top and Footer Navigation from TermSet with SPFx Application Customizer.

Create new SPFx project named spfx-react-appcust-globalnav with Yeoman Generator.

yo @microsoft/sharepoint

Choose SharePoint Online only as baseline packages, Extension as type of component and Application Customizer as type of extension named rr87GlobalNavBar.

Open project in Visual Studio Code.

code .

Check package.json file if you have version of @microsoft/… components greater or equal than 1.2.0. If not, change version numbers as this below or update your Yeoman generator template for @microsoft/sharepoint to latest version.

{
  "name": "spfx-react-appcust-globalnav",
  "version": "0.0.1",
  "private": true,
  "engines": {
    "node": ">=0.10.0"
  },
  "dependencies": {
    "@microsoft/sp-core-library": "~1.2.0",
    "@microsoft/sp-webpart-base": "~1.2.0",
    "@types/webpack-env": ">=1.12.1 <1.14.0",
    "@microsoft/sp-listview-extensibility": "1.2.0",
    "@microsoft/sp-application-base": "1.2.0"
  },
  "devDependencies": {
    "@microsoft/sp-build-web": "~1.2.0",
    "@microsoft/sp-module-interfaces": "~1.2.0",
    "@microsoft/sp-webpart-workbench": "~1.2.0",
    "gulp": "~3.9.1",
    "@types/chai": ">=3.4.34 <3.6.0",
    "@types/mocha": ">=2.2.33 <2.6.0"
  },
  "scripts": {
    "build": "gulp bundle",
    "clean": "gulp clean",
    "test": "gulp test"
  }
}

Then delete node_modules folder and run this command:

npm install

Inside src folder create components subfolder and download this file to it (author: @PaoloPia). This is for connection to SharePoint Web Service (client.svc) to get TermSet by name.

Go further to src/extensions/rr87GlobalNavBar folder and create components subfolder.
Inside this newly created folder create two subfolders and all files specified below:

  • GlobalNavBar [folder] – top navigation bar
    • GlobalNavBar.tsx
    • IGlobalNavBarProps.ts – define interface for properies – from SharePoint Term Store (via SPTermStoreService.ts file copied before) we get SP Term Objects
    • IGlobalNavBarState.ts – empty interface for state of our NavBar
  • GlobalFooterBar [folder] – footer navigation bar
    • GlobalFooterBar.tsx
    • IGlobalFooterBarProps.ts – define interface for properies – from SharePoint Term Store (via SPTermStoreService.ts file copied before) we get SP Term Objects
    • IGlobalFooterBarState.ts – empty interface for state of our NavBar

Copy this code to GlobalNavBar.tsx file. It has method for rendering global navbar. It gets menu items from props interface IGlobalNavBarProps.ts and maps them into CommandBar control from Office Fabric UI.

import * as React from 'react';
import styles from '../../Rr87GlobalNavBarApplicationCustomizer.module.scss';
import { IGlobalNavBarProps } from './IGlobalNavBarProps';
import { IGlobalNavBarState } from './IGlobalNavBarState';

import { CommandBar } from 'office-ui-fabric-react/lib/CommandBar';
import { IContextualMenuItem, ContextualMenuItemType } from 'office-ui-fabric-react/lib/ContextualMenu';

import * as SPTermStore from '../../../../components/SPTermStoreService';

export default class GlobalNavBar extends React.Component<IGlobalNavBarProps, IGlobalNavBarState> {
  
       /**
       * Main constructor for the component
       */
      constructor() {
        super();
        this.state = {
        };
      }
  
      private projectMenuItem(menuItem: SPTermStore.ISPTermObject, itemType: ContextualMenuItemType) : IContextualMenuItem {
          return({
            key: menuItem.identity,
            name: menuItem.name,
            itemType: itemType,
            href: menuItem.terms.length == 0 ?
                (menuItem.localCustomProperties["_Sys_Nav_SimpleLinkUrl"] != undefined ?
                    menuItem.localCustomProperties["_Sys_Nav_SimpleLinkUrl"]
                    : null)
                : null,
            subMenuProps: menuItem.terms.length > 0 ?
                { items : menuItem.terms.map((i) => { return(this.projectMenuItem(i, ContextualMenuItemType.Normal)); }) }
                : null,
            isSubMenu: itemType != ContextualMenuItemType.Header,
          });
      }
   
      public render(): React.ReactElement<IGlobalNavBarProps> {
  
        const commandBarItems: IContextualMenuItem[] = this.props.menuItems.map((i) => {
            return(this.projectMenuItem(i, ContextualMenuItemType.Header));
        });
   
        return (
          <div className={`ms-bgColor-neutralLighter ms-fontColor-white ${styles.app}`}>
            <div className={`ms-bgColor-neutralLighter ms-fontColor-white ${styles.top}`}>
                <CommandBar
                className={styles.commandBar}
                isSearchBoxVisible={ false }
                elipisisAriaLabel='More options'
                items={ commandBarItems }
                />
            </div>
          </div>
        );
      }
    }

Same for GlobalFooterBar.tsx here below:

import * as React from 'react';
import styles from '../../Rr87GlobalNavBarApplicationCustomizer.module.scss';
import { IGlobalFooterBarProps } from './IGlobalFooterBarProps';
import { IGlobalFooterBarState } from './IGlobalFooterBarState';

import { CommandBar } from 'office-ui-fabric-react/lib/CommandBar';
import { IContextualMenuItem, ContextualMenuItemType } from 'office-ui-fabric-react/lib/ContextualMenu';

import * as SPTermStore from '../../../../components/SPTermStoreService';

export default class GlobalFooterBar extends React.Component<IGlobalFooterBarProps, IGlobalFooterBarState> {
  
       /**
       * Main constructor for the component
       */
      constructor() {
        super();
        this.state = {
        };
      }
   
      private projectMenuItem(menuItem: SPTermStore.ISPTermObject, itemType: ContextualMenuItemType) : IContextualMenuItem {
          return({
            key: menuItem.identity,
            name: menuItem.name,
            itemType: itemType,
            href: menuItem.terms.length == 0 ?
                (menuItem.localCustomProperties["_Sys_Nav_SimpleLinkUrl"] != undefined ?
                    menuItem.localCustomProperties["_Sys_Nav_SimpleLinkUrl"]
                    : null)
                : null,
            subMenuProps: menuItem.terms.length > 0 ?
                { items : menuItem.terms.map((i) => { return(this.projectMenuItem(i, ContextualMenuItemType.Normal)); }) }
                : null,
            isSubMenu: itemType != ContextualMenuItemType.Header,
          });
      }
  
      public render(): React.ReactElement<IGlobalFooterBarProps> {
  
        const commandBarItems: IContextualMenuItem[] = this.props.menuItems.map((i) => {
            return(this.projectMenuItem(i, ContextualMenuItemType.Header));
        });
  
        return (
          <div className={`ms-bgColor-neutralLighter ms-fontColor-white ${styles.app}`}>
            <div className={`ms-bgColor-neutralLighter ms-fontColor-white ${styles.top}`}>
                <CommandBar
                className={styles.commandBar}
                isSearchBoxVisible={ false }
                elipisisAriaLabel='More options'
                items={ commandBarItems }
                />
            </div>
          </div>
        );
      }
    }

Inside src/extensions/rr87GlobalNavBar create this two files for styling:

  • Rr87GlobalNavBarApplicationCustomizer.module.scss
  • Rr87GlobalNavBarApplicationCustomizer.module.scss.ts

Add this css styles to .scss file:

.app {
    .top {
      height:40px;
      text-align:left;
      line-height:2.5;
      font-weight:bold;
      display: flex;
      align-items: left;
      justify-content: left;
    }
 
    .bottom, a, a:visited, a:hover {
      height:40px;
      text-align:center;
      line-height:2.5;
      font-weight:bold;
      display: flex;
      align-items: center;
      justify-content: center;
    }

    .commandBar {
      width: 100%;
    }

    a {
      text-decoration: none;
      color: white;
      padding-left: 10px;
      padding-right: 10px;
    }
  }

And this code to .scss.ts file:

/* tslint:disable */
require('./Rr87GlobalNavBarApplicationCustomizer.module.css');
const styles = {
  app: 'app_ed8e3377',
  top: 'top_ed8e3377',
  bottom: 'bottom_ed8e3377',
  commandBar: 'commandBar_ed8e3377',
};

export default styles;
/* tslint:enable */

The last one batch of modifications is related to Rr87FlobalNavBarApplicationCustomizer.ts file.

Import React-related things:

import * as React from 'react';
import * as ReactDom from 'react-dom';

From @microsoft/sp-application-base import two additional classes named PlaceholderContent and PlaceholderName.

import {
  BaseApplicationCustomizer,
  PlaceholderContent,
  PlaceholderName
} from '@microsoft/sp-application-base';

In addition import all pre-generated stuff related to Global and Footer NavBar:

import { escape } from '@microsoft/sp-lodash-subset';

import GlobalNavBar from './components/GlobalNavBar/GlobalNavBar';
import { IGlobalNavBarProps } from './components/GlobalNavBar/IGlobalNavBarProps';
import GlobalFooterBar from './components/GlobalFooterBar/GlobalFooterBar';
import { IGlobalFooterBarProps } from './components/GlobalFooterBar/IGlobalFooterBarProps';
import * as SPTermStore from '../../components/SPTermStoreService';

import styles from './Rr87GlobalNavBarApplicationCustomizer.module.scss';

Inside interface for Application Customizer properties define two properties which represent names for header and footer navigation term set:

NavTermSet?: string;
FooterTermSet?: string;

Go to Rr87GlobalNavBarApplicationCustomizer class, define PlaceHolder for top & bottom navbar and define variables for top & bottom items from TermStore.

private _topPlaceholder: PlaceholderContent | undefined;
private _bottomPlaceholder: PlaceholderContent | undefined;
private _topMenuItems: SPTermStore.ISPTermObject[];
private _bottomMenuItems: SPTermStore.ISPTermObject[];

Inside onInit() method call TermStore service to get TermSets for Nav Bar and Footer Bar and then render it to SharePoint modern page.

@override
  public async onInit(): Promise<void> {
    Log.info(LOG_SOURCE, `Initialized ${strings.Title}`);
   
    let termStoreService: SPTermStore.SPTermStoreService = new SPTermStore.SPTermStoreService({
      spHttpClient: this.context.spHttpClient,
      siteAbsoluteUrl: this.context.pageContext.web.absoluteUrl,
    });

    if (this.properties.NavTermSet != null) {
      this._topMenuItems = await termStoreService.getTermsFromTermSetAsync(this.properties.NavTermSet);
    }
    if (this.properties.FooterTermSet != null) {
      this._bottomMenuItems = await termStoreService.getTermsFromTermSetAsync(this.properties.FooterTermSet);
    }

    // Call render method for generating the needed html elements
    this._renderPlaceHolders();

    return Promise.resolve<void>();
  }

  private _renderPlaceHolders(): void {
   
    console.log('Available placeholders: ',
      this.context.placeholderProvider.placeholderNames.map(name => PlaceholderName[name]).join(', '));

    // 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._topMenuItems != null && this._topMenuItems.length > 0) {
        const element: React.ReactElement<IGlobalNavBarProps> = React.createElement(
          GlobalNavBar,
          {
            menuItems: this._topMenuItems,
          }
        );
   
        ReactDom.render(element, this._topPlaceholder.domElement);
      }
    }

    // Handling the bottom placeholder
    if (!this._bottomPlaceholder) {
      this._bottomPlaceholder =
        this.context.placeholderProvider.tryCreateContent(
          PlaceholderName.Bottom,
          { onDispose: this._onDispose });

      // The extension should not assume that the expected placeholder is available.
      if (!this._bottomPlaceholder) {
        console.error('The expected placeholder (Bottom) was not found.');
        return;
      }

      if (this._bottomMenuItems != null && this._bottomMenuItems.length > 0) {
        const element: React.ReactElement<IGlobalNavBarProps> = React.createElement(
          GlobalFooterBar,
          {
            menuItems: this._bottomMenuItems,
          }
        );
   
        ReactDom.render(element, this._bottomPlaceholder.domElement);
      }
    }
  }

  private _onDispose(): void {
    console.log('[TenantGlobalNavBarApplicationCustomizer._onDispose] Disposed custom nav and bottom placeholders.');
  }

This is all from code side. Run next command to serve our extension:

gulp serve --nobrowser

Open a modern site and go to the Term Store Management page of the site settings and create a new Term Group with two Term Sets – one for top Nav Bar named TopNavigation and second for Footer Bar named BottomNavigation.

2017-10-18_1122

Append the following query string with names of Term Sets created before to the modern page URL:

?loadSPFX=true&debugManifestsFile=https://localhost:4321/temp/manifests.js&customActions={"df8d44ba-6145-467b-b411-7c6d3f831d30":{"location":"ClientSideExtension.ApplicationCustomizer","properties":{"NavTermSet":"TopNavigation","FooterTermSet":"BottomNavigation"}}}

[ Complete code on GitHub ]

Cheers!
Gašper Rupnik

{End.}

Advertisements

3 thoughts on “SPFx Top & Footer Navigation from TermSet (Application Customizer)

Add yours

  1. Adding product1 to Products removes the link on Products and converts it to button. Did you experience the same? Please help.

  2. This is great, but I do not know what to write on the IGlobalBAr props and State.ts files

    also

    Property ‘getTermsFromTermSetAsync’ does not exist on type ‘SPTermStoreService’.ts

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google photo

You are commenting using your Google account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s

Powered by WordPress.com.

Up ↑

%d bloggers like this: