Библиотека Awesomplete представляет собой лёгкий инструмент для автодополнения без привязки к фреймворкам, поэтому в Angular она интегрируется как сторонняя DOM-надстройка. Основная особенность такого подключения заключается в том, что Angular управляет жизненным циклом компонентов, тогда как Awesomplete напрямую взаимодействует с DOM-элементами.
Установка библиотеки выполняется стандартно через npm:
npm install awesomplete
Подключение стилей обязательно, иначе выпадающий список не будет корректно отображаться:
// angular.json
"styles": [
"node_modules/awesomplete/awesomplete.css",
"src/styles.css"
]
или в глобальном стилевом файле:
@import "~awesomplete/awesomplete.css";
Скрипт подключать вручную обычно не требуется, так как Angular + TypeScript импортируют модуль напрямую:
import Awesomplete from "awesomplete";
Наиболее устойчивый способ — создание компонента-обёртки, который управляет экземпляром Awesomplete и синхронизирует его с Angular формами.
<input
type="text"
#inputEl
[value]="value"
(input)="onInput($event)"
/>
import { AfterViewInit, Component, ElementRef, ViewChild, Input } from "@angular/core";
import Awesomplete from "awesomplete";
@Component({
selector: "app-autocomplete",
templateUrl: "./autocomplete.component.html"
})
export class AutocompleteComponent implements AfterViewInit {
@ViewChild("inputEl", { static: true }) inputEl!: ElementRef<HTMLInputElement>;
@Input() list: string[] = [];
value = "";
private awesomplete!: Awesomplete;
ngAfterViewInit(): void {
this.awesomplete = new Awesomplete(this.inputEl.nativeElement, {
list: this.list,
minChars: 1,
autoFirst: true
});
}
onInput(event: Event): void {
const value = (event.target as HTMLInputElement).value;
this.value = value;
}
}
В Angular данные часто приходят асинхронно, поэтому важно уметь обновлять список подсказок без пересоздания компонента.
@Input() set list(value: string[]) {
this._list = value;
if (this.awesomplete) {
this.awesomplete.list = value;
}
}
get list(): string[] {
return this._list;
}
private _list: string[] = [];
В Angular Reactive Forms требуется синхронизация значения input и формы.
import { ControlValueAccessor, NG_VALUE_ACCESSOR } from "@angular/forms";
providers: [
{
provide: NG_VALUE_ACCESSOR,
useExisting: AutocompleteComponent,
multi: true
}
]
writeValue(value: string): void {
this.value = value;
if (this.inputEl) {
this.inputEl.nativeElement.value = value;
}
}
registerOnChange(fn: any): void {
this.onCha nge = fn;
}
registerOnTouched(fn: any): void {
this.onTouc hed = fn;
}
onInput(event: Event): void {
const value = (event.target as HTMLInputElement).value;
this.value = value;
this.onChange(value);
}
Awesomplete предоставляет собственные события, которые можно использовать для расширенного поведения.
Подключение через addEventListener:
ngAfterViewInit(): void {
const input = this.inputEl.nativeElement;
this.awesomplete = new Awesomplete(input, {
list: this.list
});
input.addEventListener("awesomplete-selectcomplete", (e: any) => {
console.log("Выбран элемент:", e.text.value);
});
}
Частый сценарий — загрузка подсказок с сервера.
search(term: string): void {
this.http.get<string[]>(`/api/search?q=${term}`)
.subscribe(results => {
this.awesomplete.list = results;
});
}
В шаблоне:
<input
#inputEl
(input)="search($event.target.value)"
/>
Чтобы избежать избыточных запросов к API, используется RxJS:
import { Subject } from "rxjs";
import { debounceTime, distinctUntilChanged } from "rxjs/operators";
private input$ = new Subject<string>();
ngOnInit(): void {
this.input$
.pipe(
debounceTime(300),
distinctUntilChanged()
)
.subscribe(value => {
this.search(value);
});
}
<input
#inputEl
(input)="input$.next($event.target.value)"
/>
Иногда требуется прямое управление поведением:
this.awesomplete.evaluate();
this.awesomplete.open();
this.awesomplete.close();
Примеры применения:
Awesomplete поддерживает параметры, которые критичны при интеграции в Angular UI:
this.awesomplete = new Awesomplete(input, {
list: this.list,
minChars: 2,
maxItems: 10,
autoFirst: true,
filter: Awesomplete.FILTER_STARTSWITH,
sort: Awesomplete.SORT_BYLENGTH
});
minChars снижает нагрузку на APImaxItems ограничивает DOM-рендерautoFirst ускоряет выбор клавиатуройfilter и sort позволяют управлять логикой
локального поискаПри использовании в списках или динамических формах важно избегать конфликтов DOM:
ngOnDestroy(): void {
const input = this.inputEl.nativeElement;
input.removeEventListener("awesomplete-selectcomplete", this.handler);
}
Angular не уничтожает сторонние слушатели автоматически, поэтому очистка обязательна при сложных интерфейсах.
У библиотеки нет полноценной типизации, поэтому часто используется расширение:
declare module "awesomplete" {
export default class Awesomplete {
constructor(input: HTMLElement, options?: any);
list: any[];
open(): void;
close(): void;
evaluate(): void;
}
}
Awesomplete работает вне зоны Angular, поэтому изменения DOM не
всегда отслеживаются. При необходимости синхронизации с UI используется
NgZone:
import { NgZone } from "@angular/core";
constructor(private zone: NgZone) {}
ngAfterViewInit(): void {
this.zone.runOutsideAngular(() => {
this.awesomplete = new Awesomplete(this.inputEl.nativeElement, {
list: this.list
});
});
}
В Angular часто требуется более сложная логика поиска:
this.awesomplete = new Awesomplete(input, {
list: this.list,
filter: (text: string, input: string) => {
return text.toLowerCase().includes(input.toLowerCase());
}
});
Такой подход позволяет:
При использовании в сложных формах каждый input должен иметь отдельный экземпляр Awesomplete:
@ViewChildren("inputEl") inputs!: QueryList<ElementRef>;
this.inputs.forEach((el) => {
new Awesomplete(el.nativeElement, {
list: this.list
});
});
Для предотвращения рассинхронизации между UI и моделью:
setValue(value: string): void {
this.value = value;
this.awesomplete.list = this.transform(value);
}
Удаление экземпляра должно учитывать очистку DOM-обработчиков:
ngOnDestroy(): void {
this.awesomplete = null as any;
}