PnP React Controls Part 4.2 – Extending ListView Control with Custom Paging

Introduction

Hi friends, this is part 4.2 which is the extended post of PnP React Controls Part 4.1 – Extending ListView Control. We will see how we can add custom paging to our listview component.

Note: Since this is the continuation of the earlier post, I will use the same web part. We are also going to use react-js-pagination – npm (npmjs.com) for our custom paging. There is also a paging control in PnP React Controls which we will see in the next post.

Focus on the Code

Let us see the difference in the code relevant to the paging.

Created a new folder named common under the src folder to store all the common components which will be used across the web parts.

Created a new file named Paging.tsx inside the common folder.

import { Icon } from 'office-ui-fabric-react';
import * as React from 'react';
import { useEffect, useState } from 'react';
import Pagination from 'react-js-pagination';
import styles from './Paging.module.scss';

export type PageUpdateCallback = (pageNumber: number) => void;

export interface IPagingProps {
    totalItems: number;
    itemsCountPerPage: number;
    onPageUpdate: PageUpdateCallback;
    currentPage: number;
}

export interface IPagingState {
    currentPage: number;
}

const Paging: React.FC<IPagingProps> = (props) => {
    const [currentPage, setcurrentPage] = useState<number>(props.currentPage);

    const _pageChange = (pageNumber: number): void => {
        setcurrentPage(pageNumber);
        props.onPageUpdate(pageNumber);
    };

    useEffect(() => {
        setcurrentPage(props.currentPage);
    }, [props.currentPage]);

    return (
        <div className={styles.paginationContainer}>
            <div className={styles.searchWp__paginationContainer__pagination}>
                <Pagination
                    activePage={currentPage}
                    firstPageText={<Icon iconName="ChevronLeftEnd6" aria-hidden='true'></Icon>}
                    lastPageText={<Icon iconName="ChevronRightEnd6" aria-hidden='true'></Icon>}
                    prevPageText={<Icon iconName="ChevronLeftSmall" aria-hidden='true'></Icon>}
                    nextPageText={<Icon iconName="ChevronRightSmall" aria-hidden='true'></Icon>}
                    activeLinkClass={styles.active}
                    itemsCountPerPage={props.itemsCountPerPage}
                    totalItemsCount={props.totalItems}
                    pageRangeDisplayed={5}
                    onChange={_pageChange}
                />
            </div>
        </div>
    );
};

export default Paging;
  • Added relevant imports to the custom styles and the react-js-pagination module.
  • Following are the properties that are created
    • totalItems – Total item count which is one of the parameter used to calculate the pages.
    • itemsCountPerPage – How many items to be displayed per page and together with totalItems will calculate the number of pages to be displayed.
    • onPageUpdate – Callback method to invoke when the the page numbers or arrows are clicked
    • currentPage – The current page number displayed to the users.
  • Created a state variable for the currentPage and its mapped to the Pagination control
  • Added the Pagination control with the properties mentioned below
    • activePage – Current page displayed
    • firstPageText – Icon or text to be displayed for the first page
    • lastPageText – Icon or text to be displayed for the last page.
    • prevPageText – Icon or text to be displayed for the prev page.
    • nextPageText – Icon or text to be displayed for the next page.
    • activeLinkClass – Custom class to support the default theme colors
    • itemsCountPerPage – Items to be displayed per page
    • totalItemsCount – Total item count
    • pageRangeDisplayed – Number of pages to be displayed and when it hit the range the next set of pages will be displayed.
    • onChange – Callback method to be triggered when we click on the page link

Below is the custom page style. Created a file named paging.module.scss

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

.paginationContainer {
    text-align: right;
    font-size: 14px;
    font-family: Calibri !important;
    .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: "[theme:themePrimary]" !important;
            color: "[theme:white]" !important;
          }
        }
      }
    }
  }

The paging component is created and now we have to add the component to the existing listview component. I have used the ExtendListViewDemo.tsx file. Below is the full code after modified for paging.

import * as React from 'react';
import { useState, useEffect } from 'react';
import styles from './ListViewDemo.module.scss';
import { IListViewDemoProps } from './IListViewDemoProps';
import { escape } from '@microsoft/sp-lodash-subset';
import { ListView, IViewField, SelectionMode, GroupOrder, IGrouping } from "@pnp/spfx-controls-react/lib/ListView";
import { useBoolean } from '@uifabric/react-hooks';
import "@pnp/sp/webs";
import "@pnp/sp/lists";
import "@pnp/sp/items";
import { Persona, PersonaSize } from 'office-ui-fabric-react/lib/Persona';
import { IColumn } from 'office-ui-fabric-react/lib/DetailsList';
import * as moment from 'moment';
import ListViewContextMenu from './ListViewContextMenu';
import Paging from '../../../common/Paging';

const slice: any = require('lodash/slice');

