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

92.85% Statements 13/14
100% Branches 4/4
100% Functions 5/5
92.85% Lines 13/14

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                                                                  1x   268x 542x 59x       268x       88x 88x 48x   40x         268x           56x 56x             268x    
import type { Signal } from '@angular/core';
import { toSignal } from '@angular/core/rxjs-interop';
import { PristineChangeEvent } from '@angular/forms';
import type {
  AbstractControl,
  ControlEvent,
  FormControlStatus,
  ValidationErrors,
} from '@angular/forms';
import {
  combineLatest,
  debounceTime,
  filter,
  map,
} from 'rxjs';
import type { Observable } from 'rxjs';
 
import { FORMS } from './constants';
 
/**
 * `combinedLatest` output from inner AbstractControl Observables.
 */
interface CombinedObs {
  /** Indicates that the Control value has been modified by the user. */
  readonly dirty: boolean;
  /** If the Control is invalid then contains the ValidationErrors, otherwise `undefined`. */
  readonly errors: ValidationErrors | undefined;
}
 
/**
 * Creates an Angular Signal that emits the validation errors for a Control only when the Control is
 * also modified (dirty).
 */
export const controlErrorsSignal = (control: AbstractControl): Signal<ValidationErrors | undefined> => {
  // Only care about dirty controls for purposes of displaying validation error messages.
  const controlDirty$: Observable<boolean> = control.events.pipe(
    filter((event: ControlEvent<unknown>): event is PristineChangeEvent => event instanceof PristineChangeEvent),
    map((event: PristineChangeEvent): boolean => !event.pristine),
  );
 
  // When status is INVALID emit control.errors, otherwise undefined
  const controlStatus$: Observable<ValidationErrors | undefined> = control.statusChanges.pipe(
    // Wait for input to stop before displaying error messages
    debounceTime(FORMS.inputDebounce),
    map((status: FormControlStatus): ValidationErrors | undefined => {
      const { errors } = control;
      if (status === 'INVALID' && errors) {
        return errors;
      }
      return undefined;
    }),
  );
 
  // Combine the Observables so that ValidationErrors are emitted only when the control is dirty.
  const controlErrors$: Observable<ValidationErrors | undefined> = combineLatest({
    /* eslint-disable rxjs/finnish */
    dirty: controlDirty$,
    errors: controlStatus$,
  }).pipe(
    map(({ dirty, errors }: CombinedObs): ValidationErrors | undefined => {
      if (dirty) {
        return errors;
      }
 
      return undefined;
    }),
  );
 
  return toSignal(controlErrors$);
};