All files / app/shared control-invalid-signal.util.ts

100% Statements 18/18
100% Branches 3/3
100% Functions 4/4
100% Lines 18/18

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                                              1x 805x   805x 2415x     805x                         1x 267x         267x   267x     538x 57x     481x 43x     438x 267x     171x       538x       267x    
import type { Signal } from '@angular/core';
import { toSignal } from '@angular/core/rxjs-interop';
import { PristineChangeEvent, StatusChangeEvent, TouchedChangeEvent } from '@angular/forms';
import type { AbstractControl, ControlEvent } from '@angular/forms';
import { distinctUntilChanged, map, scan } from 'rxjs';
import type { Observable } from 'rxjs';
 
/**
 * Angular AbstractControl properties that are used to determine if the control should signal an
 * invalid state.
 */
interface ControlProperties {
  /** Control value has been modified. */
  readonly dirty: boolean;
  /** Control value fails validation. */
  readonly invalid: boolean;
  /** Control has been focused in the view. */
  readonly touched: boolean;
}
 
/**
 * When all the ControlProperties are true then the Control is invalid.
 */
const isInvalid = (properties: ControlProperties): boolean => {
  let invalid = true;
 
  for (const val of Object.values(properties)) {
    invalid &&= Boolean(val);
  }
 
  return invalid;
};
 
/**
 * Create an Angular Signal that flags as modified and invalid based on the Control properties.
 *
 * 1. Invalid - the value fails validation checks.
 * 2. Dirty - the value is different from the initial value.
 * 3. Touched - the Control has been focused during the current view.
 *
 * This ensures that the aria-invalid attribute is only set on Controls that the user has interacted
 * with.
 */
export const controlInvalidSignal = (control: AbstractControl): Signal<boolean> => {
  const defaultProperties: ControlProperties = {
    dirty: control.dirty,
    invalid: control.invalid,
    touched: control.touched,
  };
  const initialValue = isInvalid(defaultProperties);
 
  const controlEvents$: Observable<boolean> = control.events.pipe(
    scan(
      (current: ControlProperties, event: ControlEvent<unknown>): ControlProperties => {
        if (event instanceof PristineChangeEvent) {
          return { ...current, dirty: !event.pristine };
        }
 
        if (event instanceof TouchedChangeEvent) {
          return { ...current, touched: event.touched };
        }
 
        if (event instanceof StatusChangeEvent) {
          return { ...current, invalid: event.status === 'INVALID' };
        }
 
        return current;
      },
      defaultProperties,
    ),
    map((properties: ControlProperties): boolean => isInvalid(properties)),
    distinctUntilChanged(),
  );
 
  return toSignal(controlEvents$, { initialValue });
};