Creating a SharePoint Application Customizer with Dynamic Navigation Menus
In this blog post, we will walk through the process of creating a SharePoint Application Customizer that dynamically fetches navigation data from SharePoint lists, and renders custom menus based on this data. We’ll use React, PnPjs, and the SharePoint Framework (SPFx) to create a solution that can adapt to the content in your SharePoint lists.
Overview
This solution fetches menu items and their respective submenus from SharePoint lists. It then renders the menus dynamically using React. The navigation structure can be customized by modifying the SharePoint lists, making it easier to manage navigation without touching the codebase. The solution includes two types of navigation menus: Default and External, with each rendering differently based on the type.
Solution Architecture
- SPFx Application Customizer: We use an SPFx Application Customizer to add custom elements to the SharePoint pages.
- React: The UI components for the navigation menus are created using React.
- PnPjs: To fetch the data from SharePoint lists using REST API.
- Placeholders: A SharePoint feature that allows custom components to be rendered in predefined areas of a page.
1. Fetching Data from SharePoint Lists
The first part of the solution involves fetching data from SharePoint lists. We’ll use HttpClient to make a REST API call to SharePoint’s _api endpoint to get items from the lists.
Here’s the code for fetching the list items and their submenus:
import { HttpClient, HttpClientResponse } from '@microsoft/sp-http';
export async function GetListItems(context: any, listTitle: string): Promise<any[]> {
try {
const siteUrl = context.pageContext.web.absoluteUrl;
const apiUrl = `${siteUrl}/_api/web/lists/getbytitle('${listTitle}')/items`;
console.log("API URL:", apiUrl);
const response: HttpClientResponse = await context.httpClient.get(apiUrl, HttpClient.configurations.v1, {
headers: {
'Accept': 'application/json;odata=nometadata',
'Content-Type': 'application/json;odata=nometadata'
}
});
console.log("Response Status:", response.status);
if (!response.ok) {
throw new Error(`Error fetching items: ${response.statusText}`);
}
const data = await response.json();
console.log("Fetched Data:", data);
const menus = await Promise.all(data.value.map(async (menu: any) => {
const subMenus = await GetSubMenuItems(context, menu.Title);
return { name: menu.Title, subMenus };
}));
console.log("Menus Data", menus);
return menus;
} catch (error) {
console.error("Error fetching list items:", error);
return [];
}
}
export async function GetSubMenuItems(context: any, menuTitle: string): Promise<any[]> {
try {
const siteUrl = context.pageContext.web.absoluteUrl;
const apiUrl = `${siteUrl}/_api/web/lists/getbytitle('${menuTitle}')/items?$select=Title,Id,url`;
console.log("API URL for Submenus:", apiUrl);
const response: HttpClientResponse = await context.httpClient.get(apiUrl, HttpClient.configurations.v1, {
headers: {
'Accept': 'application/json;odata=nometadata',
'Content-Type': 'application/json;odata=nometadata'
}
});
if (!response.ok) {
throw new Error(`Error fetching submenus: ${response.statusText}`);
}
const data = await response.json();
console.log("Submenu Data", data);
return data.value.map((item: any) => ({
title: item.Title,
id: item.Id,
url: item.url // Using the 'url' directly from the response
})) || [];
} catch (error) {
console.error("Error fetching submenu items:", error);
return [];
}
}
This code retrieves the list items (menus) and their submenus from SharePoint. The GetListItems function calls another function, GetSubMenuItems, to fetch the submenus for each menu item. Both functions return a Promise of arrays, which are used to populate the navigation menu.
2. The Application Customizer Class
Now, we need to create the Application Customizer that will insert our navigation menus into the page. This involves creating a placeholder in the SPFx context and rendering the React components into it.
import * as ReactDOM from 'react-dom';
import * as React from 'react';
import { Log } from '@microsoft/sp-core-library';
import { BaseApplicationCustomizer, PlaceholderContent, PlaceholderName } from '@microsoft/sp-application-base';
import NavigationMenu from './NavigationMenu';
const LOG_SOURCE: string = 'TopMenuApplicationCustomizer';
export default class TopMenuApplicationCustomizer
extends BaseApplicationCustomizer<ITopMenuApplicationCustomizerProperties> {
private _topPlaceholder: PlaceholderContent | undefined;
public async onInit(): Promise<void> {
Log.info(LOG_SOURCE, `Initialized Application Customizer`);
this.context.placeholderProvider.changedEvent.add(this, this._renderPlaceHolders);
return Promise.resolve();
}
private _renderPlaceHolders(): void {
if (!this._topPlaceholder) {
this._topPlaceholder = this.context.placeholderProvider.tryCreateContent(
PlaceholderName.Top,
{ onDispose: this._onDispose }
);
if (!this._topPlaceholder) {
console.error("Top placeholder not found.");
return;
}
const reactElement: React.ReactElement = React.createElement(NavigationMenu, {
type: "Default",
ListTitle: "TopMenu",
AppContext: this.context
});
ReactDOM.render(reactElement, this._topPlaceholder.domElement);
}
}
private _onDispose(): void {
console.log('Disposed top navigation customizer');
}
}
3. React Components for Navigation Menus
We then have a NavigationMenu component that conditionally renders either a default or external navigation menu. The component fetches the menu data using the GetListItems function and updates the state with the fetched data.
import * as React from 'react';
import { useEffect, useState } from 'react';
import NavigationMenuDefault from './NavigationMenuDefault';
import NavigationMenuExternal from './NavigationMenuExternal';
import { GetListItems } from './GetItemsLists';
import { BaseComponentContext } from '@microsoft/sp-component-base';
interface INavigationProps {
type: string;
ListTitle: string;
AppContext: BaseComponentContext;
}
interface IMenuItem {
name: string;
subMenus: string[];
}
const NavigationMenu: React.FC<INavigationProps> = ({ type, ListTitle, AppContext }) => {
const [menusData, setMenusData] = useState<IMenuItem[]>([]);
useEffect(() => {
const fetchMenus = async () => {
try {
const items: IMenuItem[] = await GetListItems(AppContext, ListTitle);
setMenusData(items);
} catch (error) {
console.error("Error fetching menu data:", error);
}
};
fetchMenus();
}, [AppContext]);
return (
<div>
{type === 'External' && <NavigationMenuExternal />}
{type === 'Default' && <NavigationMenuDefault menus={menusData} />}
</div>
);
};
export default NavigationMenu;
4. Default and External Navigation Menus
Finally, we have two components: NavigationMenuDefault and NavigationMenuExternal, which render the menu in two different formats. Here’s the NavigationMenuDefault component:
import * as React from 'react';
import styles from './AppCustomizer.module.scss';
interface INavigationProps {
menus: { name: string; subMenus: any[] }[];
}
const NavigationMenuDefault: React.FC<INavigationProps> = ({ menus }) => {
return (
<div className={styles.topnav}>
{menus.map((menu, index) => (
<div key={index} className={styles.dropdown}>
<button style={{ cursor: "pointer" }} className={styles.dropbtn}>{menu.name}</button>
{menu.subMenus.length > 0 && (
<div className={styles.dropdownContent}>
{menu.subMenus.map((subMenu, subIndex) => (
<a key={subIndex} href={subMenu.url}>{subMenu.title}</a>
))}
</div>
)}
</div>
))}
</div>
);
};
export default NavigationMenuDefault;
5. Conclusion
This solution provides a fully dynamic, list-based navigation system for SharePoint using SPFx and React. It allows for easy maintenance and scalability, where you can update the navigation simply by modifying SharePoint lists, without needing to redeploy code.
By leveraging placeholders in SPFx and React’s component model, we can create a highly flexible navigation system that fits any SharePoint site, supporting both internal and external links.
