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.

Building an App with SPFx, React, and TypeScript Part 3: Routing

Introduction

We’re switching gears in this post and moving away from components and data to revisit our navigation from part 1. In that post, we ended up with two stateless functional components to render our navigation. One issue that you’ll have with navigation in a web part is that you can’t change the url. If you do, you’ll end up on a new SharePoint site, instead of a new page in our app.

We added a hash to our url in part 1 to prevent it from leaving the page. In the example of the members page, we use an anchor tag and our url would look like https://localhost:4321/temp/workbench.html#/members

export interface NavigationItemProps {
    url: string,
    children: React.ReactNode
}

const navigationItem = (props:NavigationItemProps) => (
    <li className={styles.NavigationItem}>
        <a 
            href={'#' + props.url}
            >{props.children}</a>
        
    </li>
);

Replacing Anchors with NavLink

We’re going to make a few changes to NavigationItem.tsx. The NavigationItemProps interface will get a new options property called exact and we’ve replaced the anchor tag with NavLink which comes from react-router-dom. The NavLink doesn’t use an href attribute. Instead, it uses “to“. You’ll notice, in the next code snippet, that we removed the hash that we were appending to the URL.

One of the benefits of NavLink is that it automatically adds an active style to the element when the url matches it. So all we need to do is add a “.active” to our css. We can also change it to something other than “active” but that’s fine for us so we’ll leave it.

import * as React from 'react';
import { NavLink } from 'react-router-dom';

import styles from '../NavigationItem/NavigationItem.module.scss';

export interface NavigationItemProps {
    url: string,
    exact?: boolean,
    children: React.ReactNode
}

const navigationItem = (props:NavigationItemProps) => (
    <li className={styles.NavigationItem}>
        <NavLink 
            to={props.url}
            exact={props.exact}
            >{props.children}</NavLink>
        
    </li> 
);

export default navigationItem;

Routing

We currently have a new “exact” property in NavigationItemProps and we replaced the anchor tag with NavLink. These changes alone will break the app. We need to wire the app up to be able to know what to do with that NavLink tag. To do that, we have to go back to the EventHub.tsx where our app begins. We need to import HashRouter from react-router-dom and wrap our app with it. In this case, we wrap Chrome with HashRouter. At this point, the app will begin loading again, our url’s work, and an active style is added to the link that matches the current url, but we’re still not done. I want the app to load components based on the url instead of seeing everything all at once.

import * as React from 'react';
import styles from '../EventHub.module.scss';
import { IEventHubProps } from './IEventHubProps';
import { escape } from '@microsoft/sp-lodash-subset';

import Chrome from '../../statelessComponents/chrome/chrome';
import EventInfo from '../EventInfo/EventInfo';
import Members from '../Members/Members';
import { HashRouter } from 'react-router-dom';

export default class EventHub extends React.Component<IEventHubProps, {}> {
  public render(): React.ReactElement<IEventHubProps> {
    return (
      <HashRouter>
        <Chrome>
          <EventInfo />
          <Members />
        </Chrome>
      </HashRouter>
    );
  }
}

I want the contents of Chrome to be dynamic based on the URL so we’re going to add a Switch to load the proper component.

import * as React from 'react';
import styles from '../EventHub.module.scss';
import { IEventHubProps } from './IEventHubProps';
import { escape } from '@microsoft/sp-lodash-subset';

import Chrome from '../../statelessComponents/chrome/chrome';
import EventInfo from '../EventInfo/EventInfo';
import Members from '../Members/Members';
import { HashRouter, Switch, Route } from 'react-router-dom';

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

There are a few things about the previous code that we need to point out. We start by importing Switch and Route from react-router-dom. We eliminated the Members and EventInfo tags from within Chrome and added Switch. Inside of Switch, we have Route tags that define what components to load when the url matches the path. Switch will allow the proper route to be selected. So if the site url contains “/members”, we load members. If the url contains “/”, our EventInfo component loads. That route also uses exact because we don’t want other url’s to load the EventInfo. In other words, without “exact” EventInfo will load whenever if finds a “/” which would include “/members” and anything else for that matter.

