Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / productivity / SharePoint

Sharepoint Framework (SPFx) Large List Webpart using React & Office UI Components

5.00/5 (1 vote)
20 Mar 2018CPOL7 min read 46.3K   1.2K  
SharePoint framework webpart to retrieve SharePoint list items using React & Rest API and display the results using details DetailsList Office UI fabric component

Introduction

This webpart will retrieve SharePoint list items beyond the threshold limit and will display the results using DetailsList Office UI fabric component. Due to SharePoint list view threshold limit, users without admin rights will not be able to browse through the SharePoint list items. Adding and configuring this webpart will let users with at least read permissions to the site to view all items from the list .

Pre-requisites

  1. Create project ViewAllItemsWebPart by running Yeoman SharePoint Generator
  2. Install react Paging component
  3. Install jQuery

Using the Code

Listed below are the components that were created as part of this webpart:

  1. Custom property pane - A custom property pane control to show a multiselect dropdown to populate the columns when the list property is selected. Please refer to this link to create Custom property pane control.
  2. Config - Config component will be rendered if the webpart properties are not configured.
  3. Paging - This is react paging component. This is used to add pagination to the list view.
  4. ViewAllItems - The component that renders the DetailsList Office UI fabric component and binds the list items to it.
  5. services - This contains the methods to get the Lists and Columns from SharePoint using Rest API.

Let's start off with modifying the scaffolding that was generated by the Yeoman Generator.

WebPart.manifest.json

Edit the webpart manifest json file properties section to include three properties List name (listName), Column names (selectedIds), Page size (pageSize) and set the default values as shown.

JavaScript
"properties": {
 "listName": "",
 "selectedIds":[],
 "pageSize": 100
}

Define Webpart Property Constants and Property Types

Under loc folder, update en-us.js and mystrings.js to include the below script. These is to define the property constants and property types that are required in webpart property pane configuration.

en-us.js

JavaScript
define([], function() {
  return {
    "PropertyPaneDescription": "Description",
    "BasicGroupName": "Group Name",
    "ListNameFieldLabel": "Select List",
    "ColumnFieldLabel": "Select Columns",
    "PageSizeFieldLabel": "Select Page Size"
  }
});

mystrings.d.ts

JavaScript
declare interface IViewAllItemsWebPartStrings {
  PropertyPaneDescription: string;
  BasicGroupName: string;
  ListNameFieldLabel: string;
  ColumnFieldLabel: string;
  PageSizeFieldLabel: string;
}

declare module 'ViewAllItemsWebPartStrings' {
  const strings: IViewAllItemsWebPartStrings;
  export = strings;
}

Webpart.ts

Add below imports to webpart.ts file:

JavaScript
import * as React from 'react';
import * as ReactDom from 'react-dom';
import { Version } from '@microsoft/sp-core-library';
import {
  BaseClientSideWebPart,
  IPropertyPaneConfiguration,
  PropertyPaneDropdown,
  IPropertyPaneDropdownOption
} from '@microsoft/sp-webpart-base';

import * as strings from 'ViewAllItemsWebPartStrings';
import ViewAllItems from './components/ViewAllItems';
import { IViewAllItemsProps } from './components/IViewAllItemsProps';
import { IList } from './services/IList';
import { IListColumn } from './services/IListColumn';
import { ListService } from './services/ListService';
import { MultiSelectBuilder, IItemProp, PropertyPaneMultiSelect } 
         from './CustomPropertyPane/PropertyPaneMultiSelect';
import { IDropdownOption } from 'office-ui-fabric-react/lib/components/Dropdown';

Remove the existing lines of code in getPropertyPaneConfiguration method and add the below lines of code:

JavaScript
protected getPropertyPaneConfiguration(): IPropertyPaneConfiguration {
    return {
      pages: [
        {
          header: {
            description: strings.PropertyPaneDescription
          },
          groups: [
            {
              groupName: strings.BasicGroupName,
              groupFields: [
                PropertyPaneDropdown('listName', {
                  label: strings.ListNameFieldLabel,
                  options: this.lists,
                  disabled: this.listsDropdownDisabled,
                
                }),                
                PropertyPaneMultiSelect("selectedIds", {
                  label: "Select Columns",
                  selectedItemIds: this.properties.selectedIds, //Ids of Selected Items
                  onload: () => this.loadColumns(), //On load function to items 
                                                    //for drop down
                  onPropChange: this.onPropertyPaneFieldChanged,// On Property Change 
                                                                //function
                  properties: this.properties, //Web Part properties
                  key: "targetkey",  //unique key
                  disabled: !this.properties.listName
                }),
                PropertyPaneDropdown('pageSize',{
                  label: strings.PageSizeFieldLabel,
                  options:[
                    {key: '10', text: '10'},
                    {key: '25', text: '25'},
                    {key: '50', text: '50'},
                    {key: '100', text: '100'}
                  ]
                })
              ]
            }
          ]
        }
      ]
    };
  }

PropertyPaneMultiselect is the custom property pane component. Please follow the link that is provided above to add it to this webpart project.

Add methods onPropertyPaneConfigurationStart and onPropertyPaneFieldChanged to the webpart class to create cascading dropdown and load the property pane controls with data.

JavaScript
protected onPropertyPaneConfigurationStart(): void {
    this.listsDropdownDisabled = !this.lists;

    if (this.lists) {
      return;
    }

    this.context.statusRenderer.displayLoadingIndicator(this.domElement, 'lists');

    this.loadLists()
    .then((listOptions: IPropertyPaneDropdownOption[]): void => {
      this.lists = listOptions;
      this.listsDropdownDisabled = false;
      this.context.propertyPane.refresh();
      this.context.statusRenderer.clearLoadingIndicator(this.domElement);
      this.render();
    });
  }

protected onPropertyPaneFieldChanged(propertyPath: string, 
    oldValue: any, newValue: any): void {
    if (propertyPath === 'listName' && newValue) {
      // push new list value
      super.onPropertyPaneFieldChanged(propertyPath, oldValue, newValue);
      // get previously selected column
      const previousItem: string[] = this.properties.selectedIds;
      // reset selected item
      this.properties.selectedIds = [];
      // push new item value
      this.onPropertyPaneFieldChanged('selectedIds', previousItem, 
                                       this.properties.selectedIds);
      
      //this.columnsDropdown.render(this.domElement);

      this.render();
      // refresh the item selector control by repainting the property pane
      this.context.propertyPane.refresh();
      
    }
    else {
      super.onPropertyPaneFieldChanged(propertyPath, oldValue, newValue);
    }
  }

If you notice the above methods are calling methods, loadLists and loadColumns to get the data, add the below methods to the webpart class.

JavaScript
private loadLists(): Promise<IPropertyPaneDropdownOption[]> {
    const dataService = new ListService(this.context);

    return new Promise<IPropertyPaneDropdownOption[]>(resolve => {
      dataService.getLists()
      .then((response: IList[]) => {
          var options : IPropertyPaneDropdownOption[] = [];

          response.forEach((item: IList) => {
            options.push({"key": item.Title, "text": item.Title});
          });

          resolve(options);
      });
    });
  }

  private loadColumns(): Promise<IItemProp[]> {
    if (!this.properties.listName) {
      // resolve to empty options since no list has been selected
      return Promise.resolve();
    }
    const dataService = new ListService(this.context);

    return new Promise<IItemProp[]>(resolve => {
      dataService.getColumns(this.properties.listName)
      .then((response) => {
          var options : IItemProp[] = [];
          this.properties.selectedColumnsAndType = [];
          response.forEach((column: IListColumn) => {
            options.push({"key": column.StaticName, "text": column.Title});
            this.properties.selectedColumnsAndType.push
                ({"key": column.StaticName, "text": column.TypeDisplayName});
          });

          resolve(options);
      });
    });
  } 

In turn, these methods are referring to ListService class to get the data. Add new folder services to the solutions and add files ILists.ts, IListColumn.ts, ListService.ts.

ILists.ts

Interface IList.ts defines the list metadata that will be requested from the rest API request. This interface will get the list id and title.

JavaScript
export interface IList {
    Id: string;
    Title: string;
  }

IListColumns.ts

Interface IListColumns defines the column metadata that will be requested from the API request. This will get the column title, Internal name and column type.

JavaScript
export interface IListColumn {
    Title: string;
    StaticName: string;
    TypeDisplayName: string;
  }

