Полный код здесь 🔥!

Статья на русском языке (статья на русском языке)

Содержание

  1. Предисловие
  2. "Что нам нужно"
  3. Как создавать пользовательские элементы управления в Angular?
  4. Создать компонент multiselect-search
  5. Улучшение UX нашего компонента мультиселект-поиск
  6. Пример использования компонента multiselect-search
  7. Источники информации

1. Предисловие

Всем привет 👋, это мой первый пост в интернете и сегодня я хотел бы поделиться с вами решением проблемы, с которой недавно столкнулся на работе. Необходимо было реализовать компонент множественного выбора, который предлагал элементы по части слова, выполняя поиск на сервере. К тому же проблема в том, что из предложенных мне библиотек и UI-китов я не нашел ни одного подходящего решения и пришлось писать свою реализацию. Что ж, начнем!

Мой аккаунт на GitHub

2. Что нам нужно

  • Угловой 2+.
  • Средний уровень знаний (средний уровень и выше) Angular 2+.
  • Создание пользовательских элементов управления в Angular Я расскажу об этом чуть позже, это не сложно.
  • Библиотека пользовательского интерфейса — Angular Material. Наш мультиселект с поиском будет основан на некоторых компонентах Angular Material.

3. Как создать собственные элементы управления в Angular?

Если вы еще не знакомы с этой темой или хотите освежить свои знания, то прочтите ее.

Angular предлагает нам простой, но мощный инструмент для создания пользовательских элементов управления, когда не хватает нативных (input, checkbox, select и т. д.).

Еще одно важное замечание: настраиваемые элементы управления можно использовать как с реактивными формами, так и с формами, управляемыми шаблонами.

Например, мы будем использовать знакомый компонент счетчика.

Во-первых, давайте напишем следующий код:

Чтобы интегрировать пользовательский элемент управления в формы Angular, вам нужен этот элемент управления для реализации интерфейса — ControlValueAccessor. Сам Angular использует ControlValueAccessor под капотом, чтобы принуждать поведение собственных элементов управления.

Однако что делает Angular, используя ControlValueAccessor? Все просто, записывает значение из модели в DOM (представление), а также вызывает событие смены элемента управления в FormGroup и другие директивы.

Официальная документация для ControlValueAccessor находится здесь — https://angular.io/api/forms/ControlValueAccessor.

Как видно из официальной документации, интерфейс обязывает нас реализовать три обязательных метода: writeValue, registerOnChange, registerOnTouched и один необязательный setDisabledState.

writeValue(value: any) — записывает новое значение в элемент управления. Вызывается при установке значения по умолчанию new FormControl('Default value') или нового значения control.setValue('New value').

registerOnChange(fn: any) — регистрирует функцию обратного вызова, которая вызывается при изменении значения элемента управления в представлении для метода (change) в представлении.

registerOnTouched(fn: any) — определяет обратный вызов, который вызывается в случае снятия фокуса с элемента управления (при размытии).

setDisabledState(isDisabled: boolean) — (необязательно для реализации) функция, которая будет вызываться при изменении [disabled]="true"value в элементе управления.

Теперь, когда у нас есть базовые знания о ControlValueAccessor, давайте применим их к нашему CounterControlComponent.

Чтобы форма знала об изменениях управления, нам нужно вызывать метод onChange для каждого изменения value в интерфейсе. Чтобы не писать вызов onChange в каждом методе (вверх и вниз), в установщике значений мы вызываем метод onChange, который будет уведомлять форму об обновлении значения в контроле.

Чтобы Angular понял, что это пользовательский компонент, мы описали его в декораторе @Component():

…
providers: [{
    provide: NG_VALUE_ACCESSOR,
    useExisting: forwardRef(() => CounterControlComponent),
    multi: true
}]
…

Наш пользовательский элемент управления теперь готов к использованию в реактивной и управляемой шаблонами форме.

