Introduction
Modern SharePoint architecture gives fantastic OOTB global navigation, and hub site navigation has amazing potential to maintain consistency throughout the portal. Then why do we need to develop custom global navigation in modern SharePoint sites?
Let us talk about the business scenario.
Nowadays, a number of organizations are migrating classic intranet portals to modern sites. Many of them have implemented custom global navigation in a classic environment with a number of links and complex tree structures.
Employees are habitual with this global navigation. So, many organizations prefer similar global navigation in Modern SharePoint sites.
Let's Begin,
Below Artifacts are going to be used,
- SharePoint communication Site
- SharePoint Framework (SPFx)
- Application Customizer
- PnP Taxonomy
- Fluent Ui Command Bar
Steps
- Configure Terms in Term Stores
- Create Solution for Application Customizer
- Import Required Packages
- Create React Component
- Add React Component Reference to Application Customizer
- Get Terms using PnP
- Render Terms using Command Bar and styling
- Test Solution
Step 1 - Configure Terms in Term Store.
Go to URL : https://<TenantName>-admin.sharepoint.com/_layouts/15/online/AdminHome.aspx#/termStoreAdminCenter
Create Term Group.
- Copy Group GUID and Paste it into one document
Create Term Set inside created Term Group
- Enable Use Term set for Site Navigation
- Copy term Set Id and Paste it in one document.
Create Terms Inside create Term Sets.
Step 2 - Create SPFx Solution for Application Customizer
Open Node command Prompt.
Go to your physical location where you want to create a solution.
Once all required packages are installed then you will get the below message.
Open Code in visual studio code by typing below one line code in node command prompt.
C:\Demo\GlobalNavigation>code .
The solution structure looks like the ad below.
Step 3 - Import required packages from npm
Install PnP npm package,
npm install @pnp/sp
Install React Fluent UI Package,
npm install @fluentui/react
Install react and react Dom and Its Dev Dependencies.
npm install react@16.9.0 --save
npm install react-dom@16.9.0 --save
Open package.json file which should look like as below.
{
"name": "sample-demo-application",
"version": "0.0.1",
"private": true,
"main": "lib/index.js",
"scripts": {
"build": "gulp bundle",
"clean": "gulp clean",
"test": "gulp test"
},
"dependencies": {
"@fluentui/react": "^8.22.3",
"@microsoft/decorators": "1.12.1",
"@microsoft/sp-application-base": "1.12.1",
"@microsoft/sp-core-library": "1.12.1",
"@microsoft/sp-dialog": "1.12.1",
"@microsoft/sp-office-ui-fabric-core": "^1.12.1",
"@pnp/sp": "^2.7.0",
"react": "^16.9.0",
"react-dom": "^16.9.0"
},
"devDependencies": {
"@types/react": "16.9.36",
"@types/react-dom": "16.9.8",
"@microsoft/sp-build-web": "1.12.1",
"@microsoft/sp-tslint-rules": "1.12.1",
"@microsoft/sp-module-interfaces": "1.12.1",
"@microsoft/sp-webpart-workbench": "1.12.1",
"@microsoft/rush-stack-compiler-3.7": "0.2.3",
"gulp": "~4.0.2",
"ajv": "~5.2.2",
"@types/webpack-env": "1.13.1"
}
}
Step 4 - Create React Component
Create new folder called "Components" to "..src\extensions\gobalNavigationBar".
Create 3 files inside the components folder.
- GlobalNav.tsx
- IGlobalNavProps.ts
- IGlobalNavState.ts
Add below code to IGlobalNavProps.ts,
export interface IGlobalNavProps {
termGroupId:string;
termSetId: string;
}
Add below code to IGlobalNavState.ts,
import { IOrderedTermInfo } from '@pnp/sp/taxonomy';
export interface IGobalNavState {
loading: boolean;
terms: IOrderedTermInfo[];
}
Add below code to IGlobalNav.tsx,
import * as React from 'react';
import { IGlobalNavProps } from "./IGlobalNavProps";
import { IGobalNavState } from "./IGlobalNavState";
export default class GlobalNav extends React.Component<IGlobalNavProps, IGobalNavState> {
constructor(props: IGlobalNavProps) {
super(props);
this.state = {
loading: false,
terms: []
}
}
public componentDidMount() {
// Code here to get items
}
public render(): React.ReactElement<IGlobalNavProps> {
return (
<div>
Hello World
</div>
);
}
}
Step 5 - Add React Component Reference to Application Customizer
Open GobalNavigationBarApplicationCustomizer.ts file and add the below code.
GobalNavigationBarApplicationCustomizer.ts
import * as React from 'react';
import * as ReactDom from 'react-dom';
import { sp } from "@pnp/sp";
import { override } from '@microsoft/decorators';
import { Log } from '@microsoft/sp-core-library';
import {
PlaceholderContent,
PlaceholderName,
BaseApplicationCustomizer
} from '@microsoft/sp-application-base';
import GlobalNav from "./Components/GlobalNav";
import { IGlobalNavProps } from "./Components/IGlobalNavProps";
import * as strings from 'GobalNavigationBarApplicationCustomizerStrings';
const LOG_SOURCE: string = 'GobalNavigationBarApplicationCustomizer';
/**
* If your command set uses the ClientSideComponentProperties JSON input,
* it will be deserialized into the BaseExtension.properties object.
* You can define an interface to describe it.
*/
export interface IGobalNavigationBarApplicationCustomizerProperties {
// This is an example; replace with your own property
termGroupId: string;
termSetId: string;
}
/** A Custom Action which can be run during execution of a Client Side Application */
export default class GobalNavigationBarApplicationCustomizer
extends BaseApplicationCustomizer<IGobalNavigationBarApplicationCustomizerProperties> {
private _topPlaceholder: PlaceholderContent | undefined;
@override
public onInit(): Promise<void> {
Log.info(LOG_SOURCE, `Initialized ${strings.Title}`);
super.onInit().then(_ => {
console.log("super oninit called");
sp.setup({
spfxContext: this.context
});
}).then((_) => {
console.log("Sp Initilize");
this.context.placeholderProvider.changedEvent.add(this, this._renderPlaceHolders);
});
return Promise.resolve();
}
private _renderPlaceHolders(): void {
console.log("HelloWorldApplicationCustomizer._renderPlaceHolders()");
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.properties) {
// Add refrence of react component to this file.
const element: React.ReactElement<IGlobalNavProps> = React.createElement(
GlobalNav,
{
termGroupId: this.properties.termGroupId,
termSetId: this.properties.termSetId
}
);
ReactDom.render(element, this._topPlaceholder.domElement);
}
}
}
private _onDispose(): void {
console.log('[HelloWorldApplicationCustomizer._onDispose] Disposed custom top and bottom placeholders.');
}
}
Step 6: Get Terms using PnP
Open GlobalNav.tsx file and do the below changes.
GlobalNav.tsx
Add below references to the file.
import { sp } from "@pnp/sp";
import "@pnp/sp/taxonomy";
import { dateAdd, PnPClientStorage } from "@pnp/common";
import { IOrderedTermInfo } from '@pnp/sp/taxonomy';
const myKey: string = "navigationElements";
Update below code to GlobalNav.tsx.
private store = new PnPClientStorage();
constructor(props: IGlobalNavProps) {
super(props);
this.state = {
loading: false,
terms: []
}
}
Update componentdidmount() method with below code in GlobalNav.tsx.
public componentDidMount() {
this.setState({}, async () => {
// this portion is responsible for getting terms from term store
const cachedTermInfo = await this.store.local.getOrPut(myKey, () => {
return sp.termStore.groups.getById(this.props.termGroupId).sets.getById(this.props.termSetId).getAllChildrenAsOrderedTree({ retrieveProperties: true });
}, dateAdd(new Date(), "minute", 10));
if (cachedTermInfo.length > 0) {
console.log(cachedTermInfo);
this.setState({ terms: cachedTermInfo });
}
});
Step 7 - Render Terms using Command Bar
Open GlobalNav.tsx file.
Add below code before default class.
import { IButtonStyles } from '@fluentui/react';
import { createTheme, ITheme } from 'office-ui-fabric-react/lib/Styling';
import { CommandBar, ICommandBarStyleProps } from "@fluentui/react/lib/CommandBar";
const theme: ITheme = createTheme({
semanticColors: {
bodyBackground: "#333",
bodyText: "#fff"
}
});
const CommandBarProps: ICommandBarStyleProps = {
theme: theme
};
const buttonStyle: IButtonStyles = {
root: {
backgroundColor: "#333",
color: "#fff"
},
menuIcon: {
color: "#fff",
}
};
Create new method menuItems() in GlobalNav.tsx file.
private menuItems(menuItem: any, itemType: ContextualMenuItemType) {
return ({
key: menuItem.id,
name: menuItem.defaultLabel,
itemType: itemType,
href: menuItem.children.length == 0 ?
((menuItem.localProperties != undefined && menuItem.localProperties[0].properties !== undefined && menuItem.localProperties[0].properties.length > 0) ?
menuItem.localProperties[0].properties.filter(x => x.key == "_Sys_Nav_SimpleLinkUrl")[0].value !== undefined ? menuItem.localProperties[0].properties.filter(x => x.key == "_Sys_Nav_SimpleLinkUrl")[0].value : null
: null)
: null,
subMenuProps: menuItem.children.length > 0 ?
{ items: menuItem.children.map((i) => { return (this.menuItems(i, ContextualMenuItemType.Normal)); }) }
: null,
isSubMenu: itemType != ContextualMenuItemType.Header,
buttonStyles: buttonStyle
});
}
Update render() method in GlobalNav.tsx file.
public render(): React.ReactElement<IGlobalNavProps> {
var commandBarItems: any[] = [];
if (this.state.terms.length > 0) {
commandBarItems = this.state.terms.map((i) => {
return (this.menuItems(i, ContextualMenuItemType.Header));
});
}
return (
<>
{
this.state.terms.length > 0 &&
<div>
<CommandBar {...CommandBarProps}
style={{ width: "100%" }}
items={commandBarItems}
/>
</div>
}
</>
);
}
Step 8 - Test Solution
Open "../config/serve.json" file and update a couple of properties.
Update pageUrl Property and Properties,
{
"$schema": "https://developer.microsoft.com/json-schemas/core-build/serve.schema.json",
"port": 4321,
"https": true,
"serveConfigurations": {
"default": {
"pageUrl": "https://<tenantName>.sharepoint.com/sites/mySite/SitePages/myPage.aspx",
"customActions": {
"aeabebb8-02ce-4958-99b2-d640d0995588": {
"location": "ClientSideExtension.ApplicationCustomizer",
"properties": {
"termGroupId": "<Add your Term Group Id>",
"termSetId":"<Add your Term Set Id>"
}
}
}
},
"gobalNavigationBar": {
"pageUrl": "https://<tenantName>.sharepoint.com/sites/mySite/SitePages/myPage.aspx",
"customActions": {
"aeabebb8-02ce-4958-99b2-d640d0995588": {
"location": "ClientSideExtension.ApplicationCustomizer",
"properties": {
"termGroupId": "<Add your Term Group Id>",
"termSetId":"<Add your Term Set Id>"
}
}
}
}
}
}
Run below command in Visual Studio Code Terminal.
gulp clean
gulp build
gulp serve --config:"gobalNavigationBar"
Copy URL which is highlighted in yellow and paste it into the browser.
Conclusion
We have implemented the SPFx solution and get terms from the term store using PnP and render items in the command bar which is fluent UI control.
Happy Coding