Building an App with SPFx, Reactjs, and TypeScript Part 4: SharePoint Data Access and the Property Pane

Introduction

We’ve built several components that are responsible for how content is rendered. We have some components that simply accept props, like our navigation item and member stateless functional components, and others that manage the state (your data). We then went back and updated our navigation to allow us to use the HashRouter to load components based on what navigation item was clicked without redirecting us to a new page. It’s time to start replacing our dummy data with content from SharePoint.

Setting up our Lists

If you’re building an app that relies on specific data, you’re going to want to package your lists/libraries/content types as part of your solution. Since we’re building some random demo, I’ll simply create what I need, manually.

List: Event Hub – Stores the group information. In our case, this is the Office 365 User Group.

NameTypeDescription
TitleSingle line of textThe name of the group
Event LocationLocationMap location of the group
(Address, city, state, zip)
OrganizersPerson or GroupNames of the Organizer(s)
MembersNumberTotal number of members

List: Meetings – Stores the individual meetings with a lookup to the group in the Event Hub list. This would be the monthly meetings for the group.

NameTypeDescription
TitleSingle line of textThe headline for the individual meetings.
Event HubLookupA reference to the Group that the meeting belongs to.
DescriptionMultiple lines of textA description of the individual meeting.
Start TimeDate and TimeDate and start time of the meeting
End TimeDate and TimeDate and end time of the meeting

The Property Pane

I want to be able to select both of these lists as data sources for the web part. We’re going to provide a place where we can select each list from dropdowns and then we’re going to add another dropdown where we can select the Group that we want to display from the Event Hub list. In order to do that, we’ll need to update the web part property pane to let us make those selections. We’ll be using cascading dropdowns.

We haven’t touched the EventHubWebPart yet so before we do, this is what it looks like.

import * as React from 'react';
import * as ReactDom from 'react-dom';
import { Version } from '@microsoft/sp-core-library';
import {
  BaseClientSideWebPart,
  IPropertyPaneConfiguration,
  PropertyPaneTextField
} from '@microsoft/sp-webpart-base';

import * as strings from 'EventHubWebPartStrings';
import EventHub from './components/EventHub/EventHub';
import { IEventHubProps } from './components/EventHub/IEventHubProps';

export interface IEventHubWebPartProps {
  description: string;
}

export default class EventHubWebPart extends BaseClientSideWebPart<IEventHubWebPartProps> {

  public render(): void {
    const element: React.ReactElement<IEventHubProps > = React.createElement(
      EventHub,
      {
        description: this.properties.description
      }
    );

    ReactDom.render(element, this.domElement);
  }

  protected onDispose(): void {
    ReactDom.unmountComponentAtNode(this.domElement);
  }

  protected get dataVersion(): Version {
    return Version.parse('1.0');
  }

  protected getPropertyPaneConfiguration(): IPropertyPaneConfiguration {
    return {
      pages: [
        {
          header: {
            description: strings.PropertyPaneDescription
          },
          groups: [
            {
              groupName: strings.BasicGroupName,
              groupFields: [
                PropertyPaneTextField('description', {
                  label: strings.DescriptionFieldLabel
                })
              ]
            }
          ]
        }
      ]
    };
  }
}

We need to start defining the properties for this web part. Before we edit it, the code above produces a simple property pane with a description field. There are 3 sections of text in the image below which, if you look at the getPropertyPaneConfiguration, you’ll see we have the Header that has a description field that uses the value of strings.PropertyPaneDescription. We then have groups which only contains one group. That group has a name that uses strings.BasicGroupName, and fields that define the type of control, the name of the control, and the label used for that control.

So where are those values coming from? Well, if you go to your solution structure and find src/webparts/<project name>/loc, you’ll see two files:

  • mystrings.d.ts – this file defines the interface used to create the object that stores list of string values that we want to reuse
  • en-us.js – this file returns an object with a property for each one defined in mystrings.d.ts and a value for those properties.

The following code blocks are the mystrings and en-us files in order.

declare interface IEventHubWebPartStrings {
  PropertyPaneDescription: string;
  BasicGroupName: string;
  DescriptionFieldLabel: string;
}

