Angular Form Generator


Description

This is a simple form generator that can be used to generate forms in angular using material.

// silver-field.component.ts
import { CommonModule } from '@angular/common';
import { Component, Injectable, Input, Pipe, PipeTransform } from '@angular/core';
import { FormControl, FormGroup, FormsModule, ReactiveFormsModule, Validators } from '@angular/forms';
import { MatNativeDateModule } from '@angular/material/core';
import { MatDatepickerModule } from '@angular/material/datepicker';
import { MatFormFieldModule } from '@angular/material/form-field';
import { MatInputModule } from '@angular/material/input';
import { MatSelectModule } from '@angular/material/select';
import { MatSlideToggleModule } from '@angular/material/slide-toggle';
import { MatSliderModule } from '@angular/material/slider';

export const SilverFieldTypes = {
  SELECT: 'select',
  RANGE: 'range',
  TEXT: 'text',
  TOGGLE: 'toggle',
  DATE: 'date',
  DATE_RANGE: 'date-range',
  LONG_TEXT: 'long-text',
};

export class BaseField {
  controlType!: string;
  label!: string;
  key!: string;
  value: any;
  valid: any;
}

export class SelectField extends BaseField {
  override controlType = 'select';
  multiple = false;
  dropdown = true;
  options!: { [key: string]: string };
}

export class TextField extends BaseField {
  override controlType = 'text';
  type!: string;
  placeholder!: string;
}

export class TextArea extends BaseField {
  override controlType = 'long-text';
  rows!: number;
}

export class RangeField extends BaseField {
  override controlType = 'range';
  min!: number;
  max!: number;
  declare value: number;
}

export class ToggleField extends BaseField {
  override controlType = 'toggle';
  declare value: boolean;
}

export class DateField extends BaseField {
  override controlType = 'date';
  declare value: Date;
}

export class DateRangeField extends BaseField {
  override controlType = 'date-range';
  declare value: { from: Date; to: Date };
}

export type SilverField = SelectField | TextField | RangeField | ToggleField | DateField | DateRangeField | TextArea;

@Injectable({ providedIn: 'root' })
export class SilverFieldService {
  public fieldsToForm(fields: BaseField[]) {
    const formGroup = new FormGroup({});
    fields.forEach((field) => {
      const validations = [];
      if (field.valid) {
        const v = field.valid;
        if (v.required) validations.push(Validators.required);
      }
      if (field.controlType === 'date-range') {
        const range = new FormGroup({
          from: new FormControl<Date | null>(field.value.from, validations),
          to: new FormControl<Date | null>(field.value.to, validations),
        });
        formGroup.addControl(field.key, range);
      } else {
        formGroup.addControl(field.key, new FormControl(field.value, validations));
      }
    });
    return formGroup;
  }
}
@Pipe({ name: 'entries', standalone: true, pure: true })
export class EntriesPipe implements PipeTransform {
  transform(value: any): [string, any][] {
    return Object.entries(value);
  }
}

@Component({
  standalone: true,
  imports: [
    CommonModule,
    EntriesPipe,
    FormsModule,
    MatDatepickerModule,
    MatFormFieldModule,
    MatInputModule,
    MatNativeDateModule,
    MatSelectModule,
    MatSliderModule,
    MatSlideToggleModule,
    ReactiveFormsModule,
  ],
  selector: 'app-silver-field',
  template: `<ng-container [formGroup]="form">
    <ng-container [ngSwitch]="field.controlType">
      <mat-form-field *ngSwitchCase="'text'">
        <input [formControl]="form.controls[ctrlName]" matInput [type]="field.type" [placeholder]="field.placeholder" />
      </mat-form-field>
      <mat-form-field *ngSwitchCase="'long-text'">
        <textarea matInput [formControl]="form.controls[ctrlName]" [rows]="field.rows || 3"></textarea>
      </mat-form-field>
      <mat-form-field *ngSwitchCase="'select'">
        <mat-select [multiple]="field.multiple" [formControl]="form.controls[ctrlName]">
          <mat-option *ngFor="let opt of field.options | entries" [value]="opt[0]">{{ opt[1] }}</mat-option>
        </mat-select>
      </mat-form-field>
      <mat-form-field *ngSwitchCase="'date-range'">
        <mat-date-range-input [formGroup]="form.controls[ctrlName]" [rangePicker]="picker">
          <input matStartDate formControlName="from" placeholder="Start date" />
          <input matEndDate formControlName="to" placeholder="End date" />
        </mat-date-range-input>
        <mat-hint>MM/DD/YYYY - MM/DD/YYYY</mat-hint>
        <mat-datepicker-toggle matIconSuffix [for]="picker"></mat-datepicker-toggle>
        <mat-date-range-picker #picker></mat-date-range-picker>
        <mat-error *ngIf="form.controls[ctrlName].controls.from.hasError('matStartDateInvalid')"
          >Invalid start date</mat-error
        >
        <mat-error *ngIf="form.controls[ctrlName].controls.to.hasError('matEndDateInvalid')"
          >Invalid end date</mat-error
        >
      </mat-form-field>
      <mat-form-field *ngSwitchCase="'date'" appearance="fill">
        <input matInput [matDatepicker]="picker2" [formControl]="form.controls[ctrlName]" />
        <mat-hint>MM/DD/YYYY</mat-hint>
        <mat-datepicker-toggle matSuffix [for]="picker2"></mat-datepicker-toggle>
        <mat-datepicker #picker2></mat-datepicker>
      </mat-form-field>
      <mat-slider thumbLabel class="mx-3" *ngSwitchCase="'range'" [min]="field.min" [max]="field.max">
        <input matSliderThumb [formControl]="form.controls[ctrlName]" />
      </mat-slider>
      <div class="text-center" *ngSwitchCase="'toggle'">
        <mat-slide-toggle [formControl]="form.controls[ctrlName]" [checked]="field.value"></mat-slide-toggle>
      </div> </ng-container
  ></ng-container>`,
})
export class SilverFieldComponent {
  @Input() field: SilverField;
  @Input() form: FormGroup;
  @Input() ctrlName!: string;
}