Это все волшебство 🔮, теперь мы можем использовать наш пользовательский элемент управления с [(ngModel)] в формах, управляемых шаблоном:

<counter-control [(ngModel)]="controlValue"></counter-control>

А также в реактивно-управляемых формах:

<form [formGroup]="form">
    <counter-control formControlName="counter"></counter-control>
</form>

4. Создать компонент поиска с множественным выбором

Для создания нашего бокса мы будем использовать компонент Angular Material Chips и компонент Autocomplete.

Вот как будет выглядеть код нашего компонента:

Если вы уже работали с Angular Material, то для вас не так много нового, но давайте подробнее рассмотрим, что происходит в коде выше.

Во-первых, мы соединили два отдельных компонента (Chips и Autocomplete) в один с помощью композиции компонентов. Это позволило нам вносить в список Фишек произвольные элементы, а также выбирать из предложенных компонентом Автозаполнение.

Теперь поговорим о каждом компоненте отдельно:

‹mat-chip-list›

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

1️⃣ На событии добавления чипа у нас есть метод — onAddItem(), который добавляет элемент в <mat-chip-list>, если он еще не добавлен, избавляя нас от дублирования.

2️⃣ Для события удаления чипа у нас есть метод —onRemoveItem(), который удалит элемент из списка выбранных.

3️⃣ Также метод emitSearchOnTyping() отслеживает событие ввода значения в поле ввода и генерирует событие поиска, если пользователь прекращает печатать в течение 300 мс debounceTime(300) и введенное значение отличается от предыдущего distinctUntilChanged().

‹mat-autocomplete›

Этот компонент имеет 2 ключевых момента, на которые следует обратить внимание:

1️⃣ Это обработка события конкретного <mat-option> (optionSelected)="onSelectOptionFromSearchResult($event)". Метод onSelectOptionFromSearchResult() почти аналогичен методу onAddItem() и выполняет ту же функцию — добавляет элемент в выбранный список.

2️⃣ В нашем компоненте множественного выбора-поиска мы передаем шаблон из опции рисования в <mat-autocomplete>:

multiselect-search.component.ts

@Input()
public optionTemplate: TemplateRef<any>;

multiselect-search.component.html

<mat-option
    *ngFor="let searchedItem of searchResults
    [value]="searchedItem.value"
>
   <ng-container
       [ngTemplateOutlet]="optionTemplate"
       [ngTemplateOutletContext]="{ $implicit: searchedItem.source }"
    ></ng-container>
</mat-option>

✅ Вот и все!

5. Улучшение UX нашего компонента множественного поиска

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

Добавьте новый <mat-option> для каждого из этих вариантов использования:

Для индикатора загрузки

<mat-option *ngIf="loading; else searchResultsOptions" disabled>
    <div style="height: 50px;">
        <nb-spinner message="Loading..." size="large"></nb-spinner>
    </div>
</mat-option>

Для пустого результата поиска:

<mat-option *ngIf="!loading && searchResults.length === 0 && inputElement?.nativeElement.value.length !== 0" disabled>
    <div class="user-message">
        <app-user-message message="Nothing found"></app-user-message>
        <p class="caption user-message__subtext">
            Try to enter a different value for the search, or press Enter if you can confirm the current value.
        </p>
    </div>
</mat-option>

Для параметров результатов поиска:

<ng-template #searchResultsOptions>
    <mat-option
        *ngFor="let searchedItem of searchResults"
        [value]="searchedItem.value"
    >
        <ng-container
           [ngTemplateOutlet]="optionTemplate"
           [ngTemplateOutletContext]="{ $implicit: searchedItem.source }"
        ></ng-container>
    </mat-option>
</ng-template>

Полный код MultiselectSearchComponent с индикатором загрузки и пустым поисковым сообщением:

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

6. Пример использования компонента multiselect-search

Спасибо за прочтение, оставляйте свои комментарии и пожелания по улучшению статьи. Если статья была вам полезна ставьте лайк 👏.

7. Источники информации