Categories
Azure Active Directory Microsoft Graph O365 PNPJS REACT SharePoint SharePoint Online

Create a SharePoint App with React: Surface SharePoint list data with pnp/sp

Please NOTE: This blog series is currently a work in progress and subject to change.

This is part 4 in a series about creating a React App, using the Microsoft PNPJS library and Ui-Fabric in SharePoint.

Disclaimer


WebDev By The Bay is not affiliated with nor is this post endorsed by any company mentioned in this post. 

All opinions belong to the author of this post.

All Copyrights and Trademarks belong to their respective owners.

Full disclosure!

I am not a Software Engineer, I’m a Web Developer of almost 20 years. In my day job, for the past 6 years, I have been tasked with developing solutions in SharePoint.

This series is my journey in learning not only SharePoint but various other technologies.

Please Let me be clear! We will create some custom code but most of the code will be from the library examples to make learning the technology easier, at least for me.

This series is not about showing you how to code the various pieces but rather how you can piece the pieces together to make an application.

The basic setup of the React App is based on react-starter by Ted Pattison.

I will be adding how to use the PNPJS Library to surface SharePoint list data, modifying webpack.config and providing some tips on using the Ui-Fabric.

The app will use the Microsoft PNPJS library to surface the data in the backing SharePoint list data and Microsoft Power Automate to do the Approval workflows.

Please note the following:
1.The PNPJS library may be out of date at the time you read this post.
Please be sure to check for updates.
2. You will need to upload the bundle js file and index.aspx to SharePoint before you can debug communication with SharePoint List data.

App Purpose

Dashboard app to help manage Long Term Parking Requests and Approvals.

React Hooks

In Part 2 we removed the following node modules:

  • redux
  • react-redux
  • redux-thunk

We now need to add “react-hooks

On the cmd line: npm install react-hooks –save-dev

Add SharePoint Lists

Also in part 2 of this series, we created our SafePark folder, this is the home of the UI of this app.

Now we need to add the backing lists. If you don’t have a SharePoint tenant, you can get one by joining the Office 365 Developer program.

Add the following lists:

Campus – columns

Campus List columns
Campus List Columns

Building

Campus List columns
Building List Columns

Region choice field – choices can be of your choosing. I chose to enter Cities where a company may have a presence. e.g. Hollister, Salinas, Monterey, Santa Cruz

ParkingRequests

ParkingRequests List Columns
ParkingRequests List Columns

Add A Request Form

Create a new folder in components/views/ called requestForm

Add a new file in the requestForm folder called requestForm.tsx and add the following code:

import * as React from 'react';
import { DatePicker, DayOfWeek, Dropdown, IDropdownOption, IDatePickerStrings, Label, Link, MaskedTextField, mergeStyles, mergeStyleSets, MessageBar, PrimaryButton, TextField } from 'office-ui-fabric-react';
import { Icon } from 'office-ui-fabric-react/lib/Icon';
import { Stack } from 'office-ui-fabric-react/lib/Stack';

import IBuilding from "../../../models/IBuilding";
import ICampus from '../../../models/ICampus';
import './requestForm.scss';


const PolicyIcon = () => <Icon iconName="DocumentSet" className="ms-IconExample" />;

const iconClass = mergeStyles({
  fontSize: 50,
  height: 50,
  width: 50,
  margin: '0 25px'
});

const classNames = mergeStyleSets({
  deepSkyBlue: [{ color: 'deepskyblue' }, iconClass],
  greenYellow: [{ color: 'greenyellow' }, iconClass],
  salmon: [{ color: 'salmon' }, iconClass]
});

const DayPickerStrings: IDatePickerStrings = {
  months: ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December'],

  shortMonths: ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'],

  days: ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'],

  shortDays: ['S', 'M', 'T', 'W', 'T', 'F', 'S'],

  goToToday: 'Go to today',
  prevMonthAriaLabel: 'Go to previous month',
  nextMonthAriaLabel: 'Go to next month',
  prevYearAriaLabel: 'Go to previous year',
  nextYearAriaLabel: 'Go to next year',
  closeButtonAriaLabel: 'Close date picker'
};

const controlClass = mergeStyleSets({
  control: {
    margin: '0 0 15px 0',
    maxWidth: '300px'
  }
});

export interface IDatePickerState {
  firstDayOfWeek?: DayOfWeek;
}

export interface ICompanyLocationState {
    campuses: ICampus[],
    buildings: IBuilding[],
    selectedCampus: string
}

export interface ICampuses {
  Id: number;
  Title: string;
}

export interface IBuildings {
  Id: number;
  Title: string;
  Street: string;
  City: string;
  PostalCode: string;
}

