npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@ttech-iq/ngx-dynamic-form

v1.0.1

Published

Build dynamic Angular reactive forms with your own custom components. Zero UI opinions, full control.

Readme

@ttech-iq/ngx-dynamic-form

npm version License: MIT Angular

Build dynamic Angular reactive forms with your own custom components. Zero UI opinions, full control.

✨ Features

  • 🎨 Use your own components - Complete styling freedom with any UI framework
  • 🔧 Full TypeScript support - Strongly typed configurations and interfaces
  • 📦 Zero UI dependencies - Works with Tailwind, Bootstrap, Material, or vanilla CSS
  • Built-in validation - Automatic validation (required, email, minLength, maxLength, min, max, pattern)
  • 🚀 Dynamic component loading - Efficient component instantiation with Angular's ViewContainerRef
  • Modern Angular - Standalone components, signal inputs, and zoneless compatible
  • 🔄 Reactive - Real-time form value changes and validation state

📦 Installation

npm install @ttech-iq/ngx-dynamic-form

Requirements

| Angular Version | Supported | | --------------- | --------- | | 20.x | ✅ | | 19.x | ✅ | | 18.x | ✅ | | 17.x | ✅ |

Peer Dependencies:

  • @angular/common >= 17.0.0
  • @angular/core >= 17.0.0
  • @angular/forms >= 17.0.0

🚀 Quick Start

1. Create Your Custom Component

Create input components that accept control and field signal inputs:

import { Component, input } from "@angular/core";
import { FormControl, ReactiveFormsModule } from "@angular/forms";
import { FormField } from "@ttech-iq/ngx-dynamic-form";

@Component({
  selector: "app-custom-input",
  standalone: true,
  imports: [ReactiveFormsModule],
  template: `
    <div class="form-field">
      <label>{{ field().label }}</label>
      <input
        [formControl]="control()"
        [type]="field().type || 'text'"
        [placeholder]="field().placeholder || ''"
      />
    </div>
  `,
})
export class CustomInputComponent {
  readonly control = input.required<FormControl>();
  readonly field = input.required<FormField>();
}

2. Register Components in app.config.ts

import { ApplicationConfig } from "@angular/core";
import {
  DYNAMIC_FORM_CONFIG,
  DynamicFormConfigService,
} from "@ttech-iq/ngx-dynamic-form";
import { CustomInputComponent } from "./components/custom-input.component";
import { CustomSelectComponent } from "./components/custom-select.component";

export const appConfig: ApplicationConfig = {
  providers: [
    {
      provide: DYNAMIC_FORM_CONFIG,
      useValue: {
        components: {
          textbox: CustomInputComponent,
          email: CustomInputComponent,
          select: CustomSelectComponent,
        },
      },
    },
    {
      provide: DynamicFormConfigService,
      useFactory: () => {
        const service = new DynamicFormConfigService();
        service.setConfig({
          components: {
            textbox: CustomInputComponent,
            email: CustomInputComponent,
            select: CustomSelectComponent,
          },
        });
        return service;
      },
    },
  ],
};

3. Use the Dynamic Form

import { Component } from "@angular/core";
import { DynamicFormComponent, FormField } from "@ttech-iq/ngx-dynamic-form";

@Component({
  selector: "app-registration",
  standalone: true,
  imports: [DynamicFormComponent],
  template: `
    <ngx-dynamic-form
      [fields]="formFields"
      [showSubmitButton]="true"
      [submitButtonText]="'Register'"
      (formSubmit)="onSubmit($event)"
      (formValueChange)="onValueChange($event)"
      (formReady)="onFormReady($event)"
    />
  `,
})
export class RegistrationComponent {
  formFields: FormField[] = [
    {
      key: "username",
      label: "Username",
      controlType: "textbox",
      type: "text",
      required: true,
      minLength: 3,
      maxLength: 20,
      placeholder: "Enter username",
    },
    {
      key: "email",
      label: "Email Address",
      controlType: "textbox",
      type: "email",
      required: true,
      placeholder: "[email protected]",
    },
    {
      key: "country",
      label: "Country",
      controlType: "select",
      required: true,
      options: [
        { label: "United States", value: "us" },
        { label: "United Kingdom", value: "uk" },
        { label: "Canada", value: "ca" },
      ],
    },
  ];

  onSubmit(formData: any) {
    console.log("Form submitted:", formData);
  }

  onValueChange(values: any) {
    console.log("Form values changed:", values);
  }

  onFormReady(form: FormGroup) {
    console.log("Form initialized:", form);
  }
}

That's it! Your form is ready. 🎉


📋 API Reference

DynamicFormComponent

The main form component that renders your dynamic fields.

Selector: ngx-dynamic-form

Inputs

| Input | Type | Default | Description | | ------------------ | ---------------------------- | ----------- | ------------------------------------ | | fields | FormField[] | required | Array of field configurations | | showSubmitButton | boolean | true | Show/hide the built-in submit button | | submitButtonText | string | 'Submit' | Text for the submit button | | customValidators | Map<string, ValidatorFn[]> | undefined | Custom validators per field key |

Outputs

| Output | Type | Description | | ----------------- | ------------------------- | ------------------------------------------------ | | formSubmit | EventEmitter<any> | Emits form values when submitted (only if valid) | | formValueChange | EventEmitter<any> | Emits on every form value change | | formReady | EventEmitter<FormGroup> | Emits the FormGroup when initialized |

Methods

| Method | Returns | Description | | --------------------- | ----------------- | --------------------------- | | getFormValue() | any | Get current form values | | getFormControl(key) | AbstractControl | Get a specific form control | | resetForm() | void | Reset all form fields | | isValid() | boolean | Check if form is valid |


FormField Interface

Configuration object for each form field.

interface FormField {
  // Required
  key: string; // Unique identifier, used as form control name
  label: string; // Display label
  controlType: FieldType; // Component type to render

  // Optional - Values
  value?: any; // Initial value
  disabled?: boolean; // Disable the field

  // Optional - Input attributes
  type?: string; // HTML input type (text, email, password, etc.)
  placeholder?: string; // Placeholder text
  icon?: string; // Icon identifier for your component

  // Optional - Validation
  required?: boolean; // Mark as required
  minLength?: number; // Minimum character length
  maxLength?: number; // Maximum character length
  min?: number; // Minimum numeric value
  max?: number; // Maximum numeric value
  pattern?: string; // Regex pattern

  // Optional - Select/Radio/Checkbox
  options?: SelectOption[]; // Options for select, radio, multiselect

  // Optional - Advanced
  customProps?: Record<string, any>; // Pass any custom properties to your component
  onValueChange?: (value: any) => void; // Callback on value change
}

FieldType

Built-in control types (you can also use custom string keys):

type FieldType =
  | "textbox"
  | "select"
  | "multiselect"
  | "radio"
  | "checkbox"
  | "textarea"
  | "date"
  | "number";

SelectOption Interface

interface SelectOption {
  label: string;
  value: string | number | boolean;
}

🎯 Advanced Usage

Custom Validators

Add custom validators per field:

import { ValidatorFn, AbstractControl, ValidationErrors } from "@angular/forms";

// Custom validator function
function noWhitespace(control: AbstractControl): ValidationErrors | null {
  if (control.value && control.value.trim() === "") {
    return { whitespace: true };
  }
  return null;
}

@Component({
  template: `
    <ngx-dynamic-form
      [fields]="fields"
      [customValidators]="validators"
      (formSubmit)="onSubmit($event)"
    />
  `,
})
export class MyFormComponent {
  fields: FormField[] = [
    { key: "username", label: "Username", controlType: "textbox" },
  ];

  validators = new Map<string, ValidatorFn[]>([["username", [noWhitespace]]]);
}

Value Change Callbacks

React to individual field changes:

formFields: FormField[] = [
  {
    key: 'country',
    label: 'Country',
    controlType: 'select',
    options: [...],
    onValueChange: (value) => {
      // Load states/provinces based on selected country
      this.loadStates(value);
    }
  },
  {
    key: 'state',
    label: 'State',
    controlType: 'select',
    options: [] // Will be populated dynamically
  }
];

Custom Properties

Pass any additional properties to your components:

formFields: FormField[] = [
  {
    key: 'phone',
    label: 'Phone',
    controlType: 'textbox',
    customProps: {
      mask: '(000) 000-0000',
      icon: 'phone',
      showClearButton: true,
      theme: 'outlined'
    }
  }
];

Access in your component:

@Component({
  template: `
    @if (field().customProps?.icon) {
    <i [class]="'icon-' + field().customProps.icon"></i>
    }
    <input
      [formControl]="control()"
      [attr.data-mask]="field().customProps?.mask"
    />
  `,
})
export class CustomInputComponent {
  readonly control = input.required<FormControl>();
  readonly field = input.required<FormField>();
}

