I'm Sick of Meetings

We’ve all been disrupted by COVID-19. I have coworkers losing their minds having to work from home. I personally had some pretty productive days during the first few days of this mess but then…. the meetings started to pile up. If I’m not in a Teams meeting, I’m on a Zoom, a phone call, and in one accidental case, a Skype meeting. I understand that it’s the obvious way to stay in touch, but to be honest, when everyone defaults to scheduling a call, it’s a drain on productivity and makes it difficult to effectively do your job. When that happens, it’s time to start setting boundaries.

Should I Attend this Meeting?

The first question you have to ask yourself is, “do I need to be in this meeting?” Most people will say, “yeah, I was invited.” Not so fast. Maybe start by asking questions like:

  • Would anyone notice if I didn’t attend?
  • Do I have anything of value that I need to provide?
  • Am I going to provide more than a short update that can be sent via an email?
  • Am I saying anything new since the last time we met?
  • Do I need to attend the full hour?
  • Was I snoring last time?
  • Could this be a simple email/chat message?
  • Do I know what this meeting is about?

Now, that last question is important. If a meeting doesn’t have an agenda or a goal, it’s pretty safe to say that the organizer is going to be winging it. I’m guilty of this myself but with the whole social distancing/self quarantining nuisance, it’s time to stop.

What can you do?

Meetings are ingrained deep in people’s bones and in their souls and you’re probably not going to change them while we’re dealing with this pandemic so I guess that’s it, I’ve got to suck it up and attend meetings. NO! Here’s what I’m doing and it’s sort of simple. Block your calendar from meetings with your own meeting. That’s right! Fight fire with fire. Don’t go crazy and block all of your available time. You just need to block enough time to eliminate frequent context switching and be productive.

