Office 365 AI Series

Over the course of a couple of years, I’ve written several AI related topics that occasionally get brought up so I thought I’d group them together. Enjoy!

Building an App with SPFx, Reactjs, and TypeScript Part 6: React-Redux

Introduction

In this post, we’re going to look at some react-redux basics. Before we do, I have made some additions to the solution that I haven’t covered because it would’ve just been a repeat of things we’ve already gone over. The changes include some styling around the Group details (where the location, organizers, members appear), the addition of a property in the property pane which is used to point to a new meeting list, and a meetingList component which is used to render the meetings. The meeting list contains an item for each meeting with a lookup to the Event list which we’ll use to render the right meetings for the selected event.

This post was a little challenging to write and assumes you know a little about redux but maybe are having some trouble applying redux to SPFx because of TypeScript. If you are brand new to redux, I suggest you read up on it on the ReduxJS site.

My naming choices are getting messy. Event vs Group vs Meeting. That’s what I get for whipping this together without planning, but we’ll try to cover this up with redux. So what I’m calling an Event is essentially our group (Tri-State Office 365 section) and then below the event are the meetings (March, April, May) for that group.

State Management with React-Redux

Now that you’re caught up, let’s talk some react-redux. State management in this demo isn’t complex and because of that, redux isn’t really necessary here but that’s not going to stop us from adding it for the sake of discussion. In this demo, we have one state object that gets created in our Event Info component and that state is passed down as props down to any other component that needs it. We also have a 2nd state object created in the Members component. It’s created with hard coded values but we’re not going to focus on that state object here. In both components, we’re not changing any state values, and we’re not creating or deleting values from the state. We also aren’t sharing values unless we’re passing them down to a component created inside our component. For example, EventInfo passes a property from its event state down to the MeetingList component.

The state objects in both the Event Info and Members components are not global. They’re passed down to the component that needs them. If we need to share our state data with components that aren’t nested, we would need to repeat CRUD operations in the various components. Redux allows us to centralize that state by creating a central object and functions that will manage the CRUD operations needed.

There are 4 basic concepts in redux that we need to know:

  • Store – holds the application state, allows state to be accessed/updated
  • Reducers – updates your state
  • Actions – defines what we want to do (the action) and the required payload. For example, if we want to add an attendee to a meeting, the action may be named “ADD_ATTENDEE” and the payload may be an object with the attendees name and email.
  • Subscription – a registered listener that tells your component when the state has been updated in the store

We start by installing redux, react-redux, and redux-thunk (we’ll talk about thunk later). Then we’ll create new folders to house our actions and reducers.

npm install --save redux
npm install --save react-redux
npm install --save redux-thunk

As you can see, we have a new Store folder and inside of it are the Actions and Reducers folders.

In the EventHub.tsx, we import createStore from redux which will let us setup a store for our app state. The store also needs a reducer in order to set it up so we’ll create that as well. First, the reducer.

I created a file in the reducer folder named EventInfo.tsx. I like naming my reducers after the component that is expected to use it. Then I’m going to move all of my state logic over from EventInfo to that reducer. Start by importing IEventInfoState like we did in the EventInfo component. Move our initialState object over to the reducer. Then we create our reducer function which takes a state and action and for now, we’ll just return the state. For now, our reducer is ready and we can connect it to our store. We’ll revisit the reducer soon.

import { IEventInfoState } from '../../components/EventInfo/IEventInfoState';

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

export const reducer = (state: IEventInfoState = initialState, action) => {
    return state;
};

Next, we go to the EventHub.tsx component since it’s sort of our entry point in that it’s directing what we’re loading. It is in this component where we’ll create our store. (You could also choose to do this in the web part class since that’s what loads the EventHub component). To start, we need to add a few imports.

import { Store, createStore, applyMiddleware } from 'redux';
import { Provider } from 'react-redux';
import thunk from 'redux-thunk';
import { reducer } from '../../store/reducers/EventInfo';
import { IStateProps } from '../../store/IStateProps';

I haven’t mentioned IStateProps yet. In my store folder, I added IStateProps which will define what our global state object will look like. It extends the IEventHubWebPartProps which will allow our state to carry the web part properties around. You’ll also notice a familiar block of code in the group object. It’s almost identical to our “event” object except for it’s name. I mentioned that I was going to clean up some of my naming issues and this where I’m doing it. When we start using our global state, you’ll see me reference group instead of event. Under that, you’ll see a couple of functions that we’ll define later (onIncrementMembers and onInitEvent).

