feat: implement dynamic form and popup components

- 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.
This commit is contained in:
2026-02-15 03:56:17 +05:30
parent 9e34de73ee
commit 55b436c7d2
43 changed files with 2043 additions and 186 deletions
+112
View File
@@ -0,0 +1,112 @@
import { CommonModule } from '@angular/common';
import { Component, OnInit, Input, Output, EventEmitter } 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 { provideNativeDateAdapter } from '@angular/material/core';
@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]
})
export class DynamicFormComponent implements OnInit {
@Input() config!: DynamicFormConfig;
@Input() initialData: Record<string, unknown> = {};
@Output() formBuilt = new EventEmitter<FormGroup>();
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;
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();
};
}