ListService.ts

Add the below script to ListService.ts file. This file will the methods getLists and GetColumns methods that will get the Lists and Columns information from the SharePoint by passing the current context of the site appended with the SharePoint REST API as url parameter to the HTTP request.

JavaScript
export class ListService {

    constructor(private context: IWebPartContext) {
    }

    public getLists(): Promise<IList[]> {
        var httpClientOptions : ISPHttpClientOptions = {};
    
        httpClientOptions.headers = {
            'Accept': 'application/json;odata=nometadata',
            'odata-version': ''
        };
    
        return new Promise<IList[]>((resolve: (results: IList[]) => void, 
                                      reject: (error: any) => void): void => {
          this.context.spHttpClient.get(this.context.pageContext.web.serverRelativeUrl + 
                          `/_api/web/lists?$select=id,title&$filter=Hidden eq false`,
            SPHttpClient.configurations.v1,
            httpClientOptions
            )
            .then((response: SPHttpClientResponse): Promise<{ value: IList[] }> => {
              return response.json();
            })
            .then((lists: { value: IList[] }): void => {
              resolve(lists.value);
            }, (error: any): void => {
              reject(error);
            });
        });
    }

    public getColumns(listName: string): Promise<IListColumn[]> {
      var httpClientOptions : ISPHttpClientOptions = {};
 
      httpClientOptions.headers = {
          'Accept': 'application/json;odata=nometadata',
          'odata-version': ''
      };
 
      return new Promise<IListColumn[]>((resolve: (results: IListColumn[]) => void, 
                                 reject: (error: any) => void): void => {
        this.context.spHttpClient.get(this.context.pageContext.web.serverRelativeUrl + 
               `/_api/web/lists/GetByTitle('${listName}')/fields?
                 $filter=TypeDisplayName ne 
               'Attachments' and Hidden eq false and ReadOnlyField eq false`,
          SPHttpClient.configurations.v1,
          httpClientOptions
          )
          .then((response: SPHttpClientResponse): Promise<{ value: IListColumn[] }> => {
            return response.json();
          })
          .then((listColumns: { value: IListColumn[] }): void => {
            resolve(listColumns.value);
          }, (error: any): void => {
            reject(error);
          });
      });
    }      
}

Add below methods to the webpart class. These methods will be passed as properties to the ReactDOM element when the webpart render method is called.

  1. needsCofiguration - This determines whether the webpart properties that are required to render the view is configured.
  2. selectedColumns - This will return columns that are selected from the custom property pane control.
  3. configureWebPart - This method will open the webpart property pane when it is called.
JavaScript
private needsConfiguration(): boolean {
    return this.properties.listName === null ||
      this.properties.listName === undefined ||
      this.properties.listName.trim().length === 0 ||
      this.properties.selectedIds === null ||
      this.properties.selectedIds === undefined ||
      this.properties.selectedIds.length === 0;
  }

  private selectedColumns(): IItemProp[] {
    if(this.properties.selectedColumnsAndType === null ||
      this.properties.selectedColumnsAndType===undefined ||
      this.properties.selectedColumnsAndType.length === 0){
      return [];
      }
      else{
        return this.properties.selectedColumnsAndType.filter
                 (obj => this.properties.selectedIds.indexOf(obj.key) !== -1);
      }
  }
  private configureWebPart(): void {
    this.context.propertyPane.open();
  }

Remove the existing code in the webpart render method, copy and paste the below code:

JavaScript
public render(): void {
    const element: React.ReactElement<IViewAllItemsProps> = React.createElement(
      ViewAllItems,
      {
        spHttpClient: this.context.spHttpClient,
        siteUrl: this.context.pageContext.web.absoluteUrl,
        listName: this.properties.listName,
        needsConfiguration: this.needsConfiguration(),
        configureWebPart: this.configureWebPart,
        displayMode: this.displayMode,
        selectedColumns: this.selectedColumns(),
        pageSize: this.properties.pageSize
      }
    );

    ReactDom.render(element, this.domElement);
  }

The render method will load the JSX from ViewAllItems.tsx component. Add files ViewAllItems.tsx, IViewAllItemsProps.ts, ViewAllItems.module.scss to the solution.