// We will change to surface from SP List
const cmps: IDropdownOption[] = [
    {key: 'Main', text: 'Main'},
    {key: 'SouthBend', text: 'South Bend'},
];

// We will change to surface from SP List
const bldgs: IDropdownOption[] = [
    {key: '12', text: '12'},
    {key: '25', text: '25'},
];

function ParkingRequestForm () {

    return (
 <form>
    <div className="container">
      <div className="ms-Grid" dir="ltr">
          <div className="ms-Grid-row">

              <div className="ms-Grid-col ms-sm6 ms-md4 ms-lg4">
                   <Link className="policyLink" href="#/policies"><PolicyIcon /><strong> Parking Policies</strong>
                </Link>
                </div>
              <div className="ms-Grid-col ms-sm6 ms-md4 ms-lg12">
                  <Label><strong>All Fields are Required!</strong></Label>
                  <MessageBar>
                        Mobile App version is available from the Power Apps Viewer. The viewer can be downloaded from the App Store link on your mobile device.
                    </MessageBar>
              </div>

          </div>
          <div className="ms-Grid-row row-spacer">
                <div className="ms-Grid-col ms-sm6 ms-md4 ms-lg2">
                    <TextField label="EmployeeID"/>
                </div>
          </div>
           <div className="ms-Grid-row row-spacer">
                <Label><strong>Manager Information - This will eventually come from Azure Active Directory</strong></Label>
                <Stack horizontal tokens={{ childrenGap: 50 }} styles={{ root: { width: 650 } }}>
                    <TextField label="Manager Name"/>
                    <MaskedTextField label="Phone" mask="(999) 999 - 9999"/>
                    <TextField label="Email"/>
                </Stack>

            </div>

            <div className="ms-Grid-row row-spacer">
                <Label><strong>Emergency Contact Information</strong></Label>
                <Stack horizontal tokens={{ childrenGap: 50 }} styles={{ root: { width: 650 } }}>
                    <TextField label="Contact Name"/>
                    <MaskedTextField label="Phone" mask="(999) 999 - 9999"/>
                    <TextField label="Email"/>
                </Stack>

            </div>
          <div className="ms-Grid-row row-spacer">

                <div className="ms-Grid-col ms-sm6 ms-md4 ms-lg6">
                  <Label>Start Date</Label>
                    <DatePicker
                    className={controlClass.control}
                    // firstDayOfWeek={firstDayOfWeek}
                    strings={DayPickerStrings}
                    placeholder="Start Date"
                    ariaLabel="Start Date"
                    />
                </div>
                <div className="ms-Grid-col ms-sm6 ms-md4 ms-lg6">
                    <Label>Return Date</Label>
                    <DatePicker
                    className={controlClass.control}
                    // firstDayOfWeek={firstDayOfWeek}
                    strings={DayPickerStrings}
                    placeholder="Return Date"
                    ariaLabel="Return Date"
                    />
                </div>
              </div>
          </div>
           <div className="ms-Grid-row row-spacer">
                <div className="ms-Grid-col ms-sm6 ms-md4 ms-lg12">
                    <Label><strong>Vehicle Information</strong></Label>
                <Stack horizontal tokens={{ childrenGap: 50 }} styles={{ root: { width: 650 } }}>
                    <TextField label="Make"/>
                    <TextField label="Model"/>
                    <TextField label="Year"/>
                    <TextField label="License Plate"/>
                </Stack>
                </div>
          </div>
          <div className="ms-Grid-row row-spacer">
                <div className="ms-Grid-col ms-sm6 ms-md4 ms-lg12">
                    <Label><strong>Parking Information</strong></Label>
                    <Stack horizontal tokens={{ childrenGap: 50 }} styles={{ root: { width: 650 } }}>
                        <Dropdown placeholder="Select an option" label="Campus"  options={cmps} />
                        <Dropdown placeholder="Select an option" label="Building" options={bldgs} />
                    </Stack>
                </div>
          </div>
          <div className="ms-Grid-row row-spacer">
              <div className="ms-Grid-col ms-sm6 ms-md4 ms-lg12">
                <TextField label="Justification" multiline rows={4} />
            </div>
          </div>



            <div className="ms-Grid-row align-right">
                <PrimaryButton className="deepSkyBlue" text="Submit Parking Request"  allowDisabledFocus />
            </div>

      </div>
      </form>
    );
  }
export default ParkingRequestForm;

The form:

Long Term Parking Request Form
Long Term Parking Request Form

