
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
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>):
1 2 3 4 5 6 7 8 9 10 11 |
return ( <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
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:
1 2 3 4 5 6 |
private _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:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
export 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 string let 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):
1 |
import 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)):
1 2 |
const 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:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 |
private _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:
1 |
const 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:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
import { 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!
Great work @Franc.
how can you perform sorting on certain fields in this example?
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.
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?
Thanks for the feedback! Not in the backlog for now be I think it could be accomplished using Office Online iframe.
Looking very nice.
How would you add a search box, so the normal user, would be able to set the search query keywords field?
Hi, the latest version for this sample includes a search box as well 😉 https://github.com/SharePoint/sp-dev-fx-webparts/tree/master/samples/react-search-refiners
Hi.
Nice sample.
How would you be able to add a search part, where the normal user, would be able to set the search query keyword?
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!
Bonjour Damien.Ce n’est pas une erreur mais plutôt un warning. La syntaxe .modules.scss est propre au SPFx qui génère des noms de classes uniques.Je te conseille de lire l’article suivant pour la gestion des CSS dans SPFx:
https://docs.microsoft.com/en-us/sharepoint/dev/spfx/css-recommendations#use-css-modules-to-avoid-styling-conflicts
Pour enlever les warnings, ajoute juste ca https://github.com/Microsoft/web-build-tools/blob/master/core-build/gulp-core-build/README.md#addsuppressionsuppression-string–regexp dans ton fichier gulpfile.js
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?
Hi Fabian, yes you can use the ‘Query Template’ field for this purpose. Don’t forget the {searchTerms} variable (this is the user query).
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.
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.
Bonjour Nicolas,
Oui possible, mais en modifiant le template ce qui nécéssite du développement :(.
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?
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!
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.
Are you using the latest version of the Web Part available on PnP? https://github.com/SharePoint/sp-dev-fx-webparts/tree/dev/samples/react-search-refiners
Yes, I just downloaded the newest version and tried to use a query template along with the search box. Still doesn’t work. No results being shown.
Thanks, Franck, your post has been really helpful. I know you specifically said you won’t mention “integration of a search box in a custom header via SPFx extensions” but do you have any suggestions on how to replace the default “Search this site” or perhaps hijack where it calls to use your search box
Hi Chris, a quick “hack” would be to manipulate the DOM using an SPFx extension to override (i.e replace by your own or update the behavior) the OOTB search box. Dirty but it could work ;).
Hi, this seems like a really cool solution. How ever, I am getting an error trying to compile the downloaded code:
Error – [tsc] src/services/NlpService/NlpService.ts(43,74): error TS2345: Argument of type ‘HttpClientConfiguration’ is not assignable to parameter of type ‘HttpClientConfiguration’.
Error – [tsc] src/webparts/searchBox/SearchBoxWebPart.ts(123,45): error TS2345: Argument of type ‘HttpClient’ is not assignable to parameter of type ‘HttpClient’.
Error – [tsc] src/webparts/searchResults/SearchResultsWebPart.ts(193,57): error TS2345: Argument of type ‘SPHttpClient’ is not assignable to parameter of type ‘SPHttpClient’.
Error – [tsc] src/webparts/searchResults/SearchResultsWebPart.ts(534,79): error TS2345: Argument of type ‘SPHttpClientConfiguration’ is not assignable to parameter of type ‘SPHttpClientConfiguration’.
Hi, can you log your issue here https://github.com/SharePoint/sp-dev-solutions/issues? I will be easier for me to follow up and try to fix it.
You can also check your node.js and SPFx generator version. More info here https://docs.microsoft.com/en-us/sharepoint/dev/spfx/set-up-your-development-environment
Hi Frank, I must say this is a really cool solution. But one problem I am facing. I have used search refiner for blog widget. Every data I am getting except blog’s author pic.. I have used proper managed property which is working successfully for classing share-point but still not getting the author picture. Can you please help me out??
Hi, what managed property are you using? Maybe it is the wrong one. Also, did you add this property to the ‘selected properties’ option of this Web Part?
You can log your issue here https://github.com/SharePoint/sp-dev-solutions/issues so I will be able to do the follow up.
We have a managed meta data column we set as a refiner. Some of our Parent terms have Children. Each show up in the refinement panel. If we select the Parent term, the rest of the check boxes disappear, same happens if we select a Child term. Is there a way to select multiple refiners? Also, if we select a Parent term is there a way to return the Parent and all of its Child items?
Hi, the default behavior is not configured for multiple selections. You can change it directly in the code by setting an OR condition between refinement values instead of a AND.
Can I query for People Search ?
Hi, yes sure, you can use the built-in SharePoint people result source id.
Hi Great Work
I have two issues, first I’m not able to search custom columns when redirecting to a resultspage. If i have the searchbox andresults webpart on the same page it works fine. Second I get some wonky dates for Created and modified ie Created 20149-05-11 Modified 2017-02-02
A bit late but if you still encounter this issue, please submit it in the associated GitHub repository https://github.com/SharePoint/sp-dev-solutions/issues
Hi Frank,
Is there a plan to add those search hover elements on People search like in normal People Search?
Hi Christian, unfortunately, no plan for this yet. Sorry.