Работая с Angular волей-неволей будешь использовать RxJS, ведь он лежит в основе фреймворка. Это очень мощный инструмент для обработки событий и не только. Однако далеко не каждый проект использует его по полной. Часто это просто запросы на бэк, нехитрые преобразования данных и подписка. Мы с Ромой очень любим RxJS и решили собрать несколько интересных кейсов из нашей практики. Мы сделали из этого что-то вроде челленджа на 20 задачек, которые мы предлагаем решить с помощью RxJS и попрактиковать свои навыки.

Каждая задачка будет иметь некий бойлерплейт, чтобы вам было просто начать. Под спойлером я положу ссылку на свое решение и небольшое пояснение к нему. В целом задачи будут идти от простого к сложному, а полное собрание с ответами и пояснениями на английском доступно на GitHub.
#1. Создайте Observable для отслеживания фокуса на странице
В этой задаче вам предлагается область страницы со всевозможными фокусируемыми элементами внутри. Нужно сделать стрим, который будет отслеживать фокус внутри этой области.
Решение
Для отслеживания изменения фокуса нам понадобятся события focusin и focusout, поскольку focus/blur не всплывают. Мы будем получать элемент через target и relatedTarget, потому что в момент этих событий мы еще не можем проверить document.activeElement — в коллбэке событий там будет body. Если текущий элемент вне отслеживаемой зоны, будем выдавать null. Так же подобную логику можно вынести в сервис — его потом будет удобно использовать в директивах. Поскольку мы не знаем момент, когда пользователь подпишется на наш стрим, добавим такую конструкцию для получения начального значения при подписке: defer(() => of(documentRef.activeElement)). Остается только собрать все потоки в merge:
@Injectable() export class FocusWithinService extends Observable<Element | null> { constructor( @Inject(DOCUMENT) documentRef: Document, { nativeElement }: ElementRef<HTMLElement> ) { const focusedElement$ = merge( defer(() => of(documentRef.activeElement)), fromEvent(nativeElement, "focusin").pipe(map(({ target }) => target)), fromEvent(nativeElement, "focusout").pipe( map(({ relatedTarget }) => relatedTarget) ) ).pipe( map(element => element && nativeElement.contains(element) ? element : null ), distinctUntilChanged(), ); super(subscriber => focusedElement$.subscribe(subscriber)); } }
#2. Создайте поток видимости вкладки
Хороший пример работы с событиями в RxJS — использование Page Visibility API. Подобный стрим также удобно завернуть в InjectionToken. Бойлерплейта для этой задачи нет
Решение
Тут ситуация довольно тривиальная. Единственный подвох — начальное значение. Если мы сразу проверим, видима страница или нет, это значение может оказаться неактуальным на момент подписки. Можно использовать defer, как в прошлый раз, а можно начать с произвольного значения, а реальное получать в map ниже:
export const PAGE_VISIBILITY = new InjectionToken<Observable<boolean>>( "Shared Observable based on `document visibility changed`", { factory: () => { const documentRef = inject(DOCUMENT); return fromEvent(documentRef, "visibilitychange").pipe( startWith(0), map(() => documentRef.visibilityState !== "hidden"), distinctUntilChanged(), shareReplay() ); } } );
В качестве бонуса в примере мы превратим этот поток в DI-токен для удобного использования. Этот токен есть в нашей микробиблиотеке токенов на глобальные сущности.
#3. Покажите сообщение об ошибке на 5 секунд
Допустим, у нас есть кнопка логина. При нажатии на нее идет запрос на сервер, на это время кнопка будет заблокирована. При успешном логине мы выведем имя пользователя, в противном случае покажем ошибку на 5 секунд и разблокируем кнопку для повторной попытки. В примере ниже заготовлен муляж сервиса логина:
Решение
Давайте посмотрим, как мы можем ветвить потоки, на примере данной задачи. В такой ситуации типовое начало будет Subject, который мы будем дергать по нажатию на кнопку и Observable, который перебрасывает эти нажатия на запрос на сервер:
readonly submit$ = new Subject<void>(); readonly request$ = this.submit$.pipe( switchMapTo(this.service.pipe(startWith(""))), share(), );
Теперь давайте разведем запросы на нужные нам потоки. Обратите внимание на share в конце, который поможет нам избежать повторных запросов на сервер при подписках на эти витки.
При ошибке сервис бросит реальную ошибку в поток. Таким образом, имя пользователя будет просто повторной попыткой запроса:
readonly user$ = this.request$.pipe(retry());
Сообщение об ошибке же будет как раз той ошибкой, которую бросит запрос, показанной в течение 5 секунд:
readonly error$ = this.request$.pipe( ignoreElements(), catchError(e => of(e)), repeat(), switchMap(e => timer(5000).pipe(startWith(e))) );
Мы игнорируем элементы и показывает только ошибки. На этом примере хорошо видна разница между repeat и retry: первый перезапускает поток, который успешно завершился, второй перезапускает поток, закончившийся ошибкой. Аналогичным образом мы можем отвести поток, отвечающий за блокировку кнопки.
#4. Отобразите состояние загрузки в виде полосы прогресса
Если ваш сервис репортит прогресс загрузки, удобно было бы отобразить это для пользователя. Например, тут я прикручивал RxJS и прогресс к нативному fetch:
Попробуем применить похожую технику ветвления стрима для отображения прогресса.
Решение
Начнем мы, как и в прошлый раз, с Subject и общего потока. Общий поток мы разведем на два — прогресс и результат. Для первого мы используем фильтрацию:
readonly progress$ = this.response$.pipe(filter(Number.isFinite));
А для второго — преобразование, чтобы можно было перезапускать процесс:
readonly result$ = this.response$.pipe( map(response => typeof response === "string" ? response : null), distinctUntilChanged() );
#5. Сделайте обратный отсчет с перезапуском
Представьте, что вам надо сделать таймер, ведущий отсчет перед повторной отправкой кода. Отличная микрозадача на RxJS. В качестве бонуса в ответе я приведу еще и решение на CSS, которое позволяет форме подтверждения платежа по SMS в Тинькофф работать даже с отключенным JavaScript
Решение
Для обратного отсчета можно сделать простую утилитную функцию, где мы воспользуемся малоизвестным вторым аргументом takeWhile:
function countdownFrom(start: number): Observable<number> { return timer(0, 1000).pipe( map(index => start - index), takeWhile(Boolean, true) ); }
Благодаря второму аргументу, 0, который нарушит условие, тоже провалится дальше. Сам стрим будет просто использовать switchMapTo от Subject, который мы запускаем по кнопке, как в прошлых примерах. Использовать можно так:
<ng-container *ngIf="countdown$ | async as value else resend"> Resend code in {{ value }} sec. </ng-container> <ng-template #resend> <button (click)="resend$.next()">Resend code</button> </ng-template>
Заключительный 0 не пройдет ngIf и переключит шаблон назад на кнопку.
CSS-решение будет полагаться на анимацию псевдоэлементов и перезапуск ее с помощью :active состояния нажатой кнопки. Загляните в CSS-файл странички с решением:
Заключение
Это была первая неделя нашего челленджа. Впереди вас ждут еще 15 более сложных задач с использованием RxJS. Ссылки будут добавлены ниже по мере публикации.
ссылка на оригинал статьи https://habr.com/ru/company/tinkoff/blog/559346/
Добавить комментарий