IViewAllItemsProps.ts

This interface file defines all the properties that are required to load the component to ReactDOM.

JavaScript
export interface IViewAllItemsProps {
  spHttpClient: SPHttpClient;
  siteUrl: string;
  listName: string;
  selectedColumns: IItemProp[];
  needsConfiguration:boolean;
  configureWebPart: () => void;
  displayMode: DisplayMode;
  pageSize: number;
}

ViewAllItems.module.scss

These files contain all the styles that are used in ViewAllItems.tsx file.

CSS
@import '~@microsoft/sp-office-ui-fabric-core/dist/sass/SPFabricCore.scss';

.viewAllItems {
  .container {
    margin: 0px auto;
    box-shadow: 0 2px 4px 0 rgba(0, 0, 0, 0.2), 0 25px 50px 0 rgba(0, 0, 0, 0.1);
  }

  .row {
    @include ms-Grid-row;
    @include ms-fontColor-white;
    background-color: $ms-color-themeDark;
    padding: 20px;
  }

  .column {
    @include ms-Grid-col;
    @include ms-lg10;
    @include ms-xl8;
    @include ms-xlPush2;
    @include ms-lgPush1;
  }

  .title {
    @include ms-font-xl;
    @include ms-fontColor-white;
  }

  .subTitle {
    @include ms-font-l;
    @include ms-fontColor-white;
  }

  .description {
    @include ms-font-l;
    @include ms-fontColor-white;
  }
  .status{
    float:left
  }
  .button {
    // Our button
    text-decoration: none;
    height: 32px;

    // Primary Button
    min-width: 80px;
    background-color: $ms-color-themePrimary;
    border-color: $ms-color-themePrimary;
    color: $ms-color-white;

    // Basic Button
    outline: transparent;
    position: relative;
    font-family: "Segoe UI WestEuropean","Segoe UI",-apple-system,
                  BlinkMacSystemFont,Roboto,"Helvetica Neue",sans-serif;
    -webkit-font-smoothing: antialiased;
    font-size: $ms-font-size-m;
    font-weight: $ms-font-weight-regular;
    border-width: 0;
    text-align: center;
    cursor: pointer;
    display: inline-block;
    padding: 0 16px;

    .label {
      font-weight: $ms-font-weight-semibold;
      font-size: $ms-font-size-m;
      height: 32px;
      line-height: 32px;
      margin: 0 4px;
      vertical-align: top;
      display: inline-block;
    }
  }
}

ViewAllItems.tsx

ViewAllItems class inherits from React.Component which expects Props and State as arguments. We have defined the Props (IViewAllItemsProps.ts). Define state as shown below in the tsx file above the class.

JavaScript
export interface IViewAllItemsState {
  items?: any[];
  columns?:IColumn[];
  status?: string;
  currentPage?: number;
  itemCount?: number;
  pageSize?: number;
}

Inside the class, add the definition for Constructor. This sets the default props and state when the component is loaded initially.

JavaScript
private selectQuery: string[] = [];//global variable to set the select query params 
                                   //for the rest api request
private expandQuery: string[] = [];//global variable to expand the params 
                                   //for people picker 
                                   //and lookup column types
  constructor(props: IViewAllItemsProps){
    super(props);    
        
    this.state ={
      items:[],
      columns: this.buildColumns(this.props),
      currentPage:1,
      pageSize: this.props.pageSize
    };
    this._onPageUpdate = this._onPageUpdate.bind(this);
    this.getListItemsCount(`${this.props.siteUrl}/_api/web/lists/GetByTitle
                                   ('${props.listName}')/ItemCount`);
    const queryParam = this.buildQueryParams(props);
    this.readItems(`${this.props.siteUrl}/_api/web/lists/GetByTitle
                                   ('${props.listName}')/items${queryParam}`);
  }

Add the method componentWillReceiveProps to the ViewAllItems class. This is the react life cycle method that will render the JSX if the properties supplied for this component changes.

