55b436c7d2
- Added DynamicField interface to define form field structure. - Created DynamicFormConfig interface for form configuration. - Developed DynamicFormComponent to handle dynamic form rendering and validation. - Implemented DynamicPopupComponent for displaying forms in a modal dialog. - Added HTML and SCSS for dynamic form and popup styling. - Integrated Material Design components for form inputs and buttons. - Implemented form submission logic with API integration. - Added tests for DynamicForm and DynamicPopup components. - Updated global styles for Material components in themed popups. - Included Material Icons in index.html for better UI representation.
146 lines
4.5 KiB
TypeScript
146 lines
4.5 KiB
TypeScript
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<string, unknown> = {};
|
|
form!: FormGroup;
|
|
|
|
@Output() saved = new EventEmitter<unknown>();
|
|
readonly http = inject(HttpClient);
|
|
private dialogRef = inject(MatDialogRef, { optional: true }) as unknown as MatDialogRef<DynamicPopupComponent> | null;
|
|
private injectedDialogData = inject(MAT_DIALOG_DATA, { optional: true }) as { config?: DynamicFormConfig; data?: Record<string, unknown> } | 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<HTMLElement>;
|
|
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<string, unknown>)[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);
|
|
}
|
|
}
|
|
}
|