Build dynamic Angular reactive forms with your own custom components. Zero UI opinions, full control.
npm install @ttech-iq/ngx-dynamic-form


> Build dynamic Angular reactive forms with your own custom components. Zero UI opinions, full control.
- 🎨 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
``bash`
npm install @ttech-iq/ngx-dynamic-form
| 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
---
Create input components that accept control and field signal inputs:
`typescript
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:
,
})
export class CustomInputComponent {
readonly control = input.required();
readonly field = input.required();
}
`$3
`typescript
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
`typescript
import { Component } from "@angular/core";
import { DynamicFormComponent, FormField } from "@ttech-iq/ngx-dynamic-form";@Component({
selector: "app-registration",
standalone: true,
imports: [DynamicFormComponent],
template:
,
})
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: "you@example.com",
},
{
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
$3
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 | undefined | Custom validators per field key |#### Outputs
| Output | Type | Description |
| ----------------- | ------------------------- | ------------------------------------------------ |
|
formSubmit | EventEmitter | Emits form values when submitted (only if valid) |
| formValueChange | EventEmitter | Emits on every form value change |
| formReady | EventEmitter | 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 |---
$3
Configuration object for each form field.
`typescript
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; // Pass any custom properties to your component
onValueChange?: (value: any) => void; // Callback on value change
}
`$3
Built-in control types (you can also use custom string keys):
`typescript
type FieldType =
| "textbox"
| "select"
| "multiselect"
| "radio"
| "checkbox"
| "textarea"
| "date"
| "number";
`$3
`typescript
interface SelectOption {
label: string;
value: string | number | boolean;
}
`---
🎯 Advanced Usage
$3
Add custom validators per field:
`typescript
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:
,
})
export class MyFormComponent {
fields: FormField[] = [
{ key: "username", label: "Username", controlType: "textbox" },
]; validators = new Map([["username", [noWhitespace]]]);
}
`$3
React to individual field changes:
`typescript
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
}
];
`$3
Pass any additional properties to your components:
`typescript
formFields: FormField[] = [
{
key: 'phone',
label: 'Phone',
controlType: 'textbox',
customProps: {
mask: '(000) 000-0000',
icon: 'phone',
showClearButton: true,
theme: 'outlined'
}
}
];
`Access in your component:
`typescript
@Component({
template: ,
})
export class CustomInputComponent {
readonly control = input.required();
readonly field = input.required();
}
`$3
Use
formReady to get direct access to the underlying FormGroup:`typescript
@Component({
template: ,
})
export class MyComponent {
private formGroup!: FormGroup; onFormReady(form: FormGroup) {
this.formGroup = form;
}
patchValues() {
this.formGroup.patchValue({
username: "john_doe",
email: "john@example.com",
});
}
}
`$3
For module-based applications, use
DynamicFormModule.forRoot():`typescript
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
$3
`typescript
@Component({
template:
})
export class TailwindInputComponent { ... }
`$3
`typescript
@Component({
template:
})
export class BootstrapInputComponent { ... }
`$3
`typescript
@Component({
imports: [MatFormFieldModule, MatInputModule, ReactiveFormsModule],
template:
})
export class MaterialInputComponent { ... }
`---
⚠️ Troubleshooting
$3
Make sure you've registered a component for the
controlType you're using:`typescript
// In app.config.ts
{
components: {
textbox: MyInputComponent, // 'textbox' must match controlType
custom: MyCustomComponent // Custom types work too
}
}
`$3
1. Components must be
standalone: true
2. Use signal inputs: input.required
3. Import ReactiveFormsModule in your component$3
Ensure your Angular versions are consistent. Clear and reinstall if needed:
`bash
rm -rf node_modules package-lock.json
npm install
`$3
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:
`typescript
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:
`typescript
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();
readonly field = input.required(); // Optional: receive custom props as additional inputs
readonly customProp = input();
}
``---
MIT © ttech
---