Usage

import { CommonModule } from '@angular/common';
import { HttpClientModule } from '@angular/common/http';
import { ChangeDetectionStrategy, Component, inject } from '@angular/core';
import { FormGroup, FormsModule, ReactiveFormsModule } from '@angular/forms';
import { MatIconModule } from '@angular/material/icon';
import { MatTabsModule } from '@angular/material/tabs';
import { MatTooltipModule } from '@angular/material/tooltip';
import {
  SilverField,
  SilverFieldComponent,
  SilverFieldService,
  SilverFieldTypes,
} from '../../components/silver-field.component';
import { DirectiveModule } from '../../directive/directive.module';

@Component({
  selector: 'app-test',
  standalone: true,
  imports: [
    CommonModule,
    DirectiveModule,
    FormsModule,
    HttpClientModule,
    MatIconModule,
    MatTabsModule,
    MatTooltipModule,
    ReactiveFormsModule,
    SilverFieldComponent,
  ],
  template: `
    <div class="grow flex flex-col">
      <form [formGroup]="formGroup" class="w-[720px] m-auto" (ngSubmit)="submit()">
        <div *ngFor="let item of form" [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>
        <button type="submit">Submit</button>
      </form>
    </div>
  `,
  styles: [
    `
      :host {
        overflow: auto;
      }
    `,
  ],
  changeDetection: ChangeDetectionStrategy.OnPush,
})
export class TestComponent {
  #silverFieldService = inject(SilverFieldService);
  form: SilverField[];
  formGroup: FormGroup<any>;

  constructor() {
    this.makeForm();
  }

  submit() {
    console.log(this.formGroup.value);
  }

  async makeForm() {
    this.form = [
      {
        controlType: SilverFieldTypes.LONG_TEXT,
        label: 'Name',
        placeholder: 'Enter Name',
        type: 'text',
        key: 'name',
        value: '',
        valid: { required: true },
      },
      {
        controlType: SilverFieldTypes.TEXT,
        label: 'Name',
        placeholder: 'Enter Name',
        type: 'text',
        key: 'name',
        value: '',
        valid: { required: true },
      },
      {
        controlType: SilverFieldTypes.TEXT,
        label: 'Number',
        placeholder: 'Enter Number',
        type: 'number',
        key: 'number',
        value: '',
        valid: { required: true },
      },
      {
        controlType: SilverFieldTypes.SELECT,
        label: 'Select',
        key: 'select',
        value: '',
        valid: { required: true },
        multiple: false,
        dropdown: true,
        options: {
          option1: 'Option 1',
          option2: 'Option 2',
          option3: 'Option 3',
        },
      },
      {
        controlType: SilverFieldTypes.SELECT,
        label: 'Select',
        key: 'select',
        value: '',
        valid: { required: true },
        multiple: true,
        dropdown: true,
        options: {
          option1: 'Option 1',
          option2: 'Option 2',
          option3: 'Option 3',
        },
      },

      {
        controlType: SilverFieldTypes.DATE,
        label: 'Date',
        key: 'date',
        value: new Date(),
        valid: { required: true },
      },
      {
        controlType: SilverFieldTypes.DATE_RANGE,
        label: 'Date Range',
        key: 'dateRange',
        value: { from: new Date(), to: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000) },
        valid: { required: true },
      },
      {
        controlType: SilverFieldTypes.RANGE,
        label: 'Range',
        key: 'range',
        value: 0,
        min: 0,
        max: 100,
        valid: { required: true },
      },
      {
        controlType: SilverFieldTypes.TOGGLE,
        label: 'Toggle',
        key: 'toggle',
        value: false,
        valid: { required: true },
      },
    ];
    this.formGroup = this.#silverFieldService.fieldsToForm(this.form);
    // this.formGroup.valueChanges.subscribe((value) => {
    //   console.log(value);
    // });
  }
}