File

projects/isy-angular-widgets/src/lib/incomplete-date/incomplete-date.component.ts

Description

This component is used to input complete and incomplete dates. To enter an unknown day or month, 0 or x can be used.

The format DD.MM.YYYY is supported by the widget

== Century switch / Birthdays in the past

If only past dates are allowed (e.g. for already born persons), the property dateInPastConstraint can be set to true via binding.

When autocompleting e.g. 10.10.50, 10.10.1950 will be the output instead of 10.10.2050.

Implements

ControlValueAccessor Validator OnInit AfterViewInit

Metadata

Index

Properties
Methods
Inputs
Outputs
Accessors

Inputs

allowZeroFormat
Type : boolean
Default value : false
dateInPastConstraint
Type : boolean
Default value : false

Decides whether only past dates are allowed (century switch - instead of 2050 e.g. 1950)

disabled
Type : boolean
Default value : false

A disabled date picker can't be opened.

inputId
Type : string
inputLabel
Type : string
Default value : ''

The label for the input element.

placeholder
Type : string
Default value : ''

The placeholder for the input element.

readonly
Type : boolean
Default value : false

Determines whether the date picker takes input.

required
Type : boolean
Default value : false

Marks the underlying input as required for accessibility.

transferISO8601
Type : boolean
Default value : false

Specifies whether to transfer the date value in ISO 8601 format. If set to true, the date value will be transferred in ISO 8601 format (YYYY-MM-DD). If set to false, the date value will be transferred in German date format (DD.MM.YYYY).

Outputs

onInput
Type : EventEmitter<Event>

Methods

calculateNewCursorPosition
calculateNewCursorPosition(position: number)

Calculates the new cursor position

Parameters :
Name Type Optional Description
position number No

as a number

Returns : number

position as a number

convertToTransferDateFormat
convertToTransferDateFormat(value: string)

Converts the given value to the transfer date format. If transferISO8601 is true, the value is expected to be in the format "dd.mm.yyyy". Otherwise, the original value is returned.

Parameters :
Name Type Optional Description
value string No
  • The value to convert.
Returns : string

The converted value in the format "yyyy-mm-dd" if transferISO8601 is true, otherwise the original value.

ngAfterViewInit
ngAfterViewInit()
Returns : void
ngOnInit
ngOnInit()

Initializes readonly and disabled properties

Returns : void
onBlur
onBlur()

Transforms the current input on losing the focus

Returns : void
onComplete
onComplete()

Transforms the input value if necessary and updates it when user completes the mask pattern

Returns : void
onInputChange
onInputChange(event: Event)
Parameters :
Name Type Optional
event Event No
Returns : void
onKeydown
onKeydown(event: Event)

Handles the keydown event for the input element. Stores the last key pressed and the input element that triggered the event.

Parameters :
Name Type Optional Description
event Event No
  • The keyboard event triggered by the user.
Returns : void
onModelChange
onModelChange(value: string)

Handles changes to the date input model, updating the input value and cursor position as needed. This method processes the input value when the user interacts with the date field, specifically when editing the day or month parts. It replaces incomplete day or month values with a specified character if necessary, updates the input field, and recalculates the cursor position to ensure a smooth user experience.

Parameters :
Name Type Optional Description
value string No
  • The current value of the date input in the format "DD.MM.YYYY".
Returns : void
registerOnChange
registerOnChange(fn: (value: string) => void)

Reports the value back to the parent form Calls the given function on component change

Parameters :
Name Type Optional Description
fn function No

The function to be called on component change

Returns : void
registerOnTouched
registerOnTouched(fn: unknown)

Reports to the parent form that the control was touched Calls the given function on component touch

Parameters :
Name Type Optional Description
fn unknown No

The function to be called on component touch

Returns : void
setDisabledState
setDisabledState(isDisabled: boolean)

Transmits the state (enabled/disabled) to the form control Enables or disables the component

Parameters :
Name Type Optional Description
isDisabled boolean No

True to disable the component; false to enable the component

Returns : void
transformDatePart
transformDatePart(partOfDate: string, char: string)

Transforms a part of a date string

Parameters :
Name Type Optional Description
partOfDate string No

part of a date as a string

char string No

unspecified character as a string

Returns : string

transformed part of a date

updateModel
updateModel()

Updates the internal model by converting the current input value to the transfer date format and propagates the change to registered listeners.

Returns : void
validate
validate(c: AbstractControl)

