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

The [Fluent UI dropdown component](https://developer.microsoft.com/en-us/fluentui#/controls/web/dropdown) 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.

```typescript
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](https://developer.microsoft.com/en-us/fluentui#/controls/web/searchbox)) 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:

```typescript
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

```typescript
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:

```typescript
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:

```typescript
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

```typescript
<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:

```typescript
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](https://spfxappdev.github.io/ts-utility/) package to filter ([Where](https://spfxappdev.github.io/ts-utility/#where)) and to check if the search value is empty and set ([isNullOrEmpty](https://spfxappdev.github.io/ts-utility/#isnullorempty)).

## Sandbox / Demo

You can try out my solution in my [Codesandbox demo](https://jhfyv9.csb.app) (or [here](https://yj5jwh.csb.app/) 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 ;)