declare module 'EventHubWebPartStrings' {
  const strings: IEventHubWebPartStrings;
  export = strings;
}
define([], function() {
  return {
    "PropertyPaneDescription": "Description",
    "BasicGroupName": "Group Name",
    "DescriptionFieldLabel": "Description Field"
  }
});

We’re going to add to these and use them in our web part property pane. I’m going to leave the existing fields in place and I’m going to add two to my IEventHubWebPartStrings found in mystrings.d.ts. These 2 new properties will provide the labels for the new controls that I’m going to add to the property pane. EventHubFieldLabel and EventGroupFieldLabel.

declare interface IEventHubWebPartStrings {
  PropertyPaneDescription: string;
  BasicGroupName: string;
  DescriptionFieldLabel: string;

  EventHubFieldLabel: string;
  EventGroupFieldLabel: string;
}

declare module 'EventHubWebPartStrings' {
  const strings: IEventHubWebPartStrings;
  export = strings;
}

Next, we want to provide those property values to the en-us.js and again, we’ll leave the existing properties but we’ll change the values.

define([], function() {
  return {
    "PropertyPaneDescription": "The EventHub web part is a way to highlight Events held by individual groups",
    "BasicGroupName": "EventHub Configuration",
    "DescriptionFieldLabel": "Description Field",

    "EventHubFieldLabel": "Please select your EventHub List",
    "EventGroupFieldLabel": "Please select a Group"
  }
});

Those simple changes have already made an effect on our property pane. Before we add new controls, here’s what has happened to the pane. You’ll notice that the text around our text field is different but we haven’t seen where the default text comes from.

If you open your web part’s manifest file, which in my case is EventHubWebPart.manifest.json, you’ll see a section for preconfiguredEntries and that’s where we find where our description field is getting it’s value.

"preconfiguredEntries": [{
    "groupId": "5c03119e-3074-46fd-976b-c60198311f70", // Other
    "group": { "default": "Other" },
    "title": { "default": "EventHub" },
    "description": { "default": "EventHub description" },
    "officeFabricIconFontName": "Page",
    "properties": {
      "description": "EventHub"
    }
  }]

We’ll add properties (listName, and ListItem) to this for the use of our new controls and as before, we’ll leave the old description property. Note: If you have run a gulp serve, and make changes to the manifest, you won’t see the changes until you rerun the gulp serve.

"preconfiguredEntries": [{
    "groupId": "5c03119e-3074-46fd-976b-c60198311f70", // Other
    "group": { "default": "Other" },
    "title": { "default": "EventHub" },
    "description": { "default": "EventHub description" },
    "officeFabricIconFontName": "Page",
    "properties": {
      "description": "EventHub",
      "listName": "",
      "listItem": ""
    }
  }]

We also want to add listName and ListItem to the IEventHubProps.ts interface.

export interface IEventHubProps {
  description: string;
  listName: string;
  listItem: string;
}

Creating the Dropdowns

So far, we’ve set up 2 new properties which will be used to store values saved in the property pane and some additional steps to get the labels around it to show new wording. Now we can go to our EventHubWebPart.ts and update our getPropertyPaneConfiguration function to use new controls that reference our new properties. Since we’re going to use dropdowns, we need to add PropertyPaneDropdown and IPropertyPaneDropdownOptions to the import of sp-webpart-base.

import {
  BaseClientSideWebPart,
  IPropertyPaneConfiguration,
  PropertyPaneTextField,
  PropertyPaneDropdown,
  IPropertyPaneDropdownOption
} from '@microsoft/sp-webpart-base';

Next, we’re going to use the PnPJS client-side libraries to manage our data access. For more information about the libraries that make up PnPJS, check out the PnPJS repo on GitHub where you can get more information about the individual packages that are available. The libraries have some dependencies so you may want to start with the following script and add any other package that you may need.

npm install --save @pnp/sp @pnp/odata @pnp/common @pnp/logging

Once the packages are installed, we’ll import sp from @pnp/sp and create a few private variables to store the options for each dropdown. At the top of our EventHubWebPart class, we’ll add our 2 new variables and both will be of type IPropertyerPanedropdownOption[]. These will store our possible lists and the items of the selected list. We also want the controls to be disabled when there are no values so we’ll create some variables that we’ll use to set them to disabled by default.

