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;
}
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)
);
}
}
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();
};
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: '' });
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);
});
});
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.