Angular Materialでautocomplete

# はじめに

Angular Materialにはautocompleteというコンポーネントが存在します。
これは自由入力の内容に応じてセレクタの選択肢を絞り込めるものです。

オートコンプリート：https://material.angular.io/components/autocomplete/overview

しかしAngular Materialのautocompleteはそれほど使い勝手の良いものではなく、入力内容が選択肢のいずれとも一致しなくとも入力内容が残ってしまいます。
あくまで自由入力の補助であればこれでよいかと思いますが、セレクタのフィルタとしてはよろしくありません。

セレクタ：https://material.angular.io/components/select/overview

調べてみると[ng-select](https://github.com/ng-select/ng-select)というものが存在しますが、これはこれでAngular Materialとの使い合わせがよくありません。(主に[mat-form-field](https://material.angular.io/components/form-field/overview)と)

そこでセレクタのフィルタとして使えるオートコンプリートをAngular Materialを用いて作成してみました！

ReactのMUIにある[Autocomplete](https://mui.com/material-ui/react-autocomplete/)を参考に作成しました。

# 完成内容

https://stackblitz.com/edit/angular-owapls-48txyf
# 実装

## HTML

```html
<form>
  <mat-form-field appearance="fill">
    <mat-label>ラベル</mat-label>
    <input
      type="text"
      matInput
      #InputField="matInput"
      [formControl]="myControl"
      [matAutocomplete]="auto"
    />
    <mat-icon matSuffix
      >{{ InputField.focused ? 'expand_less' : 'expand_more' }}</mat-icon
    >
    <mat-autocomplete
      autoActiveFirstOption
      #auto="matAutocomplete"
      [displayWith]="autoCompleteDisplayFn"
      (closed)="onCloseAutoCompleteOptions()"
    >
      <mat-option
        *ngFor="let option of filteredOptions | async"
        [value]="option"
        [ngClass]="_allowSelection(option.id)"
      >
        {{option.value}}
      </mat-option>
    </mat-autocomplete>
  </mat-form-field>
</form>
```

## css

```css
.no-data-auto-complete {
  pointer-events: none;
  color: gray;
}
```

## TypeScript

```ts
import { Component, OnInit } from '@angular/core';
import { FormControl } from '@angular/forms';
import { Observable, of } from 'rxjs';
import { map, startWith, filter } from 'rxjs/operators';

interface ItemType {
  id: number;
  value: string;
}

@Component({
  selector: 'autocomplete-auto-active-first-option-example',
  templateUrl: 'autocomplete-auto-active-first-option-example.html',
  styleUrls: ['autocomplete-auto-active-first-option-example.css'],
})
export class AutocompleteAutoActiveFirstOptionExample implements OnInit {
  myControl = new FormControl<string | ItemType>('');
  options: ItemType[] = [
    { id: 1, value: 'aaa' },
    { id: 2, value: 'bbb' },
    { id: 3, value: 'ccc' },
  ];
  filteredOptions: Observable<ItemType[]> = of([]);

  ngOnInit() {
    this.filteredOptions = this.myControl.valueChanges.pipe(
      startWith(''),
      filter((x): x is string | ItemType => x != null),
      map((x: string | ItemType) => (typeof x === 'string' ? x : x.value)),
      map((x: string) => {
        if (x === '') {
          return [...this.options];
        }

        // 部分一致
        const filteredItemList = this.options.filter((i) =>
          i.value.includes(x)
        );
        return filteredItemList.length > 0
          ? filteredItemList
          : [{ id: -1, value: 'not found' }];
      })
    );
  }

  autoCompleteDisplayFn(item?: ItemType | null): string {
    return item ? item.value : '';
  }

  onCloseAutoCompleteOptions(): void {
    if (typeof this.myControl.value === 'string') {
      this.myControl.setValue('');
    }
  }

  _allowSelection(id: number): { [className: string]: boolean } {
    return {
      'no-data-auto-complete': id === -1,
    };
  }
}
```

- 何も見つからなかったときは `id = -1` を入れています
- 今回は入力内容の部分一致で選択肢を絞っています
- `startWith('')` を入れることで、仮に最初から値が入っていたとしても、最初に限りすべての選択肢の中から選べるようにしてあります
- あくまで選択肢のためのフィルタであって欲しいため、選択肢と完全一致している文字列が入力されていても選択肢をクリックしなければ、選択していないものとして扱います
- なにも値が存在しないときに `not found` を入れることで `closed` が発火するようにしています(選択肢がないとフォームから関心が外れても選択肢が畳まれないため `closed` は発火しない)


# 参考

- [angular/components#13013](https://github.com/angular/components/issues/13013)