はじめに
Angular Materialにはautocompleteというコンポーネントが存在します。
これは自由入力の内容に応じてセレクタの選択肢を絞り込めるものです。
オートコンプリート:https://material.angular.io/components/autocomplete/overview
しかしAngular Materialのautocompleteはそれほど使い勝手の良いものではなく、入力内容が選択肢のいずれとも一致しなくとも入力内容が残ってしまいます。
あくまで自由入力の補助であればこれでよいかと思いますが、セレクタのフィルタとしてはよろしくありません。
セレクタ:https://material.angular.io/components/select/overview
調べてみるとng-selectというものが存在しますが、これはこれでAngular Materialとの使い合わせがよくありません。(主にmat-form-fieldと)
そこでセレクタのフィルタとして使えるオートコンプリートをAngular Materialを用いて作成してみました!
ReactのMUIにあるAutocompleteを参考に作成しました。
完成内容
実装
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
.no-data-auto-complete {
pointer-events: none;
color: gray;
}
TypeScript
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
は発火しない)