Skip to content

Instantly share code, notes, and snippets.

Embed
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"
(click)="actionClicked(idx)">
{{action}}
</button>
</div>
</div>
import { Component } from "@angular/core";
import { Observable, Subject } from "rxjs";
import { BannerService } from "./banner.service";
@Component({
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) {
bannerService.init(this);
}
// 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!");
return;
}
// Click subject can only ever emit one value
this._clicks.next(idx);
this._clicks.complete();
this._clicks.unsubscribe();
this._clicks = undefined;
}
}
import { Injectable } from "@angular/core";
import { Observable, never, Subject } from "rxjs";
import { BannerOutlet } from "./banner.component";
@Injectable({
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 (!this.active) {
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.active = this.outlet!.open(message, actions);
// When the user selects an action, the banner will close
this.active.subscribe(next => {
// That means we stop watching the old banner, and...
this.active = 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);
}
});
return this.active;
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment