Build dynamic SharePoint search experience using refiners and paging with SPFx, Office UI Fabric and PnP JS library

http://thecollaborationcorner.com/2017/10/16/build-dynamic-sharepoint-search-experiences-with-refiners-and-paging-with-spfx-office-ui-fabric-and-pnp-js-library/

Recently, I’ve submitted a SPFx Web Part sample showing how to build a dynamic search experience using Office UI fabric components and SharePoint search REST API. This sample comes directly from a real intranet project within SharePoint Online.

Why you would you like to do this?

Well, if you’re currently implementing a new intranet using SharePoint communication sites, you’ve probably noticed you can’t customize the default search experience, for instance, adding your own refiners:

It can be very frustrating, especially if you’ve built a nice information architecture you want to take advantage from via refiners. Also, you can’t even change the target result page for the global search box via search settings (it has no effect). The solution is for now to use either a classic search page with display templates (not for me anymore thanks) or build your own search experience with SPFx. To embrace the future ;), I chose the second option. However, in this post, I won’t talk about the integration of a search box in a custom header via SPFx extensions but I will focus on the search, paging and refinement experience with a static search query.

The full code sample can be retrieved from this repository:

https://github.com/SharePoint/sp-dev-fx-webparts/tree/dev/samples/react-search-refiners

This sample is totally generic and can be used without any additional customization in your solution. Here is a quick demo (click on the image to start the animation):

Web Part configuration

The following settings are available in the Web Part property pane:

Parameter

Description

Search query

The base search query in KQL format.

Query template

The query template in KQL format. You can use search query variables. See this post to know which ones are allowed.

Selected properties

The search managed properties to retrieve. Then you can use these properties in the code like this (item.<managedpropertyname>):

1234567891011return (    <DocumentCard onClickHref={ <strong>item.ServerRedirectedURL</strong> ? <strong>item.ServerRedirectedURL</strong> : <strong>item.Path</strong> } className=”searchWp__resultCard”>        <div className=”searchWp__tile__iconContainer” style={{ “height”: PREVIEW_IMAGE_HEIGHT }}>            <DocumentCardPreview { …previewProps } />        </div>        <DocumentCardTitle title={ <strong>item.Title</strong> } shouldTruncate={ false } />        <div className=”searchWp__tile__footer”>            <span>{ moment(<strong>item.Created</strong>).isValid() ? moment(<strong>item.Created</strong>).format(“L”): null }</span>                                               </div>              </DocumentCard>);

Refiners

The search managed properties to use as refiners. Make sure these are refinables. With SharePoint Online, you have to reuse the default ones to do so (RefinableStringXX etc.).

The order is the same as they will appear in the refnement panel.

Number of items to retrieve per page

Quite explicit. The paging behavior is done directly by the search API (See the SearchDataProvider.ts file), not by the code on post-render.

Show paging

Indicates whether or not the component should show the paging control at the bottom.

Implementation

Overall approach

This sample uses the React container component approach directly inspired by the PnP react-todo-basic sample. This approach is pretty simple and can be resumed as follow:

A container does data fetching and then renders its corresponding sub-component. That’s it.

From a personal point of view, I always use this pattern to make my code cleaner and more readable with one folder per component, one folder for data providers and one other for models (i.e business entities). To know more about this pattern, read this article or just study the react-todo-basic sample.

An other pretty convenient library if you work with React is the immuatbility helper: This library allows to mutate a copy of data without changing the original source, like this:

123456private _addFilter(filterToAdd: IRefinementFilter): void {     // Add the filter to the selected filters collection    let newFilters = update(this.state.selectedFilters, {$push: [filterToAdd]});    this._applyFilters(newFilters);}

Search results response mapping

Because search results are by definition heterogeneous, we can’t simply use a common interface to aggregate all properties so that’s why I simply use a generic interface to build result objects dynamically:

1234567891011121314export interface ISearchResult {    [key: string]: string;    IconSrc?: string;} … // Build item result dynamically where item.Key is the managed property name // and item.Value its value as stringlet result: ISearchResult = {}; elt.Cells.map((item) => {    result[item.Key] = item.Value;});

Notice that the sp-pnp-js library already provides TypeScript typings for SharePoint search API REST response (you don’t need to do it yourself):

1import pnp, { .. SearchQuery, SearchQueryBuilder, SearchResults … } from “sp-pnp-js”;

The icon for the result type (Word, PDF, etc.) is fetched dynamically via the native mapToIcon() REST method. You can set the icon size to retrieve as parameter (16×16 pixels = 0, 32×32 pixels = 1 (default = 0)):

12const iconFileName = await web.mapToIcon(encodedFileName,1);const iconUrl = webAbsoluteUrl + “/_layouts/15/images/” + iconFileName;

Refinements handling

Refiners (i.e search filters) are retrieved from the search results of the first page via the searchQueryRefiners property (set as query parameter). It means that when an user filters the results or switches the current page, refiners are not updated according the new search results, as the default behavior of SharePoint. It means results can be empty regarding the current filters combination. We use this strategy to always have a consequent filter behavior in the UI and avoid frustration for users.

When an user applies filters, a custom refinement query is built via the searchQuery.RefinementFilters property.As a reminder, refinement filters uses the SharePoint FQL syntax. The code to build this query is availaable in the _buildRefinementQueryString() method. We do a AND condition between filter properties and a OR condition between values of a single filter property:

123456789101112131415161718192021222324252627282930313233343536373839404142private _buildRefinementQueryString(selectedFilters: IRefinementFilter[]): string {        let refinementQueryConditions: string[] = [];    let refinementQueryString: string = null;        const refinementFilters = mapValues(groupBy(selectedFilters, ‘FilterName’), (values) => {        const refinementFilter =  values.map((filter) => {            return filter.Value.RefinementToken;                               });         return refinementFilter.length > 1 ? “or(” + refinementFilter + “)” : refinementFilter.toString();    });        mapKeys(refinementFilters, (value, key) => {        refinementQueryConditions.push(key + “:” + value);    });        const conditionsCount = refinementQueryConditions.length;     switch (true) {                // No filters        case (conditionsCount === 0): {            refinementQueryString = null;            break;        }          // Just one filter        case (conditionsCount === 1): {            refinementQueryString = refinementQueryConditions[0].toString();            break;        }         // Multiple filters        case (conditionsCount > 1): {            refinementQueryString = “and(” + refinementQueryConditions.toString() + “)”;            break;        }    }     return refinementQueryString;}

User Interface

I’ve used the following Office UI Fabric:

  • Panel to display refinement panel ;). Can be positioned on the left or right.
  • GroupedList: An underestimated component that can be easily used to build collapsible sections.
  • Checkbox to activate/deactivate refiners individually.
  • DocumentCard with preview to display results as tiles with a responsive design
  • Overlay, MessageBar and Spinner to handle errors/messages and waiting sequences.
  • Button to build selected refiners.

Pagination

For the pagination, we rely directly on the SharePoint search API, results per pages are retrieved via the dedicated PnP library getPage() method. All we need to do, is to pass the right page number and the number of results we want:

1const r2 = await r.getPage(page, this._resultsCount);

By this way, we make the pagination dynamic instead of doing it « post-render » by taking advantage of the API.

Note: there is a bug prior to the 2.0.8 sp-pnp-js version regarding the page calculation. More info here: https://github.com/SharePoint/PnP-JS-Core/issues/535.

Then, to build the custom pagination control, I’ve used this very handy React component (react-js-pagination):

PnP controls

This sample also showcases the use of the PnP SPFx Controls via the Placeholder. The integration is pretty easy and done at the top level class of the Web Part like this:

1234567891011121314151617181920import { Placeholder, IPlaceholderProps } from “@pnp/spfx-controls-react/lib/Placeholder”; … const placeholder: React.ReactElement<IPlaceholderProps> = React.createElement(  Placeholder,  {    iconName: strings.PlaceHolderEditLabel,    iconText: strings.PlaceHolderIconText,    description: strings.PlaceHolderDescription,    buttonLabel: strings.PlaceHolderConfigureBtnLabel,    onConfigure: this._setupWebPart.bind(this)  }); … private _setupWebPart() {  this.context.propertyPane.open();}

This control is very useful for the initial Web Part loading scenario, when the Web Part haven’t been configured yet:

Hope this sample will give you a starting point to build awesome search experiences with SPFx. See you soon for others cool SPFx components!