import { Component, ElementRef, forwardRef, HostListener, Injectable, Input, OnChanges, OnInit, SimpleChanges, ViewChild } from '@angular/core'; import {AbstractControl, ControlValueAccessor, NG_VALIDATORS, NG_VALUE_ACCESSOR, ValidationErrors, Validator} from '@angular/forms'; import moment, {Moment} from 'moment'; import {DataCardService} from '../api/data-card.service'; import {DateAdapter} from '@angular/material/core'; import {MomentDateAdapter} from '@angular/material-moment-adapter'; @Injectable() export class DatePickerDateAdapter extends MomentDateAdapter { format(date: Moment, _: string): string { return date.format('DD.MM.YYYY'); } parse(value: string): Moment | null { const formats = [ 'DD-MM-YYYY', 'DD.MM.YYYY', 'DD/MM/YYYY', 'DD MM YYYY', 'YYYY-MM-DD', 'YYYY.MM.DD', 'YYYY/MM/DD', 'YYYY MM DD', 'YYYYMMDD']; if (value === '') { return null; } return moment(value, formats, true); } } export const APP_DATE_PICKER_CONTROL_VALUE_ACCESSOR: any = { provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => DatePickerComponent), multi: true }; export const APP_DATE_PICKER_VALIDATOR: any = { provide: NG_VALIDATORS, useExisting: forwardRef(() => DatePickerComponent), multi: true }; @Component({ selector: 'app-date-picker', templateUrl: './date-picker.component.html', styleUrls: ['./date-picker.component.scss'], providers: [ APP_DATE_PICKER_CONTROL_VALUE_ACCESSOR, APP_DATE_PICKER_VALIDATOR, {provide: DateAdapter, useClass: DatePickerDateAdapter}, ] }) export class DatePickerComponent implements OnInit, OnChanges, ControlValueAccessor, Validator { @Input() label = ''; @Input() name = ''; @Input() notRelevant: boolean | undefined; @Input() disabled: boolean | undefined; @Input() required: boolean = false; @ViewChild('inputElement', {static: true}) input: ElementRef | undefined; @ViewChild('matFormField', {static: true}) matFormField: ElementRef | undefined; public innerModel: Moment | undefined; public hover = false; // When notRelevant is toggled on, we store the old value, so we can re-fill it it gets turned off again private oldValue: Moment | undefined; private dirtyAble = false; private onChangeCallback: (newValue: Date | undefined) => any = () => {}; private onTouchedCallback: () => any = () => {}; constructor(private dataCardService: DataCardService) { } ngOnInit(): void { if (!this.disabled) { this.dataCardService.isDataCardClosed.subscribe(isClosed => this.setDisabledState(isClosed)); } if (this.input) { this.input.nativeElement.addEventListener('change', () => { this.onChangeCallback(this.innerModel?.toDate()); // Trick angular into firing change event, thus making the form validate this field. }); } } get value(): Moment | undefined { return this.innerModel; } set value(value: Moment | undefined) { if (this.innerModel?.toDate?.()?.getTime?.() !== value?.toDate?.()?.getTime?.()) { this.innerModel = value; if (this.dirtyAble) { this.onChangeCallback(this.innerModel?.toDate()); } } } @HostListener('mouseenter') onMouseEnter(): void { this.hover = !(this.disabled || this.notRelevant); } @HostListener('mouseleave') onMouseLeave(): void { this.hover = false; } @HostListener('blur') onBlur(): void { this.onTouchedCallback(); } ngOnChanges(changes: SimpleChanges): void { if (changes.notRelevant) { if (this.notRelevant) { // #Line #108-#111 was made to "dirty fix" an issue relating to indirectly modifying the date picker via the notRelevant listener. // This would, in the past circumvent the user initiated "dirty flagging". // We're now assuming that setting if oldValue is not nullish then this change occurred // because the user initiated rather than during component initialization setTimeout(() => { if (this.innerModel) { this.makeDirtyable(); } this.oldValue = this.innerModel; this.writeValue(undefined); }); } else { this.writeValue(this.oldValue?.toDate()); } } } registerOnChange(fn: any): void { this.onChangeCallback = fn; } registerOnTouched(fn: any): void { this.onTouchedCallback = fn; } setDisabledState(isDisabled: boolean): void { this.disabled = isDisabled; } writeValue(newValue: Date | undefined): void { if (newValue) { this.value = moment(newValue); } else { this.value = undefined; } } makeDirtyable(): void { // on user input we're naturally dirty-able this.dirtyAble = true; } registerOnValidatorChange(fn: () => void): void { } validate(control: AbstractControl): ValidationErrors | null { const inputFieldValue = this.input?.nativeElement.value; const inputFieldHasValue = inputFieldValue !== ''; const invalid = !this.notRelevant && inputFieldHasValue && !this.value; return invalid ? {invalidDate: inputFieldValue} : null; } }