You can manually do this but it can be tedious. If your organization uses MyAnalytics (https://myanalytics.microsoft.com) like mine does, you can use that tool to automate it. Let’s take a look at the product because if you haven’t sensed it from the above, I’ve had it with meetings.

MyAnalytics

MyAnalytics provides personal productivity insights. Now, the numbers below are patterns from the last 4 weeks but I’ll say that I think my numbers are slightly off. I’ll explain later.

There are 4 areas where the tool with help.

  • Focus
    • Shows you how much focus time you have. Focus time is defined as 2 consecutive where you’re not in a meeting.
  • Wellbeing
    • Shows how much Quiet time you have. In other words, the amount of time outside of your working hours (which in my case is set to 8am-5pm) that you haven’t been sucked back into work.
  • Network
    • Shows you how many people you’ve collaborated with in the last 4 weeks and past 12 months. So the people you’ve emailed, chatted, met with in that time. It even shows you things like the amount of time you’ve spent collaborating with specific individuals and what percentage of their emails you’ve read. I’m not going to show those numbers but I have someone in my list that I’ve only read 25% of their emails. Yikes.
  • Collaboration
    • Shows how you communicate throughout the day.

The Categories and Numbers

Ok, so I mentioned earlier that I think my numbers are off so now is a good time to explain. The tool suggests that 63% of my time has been focus time but it’s basing that number on my calendar and doesn’t (can’t) factor in impromptu meetings which seems to be my life lately. So 63% is wrong because with the number of meetings that I get pulled into, I know that I don’t often get 2 hours of focus time; HOWEVER, I will now start having more focus time.

Focus

The Focus page lets you automatically schedule focus time. You can see the green checkmarks in the top section under my focus plan. On each of those days, it found 2 hours that someone hasn’t monopolized and blocked it off with a meeting. On days that it couldn’t find 2 consecutive hours, it booked 2 separate 1 hour blocks for me. As soon as I did that, people stopped booking my time and started asking me when would be a good time or “hey, are you free at this time?”

The Focus page also gives you some good tips and insight. So you can see that I read more than half of my emails within 30 minutes of receiving them and that my calendar for the upcoming week usually fills up by Friday.

You can click on the View Suggestions link under each section to get advice with justification behind the suggestion.

Wellbeing

Wellbeing, or the “are you in danger of burning out” page, shows you how many quiet days you’ve had in the past 4 weeks, or days where you didn’t work outside of normal working hours. I seem to average 3 quiet days per week and that’s horrible. I’m a bit of a workaholic and that’s partially due to my imposter syndrome. If I’m not working, I’m studying something. It’s unhealthy, and this page will just slap you in the face with that fact. I’m not surprised by the pie chart on the right.

Network

The Network page is less of a slap in the face as it simply shows you how many people you’ve interacted with. In the last 4 weeks, I’ve emailed/chatted/met with 40 people and 140 in the past 12 months. Further down on the page, it also suggests people who you might want to keep in touch with or flag as important contacts.

Collaboration

The Collaboration page shows insight into your meeting habits. This one shows that I typically spend 30% or 13 hours of my week in meetings and 75% are recurring (again, not all my meetings are on my calendar). Realistically, I average somewhere around 17-22 hours / week in meetings.

Conclusion

I’m sick of meetings and the reality is that while we’re dealing with this pandemic, it’s going to get worse and we need to take action so that our constant connectivity doesn’t hinder our productivity. If you have MyAnalytics, use it so that you can understand how you spend your time and certainly use it to take control of your time. That simple feature to automatically block time is wonderful. If you don’t have MyAnalytics, that’s fine. You won’t have some of the cool insights but that shouldn’t stop you from booking 2 consecutive hours on your calendar where you can take care of what you need to take care of.

Troubleshooting SPFx running in a Docker Container

I recently had a chance to do a “3 city tour” for the Global Microsoft 365 Developer Bootcamp in Iselin, NJ, Malvern, PA, and NYC. I got to present alongside Tom Daly and Manpreet Singh. During the NYC event, we were joined by Peter Ward who presented the 4th session. (Session Info)

During those presentations, I walked our audience through setting up their development environment. That included, creating a tenant, setting up the app catalog, installing node, gulp, yeoman, and the spfx yeoman generator. We saw all sorts of errors during this session, from permissions issues, to version conflicts. Because of those issues, I mentioned during each session that Docker is a good way to avoid the issues that we were facing and a good way to speed up setting up an environment for new developers or new project members.

Setting up Docker was beyond the scope of these sessions because of time and we had all levels of experience in the rooms. From people who never wrote a line of code to experts.

Once you have Docker up and running, there are a few steps that you’ll need to do before you can get your spfx web part running. For this post, I am using a Docker image that was built from node:8.9.4 and I am using SPFx 1.9.1. I also created a simple SPFx web part with all the defaults. Node 10 is the recommended version.

I won’t walk through setting up Docker here. I’m assuming you have it installed and understand how to run a container. If not, Waldek Mastykarz is a good source for getting started.

Workbench is Unreachable

Once you have a solution, the first thing you likely will try to do is run a gulp serve to make sure everything is running and instead of being able to access the workbench, you’ll be greeted with the following.

In order to fix the unreachable page, you’ll need to make edits to the serve.json file found in your project’s config folder.

The old file will look like this:

{
  "$schema": "https://developer.microsoft.com/json-schemas/core-build/serve.schema.json",
  "port": 4321,
  "https": true,
  "initialPage": "https://localhost:5432/workbench",
  "api": {
    "port": 5432,
    "entryPath": "node_modules/@microsoft/sp-webpart-workbench/lib/api/"
  }
}

We need to add a hostname that points to 0.0.0.0. Your new serve.json should look like the following.

{
  "$schema": "https://developer.microsoft.com/json-schemas/core-build/serve.schema.json",
  "port": 4321,
  "https": true,
  "hostname": "0.0.0.0",
  "initialPage": "https://localhost:5432/workbench",
  "api": {
    "port": 5432,
    "entryPath": "node_modules/@microsoft/sp-webpart-workbench/lib/api/"
  }
}

Running a gulp serve again should fix the workbench.

SPLoaderError

The next issue comes when trying to load the web part. You’ll see an SPLoadedError.loadComponentError message. I’ve seen the web part work the first time you drop it on the page, but then this error comes up after refreshing or adding the web part on the page a 2nd time.

Go to the following path in your project: \node_modules\@microsoft\sp-build-web\lib\SPWebBuildRig.js. In that file, somewhere around line 91, you’re going to find the following:

const serve = spBuildCoreTasks.serve;
        spBuildCoreTasks.writeManifests.mergeConfig({
            debugBasePath: `${serve.taskConfig.https ? 'https' : 'http'}://${serve.taskConfig.hostname}:${serve.taskConfig.port}/`
        });

After the first line that sets the serve constant, you’re going to want to wrap the mergeConfig logic with an if statement. It should look like the following when you’re done.

 if (!spBuildCoreTasks.writeManifests.taskConfig.debugBasePath){
            spBuildCoreTasks.writeManifests.mergeConfig({
                debugBasePath: `${serve.taskConfig.https ? 'https' : 'http'}://${serve.taskConfig.hostname}:${serve.taskConfig.port}/`
            });
        }

You also need to set the debugBasePath that is referenced. In order to do that, you’ll need to go to \config\write-manifest.json and add the debugBasePath with the localhost as seen below.

{
  "$schema": "https://developer.microsoft.com/json-schemas/spfx-build/write-manifests.schema.json",
  "cdnBasePath": "<!-- PATH TO CDN -->",
  "debugBasePath": "https://localhost:4321"
}

Once those 3 steps are done, you should have a functioning web part. Just make sure you re-run gulp serve and load the page from https://localhost:5432/workbench. That will redirect you to https://localhost:4321.

Planning SharePoint Saturday 2019: Behind the Scenes

Image

I recently had the pleasure of organizing SharePoint Saturday Philly on June 22nd. The last time that SPS Philly was around was 4 years ago. This was my first time organizing this event, or any event for that matter, and I thought it would be nice to write down my experience.

Early Planning

During a meeting of the Tri-State Office 365 user group, a brief discussion broke out where several people expressed their desire to see SPS Philly return to our area. After that meetup, a few of us got together and started discussing what it would take to restart the event. We kicked around a few basic ideas like how to run the event with a tight budget, how many people to plan for, where to buy/rent the required items (tables, coffee, sodas, etc), and identifying the people that we need to speak to in order to host the event at the Microsoft Malvern office. We even discussed whether the event should be called SharePoint Saturday Philly or Office 365 Saturday Philly. I chose to go with SharePoint Saturday for this year, but I’m certain that it’ll go by a different name next year.

In January, I decided that I was definitely going to give it a shot and needed to choose a date. In talking with someone involved in past events, we felt it was best to avoid the winter months so I was looking at May 25th but the SharePoint Conference was scheduled for the week before. SPSDC was also eventually lined up for the beginning of June. I wanted to keep some distance from those two events so I then thought, “how about mid to late July?” No luck, SPSNYC was lined up for July. (I’m actually writing this on the day of SPSNYC.)

I finally decided that June 22nd was going to be the day. In February, I reached out to the SPS Events folks to request a site for my event info. They set me up with one and sent me a pdf which was essentially a checklist of common tasks for the event.

Sponsors and Speakers

During the month of February, I started to figure out how I was going to collect payment from sponsors, what my sponsor package would look like, setting up a domain and a social media presence. In March, I got word that another person, Manpreet Singh, inquired about hosting SPS Philly so I reached out and he and I met up and started to finalize the sponsor package and our plan for reaching out to businesses who might be interested in sponsoring. On March 15th, the call for speakers opened and then the call for sponsors on the 18th.

Over the next few months, we made sure to mention the event frequently on social media. I aimed to tweet at least two times per day on @SPS_Philly Shortly after, the speakers started to submit topics. Now things started to get interesting.

I started reaching out to potential sponsors via email; usually in the evenings or during my lunch break. This method resulting in a very low response rate. Many times, I would reach out to the wrong contact. If I couldn’t find a contact person, I would reach out to a sales or marketing team or a business’ info or contact account. The info/contact accounts were practically useless but they were last resort. I’m not sure if those accounts were monitored but I did start receiving marketing material from those accounts in many cases. There were a couple of businesses that showed interest very early on, but never pulled the trigger.

I eventually started getting responses and in many cases, it wasn’t until the 2nd or 3rd follow up email. I kept a spreadsheet of everyone I contacted, when I contacted them, if they responded, and any notes from our exchanges. Then things started to get scary. At this point, I’m committed. There was nothing that was going to make me postpone or cancel this event. That’s when I saw 2 tweets by the previous organizer that made me nervous. The 2nd tweet was as follows:

I kept pushing forward, reaching out to local businesses, businesses that deal with Office 365 and Azure, businesses that have sponsored regional events, and even some that were sponsoring the SharePoint Conference. More bad news was around the corner. I spoke to another local event organizer who mentioned that local sponsorship in general has been down and to compensate, he needed to start charging attendees to come to events so that the sponsor fees could be low enough to entice sponsors. This wasn’t an option for me if I wanted to stay under the SPS Events umbrella. One of their only rules is that the events are free to attendees. I had to keep reaching out to businesses. My co-organizer also attempted to reach out to some businesses, in person, at local events. They also expressed interest but never pulled the trigger.

Failed Diversity and Inclusion

While all of this was going on, I wanted to do something to promote diversity and inclusion, partially because my wife and I had a daughter the previous year and since then, the topic has been a little more front and center to me. I had no idea what this was going to look like at first and by the time I started doing anything about it, it was probably to late. I reached out to a couple of women led groups in the tech industry but didn’t get a response. I spoke to others and no one could think of what it would look like. It wasn’t until the event was nearing that I finally had some ideas but it was too late to organize anything. At a minimum, I thought it would be cool to have 50% of our speakers be women but we never got enough submissions to do that. We received a wide variety of topics so it worked out that I was able to accept every presentation submitted by a woman. A few backed out Every woman that submitted a topic was accepted; the topics were varied enough that I didn’t have to make decisions on one person’s presentation vs another. Several, 4 or 5, had to back out. In the end, we had 3 women speakers.

Budgets and Estimates

Meanwhile, my persistence was starting to pay off and the sponsors started coming in. Others started responding to my messages but I reached out to a few more just a little too late for them to budget for the event. Now that the funds started to come in, I started to solidify my shopping list and estimates. The food estimates were spot on. We had some left overs but not too much. The drinks, however… that was a different story.

I found a breakdown of items purchased for past events and I based my numbers off of that. The past event had 300-400 people and I capped this event at 200 so I bought half of the items listed. Well… it seems that people don’t drink sodas anymore (I don’t either) and it showed. Here are the left over drinks and there were many more that you can’t see in this photo. I still have the water, orange juices, and apple juices. I gave away the sodas.

Image

In addition to buying too much soda, I didn’t get enough coffee. I went by the stores judgement on how much I would need for the amount of people who were planning to attend and it was grossly under estimated. The first shipment of coffee was done before the first session ended. It may have been done before the first session began. A 2nd shipment came in with the lunch order and that one lasted longer.

I also rented some tables and that was the one thing that I expected to go smoothly and oddly enough, it didn’t. I placed an order through a local shop who services that location frequently. I ordered a good number of tables and asked for them to be delivered on Friday as late as possible. We agreed to 4pm. It needed to be late because Microsoft shares that building and they didn’t want to interfere with shipments for the other business. Well, they delivered the tables at 8am on Friday and it turns out, that’s not unusual for them. Luckily, it didn’t interfere with other shipments that day.

Speaker Dinner

Over the couple of months leading up to the event, my biggest concern was the budget; especially after seeing those tweets about the past event and getting feedback about recent events. I shopped around quite a bit when deciding what would be on the menu and from where. Once we were nearing the event, it was looking like I was going to have a solid budget. I was keeping the speakers up to date about looking for a venue for the speaker dinner. It was between a hotel restaurant where I secured a room block for the speakers, and P.J. Whelihan’s Pub. The hotel got back to me fairly quickly and there wasn’t much time left so I went with the hotel. That turned out to be a good decision because the food was pretty good. I was glad that we were able to have the dinner. It was great getting to know these speakers.

Game Day

The doors opened early for us to setup. I had some volunteers arrive at 7am and we started unloading the drinks and moving the tables. Breakfast also started arriving. Surprisingly, the setup continued all the way until the beginning of registration. I planned on saying a few words of welcome in the beginning but never got around to it so everyone just went from the food to their sessions. From here on out, I was running around the building and it wouldn’t stop until happy hour. My phone is setup to track my steps. It’s something that I setup for a work competition a while back and never turned off. By the time lunch rolled around, I had crushed my daily goal. I was hoping to actually see a few presentations but that never happened. Between checking on registration, food, making sure the sponsors had what they needed, and checking in on the speakers, I didn’t have enough time to actually sit in on any of them other than to take pictures.

Feedback

The feedback for the event was overwhelmingly positive. The speaker surveys where very positive as well as the event surveys. The event survey asked the attendees to tell us what they’d like us to change and what we shouldn’t change. The attendees really enjoyed the venue and the food. A few of the changes requested included, adding more time to the sessions and less time to the breaks, adding opening remarks, better communication around session changes and the lunch and learn sessions, more sessions, and use PowerApps/Flow to collect feedback. Someone suggested Philly themed food like pretzels (I did look into it but most of the food was coming from the same location for simplicity and they didn’t have pretzels).

I agreed with most of the feedback and plan to address them next year.

Lessons Learned

Some people saw during the event that I was running around with a schedule in my hands. On the back, I was making a list of lessons learned. Here are a few:

  • Choose an earlier date
  • Order more coffee!!
  • Make time for opening remarks
  • Use eventbrite for all registrations (including speakers and sponsors)
  • Buy less soda
  • Set a deadline for lunch & learn topics (our platinum sponsors got their own lunch sessions but I didn’t get all of the topics in time to add them to the schedule)
  • 5 sessions per hour instead of 6. We did 6 sessions per hour b/c the venue had strict room capacities but to be safe, I chose to not account for no-shows.
  • Make sure I have scotch tape (thanks for the tip Nancy!)
  • Delegate better. I have a tendency to take on everything when it’s crunch time but now that I know what’s involved, I should be able to manage that better.

There are more items on my list but these were some of the more important ones.

Final Thoughts

Overall, the event was successful but we did have a larger than average no-show rate. On average, an SPS event has a 20-30% no-show rate. I suspected that it would be a little larger for us and it turned out to be correct. We had close to 50% no-show. We had a week of terrible storms and that Saturday was about as perfect of a day as you could ask for. We had people coming in and out too so it might have seemed a little more than that to attendees. There were people who only stuck around for the morning sessions and people who only attended the evening sessions.

I joked that the event was like my wedding day. Fun, lots of emotions, and in the end, a bit of a blur. I’m looking forward to doing it again next year and I’ve even started looking into hosting other events. Nothing confirmed yet, but I’m looking into a few things. If I do, I’ll keep you posted.

I couldn’t have done it without the help from so many people and I am very grateful for all their help. From Mike Mukalian, Manpreet Singh, Tim Ferro, Bill Wolff, and Tom Daly for their advice, direction, and help, to the volunteers, speakers, sponsors, and attendees who helped make it all a success. Thank you all.

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.