Skip to content

Instantly share code, notes, and snippets.

What would you like to do?
Material Banner component
div.container {
width: 100%;
box-sizing: border-box;
/* Per Banner spec, with bigger left gutter to account for menu button */
padding: 8px 8px 8px 72px;
display: flex;
flex-direction: row;
align-items: flex-end;
overflow: hidden;
p.message {
flex-grow: 1;
.action-row {
margin: 0 0 0 90px;
flex-grow: 0;
white-space: nowrap;
<div class="container mat-background-default" *ngIf="opened">
<p class="message mat-body-strong" [innerHTML]="message"></p>
<div class="action-row">
<button *ngFor="let action of actions; let idx=index;"
type="button" mat-button color="accent"
import { Component } from "@angular/core";
import { Observable, Subject } from "rxjs";
import { BannerService } from "./banner.service";
selector: "banner-outlet",
templateUrl: "./banner.component.html",
styleUrls: [ "./banner.component.css" ],
export class BannerOutlet {
// Text message to display
private _message?: string;
public get message(): string | undefined { return this._message; }
// List of button labels to show
private _actions?: string[];
public get actions(): string[] | undefined { return this._actions; }
// Emits one value when the user picks an action
private _clicks?: Subject<number>;
// True if the panel is opened
public get opened(): boolean { return !!this._clicks; }
constructor(bannerService: BannerService) {
// Open this banner with a message and at least one action
open(message: string, actions: string[]): Observable<number> {
if (this._clicks) {
throw "Tried to open banner when outlet was already opened.";
if (actions.length === 0) {
throw "Tried to open banner without any action buttons.";
this._message = message;
this._actions = actions;
this._clicks = new Subject();
return this._clicks.asObservable();
actionClicked(idx: number): void {
if (!this._clicks) {
console.log("Developer Error: banner action clicked but observable available!");
// Click subject can only ever emit one value;
this._clicks = undefined;
import { Injectable } from "@angular/core";
import { Observable, never, Subject } from "rxjs";
import { BannerOutlet } from "./banner.component";
providedIn: "root",
export class BannerService {
private outlet?: BannerOutlet;
private active?: Observable<number>;
private pending: {message: string, actions: string[], ret: Subject<number>}[] = [];
// Use the supplied BannerOutlet to display messages
public init(val: BannerOutlet) {
if (this.outlet) { throw "Can't have more than one Outlet for Banner Service!"; }
this.outlet = val;
// Display a message with at least one action. Returned observable will
// emit the index of the selected action once the user clicks a button.
public open(message: string, actions: string[]): Observable<number> {
if (!this.outlet) {
console.log("Tried to open banner but no outlet was defined.", message, actions);
return never();
if (! {
return this.doOpen(message, actions);
} else {
const ret: Subject<number> = new Subject();
this.pending.push({message, actions, ret});
return ret.asObservable();
// Actually show the banner in the outlet
private doOpen(message: string, actions: string[]): Observable<number> {
// Open the outlet and save the observable = this.outlet!.open(message, actions);
// When the user selects an action, the banner will close => {
// That means we stop watching the old banner, and... = undefined;
// If there was another queued, we show it
const args = this.pending.shift();
if (args) {
setTimeout(() => this.doOpen(args.message, args.actions).subscribe(args.ret), 500);
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment