Skip to content

Commit

Permalink
feat(toast): Toast implementation (#3103)
Browse files Browse the repository at this point in the history
Implementation of twbs/bootstrap##22980
  • Loading branch information
Benoit Charbonnier committed Jun 24, 2019
1 parent 83f79cf commit bd1e9fb
Show file tree
Hide file tree
Showing 32 changed files with 874 additions and 81 deletions.
3 changes: 3 additions & 0 deletions demo/src/app/app.module.ts
Expand Up @@ -19,13 +19,15 @@ import {NgbdRatingModule} from './components/rating/rating.module';
import {NgbdTableModule} from './components/table/table.module';
import {NgbdTabsetModule} from './components/tabset/tabset.module';
import {NgbdTimepickerModule} from './components/timepicker/timepicker.module';
import {NgbdToastModule} from './components/toast/toast.module';
import {NgbdTooltipModule} from './components/tooltip/tooltip.module';
import {NgbdTypeaheadModule} from './components/typeahead/typeahead.module';
import {DefaultComponent} from './default';
import {GettingStartedPage} from './pages/getting-started/getting-started.component';
import {PositioningPage} from './pages/positioning/positioning.component';
import {NgbdSharedModule} from './shared';


const DEMOS = [
NgbdAccordionModule,
NgbdAlertModule,
Expand All @@ -42,6 +44,7 @@ const DEMOS = [
NgbdTableModule,
NgbdTabsetModule,
NgbdTimepickerModule,
NgbdToastModule,
NgbdTooltipModule,
NgbdTypeaheadModule
];
Expand Down
2 changes: 2 additions & 0 deletions demo/src/app/app.routing.ts
Expand Up @@ -16,6 +16,7 @@ import {ROUTES as RATING_ROUTES} from './components/rating/rating.module';
import {ROUTES as TABLE_ROUTES} from './components/table/table.module';
import {ROUTES as TABSET_ROUTES} from './components/tabset/tabset.module';
import {ROUTES as TIMEPICKER_ROUTES} from './components/timepicker/timepicker.module';
import {ROUTES as TOAST_ROUTES} from './components/toast/toast.module';
import {ROUTES as TOOLTIP_ROUTES} from './components/tooltip/tooltip.module';
import {ROUTES as TYPEAHEAD_ROUTES} from './components/typeahead/typeahead.module';
import {DefaultComponent} from './default';
Expand All @@ -42,6 +43,7 @@ const routes: Routes = [
{ path: 'components/rating', children: RATING_ROUTES },
{ path: 'components/table', children: TABLE_ROUTES },
{ path: 'components/tabset', children: TABSET_ROUTES },
{ path: 'components/toast', children: TOAST_ROUTES },
{ path: 'components/timepicker', children: TIMEPICKER_ROUTES },
{ path: 'components/tooltip', children: TOOLTIP_ROUTES },
{ path: 'components/typeahead', children: TYPEAHEAD_ROUTES },
Expand Down
@@ -0,0 +1,5 @@
<ngb-toast *ngIf="show" header="Click my close icon →" [autohide]="false"
(hide)="close()">
If you close me, I will automatically re-appear after a few seconds.
</ngb-toast>
<p *ngIf="!show">I'll be back!</p>
@@ -0,0 +1,10 @@
import {NgModule} from '@angular/core';
import {BrowserModule} from '@angular/platform-browser';
import {NgbModule} from '@ng-bootstrap/ng-bootstrap';

import {NgbdToastCloseable} from './toast-closeable';


@NgModule({imports: [BrowserModule, NgbModule], declarations: [NgbdToastCloseable], bootstrap: [NgbdToastCloseable]})
export class NgbdToastCloseableModule {
}
12 changes: 12 additions & 0 deletions demo/src/app/components/toast/demos/closeable/toast-closeable.ts
@@ -0,0 +1,12 @@
import {Component} from '@angular/core';

@Component({selector: 'ngbd-toast-closeable', templateUrl: './toast-closeable.html'})

export class NgbdToastCloseable {
show = true;

close() {
this.show = false;
setTimeout(() => this.show = true, 5000);
}
}
@@ -0,0 +1,8 @@
<ngb-toast>
<ng-template ngbToastHeader>
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24"><path d="M16.896 10l-4.896-8-4.896 8-7.104-4 3 11v5h18v-5l3-11-7.104 4zm-11.896 10v-2h14v2h-14zm14.2-4h-14.4l-1.612-5.909 4.615 2.598 4.197-6.857 4.197 6.857 4.615-2.598-1.612 5.909z"/></svg>
<strong class="mx-1">Fancy</strong>header here
</ng-template>
Hello, I am toast. Have you noticed my header has been generated from a Template?
</ngb-toast>
<ngb-alert type="secondary" [dismissible]="false">Clicking on the close icon won't do anything in this example.</ngb-alert>
@@ -0,0 +1,13 @@
import { NgModule } from '@angular/core';
import { FormsModule } from '@angular/forms';
import { BrowserModule } from '@angular/platform-browser';
import { NgbModule } from '@ng-bootstrap/ng-bootstrap';

import { NgbdToastCustomHeader } from './toast-custom-header';

@NgModule({
imports: [BrowserModule, FormsModule, NgbModule],
declarations: [NgbdToastCustomHeader],
bootstrap: [NgbdToastCustomHeader]
})
export class NgbdToastCustomHeaderModule {}
@@ -0,0 +1,4 @@
import { Component } from '@angular/core';

@Component({ selector: 'ngbd-toast-customheader', templateUrl: './toast-custom-header.html' })
export class NgbdToastCustomHeader {}
@@ -0,0 +1,11 @@
<p>Please click one of the button to see a Toast being displayed in the top right corner of your screen:</p>
<button class="btn btn-primary" (click)="showStandard()" ngbTooltip="Will disappear in 5s">Standard toast</button>&nbsp;
<button class="btn btn-success" (click)="showSuccess()" ngbTooltip="Will disappear in 10s">Success toast</button>&nbsp;

<ng-template #dangerTpl>
<svg xmlns="http://www.w3.org/2000/svg" fill="#fff" width="24" height="24" viewBox="0 0 24 24"><path d="M10.872 6.831l1.695 3.904 3.654-1.561-1.79 3.426 3.333.954-3.417 1.338 2.231 4.196-4.773-2.582-2.869 2.287.413-3.004-3.792-.726 2.93-1.74-1.885-2.512 3.427.646.843-4.626zm-.786-6.831l-1.665 9.119-6.512-1.228 3.639 4.851-5.548 3.294 7.108 1.361-.834 6.076 5.742-4.577 9.438 5.104-4.288-8.064 6.834-2.677-6.661-1.907 3.25-6.22-6.98 2.982-3.523-8.114z"/></svg>
Danger Danger !
</ng-template>
<button class="btn btn-danger" (click)="showDanger(dangerTpl)" ngbTooltip="Will disappear in 15s">Danger toast</button>&nbsp;

<app-toasts aria-live="polite" aria-atomic="true"></app-toasts>
@@ -0,0 +1,20 @@
import { Component } from '@angular/core';

import { ToastService } from './toast-service';

@Component({ selector: 'ngbd-toast-global', templateUrl: './toast-global.component.html' })
export class NgbdToastGlobal {
constructor(public toastService: ToastService) {}

showStandard() {
this.toastService.show('I am a standard toast');
}

showSuccess() {
this.toastService.show('I am a success toast', { classname: 'bg-success text-light', delay: 10000 });
}

showDanger(dangerTpl) {
this.toastService.show(dangerTpl, { classname: 'bg-danger text-light', delay: 15000 });
}
}
@@ -0,0 +1,13 @@
import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { NgbModule } from '@ng-bootstrap/ng-bootstrap';

import { NgbdToastGlobal } from './toast-global.component';
import { ToastsContainer } from './toasts-container.component';

@NgModule({
imports: [BrowserModule, NgbModule],
declarations: [NgbdToastGlobal, ToastsContainer],
bootstrap: [NgbdToastGlobal]
})
export class NgbdToastGlobalModule {}
14 changes: 14 additions & 0 deletions demo/src/app/components/toast/demos/howto-global/toast-service.ts
@@ -0,0 +1,14 @@
import { Injectable, TemplateRef } from '@angular/core';

@Injectable({ providedIn: 'root' })
export class ToastService {
toasts: any[] = [];

show(textOrTpl: string | TemplateRef<any>, options: any = {}) {
this.toasts.push({ textOrTpl, ...options });
}

remove(toast) {
this.toasts = this.toasts.filter(t => t !== toast);
}
}
@@ -0,0 +1,29 @@
import {Component, TemplateRef} from '@angular/core';

import {ToastService} from './toast-service';


@Component({
selector: 'app-toasts',
template: `
<ngb-toast
*ngFor="let toast of toastService.toasts"
[class]="toast.classname"
[autohide]="true"
[delay]="toast.delay || 5000"
(hide)="toastService.remove(toast)"
>
<ng-template [ngIf]="isTemplate(toast)" [ngIfElse]="text">
<ng-template [ngTemplateOutlet]="toast.textOrTpl"></ng-template>
</ng-template>
<ng-template #text>{{ toast.textOrTpl }}</ng-template>
</ngb-toast>
`,
host: {'[class.ngb-toasts]': 'true'}
})
export class ToastsContainer {
constructor(public toastService: ToastService) {}

isTemplate(toast) { return toast.textOrTpl instanceof TemplateRef; }
}
10 changes: 10 additions & 0 deletions demo/src/app/components/toast/demos/inline/toast-inline.html
@@ -0,0 +1,10 @@
<h6>Body only</h6>
<ngb-toast>
I am a simple static toast.
</ngb-toast>

<h6>With a text header</h6>
<ngb-toast header="Hello" [autohide]="false">
I am a simple static toast with a header.
</ngb-toast>
<ngb-alert type="secondary" [dismissible]="false">Clicking on the close icon won't do anything in this example.</ngb-alert>
@@ -0,0 +1,8 @@
import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { NgbModule } from '@ng-bootstrap/ng-bootstrap';

import { NgbdToastInline } from './toast-inline';

@NgModule({ imports: [BrowserModule, NgbModule], declarations: [NgbdToastInline], bootstrap: [NgbdToastInline] })
export class NgbdToastInlineModule {}
4 changes: 4 additions & 0 deletions demo/src/app/components/toast/demos/inline/toast-inline.ts
@@ -0,0 +1,4 @@
import { Component } from '@angular/core';

@Component({ selector: 'ngbd-toast-inline', templateUrl: './toast-inline.html' })
export class NgbdToastInline {}
@@ -0,0 +1,98 @@
<p>
Toasts provide feedback messages as notifications to the user.<br />
Goal is to mimic the push notifications available both on mobile and desktop operating systems.
</p>

<ngbd-overview-section [section]="sections['inline-usage']">
<p><a [routerLink]="['..', 'api']" fragment="NgbToast">NgbToast</a> component allows you to only render the corresponding markup. Use it in one of your templates, and you are done. It will render a toast.</p>
<ngbd-code [snippet]="TOAST_INLINE_BASIC"></ngbd-code>
<br />
<p>Live example available <a [routerLink]="['..', 'examples']" fragment="inline" title="Declarative inline usage">here</a>.</p>
<p>
Nonetheless, with this inline technique, you must handle the toast's lifecycle yourself, i.e. it won't disappear automagically or in other words we don't remove the markup, nor destroy the component.
</p><p>
To make it disappear, you can listen to the <a [routerLink]="['..', 'api']" fragment="NgbToast"><code>(hide)</code></a>
output and remove/destroy/hide it yourself, and <a routerLink="." fragment="toast-service">next section</a> details how to do that in a real application environment.
</p>
<ngbd-code [snippet]="TOAST_INLINE_LIFECYCLE"></ngbd-code>
</ngbd-overview-section>

<ngbd-overview-section [section]="sections['toast-service']">
<p>Let's take the opportunity to demonstrate how to simply build a global toast management service.</p>
<ngb-alert [dismissible]="false" type="secondary">
<strong>TLDR;</strong>
You don't feel reading these long explanations? Go to the live example <a [routerLink]="['..', 'examples']" fragment="howto-global" title="Toast management service">here</a>.
</ngb-alert>

<p>In order to create our global toast system, 3 simple steps need to be done:<p>
<ol>
<li>Create a global <code>AppToastService</code> to act as a global storage for toasts.</li>
<li>Create a container component <code>&lt;app-toasts&gt;</code>, acting as the host in the application to display your toasts.
You could use <code>&lt;ngb-toast&gt;</code> with an <code>*ngFor</code> to read the list of toasts to display from the service.</li>
<li>Finally, use this container component in your application.</li>
</ol>

<h4>1. Global toast service</h4>
<p>
Relying on Angular dependency injection to share some piece of logic application-wide is always a good and solid starting choice.
</p>
<p>
The service manages a collection of toasts. It also provides a public method to push a new toast to that same collection.
<ngbd-code [snippet]="APP_TOAST_SERVICE"></ngbd-code>
</p>

<ngb-alert [dismissible]="false" type="warning">
<svg:svg ngbdIcon="lightbulb" fill="currentColor" />
You could also create an interface to type your toast instead of using <code>any[]</code> here.
</ngb-alert>
<p>
Additionally, a method to remove an existing toast from the collection is also implemented.
<ngbd-code [snippet]="APP_TOAST_SERVICE_REMOVE"></ngbd-code>
</p>

<h4>2. Toast container component</h4>
<p>
As stated previously, <code>&lt;ngb-toast&gt;</code> only generates a valid Bootstrap toast markup.
You'll still have to position them properly on the screen.
<br />
Thus, as a suggestion, toasts could be rendered in the top right corner of the application, as a kind of overlay.
</p>
<p>
To achieve that, you could create a dedicated container component/element to render all toasts in a convenient way.
For example, this container could be positionned using CSS property <code>position: static</code>.
</p>
<ngb-tabset>
<ngb-tab title="Template">
<ngbd-code *ngbTabContent [snippet]="APP_TOASTS_CONTAINER_TPL"></ngbd-code>
</ngb-tab>
<ngb-tab title="Styles">
<div *ngbTabContent>
<ngbd-code [snippet]="APP_TOASTS_CONTAINER_STYLES"></ngbd-code>
<p>We provide a dedicated <code>ngb-toasts</code> CSS class you could use, or write your own styles in case some specificities would be needed.</p>
</div>
</ngb-tab>
<ngb-tab title="Component">
<ngbd-code *ngbTabContent [snippet]="APP_TOASTS_CONTAINER"></ngbd-code>
</ngb-tab>
</ngb-tabset>
<hr />
<p>
Lastly, let's use this container. Common sense would suggest to put it somewhere quite high in your hierarchy of components.
Your root component would be a good candidate.
</p>
<ngbd-code [snippet]="CONTAINER_USAGE"></ngbd-code>
<p>You're done! Just inject and use your <code>AppToastService</code> anywhere you want to create a new toast. <code>&lt;app-toasts&gt;</code> will take care of displaying them.</p>

<ngb-alert [dismissible]="false" type="warning" class="d-flex flex-row">
<div class="mr-1">
<svg:svg ngbdIcon="lightbulb" fill="currentColor" />
</div>
<div>
Note the accessibility attributes <code>aria-live="polite"</code> &amp; <code>aria-atomic="true"</code>. They are <strong>mandatory</strong> in order to be compliant with screen readers technology. More information available on <a href="https://getbootstrap.com/docs/4.3/components/toasts/#accessibility" target="_blank" rel="noopener noreferrer">Bootstrap documentation</a>.
</div>
</ngb-alert>

<p>
Click <a [routerLink]="['..', 'examples']" fragment="howto-global" title="Toast management service">here</a> to see an example a bit more advanced of this how-to.
</p>
</ngbd-overview-section>

0 comments on commit bd1e9fb

Please sign in to comment.