ngx-modialog icon indicating copy to clipboard operation
ngx-modialog copied to clipboard

ngOnDestroy is never called when using TemplateRef modal

Open danielcrisp opened this issue 7 years ago • 5 comments

  • I'm submitting a ... [x] bug report [ ] feature request [ ] question about the decisions made in the repository

  • What is the current behavior?

Plunkr showing another component embedded in the modal using the <template> approach.

https://embed.plnkr.co/dkhsP5ztQUM8Zq7ym4j0/

Open the console. You will see that ngOnInit is called each time the modal is opened, but ngOnDestroy is never called. This leads to strange behaviour in the child component because subscriptions to other services are not unsubscribed.

  • What is the expected behavior?

When a modal is closed / dismissed I would expect the child components of the modal to also be destroyed using the standard ngOnDestroy method.

If this is feature rather than a bug could you explain how we should destroy child components please?

danielcrisp avatar Jan 31 '17 11:01 danielcrisp

I've found that I can subscribe to the onDestroy observable of DialogRef inside my child component. Is that the correct way to do this?

this.dialogRef.onDestroy.subscribe(() => {
    // destroy child component here?
});

danielcrisp avatar Jan 31 '17 11:01 danielcrisp

A bit more investigation has shown this is causing memory leaks.

You can see it happening here, on a small scale, with the Plunkr above:

image

It's much more of a problem in my real app where my modals contain complex logic.

I've also found that ngOnDestroy doesn't even fire when I destroy the parent component that contains the <template>. Note: You can't do this in the Plunkr because the parent in this case is the main app component.

So somehow its getting detached and lost in memory somewhere, and I think it is a problem with angular2-modal rather than Angular itself.

I created another Plunkr, with pure Angular, which embeds and destroys a child component using different techniques. In the console you will see that in this case ngOnDestroy fires correctly.

https://plnkr.co/edit/ouK4sS2bjnL5AoIgHDAD?p=preview

danielcrisp avatar Feb 10 '17 11:02 danielcrisp

Ahhhaaa progress...

It seems I can call the clear() method on the defaultViewContainer object, then I see ngOnDestroy firing in my child component.

onClick() {
    // open the modal
    this.modal.open(this.customModalRef, overlayConfigFactory(BSModalContext))
      .then((dialog: DialogRef<BSModalContext>) => {
        // keep reference to dialog
        this.dialog = dialog;
        return dialog.result;
      })
      .then(() => {
        console.log('closed');
        // destroy when closed
        this.destroyModal();
      })
      .catch(() => {
        console.log('dismissed');
        // destroy when dismissed
        this.destroyModal();
      });
}

private destroyModal () {
    if (this.dialog) {
      this.dialog.overlay.defaultViewContainer.clear();
      this.dialog = null;
    }
}

Updated plunker:

https://plnkr.co/edit/6l8sDxh92KdDqncgtRyX?p=preview

Is this the correct approach?

danielcrisp avatar Feb 10 '17 12:02 danielcrisp

A side effect of this technique is that the close animation no longer occurs, because the contents are immediately destroyed. Perhaps if it was handled internally it could happen after the animation complete callback?

danielcrisp avatar Feb 10 '17 12:02 danielcrisp

My solution to this problem:

export class MyComponent implements OnInit {

  private _vcRef: ViewContainerRef;

  .
  .
  .

  constructor(overlay: Overlay,
              vcRef: ViewContainerRef,
              public modal: Modal) {
    this._vcRef = vcRef;
    overlay.defaultViewContainer = vcRef;
  }

  .
  .
  .

  public openModalExclusao(_parcelamento: Parcelamento) {
    this.parcelamento = _parcelamento;
    return this.modal
      .open(MotivoModalComponent, overlayConfigFactory({ parcelamento: this.parcelamento }, BSModalContext))
      .then((dialog: DialogRef<BSModalContext>) => {
        this.dialog = dialog;
        return dialog.result;
      })
      .then((result: any) => {
        this.destroyModal();
      })
      .catch((err: any) => {
        this.destroyModal();
      });
  }

  public destroyModal () {
    if (this.dialog) {
      this.dialog.overlay.defaultViewContainer = this._vcRef;
      this.dialog = null;
    }
  }
}

In the constructor of my compoment, I assign a "_vcRef" variable of type "ViewContainerRef" or "vcRef".

this._vcRef = vcRef;

In the destroyModal () method I assign the reference of "_vcRef" to "defaultViewContainer".

this.dialog.overlay.defaultViewContainer = this._vcRef;

Solved my problem.

heidermatos avatar May 22 '17 17:05 heidermatos