import { OnInit, OnDestroy, Input, Output, ChangeDetectorRef, EventEmitter } from '@angular/core'; import { FormControl } from '@angular/forms'; import { FriendlyValidationErrorsService } from './../../services/friendly-validation-errors.service'; import { Subject, Subscription } from 'rxjs'; import { debounceTime } from 'rxjs/operators'; export abstract class NativeInputComponent implements OnInit, OnDestroy { @Input() control: FormControl; @Input() set meta(meta: StringMap) { this._meta = meta; this.exposeForTemplate(); } @Output() call: EventEmitter = new EventEmitter(); readonly componentName: string; exposeMetaInTemplate: string[] = []; _meta: StringMap; pendingValidation: boolean; hasFocus: boolean = false; waitForFirstChange: boolean = false; keyUp$: Subject = new Subject(); valueSBX: Subscription; statusSBX: Subscription; keyUpSBX: Subscription; keyUpValidationDelay: number = 2000; // Validate after 2 seconds IF the control still has focus constructor( protected valErrsService: FriendlyValidationErrorsService, protected _cdr: ChangeDetectorRef ) {} ngOnInit() { // this.control is not an instance of FormControl when it's a Checkbutton in a Checkbutton group (but is when it's a solo Checkbutton) - BUT WHY? WHY!!! // Hence the 'if' statement if (this.control instanceof FormControl) { this.valueSBX = this.control.valueChanges.subscribe(val => { if (val === null) { this.control.setValue(this._meta.default); // Reset to default value (as opposed to null) after an Angular FormGroup.reset() return; } this.markForCheck(); }); this.statusSBX = this.control.statusChanges.subscribe(status => { if (status === 'PENDING') { this.pendingValidation = true; } else if (this.pendingValidation) { this.pendingValidation = false; this.markForCheck(); } }); } this.keyUpSBX = this.keyUp$.pipe(debounceTime(this.keyUpValidationDelay)).subscribe(val => { if (this.hasFocus) { // THE ORDER OF THE NEXT THREE LINES IS IMPORTANT! // We MUST make the control as dirty before setting the value to avoid corrupting the initialValue recorded in Dynaform's makeAsyncTest utility this.control.markAsTouched(); this.control.markAsDirty(); this.control.setValue(val); } }); } ngOnDestroy() { if (this.valueSBX) { this.valueSBX.unsubscribe(); } if (this.statusSBX) { this.statusSBX.unsubscribe(); } if (this.keyUpSBX) { this.keyUpSBX.unsubscribe(); } } exposeForTemplate() { // Move meta variables up a level, for direct access in templates this.exposeMetaInTemplate.map(p => this[p] = this._meta[p] !== undefined ? this._meta[p] : this[p]); } markForCheck() { if (this._cdr) { this._cdr.markForCheck(); } } handle(fnId: string, val: any): void { this.call.emit(fnId); // Add control's current value } handleChange(): void { if (this._meta.change) { this.handle(this._meta.change, this.control.value); } } getName(): string { return this.componentName; } getFirstFailureMsg(): string { if (!this.control.errors || this.control.errors === {}) { return ''; } const key = Object.keys(this.control.errors)[0]; return this._meta.valFailureMsgs[key] || this.valErrsService.getFriendly(key, this.control.errors[key]); } gainFocus(): void { this.hasFocus = true; this.waitForFirstChange = true; } loseFocus(): void { this.hasFocus = false; } handleKeyup(currentFieldValue: string): void { this.keyUp$.next(currentFieldValue); if (this.control.value && this.waitForFirstChange) { // Hide any validation errors if the control has a previous value (set after Angular's first updateOn event) and its value has changed // NOTE that this.control.value may lag behind currentFieldValue depending on the updateOn startegy chosen this.control.markAsPending(); this.waitForFirstChange = false; } } }