Categories
Microsoft Flow Microsoft Graph O365 PNPJS REACT SharePoint SharePoint Online

Creating a SharePoint App with React: Styling! Add UI-Fabric controls and styles.

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

This is part 3 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!


This series is my journey in learning these 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.

Table Of Contents

Add a header to begin generating the table of contents

The app will use the Microsoft PNPJS library to surface the data in the backing SharePoint list data.

App Purpose

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

Add Ui-fabric

In package.json make sure that “office-ui-fabric-react” is installed.

In the src directory, we need to change index.html

<!DOCTYPE html>
<html>

<head>

    <title>Extended Parking</title>
    <meta charset="utf-8" />
    <meta name="viewport" content="initial-scale=1.0">
    <link rel="stylesheet"
        href="https://static2.sharepointonline.com/files/fabric/office-ui-fabric-core/10.0.0/css/fabric.min.css" />
</head>

<body className="ms-Grid" dir="ltr">
    <div id="react-target" className="ms-Grid-row"></div>
</body>

</html>

Div id=”react-target” is where we will surface our React app and we want it to display as a Grid Row, so we give it the className of ms-Grid-row

Setup a Default Theme

For the following examples, I am just copying and pasting the Microsoft UI-Fabric Example code.
I only make minor tweaks to the example code as needed for this app.
The example code can be found here: https://developer.microsoft.com/en-us/fabric#/get-started

Change App.tsx to the following:

import * as React from 'react';
import { Route, Switch, withRouter } from "react-router-dom";
import * as AppImages from './../images/AppImages';

import './../../node_modules/office-ui-fabric-react/dist/css/fabric.min.css';
import './App.scss';
import Home from './views/home/home';
import { loadTheme } from 'office-ui-fabric-react';

loadTheme({
  palette: {
    themePrimary: '#337e80',
    themeLighterAlt: '#f4fafa',
    themeLighter: '#d4eaeb',
    themeLight: '#b2d8d9',
    themeTertiary: '#72b1b3',
    themeSecondary: '#438e8f',
    themeDarkAlt: '#2e7273',
    themeDark: '#276061',
    themeDarker: '#1d4747',
    neutralLighterAlt: '#f8f8f8',
    neutralLighter: '#f4f4f4',
    neutralLight: '#eaeaea',
    neutralQuaternaryAlt: '#dadada',
    neutralQuaternary: '#d0d0d0',
    neutralTertiaryAlt: '#c8c8c8',
    neutralTertiary: '#bab8b7',
    neutralSecondary: '#a3a2a0',
    neutralPrimaryAlt: '#8d8b8a',
    neutralPrimary: '#323130',
    neutralDark: '#605e5d',
    black: '#494847',
    white: '#ffffff',
  }
});

class App extends React.Component<any, any> {

// REMOVE
    constructor(props: {}) {
        super(props);

        this.state = {
            currUser: []
        };
    }

  render(): JSX.Element {
    return (
      <header id="app-container" className="ms-Grid grid" dir="ltr">
        <div id="banner-row" >
          <div id="banner" >
         // CHANGE   
           <div id="appTitle">SAFEPARK</div>
         // TO
             <div id="appTitle"><a href="#">SAFEPARK</a></div>  
          </div>
        </div>
        <div id="content-body-row" >
// CHANGE 
          <div id="content-body" className="content-body ms-Grid">

            <Switch>
                <Route exact={true} path="/" component={Home} />
                <Route path="/home" component={Home} />
            </Switch>



            {/* <div className="reactImage">
              <img src={AppImages.React} alt="React.js Logo" />
            </div> */}

          </div>
        </div>
// TO
<div id="content-body" className="content-body ms-Grid">
            <LeftNavPanel/>

                <Switch>
                    <Route path="/home" component={Home} />
                    <Route path="/policies" component={Policies} />
                    <Route path="/contacts" component={Contacts} />
                    <Route path="/myDashboard" component={MyDashboard} />
                    <Route path="/requestForm" component={ParkingRequestForm} />
                    <Route path="/" component={Home} />
                </Switch>

          </div>
      </header>
    );
  }
}

export default withRouter(App);

In App.tsx we can layout our default theme colors and setup our routes. We will change App.tsx as we continue in this series.

Let’s change App.scss now.

@import '~office-ui-fabric-react/dist/sass/_References.scss';



body {
  margin: 0px;
  line-height: 2.5em;
}

#app-container{
  @include ms-Fabric;
  @include ms-Grid;
  background-color: white;
  min-height: 600px;
  border-bottom-left-radius: 8px;
  border-bottom-right-radius: 8px;
  border: 0px solid ;
  @include ms-borderColor-neutralPrimaryAlt;
  margin: auto;
}

#banner-row {
  @include ms-Grid-row;
  #appTitle {
      position: relative;
      left: 100px;
  }
}


#banner {
  @include ms-Grid-col;
  @include ms-font-xxl;
  @include ms-fontColor-white;
  @include ms-sm12;
  @include ms-md12;
  @include ms-lg12;
  height: 48px;
  background-image: url('./../images/AppIcon.png');
  background-position: top 5px left 15px;
  background-repeat: no-repeat;
  background-color: $ms-color-themeDarker;
  border-bottom: 1px solid $ms-color-themeDarker;
  padding: 4px;
  padding-left: 56px;

  h2 {
    margin-bottom: 0px;
  }
}

#banner-row, #topnav, #content-body-row {
  @include ms-Grid-row;
}

#app-body {
    position: relative;
    top: 55px;
    left: 0;
    height: 100%;
    width: 100%;
}

.content-body {
  @include ms-Grid-col;
  @include ms-font-xl;
  @include ms-lg12;
  @include ms-fontColor-themePrimary;
  padding: 8px;

  h1, h2, h3 {
    margin: 4px;
  }

  p {
    @include ms-fontColor-neutralPrimary;
    margin: 4px;
  }

  .reactImage {
    @include ms-fadeIn500;
  }

}

Create The Views

Add a new folder to the components folder, call this folder views

In views, create a new folder called home

In home, create two new files, home.scss and home.tsx

// home.scss

.welcome{
    position: relative;
    left: 50px;
    // width: 400px;
    background-color: white;
    padding: 20px;
    margin: 30px;
    // box-shadow: 10px 5px 5px grey;
}

@media only screen and (min-width: 1280px ) {
    .pageContent {
        position: relative;
        display: block;
    }
}


// home.tsx

// CHANGE
import * as React from "react";

import {
  ComboBox,
  Fabric,
  IComboBox,
  IComboBoxOption,
  IComboBoxProps,
  mergeStyles,
  PrimaryButton,
  SelectableOptionMenuItemType
} from 'office-ui-fabric-react/lib/index';

import { Nav, INavLink } from 'office-ui-fabric-react/lib/Nav';
import { Link } from 'office-ui-fabric-react/lib/Link';
import { TextField } from 'office-ui-fabric-react/lib/TextField';
import { CommandBar } from 'office-ui-fabric-react/lib/CommandBar';
import { Announced } from 'office-ui-fabric-react/lib/Announced';
import { IContextualMenuProps, IContextualMenuItem, DirectionalHint, ContextualMenu } from 'office-ui-fabric-react/lib/ContextualMenu';

import {
  CheckboxVisibility,
  ColumnActionsMode,
  ConstrainMode,
  DetailsList,
  DetailsListLayoutMode,
  IColumn,
  IGroup,
  Selection,
  SelectionMode,
  buildColumns,
  IDetailsColumnProps
} from 'office-ui-fabric-react/lib/DetailsList';

import { memoizeFunction } from 'office-ui-fabric-react/lib/Utilities';
import { getTheme, mergeStyleSets } from 'office-ui-fabric-react/lib/Styling';

const theme = getTheme();
const headerDividerClass = 'DetailsListAdvancedExample-divider';
const classNames = mergeStyleSets({
  headerDivider: {
    display: 'inline-block',
    height: '100%'
  },
  headerDividerBar: [
    {
      display: 'none',
      background: theme.palette.themePrimary,
      position: 'absolute',
      top: 16,
      bottom: 0,
      width: '1px',
      zIndex: 5
    },
    headerDividerClass
  ],
  linkField: {
    display: 'block',
    overflow: 'hidden',
    textOverflow: 'ellipsis',
    maxWidth: '100%'
  },
  root: {
    selectors: {
      [`.${headerDividerClass}:hover + .${headerDividerClass}`]: {
        display: 'inline'
      }
    }
  }
});

const DEFAULT_ITEM_LIMIT = 5;
const PAGING_SIZE = 10;
const PAGING_DELAY = 2000;
const ITEMS_COUNT = 5000;


const wrapperClassName = mergeStyles({
  selectors: {
    '& > *': { marginBottom: '20px' },
    '& .ms-ComboBox': { maxWidth: '300px' }
  }
});


