How to create a Microsoft Fluent UI Searchable Dropdown Component with react

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

Did you find this article valuable?

Support $€®¥09@ by becoming a sponsor. Any amount is appreciated!