Использование с Angular

Библиотека 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";

Базовая интеграция через компонент Angular

Наиболее устойчивый способ — создание компонента-обёртки, который управляет экземпляром Awesomplete и синхронизирует его с Angular формами.

HTML-шаблон компонента

<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[] = [];

Интеграция с Reactive Forms

В Angular Reactive Forms требуется синхронизация значения input и формы.

import { ControlValueAccessor, NG_VALUE_ACCESSOR } from "@angular/forms";

Реализация ValueAccessor

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

Awesomplete предоставляет собственные события, которые можно использовать для расширенного поведения.

Основные события:

  • awesomplete-selectcomplete
  • awesomplete-select
  • awesomplete-open
  • awesomplete-close

Подключение через 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)"
/>

Доступ к экземпляру Awesomplete

Иногда требуется прямое управление поведением:

this.awesomplete.evaluate();
this.awesomplete.open();
this.awesomplete.close();

Примеры применения:

  • принудительное обновление списка
  • программное открытие dropdown
  • сброс состояния при смене контекста формы

Настройка поведения и UX

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 снижает нагрузку на API
  • maxItems ограничивает DOM-рендер
  • autoFirst ускоряет выбор клавиатурой
  • filter и sort позволяют управлять логикой локального поиска

Поддержка нескольких экземпляров

При использовании в списках или динамических формах важно избегать конфликтов DOM:

ngOnDestroy(): void {
  const input = this.inputEl.nativeElement;

  input.removeEventListener("awesomplete-selectcomplete", this.handler);
}

Angular не уничтожает сторонние слушатели автоматически, поэтому очистка обязательна при сложных интерфейсах.


Типизация Awesomplete в TypeScript

У библиотеки нет полноценной типизации, поэтому часто используется расширение:

declare module "awesomplete" {
  export default class Awesomplete {
    constructor(input: HTMLElement, options?: any);

    list: any[];
    open(): void;
    close(): void;
    evaluate(): void;
  }
}

Особенности работы внутри Angular change detection

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 и моделью:

  • значение input всегда хранится в компоненте
  • Awesomplete обновляется только через входные данные
  • любые изменения проходят через единый поток данных
setValue(value: string): void {
  this.value = value;
  this.awesomplete.list = this.transform(value);
}

Поведение при уничтожении компонента

Удаление экземпляра должно учитывать очистку DOM-обработчиков:

ngOnDestroy(): void {
  this.awesomplete = null as any;
}