Недавно Бхарат Рави опубликовал статью о директиве самосохраняющегося select-элемента на InDepth. Это интересная концепция изолирования логики в директиве, что в целом идея хорошая.
Однако в этом случае у меня есть сомнения, которые я хочу подсветить. Я предлагаю свою версию компонента, исправляющую эти моменты. Начнем с того, что назовем проблемы текущего решения.
Директивы работают со своим элементом
Когда вы задумываетесь о создании директивы, обратите внимание на работу с шаблоном. Если для функционирования нужно несколько элементов, то для этой цели лучше подойдет компонент.
«Ваши директивы не вольны модифицировать DOM за рамками своего элемента»
Вот фрагмент кода из оригинальной статьи:
handleErrorCase(element) { this.removeBackground(element); const child = this.document.createElement('img'); child.src = ERROR_ICON; const parent = this.renderer.parentNode(this.elRef.nativeElement); this.renderer.appendChild(parent, child); setTimeout(() => { this.renderer.removeChild(parent, child); }, 1000); }
Здесь директива добавляет сиблинга своему элементу, проходя через родителя. У директив не может быть своих стилей, поэтому позиционировать новый элемент будет сложно.
Мы также не знаем о стилях родителя, возможно, еще один ребенок сломает верстку. И, наконец, мы даже не знаем, существует ли родитель. Если ваша директива лежит в контенте другого компонента, есть шанс, что она отрендерится до прикрепления к DOM. Поэтому золотое правило гласит: в директивах оставайтесь в рамках их элемента.
Избегайте ручной работы с DOM
Мне понятно желание минимизировать вложенность. Вместо прикрепления иконок как картинок мы могли бы менять фон элемента через CSS и помещать иконки в нужное место. Это полезно для UX, так как картинки не блокируют клики. Но ручная работа с DOM — плохая затея по ряду причин:
-
Это угроза кросс-платформенности.
-
Это потенциально небезопасно, так как мы минуем санитайзер.
-
Оптимизация становится нашей ответственностью.
-
Это не по-Ангуляровски.
Мы могли бы применить @HostBinding
для задания стилей. Но их непросто использовать с Observable
(см. мою статью по теме) и OnPush-проверкой изменений. Кроме того, все будет гораздо прозрачнее, если у нас будет шаблон со всеми нужными элементами. Так что вместо запихивания всего в один элемент давайте сделаем небольшую обертку:
<autosave-select> <select>...</select> </autosave-select>
Пишите асинхронные действия декларативно
Обычно мы прибегаем к RxJS для асинхронных операций. Директива из статьи тоже начинает с RxJS, но быстро переходит к императивным манипуляциям и setTimeout
. Все это можно решить, не покидая реактивный мир. Я очень рекомендую всем вкладываться в изучение RxJS. Это один из самых мощных инструментов в экосистеме Angular.
Подборка задачек для тренировки RxJS от нас с Ромой
В данном случае мы можем сделать нехитрую цепочку операторов, которая обработает все за нас. Еще мы избавимся от необходимости ручной подписки. С async
-пайпом в конце мы сможем подключить OnPush и забыть про отписки.
Передача методов через инпут — сомнительная затея
Я не против чистых функций в инпутах. Мы часто используем их в Taiga UI, например методы преобразования <T>
в строку или проверку на состояние disabled
. Но в этом случае она выглядит странно.
Обычно инпуты используют для переменных значений. Для статичных данных я предпочитаю Dependency Injection. Директива задумана самодостаточной, но работа с сервером все равно скинута на родительский компонент, когда мы передаем метод через инпут. С помощью DI можно добавить уровень абстракции:
export abstract class SaveService<T> { abstract save(value: T): Observable<unknown>; }
Теперь реально передавать реализации через сервисы или директивы на случай нескольких таких компонентов на странице.
Рефактор
Теперь давайте сделаем компонент с той же задачей. Для начала мы запустим поток при выборе. Это может быть fromEvent
или Subject
+ @HostListener
:
export class AutosaveSelectComponent<T> { private readonly change$ = new Subject<T>(); @HostListener('change', ['$event.target.value']) onChange(value: T) { this.change$.next(value); } }
Затем создадим Observable
состояния, отвечающий за индикацию:
readonly state$ = this.change$.pipe( switchMap(value => this.service.save(value).pipe( switchMapTo( timer(3000).pipe( mapTo(null), startWith(State.Success) ) ), startWith(State.Loading), catchError(() => of(State.Error)) ) ), );
На каждое изменение он стартует в состоянии загрузки. При ошибке он сбрасывается на состояние ошибки, которое остается висеть. При успешном значении из нашего метода сохранения мы переключимся на таймер, чтобы показать галочку на 3 секунды.
Осталось слегка приправить CSS’ом:
<ng-content></ng-content> <ng-container [ngSwitch]="state$ | async"> <img *ngSwitchCase="state.Loading" class="icon icon_loading" alt="" src="loading.png" /> <img *ngSwitchCase="state.Success" class="icon" alt="" src="success.png" /> <img *ngSwitchCase="state.Error" class="icon" alt="" src="error.png" /> </ng-container>
Рабочий пример с использованием нескольких сервисов смотрите на StackBlitz.
ссылка на оригинал статьи https://habr.com/ru/articles/582738/
Добавить комментарий