Search code examples
angularrxjsangular-cdk

How to subscribe to Observable of Component injected via @angular/cdk/portal?


I'm trying to implement a basic (very basic) modal implementation. I've got a ModalService and a ModalComponent.

The ModalService creates an instance of the ModalComponent and injects it into the page using the @angular/cdk/portal.

I can get the modal to display just fine :-)

The ModalComponent has an Observable property that I want the ModalService to subscribe to so that when the 'close' button is clicked within the modal, a value can be emitted and the ModalService can close the modal.

However, when the component emits the value, the Service is not acting on it. From the Service side, it looks like I'm subscribed, but from the Component side, it shows 0 observers.

I thought maybe I could use a typical @Output() EventEmitter, but I'm not sure to hook that up since the in the modal class, since the child element doesn't exist initially.

I'm thinking maybe my component reference is not quite right (maybe I have 2 different ones?). I'm trying suggestion from this answer

Any ideas what I'm doing wrong?

Service

export class ModalService {

    private modalPortal: ComponentPortal<any>;
    private bodyPortalHost: DomPortalHost;

    constructor(private componentFactoryResolver: ComponentFactoryResolver,
                private appRef: ApplicationRef,
                private injector: Injector) {
    }

    showModal(modalName: string) {

        // set up the portal and portal host
        this.modalPortal = new ComponentPortal(ModalComponent);
        this.bodyPortalHost = new DomPortalHost(
            document.body,
            this.componentFactoryResolver,
            this.appRef,
            this.injector);

        // display the component in the page
        let componentRef = this.bodyPortalHost.attach(this.modalPortal);


        // listen for modal's close event
        componentRef.instance.onModalClose().subscribe(() => {
            console.log('I will be very happy if this this gets called, but it never ever does');
            this.closeModal();
        });

        // console.log(componentRef.instance.onModalClose()) shows 1 subscriber.
    }

    closeModal() {
        this.bodyPortalHost.detach();
    }
}

Component

export class ModalComponent {

    private modalClose: Subject<any> = new Subject();

    onModalClose(): Observable<any> {
        return this.modalClose.asObservable();
    }

    closeModal() {
        // console.log(this.onModalClose()) **shows zero observers**   :-(
        this.modalClose.next();
        this.modalClose.complete();
    }
}

Also, if I happen to be asking the wrong question, meaning there's a better overall approach, I'm open to suggestions. :-)

Thanks!


Solution

  • This code works for me with very few changes, so I'm confused as to what your problem is. First I created a project with the Angular CLI using ng new. Then I installed ng material using the instructions on their site.

    I created a modal service which is identical to yours:

        import {ApplicationRef, ComponentFactoryResolver, Injectable, Injector} from '@angular/core';
    import {DomPortalHost, ComponentPortal} from "@angular/cdk/portal";
    import {ModalComponent} from "./modal/modal.component";
    
    @Injectable({
      providedIn: 'root'
    })
    export class ModalService {
    
      private modalPortal: ComponentPortal<any>;
      private bodyPortalHost: DomPortalHost;
    
      constructor(private componentFactoryResolver: ComponentFactoryResolver,
                  private appRef: ApplicationRef,
                  private injector: Injector) {
      }
    
      showModal(modalName: string) {
    
        // set up the portal and portal host
        this.modalPortal = new ComponentPortal(ModalComponent);
        this.bodyPortalHost = new DomPortalHost(
          document.body,
          this.componentFactoryResolver,
          this.appRef,
          this.injector);
    
        // display the component in the page
        let componentRef = this.bodyPortalHost.attach(this.modalPortal);
    
    
        // listen for modal's close event
        componentRef.instance.onModalClose().subscribe(() => {
          console.log('I will be very happy if this this gets called, but it never ever does');
          this.closeModal();
        });
    
        // console.log(componentRef.instance.onModalClose()) shows 1 subscriber.
      }
    
      closeModal() {
        this.bodyPortalHost.detach();
      }
    }
    

    I created a modal component. The TypeScript is the same as yours:

    import { Component, OnInit } from '@angular/core';
    import {Observable, Subject} from "rxjs/index";
    
    @Component({
      selector: 'app-modal',
      templateUrl: './modal.component.html',
      styleUrls: ['./modal.component.css']
    })
    export class ModalComponent implements OnInit {
    
      constructor() { }
    
      ngOnInit() {
      }
    
      private modalClose: Subject<any> = new Subject();
    
      onModalClose(): Observable<any> {
        return this.modalClose.asObservable();
      }
    
      closeModal() {
        // console.log(this.onModalClose()) **shows zero observers**   :-(
        this.modalClose.next();
        this.modalClose.complete();
      }
    
    }
    

    You didn't give us a model for the HTML, but this is what I used:

    <p>
      modal works!
    </p>
    <button (click)="closeModal()">Close Modal</button>
    

    Here is my app.module.ts:

    import { BrowserModule } from '@angular/platform-browser';
    import { NgModule } from '@angular/core';
    import {BrowserAnimationsModule} from '@angular/platform-browser/animations';
    
    import { AppComponent } from './app.component';
    import {ModalService} from "src/app/modal.service";
    import { ModalComponent } from './modal/modal.component';
    
    @NgModule({
      declarations: [
        AppComponent,
        ModalComponent
      ],
      imports: [
        BrowserModule,
        BrowserAnimationsModule
      ],
      providers: [ModalService],
      bootstrap: [AppComponent],
      entryComponents: [ModalComponent]
    })
    export class AppModule { }
    

    Note I Defined the ModalComponent in the entryComponents and the ModalService as a provider.

    The app.component HTML:

    <button (click)="showModal()">Show Modal</button>
    

    And the app.component typescript:

    import { Component } from '@angular/core';
    import {ModalService} from "./modal.service";
    @Component({
      selector: 'app-root',
      templateUrl: './app.component.html',
      styleUrls: ['./app.component.css']
    })
    export class AppComponent {
      title = 'app';
    
      constructor(public modalService: ModalService){}
    
      showModal(){
           this.modalService.showModal("This is the modal name");
      }
    
    }
    

    I injected the ModalService into the constructor and called your showModal method in response to the button click in the main app.

    Load the app: Initial App Load

    Click the button and the modal shows up. It doesn't look like a modal, but that could be because of missing styling :

    App with Modal Shown

    Now click the close button inside the modal:

    Modal Closed

    You see the modal is gone and the console output displays your message.

    Does this post help you find the tidbit you missed?


    One thing to add. If you want to send data out of your modal to the component that called it, ust change your modalComponent's closeModal method:

      closeModal() {
        // console.log(this.onModalClose()) **shows zero observers**   :-(
        this.modalClose.next('Your Data Here, it can be an object if need be');
        this.modalClose.complete();
      }
    

    And your modal service can access the data in the onModalClose().subscribe() method:

    componentRef.instance.onModalClose().subscribe((results) => {
      console.log(results);
      console.log('I will be very happy if this this gets called, but it never ever does');
      this.closeModal();
    });