If transferISO8601 is true, it calls Validation.validUnspecifiedISODate to validate the control. Otherwise, it calls Validation.validUnspecifiedDate to validate the control. The Validation checks that the date is a valid unspecified date or valid date in German format DD.MM.YYYY resp. ISO 8601 YYYY-MM-DD. If the date is invalid and not unspecified, a INVALIDUNSPECIFIEDISODATE resp. INVALIDUNSPECIFIEDISODATE error is thrown. If the year is '0000' and allowZeroFormat is false, a INVALIDUNSPECIFIEDISODATE resp. INVALIDUNSPECIFIEDISODATE error is thrown. E.g. unspecified dates: 00.MM.YYYY, 00.00.YYYY, 00.00.0000, xx.MM.YYYY, xx.xx.YYYY, xx.xx.xxxx, YYYY-MM-00, YYYY-00-00, 0000-00-00, YYYY-MM-xx, YYYY-xx-xx, xxxx-xx-xx For valid or valid unspecified dates, no error is thrown.

Parameters :
Name Type Optional Description
c AbstractControl No

The abstract control to validate.

A ValidationErrors object if the control is invalid, otherwise null.

writeValue
writeValue(value: string)

Called by the Forms module to write a value into a form control

Parameters :
Name Type Optional Description
value string No

The new value

Returns : void

Properties

Optional field
Type : InputMask
Decorators :
@ViewChild(InputMask)
inputValue
Type : string
Default value : ''

Currently displayed date string

lastInputElement
Type : HTMLInputElement | null
Default value : null
lastKeyPressed
Type : string
Default value : ''
onChange
Type : function
Default value : () => {...}
onTouched
Type : function
Default value : () => {...}
Optional transferValue
Type : string

Accessors

isInvalid
getisInvalid()
isRequired
getisRequired()
import {
  AfterViewInit,
  Component,
  EventEmitter,
  forwardRef,
  inject,
  Injector,
  Input,
  OnInit,
  Output,
  ViewChild,
  booleanAttribute
} from '@angular/core';
import {
  AbstractControl,
  ControlValueAccessor,
  FormsModule,
  NG_VALIDATORS,
  NG_VALUE_ACCESSOR,
  NgControl,
  ValidationErrors,
  Validator,
  Validators
} from '@angular/forms';
import {IncompleteDateService} from './incomplete-date.service';
import {Validation} from '../validation/validation';
import {InputMask, InputMaskModule} from 'primeng/inputmask';
import {InputTextModule} from 'primeng/inputtext';
import {INPUT_MASK_REGEX_ISO_DATE} from '../validation/data/date-formats';

const CURSOR_POSITION = {
  DayFirstDigit: 0,
  DaySecondDigit: 1,
  DotAfterDay: 2,
  MonthSecondDigit: 4,
  DotAfterMonth: 5
} as const;

const CURSOR_SHIFT = {
  Small: 1,
  Medium: 2,
  Large: 3
} as const;

/**
 * This component is used to input complete and incomplete dates.
 * To enter an unknown day or month, `0` or `x` can be used.
 *
 * The format DD.MM.YYYY is supported by the widget
 *
 * == Century switch / Birthdays in the past
 *
 * If only past dates are allowed (e.g. for already born persons),
 * the property `dateInPastConstraint` can be set to `true` via binding.
 *
 * When autocompleting e.g. 10.10.50, 10.10.1950 will be the output instead of 10.10.2050.
 */
@Component({
  standalone: true,
  selector: 'isy-incomplete-date',
  templateUrl: './incomplete-date.component.html',
  providers: [
    {
      provide: NG_VALUE_ACCESSOR,
      useExisting: forwardRef(() => IncompleteDateComponent),
      multi: true
    },
    {
      provide: NG_VALIDATORS,
      useExisting: forwardRef(() => IncompleteDateComponent),
      multi: true
    }
  ],
  imports: [FormsModule, InputTextModule, InputMaskModule]
})
export class IncompleteDateComponent implements ControlValueAccessor, Validator, OnInit, AfterViewInit {
  /**
   * A disabled date picker can't be opened.
   */
  @Input() disabled = false;

  /**
   * Determines whether the date picker takes input.
   */
  @Input() readonly = false;

  /**
   * The label for the input element.
   */
  @Input() inputLabel = '';

  /**
   * The placeholder for the input element.
   */
  @Input() placeholder = '';

  /**
   * Decides whether only past dates are allowed (century switch - instead of 2050 e.g. 1950)
   */
  @Input() dateInPastConstraint = false;

  @Input() inputId?: string;

  /**
   * Currently displayed date string
   */
  inputValue = '';

  /**
   * Specifies whether to transfer the date value in ISO 8601 format.
   * If set to true, the date value will be transferred in ISO 8601 format (YYYY-MM-DD).
   * If set to false, the date value will be transferred in German date format (DD.MM.YYYY).
   */
  @Input() transferISO8601 = false;

  /**
   * @deprecated using the format "00.00.0000" for unknown dates is deprecated and will be removed in the future. Use "xx.xx.xxxx" instead.
   */
  @Input() allowZeroFormat = false;

  /**
   * Marks the underlying input as required for accessibility.
   */
  @Input({transform: booleanAttribute}) required = false;

  // ISO string data-side date format
  transferValue?: string;

  // To align with PrimeNG API
  // eslint-disable-next-line @angular-eslint/no-output-on-prefix
  @Output() onInput: EventEmitter<Event> = new EventEmitter<Event>();

  @ViewChild(InputMask) field?: InputMask;

  lastKeyPressed = '';
  lastInputElement: HTMLInputElement | null = null;

  /**
   * The service that contains date transformation logic
   */
  private readonly incompleteDateService = inject(IncompleteDateService);

  private readonly injector = inject(Injector);

  private get ngControl(): NgControl | null {
    return this.injector.get(NgControl, null, {self: true});
  }

  get isInvalid(): boolean {
    return !!this.ngControl?.invalid && (!!this.ngControl?.touched || !!this.ngControl?.dirty);
  }

  get isRequired(): boolean {
    return this.required || !!this.ngControl?.control?.hasValidator(Validators.required);
  }

  ngAfterViewInit(): void {
    if (this.transferISO8601) this.onChange(this.inputValue);
  }

  /**
   * Initializes readonly and disabled properties
   */
  ngOnInit(): void {
    // Enable syntax <isy-incomplete-date readonly /> (isReadOnly has value "" which is true as boolean)
    this.readonly = this.readonly || this.readonly === ('' as unknown as boolean);
    this.disabled = this.disabled || this.disabled === ('' as unknown as boolean);
  }

  /**
   * If `transferISO8601` is true, it calls `Validation.validUnspecifiedISODate` to validate the control.
   * Otherwise, it calls `Validation.validUnspecifiedDate` to validate the control.
   * The Validation checks that the date is a valid unspecified date or valid date in German format DD.MM.YYYY resp. ISO 8601 YYYY-MM-DD.
   * If the date is invalid and not unspecified, a `INVALIDUNSPECIFIEDISODATE` resp. `INVALIDUNSPECIFIEDISODATE` error is thrown.
   * If the year is '0000' and `allowZeroFormat` is false, a `INVALIDUNSPECIFIEDISODATE` resp. `INVALIDUNSPECIFIEDISODATE` error is thrown.
   * E.g. unspecified dates: 00.MM.YYYY, 00.00.YYYY, 00.00.0000, xx.MM.YYYY, xx.xx.YYYY, xx.xx.xxxx,
   * YYYY-MM-00, YYYY-00-00, 0000-00-00, YYYY-MM-xx, YYYY-xx-xx, xxxx-xx-xx
   * For valid or valid unspecified dates, no error is thrown.
   * @param c The abstract control to validate.
   * @returns A `ValidationErrors` object if the control is invalid, otherwise null.
   */
  validate(c: AbstractControl): ValidationErrors | null {
    return this.transferISO8601
      ? Validation.validUnspecifiedISODate(c, this.allowZeroFormat)
      : Validation.validUnspecifiedDate(c, this.allowZeroFormat);
  }

  /**
   * Called by the Forms module to write a value into a form control
   * @param value The new value
   */
  writeValue(value: string): void {
    this.inputValue = value;
  }

  /**
   * Handles the keydown event for the input element.
   * Stores the last key pressed and the input element that triggered the event.
   * @param event - The keyboard event triggered by the user.
   */
  onKeydown(event: Event): void {
    this.lastKeyPressed = (event as KeyboardEvent).key;
    this.lastInputElement = event.target as HTMLInputElement;
  }

  /**
   * Handles changes to the date input model, updating the input value and cursor position as needed.
   * This method processes the input value when the user interacts with the date field, specifically
   * when editing the day or month parts. It replaces incomplete day or month values with a specified
   * character if necessary, updates the input field, and recalculates the cursor position to ensure
   * a smooth user experience.
   * @param value - The current value of the date input in the format "DD.MM.YYYY".
   */
  onModelChange(value: string): void {
    const input = this.lastInputElement;

    if (!input) return;

    const cursorPos = input.selectionStart ?? 0;
    const [day = '', month = '', year = ''] = value.split('.');
    let partDay = day;
    let partMonth = month;
    const dayClean = day.replaceAll('_', '');
    const monthClean = month.replaceAll('_', '');
    const unspecifiedChar = 'x';

    this.inputValue = value;

    const isInDayOrMonthRange = this.lastKeyPressed === '.' && cursorPos <= CURSOR_POSITION.DotAfterMonth;

    if (isInDayOrMonthRange) {
      if (dayClean.length <= 1) partDay = this.transformDatePart(day, unspecifiedChar);

      if (
        cursorPos >= CURSOR_POSITION.DotAfterDay + 1 &&
        cursorPos <= CURSOR_POSITION.DotAfterMonth &&
        monthClean.length <= 1
      ) {
        if (cursorPos > CURSOR_POSITION.DotAfterDay + 1) partMonth = this.transformDatePart(month, unspecifiedChar);
      }

      const result = [partDay, partMonth, year].join('.');
      input.value = this.inputValue = result;

      const newCursor = this.calculateNewCursorPosition(cursorPos);
      input.setSelectionRange(newCursor, newCursor);
    }
  }