JavaScript
public componentWillReceiveProps(nextProps: IViewAllItemsProps): void{   
    
    this.setState({
      columns:this.buildColumns(nextProps),
      pageSize: nextProps.pageSize
    });
    this.getListItemsCount(`${this.props.siteUrl}/_api/web/lists/GetByTitle
                    ('${this.props.listName}')/ItemCount`);
      //const selectColumns = nextProps.selectedColumns === null || 
      //nextProps.selectedColumns===undefined || 
      //nextProps.selectedColumns.length === 0? "" : 
      //'?$select='+nextProps.selectedColumns.join();
    const queryParam = this.buildQueryParams(nextProps);
    this.readItems(`${this.props.siteUrl}/_api/web/lists/GetByTitle
                        ('${nextProps.listName}')/items${queryParam}`);
  }

Add render method to the class with the below code snippet. When the JSX element is loaded to DOM, it will check if the webpart needsConfiguration by calling this method which on the webpart class which returns a boolean. if the method returns true, it will load the config component JSX to DOM else it will load the current JSX to the DOM.

JavaScript
public render(): JSX.Element {

    const { needsConfiguration, configureWebPart} = this.props;
    let {items, columns, pageSize} = this.state;
    return (
      <div className={styles.viewAllItems}>
        <div>
        {needsConfiguration &&
            <Config configure={configureWebPart} {...this.props}/>
        }
        { needsConfiguration === false &&
          <div>
            <div>
              <div>
              <div className={styles.status}>{this.state.status}</div>
              <Paging
                    totalItems={ this.state.itemCount }
                    itemsCountPerPage={ this.state.pageSize }
                    onPageUpdate={ this._onPageUpdate }
                    currentPage={ this.state.currentPage }/>
              <div></div>
                <DetailsList
                  items = {items}
                  columns = {columns}
                  isHeaderVisible = {true}
                  layoutMode = {LayoutMode.justified}
                  constrainMode ={ConstrainMode.unconstrained}
                  checkboxVisibility={CheckboxVisibility.hidden}
                  onColumnHeaderClick={ this._onColumnClick }
                />
              </div>
            </div>
          </div>
        }
        </div>
      </div>
    );
  }

Add method readItems to ViewAllItems class. This method will return the list items from SharePoint.

JavaScript
private readItems(url: string) {
    this.setState({
      items: [],
      status: 'Loading all items...'
    });
    
    this.props.spHttpClient.get(url,
    SPHttpClient.configurations.v1,
    {
      headers: {
        'Accept': 'application/json;odata=nometadata',
        'odata-version': ''
      }
    }).then((response: SPHttpClientResponse): Promise<{value: any[]}> =>{
    return response.json();
    }).then((response: {value: any[]}): void => {     
      //this.props.Status(`${response.d.__next}`);
      //this.props.siteUrl = response['odata.nextLink'];
      this.setState({
        items: response.value,
        //columns: _buildColumns(response.value),
        status: `Showing items ${(this.state.currentPage - 1)*this.props.pageSize +1} - 
                ${(this.state.currentPage -1) * this.props.pageSize + 
                   response.value.length} 
                of ${this.state.itemCount}`
      });      
    }, (error: any): void => {
      this.setState({
        items: [],
        status: 'Loading all items failed with error: ' + error
      });
    });    
  }

Add method getListItemsCount which returns the list items count which is required by the Paging component to build pagination.

JavaScript
Private getListItemsCount(url: string) {
    this.props.spHttpClient.get(url,SPHttpClient.configurations.v1,
    {
      headers: {
        'Accept': 'application/json;odata=nometadata',
        'odata-version':''
      }
    }).then((response: SPHttpClientResponse): Promise<{value: number}> =>{
      return response.json();
    }).then((response: {value: number}): void => {
      this.setState({
        itemCount: response.value
      });
    });
  }

Add method _onPageUpdate method to ViewAllItems class. This method will be triggered when page numbers are clicked on the view. This method in turn calls the readItems method by passing the updated url as argument. Url is built with skiptoken, page ID and page size as parameters to get the right items from the list when the page number is clicked.

JavaScript
private _onPageUpdate(pageNumber: number) {
    this.setState({
      currentPage: pageNumber,
    });
    const p_ID = (pageNumber - 1)*this.props.pageSize;
    const selectColumns = '&$select='+this.selectQuery;
    const expandColumns = '&$expand='+this.expandQuery;
    const queryParam = `%24skiptoken=Paged%3dTRUE%26p_ID=${p_ID}&
                        $top=${this.props.pageSize}`;
    var url = `${this.props.siteUrl}/_api/web/lists/GetByTitle
               ('${this.props.listName}')/items?`+ 
               queryParam + selectColumns+expandColumns;
    this.readItems(url);    
  }