import { sp } from '@pnp/sp';

export default class EventHubWebPart extends BaseClientSideWebPart<IEventHubWebPartProps> {

  private lists: IPropertyPaneDropdownOption[];
  private items: IPropertyPaneDropdownOption[];
  private listsDropDownDisabled: boolean = true;
  private itemsDropDownDisabled: boolean = true;

Go to the getPropertyPaneConfiguration and add our new PropertyPaneDropdowns. I chose to group them together in the same group with the same group name. You can see that we are associating the control to a property, next we assign it’s label to the labels we created earlier, followed by assigning the dropdown options to the private variables that we created for this class, and the disabled property was set to boolean variable that we created.

  protected getPropertyPaneConfiguration(): IPropertyPaneConfiguration {
    return {
      pages: [
        {
          header: {
            description: strings.PropertyPaneDescription
          },
          groups: [
            {
              groupName: strings.BasicGroupName,
              groupFields: [
                PropertyPaneTextField('description', {
                  label: strings.DescriptionFieldLabel
                }),
                PropertyPaneDropdown('listName', {
                  label: strings.EventHubFieldLabel,
                  options: this.lists,
                  disabled: this.listsDropDownDisabled
                }),
                PropertyPaneDropdown('listItem', {
                  label: strings.EventGroupFieldLabel,
                  options: this.items,
                  disabled: this.itemsDropDownDisabled
                })
              ]
            }

          ]
        }
      ]
    };
  }

At this point, we have controls but no value so our drop downs are disabled. We need 2 helper functions that will be used to populate our 2 dropdowns. Both will be structured similarly. The getLists and getItems functions will return a promise of dropdown options. The getLists function gets all lists from the current site where the baseTemplate is equal to 100. That ensures that we only get generic lists. The getItems function will take the listName that will be set by the lists dropdown and use it to get the items for that list.

private getLists(): Promise<IPropertyPaneDropdownOption[]> {
    let options: IPropertyPaneDropdownOption[] = [];
    return new Promise<IPropertyPaneDropdownOption[]>((resolve: (options: IPropertyPaneDropdownOption[]) => void, reject) => {
           sp.web.lists.get().then(response => {
            response.forEach(lst => {
              if (lst.BaseTemplate == "100"){
                options.push({
                  key: lst.Title,
                  text: lst.Title
                });
              }
            });
          }).then( () => resolve(options) );
      }
    );
  }

  private getItems(): Promise<IPropertyPaneDropdownOption[]> {
    let options: IPropertyPaneDropdownOption[] = [];
    return new Promise<IPropertyPaneDropdownOption[]>( (resolve: (options: IPropertyPaneDropdownOption[]) => void, reject) => {
      
      if (this.properties.listName){
      sp.web.lists.getByTitle(this.properties.listName).items.get()
        .then(items => {
          items.forEach(item => {
            options.push({
              key: item.Id,
              text: item.Title
            });
          });
        }).then(() => resolve(options));
      }
    });
  }

Next, we’re going to call the getLists function on the onPropertyPaneConfigurationStart, and assign the results to the lists property which will be used to bind options to the lists dropdown. In the example below, “this.lists” is what we used in the getPropertyPaneConfiguration function when we created our dropdown and assigned a value to the options property.

  protected onPropertyPaneConfigurationStart(): void {
    //disable the dropdowns if we don't have items for them
    this.listsDropDownDisabled = !this.lists;
    this.itemsDropDownDisabled = !this.items;

    // if the lists dropdown has items, then return
    if (this.lists) {
      return;
    }

    this.context.statusRenderer.displayLoadingIndicator(this.domElement, 'lists');

    // get the lists for the current web, then the items if a list was selected
    this.getLists()
      .then(listsResp => {
        this.lists = listsResp;
        this.listsDropDownDisabled = false;
        this.context.propertyPane.refresh();
        return this.getItems();
      })
      .then(itemsResp => {
        this.items = itemsResp;
        this.itemsDropDownDisabled = !this.properties.listName;
        this.context.propertyPane.refresh();
        this.context.statusRenderer.clearLoadingIndicator(this.domElement);
        this.render();
      });

  }

From the onPropertyPaneConfigurationStart function, we’re loading the lists and if a list was previously selected, we load the items. Now we want to focus on changing the available options in the items dropdown if a new list is selected. To do that, we need to use the onPropertyPaneFieldChanged function.

