All files / threshold si-threshold.component.ts

92.06% Statements 58/63
42.1% Branches 8/19
100% Functions 13/13
91.07% Lines 51/56

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 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258                                                                                                                              1x           10x           10x           10x           10x           10x           10x           10x           10x           10x           10x           10x           10x           10x                 10x                 10x                 10x                 10x     10x   10x 14x 14x 42x 39x 3x 3x     70x     10x 10x         40x     10x     17x       1x 1x 1x 1x       1x 1x 1x 1x 1x 1x       1x 1x       20x 20x 20x 80x   80x                         80x     20x          
/**
 * Copyright (c) Siemens 2016 - 2025
 * SPDX-License-Identifier: MIT
 */
import { NgClass, NgTemplateOutlet } from '@angular/common';
import {
  booleanAttribute,
  Component,
  computed,
  input,
  model,
  OnChanges,
  output,
  viewChildren
} from '@angular/core';
import { FormsModule } from '@angular/forms';
import { addIcons, elementPlus, SiIconComponent } from '@siemens/element-ng/icon';
import { SiNumberInputComponent } from '@siemens/element-ng/number-input';
import {
  SelectOption,
  SelectOptionLegacy,
  SiSelectComponent,
  SiSelectSimpleOptionsDirective,
  SiSelectSingleValueDirective
} from '@siemens/element-ng/select';
import { SiTranslatePipe, t } from '@siemens/element-translate-ng/translate';
 
import { SiReadonlyThresholdOptionComponent } from './si-readonly-threshold-option.component';
 
/**
 * One step in a list of thresholds
 */
export interface ThresholdStep {
  /** Threshold value, the first step has no value */
  value?: number;
  /** One of the `SelectOption.id` */
  optionValue: string;
  /** When set to `false`, input fields are highlighted as invalid */
  valid?: boolean;
}
 
@Component({
  selector: 'si-threshold',
  imports: [
    FormsModule,
    NgClass,
    NgTemplateOutlet,
    SiIconComponent,
    SiNumberInputComponent,
    SiSelectComponent,
    SiSelectSingleValueDirective,
    SiSelectSimpleOptionsDirective,
    SiTranslatePipe,
    SiReadonlyThresholdOptionComponent
  ],
  templateUrl: './si-threshold.component.html',
  styleUrl: './si-threshold.component.scss',
  host: {
    '[class.add-remove]': 'canAddRemoveSteps()',
    '[class.horizontal]': 'horizontalLayout()',
    '[class.dec-inc-buttons]': 'showDecIncButtons()'
  }
})
export class SiThresholdComponent implements OnChanges {
  /**
   * Options to be shown in select dropdown
   *
   * @defaultValue []
   */
  readonly options = input<SelectOptionLegacy[] | SelectOption<unknown>[]>([]);
  /**
   * The thresholds
   *
   * @defaultValue []
   */
  readonly thresholdSteps = model<ThresholdStep[]>([]);
  /**
   * The unit to show
   *
   * @defaultValue ''
   */
  readonly unit = input('');
  /**
   * The min. value for the threshold value
   *
   * @defaultValue 0
   */
  readonly minValue = input(0);
  /**
   * The max. value for the threshold value
   *
   * @defaultValue 100
   */
  readonly maxValue = input(100);
  /**
   * The step size for the threshold value
   *
   * @defaultValue 1
   */
  readonly stepSize = input(1);
  /**
   * Max. number of steps, 0 for no hard limit
   *
   * @defaultValue 0
   */
  readonly maxSteps = input(0);
  /**
   * Do validation?
   *
   * @defaultValue true
   */
  readonly validation = input(true, { transform: booleanAttribute });
  /**
   * When disabled, steps cannot be added/removed
   *
   * @defaultValue true
   */
  readonly canAddRemoveSteps = input(true, { transform: booleanAttribute });
  /**
   * Use horizontal layout?
   *
   * @defaultValue false
   */
  readonly horizontalLayout = input(false, { transform: booleanAttribute });
  /**
   * Show dec/inc buttons?
   *
   * @defaultValue true
   */
  readonly showDecIncButtons = input(true, { transform: booleanAttribute });
  /**
   * The obvious
   *
   * @defaultValue false
   */
  readonly readonly = input(false, { transform: booleanAttribute });
  /**
   * Indicate that the threshold options are readonly and cannot be changed. This will also disable adding / removing steps.
   *
   * @defaultValue false
   */
  readonly readonlyConditions = input(false, { transform: booleanAttribute });
  /**
   * The aria-label for delete button
   *
   * @defaultValue
   * ```
   * t(() => $localize`:@@SI_THRESHOLD.DELETE:Delete step`)
   * ```
   */
  readonly deleteAriaLabel = input(t(() => $localize`:@@SI_THRESHOLD.DELETE:Delete step`));
  /**
   * The aria-label for add button
   *
   * @defaultValue
   * ```
   * t(() => $localize`:@@SI_THRESHOLD.ADD:Add step`)
   * ```
   */
  readonly addAriaLabel = input(t(() => $localize`:@@SI_THRESHOLD.ADD:Add step`));
  /**
   * The aria-label for input field
   *
   * @defaultValue
   * ```
   * t(() => $localize`:@@SI_THRESHOLD.INPUT_LABEL:Threshold value`)
   * ```
   */
  readonly inputAriaLabel = input(t(() => $localize`:@@SI_THRESHOLD.INPUT_LABEL:Threshold value`));
  /**
   * The aria-label for status selection
   *
   * @defaultValue
   * ```
   * t(() => $localize`:@@SI_THRESHOLD.STATUS:Status`)
   * ```
   */
  readonly statusAriaLabel = input(t(() => $localize`:@@SI_THRESHOLD.STATUS:Status`));
 
  /** Fired when validation status changes */
  readonly validChange = output<boolean>();
 
  protected readonly colors = computed(() => {
    const colorMap = new Map<unknown, string>();
    for (const opt of this.options()) {
      if (opt.type === 'option') {
        colorMap.set(opt.value, opt.iconColor ?? '');
      } else if (!opt.type) {
        colorMap.set(opt.id, opt.color ?? '');
      }
    }
    return this.thresholdSteps().map(ths => colorMap.get(ths.optionValue) ?? '');
  });
 
  protected readonly icons = addIcons({ elementPlus });
  private _valid = true;
  /**
   * Whether the current input value is valid or not.
   */
  get valid(): boolean {
    return this._valid;
  }
 
  private readonly numberInputs = viewChildren(SiNumberInputComponent);
 
  ngOnChanges(): void {
    this.validate();
  }
 
  protected deleteStep(index: number): void {
    const updated = [...this.thresholdSteps()];
    updated.splice(index, 1);
    this.thresholdSteps.set(updated);
    this.validate();
  }
 
  protected addStep(index: number): void {
    const newStep: ThresholdStep = { ...this.thresholdSteps()[index], value: undefined };
    const updated = [...this.thresholdSteps()];
    updated.splice(index + 1, 0, newStep);
    this.thresholdSteps.set(updated);
    this.validate();
    setTimeout(() => this.numberInputs()[index].inputElement().nativeElement.focus());
  }
 
  protected stepChange(): void {
    this.thresholdSteps.set([...this.thresholdSteps()]);
    this.validate();
  }
 
  private validate(): void {
    const prevValid = this.valid;
    this._valid = true;
    for (let i = 1; i < this.thresholdSteps().length; i++) {
      const step = this.thresholdSteps()[i];
 
      Iif (this.validation()) {
        const prev = this.thresholdSteps()[i - 1];
        const next = this.thresholdSteps()[i + 1];
 
        // valid: withing min/max, each step is lower than next step with step size between
        step.valid =
          step.value != null &&
          step.value >= this.minValue() &&
          step.value <= this.maxValue() &&
          (prev.value == null || step.value - this.stepSize() >= prev.value) &&
          (next?.value == null || step.value + this.stepSize() <= next.value);
        this._valid &&= step.valid;
      } else {
        step.valid = true;
      }
    }
    Iif (this.valid !== prevValid) {
      this.validChange.emit(this.valid);
    }
  }
}