// CUSTOM STYLES
import './home.scss';


export default class Home extends React.Component<{}, any> {
private _isFetchingItems: boolean;
private _selection: Selection;

constructor(props: {}) {
    super(props);

  }

  public render(): JSX.Element {


    return (
      <Fabric className={wrapperClassName}>
          <div className={classNames.root}>
            <div className="welcome">
                <h2>Welcome!</h2>
                <p>Employee Vehicle Long Term Parking Request Manager</p>
                <p>SafePark is designed to allow Employees who will be going on company related business travel a to notify Physical Security about their vehicle being parked on company property for an extended period of time.
                </p>
                <p>This helps to alert Physical Security to pay special attention to the employee' vehicle and also serves the purpose of requesting that the employee' vehicle not be subject to towing.</p>
            </div>
        </div>

      </Fabric>

    );
  }


}

//TO
import * as React from "react";
import { Fabric, mergeStyles } from 'office-ui-fabric-react/lib/index';
import { getTheme, mergeStyleSets } from 'office-ui-fabric-react/lib/Styling';

// CUSTOM STYLES
import './home.scss';


const theme = getTheme();
const headerDividerClass = 'DetailsListAdvancedExample-divider';
const classNames = mergeStyleSets({
  headerDivider: {
    display: 'inline-block',
    height: '100%'
  },
  headerDividerBar: [
    {
      display: 'none',
      background: theme.palette.themePrimary,
      position: 'absolute',
      top: 16,
      bottom: 0,
      width: '1px',
      zIndex: 5
    },
    headerDividerClass
  ],
  linkField: {
    display: 'block',
    overflow: 'hidden',
    textOverflow: 'ellipsis',
    maxWidth: '100%'
  },
  root: {
    selectors: {
      [`.${headerDividerClass}:hover + .${headerDividerClass}`]: {
        display: 'inline'
      }
    }
  }
});

const DEFAULT_ITEM_LIMIT = 5;
const PAGING_SIZE = 10;
const PAGING_DELAY = 2000;
const ITEMS_COUNT = 5000;


const wrapperClassName = mergeStyles({
  selectors: {
    '& > *': { marginBottom: '20px' },
    '& .ms-ComboBox': { maxWidth: '300px' }
  }
});

function home() {
    return (
    <div>
      <Fabric className={wrapperClassName}>
          <div className={classNames.root}>
            <div className="welcome">
                <h2>Welcome!</h2>
                <p>Employee Vehicle Long Term Parking Request Manager</p>
                <p>SafePark is designed to allow Employees who will be going on company related business travel a to notify Physical Security about their vehicle being parked on company property for an extended period of time.
                </p>
                <p>This helps to alert Physical Security to pay special attention to the employee' vehicle and also serves the purpose of requesting that the employee' vehicle not be subject to towing.</p>
            </div>
        </div>

      </Fabric>
      </div>

    );
 }
export default home;


Now we should have something similar to the following screen shot, don’t worry we will make it prettier, more pretty, ok somewhat more bearable to look at as we go forward.

Welcome - Ui-Fabric SharePoint React App
1st Version – Welcome – Ui-Fabric SharePoint React App

New version

2nd version - Welcome - Ui-Fabric SharePoint React App
2nd version – Welcome – Ui-Fabric SharePoint React App

Add Navigation

Let’s add a side panel for our navigation. Something similar to the following mockup.

Mockup of SafePark Layout

For the side panel, let’s add a Ui-Fabric Panel to App.tsx.

But first we need to add React-Hooks to the project.

On the cmd line type the following and then click Enter.

npm install @uifabric/react-hooks

Next create a new folder in src/components called leftNav

In the leftNav folder, create a new file called leftNav.tsx and then paste the following:

import * as React from 'react';
import { DefaultButton } from 'office-ui-fabric-react/lib/Button';
import { ActionButton, IIconProps } from 'office-ui-fabric-react';
import { Panel, PanelType } from 'office-ui-fabric-react/lib/Panel';
import { Nav, INavLink } from 'office-ui-fabric-react/lib/Nav';
import { useConstCallback } from '@uifabric/react-hooks';