Now, we never created a component for the About page and I did that to show how we handled unexpected URLs. The 3rd route in our switch renders an inline function that returns an h1 tag with Page Not Found. So since we don’t have a route to define what we want to do when we visit “/about”, the Switch will trigger the 3rd route and we’ll see a file not found. I threw in a style update to our navigation to add a border-bottom when the active style is added to the appropriate link. Let’s see what all of this looks like.

In this screenshot, we can see our hashroute and switch working. Our url contains a “#/” which tells it to only load our EventInfo component. Having the hash in the url allows us to append url paths that our app can read while staying on the same SharePoint site.

Here, we can see the Members component is loaded and EventInfo is gone when the url changes to “#/members.”

Finally, we see what happens when a route that wasn’t defined in the switch is met. Here we see our page not found.

Conclusion

In this post, we introduced React components designed to allow us to introduce navigation and dynamic component loading. In the next post, we’ll revisit EventInfo, and EventHub components, and we’ll also take a first look at the web part component. We’re going to make the web part configurable and use that configuration to get data from SharePoint to populate our components.

Building an App with SPFx, React, and TypeScript Part 2: Components

Introduction

In the previous post, we focused on Stateless Functional Components (SFC), which are functions that accept properties used to create elements that are returned by the function. Now we want to focus on Components which are classes where you can manage your application’s state and pass to an SFC as it’s properties.

We’ll create a few components. The main components will be the EventInfo, and Members to match the navigation menu that we created in the first part. We’re going to omit the About component.

Event Info

If you read the previous post, you’ll remember that we created a folder for each of our components and separated them further by differentiating whether they were class components or stateless functional components. We’ll start by creating an EventInfo folder and it’s class component inside. The shell of our code will include a class that extends React.Component and currently accepts any for it’s props and state. It contains a render function that will return a ReactElement and it also accepts any props but we won’t be using props here.

import * as React from 'react';

class EventInfo extends React.Component<any, any> {
    public render(): React.ReactElement<any> {
        return (
            <div>
                
            </div>
        );
    }
}

export default EventInfo;

Since we’ll be using State here, we’ll create an interface for state and put it in it’s own file named IEventInfoState.ts. Our EventInfo’s state will manage the name, location, organizers, and number of attendees.

export interface IEventInfoState {
    event: {
        name: string,
        location: string,
        organizers: string[],
        numOfAttendees: number
    }
}

Now we need to update the EventInfo.tsx to use the IEventInfoState state interface. The EventInfo class component will render an image and some information about the event that we will initialize with default values for now. We’ll come back in a future post and make it dynamic. I’m one of the organizers for the Tri-State Office 365 User Group and the information used in this example is the info from our meetup page.

import * as React from 'react';

import styles from './EventInfo.module.scss';
import { IEventInfoState } from './IEventInfoState';

const initialState: IEventInfoState = {
    event: {
        name: 'Tri-State Office 365 User Group',
        location: 'Malvern, PA',
        organizers: ['Jason', 'Michael'],
        numOfAttendees: 33
    }
}

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

    readonly state: IEventInfoState = initialState;

    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.numOfAttendees + ' attendees'}</div>
                </div>
            </div>
        );
    }
}

export default EventInfo;

The class leverages some simple styles that we’ll skip over for now. We had a placeholder under our menu bar from the previous post. We now need to go back to our EventHub.tsx, import the EventInfo component, and add it to the JSX.

import * as React from 'react';
import styles from '../EventHub.module.scss';
import { IEventHubProps } from './IEventHubProps';
import { escape } from '@microsoft/sp-lodash-subset';

import Chrome from '../../statelessComponents/chrome/chrome';
import EventInfo from '../EventInfo/EventInfo';

export default class EventHub extends React.Component<IEventHubProps, {}> {
  public render(): React.ReactElement<IEventHubProps> {
    return (
      <Chrome>
        <EventInfo />
      </Chrome>
    );
  }
}

This is what our app currently looks like. If you remember, Chrome defines the menu bar which handles the logo and nav. Then there was a main element under the menu bar that contained {props.children} which indicates that we want to render anything that our component wraps. In this case, it’s the EventInfo component.

Members