  /**
   * Calculates the new cursor position
   * @param position as a number
   * @returns position as a number
   */
  calculateNewCursorPosition(position: number): number {
    switch (position) {
      case CURSOR_POSITION.DayFirstDigit:
        return position + CURSOR_SHIFT.Large;
      case CURSOR_POSITION.DaySecondDigit:
      case CURSOR_POSITION.MonthSecondDigit:
        return position + CURSOR_SHIFT.Medium;
      case CURSOR_POSITION.DotAfterDay:
      case CURSOR_POSITION.DotAfterMonth:
        return position + CURSOR_SHIFT.Small;
      default:
        return position;
    }
  }

  /**
   * Transforms a part of a date string
   * @param partOfDate part of a date as a string
   * @param char unspecified character as a string
   * @returns transformed part of a date
   */
  transformDatePart(partOfDate: string, char: string): string {
    const partOfDateReplaced = partOfDate.replaceAll('_', '');

    return partOfDateReplaced === char || partOfDateReplaced.length === 0
      ? char.repeat(partOfDate.length)
      : '0' + partOfDateReplaced;
  }

  /**
   * Transforms the input value if necessary and updates it when user completes the mask pattern
   */
  onComplete(): void {
    this.inputValue = this.incompleteDateService.transformValue(
      this.inputValue,
      this.dateInPastConstraint,
      this.allowZeroFormat
    );
    this.updateModel();
  }

  /**
   * Transforms the current input on losing the focus
   */
  onBlur(): void {
    if (this.inputValue) {
      this.inputValue = this.incompleteDateService.transformValue(
        this.inputValue,
        this.dateInPastConstraint,
        this.allowZeroFormat
      );
    }

    const inputEl = this.field?.inputViewChild?.nativeElement as HTMLInputElement | undefined;
    if (inputEl) {
      inputEl.value = this.inputValue;
      if (this.inputValue.includes('_')) {
        inputEl.value = this.inputValue = '';
      }
    }

    this.onTouched();
    this.updateModel();
  }

  onInputChange(event: Event): void {
    this.onInput.emit(event);
  }

  /**
   * Updates the internal model by converting the current input value to the transfer date format
   * and propagates the change to registered listeners.
   */
  updateModel(): void {
    this.transferValue = this.convertToTransferDateFormat(this.inputValue);
    this.onChange(this.transferValue);
  }

  /**
   * Reports the value back to the parent form
   * Calls the given function on component change
   * @param fn The function to be called on component change
   */
  registerOnChange(fn: (value: string) => void): void {
    this.onChange = (value): void => {
      this.transferValue = this.convertToTransferDateFormat(value);
      fn(this.transferValue);
    };
  }

  /**
   * Reports to the parent form that the control was touched
   * Calls the given function on component touch
   * @param fn The function to be called on component touch
   */
  registerOnTouched(fn: unknown): void {
    this.onTouched = fn as () => void;
  }

  /**
   * Transmits the state (enabled/disabled) to the form control
   * Enables or disables the component
   * @param isDisabled True to disable the component; false to enable the component
   */
  setDisabledState(isDisabled: boolean): void {
    this.disabled = isDisabled;
  }

  /**
   * Converts the given value to the transfer date format.
   * If `transferISO8601` is true, the value is expected to be in the format "dd.mm.yyyy".
   * Otherwise, the original value is returned.
   * @param value - The value to convert.
   * @returns The converted value in the format "yyyy-mm-dd" if `transferISO8601` is true, otherwise the original value.
   */
  convertToTransferDateFormat(value: string): string {
    if (!this.transferISO8601) return value;
    if (INPUT_MASK_REGEX_ISO_DATE.test(value)) return value;

    const [day, month, year] = value.split('.');
    return `${year}-${month}-${day}`;
  }

  onChange: (value: string) => void = () => {};

  onTouched: () => void = () => {};
}
<p-inputmask
  [readonly]="readonly"
  [disabled]="disabled"
  (onInput)="onInputChange($event)"
  (onBlur)="onBlur()"
  (onComplete)="onComplete()"
  (onKeydown)="onKeydown($event)"
  mask="**.**.****"
  [ngModel]="inputValue"
  (ngModelChange)="onModelChange($event)"
  characterPattern="^[x]"
  [placeholder]="placeholder"
  [inputId]="inputId!"
  [autoClear]="false"
  styleClass="w-full"
  [invalid]="isInvalid"
  [required]="isRequired"
  [ariaRequired]="isRequired"
/>
Legend
Html element
Component
Html element with directive

results matching ""

    No results matching ""