PrimuS
PrimuS

Reputation: 2683

Observable updates UI only once

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?

Upvotes: 2

Views: 353

Answers (2)

Goga Koreli
Goga Koreli

Reputation: 2947

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

Upvotes: 2

Rafi Henig
Rafi Henig

Reputation: 6414

this is an expected behavior since this.participantList refers to a value which is already stored by the subject (since the reference hasn't been changed), you might want to spread your array to create a new array each time you want to update its value:

this._participants.next(....this.participantList);

Upvotes: 2

Related Questions