Reputation: 7547
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
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
Reputation: 2299
Facing the same issue with another requirement, the calling function needs to be informed when all snackbar
s 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
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
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();
}
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
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
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
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