[Разработка веб-сайтов, Open source, Angular, TypeScript] Учим HostBinding работать с Observable
Автор
Сообщение
news_bot ®
Стаж: 6 лет 9 месяцев
Сообщений: 27286
Как и многие другие Angular-разработчики, я мирился с одним ограничением. Если мы хотим использовать Observable в шаблоне, мы можем взять знакомый всем async пайп:
<button [disabled]=”isLoading$ | async”>
Но его нельзя применить к @HostBinding. Давным-давно это было возможно по ошибке, но это быстро исправили:
@Directive({
selector: 'button[my-button]'
host: {
'[disabled]': '(isLoading$ | async)'
}
})
export class MyButtonDirective {
Все потому, что хост байндинг относится к родительскому view и в нем этот пайп может быть не подключен. Это довольно желанная фича. Давайте посмотрим, как мы можем ее реализовать, пока нет официального решения.Как работает асинхронный байндинг?Байндинг работает с обычными данными. Когда у нас есть Observable — на этом этапе необходимо покинуть реактивный мир. Нам нужно подписаться на поток, запускать проверку изменения по каждому значению и отписаться, когда поток больше не нужен. Примерно это за нас и делает async пайп. И это то, что ложится на наши плечи, когда мы хотим забайндить какие-то реактивные данные на хост. Зачем это может понадобиться?Мы часто работаем с RxJS в Angular. Большинство наших сервисов построены на Observable-модели. Вот пара примеров, где возможность завязываться на реактивные данные в @HostBinding была бы полезна:
- Перевод атрибутов на другой язык. Если мы хотим сделать динамическое переключение языка в приложении — мы будем использовать Observable. При этом обновлять ARIA-атрибуты, title или alt для изображений довольно непросто.
- Изменение класса или стилей. Observable-сервис может управлять размером или трансформацией через изменение стилей хоста. Или, например, мы можем использовать реактивный IntersectionObserver для применения класса к sticky-шапке в таблице:
- Изменение полей и атрибутов. Иногда мы хотим завязаться на BreakpointObserver для обновления placeholder или на сервис загрузки данных для выставления disable на кнопке.
- Произвольные строковые данные, хранимые в data-атрибутах. В моей практике для них тоже иногда используются Observable-сервисы.
В Taiga UI — библиотеке, над которой я работаю, — есть несколько инструментов, чтобы сделать этот процесс максимально декларативным:
import {TuiDestroyService, watch} from '@taiga-ui/cdk';
import {Language, TUI_LANGUAGE} from '@taiga-ui/i18n';
import {Observable} from 'rxjs';
import {map, takeUntil} from 'rxjs/operators';
@Component({
selector: 'my-comp',
template: '',
providers: [TuiDestroyService],
})
export class MyComponent {
@HostBinding('attr.aria-label')
label = '';
constructor(
@Inject(TUI_LANGUAGE) language$: Observable<Language>,
@Inject(TuiDestroyService) destroy$: Observable<void>,
@Inject(ChangeDetectorRef) changeDetectorRef: ChangeDetectorRef,
) {
language$.pipe(
map(getTranslation('label')),
watch(changeDetectorRef),
takeUntil(destroy$),
).subscribe();
}
}
Как видите, тут довольно много кода просто чтобы реализовать байндинг одного значения. Как было бы здорово, если бы можно было просто написать:
@HostBinding('attr.aria-label')
readonly label$ = this.translations.get$('label');
Это потребует серьезных доработок со стороны команды Angular. Но мы можем использовать хитрый трюк в рамках публичного API, чтобы заставить это работать!Event-плагины спешат на помощь!Мы не можем добавить свою логику к байндингу на хост. Но мы можем сделать это для @HostListener! Я уже писал статью на эту тему. Прочитайте ее, если хотите узнать, как добавить декларативные preventDefault/stopPropagation и оптимизировать циклы проверки изменений. Если кратко — Angular позволяет добавлять свои сервисы для обработки событий. Подходящий сервис выбирается с помощью имени события. Давайте перепишем код следующим образом:
@HostBinding('$.aria-label.attr')
@HostListener('$.aria-label.attr')
readonly label$ = this.translations.get$('label');
Выглядит странно — пытаться решить задачу @HostBinding через @HostListener но читайте дальше и вы всё увидите.
Мы будем использовать $ в качестве индикатора в имени события. Модификатор .attr добавим в конец, а не в начало. Иначе регулярное выражение в Angular решит, что мы байндим строковый атрибут.
У плагинов для обработки событий есть доступ к элементу, имени события и функции-обработчику. Последний аргумент для нас бесполезен, так как это обертка, созданная компилятором. Так что нам нужно как-то передать наш Observable через элемент. Вот тут-то нам и пригодится @HostBinding. Мы положим Observable в поле с тем же именем, и тогда у нас будет доступ к нему внутри плагина:
addEventListener(element: HTMLElement, event: string): Function {
element[event] = EMPTY;
const method = this.getMethod(element, event);
const sub = this.manager
.getZone()
.onStable.pipe(
take(1),
switchMap(() => element[event]),
)
.subscribe(method);
return () => sub.unsubscribe();
}
Компилятор AngularПосмотрим на этот код повнимательнее. Первая строка может вас смутить. Хоть мы и можем назначать произвольные поля на элементы, Angular попытается их провалидировать:
Возможно, вы видели такое раньшеПлагины хороши тем, что подписка на события происходит раньше разрешения байндингов. Благодаря первой строке Angular считает, что у элемента присутствует это свойство. Дальше нам нужно убедиться, что Observable уже на месте — ведь на момент подписки его еще нет. Хорошо, что у нас есть доступ до NgZone и мы можем дождаться ее стабилизации, прежде чем запросить свойство элемента.
NgZone испускает onStable когда не осталось больше микро- и макрозадач в очереди. Для нас это означает, что Angular завершил цикл проверки изменений и все байндинги обновлены.
А отписку за нас сделает сам Angular — достаточно вернуть функцию, прерывающую стрим.Этого хватит, чтобы код заработал в JIT, AOT же более щепетилен. Мы добавили несуществующее поле во время выполнения, но AOT желает знать про него на этапе компиляции. До тех пор, пока эта задача не будет закрыта, мы не можем создавать свои списки разрешенных полей. Поэтому нам придется добавить NO_ERRORS_SCHEMA в модуль с подобным байндингом. Это может звучать страшно, но все, что эта схема делает, — перестает проверять, есть ли поле у элемента при байндинге. Кроме того, если у вас WebStorm, вы продолжите видеть предупреждение:
Это сообщение не мешает сборкеТакже AOT требует реализации Callable-интерфейса для использования @HostListener. Мы можем имитировать его с помощью простой функции, сохранив оригинальный тип:
function asCallable<T>(a: T): T & Function {
return a as any;
}
Итоговая запись:
@HostBinding('$.aria-label.attr')
@HostListener('$.aria-label.attr')
readonly label$ = asCallable(this.translations.get$('label'));
Другой вариант — вовсе отказаться от @HostBinding ведь нам надо назначить его лишь один раз. Если ваш стрим приходит из DI, что происходит довольно часто, можно создать FactoryProvider. В него можно передать ElementRef и назначить поле в нем:
export const TOKEN = new InjectionToken<Observable<boolean>>("");
export const PROVIDER = {
provide: TOKEN,
deps: [ElementRef, IntersectionObserverService],
useFactory: factory,
}
export function factory(
{ nativeElement }: ElementRef,
entries$: Observable<IntersectionObserverEntry[]>
): Observable<boolean> {
return nativeElement["$.class.stuck"] = entries$.pipe(map(isIntersecting));
}
Теперь достаточно будет оставить только @HostListener. Его даже можно написать прямо в декораторе класса:
@Directive({
selector: "table[sticky]",
providers: [
IntersectionObserverService,
PROVIDER,
],
host: {
"($.class.stuck)": "stuck$"
}
})
export class StickyDirective {
constructor(@Inject(TOKEN) readonly stuck$: Observable<boolean>) {}
}
Приведенный выше пример можно увидеть вживую на StackBlitz. В нем IntersectionObserver используется для задания тени на sticky-шапке таблицы:angular-async-hostbinding - StackBlitzstackblitz.comОбновление полейВ коде вы видели вызов getMethod. Байндинг в Angular работает не только на атрибутах и полях, но и на классах и стилях. Нам тоже нужно реализовать такую возможность. Для этого разберем имя нашего псевдособытия, чтобы понять, что же делать со значениями из потока:
private getMethod(element: HTMLElement, event: string): Function {
const [, key, value, unit = ''] = event.split('.');
if (event.endsWith('.attr')) {
return v => element.setAttribute(key, String(v));
}
if (key === 'class') {
return v => element.classList.toggle(value, !!v);
}
if (key === 'style') {
return v => element.style.setProperty(value, `${v}${unit}`);
}
return v => (element[key] = v);
}
Никакой мудреной логики. На этом все, осталось только зарегистрировать плагин в глобальных провайдерах:
{
provide: EVENT_MANAGER_PLUGINS,
useClass: BindEventPlugin,
multi: true,
}
Это небольшое дополнение способно существенно упростить ваш код. Нам больше не надо беспокоиться о подписке. Описанный плагин доступен в новой версии 2.1.1 нашей библиотеки @tinkoff/ng-event-plugins, а также в @taiga-ui/cdk. Поиграться с кодом можно на StackBlitz. Надеюсь, этот материал будет для вас полезным!Извините, данный ресурс не поддреживается. :(
===========
Источник:
habr.com
===========
Похожие новости:
- [Open source, Смартфоны, Софт] Мануал по настройке стандартного эквалайзера Android для самых маленьких (и не только)
- [Разработка веб-сайтов, Будущее здесь, Визуальное программирование] «Тильда для ресторанов» на Bubble без кода
- [JavaScript, ReactJS, Карьера в IT-индустрии, TypeScript] Яндекс.Практикум запустил курс «React-разработчик»
- [Разработка мобильных приложений] Как выбрать мобильную кросс-платформу в 2021 году (перевод)
- [Open source, Git, Agile, DevOps] Bебинар — Автоматизация процессов с GitLab CI/CD
- [Разработка веб-сайтов, PHP, Программирование, Проектирование и рефакторинг] Run, config, run: как мы ускорили деплой конфигов в Badoo
- [Разработка веб-сайтов, JavaScript, Программирование, ReactJS] Разрабатываем чат на React с использованием Socket.IO
- [Git, Системы управления версиями, Системы сборки, DevOps] Вышел релиз GitLab 13.8 с редактором конвейеров и первой из метрик DORA
- [CMS, Разработка веб-сайтов, Антивирусная защита, Программирование, 1С] Сказ о том, как я с гидрой боролся
- [Open source, Виртуализация, Карьера в IT-индустрии, Openshift] Поваренная книга Quarkus Cookbook, бесплатный Developer Sandbox for OpenShift и руководство CentOS Project
Теги для поиска: #_razrabotka_vebsajtov (Разработка веб-сайтов), #_open_source, #_angular, #_typescript, #_angular, #_decorator, #_binding, #_rxjs, #_observable, #_blog_kompanii_tinkoff (
Блог компании TINKOFF
), #_razrabotka_vebsajtov (
Разработка веб-сайтов
), #_open_source, #_angular, #_typescript
Вы не можете начинать темы
Вы не можете отвечать на сообщения
Вы не можете редактировать свои сообщения
Вы не можете удалять свои сообщения
Вы не можете голосовать в опросах
Вы не можете прикреплять файлы к сообщениям
Вы не можете скачивать файлы
Текущее время: 22-Ноя 18:31
Часовой пояс: UTC + 5
Автор | Сообщение |
---|---|
news_bot ®
Стаж: 6 лет 9 месяцев |
|
Как и многие другие Angular-разработчики, я мирился с одним ограничением. Если мы хотим использовать Observable в шаблоне, мы можем взять знакомый всем async пайп: <button [disabled]=”isLoading$ | async”>
@Directive({
selector: 'button[my-button]' host: { '[disabled]': '(isLoading$ | async)' } }) export class MyButtonDirective {
import {TuiDestroyService, watch} from '@taiga-ui/cdk';
import {Language, TUI_LANGUAGE} from '@taiga-ui/i18n'; import {Observable} from 'rxjs'; import {map, takeUntil} from 'rxjs/operators'; @Component({ selector: 'my-comp', template: '', providers: [TuiDestroyService], }) export class MyComponent { @HostBinding('attr.aria-label') label = ''; constructor( @Inject(TUI_LANGUAGE) language$: Observable<Language>, @Inject(TuiDestroyService) destroy$: Observable<void>, @Inject(ChangeDetectorRef) changeDetectorRef: ChangeDetectorRef, ) { language$.pipe( map(getTranslation('label')), watch(changeDetectorRef), takeUntil(destroy$), ).subscribe(); } } @HostBinding('attr.aria-label')
readonly label$ = this.translations.get$('label'); @HostBinding('$.aria-label.attr')
@HostListener('$.aria-label.attr') readonly label$ = this.translations.get$('label'); Мы будем использовать $ в качестве индикатора в имени события. Модификатор .attr добавим в конец, а не в начало. Иначе регулярное выражение в Angular решит, что мы байндим строковый атрибут.
addEventListener(element: HTMLElement, event: string): Function {
element[event] = EMPTY; const method = this.getMethod(element, event); const sub = this.manager .getZone() .onStable.pipe( take(1), switchMap(() => element[event]), ) .subscribe(method); return () => sub.unsubscribe(); } Возможно, вы видели такое раньшеПлагины хороши тем, что подписка на события происходит раньше разрешения байндингов. Благодаря первой строке Angular считает, что у элемента присутствует это свойство. Дальше нам нужно убедиться, что Observable уже на месте — ведь на момент подписки его еще нет. Хорошо, что у нас есть доступ до NgZone и мы можем дождаться ее стабилизации, прежде чем запросить свойство элемента. NgZone испускает onStable когда не осталось больше микро- и макрозадач в очереди. Для нас это означает, что Angular завершил цикл проверки изменений и все байндинги обновлены.
Это сообщение не мешает сборкеТакже AOT требует реализации Callable-интерфейса для использования @HostListener. Мы можем имитировать его с помощью простой функции, сохранив оригинальный тип: function asCallable<T>(a: T): T & Function {
return a as any; } @HostBinding('$.aria-label.attr')
@HostListener('$.aria-label.attr') readonly label$ = asCallable(this.translations.get$('label')); export const TOKEN = new InjectionToken<Observable<boolean>>("");
export const PROVIDER = { provide: TOKEN, deps: [ElementRef, IntersectionObserverService], useFactory: factory, } export function factory( { nativeElement }: ElementRef, entries$: Observable<IntersectionObserverEntry[]> ): Observable<boolean> { return nativeElement["$.class.stuck"] = entries$.pipe(map(isIntersecting)); } @Directive({
selector: "table[sticky]", providers: [ IntersectionObserverService, PROVIDER, ], host: { "($.class.stuck)": "stuck$" } }) export class StickyDirective { constructor(@Inject(TOKEN) readonly stuck$: Observable<boolean>) {} } private getMethod(element: HTMLElement, event: string): Function {
const [, key, value, unit = ''] = event.split('.'); if (event.endsWith('.attr')) { return v => element.setAttribute(key, String(v)); } if (key === 'class') { return v => element.classList.toggle(value, !!v); } if (key === 'style') { return v => element.style.setProperty(value, `${v}${unit}`); } return v => (element[key] = v); } {
provide: EVENT_MANAGER_PLUGINS, useClass: BindEventPlugin, multi: true, } =========== Источник: habr.com =========== Похожие новости:
Блог компании TINKOFF ), #_razrabotka_vebsajtov ( Разработка веб-сайтов ), #_open_source, #_angular, #_typescript |
|
Вы не можете начинать темы
Вы не можете отвечать на сообщения
Вы не можете редактировать свои сообщения
Вы не можете удалять свои сообщения
Вы не можете голосовать в опросах
Вы не можете прикреплять файлы к сообщениям
Вы не можете скачивать файлы
Вы не можете отвечать на сообщения
Вы не можете редактировать свои сообщения
Вы не можете удалять свои сообщения
Вы не можете голосовать в опросах
Вы не можете прикреплять файлы к сообщениям
Вы не можете скачивать файлы
Текущее время: 22-Ноя 18:31
Часовой пояс: UTC + 5