The next component is Members which will display the profiles of each member. Same as before, we start by creating a Members directory under components and a Members.tsx within directory. There will be a slight difference between Members and EventInfo. EventInfo was fairly self contained but Members will use a stateless functional component to render the individual members. For now, let’s take a look at the Members.tsx starting point.

import * as React from 'react';

class Members extends React.Component<any, any> {
    public render(): React.ReactElement<any> {
        return (
            <div>Here is the members section!</div>
        );
    }
}

export default Members;

Next, we can wire Members up to the EventHub so that we can start seeing changes. We import Members and add the element under our EventInfo element.

import * as React from 'react';
import styles from '../EventHub.module.scss';
import { IEventHubProps } from './IEventHubProps';
import { escape } from '@microsoft/sp-lodash-subset';

import Chrome from '../../statelessComponents/chrome/chrome';
import EventInfo from '../EventInfo/EventInfo';
import Members from '../Members/Members';

export default class EventHub extends React.Component<IEventHubProps, {}> {
  public render(): React.ReactElement<IEventHubProps> {
    return (
      <Chrome>
        <EventInfo />
        <Members />
      </Chrome>
    );
  }
}

And when we take a look at our page, we can see the members component underneath the image.

So far so good. Next, I want the members section to be a list of people with at least an image and their name. I’m going to structure this one similar to how we did the navigation. We had navigation items (plural) which was our container and navigation item (singular) which represented our individual navigation items. This time, we’re going to have a Members class, which we already created, and a Member stateless functional component to define what each person object looks like.

The difference between Members and the Navigation Items is that members will be a class while navigation items was not. The reason I chose to do it this way is because navigation items had hard coded urls and members list will not have the people hard coded into the component… well, it will when we start, but we may circle back and change that. Members list should eventually pull the data from some repository and store it in state then pass the state values to the member SFC as properties.

I’ll start with the SFC by creating a Member folder and a Member.tsx inside of the statelessComponents folder. We’ll also use the Office Fabric UI persona. I’ll just pull some sample code from the page and hard code some values for now.

import React from 'react';

import { Persona, PersonaSize } from 'office-ui-fabric-react'

const member = () => (
    <div className="ms-PersonaExample">
        <Persona
            text="Jason Rivera"
            imageUrl={""}
            size={PersonaSize.small}
            secondaryText="SharePoint Architect"
        />
      </div>
);


export default member;

Let’s go back to the Members class and simply import this member SFC to confirm that it works. The updated Members.tsx looks like this:

import * as React from 'react';

import Member from '../../statelessComponents/Member/Member';

class Members extends React.Component<any, any> {
    public render(): React.ReactElement<any> {
        return (
            <div>
                <Member />
            </div>
        );
    }
}

export default Members;

Our app now looks like this:

Now let’s try to add more people to the list by storing the list in Members and updating the Member SFC to accept data. Our updated SFC will now accept props of any type and expects those props to have a name, imageUrl, and secondaryText. It may be a good idea to create an interface for the props that defines required fields and their type. I won’t do that here for this demo.

const member = (props:any) => (
    <div className="ms-PersonaExample">
        <Persona
            text={props.name}
            imageUrl={props.imageUrl}
            size={PersonaSize.small}
            secondaryText={props.secondaryText}
        />
    </div> 
);

Now we go back to Members and pass our data in. More baby steps. We’ll create a state object that will have our hard coded data and we will add a Member tag for each object. You’ll see that before the render function, we assign values to our state. State contains an array called person and it contains 3 items. Then we set up 3 member tags in the render function and assign the values of our array items to the props defined in Member (name, imageUrl, secondaryText).

import * as React from 'react';

import Member from '../../statelessComponents/Member/Member';

class Members extends React.Component<any, any> {
   state = {
        person: [
            {
                name: "Jason Rivera",
                imageUrl: "",
                secondaryText: "SharePoint Architect"
            },
            {
                name: "Adele Vance",
                imageUrl: "",
                secondaryText: "Business Analyst"
            },
            {
                name: "John Doe",
                imageUrl: "",
                secondaryText: "Manager"
            }
        ]
    }