Please NOTE: PNPJS v2 was published Dec 20, 2019.
We will use v2 in this tutorial.
https://pnp.github.io/pnpjs/transition-guide/

Stop the project if it is still running.

Install pnp/sp to surface data from our SharePoint lists. I’m including several other useful modules that I plan to use later.

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

In requestForm.tsx : Setup our Imports

import "@pnp/sp/fields";
import "@pnp/sp/items";
import "@pnp/sp/lists";
import { IWeb, Web } from '@pnp/sp/webs';

const web: IWeb = Web(“[YOUR SHAREPOINT TENANT URL]”);

Setup our reducer and SharePoint list data functions to return the Campus and Buildings data.

First let’s setup our reducer. In the ParkingRequestForm function, create the following:

const [state, dispatch] = useReducer(reducer,[]);

useEffect(() => {
      _getCampuses();
      return () => {
        console.log('Calling get Campuses');
      }
},[])

Add the onChange handlers to the Campus and Buildings Dropdown controls.

Next add a new const for the levels since the vehicle could be parked in a parking garage.

const levels: IDropdownOption[] = [
    {key: 'ground', text: 'Ground'},
    {key: '1', text: '1'},
    {key: '2', text: '2'},
    {key: '3', text: '3'},
    {key: '4', text: '4'},
    {key: '5', text: '5'}
];

Next setup our drop down controls. We add a new drop drop down for the Levels.

<div className="ms-Grid-row row-spacer">
                <div className="ms-Grid-col ms-sm6 ms-md4 ms-lg12">
                    <Label><strong>Parking Information</strong></Label>
                    <Stack horizontal tokens={{ childrenGap: 50 }} styles={{ root: { width: 650 } }}>
                        <Dropdown placeholder="Select a Campus"
                        label="Campus"
                        defaultSelectedKey=""
                        options={state.campuses}
                        onChange={_getBuildings}/>

                        <Dropdown placeholder="Select a Building"
                        label="Building"
                        defaultSelectedKey=""
                        options={state.buildings} />

                         <Dropdown placeholder="Level"
                        label="Level"
                        defaultSelectedKey="Ground"
                        options={levels} />
                    </Stack>
                </div>
          </div>

We also need to add a text area to allow the vehicle owner to specify any distinguishing features and or marks on the vehicle. This serves two purposes in my mind, now I am not a legal expert, if you are concerned contact your company’s legal department. But this text area will help in case the owner comes back and claims that there is a scratch, dent or some other issue with the vehicle upon their return.

Add a textfield control just below the License Plate field, right after the closing </Stack>

 <TextField label="Distinguishing Features and or marks on your vehicle to help Security personnel in identifying your vehicle." multiline rows={3} />

Next we need to setup our functions to call our backing SharePoint lists.

// CAMPUSES
    async function _getCampuses(): Promise<void> {
    
    await web.lists
        .getByTitle("Campus")
        .items
        .select(
            "Id",
            "Title"
        )
        .getAll()
        .then(response => {
            const cmps = response.map(desc => {
                return {
                    ...desc
                };
            });

            dispatch({
                type: "setCampuses",
                data: cmps
            });
        })
    }

    //BUILDINGS
    async function _getBuildings(event: React.FormEvent<HTMLDivElement>, campus: IDropdownOption): Promise<void> {

    await web.lists
        .getByTitle("Buildings")
        .items.select(
            "Id",
            "Title",
            "Campus"
        )
        .filter("Campus eq %27" + campus.text + "%27")
        .getAll()
        .then(response => {
            const bldgs = response.map(desc => {
                return {
                    ...desc
                };
            });

            dispatch({
                type: "setBuildings",
                data: bldgs
            });
        })
    }

Now we need to add our Reducer. Add this right after the ParkingRequestForm function closing bracket.

function reducer(state, action) {
    switch(action.type) {
        case "setCampuses": {
            // We need to rename the keys: Id to key and Title to text
           return { campuses: action.data.map(function(cm) {
                cm['key'] = cm['Id'];
                delete cm['Id'];
                cm['text'] = cm['Title'];
                delete cm['Title'];
                return cm;
            })};
        }
         case "setBuildings": {
            // We need to rename the keys: Id to key and Title to text and keep the current state
           return {...state, buildings: action.data.map(function(bldg) {
                bldg['key'] = bldg['Id'];
                delete bldg['Id'];
                bldg['text'] = bldg['Title'];
                delete bldg['Title'];
                return bldg;
            })};
         }
        default:
            return state;
    }
}

All together requestForm.tsx should look like the following:

