Статья на русском языке (статья на русском языке)
Содержание
- Предисловие
- "Что нам нужно"
- Как создавать пользовательские элементы управления в Angular?
- Создать компонент multiselect-search
- Улучшение UX нашего компонента мультиселект-поиск
- Пример использования компонента multiselect-search
- Источники информации
1. Предисловие
Всем привет 👋, это мой первый пост в интернете и сегодня я хотел бы поделиться с вами решением проблемы, с которой недавно столкнулся на работе. Необходимо было реализовать компонент множественного выбора, который предлагал элементы по части слова, выполняя поиск на сервере. К тому же проблема в том, что из предложенных мне библиотек и UI-китов я не нашел ни одного подходящего решения и пришлось писать свою реализацию. Что ж, начнем!
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
Спасибо за прочтение, оставляйте свои комментарии и пожелания по улучшению статьи. Если статья была вам полезна ставьте лайк 👏.