Skip to content

Commit

Permalink
feat: allow teleport bindings to be passed asynchronously
Browse files Browse the repository at this point in the history
  • Loading branch information
arturovt committed Oct 25, 2021
1 parent 2f01f4e commit 05c164f
Show file tree
Hide file tree
Showing 3 changed files with 121 additions and 37 deletions.
Original file line number Diff line number Diff line change
@@ -1,20 +1,25 @@
import { Directive, Input, OnDestroy, OnInit, ViewContainerRef } from '@angular/core';
import { Directive, Input, OnChanges, OnDestroy, SimpleChanges, ViewContainerRef } from '@angular/core';
import { TeleportService } from './teleport.service';

@Directive({
selector: '[teleportOutlet]',
})
export class TeleportOutletDirective implements OnInit, OnDestroy {
@Input() teleportOutlet: string;
export class TeleportOutletDirective implements OnChanges, OnDestroy {
// We could've also used the `ngAcceptInputType`, but it's being deprecated in newer Angular versions.
@Input() teleportOutlet: string | null | undefined;

constructor(private vcr: ViewContainerRef, private service: TeleportService) {}

ngOnInit() {
this.service.ports.set(this.teleportOutlet, this.vcr);
this.service.newOutlet(this.teleportOutlet);
ngOnChanges(changes: SimpleChanges): void {
// The `teleportOutlet` might be `null|undefined`, but we don't want nullable values to be used
// as keys for the `ports` map.
if (changes.teleportOutlet && typeof this.teleportOutlet === 'string') {
this.service.ports.set(this.teleportOutlet, this.vcr);
this.service.newOutlet(this.teleportOutlet);
}
}

ngOnDestroy() {
ngOnDestroy(): void {
this.service.ports.delete(this.teleportOutlet);
}
}
95 changes: 77 additions & 18 deletions projects/ngneat/overview/src/lib/teleport/teleport.module.spec.ts
Original file line number Diff line number Diff line change
@@ -1,26 +1,85 @@
import { Component } from '@angular/core';
import { createComponentFactory, Spectator } from '@ngneat/spectator';
import { TeleportModule } from './teleport.module';

@Component({
template: ` <div *teleportTo="'projectHere'">Some view</div>
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, OnDestroy, OnInit } from '@angular/core';
import { createComponentFactory } from '@ngneat/spectator';

<section>
<ng-container teleportOutlet="projectHere"></ng-container>
</section>`,
})
class TestComponent {}
import { TeleportModule } from './teleport.module';

describe('TeleportDirective', () => {
let spectator: Spectator<TestComponent>;
describe('Synchronous behavior', () => {
@Component({
template: `
<div *teleportTo="'projectHere'">Some view</div>
<section>
<ng-container teleportOutlet="projectHere"></ng-container>
</section>
`,
})
class TestComponent {}

const createComponent = createComponentFactory({
component: TestComponent,
imports: [TeleportModule],
const createComponent = createComponentFactory({
component: TestComponent,
imports: [TeleportModule],
});

it('should render the view as sibling to the given outlet', () => {
// Arrange & act
const spectator = createComponent();
// Assert
expect(spectator.query('section')).toHaveText('Some view');
});
});

it('should render the view as sibling to the given outlet', () => {
spectator = createComponent();
expect(spectator.query('section')).toHaveText('Some view');
describe('Asynchronous behavior', () => {
@Component({
selector: 'app-hello',
template: '<div>Some view</div>',
})
class HelloComponent implements OnInit, OnDestroy {
ngOnInit(): void {}
ngOnDestroy(): void {}
}

@Component({
template: `
<app-hello *teleportTo="teleportTo"></app-hello>
<section>
<ng-template [teleportOutlet]="teleportTo"></ng-template>
</section>
`,
changeDetection: ChangeDetectionStrategy.OnPush,
})
class AsynchronousTestComponent {
teleportTo: string | null = null;

constructor(private ref: ChangeDetectorRef) {}

setTeleportToAsynchronously(): Promise<void> {
return Promise.resolve().then(() => {
this.teleportTo = 'projectHere';
// Run the change detection manually since we're inside an OnPush component.
this.ref.detectChanges();
});
}
}

const createComponent = createComponentFactory({
component: AsynchronousTestComponent,
imports: [TeleportModule],
declarations: [HelloComponent],
});

it('should render the view as sibling to the given outlet asynchronously', async () => {
// Arrange
const spectator = createComponent();
// Act
const ngOnInitSpy = spyOn(HelloComponent.prototype, 'ngOnInit').and.callThrough();
const ngOnDestroySpy = spyOn(HelloComponent.prototype, 'ngOnDestroy').and.callThrough();
await spectator.component.setTeleportToAsynchronously();
// Assert
expect(spectator.query('app-hello')).toExist();
expect(spectator.query('app-hello')).toContainText('Some view');
spectator.fixture.destroy();
expect(ngOnInitSpy).toHaveBeenCalled();
expect(ngOnDestroySpy).toHaveBeenCalled();
});
});
});
44 changes: 32 additions & 12 deletions projects/ngneat/overview/src/lib/teleport/teleport.module.ts
Original file line number Diff line number Diff line change
@@ -1,28 +1,48 @@
import { Directive, EmbeddedViewRef, Input, NgModule, OnDestroy, OnInit, TemplateRef } from '@angular/core';
import {
Directive,
EmbeddedViewRef,
Input,
NgModule,
OnChanges,
OnDestroy,
SimpleChanges,
TemplateRef,
} from '@angular/core';
import { Subscription } from 'rxjs';

import { TeleportService } from './teleport.service';
import { TeleportOutletDirective } from './teleport-outlet.directive';
import { Subscription } from 'rxjs';

@Directive({
selector: '[teleportTo]',
})
export class TeleportDirective implements OnInit, OnDestroy {
@Input() teleportTo: string;
export class TeleportDirective implements OnChanges, OnDestroy {
@Input() teleportTo: string | null | undefined;

private viewRef: EmbeddedViewRef<any>;
private subscription: Subscription | undefined;
private subscription: Subscription | null = null;

constructor(private tpl: TemplateRef<any>, private service: TeleportService) {}

ngOnInit() {
this.subscription = this.service.outlet$(this.teleportTo).subscribe((outlet) => {
if(outlet) {
this.viewRef = outlet.createEmbeddedView(this.tpl);
}
});
ngOnChanges(changes: SimpleChanges): void {
if (changes.teleportTo && typeof this.teleportTo === 'string') {
this.dispose();

this.subscription = this.service.outlet$(this.teleportTo).subscribe((outlet) => {
if (outlet) {
this.viewRef = outlet.createEmbeddedView(this.tpl);
}
});
}
}

ngOnDestroy(): void {
this.dispose();
}

ngOnDestroy() {
private dispose(): void {
this.subscription?.unsubscribe();
this.subscription = null;
this.viewRef?.destroy();
}
}
Expand Down

0 comments on commit 05c164f

Please sign in to comment.