Add method _onColumnClick to the ViewAllItems class. This method is triggered when user clicks on the headers of the column in the list view. This method does the sorting for the results that are displayed.

JavaScript
private _onColumnClick(event: React.MouseEvent<HTMLElement>, column: IColumn) {
    let { items, columns } = this.state;
    let isSortedDescending = column.isSortedDescending;

    // If we've sorted this column, flip it.
    if (column.isSorted) {
      isSortedDescending = !isSortedDescending;
    }

    // Sort the items.
    items = items!.concat([]).sort((a, b) => {
      let firstValue = a[column.fieldName];
      let secondValue = b[column.fieldName];

      if (isSortedDescending) {
        return firstValue > secondValue ? -1 : 1;
      } else {
        return firstValue > secondValue ? 1 : -1;
      }
    });

    // Reset the items and columns to match the state.
    this.setState({
      items: items,
      columns: columns!.map(col => {
        col.isSorted = (col.key === column.key);

        if (col.isSorted) {
          col.isSortedDescending = isSortedDescending;
        }
        return col;
      })
    });
  }

Add methods buildQueryParams and buildColumns to the ViewAllItems Class.

  1. buildQueryParams - This method returns url query string that is required by the SharePoint Rest API to select and expand the columns that are selected on the webpart property pane.
  2. buildColumns - This method returns array of columns that is required by the DetailsList office UI fabric component to build the view.
JavaScript
private buildQueryParams(props: IViewAllItemsProps): string{
    this.selectQuery = [];
    this.expandQuery = [];
    props.selectedColumns.forEach(element => {      
      if(element.text === "Person or Group" || element.text === "Lookup"){
        this.selectQuery.push(element.key+"/Title");
        this.expandQuery.push(element.key);
      }
      else{
        this.selectQuery.push(element.key);
      }
    });
    const queryParam = `?%24skiptoken=Paged%3dTRUE%26p_ID=1&$top=${props.pageSize}`;
    const selectColumns = this.selectQuery === null || this.selectQuery===undefined || 
          this.selectQuery.length === 0? "" : '&$select='+this.selectQuery.join();
    const expandColumns = this.expandQuery === null || this.expandQuery===undefined || 
          this.expandQuery.length === 0? "" : '&$expand='+this.expandQuery.join();
    return queryParam+selectColumns+expandColumns;
  }
  private buildColumns(props: IViewAllItemsProps): IColumn[]{
    const columns: IColumn[]=[];
    props.selectedColumns.forEach(element => {      
      if(element.text === "Person or Group" || element.text === "Lookup"){        
        const column: IColumn ={
          key: element.key,
          name: element.key.indexOf("_x0020_") !== -1?
                element.key.replace("_x0020_"," "):element.key,
          fieldName: element.key,
          minWidth: 100,
          maxWidth: 350,
          isResizable: true,
          data: 'string',
          onRender: (item: any) => {
            return (
              <span>
                { item[element.key]["Title"] }
              </span>
            );
          }
        };
        columns.push(column);
      }
      else{        
        const column: IColumn ={
          key: element.key,
          name: element.key.indexOf("_x0020_") !== -1?
                element.key.replace("_x0020_"," "):element.key,
          fieldName: element.key,
          minWidth: 100,
          maxWidth: 350,
          isResizable: true,
          data: 'string',
          isMultiline: element.text === "Multiple lines of text" ? true:false
        };
        columns.push(column);
      }
    });
    return columns;
  }

Config Component

Add folder Config to the solution and then add files IConfigProp.ts, Config.module.scss and Config.tsx files to this folder.

IConfigProp.ts

Interface IConfigProp.ts defines the properties that are required to load Config.tsx component.

JavaScript
import { DisplayMode } from '@microsoft/sp-core-library';

export interface IConfigProps {
  displayMode: DisplayMode;
  configure: () => void;
}

Config.module.scss

This file contains the style that is required to style the Config.tsx Component.

