0

I have a HelloWorldApplication which is in a development environment using gulp serve as the deployment.

I am able to deploy the solution to a page, which renders banners on the top and bottom of the page.

There is a need to pull data from a SharePoint list on all pages the solution is deployed to, which will always be consistent. The columns of importance are URL(text), ContentSteward(People Picker), and PagePublisher(People Picker).

My plan was to query the SharePoint List based on the current web which I am using this code to provide me with:

function currentWeb(): string {
  if (window
    && "location" in window
    && "protocol" in window.location
    && "pathname" in window.location
    && "host" in window.location) {
let baseUrl = window.location.protocol + "//" + window.location.host;
const pathname = window.location.pathname;
const siteCollectionDetector = "/Sites/";
if (pathname.indexOf(siteCollectionDetector) >= 0) {
baseUrl += pathname.substring(0, pathname.indexOf("/", siteCollectionDetector.length));
}
return baseUrl;
}
return null;
}

I need to be able to query the list of items for where the value for currentWeb = URL and I need to return both the PagePublisher and ContentSteward in separate text strings in the banners I am constructing.

It appears the best way to approach this is using a RestAPI, which I have attempted unsuccessfully to construct. I have also tried CAML and did not have a full enough understanding of how to make SP.ClientContext work.

export interface SiteColValues {  
  value: SiteColList[];  
}  
export interface SiteColList {  
  URL: string;  
  PagePublisher: string; 
  ContentSteward: string;
}  
function getSiteColListData(): Promise<SiteColValues> {  
  
  return this.context.spHttpClient.get("https://csp.jmrcontoso.com/sites/Default/_api/web/lists/GetByTitle('SiteInfo')/Items?$filter[URL]="+getCurrentWeb+"&$select=URL,PagePublisher,ContentSteward", SPHttpClient.configurations.v1)  
  
      .then((response: SPHttpClientResponse) => {   
        debugger;  
        return response.json();  
      });  
  } 

For the storing of the promise values to usable strings

        let cSteward: String = ""; 
        let pPublisher: String = "";

        getSiteColListData().then(listData => {cSteward.replace} );

For the display of the banner, using html

        let classValue: string = "<div style=\"font-size: 1em; width:600px; float:left;\">Content Steward: " + cSteward + "<br/>Page Publisher: "+ pPublisher +"<br>Last Modified: {0} | Last Reviewed: {1}</div> 

how the banners get added

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

    private _top1Placeholder: PlaceholderContent | undefined;
    private _top2Placeholder: PlaceholderContent | undefined;
    private _bottomPlaceholder: PlaceholderContent | undefined;

  @override
  public onInit(): Promise<void> {
  Log.info(LOG_SOURCE, `Initialized ${strings.Title}`);

  // Wait for the placeholders to be created (or handle them being changed) and then
  // render.
  this.context.placeholderProvider.changedEvent.add(this, this._renderPlaceHolders);

  return Promise.resolve();
}

private _renderPlaceHolders(): void {
  console.log("HelloWorldApplicationCustomizer._renderPlaceHolders()");
  console.log(
    "Available placeholders: ",
    this.context.placeholderProvider.placeholderNames
      .map(name => PlaceholderName[name])
      .join(", ")
  );

  // Handling the top1 placeholder
  if (!this._top1Placeholder) {
    this._top1Placeholder = this.context.placeholderProvider.tryCreateContent(
      PlaceholderName.Top,
      { onDispose: this._onDispose }
    );

    // The extension should not assume that the expected placeholder is available.
    if (!this._top1Placeholder) {
      console.error("The expected placeholder (Top1) was not found.");
      return;
    }

    if (this.properties) {
      let top1String: string = "Test is";
      if (!top1String) {
        top1String = "(Top1 property was not defined.)";
      }

      if (this._top1Placeholder.domElement) {
        this._top1Placeholder.domElement.innerHTML = `
        <div class="${styles.app}">
          <div class="${styles.top1}">
             ${escape(
              top1String
            )}
          </div>
        </div>`;
      }
    }
  }

  // Handling the top2 placeholder
  if (!this._top2Placeholder) {
    this._top2Placeholder = this.context.placeholderProvider.tryCreateContent(
      PlaceholderName.Top,
      { onDispose: this._onDispose }
    );

    // The extension should not assume that the expected placeholder is available.
    if (!this._top2Placeholder) {
      console.error("The expected placeholder (Top2) was not found.");
      return;
    }

    if (this.properties) {
      let top2String: string = "Test";
      if (!top2String) {
        top2String = "(Top2 property was not defined.)";
      }

      if (this._top2Placeholder.domElement) {
        this._top2Placeholder.domElement.innerHTML = `
        <div class="${styles.app}">
          <div class="${styles.top2}">
             ${escape(
              top2String
            )}
          </div>
        </div>`;
      }
    }
  }
  // Handling the bottom placeholder
  if (!this._bottomPlaceholder) {
    this._bottomPlaceholder = this.context.placeholderProvider.tryCreateContent(
      PlaceholderName.Bottom,
      { onDispose: this._onDispose }
    );

    // The extension should not assume that the expected placeholder is available.
    if (!this._bottomPlaceholder) {
      console.error("The expected placeholder (Bottom) was not found.");
      return;
    }

    if (this.properties) {
      let bottomString: string = this.properties.Bottom;
      if (!bottomString) {
        bottomString = "(Bottom property was not defined.)";
      }

      if (this._bottomPlaceholder.domElement) {
        
        let d: Date = new Date();
        let today: String = d.toISOString().split('T')[0];
        let year = d.getFullYear();
        let month = d.getMonth();
        let day = d.getDate();
        let newdate: String = new Date(year+25,month,day).toISOString().split('T')[0];
        let classValue: string = "<div style=\"font-size: 1em; width:600px; float:left;\">Content Steward: " + cSteward + "<br/>Page Publisher: "+ pPublisher +"<br>Last Modified: {0} | Last Reviewed: {1}</div>;

        this._bottomPlaceholder.domElement.innerHTML = classValue;
      }
    }
  }
}
2
  • So how exactly does classValue end up on the page as HTML? Commented May 10 at 19:53
  • Updated to include how the banners get added. Commented May 13 at 10:49

1 Answer 1

0

You should explore that context object a little bit... You don't need your currentWeb function, you can get the current web URL from this.context.pageContext.web.absoluteUrl.

Then, to get people's names from a user field, you have to use $expand in your REST query, because people picker fields are actually lookup fields under the hood.

And, your $filter is a little bit off, you don't use square brackets there.

When I have to construct long REST queries, I like to break up the sections into their own variables which makes it a bit easier to read. So, I think you're probably looking to do something like this:

// changing this to resolve a SiteColList object because we're 
// going to handle getting that out of the response inside the function

function getSiteColListData(): Promise<SiteColList> {

    // immediately return a promise that we control, so we can resolve it when we are ready
    // also, make the inner promise executor function "async"
    // so we can use await to make it easier to see the flow of things

    return new Promise<SiteColList> (async (resolve, reject) => {

        // build the query
        const filter = `$filter=URL eq '${this.context.pageContext.web.absoluteUrl}'`;
        const select = `$select=URL,PagePublisher/Title,ContentSteward/Title`;
        const expand = `$expand=PagePublisher,ContentSteward`;
        
        const uri = `https://csp.jmrcontoso.com/sites/Default/_api/web/lists/GetByTitle('SiteInfo')/Items?${filter}&${select}&${expand}`;

        // make the request, include the 'accept' header to make sure you jet a JSON response
        const response = await this.context.spHttpClient.get(uri, SPHttpClient.configurations.v1, {
            headers: {
                accept: 'application/json'
            }
        });

        if (response.ok) {

            const responseJSON = await response.json()
            const listItem = responseJSON.value[0]

            resolve({
                URL: listItem.URL,
                PagePublisher: listItem.PagePublisher.Title,
                ContentSteward: listItem.ContentSteward.Title
            });

        } else {
            // if the request was bad, figure out why
            console.log(response)
            reject(response)
        }

    })
}

Ok, so I've never done an application customizer before, but according to this other question and answer, it looks like you can do async data fetching with them.

So what I would do in your case is

  • add a class property to store the data you need to fetch
  • make your renderPlaceHolders function async so you can await and fetch your data (if you need to)
  • fetch your data and wait for it to arrive before you need to use it

That would look something like this:

// in the section where you are defining class properties...
private _top1Placeholder: PlaceholderContent | undefined;
private _top2Placeholder: PlaceholderContent | undefined;
private _bottomPlaceholder: PlaceholderContent | undefined;

// ...add a new class property to hold the fetched data
private _fetchedData: SiteColList | undefined;

// ... then ...

// make the renderPlaceHolders function async so you can
// use await inside the function

private async _renderPlaceHolders(): Promise<void> {
    console.log("HelloWorldApplicationCustomizer._renderPlaceHolders()");

    // before you do anything else, make sure you load the data you need
    // if it is not already loaded
    if (!this._fetchedData) {
        // you can use the same function i outlined above in my answer
        // just store the result in your class property
        this._fetchedData = await getSiteColListData();
    }

    // now you have the data and you can use it

    // ... the rest of your existing function as it is ...
    // ... down to where you need to use that fetched data
    
    const cSteward = this._fetchedData.ContentSteward;
    const pPublisher = this._fetchedData.PagePublisher;

    let classValue: string = "<div style=\"font-size: 1em; width:600px; float:left;\">Content Steward: " + cSteward + "<br/>Page Publisher: "+ pPublisher +"<br>Last Modified: {0} | Last Reviewed: {1}</div>;

    // ... etc

    // but then at the very end of the _renderPlaceHolders you might have to
    return Promise.resolve()
}
9
  • Thank you for the detailed and quick response! When I try and read the response from the function I am getting back the object but am unsure of how to read the data from the object to a string I can use. code block let classValue: string = "<div style=\"font-size: 1em; width:600px; float:left;\">Content Steward: "+ getSiteColListData() +"<br/>Page Publisher: "+ "pp" +"<br>Last Modified: {0} | Last Reviewed: {1}</div> Commented May 9 at 18:25
  • the above reference returns "Content Steward: [object Promise]" Commented May 9 at 18:37
  • Yes, Promises are asynchronous, so you have to wait for them to do whatever they are doing before you can use the eventual return value. So immediately when you call getSiteColListData() you have a Promise object. You would have to do something like getSiteColListData().then(listData => { //do something with the list data here }). I don't think you can use it inline in string concatenation like that. You are going to have to find a way to wait for the data to come back before you can build your HTML. Commented May 9 at 19:05
  • That makes sense, I am struggling with extracting the information from listData and storing it in a usable way that I can convert to a string that can be read from an HTML inline. I have a bottom banner which I am able to put text into, but every reference to handling promises and waiting for the data to come back and extracting it is not bearing fruit. If you could please point me in a general direction of how to specifically be able to extract the data from the interfaces? Commented May 10 at 14:47
  • Based on the fact that you reference using this.context.spHttpClient, I am assuming you are using SPFx? If that is true, are you also using React? If you are, typically how you handle something like this is to use the component's state. You set up state variables for the strings you want to render, and default them to blank strings. You use those state variables in the lines where you build your HTML. Then when the async data is returned, you set the state variables with the returned data. This will trigger a re-render of the component and use the new string data (from state) in your HTML. Commented May 10 at 15:08

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.