 public render(): React.ReactElement<any> {

        return (
            <div>
                <Member 
                    name={this.state.person[0].name}
                    imageUrl={this.state.person[0].imageUrl}
                    secondaryText={this.state.person[0].secondaryText} />
                <Member 
                    name={this.state.person[1].name}
                    imageUrl={this.state.person[1].imageUrl}
                    secondaryText={this.state.person[1].secondaryText}/>
                <Member 
                    name={this.state.person[2].name}
                    imageUrl={this.state.person[2].imageUrl}
                    secondaryText={this.state.person[2].secondaryText}/>
            </div>
        );
    }
}

export default Members;

Here is what our app looks like after we add the 3 personas. We don’t want to have to hard code our Member tags so next we’ll work on making that part dynamic.

Back in our render method, we’ll create a membersList variable that will store an array of our Member tags. We’ll use the map function to return a Member tag for each person listed in our state. Then we’ll use the membersList in the render function’s return.

import * as React from 'react';

import Member from '../../statelessComponents/Member/Member';
import styles from './Members.module.scss';

class Members extends React.Component<any, any> {
   state = {
        person: [
            {
                name: "Jason Rivera",
                imageUrl: "",
                secondaryText: "SharePoint Architect"
            },
            {
                name: "Adele Vance",
                imageUrl: "",
                secondaryText: "Business Analyst"
            },
            {
                name: "John Doe",
                imageUrl: "",
                secondaryText: "Manager"
            }
        ]
    }

    public render(): React.ReactElement<any> {

        // create a member tag for each person in our state
        let membersList = (
            this.state.person.map(p =>{
                return (
                <Member 
                    name={p.name}
                    imageUrl={p.imageUrl}
                    secondaryText={p.secondaryText} />
                );
            })
        );

        return (
            <div className={styles.Members}>
                {membersList}
            </div>
        );
    }
}

export default Members;

I added some styles to clean it up a little and the current look, while not perfect, is as follows.

Conclusion

At this point, we have an app that is mostly comprised of stateless functional components. For the most part, they receive some data and define how that data is rendered. We have a couple of components that will manage state and feed that data to the functional components and for now, our data has been hard coded throughout.

In the next post, we’re going to switch gears from components and data and revisit the navigation.

Building an App with SPFx, React, and TypeScript Part 1: Stateless Functional Components

Introduction

This is the first of a series of posts where I’ll walk through the process of building an event management web part for SharePoint Online. I decided to do this to go over several concepts in React and how they’re implemented using TypeScript. I go over the main points of the solution and try to walk through how I went about it. The solution is available on github.

I’m going to skip over setting up the project. There are several resources that can walk you through that process. I wrote a series a while back on quickly setting up a project that you can refer to but keep in mind that some of the details may be outdated as the packages used were older versions.

Setup

To start, I created my Event Hub project, I did a little bit of organization. Inside the components directory, I created an EventHub folder and moved my related event hub files into it. I also created a “statelessComponents” directory to separate my stateless functional components.

Note: This folder setup is just how I decided to do it for this particular solution. Typically my components are in a “Containers” folder and my stateless functional components are in a “Components” folder. You can organize your files however you want.

The EventHub component that was created for me is a Component that manages state and not a Stateless Functional Component. We’ll briefly touch on the EventHub just for some initial setup but this post will focus on Stateless Functional Components as most of your app’s components should be made up of these types of components.

Creating a Stateless Functional Component

Inside my statelessComponents directory, I created another called “chrome” and inside of it, I created chrome.tsx. This will be a stateless functional component since, as of now, I don’t plan on managing state with it. It’s purpose is to define the apps layout and for now, it’ll simply be a top nav bar and a main content area.

Before I write anything in chrome.tsx, I’m going to install a a required package that we’ll need.

npm i --save @types/react-router-dom

Once that’s installed, we can start building our chrome. As I mentioned, it’s role is to define the app’s layout. We’ll keep it simple and set up some placeholders. The following code is really simple. It’s just a stateless functional component so there’s no state and you can see a wrapping div, and two elements inside. The first element is a div where the top nav will appear and the main will display the contents of any component that we wrap with this chrome component.

import * as React from 'react';

const chrome = (props:any) => (
    <div>
        <div>Here is where the top nav will be</div>
        <main>
            {props.children}
        </main>
    </div>
);

export default chrome;

If we decide to add more to the layout, like a footer, a left nav, or anything else, we can edit the chrome component. We’ll revisit this component later as we start build but right now, it’s enough to show a header and the contents of other components wrapped by it.

Using our Chrome

At this point, let’s go back to EventHub.tsx, which was created for us when we setup our SPFx web part. The following is the default code that appears in the class. This is where the web part’s UI is defined. We’re going to remove most of it and apply our own.

import * as React from 'react';
import styles from '../EventHub.module.scss';
import { IEventHubProps } from './IEventHubProps';
import { escape } from '@microsoft/sp-lodash-subset';

export default class EventHub extends React.Component<IEventHubProps, {}> {
  public render(): React.ReactElement<IEventHubProps> {
    return (
      <div className={ styles.eventHub }>
        <div className={ styles.container }>
          <div className={ styles.row }>
            <div className={ styles.column }>
              <span className={ styles.title }>Welcome to SharePoint!</span>
              <p className={ styles.subTitle }>Customize SharePoint experiences using Web Parts.</p>
              <p className={ styles.description }>{escape(this.props.description)}</p>
              <a href="https://aka.ms/spfx" className={ styles.button }>
                <span className={ styles.label }>Learn more</span>
              </a>
            </div>
          </div>
        </div>
      </div>
    );
  }
}

I’m going to edit this component by removing the JSX that is being returned and replacing it with some simple text wrapped by my chrome component. We need to import Chrome from our chrome component which you can see right before the class definition. Then you’ll see that I use Chrome in the return statement with some simple text. We’ll replace the text with a component later but this should be enough to have our layout produce a header and a welcome message underneath.

import * as React from 'react';
import styles from '../EventHub.module.scss';
import { IEventHubProps } from './IEventHubProps';
import { escape } from '@microsoft/sp-lodash-subset';

import Chrome from '../../statelessComponents/chrome/chrome'

export default class EventHub extends React.Component<IEventHubProps, {}> {
  public render(): React.ReactElement<IEventHubProps> {
    return (
      <Chrome>
        Welcome to the event hub
      </Chrome>
    );
  }
}

Running a gulp serve will open our workbench and we can drop our event hub web part to the page. Again, I assume you know the basics of creating and running an SPFx web part. The result should look like the following image. Nothing special just yet but we have Chrome.tsx defining a top nav that we’ll see regardless of what component we want to render and we can see the contents of another component rendered underneath the top nav section.

Adding Navigation

Let’s focus on the top nav bar which we’ll call the Menu Bar. I’m going to create a series of stateless functional components (SFC) to make it up. The components will be the MenuBar which will contain a logo, and navigation items.

Let’s start building these components in sort of a reverse order. Structurally, our menu will look something like:

  • MenuBar
    • Logo
    • Navigation Items
      • Navigation Item

We’ll begin with the Navigation Items. Under the stateless components directory, I created a Navigation Folder which will contain the MenuBar component, the Navigation Items, and the Navigation Item. With that said, the path to my Navigation Items is /statelessComponents/Navigation/NavigationItems/NavigationItems.tsx.

For now, I’ll hard code a few items and start with an unordered list.

import * as React from 'react';

import styles from '../NavigationItems/NavigationItems.module.scss';

const navigationItems = () => (
    <ul className={styles.NavigationItems}>
        <li>Home</li>
        <li>About</li>
        <li>Members</li>
    </ul>
);

export default navigationItems;

Let’s create a component for the logo. I’m going to use the office fabric’s icons for this one so we’ll need to import it. I’m importing an older version of the package because as of the time that I’m writing this, the latest version isn’t working with SPFx.

npm install --save office-ui-fabric-react@5.135.0

Once installed, we can create our logo component which will simply be a call to an Office Fabric icon. This component will be located under statelessComponents/Logo/Logo.tsx.

import * as React from 'react';

import { Icon } from 'office-ui-fabric-react';

const logo = () => (
    <div>
        <Icon iconName='ScheduleEventAction' className='ScheduleEventAction' />
    </div>
);

export default logo;