CSS
.placeholder {
    display: -webkit-box;
    display: -ms-flexbox;
    display: flex;

    .placeholderContainer {
        -webkit-box-align: center;
        -ms-flex-align: center;
        -ms-grid-row-align: center;
        align-items: center;
        color: "[theme:neutralSecondary, default: #666666]";
        background-color: "[theme:neutralLighter, default: #f4f4f4]";
        width: 100%;
        padding: 80px 0;

        .placeholderHead {
            color: "[theme:neutralPrimary, default: #333333]";

            .placeholderHeadContainer {
                height: 100%;
                white-space: nowrap;
                text-align: center;
            }

            .placeholderIcon {
                display: inline-block;
                vertical-align: middle;
                white-space: normal;
            }

            .placeholderText {
                display: inline-block;
                vertical-align: middle;
                white-space: normal
            }
        }

        .placeholderDescription {
            width: 65%;
            vertical-align: middle;
            margin: 0 auto;
            text-align: center;

            .placeholderDescriptionText {
                color: "[theme:neutralSecondary, default: #666666]";
                font-size: 17px;
                display: inline-block;
                margin: 24px 0;
                font-weight: 100;
            }

            button {
              font-size: 14px;
              font-weight: 400;
              box-sizing: border-box;
              display: inline-block;
              text-align: center;
              cursor: pointer;
              vertical-align: top;
              min-width: 80px;
              height: 32px;
              background-color: "[theme:themePrimary, default: #0078d7]";
              color: #fff;
              user-select: none;
              outline: transparent;
              border-width: 1px;
              border-style: solid;
              border-color: transparent;
              border-image: initial;
              text-decoration: none;
            }
        }
    }
}

[dir=ltr] .placeholder,
[dir=rtl] .placeholder {

    .placeholderContainer {

        .placeholderHead {

            .placeholderText {
                padding-left: 20px;
            }
        }
    }
}

.placeholderOverlay {
    position: relative;
    height: 100%;
    z-index: 1;

    .placeholderSpinnerContainer {
        position: relative;
        width: 100%;
        margin: 164px 0
    }
}

Config.tsx

Replace class with the below code snippet in the Config.tsx. The _handleButtonClick event will trigger the configureWebPart method on the webpart.ts file which is passed to this component as a property.

JavaScript
import * as React from 'react';
import { Fabric } from 'office-ui-fabric-react';
import { DisplayMode } from '@microsoft/sp-core-library';
import { IConfigProps } from './IConfigProps';
import styles from './Config.module.scss';
import { PrimaryButton } from 'office-ui-fabric-react/lib/Button';

export class Config extends React.Component<IConfigProps, {}> {

    constructor(props: IConfigProps){
        super(props);
        this._handleBtnClick = this._handleBtnClick.bind(this);
    }
  public render(): JSX.Element {
    return (
      <Fabric>
        { this.props.displayMode === DisplayMode.Edit &&
            <div className={`${styles.placeholder}`}>
        <div className={styles.placeholderContainer}>
          <div className={styles.placeholderHead}>
            <div className={styles.placeholderHeadContainer}>
              <i className={`${styles.placeholderIcon} ms-fontSize-su 
                             ms-Icon ms-ICon--CheckboxComposite`}></i>
              <span className={`${styles.placeholderText} ms-fontWeight-light 
                                  ms-fontSize-xxl`}>Configure your web part</span>
            </div>
          </div>
          <div className={styles.placeholderDescription}>
            <span className={styles.placeholderDescriptionText}>
             Please configure the web part.</span>
          </div>
          <div className={styles.placeholderDescription}>
             <PrimaryButton
                text="Configure"
                ariaLabel="Configure"
                ariaDescription="Please configure the web part"
                onClick={this._handleBtnClick} />            
          </div>
        </div>
      </div>          
        }
        { this.props.displayMode === DisplayMode.Read &&
          <div className={`${styles.placeholder}`}>
        <div className={styles.placeholderContainer}>
          <div className={styles.placeholderHead}>
            <div className={styles.placeholderHeadContainer}>
              <i className={`${styles.placeholderIcon} ms-fontSize-su ms-Icon 
                             ms-ICon--CheckboxComposite`}></i>
              <span className={`${styles.placeholderText} 
                    ms-fontWeight-light ms-fontSize-xxl`}>
                    Configure your web part</span>
            </div>
          </div>
          <div className={styles.placeholderDescription}>
            <span className={styles.placeholderDescriptionText}>
             Please configure the web part.</span>
          </div>          
        </div>
      </div>
        }
      </Fabric>
    );
  }
  private _handleBtnClick(event?: React.MouseEvent<HTMLButtonElement>) {
    this.props.configure();
  }
}

Paging Component

Add folder Paging to the solution and then add files IPagingProp.ts, Paging.module.scss and Paging.tsx files to this folder.

IPagingProp.ts

Interface IPagingProp.ts defines the properties that are required to load Paging.tsx component.

JavaScript
export interface IPagingProps {
    totalItems: number;
    itemsCountPerPage: number;
    onPageUpdate: (pageNumber: number) => void;
    currentPage: number;
}

Paging.module.scss

This file contains the style that is required to style the Paging.tsx Component.

CSS
.paginationContainer {
    text-align: right;
    .searchWp__paginationContainer__pagination {
        display: inline-block;
        text-align: center;

        ul {
            display: inline-block;
            padding-left: 0;
            border-radius: 4px;

            li {
                display: inline;

                a {
                    float: left;
                    padding: 5px 10px;
                    text-decoration: none;
                    border-radius: 15px;
    
                    i {
                        font-size: 10px;
                    }        
                }
    
                a:visited {
                    color: inherit;
                }
    
                a.active {
                    background-color: #0078d7!important;
                    color: white!important;
                }
            }
        }
    }
}

Paging.tsx

Replace class with the below code snippet in the Paging.tsx. The _onPageUpdate event will trigger the _onPageUpdate method on the ViewAllItems.tsx file which is passed as a property to this component.

JavaScript
import * as React from "react";
import {IPagingProps} from "./IPagingProps";
import { PrimaryButton } from 'office-ui-fabric-react/lib/Button';
import Pagination from "react-js-pagination";
import styles from './Paging.module.scss';

export default class Paging extends React.Component<IPagingProps, null> {

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

        this._onPageUpdate = this._onPageUpdate.bind(this);
    }

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

        return(
            <div className={`${styles.paginationContainer}`}>
                <div className={`${styles.searchWp__paginationContainer__pagination}`}>
                <Pagination
                    activePage={this.props.currentPage}
                    firstPageText={<i className="ms-Icon 
                    ms-Icon--ChevronLeftEnd6" aria-hidden="true"></i>}
                    lastPageText={<i className="ms-Icon 
                    ms-Icon--ChevronRightEnd6" aria-hidden="true"></i>}
                    prevPageText={<i className="ms-Icon 
                    ms-Icon--ChevronLeft" aria-hidden="true"></i>}
                    nextPageText={<i className="ms-Icon 
                    ms-Icon--ChevronRight" aria-hidden="true"></i>}
                    activeLinkClass={ `${styles.active}` }
                    itemsCountPerPage={ this.props.itemsCountPerPage }
                    totalItemsCount={ this.props.totalItems }
                    pageRangeDisplayed={10}
                    onChange={this.props.onPageUpdate}
                />                      
                </div>
            </div>
        );
    }

    private _onPageUpdate(pageNumber: number): void {
        this.props.onPageUpdate(pageNumber);
    }

Package and Deploy

  1. Execute the following gulp task to bundle your solution. This executes a release build of your project by using a dynamic label as the host URL for your assets. This URL is automatically updated based on your tenant CDN settings:
    JavaScript
    gulp bundle --ship
  2. Execute the following task to package your solution. This creates an updated webpart.sppkg package on the sharepoint/solution folder.
    JavaScript
    gulp package-solution --ship
  3. Upload or drag and drop the newly created client-side solution package to the app catalog in your tenant.
  4. Based on your tenant settings, if you would not have CDN enabled in your tenant, and the includeClientSideAssets setting would be true in the package-solution.json, the loading URL for the assets would be dynamically updated and pointing directly to the ClientSideAssets folder located in the app catalog site collection.

Image 1

Image 2

Image 3

History

  • 20th March, 2018 - Article published

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)