import * as React from 'react';
import { sp } from "@pnp/sp";
import "@pnp/sp/fields";
import "@pnp/sp/items";
import "@pnp/sp/lists";
import { IWeb, Web } from '@pnp/sp/webs';
import { DatePicker, DayOfWeek, Dropdown, IDatePickerStrings, IDropdownOption, Label, Link, MaskedTextField, mergeStyles, mergeStyleSets, MessageBar, PrimaryButton, TextField } from 'office-ui-fabric-react';
import { Icon } from 'office-ui-fabric-react/lib/Icon';
import { Stack } from 'office-ui-fabric-react/lib/Stack';

import { useEffect, useReducer } from 'react';
import IBuilding from "../../../models/IBuilding";
import ICampus from '../../../models/ICampus';
import './requestForm.scss';

const web: IWeb = Web("[YOUR SHAREPOINT TENANT URL]");
const PolicyIcon = () => <Icon iconName="DocumentSet" className="ms-IconExample" />;

const iconClass = mergeStyles({
  fontSize: 50,
  height: 50,
  width: 50,
  margin: '0 25px'
});

const classNames = mergeStyleSets({
  deepSkyBlue: [{ color: 'deepskyblue' }, iconClass],
  greenYellow: [{ color: 'greenyellow' }, iconClass],
  salmon: [{ color: 'salmon' }, iconClass]
});

const DayPickerStrings: IDatePickerStrings = {
  months: ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December'],

  shortMonths: ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'],

  days: ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'],

  shortDays: ['S', 'M', 'T', 'W', 'T', 'F', 'S'],

  goToToday: 'Go to today',
  prevMonthAriaLabel: 'Go to previous month',
  nextMonthAriaLabel: 'Go to next month',
  prevYearAriaLabel: 'Go to previous year',
  nextYearAriaLabel: 'Go to next year',
  closeButtonAriaLabel: 'Close date picker'
};

const controlClass = mergeStyleSets({
  control: {
    margin: '0 0 15px 0',
    maxWidth: '300px'
  }
});

export interface IDatePickerState {
  firstDayOfWeek?: DayOfWeek;
}

export interface ICompanyLocationState {
    campuses: ICampus[],
    buildings: IBuilding[],
    selectedCampus: string
}

export interface ICampuses {
  Id: number;
  Title: string;
}

export interface IBuildings {
  Id: number;
  Title: string;
  Street: string;
  City: string;
  PostalCode: string;
}

const levels: IDropdownOption[] = [
    {key: 'ground', text: 'Ground'},
    {key: '1', text: '1'},
    {key: '2', text: '2'},
    {key: '3', text: '3'},
    {key: '4', text: '4'},
    {key: '5', text: '5'}
];


function ParkingRequestForm () {
const [state, dispatch] = useReducer(reducer,[]);

useEffect(() => {
      _getCampuses();
      return () => {
        console.log('Calling get Campuses');
      }
},[])


return (
 <form>
    <div className="container">
      <div className="ms-Grid" dir="ltr">
          <div className="ms-Grid-row">

              <div className="ms-Grid-col ms-sm6 ms-md4 ms-lg4">
                   <Link className="policyLink" href="#/policies"><PolicyIcon /><strong> Parking Policies</strong>
                </Link>
                </div>
              <div className="ms-Grid-col ms-sm6 ms-md4 ms-lg12">
                  <Label><strong>All Fields are Required!</strong></Label>
                  <MessageBar>
                        Mobile App version is available from the Power Apps Viewer. The viewer can be downloaded from the App Store link on your mobile device.
                    </MessageBar>
              </div>

          </div>
          <div className="ms-Grid-row row-spacer">
                <div className="ms-Grid-col ms-sm6 ms-md4 ms-lg2">
                    <TextField label="EmployeeID"/>
                </div>
          </div>
           <div className="ms-Grid-row row-spacer">
                <Label><strong>Manager Information - This will eventually come from Azure Active Directory</strong></Label>
                <Stack horizontal tokens={{ childrenGap: 50 }} styles={{ root: { width: 650 } }}>
                    <TextField label="Manager Name"/>
                    <MaskedTextField label="Phone" mask="(999) 999 - 9999"/>
                    <TextField label="Email"/>
                </Stack>

            </div>

            <div className="ms-Grid-row row-spacer">
                <Label><strong>Emergency Contact Information</strong></Label>
                <Stack horizontal tokens={{ childrenGap: 50 }} styles={{ root: { width: 650 } }}>
                    <TextField label="Contact Name"/>
                    <MaskedTextField label="Phone" mask="(999) 999 - 9999"/>
                    <TextField label="Email"/>
                </Stack>

            </div>
          <div className="ms-Grid-row row-spacer">

                <div className="ms-Grid-col ms-sm6 ms-md4 ms-lg6">
                  <Label>Start Date</Label>
                    <DatePicker
                    className={controlClass.control}
                    // firstDayOfWeek={firstDayOfWeek}
                    strings={DayPickerStrings}
                    placeholder="Start Date"
                    ariaLabel="Start Date"
                    />
                </div>
                <div className="ms-Grid-col ms-sm6 ms-md4 ms-lg6">
                    <Label>Return Date</Label>
                    <DatePicker
                    className={controlClass.control}
                    // firstDayOfWeek={firstDayOfWeek}
                    strings={DayPickerStrings}
                    placeholder="Return Date"
                    ariaLabel="Return Date"
                    />
                </div>
              </div>
          </div>
           <div className="ms-Grid-row row-spacer">
                <div className="ms-Grid-col ms-sm6 ms-md4 ms-lg12">
                    <Label><strong>Vehicle Information</strong></Label>
                <Stack horizontal tokens={{ childrenGap: 50 }} styles={{ root: { width: 650 } }}>
                    <TextField label="Make"/>
                    <TextField label="Model"/>
                    <TextField label="Year"/>
                    <TextField label="Color"/>
                    <TextField label="License Plate"/>
                </Stack>
                <TextField label="Distinguishing features and or marks to assist Security personnel in identifying your vehicle." multiline rows={3} />
                </div>
          </div>
          <div className="ms-Grid-row row-spacer">
                <div className="ms-Grid-col ms-sm6 ms-md4 ms-lg12">
                    <Label><strong>Parking Information</strong></Label>
                    <Stack horizontal tokens={{ childrenGap: 50 }} styles={{ root: { width: 650 } }}>
                        <Dropdown placeholder="Select a Campus"
                        label="Campus"
                        defaultSelectedKey=""
                        options={state.campuses}
                        onChange={_getBuildings}/>

                        <Dropdown placeholder="Select a Building"
                        label="Building"
                        defaultSelectedKey=""
                        options={state.buildings} />

                         <Dropdown placeholder="Level"
                        label="Level"
                        defaultSelectedKey="Ground"
                        options={levels} />
                    </Stack>
                </div>
          </div>
          <div className="ms-Grid-row row-spacer">
              <div className="ms-Grid-col ms-sm6 ms-md4 ms-lg12">
                <TextField label="Justification" multiline rows={4} />
            </div>
          </div>



            <div className="ms-Grid-row align-right">
                <PrimaryButton className="deepSkyBlue" text="Submit Parking Request"  allowDisabledFocus />
            </div>

      </div>
      </form>
    );



    // CAMPUSES
    async function _getCampuses(): Promise<void> {

    await web.lists
        .getByTitle("Campus")
        .items
        .select(
            "Id",
            "Title"
        )
        .getAll()
        .then(response => {
            const cmps = response.map(desc => {
                return {
                    ...desc
                };
            });

            dispatch({
                type: "setCampuses",
                data: cmps
            });
        })
    }

    //BUILDINGS
    async function _getBuildings(event: React.FormEvent<HTMLDivElement>, campus: IDropdownOption): Promise<void> {

    await web.lists
        .getByTitle("Buildings")
        .items.select(
            "Id",
            "Title",
            "Campus"
        )
        .filter("Campus eq %27" + campus.text + "%27")
        .getAll()
        .then(response => {
            const bldgs = response.map(desc => {
                return {
                    ...desc
                };
            });

            dispatch({
                type: "setBuildings",
                data: bldgs
            });
        })
    }

}
export default ParkingRequestForm;


function reducer(state, action) {
    switch(action.type) {
        case "setCampuses": {
            // We need to rename the keys: RENAME Id to key and Title to text
           return { campuses: action.data.map(function(cm) {
                cm['key'] = cm['Id'];
                delete cm['Id'];
                cm['text'] = cm['Title'];
                delete cm['Title'];
                return cm;
            })};
        }
         case "setBuildings": {
            // We need to rename the keys: RENAME Id to key and Title to text and keep the current state
           return {...state, buildings: action.data.map(function(bldg) {
                bldg['key'] = bldg['Id'];
                delete bldg['Id'];
                bldg['text'] = bldg['Title'];
                delete bldg['Title'];
                return bldg;
            })};
         }
        default:
            return state;
    }
}

Our Request Form should now look like the following:

Request Form with Parking Input fields
Request Form with Parking Input fields

This is getting a little long, so I am going to create another post to cover saving the form data.

Next – Part 5: Save the form data to the SharePoint list with pnp/sp

References

Register your app with Azure Active Directory
Overview