Demystifying Taiga UI root component: portals pattern in Angular

Post Editor

Just before new year we announced our new Angular UI kit library Taiga UI. If you go through Getting started steps, you will see that you need to wrap your app with the tui-root component. Let's see what it does and explore what portals are and how and why we use them.

6 min read
post

Demystifying Taiga UI root component: portals pattern in Angular

Just before new year we announced our new Angular UI kit library Taiga UI. If you go through Getting started steps, you will see that you need to wrap your app with the tui-root component. Let's see what it does and explore what portals are and how and why we use them.

post
post
6 min read
6 min read

Just before new year, Roman, my colleague, announced our new Angular UI kit library Taiga UI. If you go through Getting started steps, you will see that you need to wrap your app with the tui-root component. Let's see what it does and explore what portals are and how and why we use them.

What is a portal?Link to this section

Imagine you have a select component. It has a drop-down block with suggestions. If we keep it at the same position in DOM as the hosting component, we will run into all sorts of trouble. Items pop through and containers can chop off content:

Verticality issues are often solved with z-index, effectively starting World War Z in your app. It's not uncommon to see values like 100, 10000, 10001. But even if you manage to get it right — overflow: hidden would still get you there. So what can we do? Instead of having drop-down near its host we can show it in a dedicated container on top of everything. Then your application content can live in its own isolated context eliminating z-index problems. This container is exactly what a portal is. And it is what the Taiga UI root component sets up for you, among other things. Let's look at its template:

<>Copy
<tui-scroll-controls></tui-scroll-controls> <tui-portal-host> <div class="content"><ng-content></ng-content></div> <tui-dialog-host></tui-dialog-host> <ng-content select="tuiOverDialogs"></ng-content> <tui-notifications-host></tui-notifications-host> <ng-content select="tuiOverNotifications"></ng-content> </tui-portal-host> <ng-content select="tuiOverPortals"></ng-content> <tui-hints-host></tui-hints-host> <ng-content select="tuiOverHints"></ng-content>

Generic and dedicated portalsLink to this section

Both tui-dialog-host and tui-portal-host are portals in their nature. But they work differently. Let's explore the second one first. Taiga UI uses it mostly to display drop-downs. But it's a generic container. It is controlled by a very simple service:

<>Copy
@Injectable({ providedIn: 'root', }) export class TuiPortalService { private host: TuiPortalHostComponent; add<C>( componentFactory: ComponentFactory<C>, injector: Injector ): ComponentRef<C> { return this.host.addComponentChild(componentFactory, injector); } remove<C>({hostView}: ComponentRef<C>) { hostView.destroy(); } addTemplate<C>( templateRef: TemplateRef<C>, context?: C ): EmbeddedViewRef<C> { return this.host.addTemplateChild(templateRef, context); } removeTemplate<C>(viewRef: EmbeddedViewRef<C>) { viewRef.destroy(); } }

And the component itself is rather straightforward. All it does is show templates and dynamic components on top of everything. No other logic is included (except a little position: fixed helper for iOS). It means that positioning, closing and the rest is handled by portal items on their own. It's a good idea to have a generic portal for special cases. Like a fixed «Scroll to top» button displayed above content or anything else you, as a library user might need.

If we were to architect a drop-down — we would need to come up with a positioning solution. We have several options here:

  1. Position drop-down once and prevent scrolling while it's open. This is what material does by default.
  2. Position once and close if scrolling occurred. That's how native drop-downs behave.
  3. Follow host position when it changes

We went with the third option. It's not that trivial as it turned out. You cannot really get two positions in sync, even with requestAnimationFrame. Because once you query the host position — it triggers a layout recalculation. So by the time the next frame comes and drop-down is positioned — the host already changes location a little bit. This causes visible jumps, even on fast machines. We got around that by using absolute positioning, rather than fixed. Because the portal container wraps the entire page, position values stay the same during scroll. If the host is in a fixed container, though, it would still jump. But we can detect that when we open the drop-down and use fixed positioning for it as well.

And then there's this:

If the host leaves the visible area — we need to close the drop-down. That's a job for Obscured service. It detects when the host is fully obscured by anything and closes drop-down in that case.

DialogsLink to this section

For dedicated portals study we can take a look at dialogs. Toast notifications and hints are very similar but there are some interesting topics to discuss with modals.

This is how dialog host looks like:

<>Copy
<section *ngFor="let item of dialogs$ | async" polymorpheus-outlet tuiFocusTrap tuiOverscroll="all" class="dialog" role="dialog" aria-modal="true" [attr.aria-labelledby]="item.id" [content]="item.component" [context]="item" [@tuiParentAnimation] ></section> <div class="overlay"></div>

Instead of being a generic host it has an ngFor loop over particular items. This allows us to bundle some logic in, like focus trap and page scroll blocking. There is also a clever use of dependency injection here, allowing dialogs to be design and data model agnostic. Host collects observables with dialogs through a dedicated multi token, merges these streams and shows the result. That way you can have multiple designs for dialogs in the same app. Taiga UI has two built-in designs — base and mobile. But you can easily add your own. Let's see how.

Dialog service returns Observable. When you subscribe to it, modal popup is shown, when you terminate subscription it is closed. Dialog can also send back data through that stream. First, we design our dialog component. All that’s important here, really, is that you can inject POLYMORPHEUS_CONTEXT in constructor. It would contain an object with content and observer for a particular dialog instance. You can close dialog from within by calling complete on observer and you can send back data using next method. Plus all the options you will provide to the service that we will create by extending an abstract class:

<>Copy
const DIALOG = new PolymorpheusComponent(MyDialogComponent); const DEFAULT_OPTIONS: MyDialogOptions = { label: '', size: 's', }; @Injectable({ providedIn: 'root', }) export class MyDialogService extends AbstractTuiDialogService<MyDialogOptions> { protected readonly component = DIALOG; protected readonly defaultOptions = DEFAULT_OPTIONS; }

In it we provide default config and a component to use and we're all set.

Dialogs, like everything in Taiga UI use ng-polymorpheus for customizable content. You can read more about making interface free, flexible components with it in this article.

Focus trapping is handled by the tuiFocusTrap directive. Since we have drop-downs later in DOM and we can have multiple dialogs open at the same time — we don't care if focus goes farther in the DOM. If it went somewhere prior to dialog though — we return focus back with a few helpers from @taiga-ui/cdk:

<>Copy
@HostListener('window:focusin.silent', ['$event.target']) onFocusIn(node: Node) { if (containsOrAfter(this.elementRef.nativeElement, node)) { return; } const focusable = getClosestKeyboardFocusable( this.elementRef.nativeElement, false, this.elementRef.nativeElement, ); if (focusable) { focusable.focus(); } }

Blocking page scroll is dealt with by combination of a directive and some logic inside the root component. Root just hides scrollbars when a dialog is open, while Overscroll directive takes care of touch and wheel scroll. There's a CSS rule for overscroll behavior. However it's not sufficient. It doesn't help when dialog is small enough that it doesn't have its own scroll. That's why we have a directive with some additional logic stopping scroll if it will happen in some patent node.

Bonus: what else does tui-root do?Link to this section

As far as portals go — this covers most of it. Let's also take a quick look at what else is bundled with the root component. You saw in the template that it has tui-scroll-controls. These are custom scrollbars that control global scroll. You may have also noticed named content projections like <ng-content select="tuiOverDialogs"></ng-content>. With those you can slide some content in-between layers of Taiga UI if you need. For example, if you run another library for toasts or dialogs and want them properly placed vertically.

It also registers several event manager plugins in the DI. You can read about them in a dedicated article. It is important that TuiRootModule goes after BrowserModule so they are registered at the right order. But don't worry — if you get it wrong you will see an assertion message in the console.

That wraps it up for portals and the root component. Taiga UI is open-source and you can check it out on GitHub and npm. You can also browse the demo portal with documentation and play with it using this StackBlitz starter. Stay tuned for more articles on interesting features we have!

Discuss with community

Share

About the author

author_image

I’m a devoted Angular developer and a musician. I use one thing to help me not to starve while doing another and I love doing both. I work on Angular UI Kit at Tinkoff and I like sharing my findings.

author_image

About the author

Alex Inkin

I’m a devoted Angular developer and a musician. I use one thing to help me not to starve while doing another and I love doing both. I work on Angular UI Kit at Tinkoff and I like sharing my findings.

About the author

author_image

I’m a devoted Angular developer and a musician. I use one thing to help me not to starve while doing another and I love doing both. I work on Angular UI Kit at Tinkoff and I like sharing my findings.

NxAngularCli
NxAngularCli
NxAngularCli

Featured articles

Angularpost
10 May 20219 min read
Angular Forms: reactive design patterns catalog

In this post, you'll find a set of design patterns for building Angular forms based on two pillars: separation of responsibilities and reactive programming to tackle the complexity of rich and complex Angular forms.

Angularpost
6 May 20216 min read
A journey into NgRx Selectors

This article dives deep into NgRx selectors and will help you understand what role that play in NgRx architecture and how they help decrease the complexity of a codebase