Implementing Dynamic Menus in SPFx Application Customizer

In this article, we explore a practical solution for implementing dynamic menus in a SharePoint Framework (SPFx) Application Customizer. We attempted to use PnPjs but encountered issues due to the nature of the Application Customizer context, which does not match the WebPart context. As a result, we opted for the HttpClient approach to fetch list items directly from SharePoint.

The Challenge with PnPjs in Application Customizers

PnPjs is a fantastic library for SharePoint development, but when working within an SPFx Application Customizer, it does not always provide the expected context. Specifically, retrieving lists and list items using PnPjs failed, leading to our decision to use HttpClient for direct API calls.

Solution: Using HttpClient to Retrieve List Items

We developed the following function to fetch SharePoint list items using HttpClient:

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',  // Enforce JSON response
                '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();  // Parse JSON response
        console.log("Processed Data:", data);

        return data.value || [];  
    } catch (error) {
        console.error("Error fetching list items:", error);
        return [];
    }
}

Integrating the Function into an SPFx Application Customizer

Application Customizer Initialization

The TopMenuApplicationCustomizer extends BaseApplicationCustomizer and ensures that our navigation menu is dynamically rendered at the top of the SharePoint page.

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';
import * as strings from 'TopMenuApplicationCustomizerStrings';

const LOG_SOURCE: string = 'TopMenuApplicationCustomizer';

export interface ITopMenuApplicationCustomizerProperties {}

export default class TopMenuApplicationCustomizer extends BaseApplicationCustomizer<ITopMenuApplicationCustomizerProperties> {
  private _topPlaceholder: PlaceholderContent | undefined;

  public async onInit(): Promise<void> {
    Log.info(LOG_SOURCE, `Initialized ${strings.Title}`);
    this.context.placeholderProvider.changedEvent.add(this, this._renderPlaceHolders);
    return Promise.resolve();
  }

  private _renderPlaceHolders(): void {
    console.log("Rendering Placeholders");

    if (!this._topPlaceholder) {
      this._topPlaceholder = this.context.placeholderProvider.tryCreateContent(PlaceholderName.Top, { onDispose: this._onDispose });

      if (!this._topPlaceholder) {
        console.error("The expected placeholder (Top) was not found.");
        return;
      }

      if (this._topPlaceholder.domElement) {
        const reactElt: React.ReactElement = React.createElement(NavigationMenu, { type: "Default", ListTitle: "TopMenu", AppContext: this.context });
        ReactDOM.render(reactElt, this._topPlaceholder.domElement);
      }
    }
  }

  private _onDispose(): void {
    console.log("Disposed custom top placeholders.");
  }
}

Creating the Navigation Menu Component

The NavigationMenu component fetches menu items dynamically from a SharePoint list and renders them accordingly.

import * as React from 'react';
import { useEffect, useState } from 'react';
import NavigationMenuDefault from './NavigationMenuDefaut';
import NavigationMenuExternal from './NavigationMenuExternal';
import { GetListItems } from './GetItensLists';
import { BaseComponentContext } from '@microsoft/sp-component-base';

interface INavigationProps {
  type: string;
  ListTitle: string;
  AppContext: BaseComponentContext;
}

interface IMenuItem {
  name: string;
  subMenus: string[];
}

interface ISharePointListItem {
  Title: string;
  SubMenus?: string;
}

const NavigationMenu: React.FC<INavigationProps> = ({ type, ListTitle, AppContext }) => {
  const [menusData, setMenusData] = useState<IMenuItem[]>([]);

  useEffect(() => {
    const fetchMenus = async () => {
      try {
        const items: ISharePointListItem[] = await GetListItems(AppContext, ListTitle);
        const formattedMenus: IMenuItem[] = items.map((item: ISharePointListItem) => ({
          name: item.Title,
          subMenus: item.SubMenus ? item.SubMenus.split(';') : []
        }));

        setMenusData(formattedMenus);
      } 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;

Rendering the Navigation Menu

Finally, the NavigationMenuDefault component renders the dynamic navigation items.

import styles from './AppCustomizer.module.scss';
import * as React from 'react';

interface INavigationProps {
  menus: { name: string; subMenus: string[] }[];
}

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((subItem, subIndex) => (
                <a key={subIndex} href="#">{subItem}</a>
              ))}
            </div>
          )}
        </div>
      ))}
    </div>
  );
};

export default NavigationMenuDefault;

Conclusion

This solution ensures dynamic menu rendering in an SPFx Application Customizer without relying on PnPjs. Instead, we use HttpClient for API calls, providing a robust way to fetch SharePoint list items.

Edvaldo Guimrães Filho Avatar

Published by

Categories: