All files / select/options si-select-lazy-options.directive.ts

95.23% Statements 40/42
87.5% Branches 14/16
100% Functions 12/12
95.12% Lines 39/41

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                                  1x         5x           5x           5x   5x   5x 5x     5x       4x 2x   2x 2x 1x   1x           4x 4x         5x 5x 5x         7x       7x     10x   4x     10x   7x 7x 3x 2x   3x     4x     4x           16x 16x       11x 5x   6x       6x 6x       6x      
/**
 * Copyright (c) Siemens 2016 - 2025
 * SPDX-License-Identifier: MIT
 */
import { Directive, input, OnDestroy, signal } from '@angular/core';
import { of, Subject, switchMap } from 'rxjs';
import { debounceTime, takeUntil } from 'rxjs/operators';
 
import { SelectItem, SelectOption } from '../si-select.types';
import { SelectOptionSource } from './si-select-option.source';
import { SI_SELECT_OPTIONS_STRATEGY, SiSelectOptionsStrategy } from './si-select-options-strategy';
 
@Directive({
  // eslint-disable-next-line @angular-eslint/directive-selector
  selector: 'si-select[optionSource]',
  providers: [{ provide: SI_SELECT_OPTIONS_STRATEGY, useExisting: SiSelectLazyOptionsDirective }]
})
export class SiSelectLazyOptionsDirective<T> implements SiSelectOptionsStrategy<T>, OnDestroy {
  /**
   * {@inheritDoc SiSelectOptionsStrategy#loading}
   * @defaultValue false
   */
  readonly loading = signal(false);
 
  /**
   * {@inheritDoc SiSelectOptionsStrategy#rows}
   * @defaultValue []
   */
  readonly rows = signal<SelectItem<T>[]>([]);
 
  /**
   * {@inheritDoc SiSelectOptionsStrategy#selectedRows}
   * @defaultValue []
   */
  readonly selectedRows = signal<SelectOption<T>[]>([]);
 
  readonly optionSource = input.required<SelectOptionSource<T>>();
 
  private valueChange = new Subject<void>();
  private filterChange = new Subject<string | undefined>();
 
  constructor() {
    this.filterChange
      .pipe(
        debounceTime(100),
        switchMap(filterInput => {
          if (filterInput) {
            return this.optionSource().getOptionsForSearch!(filterInput);
          } else {
            const optionSource = this.optionSource();
            if (optionSource.getAllOptions) {
              return optionSource.getAllOptions();
            } else {
              return of(this.selectedRows());
            }
          }
        })
      )
      .subscribe(rows => {
        this.loading.set(false);
        this.rows.set(rows);
      });
  }
 
  ngOnDestroy(): void {
    this.valueChange.next();
    this.valueChange.complete();
    this.filterChange.complete();
  }
 
  /** {@inheritDoc SiSelectOptionsStrategy.onValueChange} */
  onValueChange(value: T[]): void {
    this.valueChange.next();
 
    // To prevent flickering, we need to check if we already got all the requested options.
    // This should always be the case if the user selects one
    const knownSelectedOptions = value
      .map(
        valueEntry =>
          this.selectedRows().find(selected => this.valueEqual(selected.value, valueEntry)) ??
          (this.rows().find(
            selected => selected.type === 'option' && this.valueEqual(selected.value, valueEntry)
          ) as SelectOption<T>)
      )
      .filter(v => !!v);
 
    const optionSource = this.optionSource();
    if (knownSelectedOptions.length === value.length) {
      if (optionSource.compareOptions) {
        knownSelectedOptions.sort(optionSource.compareOptions);
      }
      this.selectedRows.set(knownSelectedOptions);
    } else {
      // We don't have all options, so we need to fetch them.
      optionSource
        .getOptionsForValues(value)
        .pipe(takeUntil(this.valueChange))
        .subscribe(selectedOptions => this.selectedRows.set(selectedOptions));
    }
  }
 
  /** {@inheritDoc SiSelectOptionsStrategy.onFilter} */
  onFilter(filterInput?: string): void {
    this.filterChange.next(filterInput);
    this.loading.set(true);
  }
 
  private valueEqual(a?: T, b?: T): boolean {
    if (a === b) {
      return true;
    }
    Iif (!a || !b) {
      return false;
    }
 
    const optionSource = this.optionSource();
    Iif (optionSource.valuesEqual) {
      return optionSource.valuesEqual(a, b);
    }
 
    return false;
  }
}