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

Build dynamic SharePoint search experience using 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>):

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:

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:

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):

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)):

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:

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:

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:

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!

 

 

 

19 Comments

Add yours
    • Franck Cornu

      Hi steve,

      You can just configure the searchQuery object according to your needs in the search data provider search() method:


      let sortList: Sort[] = [
      {
      Property: 'Created',
      Direction: SortDirection.Descending
      },
      {
      Property: 'Size',
      Direction: SortDirection.Ascending
      }
      ];

      searchQuery.SortList = sortList;

      For now, there is no way to configure it through the property pane.

  1. Michał Guzowski

    Great peace of code! Thank you for that. By the way – are you planning to implement a document preview like in an old fashion Search Results (with ability to scroll thru pages)? Is there a way to implement it anyhow?

  2. Damien

    Bonjour Franck,
    Merci beaucoup pour ce développement très utile pour personnaliser le search dans la nouvelle expérience.

    Lorsque j’essaie de packager le projet, j’obtiens les erreurs/warnings suivants :
    Warning – [sass] src\webparts\searchBox\SearchBoxWebPart.scss: filename should end with module.scss
    Warning – [sass] src\webparts\searchResults\components\SearchResultsWebPart.scss: filename should end with module.scss
    Warning – [sass] src\webparts\searchResults\components\Layouts\SearchResultsTemplate.scss: filename should end with module.scss

    Et si je les renomme en module.scss, j’obtiens 1007 erreurs/warning de type
    [sass] The local CSS class .* is not camelCase and will not be type-safe

    Tu as déjà rencontré ce type d’erreur ? Tu sais comment y remédier?
    Merci!

  3. Fabian

    Great work!
    Is there a way to get a set of default search results (e.g. only ContentTypes XY) and once the user is searching for something, the results will narrow it down to ContentType XY & the user query?

  4. Nicolas

    Bonjour Franck,
    Un grand merci pour ce développement compatible avec les sites de communication.
    Je m’en sers pour interroger notre annuaire d’entreprise via une source de résultats sur de la recherche de personnes.
    Je ne fais pas personnellement de développement mais est-ce qu’il serait possible d’identifier que le résultat de la recherche est de type personne et faire en sorte d’afficher
    les éléments sous la forme de cartelettes « interactives » qui comme pour un contact (webpart MS) en passant dessus avec la souris permet d’avoir le popup de détails
    et enfin le panneau complet des informations de l’utilisateur ?
    Merci pour vos réponses et votre travail.
    Cordialement.

  5. Nicolas

    Bonjour Franck,
    Merci pour ce développement compatible avec les sites de communication.
    Je m’en sers pour interroger notre annuaire d’entreprise via une source de résultats de recherche de personnes.
    Personnellement je ne développe pas mais j’aurais voulu savoir s’il serait possible d’identifier que les éléments sont de type personne et de faire en sorte de les présenter
    comme un contact (webpart MS) pour avoir la possibilité quand l’on passe dessus d’avoir le popup de détails et enfin le panneau avec toutes les informations de l’utilisateur ?
    Merci de vos réponses et de votre travail.
    Cordialement.

  6. Fabian

    Unfortunately, the query template doesn’t work, really. No matter what I type in, it will not show up. The default value would be {searchTerms} Path:{Site} which should already display SOME data (if there are documents uploaded to the current site, which we have). However, it doesn’t really work. I tried to test a few settings, but it doesn’t look like it is changing anything regarding the query template. Any advice?

    • Franck Cornu

      Hi Fabian,

      The query template works in addition to a base query (i.e query query keywords). The {searchTerms} token is the link between them. If you enter « * » as query keyword, you should be able to get documents. If not, please submit a new issue here if repro steps. Thanks!

  7. Fabian

    Yeah exactly, if I use * in the search box as well as for the query template, I don’t get any results. The query template by itself (without the search box) works great. Also the search box by itself without the query template works great, but both combined won’t.

+ Leave a Comment

This site uses Akismet to reduce spam. Learn how your comment data is processed.