const LeftNav: React.FunctionComponent<{ panelType: PanelType }> = props => {
  const { panelType } = props;
  const [isOpen, setIsOpen] = React.useState(false);

  const openPanel = useConstCallback(() => setIsOpen(true));
  const dismissPanel = useConstCallback(() => setIsOpen(false));
  const parkingIcon: IIconProps = { iconName: 'ParkingLocation' };
  return (

    <div>

        <ActionButton iconProps={parkingIcon} onClick={openPanel} allowDisabledFocus disabled={false} checked={false}>
            // Main Menu
              Actions
        </ActionButton>

      <Panel
        isOpen={isOpen}
        onDismiss={dismissPanel}
        type={panelType}
        customWidth={panelType === PanelType.custom || panelType === PanelType.customNear ? '888px' : undefined}
        closeButtonAriaLabel="Close"
        headerText="Main"
      >
          <Nav
      onLinkClick={_onLinkClick}
      selectedKey="key3"
      selectedAriaLabel="Selected"
      ariaLabel="Nav basic example"
      styles={{
        root: {
          width: 208,
          height: 350,
          boxSizing: 'border-box',
          border: '1px solid #eee',
          overflowY: 'auto'
        }
      }}
      groups={[
        {
          links: [
            {
              name: 'Requests',
              url: '/',
              expandAriaLabel: 'Parking Requests',
              collapseAriaLabel: 'Collapse Parking Requests',
              links: [
                {
                  name: 'New',
                  url: '#/requestForm',
                  key: 'key1',
                  icon: 'circleAddition'
                },
                {
                  name: 'Dashboard',
                  url: '#/myDashboard',
                  disabled: false,
                  key: 'key2',
                  icon: 'ViewDashboard'
                }
              ],
              isExpanded: true
            },
            {
              name: 'Security Contacts',
              url: '#/contacts',
              key: 'key4',
              icon: 'SecurityGroup'
            },
            {
              name: 'Policies',
              url: '#/policies',
              key: 'key3',
              icon: 'DocumentManagement'
            }
          ]
        }
      ]}
    />
      </Panel>
    </div>
  );
};


export const LeftNavPanel: React.FunctionComponent = () => {

  return (
    <div>
      <LeftNav panelType={PanelType.smallFixedNear} />
    </div>
  );
};

function _onLinkClick(ev: React.MouseEvent<HTMLElement>, item?: INavLink) {
  if (item && item.name === 'News') {
    alert('News link clicked');
  }
}

Now we have a Main Menu button

Welcome - Ui-Fabric SharePoint React App
Main Menu Button – 1st Version

New Version

2nd version - Welcome - Ui-Fabric SharePoint React App
2nd version – Welcome – Ui-Fabric SharePoint React App

If you click the Main Menu Button you should now see a collapsible panel that expands from the left side of the screen. Like the following screen shot.

Left Nav Panel
Left Nav Panel

Let’s change the plain Main Menu Button to an Action Button.

In leftNav.tsx add the following:

import { ActionButton, IIconProps } from 'office-ui-fabric-react';

Just below the dismissPanel const add,

const parkingIcon: IIconProps = { iconName: 'ParkingLocation' };

Replace the DefaultButton line with the following,

<ActionButton iconProps={parkingIcon} onClick={openPanel} allowDisabledFocus disabled={false} checked={false}>
            Main Menu
</ActionButton>

Now the homepage should look similar to the following.

WebDev By The Bay - 2nd version - Welcome - Ui-Fabric SharePoint React App
WebDev By The Bay – 2nd version – Welcome – Ui-Fabric SharePoint React App

Pages or views

Pages

  • requestForm
  • myDashboard
  • contacts
  • policies

Let’s start with the policies page.

Policies page

Install the uifabric example data.

On the command line type and execute,

npm install @uifabric/example-data

Create a new folder in the component/views folder called policies.

In the policies folder create a new file called policies.tsx

For now let’s serve the data from the UIFabric example data. Later we will surface the policies from a SharePoint Document Library using the Microsoft PNPJS libraries.

For the content of the policies page I’m just going to copy the code from the ListBasicExample Code which can be found here: https://developer.microsoft.com/en-us/fabric#/controls/web/list

Copy and Paste the ListBasicExample code into policies.tsx.

Let’s change the class name from ListBasicExample to Policies and change

createListItems(5000);

to

createListItems(10);

Before the line,

 <TextField label={'Filter by name' + resultCountText} onChange={this._onFilterChanged} />

type

<h3>Parking Policies</h3>  

policies.tsx should now be similar to the following

