Упрощаем работу с Angular с помощью @taiga-ui/cdk: 5 наших лучших практик

d1822747ba40e7296d75ebbca9d4c456.png

CDK — базовый пакет библиотеки компонентов Taiga UI. Он не имеет никакой привязки к визуальной составляющей библиотеки, а скорее служит набором полезных инструментов для упрощения создания Angular-приложений.

Среди всех этих инструментов я выделил мою пятерку фаворитов. Я использую их во всех своих проектах и уже давно не представляю, как писать на Angular без них, потому что они ежедневно экономят мне массу времени.

Дисклеймер о весе библиотеки

Перед написанием статьи хотелось бы ответить на вопрос: «Для чего тащить в бандл целый мультитул, когда мне нужна пара функций?»

По результатам bundlephobia мы получим следующую картинку:

19bf88a3b7c5d8057cd61cc17fc2cd69.png

23 КБ — результат не самый страшный, но и не очень приятный. Но все сущности наших библиотек лежат в отдельных Secondary Entry Point, что делает их полностью tree shakable. Это значит, что такой объем в бандле мы получим только в случае импорта и использования всех сущностей библиотеки в нашем приложении. Если вы импортите пару сущностей — только они и попадут к вам в бандл, добавив к нему в результате меньше 1 КБ.

tuiPure — продвинутая мемоизация вычислений

Это декоратор, который можно вешать на геттеры и чистые методы классов. Давайте разберем оба сценария.

Как геттер

Можно повесить декоратор tuiPure на геттер. В таком случае он позволяет сделать отложенные вычисления. 

Пример 1. Скрываем и показываем сороковое число Фибоначчи, когда пользователь нажимает на кнопку

// template
fibonacci(40) = {{ fibonacci40 }}
// component @tuiPure get fibonacci40(): number { return calculateFibonacci(40); }

Когда мы запросим число в первый раз, функция посчитает сороковой элемент Фибоначчи. Все последующие запросы будут сразу возвращать ранее посчитанное число.

Пример 2. У нас есть компонент pull to refresh, который эмулирует поведение под iOS и Android. Один из его стримов вызывается только для Андроида, а для iOS — нет. Завернем его в getter с pure, и ненужный Observable не будет создан для iOS.




   
@tuiPure
get loaderTransform$(): Observable {
    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);
}

@tuiPure
private computeFilteredItems(items: readonly string[]): readonly string[] {
   return items.filter(someCondition);
}

В этом случае можно вызвать геттер из шаблона, один раз отфильтровать items, и он будет возвращать тот же самый массив до тех пор, пока this.itemsне изменится. Это поможет избежать лишних операций пересоздания массива на каждый чих проверки изменений, а также проблем из-за постоянных изменений ссылки на массив при передаче дальше. При этом нам не придется, например, самим синхронизировать состояние в ngOnChanges, если this.items— инпут компонента.

Документация по tuiPure

*tuiLet

Это простая структурная директива для объявления локальных переменных в шаблонах.


   

Timer value: {{time}}

It can be used many times:

It subsribed once and async pipe unsubsribes it after component destroy

Вместо *tuiLetможно использовать *ngIf если вам не нужно показывать шаблон при falsy-значении (или если оно не предусмотрено). Но если вы работаете, например, с числами, то 0, скорее всего, является вполне адекватным значением. Тут и поможет *tuiLet 

Документация по tuiLet

Метапайпы tuiMapper и tuiFilter

Мы создали пайп, чтобы не создавать другие пайпы, — tuiMapper.

Он очень простой. Это pure-пайп, который принимает в себя чистую функцию для преобразования и произвольное количество аргументов к ней. В шаблоне это выглядит так:

{{value | tuiMapper : mapper : arg1 : arg2 }}

Также удобно и преобразовывать данные для инпутов компонентов в шаблоне или использовать через *ngIf / *tuiLet:

Добавление цветных маркеров-точек в календарях @taiga-ui/core

Чистые пайпы кэшируют предыдущее значение и не пересчитываются на каждый цикл проверки изменений, что полностью решает проблемы производительности. А такая передача чистых функций извне позволяет орудовать хендлерами, подменять их или даже передавать в компонент извне. Кроме того, нам не нужно создавать отдельный пайп под каждый кейс, если он нужен нам лишь в одном месте.

А еще у нас есть tuiFilter для удобства фильтрации массивов. Фактически это частный случай маппера, но нам нужен довольно часто. Поскольку это чистый пайп, то никаких проблем с производительностью из-за пересоздания массивов тут нет.

Документация на mapper / документация на filter

destroy$

Это Observable-based сервис, который упрощает процесс отписки в компонентах и директивах.

@Component({
   // ...
   providers: [TuiDestroyService],
})
export class TuiDestroyExample {
   constructor(
     @Inject(TuiDestroyService) 
     private readonly destroy$: Observable
   ) {}

   // …
   subscribeSomething() {
       fromEvent(this.element, 'click')
           .pipe(takeUntil(this.destroy$))
           .subscribe(() => {
               console.log('click');
           });
   }
}

Все что нам нужно — добавить его в providers компонента и заинжектить в конструкторе. Я предпочитаю писать типы сущностей из DI, которые минимально необходимы в компоненте. Здесь это Observable. Но можно писать и покороче:

constructor(private destroy$: TuiDestroyService) {}

Кстати, сервис в такой ситуации привязывается не к лайфсайклу компонента, а к его DI Injector«у. Это может помочь в ситуации, когда нужно подписаться в сервисе или внутри DI фабрики. Такие кейсы довольно редки, но зато TuiDestroyService в них буквально спасает — например, когда мы хотели дергать markForCheck из фабрики токена в статье о DI фокусах для проброса данных. 

Ссылка на документацию

Плагины ng-event-plugins

Фактически это внешняя библиотека ng-event-plugins, которая поставляется вместе с cdk (прямая зависимость, которую не нужно устанавливать отдельно). Она добавляет свои обработчики к менеджеру плагинов Angular. В ней есть несколько очень полезных плагинов, которые добавляют ряд возможностей в шаблоны компонентов.

Например, .stopи .preventпозволяют декларативно делать stopPropagation и preventDefault на любой прилетающий ивент.

Было:


    Choose date
export class SomeComponent {
   // …
   handle(event: MouseEvent) {
      event.preventDefault();
      event.stopPropagation();

      this.onMouseDown(event);
   }
}

Стало:


    Choose date

Или модификатор .silent который позволяет не запускать проверку изменений на событие:

Callbacks to mousemove will not trigger change detection

Можно отслеживать ивенты в capture-фазе с помощью .capture:

Clicks will be stopped before reaching this DIV

Все это работает и с @HostListener«ами, и с кастомными событиями. Вы можете почитать подробнее в документации ng-event-plugins.

Итого

Мы посмотрели ряд сущностей пакета @taiga-ui/cdk. Надеюсь, какие-нибудь из них вам приглянулись и тоже будут помогать во всех дальнейших проектах!

Кстати, у меня еще есть статья про саму библиотеку Taiga UI, в которой описаны остальные пакеты и общая философия библиотеки.

© Habrahabr.ru