How to create a Microsoft Fluent UI Searchable Dropdown Component with react
The Fluent UI dropdown component provided by Microsoft is good. But unfortunately it does not support to "search" for items from the list. In this post, I will describe how to create a custom React component with the ability to filter the dropdown components.
The user should be able to search/filter the list in the dropdown component. You can fulfill this requirement with a custom React component.
Just create a new react component. The Properties are inherited from IDropdownProps
but with two additional properties.
onSearchValueChanged(searchValue: string): void;
searchboxProps?: Omit<ISearchBoxProps, 'onChange' | 'onClear' | 'onSearch'>;
The onSearchValueChanged
property is required because you need to handle the event when a user enters something in the search field (= You want to filter the list according to the search term).
Why I exclude the onChange
, onClear
and onSearch
for searchboxProps
?
I think all three properties should deal with the same event: the user searching for something. This is the reason why the onSearchValueChanged
property is required. As a developer, you should be able to determine how the list should be filtered. In my opinion, the onChange
, onClear
, and onSearch
properties (from the Fluent UI SearchBox Component) should trigger the event with a single event. This is the reason why I created the onSearchValueChanged
property. This event is automatically triggered on onChange
, onClear
, and onSearch
.
Use searchboxProps
to set all search box properties, except onChange
, onClear
, and onSearch
. The reason was explained earlier. These events are not needed anymore. Use onSearchValueChanged
instead.
The render
method
The content of the render
method is really simple and short. It is enough to render the default Dropdown
component of Fluent UI and pass all the properties of the component to the control (since the properties inherit from IDropdownProps
). Here is the code:
public render(): React.ReactElement<ISearchableDropDownProps> {
return (
<Dropdown
{...this.props}
options={this.getOptions()}
onRenderOption={(
option?: ISelectableOption,
defaultRender?: (props?: ISelectableOption) => JSX.Element | null,
): JSX.Element | null => {
return this.onRenderOption(option, defaultRender);
}}
/>
);
}
As you can see, I am overriding the onRenderOption
method and the options
property. I'll describe why I'm doing this. First, let's take a look at the getOptions
method
The getOptions
method
private getOptions(): IDropdownOption[] {
const result: IDropdownOption[] = [];
result.push({
key: "search",
text: "",
itemType: SelectableOptionMenuItemType.Header,
});
return result.concat([...this.props.options]);
}
I think it's self-explanatory. We add a "dummy" option with the key "search" as the option header. Then, the original options are merged.
The onRenderOption
method
Now, let's take a look at what the onRenderOption looks like:
private onRenderOption(
option?: ISelectableOption,
defaultRender?: (props?: ISelectableOption) => JSX.Element | null,
): JSX.Element | null {
if (!option) {
return null;
}
if (
option.itemType === SelectableOptionMenuItemType.Header &&
option.key === "search"
) {
return (
<SearchBox
{...this.props.searchboxProps}
onChange={(
ev?: React.ChangeEvent<HTMLInputElement>,
newValue?: string,
): void => {
if (typeof this.props.onSearchValueChanged === "function") {
this.props.onSearchValueChanged(newValue || "");
}
}}
onSearch={(newValue: string): void => {
if (typeof this.props.onSearchValueChanged === "function") {
this.props.onSearchValueChanged(newValue);
}
}}
onClear={() => {
if (typeof this.props.onSearchValueChanged === "function") {
this.props.onSearchValueChanged("");
}
}}
/>
);
}
if (typeof this.props.onRenderOption === "function") {
return this.props.onRenderOption(option, defaultRender);
}
if (!defaultRender) {
return null;
}
return defaultRender(option);
}
Okay, this method is a bit longer, but not too difficult to understand. We check if the current option element (ISelectableOption
) is of type "Header" and the key is "search". If so, we add the FluentUI SearchBox
component and pass all searchboxProps
properties to the search box. Then, we override the onChange
, onSearch
, and onClear
methods. This allows us to trigger our own onSearchValueChanged
function (see above).
Otherwise, if it is not our search option, we call the custom onRenderOption
method (= Your own defined render method) if this property is defined, or execute the defaultRender
method.
That's it. The component is ready for use. Here is the complete code:
import * as React from "react";
import {
Dropdown,
IDropdownProps,
IDropdownOption,
SelectableOptionMenuItemType,
ISelectableOption,
SearchBox,
ISearchBoxProps,
} from "@fluentui/react";
export interface ISearchableDropDownProps extends IDropdownProps {
onSearchValueChanged(searchValue: string): void;
searchboxProps?: Omit<ISearchBoxProps, "onChange" | "onClear" | "onSearch">;
}
interface ISearchableDropDownState {}
export default class SearchableDropDown extends React.Component<
ISearchableDropDownProps,
ISearchableDropDownState
> {
public state: ISearchableDropDownState = {};
public static defaultProps: Partial<ISearchableDropDownProps> = {
searchboxProps: {
autoComplete: "false",
autoFocus: true,
},
};
public render(): React.ReactElement<ISearchableDropDownProps> {
return (
<Dropdown
{...this.props}
options={this.getOptions()}
onRenderOption={(
option?: ISelectableOption,
defaultRender?: (props?: ISelectableOption) => JSX.Element | null,
): JSX.Element | null => {
return this.onRenderOption(option, defaultRender);
}}
/>
);
}
private onRenderOption(
option?: ISelectableOption,
defaultRender?: (props?: ISelectableOption) => JSX.Element | null,
): JSX.Element | null {
if (!option) {
return null;
}
if (
option.itemType === SelectableOptionMenuItemType.Header &&
option.key === "search"
) {
return (
<SearchBox
{...this.props.searchboxProps}
onChange={(
ev?: React.ChangeEvent<HTMLInputElement>,
newValue?: string,
): void => {
if (typeof this.props.onSearchValueChanged === "function") {
this.props.onSearchValueChanged(newValue || "");
}
}}
onSearch={(newValue: string): void => {
if (typeof this.props.onSearchValueChanged === "function") {
this.props.onSearchValueChanged(newValue);
}
}}
onClear={() => {
if (typeof this.props.onSearchValueChanged === "function") {
this.props.onSearchValueChanged("");
}
}}
/>
);
}
if (typeof this.props.onRenderOption === "function") {
return this.props.onRenderOption(option, defaultRender);
}
if (!defaultRender) {
return null;
}
return defaultRender(option);
}
private getOptions(): IDropdownOption[] {
const result: IDropdownOption[] = [];
result.push({
key: "search",
text: "",
itemType: SelectableOptionMenuItemType.Header,
});
return result.concat([...this.props.options]);
}
}
How to use it
You can use the component like the FluentUI Dropdown
component, but with two relevant exceptions. First, you need to take care of filtering the options when a user searches for something, or set the initial values when the search is finished or something is selected. Second, you should definitely set the defaultSelectedKey(s)
or selectedKey(s)
property; otherwise, the "selected" element(s) will be changed when the user searches for an element.
Example
<SearchableDropDown
defaultSelectedKey={this.state.selectedKeys}
onChange={(ev: any, option) => {
this.setState({
selectedKeys: option ? option.key.toString() : "",
filteredItems: [...initialItems],
});
}}
onSearchValueChanged={(searchValue: string) => {
const newOptions = this.onDropDownSearch(
searchValue,
initialItems,
);
this.setState({
filteredItems: newOptions,
});
}}
options={this.state.filteredItems}
/>
The onDropDownSearch
method looks like this:
private onDropDownSearch(
searchValue: string,
initialValues: IDropdownOption[],
): IDropdownOption[] {
if (isNullOrEmpty(searchValue)) {
return [...initialValues];
}
//OR FIlter but whatever you want...
const filteredOptions = [...initialValues].Where(
(i) =>
i.text.Contains(searchValue) &&
(!isset(i.itemType) || i.itemType === DropdownMenuItemType.Normal),
);
return filteredOptions;
}
BTW: In this example, I use my @spfxappdev/utility package to filter (Where) and to check if the search value is empty and set (isNullOrEmpty).
Sandbox / Demo
You can try out my solution in my Codesandbox demo (or here with many options/items). Feel free to copy/modify the code (maybe you can write a comment about what was changed 😊)
What do you think? How do you like it? I would be happy about feedback.
Happy coding ;)
Subscribe to my newsletter
Read articles from $€®¥09@ directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by
$€®¥09@
$€®¥09@
I am Sergej and I am a Software Architect from Germany (AURUM GmbH). I have been developing on Microsoft technologies for more than 14 years, especially in SharePoint / Microsoft 365. With this blog, I want to share my knowledge with you and also improve my English skills. The posts are not only about SPFx (SharePoint Framework) but also about tips & tricks around the M365 world & developments of all kinds. The posts are about TypeScript, C#, Node.js, Vue.js, Visual Studio/ VS Code, Quasar, PowerShell, and much more. I hope you will find some interesting posts. I would also be happy if you follow me. Greetings from Germany Sergej / $€®¥09@