import { AfterViewInit, Component, ElementRef, forwardRef, Input, OnChanges, SimpleChanges, ViewChild } from '@angular/core'; import { AbstractControl, ControlContainer, ControlValueAccessor, NG_VALIDATORS, NG_VALUE_ACCESSOR, NgForm, ValidationErrors, Validator } from '@angular/forms'; import { DataCardService } from '../api/data-card.service'; import { fromEvent } from 'rxjs'; import { debounceTime, distinctUntilChanged, filter, tap } from 'rxjs/operators'; import { ToNumberPipe } from '../pipes/to-number.pipe'; type InputType = 'color' | 'date' | 'datetime-local' | 'email' | 'month' | 'number' | 'password' | 'search' | 'tel' | 'text' | 'time' | 'url'; export const APP_INPUT_CONTROL_VALUE_ACCESSOR: any = { provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => InputComponent), multi: true }; export const APP_INPUT_CONTROL_VALIDATORS: any = { provide: NG_VALIDATORS, useExisting: forwardRef(() => InputComponent), multi: true }; @Component({ selector: 'app-input', templateUrl: './input.component.html', styleUrls: [ './input.component.scss' ], providers: [ APP_INPUT_CONTROL_VALUE_ACCESSOR, APP_INPUT_CONTROL_VALIDATORS ], viewProviders: [ { provide: ControlContainer, useExisting: NgForm } ], // tslint:disable-next-line:no-host-metadata-property host: { '(mouseenter)': 'onMouseEnter()', '(mouseleave)': 'onMouseLeave()', '(blur)': 'onBlur()', } }) export class InputComponent implements AfterViewInit, OnChanges, ControlValueAccessor, Validator { @Input() type: InputType = 'text'; @Input() minDecimals: number | undefined; @Input() pattern: string | null = null; @Input() label = ''; @Input() unit = ''; @Input() name = ''; @Input() notRelevant: boolean | undefined; @Input() integersOnly: boolean | undefined; @Input() debounceInMs: number | undefined; @ViewChild('input', { static: true }) input: ElementRef | undefined; public innerModel = ''; public disabled = false; public hover = false; public required = false; public invalid = false; // When notRelevant is toggled on, we store the old value, so we can re-fill it it gets turned off again private oldValue: string | undefined; private dirtyAble = false; private onChangeCallback: (newValue: string) => any = () => {}; private onTouchedCallback: () => any = () => {}; constructor( private elementRef: ElementRef, private dataCardService: DataCardService, private toNumberPipe: ToNumberPipe, private form: NgForm ) { this.dataCardService.isDataCardClosed.subscribe(isClosed => this.setDisabledState(isClosed)); } ngOnChanges(changes: SimpleChanges): void { if (changes.notRelevant) { if (this.notRelevant) { this.oldValue = this.innerModel; this.writeValue(''); } else { this.writeValue(this.oldValue); } } } ngAfterViewInit(): void { if (this.input) { if (this.integersOnly) { this.input.nativeElement.setAttribute('onkeypress', 'return event.charCode >= 48 && event.charCode <= 57'); } // We handle debounce this slightly complicated way to avoid change detection running for every debounced keystroke fromEvent(this.input.nativeElement, 'keyup') .pipe( filter(Boolean), debounceTime(this.debounceInMs || 0), distinctUntilChanged(), tap(() => { // tslint:disable-next-line:no-non-null-assertion We literally just checked if it was null/undefined... const inputValue = this.input!.nativeElement.value; const transformedValue = this.type === 'number' ? this.toNumberPipe.transform(inputValue, 'da') : inputValue; this.writeValue(transformedValue); }) ).subscribe(); if (this.pattern) { this.input.nativeElement.setAttribute('pattern', this.pattern); } } this.required = this.elementRef.nativeElement.hasAttribute('required') && this.elementRef.nativeElement.attributes.required.value !== 'false'; // Updating validation view status: // NgForm is rendered asynchronously with template driven forms, // thus we do not at this point in time have an attached control. // // Using an injected NgControl does not work in all instances (for reasons unknown), and thus relying on // the this.elementRef to have its 'ng-invalid' flag/class set is not sufficient. // // As a hack, we listen to all INVALID changes for the form, and if we have a failed validation somewhere in the form, // we check if it's ourselves that failed, so we can properly set our own invalid flag and update the view accordingly. this.form.form.statusChanges.subscribe(status => { if (status === 'INVALID') { this.invalid = this.form.form.get(this.name)?.status === 'INVALID'; } else { this.invalid = false; } }); } get value(): any { return this.innerModel || ''; } set value(value: any) { if (value !== this.innerModel) { this.innerModel = value; if (this.dirtyAble) { this.onChangeCallback(value); } } } onMouseEnter(): void { this.hover = !(this.disabled || this.notRelevant === true); } onMouseLeave(): void { this.hover = false; } // Set touched on blur onBlur(): void { this.onTouchedCallback(); } registerOnChange(fn: any): void { this.onChangeCallback = fn; } registerOnTouched(fn: any): void { this.onTouchedCallback = fn; } setDisabledState(isDisabled: boolean): void { this.disabled = isDisabled; } writeValue(newValue: any): void { this.value = newValue; } makeDirtyAble(): void { this.dirtyAble = true; } private innerInputInvalid(): boolean { return !!this.input?.nativeElement?.validationMessage; } validate(control: AbstractControl): ValidationErrors | null { if (!this.innerInputInvalid()) { return null; } return { invalidInput: this.input?.nativeElement?.validationMessage }; } }