import * as React from 'react';
import { getRTL } from 'office-ui-fabric-react/lib/Utilities';
import { FocusZone, FocusZoneDirection } from 'office-ui-fabric-react/lib/FocusZone';
import { TextField } from 'office-ui-fabric-react/lib/TextField';
import { Image, ImageFit } from 'office-ui-fabric-react/lib/Image';
import { Icon } from 'office-ui-fabric-react/lib/Icon';
import { List } from 'office-ui-fabric-react/lib/List';
import { ITheme, mergeStyleSets, getTheme, getFocusStyle } from 'office-ui-fabric-react/lib/Styling';
import { createListItems, IExampleItem } from '@uifabric/example-data';

//ADD
import './policies.scss';

export interface IListBasicExampleProps {
  items?: IExampleItem[];
}

export interface IListBasicExampleState {
  filterText?: string;
  items?: IExampleItem[];
}

interface IListBasicExampleClassObject {
  itemCell: string;
  itemImage: string;
  itemContent: string;
  itemName: string;
  itemIndex: string;
  chevron: string;
}

const theme: ITheme = getTheme();
const { palette, semanticColors, fonts } = theme;

const classNames: IListBasicExampleClassObject = mergeStyleSets({
  itemCell: [
    getFocusStyle(theme, { inset: -1 }),
    {
      minHeight: 54,
      padding: 10,
      boxSizing: 'border-box',
      borderBottom: `1px solid ${semanticColors.bodyDivider}`,
      display: 'flex',
      selectors: {
        '&:hover': { background: palette.neutralLight }
      }
    }
  ],
  itemImage: {
    flexShrink: 0
  },
  itemContent: {
    marginLeft: 10,
    overflow: 'hidden',
    flexGrow: 1
  },
  itemName: [
    fonts.xLarge,
    {
      whiteSpace: 'nowrap',
      overflow: 'hidden',
      textOverflow: 'ellipsis'
    }
  ],
  itemIndex: {
    fontSize: fonts.small.fontSize,
    color: palette.neutralTertiary,
    marginBottom: 10
  },
  chevron: {
    alignSelf: 'center',
    marginLeft: 10,
    color: palette.neutralTertiary,
    fontSize: fonts.large.fontSize,
    flexShrink: 0
  }
});

export class Policies extends React.Component<IListBasicExampleProps, IListBasicExampleState> {
private _originalItems: IExampleItem[];

  constructor(props: IListBasicExampleProps) {
    super(props);

    this._originalItems = props.items || createListItems(10);
    this.state = {
      filterText: '',
      items: this._originalItems
    };
  }

  public render(): JSX.Element {
    const { items = [] } = this.state;
    const resultCountText = items.length === this._originalItems.length ? '' : ` (${items.length} of ${this._originalItems.length} shown)`;

    return (
// CHANGE
      <FocusZone direction={FocusZoneDirection.vertical}>
         <h3>Parking Policies</h3>
        <TextField label={'Filter by name' + resultCountText} onChange={this._onFilterChanged} />
        <List items={items} onRenderCell={this._onRenderCell} />
      </FocusZone>

// TO
 <div className="policiesDetails">
            <FocusZone direction={FocusZoneDirection.vertical}>
                <h3>Parking Policies</h3>
                <TextField label={'Filter by name' + resultCountText}    onChange={this._onFilterChanged} />
              <List items={items} onRenderCell={this._onRenderCell} />
            </FocusZone>
      </div>
    );
  }

  private _onFilterChanged = (_: any, text: string): void => {
    this.setState({
      filterText: text,
      items: text ? this._originalItems.filter(item => item.name.toLowerCase().indexOf(text.toLowerCase()) >= 0) : this._originalItems
    });
  };

  private _onRenderCell(item: IExampleItem, index: number | undefined): JSX.Element {
    return (
      <div className={classNames.itemCell} data-is-focusable={true}>
        <Image className={classNames.itemImage} src={item.thumbnail} width={50} height={50} imageFit={ImageFit.cover} />
        <div className={classNames.itemContent}>
          <div className={classNames.itemName}>{item.name}</div>
          <div className={classNames.itemIndex}>{`Item ${index}`}</div>
          <div>{item.description}</div>
        </div>
        <Icon className={classNames.chevron} iconName={getRTL() ? 'ChevronLeft' : 'ChevronRight'} />
      </div>
    );
  }
}


Launch the app by typing npm start and click Enter on the command line. Navigate to the Policies page.

Your screen should be similar to the following:

WebDev By The Bay - SafePark React App in SharePoint. Policies Page
WebDev By The Bay – SafePark React App in SharePoint. Policies Page

Add a new file to the policies folder called policies.scss and add the following,

