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 and Submenus

We developed the following function to fetch SharePoint list items using HttpClient. In this version, the submenus are stored in a separate list, which is named after the main menu title.

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);

        const menuItems = await Promise.all(data.value.map(async (item: any) => {
            const submenuListTitle = item.Title;  // Assume submenu list is named after the menu item title
            const submenus = await GetSubMenuItems(context, submenuListTitle);
            return { ...item, subMenus: submenus };
        }));

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

export async function GetSubMenuItems(context: any, submenuListTitle: string): Promise<string[]> {
    try {
        const siteUrl = context.pageContext.web.absoluteUrl;
        const apiUrl = `${siteUrl}/_api/web/lists/getbytitle('${submenuListTitle}')/items`;

        console.log("Submenu 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("Submenu Response Status:", response.status);

        if (!response.ok) {
            throw new Error(`Error fetching submenu items: ${response.statusText}`);
        }

        const data = await response.json();
        console.log("Processed Submenu Data:", data);

        return data.value.map((item: any) => item.Title) || [];
    } catch (error) {
        console.error("Error fetching submenu 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.");
  }
}

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. The enhancement of fetching submenu items from lists named after their parent menu items further increases flexibility and scalability.

Edvaldo Guimrães Filho Avatar

Published by

Categories: