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

100% Statements 37/37
87.5% Branches 7/8
100% Functions 12/12
100% Lines 34/34

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 126 127 128                                                                  1x       4x             4x             4x   4x 4x 4x     4x 11x 6x 4x 4x   4x         2x                     4x   4x   2x       2x             2x 2x       4x 4x 2x 2x   2x 1x   4x         6x 6x 6x         4x 4x 1x   4x      
/**
 * Copyright (c) Siemens 2016 - 2025
 * SPDX-License-Identifier: MIT
 */
import { ComponentPortal, DomPortalOutlet } from '@angular/cdk/portal';
import {
  booleanAttribute,
  ChangeDetectorRef,
  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 cdRef = inject(ChangeDetectorRef);
 
  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();
      }
      this.cdRef.markForCheck();
    });
  }
 
  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();
  }
}