export interface IStateProps extends IEventHubWebPartProps {
    group?: {
        name: string,
        location: string,
        organizers: string[],
        numOfMembers: number,
        meetings: any[],
        
    },
    onIncrementMembers?: () => any;
    onInitEvent?: (props:any) => any;
}

Going back to EventHub, I’ll need to add a public property and a constructor where I’ll instantiate my store object. The createStore function expects a reducer and I’m going to also add thunk as I’ll need it later.

  private store: Store<IStateProps>;

  public constructor(props:IEventHubProps) {
    super(props);

    this.store = createStore(
      reducer, 
      applyMiddleware(thunk)
    );
  }

We now have our imports, and our store. Next, we need to apply our Provider and connect the store. To do that, we’ll wrap everything in our render’s return with Provider and assign our store to the store prop. What this does is it makes our new redux store object available to any component that uses “connect.” It let’s those components use the global state from the store.

public render(): React.ReactElement<IEventHubProps> {
    return (
      <Provider store={this.store}>
        <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>
      </Provider>
    );
  }

Now we can start taking steps to remove our state management from our components and leverage redux. We’ll return to our EventInfo component.

In order to connect to redux, we need to add a new import. The following gives us a function that will allow us to make that connection and while we’re importing, we’ll add IStateProps here too.

import { connect } from 'react-redux';
import { IStateProps } from '../../store/IStateProps';

Next, right above our export and outside of our component class, we’ll create a new const called mapStateToProps that will hold our state values. I mentioned that I was choosing poor names throughout the web part so I’m going to make a slight naming correction here. We’re going to map our state event to a group property and refer to our events as groups from here on. As you can see, we take the props that were passed to the component, represented by ownProps, and we’re assigning their values back to properties that will be part of our new state object. We are also taking our old state’s event property and mapping it to the new state’s group property. Then we need to pass our mappings as a parameter to a connection function that wraps our export. This will let me quickly clean up that naming issue that I mentioned. I could also go back and just clean it up everywhere but I thought this would be a good way to demonstrate that we’re using different objects now.

Finally, if you look down at the export, you’ll see the connect function that accepts our mapStateToProps object and wraps our EventInfo. This is where we connect our component to redux and how our new state get’s connected.

const mapStateToProps = (state:IEventInfoState, ownProps:IEventHubProps):IStateProps => {
    return {
        description: ownProps.description,
        listName: ownProps.listName,
        listItem: ownProps.listItem,
        meetingListName: ownProps.meetingListName,
        group: {
            ...state.event,  
        }

    };
};


export default connect(mapStateToProps)(EventInfo);

Here’s where TypeScript makes things interesting. If you aren’t using TypeScript, you can start using that group property by using “this.props.group” but because we are, group isn’t found. The reason it’s not found is because in our class declaration line, we are still using the old interface for our props (IEventHubProps) which doesn’t include a group property. IEventHubProps just contains the properties that we captured in the property pane.

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

We need to replace IEventHubProps with IStateProps. Remember, it extends IEventHubProps so we still have the web part properties, and it has our old state properties which we’ll map data to. The class should look like this now.

class EventInfo extends React.Component<IStateProps, IEventInfoState> 

Now we can start replacing state references in our EventInfo component with our new prop. For example, “this.state.event.organizers” becomes “this.props.group.organizers”

public render(): React.ReactElement<any> {
        return (
            <div>
            <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.props.group.name}</h2>
                    {/*<h2>{this.state.event.name}</h2>*/}
                    <div>
                        <Icon iconName='MapPin' className='MapPin' />
                        {this.props.group.location}
                        {/*{this.state.event.location}*/}
                    </div>
                    <div>
                        <Icon iconName='PartyLeader' className='PartyLeader' />
                        {'Organizers: ' + this.props.group.organizers.join(', ')}
                        {/*{'Organizers: ' + this.state.event.organizers.join(', ')}*/}
                    </div>
                    <div>
                        <Icon iconName='Group' className='Group' />
                        {this.props.group.numOfMembers + ' members'}
                    </div>
                </div>
                
                </div>
                <hr />
                <MeetingList meetings={this.props.group.meetings} />
                {/*<MeetingList meetings={this.state.event.meetings} />*/}
            </div>
        );
    }

