I am beginning to learn Angular (not AngularJS / old Angular, but the new hotness), and came across the need to communicate a simple status from one component to another. In most other component-based UI programming paradigms, the thing to do here is to use a Message Bus service to communicate between components. The advantage is low coupling; components don’t need to know about each other, they just need to know about this thing called a Message Bus.
I did some digging, and came across this old blog post about how to build a message bus service in Angular. Unfortunately, copying the code into my IDE gave me an error about a missing filter method… Alas. After fiddling for a while, here’s what I came up with instead:
import { Injectable } from '@angular/core';
import {Observable, Subject, Subscription} from 'rxjs';
import {filter} from 'rxjs/operators';
interface Message {
channel: string;
data: any;
}
@Injectable({
providedIn: 'root'
})
export class MessagingService {
private message$: Subject<Message>;
constructor() {
this.message$ = new Subject<Message>();
}
public publish<T>(message: T): void {
const channel = (<any>message.constructor).name;
this.message$.next({ channel: channel, data: message });
}
public subscribe<T>(messageType: { new(...args: any[]): T }, subscriber: (value: T) => void): Subscription {
const channel = (<any>messageType).name;
const filtered = this.message$.subscribe(m => {
if (m.channel === channel) {
subscriber(m.data);
}
});
return filtered;
}
}
Now I recognize that there are quite a few issues with this implementation. Not the least of which is that each subscriber gets every message, and then has to do string comparison on a payload field to determine if the message is one that they are interested in. After reworking it, I came up with a much more CPU-efficient implementation:
import { Injectable } from '@angular/core';
import {Subject, Subscription} from 'rxjs';
@Injectable({
providedIn: 'root'
})
export class MessagingService {
private subjects$: Map<string, Subject<any>>;
constructor() {
this.subjects$ = new Map<string, Subject<any>>();
}
private getOrCreateChannel(channelName) {
let channel = this.subjects$.get(channelName);
if (channel === undefined) {
channel = new Subject<any>();
this.subjects$.set(channelName, channel);
}
return channel;
}
public publish<T>(message: T): void {
const channelName = (<any>message.constructor).name;
const channel = this.getOrCreateChannel(channelName);
channel.next(message);
}
/**
* A more efficient publish method, for cases where a publisher repeatedly produces values on the same topic
* @param messageType the type of message that we expect tp publish
*/
public getPublisher<T>(messageType: { new(...args: any[]): T }): (T) => void {
return this.getOrCreateChannel((<any>messageType).name).next;
}
public subscribe<T>(messageType: { new(...args: any[]): T }, subscriber: (value: T) => void): Subscription {
const channelName = (<any>messageType).name;
const channel = this.getOrCreateChannel(channelName);
return channel.subscribe(subscriber);
}
}