Skip to main content

Command Palette

Search for a command to run...

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

Updated
5 min read
How to create a Microsoft Fluent UI Searchable Dropdown Component with react
$

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@

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

M

How can I use it in SPFx React with hooks ?

$

Hi Miguel Carreon What do you want to do? Do you want to use this class component in a functional component (hooks) in SPFx? Or would you like to know how you can "rebuild" the component with hooks?

M

$€®¥09@Hi, I like to know how I can rebuild the component with hooks

M

$€®¥09@ Hi, I like to know how I can rebuild the component with hooks

$

Miguel Carreon The current code of the class component can be found here

You need to change the class component to a function component. The current component is stateless, so you need to replace all class methods with functions and it should work.

Alternatively, you can use my npm package @spfxappdev/fluentui-react-controls which contains also this control

A Live demo (PnP community call) is here: https://www.youtube.com/watch?v=nxG7n4e2Xlc

B

Hi , Can you please help , how to setup dev environment for spfx viva connection. Currently i have 30 web-parts under my production workspace. I need to host this in same tenant with dev environment.

$

Hi, I'm not sure if I understand you correctly or what you are planning to do! You want two different "environments" on the same tenant with possibly different versions of the web part(s)? If yes, create a site collection app catalog and deploy it in the site collection only.

B

Thanks Let me try out same. :) $€®¥09@

1

More from this blog

S

SharePoint SPFx Development by $€®¥09@

30 posts

Create Fluent UI Searchable Dropdown in React