Accessing the FormGroup

Use formReady to get direct access to the underlying FormGroup:

@Component({
  template: `
    <ngx-dynamic-form [fields]="fields" (formReady)="onFormReady($event)" />
    <button (click)="patchValues()">Patch Values</button>
  `,
})
export class MyComponent {
  private formGroup!: FormGroup;

  onFormReady(form: FormGroup) {
    this.formGroup = form;
  }

  patchValues() {
    this.formGroup.patchValue({
      username: "john_doe",
      email: "[email protected]",
    });
  }
}

Using with NgModule

For module-based applications, use DynamicFormModule.forRoot():

import { NgModule } from "@angular/core";
import { DynamicFormModule } from "@ttech-iq/ngx-dynamic-form";
import { CustomInputComponent } from "./components/custom-input.component";

@NgModule({
  imports: [
    DynamicFormModule.forRoot({
      components: {
        textbox: CustomInputComponent,
        select: CustomSelectComponent,
      },
    }),
  ],
})
export class AppModule {}

🎨 Styling Examples

Tailwind CSS

@Component({
  template: `
    <div class="mb-4">
      <label class="block text-gray-700 text-sm font-bold mb-2">
        {{ field().label }}
        @if (field().required) { <span class="text-red-500">*</span> }
      </label>
      <input
        [formControl]="control()"
        class="shadow appearance-none border rounded w-full py-2 px-3
               text-gray-700 leading-tight focus:outline-none
               focus:ring-2 focus:ring-blue-500"
        [class.border-red-500]="control().invalid && control().touched"
      />
    </div>
  `
})
export class TailwindInputComponent { ... }

Bootstrap

@Component({
  template: `
    <div class="mb-3">
      <label class="form-label">{{ field().label }}</label>
      <input
        [formControl]="control()"
        class="form-control"
        [class.is-invalid]="control().invalid && control().touched"
      />
      <div class="invalid-feedback">
        Please provide a valid {{ field().label | lowercase }}.
      </div>
    </div>
  `
})
export class BootstrapInputComponent { ... }

Angular Material

@Component({
  imports: [MatFormFieldModule, MatInputModule, ReactiveFormsModule],
  template: `
    <mat-form-field appearance="outline" class="w-full">
      <mat-label>{{ field().label }}</mat-label>
      <input matInput [formControl]="control()" />
      <mat-error>{{ getErrorMessage() }}</mat-error>
    </mat-form-field>
  `
})
export class MaterialInputComponent { ... }

⚠️ Troubleshooting

"No component registered for field type: X"

Make sure you've registered a component for the controlType you're using:

// In app.config.ts
{
  components: {
    textbox: MyInputComponent,  // 'textbox' must match controlType
    custom: MyCustomComponent   // Custom types work too
  }
}

Components not rendering

  1. Components must be standalone: true
  2. Use signal inputs: input.required<FormControl>()
  3. Import ReactiveFormsModule in your component

TypeScript errors with FormControl

Ensure your Angular versions are consistent. Clear and reinstall if needed:

rm -rf node_modules package-lock.json
npm install

Validation not triggering

Check your FormField configuration:

  • Use required: true for required validation
  • Use type: 'email' for email validation
  • Use minLength, maxLength for length validation

📚 Exports

All public API exports:

import {
  // Components
  DynamicFormComponent,
  DynamicFormFieldComponent,

  // Module (for NgModule apps)
  DynamicFormModule,

  // Services
  DynamicFormConfigService,
  FormFieldControlService,

  // Injection Token
  DYNAMIC_FORM_CONFIG,

  // Types & Interfaces
  FormField,
  FieldType,
  SelectOption,
  DynamicFormConfig,
  IDynamicFieldComponent,

  // Type Guard
  isDynamicFieldComponent,
} from "@ttech-iq/ngx-dynamic-form";

🤝 Component Contract

Your custom components must implement this interface:

import { input } from "@angular/core";
import { FormControl } from "@angular/forms";
import { FormField } from "@ttech-iq/ngx-dynamic-form";

@Component({
  standalone: true,
  imports: [ReactiveFormsModule],
  template: `...`,
})
export class YourComponent {
  // Required signal inputs
  readonly control = input.required<FormControl>();
  readonly field = input.required<FormField>();

  // Optional: receive custom props as additional inputs
  readonly customProp = input<string>();
}

📄 License

MIT © ttech


🔗 Links