angulartypescriptrxjsrxjs-observablesbehaviorsubject

Passing Data Between Components in Angular using a service with a RxJS subject


What I'm Trying To Do

I have two unrelated components in my Angular client app. The first component is responsible for fetching and processing data from the server. I'm trying to use the subscribe callback function to navigate to a new page and show a processed data (blob). To be more specific I want to go to a new tab and do a preview of the word document using an unrelated component. I found that it is possible to pass data through unrelated components using a service with a RxJS subject.

What I'm Getting

I've implemented some code, but a blob doesn't reach the preview component.

What I've tried

Here's some code I came up with:

Service for transferring a Blob object

@Injectable()
export class DocPreviewService {
  private blob = new BehaviorSubject<Blob | undefined>(undefined);
  public share$ = this.blob.asObservable();

  constructor() {}

  setBlob(blob: Blob) {
    this.blob.next(blob);
  }
}

Function responsible for retrieving the blob from the server (first component)

  showReport(selected_id: string) {
    const url = this.router.serializeUrl(this.router.createUrlTree([`tools/${selected_id}/preview`]));
    // it's important that the page opens in a new tab
    window.open(url, '_blank');

    this.report_service.createReport(this.calc_items[selected_id]).subscribe({
      next: (doc_blob: Blob) => {
        {          
          this.doc_preview_service.setBlob(doc_blob);
        }
      },
      error: (error: any) => {
      },
    })
  }

Component responsible for viewing a document (second component)

export class DocPreviewComponent implements OnChanges, AfterViewInit, OnDestroy {
  doc_blob?: Blob;
  @ViewChild('doc_preview') doc_preview!: ElementRef;
  subscription: Subscription;
  
  constructor(private doc_preview_service: DocPreviewService, private local_storage: LocalStorageService) {
    this.subscription = this.doc_preview_service.share$.subscribe(blob => {
      this.doc_blob = blob;
    });
  }

  ngOnChanges(changes: SimpleChanges): void {}

  ngAfterViewInit(): void {
    if (this.doc_blob) {
      doc.renderAsync(this.doc_blob, this.doc_preview.nativeElement)
      .then(x => console.log("docx: finished"));
    }
  }

  ngOnDestroy(): void {
    this.subscription.unsubscribe();
  }
}

Any help would be much appreciated. Thank you.


Solution

  • Actually, the subject emitter method has the limitation that it only works for communication between unrelated components in a single tab, cross tab communication is not possible, because the service will have two separate instances in each tab, so the event is never received

    To solve this scenario, all you need to do is append the API call params to the URL

    Code opening the tab

      showReport(selected_id: string) {
        // basically we append the required param needed for making the API 
        // call, then we use it to call the blob logic on the other tab initialization!
        const url = this.router.serializeUrl(this.router.createUrlTree([`tools/${selected_id}/preview?id=${this.calc_items[selected_id]}`]));
        // it's important that the page opens in a new tab
        window.open(url, '_blank');
      }
    

    The component opening the blob will be

    export class DocPreviewComponent implements OnChanges, AfterViewInit, OnDestroy {
      doc_blob?: Blob;
      @ViewChild('doc_preview') doc_preview!: ElementRef;
      subscription: Subscription;
      
      constructor(private doc_preview_service: DocPreviewService, private local_storage: LocalStorageService) { }
    
      ngOnChanges(changes: SimpleChanges): void {}
    
      ngAfterViewInit(): void {
          this.report_service.createReport(this.activatedRoute.snapshot.queryParams.id).subscribe({
             next: (doc_blob: Blob) => {
                  this.doc_blob = doc_blob;
                  if (this.doc_blob) {
                      doc.renderAsync(this.doc_blob, this.doc_preview.nativeElement)
                      .then(x => console.log("docx: finished"));
                  }
             },
             error: (error: any) => {},
         })
      }
    
      ngOnDestroy(): void {
        this.subscription.unsubscribe();
      }
    }
    

    This above code might contain error, but I hope you take this approach and solve your issue!