  // compare the old value with the newly selected value and get the new set of items if needed
  protected onPropertyPaneFieldChanged(propertyPath: string, oldValue: any, newValue: any) {
    if (propertyPath === 'listName' && newValue) {
      super.onPropertyPaneFieldChanged(propertyPath, oldValue, newValue);
      const previousItem: string = this.properties.listItem;
      this.properties.listItem = undefined;

      this.onPropertyPaneFieldChanged('listItem', previousItem, this.properties.listItem);
      this.itemsDropDownDisabled = true;

      this.getItems()
        .then(itemResp => {
          this.items = itemResp;
          this.itemsDropDownDisabled = false;
          this.render();
          this.context.propertyPane.refresh();

        });
    }
    else {
      super.onPropertyPaneFieldChanged(propertyPath, oldValue, newValue);
    }
  }

Now we can select our list which will then populate the items dropdown with the items of the selected list.


At this point, we’ve wired up the property pane to the properties BUT we have one problem. Because of the way that we’ve structured the solution, our EventInfo React component isn’t receiving the props that we’re setting. Instead, the EventHub component is receiving the props because of the following code.

  public render(): void {
    const element: React.ReactElement<IEventHubProps > = React.createElement(
      EventHub,
      {
        description: this.properties.description,
        listName: this.properties.listName,
        listItem: this.properties.listItem
      }
    );

    ReactDom.render(element, this.domElement);
  }

I chose to use EventHub as the place where we begin to setup our UI. We could’ve done that in this web part. It’s up to you. If you want to do it in EventHubWebPart and use JSX like we are doing in the EventHub, then you’ll need to change the extension from ts to tsx and create the render function.

EventHub is rendering our navigation and displaying our components. If we swap EventHub from the previous code sample with EventInfo, we can then use the properties but we lose our navigation and the ability to load other components based on our URL. What we can do is tell EventHub to pass all of it’s properties down to EventInfo and that will solve our problem. If we go back to EventHub.tsx, we just need to update the 2nd route where we load the EventInfo to take the props and pass it to EventInfo.

export default class EventHub extends React.Component<IEventHubProps, {}> {
  
  public render(): React.ReactElement<IEventHubProps> {
    console.log('loading event hub component...')
    return (
      <HashRouter>
        <Chrome>
          <Switch>
              <Route path="/members" component={Members} />
              <Route 
                path="/" 
                exact 
                render={() => (<EventInfo {...this.props} />) } />
              <Route render={() => <h1>Page Not found</h1>} />
          </Switch>
        </Chrome>
      </HashRouter>
    );
  }
}

Adding Data Access to Event Info

Now that our property pane can store some basic configuration, we can go back to our EventInfo.tsx and prep it to be able to use those properties to access data from SharePoint to use in our web part. We’ll add an import for the IEventHubProps and the pnp/sp package. We’ll also clear out our event state from EventInfo.

import { IEventHubProps } from './IEventHubProps';
import { sp } from '@pnp/sp';

const initialState: IEventInfoState = {
    event: {
        name: '',
        location: '',
        organizers: [],
        numOfAttendees: 0
    }
}

We can update our class definition to use the IEventHubProps which will allow us to use our typed properties from our interface.

class EventInfo extends React.Component<IEventHubProps, IEventInfoState> {

Next, we want to get our data from the Event Hub list so we’ll use the componentDidMount function to set our component’s state when it loads. The following uses the list name and the item id from the properties pane to get the list item out of the list. I narrow down the fields returned by the “select.” Once I have the values, I set my state object so that I can reuse the values later.

    public componentDidMount(): void {
        if (this.props.listName)
        {
            // store the item id
            const id = Number(this.props.listItem);

            sp.web.lists.getByTitle(this.props.listName)
                .items.getById(id).select("Title", "Organizers/Title", "Event_x0020_Location/Address", "Members").expand("Organizers/Title").get().then((item: any) => {
                    
                    // get the location field's values in JSON format
                    const location = JSON.parse(item['Event_x0020_Location']);
                    // get the Organizer field's values
                    const meetingOrganizers = item['Organizers'];

                    // set the event state
                    this.setState({ event: {
                        name: item.Title,
                        location: location.Address.City + ', ' + location.Address.State,
                        organizers: meetingOrganizers.map(o => o.Title),
                        numOfMembers: item['Members']
                        } 
                    });
                });
        }
    }

For more info on how I got the values out of the location field (Event Location) and the person field (Organizers), check out the two blog posts I recently released on those two fields.

At this point, we can go to the render function and use our state values. To do that, we use “this.state” and reference our objects and properties from there.

public render(): React.ReactElement<any> {
        return (
            <div className={styles.EventInfo}>
                <img src="https://secure.meetupstatic.com/photos/event/8/c/e/0/600_466836064.jpeg" alt="test" />
                <div className={styles.Details}>
                    <h2>{this.state.event.name}</h2>
                    <div>{this.state.event.location}</div>
                    <div>{'Organizers: ' + this.state.event.organizers.join(', ')}</div>
                    <div>{this.state.event.numOfMembers + ' members'}</div>
                </div>
            </div>
        );
    }

Conclusion

In this post, we got our first look at the EventHubWebPart and how to create configurable properties. We then saw how the web part passed its configuration values to the EventHub component and then from that component down to the EventInfo component. We also used PNPJs to populate our property pane fields with our current site’s lists and the items from one of the selected lists. We also used PnPJS to get our list item data to display in our UI. After all of that, we’ll switch to a simpler topic and talk about how to style our components in the next post.

Read Names from a Person Field with PnPJS

If you’re starting out and you need to read names from a person field, it may not be clear how to go about this. In this example, I have a list that has an Organizer person field. In order for me to get the name(s) in that field for a given item, I will name the fields that I need in my select and then expand my Organizer field. If you don’t do this, all that get’s returned is an OrganizerId field.

sp.web.lists.getByTitle(this.props.listName)
   .items.getById(id)
   .select("Title", "Organizers/Title", "Event_x0020_Location/Address", "Members")
    .expand("Organizers/Title")
    .get().then((item: any) => {
      
      // array of meeting organizers
      const meetingOrganizers = item['Organizers'];

      console.log(meetingOrganizers[0].Title)

});

In the example, we’re naming a few fields that we want returned. Title, Organizer/Title, Event Location/Address, and Members. Then we expand Organizers/Title. Expanding Organizer/Title will include the names of the individuals as an array of objects. I used a console.log to display the first record in this example. Hope that saves you some time.

How to Extract Location Data with PnPJS

The SharePoint location field is a nice way of adding location information with the help of Bing Maps. It contains quite a bit of information about a given location. The field stores the data in JSON format so it’s simple to get the data that you need.

A location will be stored in the following format:

{
	"EntityType": "LocalBusiness",
	"LocationSource": "Bing",
	"LocationUri": "https://www.bingapis.com/api/v6/localbusinesses/YN873x128404500",
	"UniqueId": "https://www.bingapis.com/api/v6/localbusinesses/YN873x128404500",
	"DisplayName": "Microsoft",
	"Address": {
		"Street": "45 Liberty Boulevard",
		"City": "Malvern",
		"State": "PA",
		"CountryOrRegion": "US",
		"PostalCode": "19355"
	},
	"Coordinates": {
		"Latitude": 40.05588912963867,
		"Longitude": -75.52118682861328
	}
}

If you’re building a web part with React, and are using the pnpjs libraries, it’s pretty straight forward and here’s a snippet showing how you might do it.

import { sp } from '@pnp/sp';

...

sp.web.lists.getByTitle(this.props.listName)
     .items.getById(id).get().then((item: any) => {
         //parse the event location info
        const location = JSON.parse(item['Event_x0020_Location']);
                   
        console.log(location.Address.City);
        console.log(location.Address.State);

});

So once you have an item, you can assign it’s location field to a variable/const using JSON.Parse. Once you have that, you can access the data via properties. Address is available and has properties of it’s own. City, State, etc. That’s it. Pretty simple.