Building Headers and Footers that work on Classic and Modern sites

One of the partners I consult for is migrating a Fortune 500 financial services company to SharePoint Online. The company wants to take advantage of modern team and communications sites, yet where they need features that aren’t available in modern SharePoint, they’ve decided to stick with classic Publishing sites.

The challenge is: how to build global navigation and footers that will work on both classic and modern sites. There are a few reasons this is important:

  • It provides common navigation across all kinds of sites, making the Intranet easier to use
  • It provides a common footer across all kinds of sites, ensuring compliance messages are delivered consistently
  • It reduces coding and maintenance, because one set of code is used across old and new sites

So I undertook a little Proof of Concept, and here are the results. The solution is usable as-is if your needs are simple. The real intent, however is to prove out a pattern for developing any header and footer that will work on both modern and classic sites.

ClassicFullHeaderFooter
Figure 1

Figure 1 shows how it looks on a classic publishing site. A simple navigation menu is added on the top of the page, and a footer containing a message and a set of links appears at the bottom.

The sample code has moved to the SharePoint Framework Extensions repo.

The menu and footer look the same in a modern team site, as shown in Figure 2. It also works on modern list pages in classic Publishing sites, making the experience less jarring when users browse between modern and classic pages. This could also be used in hybrid environments, as the classic solution should work the same on premises as online (though you will probably need a separate set of files to avoid cross-site scripting issues.)

ModernFullHeaderFooter
Figure 2

When the screen becomes narrow, the top menu switches to a hamburger style menu that pushes the screen down with a hierarchy of options. Figure 3 shows this on the classic publishing page, which is clearly not responsive.

ClassicPhoneHeaderFooter
Figure 3

The hamburger menu looks right at home on the modern site, viewed here in an iPhone emulator.

ModernPhoneHeaderFooter
Figure 4

The menu and footer content are stored in a simple JSON file in SharePoint. While this doesn’t provide for security trimming, it is fast, and if there’s someone who understands JSON, they can easily keep it up-to-date. (The partner I’m working with has already developed a much cooler and more advanced solution for SPFx using the Term Store).

NOTE: This sample uses features (fetch and ES6 Promises) which are not available in Internet Explorer. If you want to use it in IE, you need to add polyfills for these features. As it happens, these are the same as the ones needed by PnP JS Core; their documentation does a good job of explaining.

Where to get it

Full source code and build/installation instructions are available as samples in the SharePoint Framework Extensions repo.

Approach

My friend and colleague Julie Turner recently published a series of articles called Conquer your dev toolchain in ‘Classic’ SharePoint. She shows how to use SharePoint Framework style tooling such as typescript and webpack on classic sites. This was the starting point for the project, and I recommend anyone who hasn’t got this working to go through these articles. This was key to having common code between the classic solution and SharePoint Framework.

From there, the approach was to push as much logic as possible into common code (in the “common” directory in both Classic and Modern folders). A quick run of Beyond Compare confirms they are identical. Only the files shown in blue are unique to their environment.

BeyondCompare180311
Figure 5

On the SharePoint Framework side, I generated an Application Customizer extension using React. I developed the header and footer there, then ported them over to a project based on Julie’s article. A few tweaks to the configuration files were necessary; they’re all in the git repo.

Bootstrapping

In the SharePoint Framework version of the solution, the UI is “bootstrapped” in the CustomHeaderFooterApplicationCustomizer.ts file, which was initially generated by the Yeoman generator. All references to React have been removed from there, however; all it needs to do is find the DOM elements that for header and footer, use a (common) service to read in the data, and common rendering code takes care of the rest.

@override
public onInit(): Promise<void> {

  const promise = new Promise<void>((resolve, reject) => {

    HeaderFooterDataService.get(url)
    .then ((data: IHeaderFooterData) => {

        const header: PlaceholderContent = this.context.placeholderProvider.tryCreateContent(
        PlaceholderName.Top,
        { onDispose : this._onDispose }
        );
        const footer: PlaceholderContent = this.context.placeholderProvider.tryCreateContent(
        PlaceholderName.Bottom,
        { onDispose : this._onDispose }
        );

        if (header || footer) {
        ComponentManager.render(header ? header.domElement : null,
            footer ? footer.domElement : null, data);
        }

        resolve();
    })
    // (exception handling removed for brevity)
  });

  return promise;
}

In the Classic version of the solution, the UI is “bootstrapped” in bootHeaderFooter.ts. It generates its own DOM elements for the header and footer, and connects them above and below a well-known HTML element, s4-workspace. While there’s no guarantee Microsoft will always provide an element with that ID, it’s a lot less likely to break than a custom master page.

export class bootstrapper {

  public onInit(): void {

    const header = document.createElement("div");
    const footer = document.createElement("div");

    const workspace = document.getElementById('s4-workspace');

    if (workspace) {

      workspace.parentElement.insertBefore(header,workspace);
      workspace.appendChild(footer);

      const url = // (your JSON file URL)  '
      HeaderFooterDataService.get(url)
        .then ((data: IHeaderFooterData) => {
          ComponentManager.render(header, footer, data);
        })
        .catch ((error: string) => {
          // (omitted for brevity)
        });
    }
  }
}

Then some inline code uses the old Scripting On Demand library to run the bootstrapper.

(<any>window).ExecuteOrDelayUntilBodyLoaded(() => {
  if (window.location.search.indexOf('IsDlg=1') < 0) {
    let b = new bootstrapper();
    b.onInit();  
  }
})

Everything else in the solution is common.

Installation

Installation on the SharePoint Framework side is pretty standard … package the solution, upload it to the app catalog, and add it to a site. On the Classic side, I used PnP PowerShell with JavaScript injection.

Write-Output "`n`nAdding script links"
Add-PnPJavaScriptLink -Name React -Url "https://cdnjs.cloudflare.com/ajax/libs/react/15.6.2/react.js" -Sequence 100
Add-PnPJavaScriptLink -Name ReactDom -Url "https://cdnjs.cloudflare.com/ajax/libs/react-dom/15.6.2/react-dom.js" -Sequence 200
Add-PnPJavaScriptLink -Name HeaderFooter -Url "https://<tenant>.sharepoint.com/sites/scripts/scripts/bundleClassic.js" -Sequence 300

Detailed build and installation instructions are in the readme file on Github.

User Interface

The UI is in React and 100% common to the Classic and SPFx versions. There’s a component each for the header and footer, plus a small class called ComponentManager that renders them into two DOM elements provided by the bootstrapper.

public static render(headerDomElement: HTMLElement, footerDomElement: HTMLElement, data: IHeaderFooterData): void {

    if (headerDomElement) {
        const reactElt: React.ReactElement<IHeaderProps> = React.createElement(Header, {
            links: data.headerLinks
        });
        ReactDOM.render(reactElt, headerDomElement);
    }

    if (footerDomElement) {
        const reactElt: React.ReactElement<IFooterProps> = React.createElement(Footer, {
            message: data.footerMessage,
            links: data.footerLinks
        });
        ReactDOM.render(reactElt, footerDomElement);
    }
}

You can check the code to see the menus and footer components in React. I think the coolest part is the top menu, which is implemented entirely in CSS based on this brilliant example from Tony Thomas.

Getting the data

Both header and footer data are stored in a single JSON file; there’s a sample in the common/sample folder. Here’s an excerpt to give you the idea:

{
	"headerLinks": [
		{
			"name": "Home",
			"url": "/sites/pubsite",
			"children": []
		},
		{
			"name": "Companies",
			"url": "#",
			"children": [
				{
					"name": "Contoso",
					"url": "#"
				},
				{
					"name": "Fabrikam",
					"url": "#"
                }
            ]
		}
	],
	"footerMessage": "Contoso corporation, all rights reserved",
	"footerLinks": [
		{
			"name": "Office Developer Home",
			"url": "#"
		}
	]
}

This approach is very simple and fast. The JSON is described by two interfaces in the common/model directory, ILink.ts and IHeaderFooter.ts.

export interface ILink {
    name: string;
    url: string;
    children: ILink[];
}

export interface IHeaderFooterData {
    headerLinks: ILink[];
    footerMessage: string;
    footerLinks: ILink[];
}

A simple service, common/services/HeaderFooterDataService.ts, reads in the JSON using Fetch.

Conclusion

SPFx is based on open source technology which can be used to target any web site, even a classic SharePoint site. By leveraging these same tools outside of SharePoint Framework, developers can reuse their work and provide consistency between classic and modern SharePoint pages.

Thanks for reading, and please let me know if you use this approach in your project, or if you have any feedback or suggestions!

One thought on “Building Headers and Footers that work on Classic and Modern sites

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 )

w

Connecting to %s