I left the old state properties commented in the code above for reference. If you were to run this, you wouldn’t see any data and that’s because we haven’t set our new state object. We’ve simply defined what it would look like, how we’re going to map the old state to the new state, and connected our component to redux to allow it to use the new mapped object. We need to replace all of the local state logic that we had in our ComponentDidMount function and move it to our global state.

Actions and Reducers

The first thing that I’ll do is add actionTypes.ts to the actions folder. This is just going to store some global constants that we’ll use in our action creators and reducers. These constants are used in multiple places so it’s just good practice to centralize it to avoid inadvertently making a typo and wasting time trying to find it.

export const INCREMENT_MEMBER = 'INCREMENT_MEMBER';
export const DECREMENT_MEMBER = 'DECREMENT_MEMBER';
export const SET_EVENT = 'SET_EVENT';
export const INIT_EVENT = 'INIT_EVENT';

Now back to our reducer that we saw earlier. Our reducer will receive our current state, create a new state object, make the necessary updates, and pass that back to our component. Here’s what the new reducer looks like. A switch statement will decide what to do based on the action type. So when we want to increment the number of members, we take the value from the state that’s passed in, add a 1, assign it to the new state object, and send that back.

export const reducer:Reducer<any> = (state: IEventInfoState = initialState, action) => {
    let newState = Object.assign({}, state);

    switch (action.type){
        case actionTypes.INIT_EVENT, actionTypes.SET_EVENT:
            newState.event = action.payload;
            return newState;
        case actionTypes.INCREMENT_MEMBER:
            newState.event.numOfMembers = state.event.numOfMembers + 1;
            return newState;            
        default: 
            return state;
    }
};

Now we move on to the actions. We have a new eventInfo.ts in our action folder and all of the logic that was in our ComponentDidMount which would get our data from a SharePoint list now lives here with some changes.

We have a new initEvent function that accepts properties of type IEventHubWebPartProps which are the props that we set in the property pane. We’re still in the actions/eventInfo.tsx at this point. We wrap that in a dispatch and at the end, when we have our object fully created, we call a dispatch with another function and our payload.

export const initEvent = (props:IEventHubWebPartProps) => dispatch => {
    let selectedEvent:any = {};

    if (props.listName)
    {
        // store the item id
        const id = Number(props.listItem);

        sp.web.lists.getByTitle(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
                selectedEvent = { 
                    event: {
                        name: item.Title,
                        location: location.Address.City + ', ' + location.Address.State,
                        organizers: meetingOrganizers.map(o => o.Title),
                        numOfMembers: item['Members'],
                        meetings: []
                    } 
                };
            }).then(() => {
                const updateEvents = {...selectedEvent.event};

                sp.web.lists.getByTitle(props.meetingListName)
                    .items.filter(`Event_x0020_HubId eq ${id}`).get().then((items: any) => {


                        items.forEach(function (m) {
                            let meetingDate = new Date(m.Start_x0020_Time);
                            
                            updateEvents.meetings.push({
                                date: meetingDate.toDateString(),
                                title: m.Title,
                                description: m.Description
                            });
                        });

                        selectedEvent.event = updateEvents;
                        dispatch(setEvent(selectedEvent.event));
                    });
            });
    }
};

The setEvent function is simply our action with the payload.

export const setEvent = (event) => {
    return {
        type: actionTypes.SET_EVENT,
        payload: event
    }
};  

We now need to call all of this somehow. In order to do that, we need to dispatch our actions to the store. So we once again go back to our EventInfo.tsx component and right under mapStateToProps, we will create mapDispatchToProps and add that to our connection function.

const mapDispatchToProps = (dispatch) => {
    return {
        onIncrementMembers: () => dispatch({type: 'INCREMENT_MEMBER'}),
        onInitEvent: (wpProps) => dispatch(actions.initEvent(wpProps))
    };
};


export default connect(mapStateToProps, mapDispatchToProps)(EventInfo);

The first thing you may notice is that we are defining functions. Those functions are the ones that are part of our IStateProps interface. The idea is that we will be able to use our new props to call our functions. So just like we were able to call this.props.group.organizers, we can also call this.props.onInitEvent().

