File

src/app/utils/query-component/query.component.ts

Description

This component is intended to be used in combination with an Observable. The component expects that either an ng-template or a MatButton is in the body and that the appropriate directive is used on it (QueryBodyDirective with ng-template, QueryButtonDirective with button). Once the appropriate function is called, the component subscribes to the observable, showing the user a loading indication and, if applicable, disables the button. Once the query finished, the content in the ng-template is shown or the button is enabled again. If the subscription yielded an error, a message is shown instead of the ng-template, or the button is re-enabled. Note that no verification has to be made if the query was successful, when using an ng-template. The content of the ng-template is only executed once the #queryState is Ready.

Example 1: Using ng-template

This example will show, instead of the content directly, a loading indicator as soon as the page is loaded.

<app-query-component errorMessage="This is the message shown if the query failed">
  <ng-template appQueryBody>
    This will only be shown if the query was successful!
    <!-- <div *ngIf="this.data"> Not required! -->
    Result is: {{this.data}}
    <!-- </div> -->
  </ng-template>
</app-query-component>
// ...

@ViewChild(QueryComponent) query: QueryComponent;

// ...

ngAfterViewInit() {
  // Have to use ngAfterViewInit, otherwise query will be undefined!

  this.query.listenTo(this.someObservable,
    result => {
      this.data = result;
      console.log('Result callback');
    },
    error => {
      console.log('Error callback');
    }
  );
}

Example 2: Using button

This example will subscribe to the observable as soon as the button is clicked. The button is disabled and a loading indicator is shown as long as the subscription has not yielded a result.

<app-query-component errorMessage="Failed to do something">
  <button mat-raised-button (click)="this.onClick()" appQueryButton>
    Do something that takes a while...
  </button>
</app-query-component>
// ...

@ViewChild(QueryComponent) query: QueryComponent;

// ...

onClick() {
  this.query.listenTo(this.somethingThatTakesAWhile,
    result => {
      console.log('Result callback');
    },
    error => {
      console.log('Error callback');
    }
  );
}

Implements

OnDestroy AfterViewInit

Metadata

selector app-query-component
styleUrls query.component.scss
templateUrl query.component.html

Index

Properties
Methods
Inputs

Constructor

constructor(notify: UserNotifyService, changeDetector: ChangeDetectorRef)
Parameters :
Name Type Optional
notify UserNotifyService No
changeDetector ChangeDetectorRef No

Inputs

errorMessage
Type : string
Default value : 'Failed to run query!'

Error message to be shown if the subscription failed

Methods

Public listenTo
listenTo(query: Observable, success?: (value?: T) => void, error?: (undefined) => void)
Type parameters :
  • T

Subscribe the query component to the observable

Parameters :
Name Type Optional Description
query Observable<T> No

The observable

success function Yes

The function to be called if the query was successful

error function Yes

The function to be called if the query had an error

Returns : void
Public ready
ready()

Quick way of checking if this.query.queryState === QueryComponentState.Ready

Returns : boolean
Public setError
setError()

Manually sets this query component to the error state. This may be useful if e.g. the query ran successfully, the contained data in it however is not valid.

Returns : void
Private updateButton
updateButton()
Returns : void

Properties

body
Type : QueryBodyDirective
Decorators :
@ContentChild(QueryBodyDirective)
button
Type : QueryButtonDirective
Decorators :
@ContentChild(QueryButtonDirective)
buttonMode
Type : boolean

If true, a button is in the query body, not a template

queryState
Type : QueryComponentState
Default value : QueryComponentState.Loading

The current state of the query

Private Optional subscription
Type : Subscription
import {AfterViewInit, ChangeDetectorRef, Component, ContentChild, Directive, Input, OnDestroy, TemplateRef} from '@angular/core';
import {Observable, Subscription} from 'rxjs';
import {UserNotifyService} from '@app/user-notify/user-notify.service';
import {MatButton} from '@angular/material/button';

/**
 * The current state of the query
 */
export enum QueryComponentState {
  /** The query was executed successfully */
  Ready,
  /** The query has not yet finished */
  Loading,
  /** The query returned an error */
  Error
}

/**
 * This directive is used on an `ng-template`, to indicate that the contents of it should be shown once
 * the query finished successfully
 */
@Directive({
  selector: '[appQueryBody]'
})
export class QueryBodyDirective {
  constructor(public template: TemplateRef<unknown>) {}
}

/**
 * This directive is used on a `button`, to inidcate that the {@link QueryComponent} should manage the state of this
 * button
 */
@Directive({
  selector: '[appQueryButton]'
})
export class QueryButtonDirective {
  constructor(public element: MatButton) {}
}

/**
 * This component is intended to be used in combination with an `Observable`.
 * The component expects that either an `ng-template` or a `MatButton` is in the body and that the appropriate directive
 * is used on it ({@link QueryBodyDirective} with `ng-template`, {@link QueryButtonDirective} with `button`).
 * Once the appropriate function is called, the component subscribes to the observable, showing the user a loading
 * indication and, if applicable, disables the `button`.
 * Once the query finished, the content in the `ng-template` is shown or the button is enabled again.
 * If the subscription yielded an error, a message is shown instead of the `ng-template`, or the button is re-enabled.
 * Note that no verification has to be made if the query was successful, when using an `ng-template`.
 * The content of the `ng-template` is only executed once the {@link #queryState} is `Ready`.
 *
 * #### Example 1: Using `ng-template`
 * This example will show, instead of the content directly, a loading indicator as soon as the page is loaded.
 * ```html
 * <app-query-component errorMessage="This is the message shown if the query failed">
 *   <ng-template appQueryBody>
 *     This will only be shown if the query was successful!
 *     <!-- <div *ngIf="this.data"> Not required! -->
 *     Result is: {{this.data}}
 *     <!-- </div> -->
 *   </ng-template>
 * </app-query-component>
 * ```
 *
 * ```ts
 * // ...
 *
 * @ViewChild(QueryComponent) query: QueryComponent;
 *
 * // ...
 *
 * ngAfterViewInit() {
 *   // Have to use ngAfterViewInit, otherwise query will be undefined!
 *
 *   this.query.listenTo(this.someObservable,
 *     result => {
 *       this.data = result;
 *       console.log('Result callback');
 *     },
 *     error => {
 *       console.log('Error callback');
 *     }
 *   );
 * }
 * ```
 *
 * #### Example 2: Using `button`
 * This example will subscribe to the observable as soon as the button is clicked.
 * The button is disabled and a loading indicator is shown as long as the subscription has not yielded a result.
 * ```html
 * <app-query-component errorMessage="Failed to do something">
 *   <button mat-raised-button (click)="this.onClick()" appQueryButton>
 *     Do something that takes a while...
 *   </button>
 * </app-query-component>
 * ```
 *
 * ```ts
 * // ...
 *
 * @ViewChild(QueryComponent) query: QueryComponent;
 *
 * // ...
 *
 * onClick() {
 *   this.query.listenTo(this.somethingThatTakesAWhile,
 *     result => {
 *       console.log('Result callback');
 *     },
 *     error => {
 *       console.log('Error callback');
 *     }
 *   );
 * }
 * ```
 */
@Component({
  templateUrl: 'query.component.html',
  selector: 'app-query-component',
  styleUrls: ['query.component.scss']
})
export class QueryComponent implements OnDestroy, AfterViewInit {
  /** Error message to be shown if the subscription failed */
  @Input() errorMessage = 'Failed to run query!';
  @ContentChild(QueryBodyDirective) body: QueryBodyDirective;
  @ContentChild(QueryButtonDirective) button: QueryButtonDirective;

  /** @ignore */
  readonly State = QueryComponentState;
  /** The current state of the query */
  queryState: QueryComponentState = QueryComponentState.Loading;
  private subscription?: Subscription;
  /** If true, a button is in the query body, not a template*/
  buttonMode: boolean;

  constructor(private notify: UserNotifyService, private changeDetector: ChangeDetectorRef) {}

  ngAfterViewInit() {
    this.buttonMode = !this.body && !!this.button;
    if (this.buttonMode) {
      this.queryState = QueryComponentState.Ready;
    }
    this.changeDetector.detectChanges();
  }

  ngOnDestroy() {
    this.subscription?.unsubscribe();
  }

  /** Quick way of checking if `this.query.queryState === QueryComponentState.Ready` */
  public ready(): boolean {
    return this.queryState === QueryComponentState.Ready;
  }

  /**
   * Subscribe the query component to the observable
   * @param query The observable
   * @param success The function to be called if the query was successful
   * @param error The function to be called if the query had an error
   */
  public listenTo<T>(query: Observable<T>, success?: (value: T) => void, error?: (error) => void): void {
    this.queryState = QueryComponentState.Loading;
    this.changeDetector.detectChanges();
    this.subscription?.unsubscribe();
    this.updateButton();

    this.subscription = query.subscribe(
      (value: T) => {
        if (success) {
          success(value);

          // Check if the callback changed the query state, e.g. by calling setError()
          if (this.queryState === QueryComponentState.Error) {
            return;
          }
        }

        this.queryState = QueryComponentState.Ready;
        this.changeDetector.detectChanges();
        this.updateButton();
      },
      (err) => {
        if (error) {
          error(err);
        }

        this.setError();
        this.notify.notifyError(this.errorMessage, err);
      }
    );
  }

  /**
   * Manually sets this query component to the error state.
   * This may be useful if e.g. the query ran successfully, the contained data in it however is not valid.
   */
  public setError(): void {
    this.queryState = QueryComponentState.Error;
    this.changeDetector.detectChanges();
    this.updateButton();
  }

  private updateButton(): void {
    if (!this.buttonMode) {
      return;
    }

    this.button.element.disabled = this.queryState === QueryComponentState.Loading;
  }
}
<ng-container *ngIf="buttonMode; else bodyMode" xmlns="http://www.w3.org/1999/html">
  <div style="display: flex; align-items: center; width: 100%">
    <ng-content></ng-content>
    <span style="margin-left: 4px" *ngIf="queryState === State.Loading">
      <mat-spinner diameter="20"></mat-spinner>
    </span>
  </div>
</ng-container>

<ng-template #bodyMode>
  <ng-container *ngIf="queryState === State.Ready">
    <ng-container *ngTemplateOutlet="body.template"></ng-container>
  </ng-container>

  <div *ngIf="queryState === State.Error" class="container">
    <div class="error">
      <span class="error-text">{{ errorMessage }}</span>
    </div>
  </div>

  <div *ngIf="queryState === State.Loading" class="container">
    <div class="load-container">
      <mat-spinner diameter="30" style="margin-right: 12px"></mat-spinner>
      <span style="font-size: 1.2em">Loading...</span>
    </div>
  </div>
</ng-template>

query.component.scss

.container {
  display: flex;
  flex-direction: column;
  justify-content: center;
  align-items: center;
  width: 100%;
  height: 100%;
  margin: 10px;
}

.error {
  border: red 1px solid;
  border-radius: 10px;
  padding: 18px;
  width: fit-content;
  background-color: rgba(255, 0, 0, 0.1);
}

.load-container {
  display: flex;
  flex-direction: row;
  justify-content: center;
  align-items: center;
  width: fit-content;
  height: fit-content;
  background-color: #ababab;
  padding: 10px 16px 10px 16px;
  border-radius: 100px;
  border: 1px solid rgba(0, 0, 0, 0.1);
  font-weight: 500;
  color: whitesmoke;
}

.error-text {
  color: red;
}
Legend
Html element
Component
Html element with directive

results matching ""

    No results matching ""