Search code examples
angularobservableangular-servicesangular-componentseventemitter

Angular subscribe to EventEmitter but fetch existing items


I'm trying to build a notification component. For that I need to send notifications to NotificationComponent through service. My way of doing that is to send notifications to component by using EventEmitter but I want NotificationComponent to fetch existing notifications when it is first time routed to by subscribing to Observable of Notifications but it is not working. Looking at my code, please point out problem or suggest alternative way.

At present it only displays notification from notification number 2(That mean first notification does not arrive to NotificationComponent but 2 and so on arrive)

Following is the code of NotificationService

import { Injectable } from '@angular/core';
import { Notification } from './notification';
import { from } from 'rxjs';
import { Router } from '@angular/router';
import { EventEmitter } from '@angular/core';

@Injectable({
  providedIn: 'root'
})
export class NotificationService {
  private notifications = [];
  addEvent = new EventEmitter();
  removeEvent = new EventEmitter();

  constructor(private router: Router) {
  }

  add(title: string, message: string, expiresInSeconds: number = null) {
    const noti = new Notification();
    noti.title = title;
    noti.message = message;
    this.notifications.push();

    this.addEvent.emit(noti);
    this.router.navigate([{ outlets: {notification: 'notification'}}]);

    if (expiresInSeconds > 0) {
      setTimeout(() => {
        this.removeById(noti.id);
      }, expiresInSeconds * 1000);
    }
  }

  /* get notifications */
  getNotifications() {
    return from(this.notifications);
  }

  /* remove a notification */
  removeById(notificationId) {
    const index = this.notifications.findIndex(n => {
      return notificationId === n.id;
    });
    this.notifications.splice(index, 1);

    this.removeEvent.emit(notificationId);
  }

}

Following is the code of NotificationComponent class

import { Component, OnInit } from '@angular/core';
import { NotificationService } from '../notification.service';
import { Notification } from '../notification';
import { faWindowClose } from '@fortawesome/free-solid-svg-icons';
import { Router } from '@angular/router';

@Component({
  selector: 'app-notifications',
  templateUrl: './notifications.component.html',
  styleUrls: ['./notifications.component.scss']
})
export class NotificationsComponent implements OnInit {
  notifications: Notification[] = [];
  faWindowClose = faWindowClose;

  constructor(private ns: NotificationService,
              private router: Router) {
  }

  ngOnInit() {
    this.ns.getNotifications()
      .subscribe(notifications => {
      this.notifications = notifications;
    });

    this.ns.addEvent
      .subscribe((notification => {
        this.notifications.push(notification);
      }));

    this.ns.removeEvent
      .subscribe((notification => {
        const index = this.notifications.findIndex(n => {
          return n.id === notification.id;
        });
        this.notifications.splice(index, 1);
      }));
  }

  removeNotification(id) {
    this.ns.removeById(id);

    if (this.notifications.length <= 0) {
      this.router.navigateByUrl('/user-panel');
    }
  }
}

Following is the template html of NotificationComponent

<div *ngIf="notifications.length >= 1"
  class="notification d-flex flex-row-reverse p-0">
  <div *ngFor="let n of notifications"
    class="alert alert-dark mx-1 p-0">

    <div class="d-flex flex-row p-0">
      <div class="flex-item flex-grow-1">
        <h5>{{n.title}}</h5>
      </div>
      <div class="flex-item">
        <button class="btn btn-sm btn-secondary"
          (click)="removeNotification(n.id)">
          <fa-icon [icon]="faWindowClose"></fa-icon>
        </button>
      </div>
    </div>

    <div>{{n.message}}</div>
  </div>
</div>

Solution

  • convert addEvent to ReplaySubject. Your component renders after the first notification is emitted and subscribes to events only after the first event is gone. With ReplaySubject your component should get that first event.

    addEvent = new ReplaySubject(1);
    

    also it seems there is a small bug

    this.notifications.push();
    

    no notification is pushed to the array and will cause error when calling removeById