.policiesDetails {
    position: relative;
    left: 23%;
    width: 600px;
    // background-color: rgba(255, 255, 255, 1);
    padding: 20px;
    margin: 30px;
    // box-shadow: 10px 5px 5px grey;
}

Now the Policies page should look similar to the following,

Policies Page version 2
Policies Page version 2

Contacts page

We will eventually surface contacts from Azure Active Directory using the Microsoft Graph. For now let’s setup the basic structure and surface the contact images from the UiFabric example data.

For the contacts page I’m just going to copy the code from the Persona/Rendering custom coin. Code can be found here: https://developer.microsoft.com/en-us/fabric#/controls/web/persona

Create a contacts folder in src/components/views

In the contacts folder create a file called contacts.tsx

Copy and paste the Persona/Rendering custom coin code into contacts.tsx and change the const PersonaCustomCoinRenderExample to Contacts.

Change the const examplePersona to contactPersona. Then change the Persona {…examplePersona} to {…contactPersona}

contacts.tsx should be similar to the following:

import * as React from 'react';
import { IPersonaProps, IPersonaSharedProps, Persona, PersonaSize, PersonaPresence } from 'office-ui-fabric-react/lib/Persona';
import { Stack } from 'office-ui-fabric-react/lib/Stack';
import { TestImages } from '@uifabric/example-data';
import { mergeStyles } from 'office-ui-fabric-react/lib/Styling';

//ADD
import './contacts.scss';

const customCoinClass = mergeStyles({
  borderRadius: 20,
  display: 'block'
});

const examplePersona: IPersonaSharedProps = {
  imageInitials: 'TR',
  text: 'Ted Randall',
  secondaryText: 'Project Manager',
  optionalText: 'Available at 4:00pm'
};

//CHANGE
export const Contacts: React.FunctionComponent = () => {
  return (
    <Stack tokens={{ childrenGap: 10 }}>
      <div>Custom render function in place of persona coin's image</div>
      <Persona
        {...examplePersona}
        size={PersonaSize.size72}
        presence={PersonaPresence.online}
        onRenderCoin={_onRenderCoin}
        imageAlt="Custom Coin Image"
        imageUrl={TestImages.personaMale}
        coinSize={72}
      />
    </Stack>
  );
};

//TO
function contacts() {
  return (
      <div className="contactDetails">
        <Stack tokens={{ childrenGap: 10 }}>
        <div>Contacts</div>
        <Persona
            {...contactPersona}
            size={PersonaSize.size72}
            presence={PersonaPresence.online}
            onRenderCoin={_onRenderCoin}
            imageAlt="Custom Coin Image"
            imageUrl={TestImages.personaMale}
            coinSize={72}
        />
        </Stack>
    </div>
  );
}
export default contacts;

function _onRenderCoin(props: IPersonaProps): JSX.Element {
  const { coinSize, imageAlt, imageUrl } = props;
  return <img src={imageUrl} alt={imageAlt} width={coinSize} height={coinSize} className={customCoinClass} />;
}

Import the Contacts view into App.tsx and add the /contacts route.

<Route path="/contacts" component={Contacts} />

Launch the app if it is not already launched and navigate to the Contacts page. Close the side nav panel and you should see something similar to the following: (We will change the side panel later, so that we won’t need to close it to see the contents of the page.)

Contacts Page - version 1
Contacts Page – version 1

Add a new file to the contacts folder called contacts.scss and add the following.

.contactDetails {
    position: relative;
    left: 23%;
    width: 600px;
    // background-color: rgba(255, 255, 255, 1);
    padding: 20px;
    margin: 30px;
    // box-shadow: 10px 5px 5px grey;
}

Now the contacts page should be similar to the following.

Contacts - version 2
Contacts – version 2

My Dashboard Page

For the myDashboard page, we will copy the code from several UI-Fabric examples.

https://developer.microsoft.com/en-us/fabric#/controls/web

Mockup of what we will create:

My Dashboard
My Dashboard
My Dashboard - Date Picker - Change Return Date
My Dashboard – Date Picker

Create a new folder in the component/views folder called myDashboard

I’ll be keeping myDashboard as a class file instead porting it to functional component. You can mix both styles. For future projects I prefer functional components.

In the myDashboard folder create two new files, one named myDashboard.tsx and the other is named myDashboard.scss

Most of the code is a copy of home.tsx but to save some time:

import * as React from 'react';
import { DetailsList, DetailsListLayoutMode, Selection, IColumn } from 'office-ui-fabric-react/lib/DetailsList';
import { getTheme, mergeStyleSets } from 'office-ui-fabric-react/lib/Styling';
import {
  Fabric,
  mergeStyles
} from 'office-ui-fabric-react/lib/index';

import { ActionButton, IIconProps } from 'office-ui-fabric-react';

const theme = getTheme();
const headerDividerClass = 'DetailsListAdvancedExample-divider';

export interface IDetailsListBasicExampleItem {
  key: number;
  name: string;
  value: number;
}

export interface IDetailsListBasicExampleState {
  items: IDetailsListBasicExampleItem[];
  selectionDetails: {};
}


// CUSTOM STYLES
import './myDashboard.scss';

const classNames = mergeStyleSets({
  headerDivider: {
    display: 'inline-block',
    height: '100%'
  },
  headerDividerBar: [
    {
      display: 'none',
      background: theme.palette.themePrimary,
      position: 'absolute',
      top: 16,
      bottom: 0,
      width: '1px',
      zIndex: 5
    },
    headerDividerClass
  ],
  linkField: {
    display: 'block',
    overflow: 'hidden',
    textOverflow: 'ellipsis',
    maxWidth: '100%'
  },
  root: {
    selectors: {
      [`.${headerDividerClass}:hover + .${headerDividerClass}`]: {
        display: 'inline'
      }
    }
  }
});
const wrapperClassName = mergeStyles({
  selectors: {
    '& > *': { marginBottom: '20px' },
    '& .ms-ComboBox': { maxWidth: '300px' }
  }
});

export default class MyDashboard extends React.Component<{}, IDetailsListBasicExampleState> {
private _selection: Selection;
  private _allItems: IDetailsListBasicExampleItem[];
  private _columns: IColumn[];

  constructor(props: {}) {
    super(props);

    this._selection = new Selection({
      onSelectionChanged: () => this.setState({ selectionDetails: this._getSelectionDetails() })
    });

    // Populate with items for demos.
    this._allItems = [];
    for (let i = 0; i < 10; i++) {
      this._allItems.push({
        key: i,
        name: 'Item ' + i,
        value: i
      });
    }

    this._columns = [
      { key: 'column1', name: 'Name', fieldName: 'name', minWidth: 100, maxWidth: 200, isResizable: true },
      { key: 'column2', name: 'Value', fieldName: 'value', minWidth: 100, maxWidth: 200, isResizable: true }
    ];

    this.state = {
      items: this._allItems,
      selectionDetails: this._getSelectionDetails()
    };
  }


  public render(): JSX.Element {
    const { items, selectionDetails } = this.state;
    const postUpdateIcon: IIconProps = { iconName: 'PostUpdate' };
    return (
      <Fabric className={wrapperClassName}>
          <div className={classNames.root}>
            <div className="details">
                <h2>My Dashboard!</h2>
                <p>Request Details</p>
                <p><strong><label>Start Date:</label></strong> 11/01/2019 </p>
                <p><strong><label>Return Date:</label></strong> 12/01/2019 <ActionButton iconProps={postUpdateIcon}  allowDisabledFocus disabled={false} checked={false}>
            CHANGE RETURN DATE
        </ActionButton></p>
                <p><strong><label>Comments:</label></strong> Lorem ipsum dolor sit amet, consectetur adipisicing elit. Repudiandae vero eos consectetur sequi laborum beatae aut, ipsum sed excepturi at nihil hic harum exercitationem expedita quod. Saepe maiores quas quidem.</p>
            </div>

            <div className="securityNotificationsWrapper">
                <h3>Security Notifications</h3>
                <DetailsList
                    items={items}
                    columns={this._columns}
                    setKey="set"
                    layoutMode={DetailsListLayoutMode.justified}
                    selection={this._selection}
                    selectionPreservedOnEmptyClick={true}
                    ariaLabelForSelectionColumn="Toggle selection"
                    ariaLabelForSelectAllCheckbox="Toggle selection for all items"
                    checkButtonAriaLabel="Row checkbox"
                    onItemInvoked={this._onItemInvoked}
                />
            </div>
        </div>

      </Fabric>

    );
  }
 private _getSelectionDetails(): string {
    const selectionCount = this._selection.getSelectedCount();

    switch (selectionCount) {
      case 0:
        return 'No items selected';
      case 1:
        return '1 item selected: ' + (this._selection.getSelection()[0] as IDetailsListBasicExampleItem).name;
      default:
        return `${selectionCount} items selected`;
    }
  }

