File

src/app/dialogs/http-progress/http-progress.dialog.ts

Description

Dialog that presents a progressbar for HTTP-requests, handles errors and automatically closes after receiving the result.

Implements

OnDestroy

Metadata

Index

Properties
Methods

Constructor

Public constructor(dialogRef: MatDialogRef<HttpProgressDialog<T>>, request: Observable>, snackBarService: SnackBarService)
Parameters :
Name Type Optional
dialogRef MatDialogRef<HttpProgressDialog<T>> No
request Observable<HttpEvent<T>> No
snackBarService SnackBarService No

Methods

Public cancelRequest
cancelRequest()
Returns : void
Public ngOnDestroy
ngOnDestroy()
Returns : void
Private onError
onError(error: literal type | undefined)
Parameters :
Name Type Optional
error literal type | undefined No
Returns : Observable<HttpEvent<T>>
Private onHeaderReceived
onHeaderReceived()
Returns : void
Private onProgress
onProgress(progress: HttpProgressEvent)
Parameters :
Name Type Optional
progress HttpProgressEvent No
Returns : void
Private onResponse
onResponse(response: HttpResponse)
Parameters :
Name Type Optional
response HttpResponse<T> No
Returns : void
Private onSent
onSent()
Returns : void

Properties

Public Readonly progress$
Default value : new EventEmitter<number>()
Public Readonly progressMode$
Type : Observable<ProgressBarMode>
Default value : this.state.pipe( map((state) => { switch (state) { case 'sending': return 'buffer'; case 'querying': return 'query'; default: return 'determinate'; } }), )
Private Optional requestSubscription
Type : Subscription
Public Readonly state
Default value : new EventEmitter<State>()
import { HttpEvent, HttpEventType, HttpProgressEvent, HttpResponse } from '@angular/common/http';
import { Component, EventEmitter, Inject, OnDestroy } from '@angular/core';
import { MAT_LEGACY_DIALOG_DATA as MAT_DIALOG_DATA, MatLegacyDialogRef as MatDialogRef } from '@angular/material/legacy-dialog';
import { LegacyProgressBarMode as ProgressBarMode } from '@angular/material/legacy-progress-bar';
import { Observable, of, Subscription } from 'rxjs';
import { catchError, map } from 'rxjs/operators';

import { TranslationDTO } from 'src/app/model/dto/translation.dto';
import { SnackBarService } from 'src/app/services/snack-bar.service';

type State = 'sending' | 'querying' | 'fetching';

/**
 * Dialog that presents a progressbar for HTTP-requests, handles errors and automatically closes after receiving the result.
 */
@Component({
  templateUrl: './http-progress.dialog.html',
  styleUrls: ['./http-progress.dialog.scss'],
})
export class HttpProgressDialog<T> implements OnDestroy {
  public readonly progress$ = new EventEmitter<number>();

  public readonly state = new EventEmitter<State>();

  public readonly progressMode$: Observable<ProgressBarMode> = this.state.pipe(
    map((state) => {
      switch (state) {
        case 'sending':
          return 'buffer';
        case 'querying':
          return 'query';
        default:
          return 'determinate';
      }
    }),
  );

  private requestSubscription?: Subscription;

  public constructor(
    private readonly dialogRef: MatDialogRef<HttpProgressDialog<T>>,
    @Inject(MAT_DIALOG_DATA) private readonly request: Observable<HttpEvent<T>>,
    private readonly snackBarService: SnackBarService,
  ) {
    dialogRef.disableClose = true;
    this.requestSubscription = this.request.pipe(catchError((error) => this.onError(error))).subscribe((event) => {
      switch (event.type) {
        case HttpEventType.Sent:
          this.onSent();
          break;
        case HttpEventType.ResponseHeader:
          this.onHeaderReceived();
          break;
        case HttpEventType.DownloadProgress:
          this.onProgress(event);
          break;
        case HttpEventType.Response:
          this.onResponse(event);
      }
    });
  }

  public ngOnDestroy(): void {
    this.requestSubscription?.unsubscribe();
  }

  public cancelRequest(): void {
    this.dialogRef.close();
  }

  private onSent(): void {
    this.state.emit('querying');
  }

  private onHeaderReceived(): void {
    this.state.emit('fetching');
  }

  private onProgress(progress: HttpProgressEvent): void {
    const current = progress.loaded;
    // Use maximum safe integer as fallback, if no Content-Length Header is present.
    const total = progress.total ?? Number.MAX_SAFE_INTEGER;
    this.progress$.emit((100 * current) / total);
  }

  private onResponse(response: HttpResponse<T>): void {
    const body = response.body;
    if (body === undefined || body === null) {
      this.onError({ error: { message: 'api.error.unknown' } });
    } else {
      this.dialogRef.close(response.body);
    }
  }

  private onError(error: { error?: { message: string | TranslationDTO | undefined } } | undefined): Observable<HttpEvent<T>> {
    // Delay closing of dialog to prevent flickering.
    setTimeout(() => this.dialogRef.close(), 250);
    const message = error?.error?.message ?? 'api.error.unknown';
    if (typeof message === 'string') {
      this.snackBarService.openSnackBar({ key: message }, undefined, 10000);
    } else {
      // Message is TranslationDTO.
      this.snackBarService.openSnackBar(message, undefined, 10000);
    }
    return of();
  }
}
<mat-progress-bar [mode]="(progressMode$ | async) || 'buffer'" [value]="(progress$ | async) || 0"></mat-progress-bar>
<button mat-icon-button color="warn" (click)="cancelRequest()"><mat-icon>cancel</mat-icon></button>

./http-progress.dialog.scss

:host {
  display: flex;
  align-items: center;
  justify-content: space-between;
}
Legend
Html element
Component
Html element with directive

results matching ""

    No results matching ""