SharePoint Framework End User Managed Placeholder Content Part 2

Series Recap:

  1. Part 1 – Create the Content Repository for managing your Placeholder content
  2. Part 2 – Inject your managed Placeholder content into your pages
  3. Part 3 – Cache placeholder content in localStorage
  4. Part 4 (New!) – Update code for the 1.2.0 SharePoint Framework Extensions Release Candidate

Welcome back! Although this is only the second post of the series, this is the post where we’ll create a real, working product that allows our end users to manage their placeholder content via a regular SharePoint list, and have it automatically injected into every page, via the SharePoint Framework Extension model! When we last left off, we had created a list instance in our solution called “SPFx Placeholders” that allows our users to create an endless amount of Key:Value pairs, where the “Key” is the official SharePoint Placeholder name, and the “Value” is a rich text field that allows users to control the content appearing in the placeholder (including utilizing HTML!).

To now bring that content into the page, we’ll want to first add a dependency to SP PNP JS, so that we can use this library to read our SharePoint list data. While it’s not required you utilize SP PNP JS to read SharePoint data, I highly recommend this library, as it makes querying for SharePoint data extremely easy, and this type of activity is just the kind of task it was originally created to assist with.

Let’s jump back to our favorite command window, ensure we’re in the root of our application, and issue the following:

npm install sp-pnp-js

The above will install the SP PNP JS library, and we’ll then be able to import it into our solution, and use it to quickly query data within our SharePoint solution.

Next, let’s create a new file in the same folder as our manifest file called PlaceholderItems.ts and insert the following content (the folder you’ll want to create the file in is called ‘placeholdersExtension’ if you utilized the text used in the first post of this series):

import * as pnp from 'sp-pnp-js';
import { Environment, EnvironmentType } from '@microsoft/sp-core-library';

const PLACEHOLDER_LISTNAME: string = "SPFx Placeholders";
const FIELD_TITLENAME: string = "Title";
const FIELD_SPFXCONTENTNAME: string = "SPFxContent";

export interface IPlaceholderItem {
    Title: string;
    SPFxContent: string;
}

export class PlaceholderItems {

    public static GetItems() : Promise<IPlaceholderItem[]> {
        return (Environment.type === EnvironmentType.Local) ? 
            this.GetMockListItems() : this.GetRealListItems();

    }

    private static GetRealListItems() : Promise<IPlaceholderItem[]> {
        return pnp.sp.web.lists.getByTitle(PLACEHOLDER_LISTNAME).items.
        select(FIELD_TITLENAME, FIELD_SPFXCONTENTNAME).get().then((data: IPlaceholderItem[]) => { 
           return data;
        });

    }

    private static GetMockListItems() : Promise<IPlaceholderItem[]> {
        return new Promise<IPlaceholderItem[]> ((resolve) => {
            resolve(
                [
                    { Title: "PageHeader", SPFxContent: "Header Content" },
                    { Title: "PageFooter", SPFxContent: "Footer Content" } 
                ]
            );
        });


    }
}

Let’s go ahead and recap what this file essentially does:

  1. We’re importing the SP PNP JS library into the file, so that we can utilize it to retrieve SharePoint data
  2. We’re importing Environment classes so that we can distinguish between production and development environments (not necessarily needed yet since we can’t test extensions locally in preview mode, but this is a good practice to follow, and will also help with unit testing)
  3. Our ‘GetItems’ method will look at our current environment, and correctly call the related method to retrieve real or mock data
  4. Notice how our ‘GetRealListItems’ and ‘GetMockListItems’ methods are private, as our Customizer class shouldn’t have to determine which to call, since our main ‘GetItems’ will take care of this
  5. Both our real and mock data methods return a promise of the IPlaceholderItem[] interface we’ve declared, which allows our public ‘GetItems’ method to call either, via a simple, ternary operator (admittedly, a personal favorite of mine)
  6. For retrieving local SharePoint data , we utilize:
    return pnp.sp.web.lists.getByTitle(PLACEHOLDER_LISTNAME).items.
            select(FIELD_TITLENAME, FIELD_SPFXCONTENTNAME).get().then((data: IPlaceholderItem[]) => { 
               return data;
            });
    

    This uses our constant strings that point to the correct list and field names

Now that we have a class and interface to support retrieving the data, the only thing left to do is update our Customizer file, and have it retrieve the content and place it on the page. You’ll want to locate your Customizer file (should be called ‘PlaceholderExtensionApplicationCustomizer.ts’ if you’ve followed naming conventions from the first post in the series) and replace all of its contents with the following:

import { override } from '@microsoft/decorators';
import { BaseApplicationCustomizer, Placeholder } from '@microsoft/sp-application-base';
import { PlaceholderItems, IPlaceholderItem } from './PlaceholderItems';

/** A Custom Action which can be run during execution of a Client Side Application */
export default class PlaceholdersExtensionApplicationCustomizer
  extends BaseApplicationCustomizer<any> {

  @override
  public onRender(): void {
    try {
      //Grab the placeholders that the page currently offers
      let pagePlaceHolders:ReadonlyArray<string> = this.context.placeholders.placeholderNames;

      //Get our list of placeholders and loop through them
      PlaceholderItems.GetItems().then((data:IPlaceholderItem[]) => {
        data.forEach((element:IPlaceholderItem) => {

          //Look for a matching placeholder in the list of official placeholders
          let index:Number = pagePlaceHolders.indexOf(element.Title);
          if (index !== -1) {

            //Grab the placeholder
            let currentPlaceholder: Placeholder = this.context.placeholders.tryAttach(
              element.Title, { onDispose: this._onDispose}
            );

            //Insert our content
            currentPlaceholder.domElement.innerHTML = element.SPFxContent;
          }
        });
      }
      );
    }
    finally {    
    }
    
  }

  private _onDispose(): void { }
}

We’re almost ready to see our solution in action, but first, let’s enter two entries into our ‘SPFx Placeholders’ list with the following data (these will allow you to populate the first two placeholders available for the header and footer sections of the page):

  1. Title: ‘PageHeader’ Content: ‘enter your page header content’
  2. Title: ‘PageFooter’ Content: ‘enter your page footer content’

At this point, we can now test our solution via the methods found on the Extension preview documentation page. This essentially has us running ‘gulp serve –nobrowser’, visiting a list view on our site, and substituting in the ID found in our manifest file: (you’ll want to replace [[ListViewUrl]] with your list view url, and [[IDFromManifest]] with the ID located in your manifest file)

[[ListViewUrl]]?loadSPFX=true&debugManifestsFile=https://localhost:4321/temp/manifests.js&customActions={[[IDFromManifest]]:{"location":"ClientSideExtension.ApplicationCustomizer"}}

Provided you’ve entered everything correctly, you’ll now see our managed content displayed in the header and footer, per the below

Congratulations! We now have a working proof of concept that puts the content management of our placeholders directly into the hands of our end users, while still following best practices. In our third and final chapter, we’ll look at how more dynamic content can be injected into these areas, as well as how we can take advantage of caching, to reduce the amount of times we query for our data on page loads.

Cheers,
Matt

Matt Jimison

Microsoft 365 Geek - Husband, father, lover of basketball, football, smoking / grilling, music, movies, video games, and craft beer!

Leave a Reply

Your email address will not be published. Required fields are marked *