import { Component, EventEmitter, inject, Input, Output, OnInit, ElementRef, OnDestroy } from '@angular/core'; import { DynamicFormComponent } from '../dynamic-form/dynamic-form'; import { CommonModule } from '@angular/common'; import { DynamicFormConfig } from '../dynamic-form/dynamic-form-config'; import { FormGroup } from '@angular/forms'; import { HttpClient } from '@angular/common/http'; import { MatDialogRef, MAT_DIALOG_DATA } from '@angular/material/dialog'; import { environment } from '../../../environments/environment'; @Component({ selector: "app-dynamic-popup", templateUrl: "./dynamic-popup.html", styleUrls: ['./dynamic-popup.scss'], standalone: true, imports: [DynamicFormComponent, CommonModule] }) export class DynamicPopupComponent implements OnInit, OnDestroy { @Input() config!: DynamicFormConfig; @Input() data: Record = {}; form!: FormGroup; @Output() saved = new EventEmitter(); readonly http = inject(HttpClient); private dialogRef = inject(MatDialogRef, { optional: true }) as unknown as MatDialogRef | null; private injectedDialogData = inject(MAT_DIALOG_DATA, { optional: true }) as { config?: DynamicFormConfig; data?: Record } | undefined; private el = inject(ElementRef); private focusableElements: HTMLElement[] = []; private keydownHandler?: (e: KeyboardEvent) => void; titleId = `popup-title-${Math.random().toString(36).slice(2,9)}`; ngOnInit(): void { if (this.injectedDialogData) { if (this.injectedDialogData.config) { this.config = this.injectedDialogData.config; } if (this.injectedDialogData.data) { this.data = this.injectedDialogData.data; } } // setup focus trap this.keydownHandler = (e: KeyboardEvent) => this.onKeyDown(e); this.el.nativeElement.addEventListener('keydown', this.keydownHandler as EventListener); } onFormBuilt(f: FormGroup) { this.form = f; setTimeout(() => { // collect focusable elements inside this component const nodes = this.el.nativeElement.querySelectorAll('button, a, input, textarea, select, [tabindex]:not([tabindex="-1"])') as NodeListOf; this.focusableElements = Array.from(nodes).filter(n => !n.hasAttribute('disabled')); if (this.focusableElements.length) { this.focusableElements[0].focus(); } }, 0); } submit() { let payload: unknown; if (this.containsFile()) { payload = new FormData(); Object.keys(this.form.value).forEach(k => { (payload as FormData).append(k, this.form.value[k]); }); } else { payload = this.form.value; } // If no API configured, just close with form value if (!this.config.api?.save) { this.saved.emit(payload); if (this.dialogRef) { this.dialogRef.close(payload); } else { this.close(); } return; } const url = this.resolveApiUrl(this.config.api.save as string); let apiBody = payload; if (this.config.api.bodyKey && payload && typeof payload === 'object' && !(payload instanceof FormData)) { apiBody = (payload as Record)[this.config.api.bodyKey]; } this.http.request( this.config.api.method, url, { body: apiBody } ).subscribe(res => { this.saved.emit(res); if(this.dialogRef){ this.dialogRef.close(res); } else { this.close(); } }); } private resolveApiUrl(path: string) { if (!path) return path; if (/^https?:\/\//i.test(path)) return path; const base = (environment.apiUrl || '').replace(/\/$/, ''); return `${base}/${path.replace(/^\//, '')}`; } containsFile() { return this.config.fields.some(f => f.type === "file"); } close() { this.saved.emit(null); if(this.dialogRef){ this.dialogRef.close(null); } } onKeyDown(e: KeyboardEvent){ if (e.key !== 'Tab') return; if (!this.focusableElements.length) return; const first = this.focusableElements[0]; const last = this.focusableElements[this.focusableElements.length - 1]; if (e.shiftKey) { if (document.activeElement === first) { e.preventDefault(); last.focus(); } } else { if (document.activeElement === last) { e.preventDefault(); first.focus(); } } } ngOnDestroy(): void { if (this.keydownHandler) { this.el.nativeElement.removeEventListener('keydown', this.keydownHandler as EventListener); } } }