131 lines
4.4 KiB
TypeScript
131 lines
4.4 KiB
TypeScript
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<string, unknown> = {};
|
|
|
|
@Output() formBuilt = new EventEmitter<FormGroup>();
|
|
|
|
private dialog = inject(MatDialog);
|
|
|
|
form!: FormGroup;
|
|
|
|
ngOnInit() {
|
|
const group: Record<string, AbstractControl> = {};
|
|
|
|
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<string, FormControl> = {};
|
|
|
|
config.forEach(f => {
|
|
group[f.name] = new FormControl(
|
|
(item as Record<string, unknown>)[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<string, (number | string)[]>();
|
|
|
|
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();
|
|
};
|
|
}
|