
Angular Dynamic Form Modal
Description
This is a Dynamic Form Modal Service that can be used to show dynamic forms in angular using material.
// form-modal.service.ts
import { CommonModule } from '@angular/common';
import { Component, Injectable, OnInit, inject } from '@angular/core';
import { FormGroup, FormsModule, ReactiveFormsModule } from '@angular/forms';
import { MatButtonModule } from '@angular/material/button';
import { MAT_DIALOG_DATA, MatDialog, MatDialogModule, MatDialogRef } from '@angular/material/dialog';
import { MatFormFieldModule } from '@angular/material/form-field';
import { lastValueFrom } from 'rxjs';
import { SilverField, SilverFieldComponent, SilverFieldService } from '../components/silver-field.component';
import { Deferred, defer } from '../shared/fun';
export interface ModalDataI {
heading: string;
form: SilverField[];
btn1Name?: string;
btn2Name?: string;
}
interface Ref_I {
onFormCreate: Deferred<any>;
form: Promise<any>;
afterClose: Promise<any>;
dialogRef: MatDialogRef<FormModalComponent, any>;
}
@Injectable({ providedIn: 'root' })
export class FormModalService {
readonly #dialog = inject(MatDialog);
public async open(data: ModalDataI, options = {}) {
const dialogRef = this.#dialog.open(FormModalComponent, {
width: '400px',
data: { data },
...options,
});
return await lastValueFrom(dialogRef.afterClosed());
}
public openForm(data: ModalDataI, options = {}) {
const onFormCreate = defer<any>();
const ref: Ref_I = {
onFormCreate,
form: onFormCreate.promise,
afterClose: undefined as Promise<any>,
dialogRef: undefined as MatDialogRef<FormModalComponent>,
};
const dialogRef = this.#dialog.open(FormModalComponent, {
width: '400px',
data: { data, ref },
...options,
});
ref.dialogRef = dialogRef;
ref.afterClose = lastValueFrom(dialogRef.afterClosed());
return ref;
}
}
@Component({
standalone: true,
imports: [
CommonModule,
FormsModule,
MatButtonModule,
MatDialogModule,
MatFormFieldModule,
ReactiveFormsModule,
SilverFieldComponent,
],
template: `<div mat-dialog-title style="font-size: 1.2rem">
{{ modalData.heading }}
</div>
<mat-dialog-content class="mat-typography" [formGroup]="formGroup">
<div *ngFor="let item of form; let i = index" [formGroupName]="item.key" class="flex flex-col">
<div>{{ item.label }}</div>
<app-silver-field
class="flex flex-col"
[field]="item"
[ctrlName]="item.key"
[form]="formGroup"
></app-silver-field>
</div>
</mat-dialog-content>
<mat-dialog-actions align="end">
<button mat-button [mat-dialog-close]="false" color="warn">
{{ btn1Name }}
</button>
<button mat-button color="accent" (click)="submit()" [disabled]="formGroup.invalid">
{{ btn2Name }}
</button>
</mat-dialog-actions>`,
})
export class FormModalComponent implements OnInit {
readonly #dialogRef: MatDialogRef<FormModalComponent> = inject(MatDialogRef);
readonly #silverFieldService = inject(SilverFieldService);
readonly #data: { data: ModalDataI; ref: any } = inject(MAT_DIALOG_DATA);
public formGroup!: FormGroup<any>;
public modalData: ModalDataI;
public btn1Name!: string;
public btn2Name!: string;
public form: SilverField[] = [];
public ref: Ref_I;
public ngOnInit(): void {
this.modalData = this.#data.data;
this.ref = this.#data.ref;
this.btn1Name = this.modalData.btn1Name ?? 'Cancel';
this.btn2Name = this.modalData.btn2Name ?? 'Confirm';
this.form = this.modalData.form ?? [];
this.formGroup = this.#silverFieldService.fieldsToForm(this.form);
if (this.ref) this.ref.onFormCreate.resolve(this.formGroup);
}
public submit() {
this.#dialogRef.close({ action: true, value: this.formGroup.value });
}
}
Definition of defer
export interface Deferred<T> {
resolve: (value: T | PromiseLike<T>) => void;
reject: (reason: unknown) => void;
promise: Promise<T>;
}
export const defer = <T = void>(): Deferred<T> => {
let resolve!: (value: T | PromiseLike<T>) => void;
let reject!: (reason: unknown) => void;
const promise = new Promise<T>((_resolve, _reject) => {
resolve = _resolve;
reject = _reject;
});
return { resolve, reject, promise };
};
Usage
import { Component } from '@angular/core';
import { FormModalService } from './form-modal.service';
@Component({
selector: 'test-page',
standalone: true,
imports: [CommonModule],
template: `<button (click)="open()">Open</button> <button (click)="open2()">Open2</button>`,
})
export class TestComponent implements OnInit {
#formModalService = inject(FormModalService);
async open() {
const result = await this.#formModalService.open({
heading: 'Set Required FPS',
form: [
{
controlType: 'text',
label: 'FPS',
placeholder: 'Enter FPS',
type: 'number',
key: 'fps',
value: '',
valid: { required: true },
},
],
btn1Name: 'Cancel',
btn2Name: 'Apply',
});
if (!result?.action) return;
console.log(result.value);
}
async open2() {
const data = this.#formModalService.openForm({
heading: 'Set Required FPS',
form: [
{
controlType: 'text',
label: 'FPS',
placeholder: 'Enter FPS',
type: 'number',
key: 'fps',
value: '',
valid: { required: true },
},
],
btn1Name: 'Cancel',
btn2Name: 'Apply',
});
const form: FormGroup = await data.form;
form.get('fps')?.valueChanges.subscribe((v) => {
console.log(v);
});
const result = await data.afterClose;
if (!result?.action) return;
console.log(form.value);
}
}