Skip to content

Instantly share code, notes, and snippets.

@vs-borodin
Last active December 9, 2025 17:13
Show Gist options
  • Select an option

  • Save vs-borodin/38e87d0dbbebd7ac0cb6a9eb63e592d6 to your computer and use it in GitHub Desktop.

Select an option

Save vs-borodin/38e87d0dbbebd7ac0cb6a9eb63e592d6 to your computer and use it in GitHub Desktop.
teleport.ts
import { afterRenderEffect, DestroyRef, Directive, ElementRef, inject, input } from '@angular/core';
@Directive({ selector: 'ng-teleport' })
export class Teleport {
private readonly destroyRef = inject(DestroyRef);
private readonly el: HTMLElement /* https://github.com/angular/angular/issues/53894 */ = inject(
ElementRef<HTMLElement>
).nativeElement;
readonly to = input.required<string>();
readonly disabled = input(false);
private startAnchor?: Comment | null;
private endAnchor?: Comment | null;
constructor() {
afterRenderEffect(() => {
if (this.disabled()) {
this.detach();
} else {
this.attach(this.to());
}
});
this.destroyRef.onDestroy(() => this.detach());
}
private attach(targetSelector: string): void {
if (this.startAnchor) {
this.detach();
}
const document = this.el.ownerDocument;
const target = document.querySelector(targetSelector);
if (!target) {
throw new Error(`Target "${targetSelector}" not found`);
}
const children = document.createDocumentFragment();
while (this.el.firstChild) {
children.appendChild(this.el.firstChild);
}
target.append(
(this.startAnchor = document.createComment('teleport-start')),
children,
(this.endAnchor = document.createComment('teleport-end'))
);
}
private detach(): void {
let current = this.startAnchor?.nextSibling;
while (current && current !== this.endAnchor) {
const next = current.nextSibling;
this.el.appendChild(current);
current = next;
}
this.startAnchor?.remove();
this.endAnchor?.remove();
this.startAnchor = null;
this.endAnchor = null;
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment