Skip to content

Instantly share code, notes, and snippets.

@isaaclyman
Last active May 1, 2019 18:03
Show Gist options
  • Save isaaclyman/94e327f3a071c118f2056d2080944409 to your computer and use it in GitHub Desktop.
Save isaaclyman/94e327f3a071c118f2056d2080944409 to your computer and use it in GitHub Desktop.
A service for Angular 7+ that provides plug-and-play status toasts for Observables that users care about.
--- TITLE FILE ---

ToastInfoService

This service uses the @healthcatalyst/cashmere design library.

The module that provides ToastInfoService should import CashmereModule from @healthcatalyst/cashmere and BrowserModule from @angular/platform-browser.

To use the service's followObservable method, choose any action in your app that results in an Observable, e.g. a user clicks a button which triggers an API call.

Before:

clickSubmit(): void {
  this.apiService.doSubmit().pipe(
    filter(result => result.status === 200),
    take(1)
  ).subscribe(result => {
    alert(`Submitted! Response was: ${result.message}`);
  });
}

After:

clickSubmit(): void {
  this.toastInfoService.followObservable(
    () => this.apiService.doSubmit(),
    'Submitting data'
  ).pipe(
    filter(result => result.status === 200),
    take(1)
  ).subscribe(result => {
    alert(`Submitted! Response was: ${result.message}`);
  });
}

All you have to do is wrap your Observable-producing method in a followObservable call and name it (use the format "doing thing"). ToastInfoService will handle a loading toast, a success toast, and an error toast with a Try Again button. If your subscriber specifies an error method, that method will be called only when an error toast disappears without the user clicking Try Again.

An optional third argument to followObservable is an object with three fields:

  • showSpinner: Defaults to true. If false, will not show a Loading toast while the Observable is waiting for a response.
  • showSuccess: Defaults to true. If false, will not show a Success toast if the Observable succeeds. This is good for situations where success is either assumed (e.g. initial page load) or self-evident (e.g. route change).
  • tryAgain: Defaults to true. If false and the Observable throws an error, the error toast will not have a "Try Again" button and any error subscribers will be called immediately.
import {Component} from '@angular/core';
@Component({
template: `
<div class="wrap">
<div class="icon">
<hc-icon fontSet="fa" fontIcon="fa-exclamation-circle" hcIconLg></hc-icon>
</div>
<div class="message">
<div class="header">
Error
</div>
<div class="body">
{{ message }}
</div>
<div *ngIf="tryAgain" class="action">
<button hc-button buttonStyle="secondary" size="sm" (click)="tryAgain()">Try Again</button>
</div>
</div>
</div>
`,
styles: [
`
.wrap {
background-color: #f13c45;
border-radius: 5px;
color: white;
display: flex;
min-height: 65px;
padding: 10px 10px;
width: 300px;
}
.icon {
align-items: center;
display: flex;
margin-left: 5px;
margin-right: 20px;
}
.message {
display: flex;
flex-direction: column;
justify-content: center;
}
.header {
font-size: 18px;
font-weight: 600;
margin-bottom: 4px;
}
.body {
font-size: 12px;
}
.action {
margin-top: 6px;
}
`
]
})
export class ToastErrorComponent {
message = '';
tryAgain: () => any;
}
import {Injectable} from '@angular/core';
import {HcToasterService} from '@healthcatalyst/cashmere';
import {ToastLoadingComponent} from './toast-loading.component';
import {ToastErrorComponent} from './toast-error.component';
import {Observable, Subject} from 'rxjs';
export interface FollowObservableOptions {
showSpinner?: boolean;
showSuccess?: boolean;
tryAgain?: boolean;
}
@Injectable()
export class ToastInfoService {
constructor(private toasterService: HcToasterService) {}
followObservable<T>(
method: () => Observable<T>,
displayName: string,
{showSpinner = true, showSuccess = true, tryAgain = true}: FollowObservableOptions = {
showSpinner: true,
showSuccess: true,
tryAgain: true
},
_subject?: Subject<T>
): Observable<T> {
const closeLoading = showSpinner ? this.showLoading(displayName) : () => {};
if (!_subject) {
_subject = new Subject<T>();
}
const observable = method();
observable.subscribe(
res => {
closeLoading();
if (showSuccess) {
this.showSuccess(`${displayName} succeeded.`);
}
_subject!.next(res);
_subject!.complete();
},
err => {
closeLoading();
this.showError(
`${displayName} failed.`,
tryAgain
? () => {
this.followObservable(method, displayName, {showSpinner, showSuccess, tryAgain}, _subject);
}
: undefined,
() => {
_subject!.error(err);
_subject!.complete();
}
);
}
);
return _subject.asObservable();
}
showLoading(message: string): () => void {
const ref = this.toasterService.addToast(
{
position: 'top-right',
timeout: 0,
clickDismiss: true,
type: 'custom'
},
ToastLoadingComponent,
{
message
}
);
return () => {
ref.close();
};
}
showSuccess(message: string): () => void {
const ref = this.toasterService.addToast({
header: 'Success',
body: message,
position: 'top-right',
timeout: 5000,
clickDismiss: true,
type: 'success'
});
return () => {
ref.close();
};
}
showError(message: string, tryAgainFn?: () => any, onFailure?: () => any): () => void {
let triedAgain = false;
const ref = this.toasterService.addToast(
{
position: 'top-right',
timeout: 8000,
clickDismiss: true,
type: 'custom',
toastClosed: () => {
if (!triedAgain && onFailure) {
onFailure();
}
}
},
ToastErrorComponent,
{
message,
tryAgain: tryAgainFn
? () => {
triedAgain = true;
tryAgainFn();
}
: undefined
}
);
return () => {
ref.close();
};
}
}
import {Component} from '@angular/core';
@Component({
selector: 'lm-toast-info-service-loading',
template: `
<div class="wrap">
<div class="spinner-container">
<hc-progress-spinner diameter="40" color="white"></hc-progress-spinner>
</div>
<div class="message">
<div class="header">
Just a moment
</div>
<div class="body">
{{ message }}
</div>
</div>
</div>
`,
styles: [
`
.wrap {
background-color: #f8961d;
border-radius: 5px;
color: white;
display: flex;
height: 65px;
padding: 20px 10px;
width: 300px;
}
.spinner-container {
align-items: center;
display: flex;
margin-left: 5px;
margin-right: 20px;
padding-left: 25px;
width: 50px;
}
.message {
display: flex;
flex-direction: column;
justify-content: center;
}
.header {
font-size: 18px;
font-weight: 600;
margin-bottom: 4px;
}
.body {
font-size: 12px;
}
`
]
})
export class ToastLoadingComponent {
message = '';
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment