Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Angular 2/4 - Material Design Snackbars multiple message in sequence

I have implemented a "snackbar service" that display a snackbar:

snackbar.service.ts

import { Subscription } from 'rxjs/Subscription';
import { Subject } from 'rxjs/Subject';
import { Inject, Injectable, OnDestroy } from '@angular/core';
import { MatSnackBar, MdSnackBarConfig } from '@angular/material/snack-bar';
import { MdSnackBarRef, SimpleSnackBar } from '@angular/material/snack-bar';

export class SnackBarMessage  {
  message: string;
  action: string = null;
  config: MdSnackBarConfig = null;
}

@Injectable()
export class SnackBarService implements OnDestroy
{
    private messageQueue: Subject<SnackBarMessage> = new Subject<SnackBarMessage>();
    private subscription: Subscription;
    private snackBarRef:  MdSnackBarRef<SimpleSnackBar>;


    constructor(public snackBar: MatSnackBar){
        this.subscription = this.messageQueue.subscribe(message => { 
            this.snackBarRef = this.snackBar.open(message.message, message.action, message.config);
        });
    }

    ngOnDestroy() {
        this.subscription.unsubscribe();
    }

    /**
     * Add a message
     * @param message The message to show in the snackbar.
     * @param action The label for the snackbar action.
     * @param config Additional configuration options for the snackbar.
     */
    add(message: string, action?: string, config?: MdSnackBarConfig): void{

        if ( !config ){
            config = new MdSnackBarConfig();
            config.duration = 10000;
        }

        let sbMessage = new SnackBarMessage();
        sbMessage.message = message;
        sbMessage.action = action;
        sbMessage.config = config;

        this.messageQueue.next(sbMessage);
    }
}

I want display multiple snackbars in sequence:

test.component.ts

import { Component } from '@angular/core';
import { SnackBarService } from 'app/core/services/snackbar.service';

@Component({
  selector: 'app-test',
  templateUrl: './test.component.html',
  styleUrls: ['./test.component.scss']
})
export class TestComponent {

   constructor(public snackBarService: SnackBarService) {
     this.snackBarService.add('A');
     this.snackBarService.add('B');
     this.snackBarService.add('C');
   }
}

But all message are displayed at same time (overlapping).

How can I wait for a snackBar afterDismissed for display a new message into messageQueue?

like image 829
ar099968 Avatar asked Nov 21 '17 09:11

ar099968


Video Answer


2 Answers

As @Aamir Khan pointed out - using afterDismissed, I have tweaked your code a bit.

  showNext() {
  if (this.msgQueue === 0) {
    return;
  }

  let message = this.msgQueue.shift();
  this.isInstanceVisible = true;
  this.snackBarRef = this.snackBar.open(message.message, message.action, {duration: 2000});
  this.snackBarRef.afterDismissed().subscribe(() => {
    this.isInstanceVisible = false;
    this.showNext();
  });
}

And inside add() added this -

this.msgQueue.push(sbMessage);
if (!this.isInstanceVisible) {
     this.showNext(); 
}

Plunker

Caution - Its kind of a dirty and non standard way, not an ideal user experience (IMO), above code might have some memory leaks and race conditions, due to usage of flags.

like image 53
Ankit Avatar answered Sep 30 '22 12:09

Ankit


Here is my solution

import {Injectable, NgZone, OnDestroy} from '@angular/core';
import {MatSnackBar, MatSnackBarRef, TextOnlySnackBar} from '@angular/material/snack-bar';
import {BehaviorSubject, EMPTY} from 'rxjs';
import {concatMap} from 'rxjs/operators';


interface ToasterMessage {
  type: ToasterMessageType;
  message: string;
}

@Injectable({
  providedIn: 'root'
})
export class SnackService implements OnDestroy {

  private toastSteam: BehaviorSubject<ToasterMessage | {}> = new BehaviorSubject<ToasterMessage | {}>({});
  private toastStream$ = this.toastSteam.asObservable();

  constructor(private snackBar: MatSnackBar, private zone: NgZone) {
    // for each toast message we map an observable, and we concat to wait for the previous
    // to finish. We can use mergeMap or switchMap if we want other behavior
    this.toastStream$.pipe(
      concatMap((toast: ToasterMessage | {}) => {
        if (!('type' in toast)) {
          return EMPTY;
        }
        return this.handleToastMessage(toast).afterDismissed();
      })
    ).subscribe((_) => {
    });
  }


  openSnackBar(message: string,
               action: string | undefined,
               color: string = 'green-snackbar'): MatSnackBarRef<TextOnlySnackBar> {
    return this.zone.run(() => {
      const snackBarRef = this.snackBar.open(message, action, {
        duration: 4000,
        panelClass: color,
        horizontalPosition: 'center',
      });
      snackBarRef.onAction().subscribe(() => snackBarRef.dismiss());
      return snackBarRef;
    });
  }

  // just a helper method to make code simpler
  handleToastMessage(toast: ToasterMessage): MatSnackBarRef<TextOnlySnackBar> {
    return this.openSnackBar(toast.message, 'x', `${toast.type}-snackbar`);
  }

  addToast(toast: ToasterMessage): void {
    this.toastSteam.next(toast);
  }

  ngOnDestroy(): void {
    this.toastSteam.next({});
    this.toastSteam.complete();
  }

}
like image 35
Kostas Oreopoulos Avatar answered Sep 30 '22 11:09

Kostas Oreopoulos