ar099968
ar099968

Reputation: 7547

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?

Upvotes: 13

Views: 24989

Answers (7)

Kostas Oreopoulos
Kostas Oreopoulos

Reputation: 93

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();
  }

}

Upvotes: 1

Ravid Goldenberg
Ravid Goldenberg

Reputation: 2299

Facing the same issue with another requirement, the calling function needs to be informed when all snackbars were closed, I finally went with this solution using recursion:

The calling method:

testFunction(): void {
    this.showMsgs().subscribe(() => {
        // Do stuff after all messages were closed
    }
}

And the function that is showing all messages one after the other and letting the caller function that all the messengers were closed:

showMsgs(): Observable<any> {
    if (this.msgList.length) {
        const msg = this.msgList.pop();
        let closedEvent: Observable<any>;
        // If there are other msgs they should be shown first
        if (this.msgList.length) {
            closedEvent = this.showMsgs().pipe(
                switchMap(() => {
                    return this.snacBar.open(msg, 'Ok', {
                        verticalPosition: 'top' }).afterDismissed();
                }));
        } else { 
            // This is the only msg on the list, show it and return the close subscription
            closedEvent = this.snacBar.open(msg, 'Ok', {
                verticalPosition: 'top' }).afterDismissed();
        }
        return closedEvent;
    }

Upvotes: 0

Igor Simic
Igor Simic

Reputation: 540

list of messages:

messages = [
  {
    text: "This is message 1",
  },
  {
    text: "This is message 2",
  }

];

loop through them and display one message after another

message.forEach( (message, index) => {

        setTimeout(() => {

            this.snackBar.open(message.text, action, {
                duration: this.timeOut,
                verticalPosition: 'bottom', // 'top' | 'bottom'
                horizontalPosition: 'end', //'start' | 'center' | 'end' | 'left' | 'right'
                panelClass: [className],
            });


        }, index * (this.timeOut+500)); // 500 - timeout between two messages

    });

demo:

https://www.coditty.com/code/angular-material-display-multiple-snackbars-messages-in-sequence

Upvotes: 1

Ankit
Ankit

Reputation: 1510

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.

Upvotes: 15

MaylorTaylor
MaylorTaylor

Reputation: 5041

Here is @Ankit's Plunker broken out here.

import {Subscription} from 'rxjs';
import {Injectable, OnDestroy} from '@angular/core';
import {MatSnackBar, MatSnackBarConfig} from '@angular/material/snack-bar';
import {MatSnackBarRef, SimpleSnackBar} from '@angular/material/snack-bar';
import {SnackBarMessage} from './snackBarMessage.model';

@Injectable()
export class NotificationService implements OnDestroy {
  private messageQueue: Array<any> = Array<any>();
  private subscription: Subscription;
  private snackBarRef: MatSnackBarRef<SimpleSnackBar>;
  private isInstanceVisible = false;

  constructor(public snackBar: MatSnackBar) {}

  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.
   * @param classOverride Adds a css class on the snackbar so you can add color.

   */
  show(
    message: string,
    action?: string,
    config?: MatSnackBarConfig,
    classOverride: string = 'blue-snackbar'
  ): void {
    if (!config) {
      config = new MatSnackBarConfig();
      config.duration = 3000;
      config.verticalPosition = 'bottom';
      config.horizontalPosition = 'end';
      config.panelClass = [classOverride];
    }

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

    this.messageQueue.push(sbMessage);

    if (!this.isInstanceVisible) {
      this.showNext();
    }
  }

  private showNext() {
    if (this.messageQueue.length === 0) {
      return;
    }

    const message = this.messageQueue.shift();
    this.isInstanceVisible = true;

    this.snackBarRef = this.snackBar.open(
      message.message,
      message.action,
      message.config
    );

    this.snackBarRef.afterDismissed().subscribe(() => {
      this.isInstanceVisible = false;
      this.showNext();
    });
  }
}

Upvotes: 2

Shailesh Ladumor
Shailesh Ladumor

Reputation: 7252

you need to use time out method. hide snackbar in specific time and open another snackbar using timeout function

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

Upvotes: 2

Vivek Doshi
Vivek Doshi

Reputation: 58593

You can achieve this by making these simple changes :

this.snackBarService.add(['A','B','C']); // pass messages as array

add(messages: Array<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;

    messages.forEach((message,index) => {
        setTimeout((message) => {
            sbMessage.message = message;
            this.messageQueue.next(sbMessage);
        },(config.duration*index);
    })
}

Upvotes: 2

Related Questions