I am fairly new to Angular (10) and try to grasp the concept of Observables. I have a Service and a Component, the services fills an array with participants and the component is supposed to display them
Service
export class MercureService {
private _participants = new BehaviorSubject<ParticipantDTO[]>([]);
private participantList: Array<ParticipantDTO> = [];
constructor() {
}
get participants$(): Observable<Array<ParticipantDTO>> {
return this._participants.asObservable();
}
admin_topic(topic: AdminTopic): void {
const url = new URL(environment.mercure);
url.searchParams.append('topic', `${topic.sessionUuid}_${topic.userUuid}`);
const eventSource = new EventSource(url.toString());
eventSource.onmessage = event => {
const json = JSON.parse(event.data);
console.log('NEW EVENT');
// ...
if (json.event === BackendEvents.NEW_PARTICIPANT) {
this.participantList.push({voter: json.data.voter, voterUuid: json.data.voterUuid, vote: '0'});
this._participants.next(this.participantList);
}
};
}
Component.ts
export class StartSessionComponent implements OnInit, OnDestroy {
// ...
unsubscribe$: Subject<void> = new Subject<void>();
participantList: ParticipantDTO[];
constructor(
// ...
public mercure: MercureService
) {}
ngOnInit(): void {
this.mercure.participants$
.pipe(takeUntil(this.unsubscribe$))
.subscribe((data) => {
this.participantList = data;
});
this.mercure.admin_topic({sessionUuid: '123', userUuid: '456'});
}
ngOnDestroy(): void {
this.unsubscribe$.next();
this.unsubscribe$.complete();
}
Component.html
...
<div *ngFor="let participant of this.mercure.participants$ | async">
<p>{{participant.voter}} - Vote ({{participant.vote}})</p>
</div>
...
So I am no sending a message and it gets picked up by the EventSource, the console
says
NEW EVENT
and the UI gets updated (adds a new <p>WHATEVER NAME - VOTE XXX</p>
). However, when I send a second message from the Server, I get
NEW EVENT
again, but the UI does not get updated. I suspect I am doing sth wrong with the Observable, can somebaody help please?
The issue is that EventSource events are emitted outside of Angular, so that whatever happens inside your eventSource.onmessage
is not updating the UI properly. That's why you need to wrap whatever is happening inside the onmessage
to be run inside the Angular with the help of NgZone.
See example:
constructor(
private zone: NgZone
) { }
admin_topic(topic: AdminTopic): void {
const url = new URL(environment.mercure);
url.searchParams.append('topic', `${topic.sessionUuid}_${topic.userUuid}`);
const eventSource = new EventSource(url.toString());
eventSource.onmessage = event => {
this.zone.run(() => { // This is what you need
const json = JSON.parse(event.data);
console.log('NEW EVENT');
// ...
if (json.event === BackendEvents.NEW_PARTICIPANT) {
this.participantList.push({ voter: json.data.voter, voterUuid: json.data.voterUuid, vote: '0' });
this._participants.next(this.participantList);
}
})
};
}
Another solution would be to use Event Source wrapper that will do the exact same thing for you and will give you the ease of use. It will be also wrapped in the Observable, so that you can have rich experience. See this article for example: Event Source wrapped with NgZone and Observable