Next, we’ll bring the Logo and NavigationItems together inside the MenuBar component. This new component will be found under statelessComponents/Navigation/MenuBar/MenuBar.tsx.

import * as React from 'react';

import NavigationItems from '../NavigationItems/NavigationItems';
import Logo from '../../Logo/Logo';
import styles from '../MenuBar/MenuBar.module.scss';

const menuBar = () => {
    return (
        <header className={styles.MenuBar}>
            <Logo  />
            <nav>
                <NavigationItems /> 
            </nav>
        </header>
    );
}

export default menuBar;

Now we need to go back to our Chrome.tsx and introduce MenuBar.tsx so that we can start seeing something other than our placeholder text. Back in the Chrome.tsx, we now import MenuBar and replace our placeholder with the new element.

import * as React from 'react';
import MenuBar from '../Navigation/MenuBar/MenuBar';

const chrome = (props:any) => (
    <div>
        <MenuBar />
        <main>
            {props.children}
        </main>
    </div>
);

export default chrome;

At this point, we have a menu bar at the top of our web part with a logo, followed by a few links. Our navigation items component has a hard coded set of links. I want to pull them out into their own component (navigation item). This SFC will change later and if you are familiar with routing in react, the href should provide a hint around what those changes will be.

import * as React from 'react';

import styles from '../NavigationItem/NavigationItem.module.scss';

export interface NavigationItemProps {
    url: string,
    children: React.ReactNode
}

const navigationItem = (props:NavigationItemProps) => (
    <li className={styles.NavigationItem}>
        <a 
            href={'#' + props.url}
            >{props.children}</a>
        
    </li>
);

export default navigationItem;

The above code is defining our list item, applying a class, adds an anchor tag inside of it and assigns the url prop to the href . The title of the link is the content that this navigationItem component will wrap. We’ll see that shortly.

Now that we have an SFC for our individual items, we need to go back to our NavigationItems SFC to start using our single items. Back in NavigationItems.tsx, we will import NavigationItem and replace our links. (The names are similar but hopefully, it’s no too difficult to follow along).

import * as React from 'react';

import NavigationItem from '../NavigationItem/NavigationItem';
import styles from '../NavigationItems/NavigationItems.module.scss';


const navigationItems = () => (
    <ul className={styles.NavigationItems}>
        <NavigationItem url='/' >Home</NavigationItem>
        <NavigationItem url='/about'>About</NavigationItem>
        <NavigationItem url='/members'>Members</NavigationItem>
    </ul>
);

export default navigationItems;

After some style updates, we have a simple bar along the top of our web part with a logo made from an Office Fabric UI icon and a few links. Here are a few shots of what our project structure and web part look like.

Conclusion

We started with an SPFx solution and restructured the directories a little bit. I used the Component folder for our stateful components and created a “statelessComponents” folder for our stateless functional components. That’s not a common practice. I did that just for this demo because I’m using this solution to explain certain concepts outside of this blog series and naming the folders this way will probably help people unfamiliar with react remember what is in those folders.

We did make some changes to our EventHub component just so that we can see our new additions but this post focused mostly on the components that don’t maintain their own state.

In the next post, we’re going to focus on components that will handle state. These components will maintain the content and pass down the appropriate data as props. We already saw an example of that when we set up our navigation items to pass props down to the navigation item.

The Return of SharePoint Saturday Philly

Last night, we announced that June 22, 2019 will be the return of SharePoint Saturday Phillywith a twist. The innovations made in Office 365, over the years, with SharePoint as a core product make it nearly impossible to look at SharePoint in a bubble. We’ve decided that SharePoint Saturday Philly will focus on the Microsoft Cloud as a whole. What does that mean? We want to see our speakers talk about Azure, Dynamics, DevOps, the suite of Office 365 products, and of course, SharePoint.

There’s been some buzz lately about bringing this event back to our area so we got a small group together and we’re making it happen, but we will need help from Sponsors to ensure that we can make it happen. We’ve opened the call for speakers. Attendee registration is also open.

Seats are limited. Let’s make this a successful return!

For more information, please visit our SPSPhilly page on SPSEvents.org or contact the SPSPhilly team at contact@spsphilly.org