Create a custom filter control¶
A filter control is the UI control used to render the values of a single filter in the 'Search Filters' Web Part. Out of the box the solution ships with a checkbox, combo box, date range, date interval, people, static people, taxonomy picker and hierarchical control.
Starting with @pnp/modern-search-extensibility v2.1.0, an extensibility library can add its own filter controls. They show up in the 'Filter template' column of the filters configuration and are rendered by the builtin filter layouts (vertical, horizontal, panel) like any builtin control.
Filter layouts vs filter controls
A filter layout (custom layout with type: LayoutType.Filter) controls how the whole set of filters is laid out on the page. A filter control only renders the values of one filter, and is reusable across every layout.
Filter control creation process¶
- Create the web component rendering the control.
- Register the filter control for discovery.
- Register the library with the 'Search Filters' Web Part.
Create the web component¶
A filter control is rendered as a custom HTML element, so it is a regular custom web component. Create it as usual by extending BaseWebComponent and returning it from getCustomWebComponents().
The builtin layouts render your component with the following attributes:
| Attribute | Description |
|---|---|
data-instance-id |
The 'Search Filters' Web Part instance ID. Pass it back in the events you dispatch so the Web Part can resolve the right context. |
data-filter-name |
The internal name of the filter (i.e. the data source field / managed property). |
data-filter |
The whole filter as a JSON string (IDataFilterInternal): display name, values with their selected/count/disabled state, isMulti, operator, canApply, canClear, etc. |
data-selected-filters |
The currently submitted filters as a JSON string (IDataFilter[]). |
data-is-multi |
true when the filter is configured to allow multiple values. |
data-show-count |
true when the filter is configured to show the values count. |
data-operator |
The operator (and/or) configured between the filter values. |
data-theme-variant |
The current theme as a JSON string. |
To apply a selection, dispatch the same events as the builtin controls, using the constants from ExtensibilityConstants:
import { ExtensibilityConstants, IDataFilterInfo, FilterComparisonOperator } from '@pnp/modern-search-extensibility';
this.dispatchEvent(new CustomEvent(ExtensibilityConstants.EVENT_FILTER_UPDATED, {
detail: {
filterName: this.filterName,
filterValues: [{
name: 'My value',
value: 'MyValue',
selected: true,
operator: FilterComparisonOperator.Eq
}],
instanceId: this.instanceId
} as IDataFilterInfo,
bubbles: true,
cancelable: true
}));
| Event | Purpose |
|---|---|
ExtensibilityConstants.EVENT_FILTER_UPDATED |
A value has been selected or unselected. |
ExtensibilityConstants.EVENT_FILTER_APPLY_ALL |
Apply all pending values at once (multi values scenario). |
ExtensibilityConstants.EVENT_FILTER_CLEAR_ALL |
Clear all values at once. |
ExtensibilityConstants.EVENT_FILTER_VALUE_OPERATOR_UPDATED |
The AND/OR operator between values changed. |
Register the filter control information¶
In the library main entry point (i.e. the class implementing IExtensibilityLibrary), return an IFilterControlDefinition object from the getCustomFilterControls() method:
| Property | Description |
|---|---|
key |
An unique internal key for your control. This value is persisted as the selectedTemplate property of the filters configuration. |
name |
The friendly name of your control, displayed in the 'Filter template' dropdown of the filters configuration. |
componentName |
The name of the custom HTML element to render. The component must also be returned by getCustomWebComponents() so it gets registered on the page. |
filterType |
FilterType.Refiner (default) when the values come from the data source, FilterType.StaticFilter when your control provides its own values. Static filters are not requested as refiners/aggregations from the connected data source. |
showOperator |
Renders the builtin AND/OR operator control above your control when the filter allows multiple values. Default is false. |
showApplyButtons |
Renders the builtin 'Apply'/'Clear' buttons below your control when the filter allows multiple values. Default is false. |
supportsMultiValues |
Set to false to disable the 'Multi values' option in the filters configuration for this control. Default is true. |
supportsValuesCount |
Set to false to disable the 'Show count' option in the filters configuration for this control. Default is true. |
supportsMaxBuckets |
Set to false to disable the 'Number of values' option in the filters configuration for this control. Default is true. |
public getCustomFilterControls(): IFilterControlDefinition[] {
return [
{
key: 'MyCompanyRatingFilter',
name: 'Rating',
componentName: 'my-rating-filter',
filterType: FilterType.Refiner,
showApplyButtons: true,
supportsValuesCount: false
}
];
}
public getCustomWebComponents(): IComponentDefinition<any>[] {
return [
{
componentName: 'my-rating-filter',
componentClass: MyRatingFilterWebComponent
}
];
}
Complete example: a simple 'tags' filter control¶
The following control renders the values of a filter as clickable pills instead of checkboxes. It is a full working example you can copy in your extensibility library: two files, no extra dependencies.
Custom element naming
The componentName must contain a dash (my-tags-filter, not mytagsfilter), otherwise the browser refuses to register the custom element.
1. The web component¶
Create src/components/MyTagsFilterWebComponent.tsx:
import * as React from 'react';
import * as ReactDOM from 'react-dom';
import { BaseWebComponent, ExtensibilityConstants, IDataFilterInfo, IDataFilterInternal } from '@pnp/modern-search-extensibility';
import { IReadonlyTheme } from '@microsoft/sp-component-base';
export interface IMyTagsFilterProps {
/** The whole filter (values, display name, etc.), from the 'data-filter' attribute */
filter?: IDataFilterInternal;
/** The filter internal name, from the 'data-filter-name' attribute */
filterName?: string;
/** 'true' when the filter shows the values count, from the 'data-show-count' attribute */
showCount?: boolean;
/** The current theme, from the 'data-theme-variant' attribute */
themeVariant?: IReadonlyTheme;
/** Called when a value is selected or unselected */
onValueUpdated: (name: string, value: string, selected: boolean) => void;
}
const MyTagsFilter: React.FunctionComponent<IMyTagsFilterProps> = (props) => {
const values = props.filter?.values || [];
const primaryColor = props.themeVariant?.palette?.themePrimary || '#0078d4';
const textColor = props.themeVariant?.semanticColors?.bodyText || '#323130';
return (
<div role='group' aria-label={props.filter?.displayName} style={{ display: 'flex', flexWrap: 'wrap', gap: 8, padding: 8 }}>
{values.map((filterValue) => (
<button
key={filterValue.value}
type='button'
disabled={filterValue.disabled}
aria-pressed={filterValue.selected}
onClick={() => props.onValueUpdated(filterValue.name, filterValue.value, !filterValue.selected)}
style={{
cursor: filterValue.disabled ? 'default' : 'pointer',
borderRadius: 16,
padding: '4px 12px',
border: `1px solid ${primaryColor}`,
background: filterValue.selected ? primaryColor : 'transparent',
color: filterValue.selected ? '#ffffff' : textColor
}}
>
{filterValue.name}{props.showCount && filterValue.count !== undefined ? ` (${filterValue.count})` : ''}
</button>
))}
</div>
);
};
export class MyTagsFilterWebComponent extends BaseWebComponent {
public connectedCallback(): void {
// Turns the 'data-*' attributes set by the filter layout into camelCase props
// (ex: 'data-filter-name' becomes 'filterName', 'data-filter' is JSON parsed)
const props = this.resolveAttributes();
const element = <MyTagsFilter
{...props}
onValueUpdated={(name: string, value: string, selected: boolean) => {
const detail: IDataFilterInfo = {
filterName: props.filterName,
filterValues: [{ name, value, selected }],
instanceId: props.instanceId
};
// The 'Search Filters' Web Part listens to this event to update the filter
this.dispatchEvent(new CustomEvent(ExtensibilityConstants.EVENT_FILTER_UPDATED, {
detail,
bubbles: true,
cancelable: true
}));
}}
/>;
ReactDOM.render(element, this);
}
}
No local state needed
The Web Part re-renders the control with an updated data-filter after every selection, including pending (not yet applied) selections of a multi values filter. Reading filterValue.selected from the props is therefore enough, and the builtin 'Apply'/'Clear' buttons work out of the box thanks to showApplyButtons: true below. Keep in mind the Web Part moves the selected values first in the list of a multi values filter, so the pills reorder as the user selects them.
2. Register it in the library¶
In the class implementing IExtensibilityLibrary:
import { IExtensibilityLibrary, IFilterControlDefinition, IComponentDefinition, FilterType } from '@pnp/modern-search-extensibility';
import { MyTagsFilterWebComponent } from './components/MyTagsFilterWebComponent';
export class MyExtensibilityLibrary implements IExtensibilityLibrary {
public getCustomFilterControls(): IFilterControlDefinition[] {
return [
{
key: 'MyTagsFilter',
name: 'Tags',
componentName: 'my-tags-filter',
filterType: FilterType.Refiner,
showApplyButtons: true
}
];
}
public getCustomWebComponents(): IComponentDefinition<any>[] {
return [
{
componentName: 'my-tags-filter',
componentClass: MyTagsFilterWebComponent
}
];
}
// ... the other IExtensibilityLibrary methods
}
Deploy the library, register its manifest ID on the 'Search Filters' Web Part, and pick 'Tags' in the 'Filter template' column of your filter. No template change is required: the builtin vertical, horizontal and panel layouts render it automatically.
Use the control in a custom filter layout¶
The builtin layouts render custom filter controls automatically. If you write your own filter layout or customize a builtin template, use the customFilterControl Handlebars helper to render whichever custom control is configured for a filter:
{{#each @root.filters as |filter|}}
{{{customFilterControl filter @root.instanceId @root.theme @root.selectedFilters}}}
{{/each}}
Two companion helpers are available:
| Helper | Description |
|---|---|
{{#isCustomFilterControl filter}}...{{else}}...{{/isCustomFilterControl}} |
Block helper telling whether the filter uses a custom control, so you can branch between your custom markup and the builtin one. |
{{{customFilterControlFooter filter @root.instanceId @root.theme}}} |
Renders the builtin 'Apply'/'Clear' buttons for controls declaring showApplyButtons: true. |
Important
The custom controls of a filter are only resolved when the extensibility library providing them is registered on the 'Search Filters' Web Part itself (last property pane page, 'Extensibility configuration' section).