Understanding Teleportation in Angular
In Angular applications, teleportation refers to relocating a view fragment—specifically, moving it into a different component—while preserving the original component's data and event bindings. This mechanism shares conceptual similarities with content projection, though it removes the requirement for maintaining a parent-child relationship; for instance, a fragment can be moved from a child component to a parent component.
This capability to alter element positions within the DOM tree without breaking the original view-component bindings emerges naturally from Angular's architecture. The logical component tree exists independently, even at runtime, and maintains its connections to view templates separately from the DOM tree. Consequently, modifications to the DOM tree do not impact the logical component tree, enabling seamless relocation of view fragments.

Image source: https://i.redd.it/kko442mrgim71.jpg
When Does Teleportation Prove Useful?
Teleportation becomes valuable when content projection alone falls short, requiring us to momentarily invert the dependency direction in the component hierarchy—where a fragment from an ancestor view depends on a descendant. A practical illustration involves a header widget that adapts based on which sub-page the user currently views.
Existing Solutions to Consider
@angular/cdk/portal
Reference: https://material.angular.io/cdk/portal/overview
The Portal mechanism, belonging to the Component Dev Kit (CDK), serves as a foundational, low-level system upon which various Angular Material components are constructed—notably overlays used by elements such as dialogs. This system comprises two core components:
- The portal outlet, which acts as the designated "slot" where we intend to render a view fragment dynamically,
- The view fragment itself, which we wish to render dynamically—provided as a reference to either a DOM element, a TemplateRef, or a component.
To facilitate remote teleportation, we must establish a mechanism capable of transferring the view fragment from its original location to the designated outlet. A straightforward approach involves creating a singleton service that takes on multiple responsibilities: registering (and remembering) the outlets and managing the commencement and termination of the teleportation process.
import {ApplicationRef, ComponentFactoryResolver, Injectable, Injector} from '@angular/core';
import {DomPortalOutlet, Portal} from '@angular/cdk/portal';
@Injectable({
providedIn: 'root'
})
export class AngularCdkTeleportService {
private portalOutlet: DomPortalOutlet | null = null;
constructor(private cfr: ComponentFactoryResolver,
private appRef: ApplicationRef,
private injector: Injector) {
}
registerPortalOutlet(outletElement: HTMLElement): void {
this.portalOutlet = new DomPortalOutlet(
outletElement,
this.cfr,
this.appRef,
this.injector,
document
)
}
unregisterPortalOutlet(): void {
this.portalOutlet?.dispose();
this.portalOutlet = null;
}
teleport(portal: Portal<any>): void {
this.portalOutlet?.attach(portal);
}
finishTeleportation(): void {
this.portalOutlet?.detach();
}
}
In our context, the registerPortalOutlet method receives a reference to a DOM element and establishes our "slot" for the view fragment within that element. Conversely, the unregisterPortalOutlet method disposes of such a slot. The teleport and finishTeleportation methods handle the commencement and cessation of rendering for the view fragment passed in as the "portal".
Here is an illustrative implementation for the content component intended for teleportation:
@Component({
selector: 'app-example',
changeDetection: ChangeDetectionStrategy.OnPush,
template: `
<div #content>
I've been teleported by cdk portal!
</div>
`
})
export class ExampleComponent implements OnDestroy {
@ViewChild('content') set content(elemRef: ElementRef<HTMLElement>) {
this.angularCdkTeleportService.teleport(new DomPortal(elemRef));
}
constructor(private readonly angularCdkTeleportService: AngularCdkTeleportService) {
}
ngOnDestroy(): void {
this.angularCdkTeleportService.finishTeleportation();
}
}
Using @ViewChild, we obtain a reference to a DOM element, instantiate a DomPortal object from it, and hand it off to our service. It's crucial to terminate the teleportation within ngOnDestroy.
Here is an illustrative implementation for the outlet portal side:
@Component({
selector: 'app-root',
changeDetection: ChangeDetectionStrategy.OnPush,
template: `
<div #angularCdkPortalOutlet></div>`
})
export class AppComponent implements OnDestroy {
@ViewChild('angularCdkPortalOutlet') set angularCdkPortalOutletElement(elementRef: ElementRef<HTMLElement>) {
this.angularCdkTeleportService.registerPortalOutlet(
elementRef.nativeElement
);
}
constructor(private readonly angularCdkTeleportService: AngularCdkTeleportService) {
}
ngOnDestroy(): void {
this.angularCdkTeleportService.unregisterPortalOutlet();
}
}
Following a pattern similar to the previous example, we acquire a reference to the DOM element designated as our "slot" for dynamic content and pass it along to the service. Be sure to destroy this slot within ngOnDestroy.
The implementation shown here is a simplified version. We can readily extend it to accommodate multiple slots identified by keys, or to manage scenarios where the UI piece is registered for teleportation before the outlet itself is registered. Additionally, rather than manually passing references to the service, we could craft custom directives to streamline this process.
@ngneat/overview > teleporting
Reference: https://github.com/ngneat/overview#Teleporting
The @ngneat/overview library offers two directives for handling teleportation:
teleportOutlet, which designates a "slot" at the specified location,*teleportTo, which specifies the content designated for teleportation.
Documentation snippet illustrating teleportOutlet:
@Component({
template: `
<div class="flex">
<ng-container teleportOutlet="someId"></ng-container>
</div>
`
})
export class FooComponent {}
Documentation snippet illustrating *teleportTo:
@Component({
template: `
<section *teleportTo="'someId'">
{{ value }}
</section>
`
})
export class BarComponent {
value = '...'
}
Building a Custom Teleportation Solution
If the existing solutions aren't a fit for your needs—whether due to preference or constraints—constructing the entire mechanism from scratch is quite achievable.
We'll designate ViewContainerRef as our "slot" for hosting dynamic content.
import {Injectable, TemplateRef, ViewContainerRef} from '@angular/core';
@Injectable({
providedIn: 'root'
})
export class CustomTeleporterService {
private portalOutlet: ViewContainerRef | null = null;
registerPortalOutlet(viewContainerRef: ViewContainerRef): void {
this.portalOutlet = viewContainerRef;
}
unregisterPortalOutlet(): void {
this.portalOutlet = null;
}
startTeleportation(templateRef: TemplateRef<unknown>): void {
this.portalOutlet?.createEmbeddedView(templateRef);
}
finishTeleportation(): void {
this.portalOutlet?.clear();
}
}
Drawing significant inspiration from the @ngneat solution, we formulate two custom directives:
@Directive({
selector: '[customTeleportOutlet]',
})
export class CustomPortalOutletDirective implements OnDestroy {
constructor(
private readonly viewContainerRef: ViewContainerRef,
private readonly teleportService: CustomTeleporterService
) {
this.teleportService.registerPortalOutlet(this.viewContainerRef);
}
ngOnDestroy(): void {
this.teleportService.unregisterPortalOutlet();
}
}
@Directive({
selector: '[customTeleportTo]',
})
export class CustomTeleportToDirective implements OnDestroy {
constructor(
private readonly templateRef: TemplateRef<unknown>,
private readonly teleportService: CustomTeleporterService
) {
this.teleportService.startTeleportation(this.templateRef);
}
ngOnDestroy(): void {
this.teleportService.finishTeleportation();
}
}
A usage example appears as follows:
@Component({
selector: 'app-example',
changeDetection: ChangeDetectionStrategy.OnPush,
template: `
<div *customTeleportTo>
I've been teleported!
</div>`
})
export class ExampleComponent {}
@Component({
selector: 'app-root',
changeDetection: ChangeDetectionStrategy.OnPush,
template: `
<ng-container customTeleportOutlet></ng-container>`
})
export class AppComponent {}
As with the @angular/cdk example, this tailored solution is worth enhancing with additional features and safety measures.
Wrapping Up
The repository housing all the aforementioned implementations is available at the following location:
https://github.com/mateusz-dobrowolski-va/angular-teleportation
