All files / loading-spinner si-loading-spinner.directive.ts

100% Statements 35/35
87.5% Branches 7/8
100% Functions 12/12
100% Lines 32/32

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125                                                                1x       4x             4x             4x   4x 4x     4x 11x 6x 4x 4x   4x         3x                     4x   4x   3x       2x             3x 3x       4x 5x 3x 3x   2x 1x           6x 6x 6x         4x 4x 2x   4x      
/**
 * Copyright (c) Siemens 2016 - 2025
 * SPDX-License-Identifier: MIT
 */
import { ComponentPortal, DomPortalOutlet } from '@angular/cdk/portal';
import {
  booleanAttribute,
  computed,
  Directive,
  ElementRef,
  inject,
  Injector,
  input,
  OnChanges,
  OnDestroy,
  OnInit,
  ViewContainerRef
} from '@angular/core';
import { BehaviorSubject, combineLatest, merge, Subscription, timer } from 'rxjs';
import { filter, map, switchMap, takeUntil } from 'rxjs/operators';
 
import {
  LOADING_SPINNER_BLOCKING,
  LOADING_SPINNER_OVERLAY,
  SiLoadingSpinnerComponent
} from './si-loading-spinner.component';
@Directive({
  selector: '[siLoading]',
  host: {
    class: 'position-relative'
  }
})
export class SiLoadingSpinnerDirective implements OnInit, OnChanges, OnDestroy {
  /**
   * Displays the loading spinner when the value is either true or non-zero.
   */
  readonly siLoading = input.required<boolean | number>();
 
  /**
   * Displays semi-transparent backdrop for the spinner, default is false.
   *
   * @defaultValue false
   */
  readonly blocking = input(false, { transform: booleanAttribute });
 
  /**
   * Specifies if the spinner should be displayed after a delay, default is true.
   *
   * @defaultValue true
   */
  readonly initialDelay = input(true, { transform: booleanAttribute });
 
  private el = inject(ElementRef);
  private readonly viewRef = inject(ViewContainerRef);
 
  private sub?: Subscription;
  private progressSubject = new BehaviorSubject(false);
  private off$ = this.progressSubject.pipe(filter(val => !val));
  private on$ = this.progressSubject.pipe(filter(val => val));
  private readonly initialWaitTime = computed(() => (this.initialDelay() ? 500 : 0));
  private minSpinTime = 500;
  private portalOutlet?: DomPortalOutlet;
  private readonly compPortal = new ComponentPortal(
    SiLoadingSpinnerComponent,
    this.viewRef,
    Injector.create({
      providers: [
        { provide: LOADING_SPINNER_BLOCKING, useFactory: () => this.blocking() },
        {
          provide: LOADING_SPINNER_OVERLAY,
          useValue: true
        }
      ]
    })
  );
 
  // this makes sure the spinner only displays with a delay of 500ms and stays for 500ms so
  // that it doesn't flicker
  protected readonly spinner$ = this.on$.pipe(
    switchMap(() =>
      merge(
        timer(this.initialWaitTime()).pipe(
          map(() => true),
          takeUntil(this.off$)
        ),
        combineLatest([this.off$, timer(this.initialWaitTime() + this.minSpinTime)]).pipe(
          map(() => false)
        )
      )
    )
  );
 
  private createPortal(): void {
    this.portalOutlet ??= new DomPortalOutlet(this.el.nativeElement);
    this.compPortal.attach(this.portalOutlet);
  }
 
  ngOnInit(): void {
    this.sub = this.spinner$.subscribe(val => {
      if (val) {
        if (!this.compPortal.isAttached) {
          this.createPortal();
        }
      } else if (this.compPortal.isAttached) {
        this.compPortal.detach();
      }
    });
  }
 
  ngOnChanges(): void {
    const newState = !!this.siLoading();
    if (newState !== this.progressSubject.value) {
      this.progressSubject.next(newState);
    }
  }
 
  ngOnDestroy(): void {
    this.sub?.unsubscribe();
    if (this.compPortal.isAttached) {
      this.compPortal.detach();
    }
    this.portalOutlet?.dispose();
  }
}