const ExtendListViewDemo: React.FC<IListViewDemoProps> = (props) => {
    const {
        description,
        isDarkTheme,
        environmentMessage,
        hasTeamsContext,
        userDisplayName
    } = props;
    const [loading, { setTrue: displayLoading, setFalse: hideLoading }] = useBoolean(true);
    const [listItems, setListItems] = useState<any[]>(undefined);

    const [pagedItems, setPagedItems] = useState<any[]>([]);
    const [pageSize, setPageSize] = useState<number>(10);
    const [currentPage, setCurrentPage] = useState<number>(1);

    const viewFields: IViewField[] = [
        {
            name: 'CaseID',
            displayName: 'Case ID',
            minWidth: 100,
            maxWidth: 150,
            sorting: true
        },
        {
            name: '',
            displayName: '',
            sorting: false,
            maxWidth: 40,
            render: (rowItem: any) => {
                return (
                    <ListViewContextMenu item={rowItem} />
                );
            }
        },
        {
            name: 'CaseStatus',
            displayName: 'Case Status',
            minWidth: 100,
            maxWidth: 150
        },
        {
            name: 'BizSegment',
            displayName: 'Business Segment',
            minWidth: 100,
            maxWidth: 150
        },
        {
            name: 'Author.Title',
            displayName: 'Created By',
            minWidth: 200,
            maxWidth: 250,
            sorting: true,
            render: (item?: any, index?: number, column?: IColumn) => {
                return (
                    <Persona size={PersonaSize.size24} showInitialsUntilImageLoads imageShouldStartVisible
                        text={item['Author.Title']} imageUrl={`/_layouts/15/userphoto.aspx?username=${item['Author.EMail']}&size=M`} />
                );
            }
        },
        {
            name: 'Created',
            displayName: 'Created',
            minWidth: 200,
            maxWidth: 250,
            sorting: true,
            render: (item?: any, index?: number, column?: IColumn) => {
                return (
                    <div>{moment(item.Created).format("DD-MMM-YYYY")}</div>
                );
            }
        }
    ];
    const groupFields: IGrouping[] = [
        {
            name: 'Author.Title',
            order: GroupOrder.ascending
        }
    ];

    const _getListItems = async (): Promise<void> => {
        const items: any[] = await props.sp.web.lists.getByTitle('Case Master').items
            .select('CaseID', 'CaseStatus', 'BizSegment', 'Author/Title', 'Author/EMail', 'Created')
            .expand('Author')
            ();
        setListItems(items);
        hideLoading();
    };

    const _getSelectedItem = (items: any[]) => {
        console.log('Selected Item(s): ', items);
    };

    const _onPageUpdate = async (pageno?: number) => {
        var currentPge = (pageno) ? pageno : currentPage;
        var startItem = ((currentPge - 1) * pageSize);
        var endItem = currentPge * pageSize;
        let filItems = slice(listItems, startItem, endItem);
        setCurrentPage(currentPge);
        setPagedItems(filItems);
    };

    useEffect(() => {
        if (listItems) _onPageUpdate();
    }, [listItems]);

    useEffect(() => { _getListItems() }, []);

    return (
        <section className={`${styles.listViewDemo} ${hasTeamsContext ? styles.teams : ''}`}>
            <div className={styles.welcome}>
                <img alt="" src={isDarkTheme ? require('../assets/welcome-dark.png') : require('../assets/welcome-light.png')} className={styles.welcomeImage} />
                <h2>Well done, {escape(userDisplayName)}!</h2>
            </div>
            <div>
                {loading ? (
                    <div>Loading, Please wait...</div>
                ) : (
                    <>
                        <ListView
                            items={pagedItems}
                            viewFields={viewFields}
                            compact={true}
                            selectionMode={SelectionMode.multiple}
                            selection={_getSelectedItem}
                            showFilter={true}
                            filterPlaceHolder="Search..."
                            stickyHeader={true}
                            groupByFields={groupFields}
                        />
                        <div style={{ width: '100%', display: 'inline-block' }}>
                            <Paging
                                totalItems={listItems.length}
                                itemsCountPerPage={pageSize}
                                onPageUpdate={_onPageUpdate}
                                currentPage={currentPage} />
                        </div>
                    </>
                )}
            </div>
        </section>
    );
};

export default ExtendListViewDemo;

Following are the changes done

  • Added imports to the Paging control from the common folder.
  • Created 3 state variable
    • pagedItems – Storing the items per page
    • pageSize – To store the items per page and then pass it to the paging component. This is optional and you can pass the value directly without the state variable
    • currentPage – To store the current page
  • Created a method named _onPageUpdate to calculate the items to be displayed on the current page based on the total items and the current page number. This method is invoked when the page links are clicked and this is also invoked when the items are fetched successfully from the SharePoint list.
  • Finally, the Paging component is added next to the listview control.

Reference URL

ListView – @pnp/spfx-controls-react

Sample Implementation

Conclusion

I hope you had learned something about one of the pnp control. There are lot to come in future.

Please free to post your comments and let me know if you want me to post article on any particular feature or plugins that relates to SharePoint. Thanks for your support.

The next post will be how to use PnP React Paging control in ListView.

sudharsank/pnpcontrols-demo: Demo project on different PnP React Controls. (github.com)

Happy Coding…

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s