Angular Portal not triggering change detection for ComponentPortal

1k views Asked by At

I am relatively new to angular and have a feature where I need to popout a component in a separate child window and on the window close I need to attach it again to the parent window

I have read about angular portal and decided to use it. I have written the following service to manage the portal and it's creation:

import { ApplicationRef, ComponentFactoryResolver, ComponentRef, Injectable, Injector, InjectionToken, Inject } from '@angular/core';
import { PopoutComponent } from '../popout/popout.component';
import { POPOUT_REFS } from './portal.token';
import { EventService } from './event.service';

@Injectable({
  providedIn: 'root'
})
export class PortalService {

  constructor(private injector: Injector,
    private componentFactoryResolver: ComponentFactoryResolver,
    private applicationRef: ApplicationRef,
    private eventService: EventService ) {
      this.eventService.popoutEvent$.subscribe((val) => {
        if(val && this.isPopoutWindowOpen()) {
          this.closePopoutWindow();
        }
      })
    }

  popOutComponent() {
    const popoutWindow: Window = this.createWindow('POP_OUT_COMPONENT');
    setTimeout(() => {
      this.createPortal(popoutWindow);
    }, 1000);

  }

  private createWindow(target): Window {
    const ref = window.open("index.html", "_blank", "height=400; width=400;directories=0,titlebar=0,toolbar=0,location=0,status=0,menubar=no;", true);
    setTimeout(() => {
      ref.document.body.innerHTML = "";
    }, 1000);
    return ref;
  }

  private createPortal(popoutWindow: Window) {
    if(popoutWindow) {
      const outlet = new DomPortalOutlet(popoutWindow.document.body, this.componentFactoryResolver, this.applicationRef, this.injector);
      const conatinerInstance = this.attachPopoutContainer(outlet);
      this.attachUnloadEventToPopoutWindow(popoutWindow);

      POPOUT_REFS['windowInstance'] = popoutWindow;
      POPOUT_REFS['containerInstance'] = conatinerInstance;
      POPOUT_REFS['portalOutlet'] = outlet;
    }
  }

  private attachPopoutContainer(outlet: DomPortalOutlet) {
    const containerPortal = new ComponentPortal(PopoutComponent, null, null);
    const containerRef: ComponentRef<PopoutComponent> = outlet.attach(containerPortal);
    return containerRef.instance;
  }

  private attachUnloadEventToPopoutWindow(popoutWindow: Window) {
    popoutWindow.addEventListener("beforeunload", () => {
      POPOUT_REFS['portalOutlet'].detach();
      this.eventService.publishPopoutEvent(true);
    });
  }

  isPopoutWindowOpen() {
    return POPOUT_REFS['windowInstance'] && !POPOUT_REFS['windowInstance'].closed;
  }

  closePopoutWindow() {
    Object.keys(POPOUT_REFS).forEach(popout => {
      if(POPOUT_REFS['windowInstance']) {
        POPOUT_REFS['windowInstance'].close();
      }
    });
  }

  focusPopoutWindow() {
    POPOUT_REFS['windowInstance'].focus();
  }

}

When I am clicking a button say detach in the component I want to render in the child window it removes the component in main window since it is being rendered based on ngIf. The component works fine in the new child component. When I close the child window and based on the condition again the component is rendered back, but even on change of properties, angular change detection is not triggered. I have to manually trigger change detection to make it work. Here is my MainComponent where I need to do manual triggers for change detection to reflect changes in the detached component:

import { Component, OnInit, ChangeDetectorRef } from '@angular/core';
import { EventService } from '../service/event.service';
import { PortalService } from '../service/portal.service';
import { ChangeDetectionStrategy } from '@angular/compiler/src/compiler_facade_interface';

@Component({
  selector: 'app-main',
  templateUrl: './main.component.html',
  styleUrls: ['./main.component.css']
})
export class MainComponent implements OnInit {
  isAttached: boolean;
  num: number;

  constructor(
    private eventService: EventService,
    private portalService: PortalService,
    private changeDetectorRef: ChangeDetectorRef
    ) {
    this.isAttached = true;
  }

  ngOnInit() {
    const popoutComponent = this.handlePopoutComponent.bind(this);
    this.eventService.numberEvent$.subscribe(val => {
      this.num = val;
    });
    this.eventService.popoutEvent$.subscribe(val => {
      if (val != null) {
        popoutComponent(val);
      }
    });
  }

  handlePopoutComponent(val: boolean) {
    this.isAttached = val;
    this.changeDetectorRef.detectChanges();
    if (this.isAttached === false && !this.portalService.isPopoutWindowOpen()) {
      this.portalService.popOutComponent();
    }
  }

  onNumberPublish() {
    this.num += 1;
    this.eventService.publishNumberEvent(this.num);
  }
}

Is there a correct way to do this? I am guessing the window close event does not cleanly dispose and reattach the component to main window.

1

There are 1 answers

3
maxkart On

It might be worth considering a light eventbus for this scenario ..

export class EventBusService implements OnDestroy{

  private eventBus : Subject<App.Event>= new Subject();
  public eventBus$ : Observable<App.Event> = this.eventBus.asObservable();

  constructor() { }

  fire(event : App.Event){
    this.eventBus.next(event);
  }  

  ngOnDestroy(){
    this.eventBus.unsubscribe();
  }

}

You can fire events from your main as well as the pop-up component and handle them accordingly.