Home Development of Websites Portal pattern in Angular: Why you need a root component in Taiga UI

Portal pattern in Angular: Why you need a root component in Taiga UI

by admin

My colleague Roman recently announced about the release of our new component library for Angular Taiga UI. In the instructions. Getting started says that the application needs to be wrapped in some tui-root Let’s figure out what it does, find out how and why we use portals and what it is all about.

Portal pattern in Angular: Why you need a root component in Taiga UI

What is a portal?

Imagine a component select It has a dropdown box with options to choose from. If you store it in the same place in the DOM as the component itself, you can run into a number of problems. Downstream elements can pop out on top, and the container can cut off the contents of :

Portal pattern in Angular: Why you need a root component in Taiga UI

Depth problems are usually solved via z-index which triggers War of the Worlds Z in your application. You can often find values of 100, 10000, 10001. But even if you manage to do this correctly, from overflow: hidden still can’t escape. So what to do?

Instead of putting the dropdown nextto the host, we put it in a special container on top of the whole application. Then the rest of the elements will be in their isolated context and problems with z-index will go away. This container is a "portal". The Root-component in Taiga UI is needed just for creation of such portals. Let’s look at its template :

<tui-scroll-controls> </tui-scroll-controls><tui-portal-host><div class="content"> <ng-content> </ng-content> </div><tui-dialog-host> </tui-dialog-host><ng-contentselect="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>

General and specialized portals

And tui-dialog-host , and tui-portal-host are essentially portals. But they work differently. First, let’s take a look at the second one. In Taiga UI, it’s used to display dropdowns. But it’s a general purpose portal. It’s controlled by a very simple service :

@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();}}

The component itself is also uncomplicated. All it does is display templates and dynamic components on top of the application. This means that positioning, closing and all other logic lies on the shoulders of the output elements themselves.

Such a universal portal is useful if you need something special. For example, the "Up" button, always visible above the content.


When creating a solution for dropdowns, you have to think about positioning. Here we have several options :

  1. Position once and lock the scroll. This is how Material works by default.

  2. Position once and close if a scroll occurs. This is how native components behave.

  3. Monitor host position.

We decided to take the third way. It turned out to be not so simple. It is impossible to completely synchronize the continuation of the two elements, even through requestAnimationFrame Because requesting the host’s position causes a reflow – by the end of the frame, when the dropout is put in a new location the host will still change its position slightly. This causes visible jumps even on fast machines. We got around this by using absolute positioning instead of fixed positioning. Because the portal container wraps around the entire application, the position of the dropdown does not change during scrolling. If the host itself is in a fixed container, this won’t work. But we can notice it at the moment of opening and position the dropdown also fixed.

Well, there’s also this :

Portal pattern in Angular: Why you need a root component in Taiga UI

If the host leaves the visible area, you need to close the dropdown. This is done by the Obscured service. It keeps track of the host and closes the dropdown if it overlaps completely.


To deal with specialized portals, let’s take a look at dialogs. Notifications and prompts work the same way, but there are a couple more interesting issues to discuss using modal windows as examples.

This is what the dialog component looks like :

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

As you can see, it has a loop of ngFor that goes through specific elements. This allows you to lay down some logic, like grabbing focus or locking the scroll. It also uses a clever dependency implementation that allows dialogs to be independent of the data model and specific appearance. The host collects threads with dialogs using a special multitoken, combines them into one and outputs the result.

Thus, several kinds of dialogs can coexist in one application. Taiga UI supports two popup designs: the base one and the one that imitates native alerts on mobile devices. The latter looks different on iOS and Android. You can easily add your own implementation. Let’s see how to do it.

The modal window display service returns Observable When you subscribe to it, the window is displayed, when you unsubscribe, it is closed. The dialog can also send data to this thread. First, let’s create our own dialog component. Everything that is important here, it can take from the DI token POLYMORPHEUS_CONTEXT This object will contain the field content with the content of the modal and observer of a particular dialog instance. It can be terminated via complete which will close the dialog box, and the data can be returned with the next In addition, this object will contain any parameters you wish to add to your dialog implementation. The service for showing them is inherited from the abstract class :

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;}

You just need to set the default options and your component.

Dialogs, like everything in Taiga UI, use ng-polymorpheus For customizing content. To learn more about how to create flexible, data model-independent components, see of this article.

Focus capture is realized by the directive tuiFocusTrap Because the focus can go into dropdowns later in the DOM and we can have multiple dialogs open, the traps ignore focus moving to lower-level elements. If, however, the focus moves somewhere higher up the tree, we bring it back :

@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();}}

A combination of a directive and a little logic inside the root component is used to lock the scrollbar. Root is required to hide the scrollbar when opening a dialog, while the directive tuiOverscroll takes over the scrolling with the tap or the mouse wheel. There is a CSS rule. overscroll-behavior. However, it is not enough. It won’t help if the dialog is too small to have no internal scroll. So we created a special directive with extra logic to block scrolling if it’s going to happen in the parent containers.

Bonus : what else does tui-root do?

We’ve discussed everything else about portals. Let’s take a look at what else is built into the root component. You’ve seen in the template tui-scroll-controls This is a custom scrollbar that controls global window scrolling. You may also have noticed a named content projection like <ng-content select="tuiOverDialogs"></ng-content> With these slots you can slip your content between Taiga UI layers. For example, if you are using other notification solutions and want to place them correctly along the depth of the application.

Also root registers some event manager plugins in DI. Read more about this at. In a separate article. It is important that TuiRootModule goes after BrowserModule so that the plugins are registered in the right order. But don’t worry: if you get it wrong, you will see a message in the console.

That’s all I wanted to tell you about portals and the root component. Taiga UI is already in open-source, and you can take a look at it on "Github" and pull from the npm. You can walk around On the demoportal with documentation or play around with this StackBlitz-starter. Stay tuned, we’ll be sure to tell you more about the interesting features we have in the future!

You may also like