@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
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-formRequirements
| 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
- Components must be
standalone: true - Use signal inputs:
input.required<FormControl>() - Import
ReactiveFormsModulein 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 installValidation not triggering
Check your FormField configuration:
- Use
required: truefor required validation - Use
type: 'email'for email validation - Use
minLength,maxLengthfor 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