The first function is a typical action dispatch where you can pass in a type and payload and the reducer takes it from there. The second function is a little more involved. In that function, we call another function that lives with our actions and that creates our object that gets assigned to the state. Once the object is created, that dispatch is called, and our action is given our events. Since we need to call a function first to create the object that is eventually passed to our action, we need to use thunk. There isn’t anything extra that we need to do for it to work. All the setup was done when we created our store in EventHub.tsx. Thunk allows us to pass a function to the dispatch. Without it, you’d see an error telling you that it’s expecting a simple object and not a function.

The wiring up of everything is annoying and time consuming since there are so many steps and you end up going back and forth but at this point, we have 2 final steps. Back in our EventInfo component, we need to update our ComponentDidMount and have it simply call our props’ onInitEvent function which was defined in mapDispatchToProps. The new componentDidMount looks like the following snippet and all of the code that used to be in that function are now in the initEvent in our action.

public componentDidMount(): void {
        this.props.onInitEvent(this.props);
}

Finally, just to wrap it up and show that redux is wired up, I’ll throw in a button on the page which takes the total number of members, adds 1, and displays it on the page. It’s not saving to SharePoint; just adding to the state. If you wanted to save, you could just use PnPJS to update SharePoint with the new value from your state.

<div>
   <button onClick={this.props.onIncrementMembers} >Add Member</button>
</div>

Now I’ll click the button a bunch of times and you’ll see that I went from 344 member to 356.

Conclusion

I know… that was a lot and probably a bit confusing but that’s because of all of the bouncing around that you end up doing to wire everything up. To summarize, we created a Store object that accepts a reducer. We have a global State object that get’s centrally updated. We moved our state logic into our action creator instead of our components so that all the updates happen in a single location. The actions are what send data to our store. We also have a reducer that defines how our state will change. The example that we saw was adding 1 to our member counts. Inside our components, we have a mapStateToProps that defines which state property get’s mapped to which global state property. We also have a mapDispatchToProps where we dispatch our actions to the store. *Phew*. I hope this series was helpful. I know that some of the react-redux setup was tricky for me when I first tried it.

Building an App with SPFx, Reactjs, and TypeScript Part 5: Styling Components

Introduction

Part 4 was a bit long so let’s go over a lighter topic and talk about styling. We’ve seen how we build everything using components. We’ve even isolated our components by placing them in their own folders inside the solution. We’ll go through some examples that show how we style each of our components.

Setup

The following is a snippet of the folder hierarchy currently in place.

  • webparts
    • components
      • EventHub
        • EventHub.tsx
        • IEventHubProps.ts
      • EventInfo
        • EventInfo.tsx
        • IEventInfoState.ts

SPFx is setup to make use of SCSS files that get bundled. When you want to style a component, create an scss file in the same directory as your component and follow the following format: “<componentName>.module.scss

  • webparts
    • components
      • EventHub
        • EventHub.module.scss
        • EventHub.tsx
        • IEventHubProps.ts
      • EventInfo
        • EventInfo.module.scss
        • EventInfo.tsx
        • IEventInfoState.ts

I won’t go in depth on how this solution is styled here. I’m going to update the css in this solution over the course of these posts. Microsoft has some good material on working with css that you can reference as needed. Here’s a sample of the css used for the EventInfo component.

.EventInfo {
    display: flex;
    flex-flow: row;
    padding: 5px;
    justify-content: space-between;
    
    img {
        max-width: 450px;
        height: 250px;
        border-radius: 5px;
        border: 1px solid #ccc;
        box-shadow: 1px 2px;
        margin: 0px 10px;
    }

    .Details {
        padding: 0px, 15px;

        div {
            margin: 5px 0px;
        }
    }
}

Once you have your .scss defined, you’ll need to import it into your solution. When the solution gets packaged, it’s exported as styles so when I want to import the EventInfo.module.scss into my component, I’ll use the following:

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

Then, when it’s time to apply a style to your component you’ll simply reference it with {styles.className}. In the example, I have 2 divs. The container div is using “styles.EventInfo” which uses the Flex display, as seen above, and some padding. The inner div uses “styles.Details” which is just applying some padding/margins.

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

That’s it. Like I said, I wasn’t going to go to deep into it but if you’re getting started, that’s square one and the reference provided above can help fill in the blanks as you hit the occasional snag. In the next post, we’re going to see how we can use React-Redux to centralize our app state. This app is small and doesn’t require redux but we’ll go through a simple example.

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.