How @taiga-ui/cdk can help you simplify your working with Angular: our 5 best practices
I decided to pick my favorite 5 tools from taiga-ui/cdk and explain how they can in any Angular app

How @taiga-ui/cdk can help you simplify your working with Angular: our 5 best practices
I decided to pick my favorite 5 tools from taiga-ui/cdk and explain how they can in any Angular app


CDK is the basic package of the component library Taiga UI.
It has nothing to do with the visual part of the library (actual UI) but is rather a set of useful tools to simplify working with Angular and to make it faster.
You'll find many useful tools inside and I decided to pick my favorite 5 and write about them here. I use these tools in all my projects and already cannot imagine how to develop Angular apps without them because they save me a lot of time every day.
Disclaimer about package weightLink to this section
You may ask: “Why do I have to add such a big multitool into my application if I need a couple of simple functions?”. Well, here's an answer for you.
Taking a look at bundlephobia we get the following picture:
23 KB is not a terrible result but it is still a considerable size. The thing is, CDK uses Secondary Entry Points (you can also read a great article by Kevin Kreuzer) and hence fully tree-shakable. That means that we’ll get all the 23 KB in our bundle only if we import and use all the entities from the package. If we need two functions, they will be the only ones in our final bundle and they will add less than 1 KB to it.
tuiPure is an advanced memoizationLink to this section
This is a decorator that can be used with getters and pure class methods. Let’s see both situations:
As a getterLink to this section
We can add `tuiPure` decorator to a getter. In this case we can make a lazy calculation. If a user does not press a button to make “show” property true
, getter will never be calculated.
Example #1: showing and hiding 40th Fibonacci number when user clicks a button.
<>Copy// template <div *ngIf="show">fibonacci(40) = {{ fibonacci40 }}</div> // component @tuiPure get fibonacci40(): number { return calculateFibonacci(40); }
When the user asks for a number the first time, the function calculates the 40th Fibonacci element with a method. All next requests will return the previously calculated number.
Example #2: we have a Pull-To-Refresh component that emulates native iOS and Android behavior. One of its streams should be called on Android only and not for iOS. If we just create a stream in a constructor, it will be made on both OS. Let’s wrap it with tuiPure getter and this way we’ll not create an unused Observable on iOS.
<>Copy<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> @tuiPure get loaderTransform$(): Observable<string> { return this.pulling$.pipe( map(distance => translateY(Math.min(distance, ANDROID_MAX_DISTANCE))), ); }
Or we can also use “changes” of ContentChild / ContentChildren. If we call such a getter from a template, we can be sure that all the content is ready because Angular renders all the content before its view. And taking care of the order, we can also do it with ViewChild / ViewChildren.
As a methodLink to this section
@tuiPure can also be used with a method to add memoization. In this case it will calculate the value after the first method call. All the following calls will be returned with the same result immediately until one of the arguments changes. If it changes, the result will be recalculated.
This is very useful for compliant math calculations or in combination with a getter to create non-primitive values like objects and arrays.
<>Copyget filteredItems(): readonly string[] { return this.computeFilteredItems(this.items); } @tuiPure private computeFilteredItems(items: readonly string[]): readonly string[] { return items.filter(someCondition); }
When we call the getter from our template, the pure method filters an items array once and returns the same filtered array until this.items
changes. It helps to avoid repeating recreation of new arrays on every cycle of change detection and solve potential problems with different references to an array that we bind in the template. Using this pattern, we do not need to manually sync filtered array in ngOnChanges if “items” are an input of component.
It is a function that patches getter’s or method’s TypedPropertyDescriptor and returns its new version with wrapped memoization. See the implementation to explore it in more detail. It can be also easily extended with memoization of all values, not only the last one.
*tuiLetLink to this section
This is a simple strutural directive that allows to define local variables in templates:
<>Copy<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>
You can also use *ngIf
instead of *tuiLet
if you do not need to show falsy values (or if it is not applicable). But if we work with numbers, for example, zero is most likely one of the supported values. This is a case for *tuiLet
because it will show zero while *ngIf
will hide the element.
Here you can find the implementation. It is a good simple example of how to make a structural directive.
Metapipes tuiMapper and tuiFilterLink to this section
We created a pipe to avoid creating other pipes and it is called tuiMapper.
It is a pure pipe that takes a pure transformation function and any number of arguments for it. So, we get the following template:
<>Copy{{value | tuiMapper : mapper : arg1 : arg2 }}
It is also convenient to transform data for inputs of other components in template or use them with *ngIf
/ *tuiLet
:
<>Copy<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>
Pure pipes cache previous values and do not recalculate every cycle of change detection that solves all potential performance issues.
So, we can operate with handlers (pure functions that transform value), change them or even pass as an input of component from the outside. We do not need to create a new pipe for each case if we need it once.
We also have a tuiFilter pipe for array filtering. It is actually a special case of mapper but we need it quite often. This is a pure pipe, so there are no issues with performance because pure pipes cache calculated values and do not recreate arrays or objects for every change detection cycle.
mapper documentation / filter documentation
And here you can find tuiMapper and tuiFilter implementations that are actually pure pipes with well-typed transform methods.
destroy$Link to this section
It is an Observable-based service that simplifies the process of unsubscription in components and directives.
<>Copy@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'); }); } }
Everything you need is to add it into “providers” of the component and to inject it in the constructor. I like to write types of DI entities as narrow as possible and it is Observable<void>
here. But it can also be written shorter:
<>Copyconstructor(private destroy$: TuiDestroyService) {}
By the way, such service does not bind to the component lifecycle but rather to the components DI injector. It means that it can even be used when subscribing to a stream inside a service or DI factory. Such cases are rare but TuiDestroyService is very helpful in them because there are no simple alternatives. For example, we call markForCheck from the token factory in the article about DI tricks to provide data through components.
And take a look how simple the implementation is! It is just a subject-based service with “ngDestroy” method.
ng-event-pluginsLink to this section
This is actually another library ng-event-plugins that comes with CDK as a dependency (you do not need to install it by yourself). It adds new handlers to the plugin manager in Angular. There are also several useful built-in plugins that simplify working with events in component templates.
For example, .stop and .prevent allow to call stopPropagation and preventDefault on an event in a declarative way.
Before:
<>Copy<some-input (mousedown)="handle($event)" > Choose date </some-input>
<>Copyexport class SomeComponent { // … handle(event: MouseEvent) { event.preventDefault(); event.stopPropagation(); this.onMouseDown(event); } }
Now:
<>Copy<some-input (mousedown.prevent.stop)="onMouseDown()" > Choose date </some-input>
Or .silent modifier that allows us to prevent change detection after event comes:
<>Copy<div (mousemove.silent)="onMouseMove()"> Callbacks to mousemove will not trigger change detection </div>
We can catch events in a .capture phase:
<>Copy<div (click.capture.stop)="onClick()"> <div (click)="never()"> Clicks will be stopped before reaching this DIV </div> </div>
And all this works also with @HostListener and custom events. You can read about it more in ng-event-plugins documentation or in the detailed article by Alex Inkin.
FinallyLink to this section
We took a look at several entities from @taiga-ui/cdk. I hope that some of them will be useful for you.
By the way, there is also another article about Taiga UI library itself in that I described its other packages and its philosophy.
And do not forget to follow me on Twitter: @marsibarsi. There will be much cool Angular content soon!
Comments (0)
Be the first to leave a comment
About the author

Frontend Developer at Tinkoff.ru

About the author

Frontend Developer at Tinkoff.ru