How can I use an AlertService (mat snackbar for error messages) inside another service?

Viewed 2438

I'm trying to subscribe an observable inside a service. However, I need to use AlertService for showing errors. A service inside another service (circular dependency?).

Here is AlertService

@Injectable()
export class AlertService {
  private subject = new Subject<any>();
  private keepAfterNavigationChange = false;

  constructor(private router: Router) {
    // clear alert message on route change
    router.events.subscribe(event => {
      if (event instanceof NavigationStart) {
        if (this.keepAfterNavigationChange) {
          // only keep for a single location change
          this.keepAfterNavigationChange = false;
        } else {
          // clear alert
          this.subject.next();
        }
      }
    });
  }

  success(message: string, keepAfterNavigationChange = false) {
    this.keepAfterNavigationChange = keepAfterNavigationChange;
    this.subject.next({ type: 'success', text: message });
  }

  error(message: string, keepAfterNavigationChange = false) {
    this.keepAfterNavigationChange = keepAfterNavigationChange;
    this.subject.next({ type: 'error', text: message });
  }

  getMessage(): Observable<any> {
    return this.subject.asObservable();
  }
}

AlertService becames a Mat Snackbar on AlertComponent. I'm rendering this snackbar on another components.

export class AlertComponent implements OnInit {
  message: any;

  constructor(private alertService: AlertService, public snackBar: MatSnackBar) { }

  ngOnInit() {
    // trigger Snackbar after AlertService is called
    this.alertService.getMessage().subscribe(message => {
      if (message != null) {
        // there is a message to show, so change snackbar style to match the message type
        if (message.type === 'error') {
          this.snackBar.open(message.text, undefined, { duration: 8000, verticalPosition: 'bottom', panelClass: ['snackbar-error'] });
        } else if (message.type === 'success') {
          this.snackBar.open(message.text, undefined, { duration: 8000, verticalPosition: 'bottom', panelClass: ['snackbar-success'] });
        } else {
          this.snackBar.open(message.text, undefined, { duration: 8000, verticalPosition: 'bottom' });
        }
      }
    });
  }

}

I'm able to subscribe inside components like this:

export class AboutComponent implements OnInit {
  
  ngOnInit() {
    this.emailService.sendEmail('example@gmail.com')
      .subscribe(code => {
          console.log(code);
          this.alertService.success('Thanks for your message!');
      }, error => {
        this.alertService.error('Error sending message.');
      }
    );
  }
      
}

@Injectable()
export class EmailService {

    constructor(private http: HttpClient) { }

    sendEmail(email: Email) {
        return this.http.post(BACKEND_URL + 'send', email);
    }
}

However im trying to subscribe inside service because EmailService will be used in more than one component. How can I achieve this behavior?

2 Answers

Your service can be injected in other services

@Injectable()export class EmailService {

constructor(private http: HttpClient, private alertService: AlertService) { }

sendEmail(email: Email) {
    return this.http.post(BACKEND_URL + 'send', email).map( result => this.alertService.alert(result););


  }
}

It would have been circular if AlertService uses EmailService and EmailService uses AlertService

The only drawback of this solution is that when you have multiple alerts, the one showing will be dismissed so that you won't have the time to read it. I modified slightly your code to allow multiple alerts. Alertservice looks like this:

export class AlertService {
  
  private subject = new ReplaySubject<any>();

  private  showsubject = new BehaviorSubject<any>(1);
  
  public alert =                
  zip(this.subject.asObservable(),this.showsubject.asObservable());

  success(message: string) {
    this.subject.next({ type: 'success', text: message });
  }

  error(message: string) {
    this.subject.next({ type: 'error', text: message });
  }

  shownext(){
    this.showsubject.next(1);
  }
}

and in AlertComponent we have:

    ngOnInit(): void {  
    
    this.alertService.alert.subscribe(([message,show]) => {
      if ( message&&message.text ) {
        // there is a message to show, so change snackbar style to match the message type
        if (message.type === 'error') {
         this.snackbarref  =  this.snackBar.open(message.text, 'Ok', { verticalPosition: 'top', panelClass: ['snackbar-error'] });
        } else if (message.type === 'success') {
          this.snackbarref= this.snackBar.open(message.text, undefined, { duration: 3000, verticalPosition: 'top', panelClass: ['snackbar-success'] });
        } else {
          this.snackbarref= this.snackBar.open(message.text, 'Ok', { duration: 3000, verticalPosition: 'top' });
        }   
        this.snackbarref.afterDismissed().subscribe(()=>this.alertService.shownext());
      }
      else this.alertService.shownext();
    });    
  }

Practiclly the zip operator is used to control when the next alert is shown.

Related