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);
  }
}