CDK базовый пакет библиотеки компонентов Taiga UI. Он не имеет никакой привязки к визуальной составляющей библиотеки, а скорее служит набором полезных инструментов для упрощения создания Angular-приложений.
Среди всех этих инструментов я выделил мою пятерку фаворитов. Я использую их во всех своих проектах и уже давно не представляю, как писать на Angular без них, потому что они ежедневно экономят мне массу времени.
Дисклеймер о весе библиотеки
Перед написанием статьи хотелось бы ответить на вопрос: Для чего тащить в бандл целый мультитул, когда мне нужна пара функций?
По результатам bundlephobia мы получим следующую картинку:
23 КБ результат не самый страшный, но и не очень приятный. Но все сущности наших библиотек лежат в отдельных Secondary Entry Point, что делает их полностью tree shakable. Это значит, что такой объем в бандле мы получим только в случае импорта и использования всех сущностей библиотеки в нашем приложении. Если вы импортите пару сущностей только они и попадут к вам в бандл, добавив к нему в результате меньше 1 КБ.
tuiPure продвинутая мемоизация вычислений
Это декоратор, который можно вешать на геттеры и чистые методы классов. Давайте разберем оба сценария.
Как геттер
Можно повесить декоратор tuiPure на геттер. В таком случае он позволяет сделать отложенные вычисления.
Пример 1. Скрываем и показываем сороковое число Фибоначчи, когда пользователь нажимает на кнопку
// template<div *ngIf="show">fibonacci(40) = {{ fibonacci40 }}</div>// component@tuiPureget fibonacci40(): number { return calculateFibonacci(40);}
Когда мы запросим число в первый раз, функция посчитает сороковой элемент Фибоначчи. Все последующие запросы будут сразу возвращать ранее посчитанное число.
Пример 2. У нас есть компонент pull to refresh, который эмулирует поведение под iOS и Android. Один из его стримов вызывается только для Андроида, а для iOS нет. Завернем его в getter с pure, и ненужный Observable не будет создан для iOS.
<tui-mobile-ios-loader *ngIf="isIOS; else angroidLoader"></tui-mobile-ios-loader><ng-template #angroidLoader> <tui-mobile-android-loader [style.transform]="loaderTransform$ | async" ></tui-mobile-android-loader></ng-template>
@tuiPureget loaderTransform$(): Observable<string> { return this.pulling$.pipe( map(distance => translateY(Math.min(distance, ANDROID_MAX_DISTANCE))), );}
Также можно обратиться к changes от ContentChild / ContentChildren: если мы вызываем такой геттер из шаблона, то уже можем быть уверены, что content готов. При соблюдении порядка также это можно провернуть и с ViewChild / ViewChildren.
Как метод
На метод тоже можно повесить декоратор @tuiPure. Тогда он будет работать следующим образом: при первом вызове метода посчитает значение и вернет его. Все последующие вызовы с теми же самыми аргументами будут возвращать уже посчитанный результат. Если аргумент изменится результат пересчитается.
Это полезно не только для сложных вычислений, но и в связке с геттером для создания непримитивов объектов или массивов.
get filteredItems(): readonly string[] { return this.computeFilteredItems(this.items);}@tuiPureprivate computeFilteredItems(items: readonly string[]): readonly string[] { return items.filter(someCondition);}
В этом случае можно вызвать геттер из шаблона, один раз
отфильтровать items, и он будет возвращать тот же самый массив до
тех пор, пока this.items
не изменится. Это поможет
избежать лишних операций пересоздания массива на каждый чих
проверки изменений, а также проблем из-за постоянных изменений
ссылки на массив при передаче дальше. При этом нам не придется,
например, самим синхронизировать состояние в ngOnChanges, если
this.items
инпут компонента.
*tuiLet
Это простая структурная директива для объявления локальных переменных в шаблонах.
<ng-container *tuiLet="timer$ | async as time"> <p>Timer value: {{time}}</p> <p> It can be used many times: <tui-badge [value]="time"></tui-badge> </p> <p> It subsribed once and async pipe unsubsribes it after component destroy </p></ng-container>
Вместо *tuiLet
можно использовать *ngIf
если вам не нужно показывать шаблон при falsy-значении (или если
оно не предусмотрено). Но если вы работаете, например, с числами,
то 0, скорее всего, является вполне адекватным значением. Тут и
поможет *tuiLet
Метапайпы tuiMapper и tuiFilter
Мы создали пайп, чтобы не создавать другие пайпы, tuiMapper.
Он очень простой. Это pure-пайп, который принимает в себя чистую функцию для преобразования и произвольное количество аргументов к ней. В шаблоне это выглядит так:
{{value | tuiMapper : mapper : arg1 : arg2 }}
Также удобно и преобразовывать данные для инпутов компонентов в
шаблоне или использовать через *ngIf
/
*tuiLet
:
<div *ngIf="item | tuiMapper : toMarkers : itemIsToday(item) : !!getItemRange(item) as markers" class="dots"> <div class="dot" [tuiBackground]="markers[0]"></div> <div *ngIf="markers.length > 1" class="dot" [tuiBackground]="markers[1]" ></div></div>
Добавление цветных маркеров-точек в календарях @taiga-ui/core
Чистые пайпы кэшируют предыдущее значение и не пересчитываются на каждый цикл проверки изменений, что полностью решает проблемы производительности. А такая передача чистых функций извне позволяет орудовать хендлерами, подменять их или даже передавать в компонент извне. Кроме того, нам не нужно создавать отдельный пайп под каждый кейс, если он нужен нам лишь в одном месте.
А еще у нас есть tuiFilter для удобства фильтрации массивов. Фактически это частный случай маппера, но нам нужен довольно часто. Поскольку это чистый пайп, то никаких проблем с производительностью из-за пересоздания массивов тут нет.
Документация на mapper / документация на filter
destroy$
Это Observable-based сервис, который упрощает процесс отписки в компонентах и директивах.
@Component({ // ... providers: [TuiDestroyService],})export class TuiDestroyExample { constructor( @Inject(TuiDestroyService) private readonly destroy$: Observable<void> ) {} // subscribeSomething() { fromEvent(this.element, 'click') .pipe(takeUntil(this.destroy$)) .subscribe(() => { console.log('click'); }); }}
Все что нам нужно добавить его в providers компонента и заинжектить в конструкторе. Я предпочитаю писать типы сущностей из DI, которые минимально необходимы в компоненте. Здесь это Observable<void>. Но можно писать и покороче:
constructor(private destroy$: TuiDestroyService) {}
Кстати, сервис в такой ситуации привязывается не к лайфсайклу компонента, а к его DI Injectorу. Это может помочь в ситуации, когда нужно подписаться в сервисе или внутри DI фабрики. Такие кейсы довольно редки, но зато TuiDestroyService в них буквально спасает например, когда мы хотели дергать markForCheck из фабрики токена в статье о DI фокусах для проброса данных.
Плагины ng-event-plugins
Фактически это внешняя библиотека ng-event-plugins, которая поставляется вместе с cdk (прямая зависимость, которую не нужно устанавливать отдельно). Она добавляет свои обработчики к менеджеру плагинов Angular. В ней есть несколько очень полезных плагинов, которые добавляют ряд возможностей в шаблоны компонентов.
Например, .stop
и .prevent
позволяют
декларативно делать stopPropagation и preventDefault на любой
прилетающий ивент.
Было:
<some-input (mousedown)="handle($event)"> Choose date</some-input>
export class SomeComponent { // handle(event: MouseEvent) { event.preventDefault(); event.stopPropagation(); this.onMouseDown(event); }}
Стало:
<some-input (mousedown.prevent.stop)="onMouseDown()"> Choose date</some-input>
Или модификатор .silent
который позволяет не
запускать проверку изменений на событие:
<div (mousemove.silent)="onMouseMove()"> Callbacks to mousemove will not trigger change detection</div>
Можно отслеживать ивенты в capture-фазе с помощью
.capture
:
<div (click.capture.stop)="onClick()"> <div (click)="never()">Clicks will be stopped before reaching this DIV</div></div>
Все это работает и с @HostListenerами, и с кастомными событиями. Вы можете почитать подробнее в документации ng-event-plugins.
Итого
Мы посмотрели ряд сущностей пакета @taiga-ui/cdk. Надеюсь, какие-нибудь из них вам приглянулись и тоже будут помогать во всех дальнейших проектах!
Кстати, у меня еще есть статья про саму библиотеку Taiga UI, в которой описаны остальные пакеты и общая философия библиотеки.