  private _onFilter = (ev: React.FormEvent<HTMLInputElement | HTMLTextAreaElement>, text: string): void => {
    this.setState({
      items: text ? this._allItems.filter(i => i.name.toLowerCase().indexOf(text) > -1) : this._allItems
    });
  };

  private _onItemInvoked = (item: IDetailsListBasicExampleItem): void => {
    alert(`Item invoked: ${item.name}`);
  };

}

The DetailsList example code I used as a reference can be found here:https://developer.microsoft.com/en-us/fabric#/controls/web/detailslist/basic

In App.tsx add the following Route and Import MyDashboard.

<Route path="/myDashboard" component={MyDashboard} />

Change myDasboard.scss to

.details {
    position: relative;
    left: 23%;
    width: 400px;
    background-color: beige;
    padding: 20px;
    margin: 30px;
    box-shadow: 10px 5px 5px grey;
}

.securityNotificationsWrapper {
    position: absolute;
    top: 20px;
    right: 10px;
    width: 400px;
    height: auto;
    background-color: rgba(49, 55, 49, .43);
    padding: 20px;
    margin: 30px;
    box-shadow: 10px 5px 5px grey;
}

Navigate to the myDashboard page, it should now look something like:

My Dashboard
My Dashboard

Create A Change Return Date Form

The DatePicker example code I used as a reference can be found here:

https://developer.microsoft.com/en-us/fabric#/controls/web/datepicker

In the Dashboard details, did you notice I snuck in an Icon Button with the text Change Return Date. This link will show a Panel that appears from the right side of the screen, this panel contains a Textfield (Justification), a DatePicker (Return Date) and a Submit button.

In the myDashboard folder, create two new files, changeReturnDateForm.tsx and returnDateFormPanel.tsx

//changeReturnDateForm.tsx

import { DatePicker, DayOfWeek, IDatePickerStrings, mergeStyleSets, PrimaryButton, TextField } from 'office-ui-fabric-react';
import * as React from 'react';

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 class ChangeReturnDateForm extends React.Component<{}, IDatePickerState> {
  public constructor(props: {}) {
    super(props);

    this.state = {
      firstDayOfWeek: DayOfWeek.Sunday
    };
  }

  public render() {
    const { firstDayOfWeek } = this.state;

    return (
      <div className="docs-DatePickerExample">
          <TextField label="Justification" multiline rows={3} />
          <br />
        <DatePicker
          className={controlClass.control}
          firstDayOfWeek={firstDayOfWeek}
          strings={DayPickerStrings}
          placeholder="Select a date..."
          ariaLabel="Select a date"
        />
// We will change the onClick handler in the PNPJS part of this series 
        <PrimaryButton text="Primary" onClick={_alertClicked} allowDisabledFocus />
      </div>
    );
  };

}


function _alertClicked(): void {
  alert('Clicked');
}

// returnDateFormPanel.tsx

import { useConstCallback } from '@uifabric/react-hooks';
import { IIconProps } from 'office-ui-fabric-react';
import { ActionButton } from 'office-ui-fabric-react/lib/Button';
import { Panel } from 'office-ui-fabric-react/lib/Panel';
import * as React from 'react';
import { ChangeReturnDateForm } from './changeReturnDateForm';

const changeReturnDateIcon: IIconProps = { iconName: 'CalendarSettings' };

export const ReturnDateFormPanel: React.FunctionComponent = () => {
  const [isOpen, setIsOpen] = React.useState(false);

  const openPanel = useConstCallback(() => setIsOpen(true));
  const dismissPanel = useConstCallback(() => setIsOpen(false));

  return (
    <div>
      <ActionButton iconProps={changeReturnDateIcon} allowDisabledFocus onClick={openPanel}>
      Change Return Date
    </ActionButton>
      <Panel
        headerText="Return Date"
        isOpen={isOpen}
        onDismiss={dismissPanel}
        // You MUST provide this prop! Otherwise screen readers will just say "button" with no label.
        closeButtonAriaLabel="Close"
      >
        <span>
            <ChangeReturnDateForm/>
        </span>
      </Panel>
    </div>
  );
};

Now you should see something similar to the following.

Change Return Date Button
Change Return Date Button

Click the Change Return Date button and you should see the right side panel appear.

Right Side Panel with DatePicker control
Right Side Panel with DatePicker control

Next – Part 4: Surface SharePoint list data with pnp/sp

References