import { CommonModule } from '@angular/common'; import { Component, OnInit, Input, Output, EventEmitter, inject } from '@angular/core'; import { ReactiveFormsModule, FormGroup, FormControl, Validators, FormArray, AbstractControl } from '@angular/forms'; import { DynamicFormConfig } from './dynamic-form-config'; import { DynamicField } from './dynamic-field'; import { MatFormFieldModule } from '@angular/material/form-field'; import { MatInputModule } from '@angular/material/input'; import { MatSelectModule } from '@angular/material/select'; import { MatButtonModule } from '@angular/material/button'; import { MatIconModule } from '@angular/material/icon'; import { MatDatepickerModule } from '@angular/material/datepicker'; import { MatDialog, MatDialogModule } from '@angular/material/dialog'; import { provideNativeDateAdapter } from '@angular/material/core'; import { ConfirmDialogComponent } from '../confirm-dialog/confirm-dialog'; @Component({ selector: "app-dynamic-form", templateUrl: "./dynamic-form.html", standalone: true, styleUrls: ['./dynamic-form.scss'], providers: [provideNativeDateAdapter()], imports: [ReactiveFormsModule, CommonModule, MatFormFieldModule, MatInputModule, MatSelectModule, MatButtonModule, MatIconModule, MatDatepickerModule, MatDialogModule] }) export class DynamicFormComponent implements OnInit { @Input() config!: DynamicFormConfig; @Input() initialData: Record = {}; @Output() formBuilt = new EventEmitter(); private dialog = inject(MatDialog); form!: FormGroup; ngOnInit() { const group: Record = {}; this.config.fields.forEach(f => { if (f.type === "array") { group[f.name] = new FormArray( this.initialData[f.name] && Array.isArray(this.initialData[f.name]) ? (this.initialData[f.name] as unknown[]).map((item: unknown) => this.buildArrayItem(f.itemConfig!, item) ) : [this.buildArrayItem(f.itemConfig!, {})] ); } else { group[f.name] = new FormControl( this.initialData[f.name] || "", f.required ? Validators.required : null ); } }); this.form = new FormGroup(group); this.formBuilt.emit(this.form); } buildArrayItem(config: DynamicField[], item: unknown) { const group: Record = {}; config.forEach(f => { group[f.name] = new FormControl( (item as Record)[f.name] || "", f.required ? Validators.required : null ); }); return new FormGroup(group); } addArrayItem(field: DynamicField) { const array = this.form.get(field.name) as FormArray; array.push(this.buildArrayItem(field.itemConfig!, {})); } removeArrayItem(field: DynamicField, index: number) { const array = this.form.get(field.name) as FormArray; this.dialog.open(ConfirmDialogComponent, { data: { title: 'Remove Item', message: `Are you sure you want to remove this ${field.label?.toLowerCase() ?? 'item'}?`, confirmLabel: 'Remove', confirmColor: 'warn' }, panelClass: 'dark-popup-panel', width: '400px' }).afterClosed().subscribe((confirmed: boolean) => { if (confirmed) { array.removeAt(index); } }); } getArrayControls(name: string): AbstractControl[] { return (this.form.get(name) as FormArray).controls; } onFileChange(e: Event, field: string) { const target = e.target as HTMLInputElement; if (target?.files?.length) { const file = target.files[0]; this.form.patchValue({ [field]: file }); } } private yearOptionsCache = new Map(); getYearOptions(field: DynamicField): (number | string)[] { const asString = field.yearRange?.valueType === 'string'; const key = `${field.name}_${field.yearRange?.start}_${field.yearRange?.end}_${asString}`; if (this.yearOptionsCache.has(key)) return this.yearOptionsCache.get(key)!; const currentYear = new Date().getFullYear(); const start = field.yearRange?.start ?? (currentYear - 50); const end = field.yearRange?.end ?? (currentYear + 5); const years: (number | string)[] = []; for (let y = end; y >= start; y--) { years.push(asString ? y.toString() : y); } this.yearOptionsCache.set(key, years); return years; } compareYearValues = (a: unknown, b: unknown): boolean => { if (a == null && b == null) return true; if (a == null || b == null) return false; return a.toString() === b.toString(); }; }