Angular Communication Between Tabs

bob.ts - Oct 18 '21 - - Dev Community

For a client project, I had a Summary and Detail Page. The Detail Page was opened in a new tab. They needed the summary to update when the state of the detail information changed.

Having worked with BroadcastChannel in the past (see HERE), I set about creating a service to handle this functionality.

Setup Code

First, I needed an interface ...

export interface BroadcastMessage {
  type: string;
  payload: any;
}
Enter fullscreen mode Exit fullscreen mode

Broadcast Service

Then, there's the code ...

import { Injectable } from '@angular/core';
import { Observable, Subject } from 'rxjs';
import { filter } from 'rxjs/operators';

import { BroadcastMessage } from '@core/interfaces/broadcast-message';

import config from '@core/constants/config.json';

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

  broadcastChannel: any;
  onMessage = new Subject<any>();

  constructor() {
    this.initialize();
  }

  initialize() {
    const name: string = config.details.detailChangeChannel;
    this.broadcastChannel = new BroadcastChannel(name);
    this.broadcastChannel.onmessage = (message) => this.onMessage.next(message.data);
  }

  publish(message: BroadcastMessage): void {
    this.broadcastChannel.postMessage(message);
  }

  messagesOfType(type: string): Observable<BroadcastMessage> {
    return this.onMessage.pipe(
      filter(message => message.type === type)
    );
  }

}
Enter fullscreen mode Exit fullscreen mode

As you can see, I pulled the initialization code out of the constructor; this makes it easier for me to test the code. The channel name is stored in a configuration JSON file.

There is a publish function that simply posts a message. In the initialize function we are watching the onmessage and passing the data to the onMessage Subject.

This then allows the developer to filter to the messages they are looking for using the messagesOfType function.

Here's a look at an implementation of messagesOfType.

this.broadcastService.messagesOfType(config.details.detailChangeEvent).subscribe(this.handleBroadcastMessage.bind(this));

...

handleBroadcastMessage = (): void => {
  this.getUpdatedData();
};
Enter fullscreen mode Exit fullscreen mode

The string passed in via messagesOfType above is also in the config.json file mentioned previously.

Here's a look at an implementation of publish.

this.broadcastService.publish({ type: config.details.detailChangeEvent, payload: '' });
Enter fullscreen mode Exit fullscreen mode

Unit Tests

This code deserves to be tested ...

import { TestBed } from '@angular/core/testing';

import { BroadcastService } from './broadcast.service';

import { BroadcastMessage } from '@core/interfaces/broadcast-message';

import config from '@core/constants/config.json';

describe('BroadcastService', () => {
  let service: BroadcastService;

  beforeEach(() => {
    TestBed.configureTestingModule({});
    service = TestBed.inject(BroadcastService);
  });

  it('should be created', () => {
    expect(service).toBeTruthy();
    expect(service.broadcastChannel.name).toEqual(config.details.detailChangeChannel);
  });

  it('expects "publish" to trigger a postMessage', () => {
    const message: BroadcastMessage = { type: 'TEST', payload: 'DATA' };
    spyOn(service.broadcastChannel, 'postMessage').and.stub();
    service.publish(message);
    expect(service.broadcastChannel.postMessage).toHaveBeenCalledWith(message);
  });

  it('expects "messagesOfType" to capture and return message if type matches', (done) => {
    const type: string = 'TEST';
    const message: BroadcastMessage = { type: type, payload: 'DATA' };
    let expected: BroadcastMessage = Object.assign({}, message);
    service.messagesOfType(type).subscribe(result => {
      expect(result).toEqual(expected);
      done();
    });
    service.onMessage.next(message);
  });
});
Enter fullscreen mode Exit fullscreen mode

Limitations

Keep the following in mind when using the BroadcastChannel. It will only work when ...

  • All browser windows are running on the same host and port.
  • All browser windows are using the same scheme (it will not work if one app is opened with https and the other with http).
  • The browser windows aren’t opened in incognito mode.
  • And browser windows are opened in the same browser (there is no cross-browser compatibility).

I will leave checking browser version compatibility to caniuse.

Summary

And, that's it. I now have a tool I can use in Angular to pass messages between tabs.

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .