diff --git a/npm/ng-packs/apps/dev-app/src/app/dynamic-form-page/dynamic-form-page.component.html b/npm/ng-packs/apps/dev-app/src/app/dynamic-form-page/dynamic-form-page.component.html
index 62bed6e07f..c675dca707 100644
--- a/npm/ng-packs/apps/dev-app/src/app/dynamic-form-page/dynamic-form-page.component.html
+++ b/npm/ng-packs/apps/dev-app/src/app/dynamic-form-page/dynamic-form-page.component.html
@@ -17,7 +17,7 @@
User Registration Form
- All 16 field types with accessibility support
+ All 16 field types + Nested Forms (Group & Array) with full accessibility support
@if (formFields.length) {
@@ -84,13 +84,26 @@
File (Single/Multiple)
+
+
Nested Forms
+
+ Group (Nested Fields)
+ Array (Dynamic List)
+
+
- Features: Full validation support, conditional logic,
+ Features: Full validation support, conditional logic, nested forms (groups & arrays),
grid-based layout, ARIA accessibility, keyboard navigation, and screen reader support.
+
+
+
+ New: Try the nested forms below! Add/remove phone numbers dynamically,
+ or fill in your work experience. The Address section shows a grouped form.
+
diff --git a/npm/ng-packs/apps/dev-app/src/app/dynamic-form-page/form-config.service.ts b/npm/ng-packs/apps/dev-app/src/app/dynamic-form-page/form-config.service.ts
index 1935a8fcbb..b88423f945 100644
--- a/npm/ng-packs/apps/dev-app/src/app/dynamic-form-page/form-config.service.ts
+++ b/npm/ng-packs/apps/dev-app/src/app/dynamic-form-page/form-config.service.ts
@@ -237,6 +237,143 @@ export class FormConfigService {
order: 19,
validators: [{ type: 'requiredTrue', message: 'You must agree to the terms' }],
},
+
+ // Section 10: NESTED FORM - Phone Numbers (Array)
+ {
+ key: 'phoneNumbers',
+ type: 'array',
+ label: 'Phone Numbers',
+ gridSize: 12,
+ order: 20,
+ minItems: 1,
+ maxItems: 5,
+ children: [
+ {
+ key: 'type',
+ type: 'select',
+ label: 'Type',
+ gridSize: 4,
+ required: true,
+ options: {
+ defaultValues: [
+ { key: 'mobile', value: 'Mobile' },
+ { key: 'home', value: 'Home' },
+ { key: 'work', value: 'Work' },
+ { key: 'other', value: 'Other' }
+ ]
+ },
+ validators: [{ type: 'required', message: 'Phone type is required' }],
+ },
+ {
+ key: 'number',
+ type: 'tel',
+ label: 'Number',
+ gridSize: 8,
+ required: true,
+ placeholder: '555-123-4567',
+ validators: [{ type: 'required', message: 'Phone number is required' }],
+ },
+ ]
+ },
+
+ // Section 11: NESTED FORM - Work Experience (Array with nested group)
+ {
+ key: 'workExperience',
+ type: 'array',
+ label: 'Work Experience',
+ gridSize: 12,
+ order: 21,
+ minItems: 0,
+ maxItems: 10,
+ children: [
+ {
+ key: 'company',
+ type: 'text',
+ label: 'Company Name',
+ gridSize: 6,
+ required: true,
+ validators: [{ type: 'required', message: 'Company name is required' }],
+ },
+ {
+ key: 'position',
+ type: 'text',
+ label: 'Position',
+ gridSize: 6,
+ required: true,
+ validators: [{ type: 'required', message: 'Position is required' }],
+ },
+ {
+ key: 'startDate',
+ type: 'date',
+ label: 'Start Date',
+ gridSize: 6,
+ required: true,
+ validators: [{ type: 'required', message: 'Start date is required' }],
+ },
+ {
+ key: 'endDate',
+ type: 'date',
+ label: 'End Date',
+ gridSize: 6,
+ },
+ {
+ key: 'currentJob',
+ type: 'checkbox',
+ label: 'Currently working here',
+ gridSize: 12,
+ },
+ {
+ key: 'description',
+ type: 'textarea',
+ label: 'Job Description',
+ placeholder: 'Describe your responsibilities...',
+ gridSize: 12,
+ maxLength: 500,
+ },
+ ]
+ },
+
+ // Section 12: NESTED FORM - Address Group (Group type)
+ {
+ key: 'address',
+ type: 'group',
+ label: 'Address Information',
+ gridSize: 12,
+ order: 22,
+ children: [
+ {
+ key: 'street',
+ type: 'text',
+ label: 'Street Address',
+ gridSize: 8,
+ placeholder: '123 Main St',
+ },
+ {
+ key: 'apartment',
+ type: 'text',
+ label: 'Apt/Suite',
+ gridSize: 4,
+ placeholder: 'Apt 4B',
+ },
+ {
+ key: 'city',
+ type: 'text',
+ label: 'City',
+ gridSize: 6,
+ required: true,
+ validators: [{ type: 'required', message: 'City is required' }],
+ },
+ {
+ key: 'zipCode',
+ type: 'text',
+ label: 'ZIP Code',
+ gridSize: 6,
+ required: true,
+ pattern: '[0-9]{5}',
+ validators: [{ type: 'required', message: 'ZIP code is required' }],
+ },
+ ]
+ },
];
return of(formConfig);
diff --git a/npm/ng-packs/packages/components/dynamic-form/NESTED-FORMS.md b/npm/ng-packs/packages/components/dynamic-form/NESTED-FORMS.md
new file mode 100644
index 0000000000..0c88c27a2d
--- /dev/null
+++ b/npm/ng-packs/packages/components/dynamic-form/NESTED-FORMS.md
@@ -0,0 +1,343 @@
+# Nested Forms Guide
+
+## Overview
+
+Dynamic Form now supports **nested forms** with two new field types:
+- **`group`** - Group related fields together
+- **`array`** - Dynamic lists with add/remove functionality
+
+## Quick Start
+
+### 1. Group Type (Nested Fields)
+
+Group related fields together with visual hierarchy:
+
+```typescript
+{
+ key: 'address',
+ type: 'group',
+ label: 'Address Information',
+ gridSize: 12,
+ children: [
+ {
+ key: 'street',
+ type: 'text',
+ label: 'Street',
+ gridSize: 8
+ },
+ {
+ key: 'city',
+ type: 'text',
+ label: 'City',
+ gridSize: 4
+ },
+ {
+ key: 'zipCode',
+ type: 'text',
+ label: 'ZIP Code',
+ gridSize: 6
+ }
+ ]
+}
+```
+
+**Output:**
+```json
+{
+ "address": {
+ "street": "123 Main St",
+ "city": "New York",
+ "zipCode": "10001"
+ }
+}
+```
+
+### 2. Array Type (Dynamic Lists)
+
+Create dynamic lists with add/remove buttons:
+
+```typescript
+{
+ key: 'phoneNumbers',
+ type: 'array',
+ label: 'Phone Numbers',
+ minItems: 1,
+ maxItems: 5,
+ gridSize: 12,
+ children: [
+ {
+ key: 'type',
+ type: 'select',
+ label: 'Type',
+ gridSize: 4,
+ options: {
+ defaultValues: [
+ { key: 'mobile', value: 'Mobile' },
+ { key: 'home', value: 'Home' },
+ { key: 'work', value: 'Work' }
+ ]
+ }
+ },
+ {
+ key: 'number',
+ type: 'tel',
+ label: 'Number',
+ gridSize: 8
+ }
+ ]
+}
+```
+
+**Output:**
+```json
+{
+ "phoneNumbers": [
+ { "type": "mobile", "number": "555-1234" },
+ { "type": "work", "number": "555-5678" }
+ ]
+}
+```
+
+## Features
+
+### Array Features
+- ✅ **Add Button** - Adds new item (respects maxItems)
+- ✅ **Remove Button** - Removes item (respects minItems)
+- ✅ **Item Counter** - Shows current count and limits
+- ✅ **Item Labels** - "Phone Number #1", "Phone Number #2"
+- ✅ **Min/Max Validation** - Buttons automatically disabled
+- ✅ **Empty State** - Shows info message when no items
+
+### Group Features
+- ✅ **Visual Hierarchy** - Border and background styling
+- ✅ **Legend Label** - Fieldset with legend for accessibility
+- ✅ **Grid Support** - All children support gridSize
+- ✅ **Nested Groups** - Groups inside groups supported
+
+### Recursive Support
+- ✅ **Array in Array** - Phone numbers can have sub-arrays
+- ✅ **Group in Array** - Work experience can have grouped fields
+- ✅ **Array in Group** - Address can have multiple phone numbers
+- ✅ **Unlimited Nesting** - No depth limit
+
+## Advanced Examples
+
+### Complex Nested Structure
+
+```typescript
+{
+ key: 'workExperience',
+ type: 'array',
+ label: 'Work Experience',
+ minItems: 0,
+ maxItems: 10,
+ children: [
+ {
+ key: 'company',
+ type: 'text',
+ label: 'Company Name',
+ gridSize: 6,
+ required: true
+ },
+ {
+ key: 'position',
+ type: 'text',
+ label: 'Position',
+ gridSize: 6,
+ required: true
+ },
+ {
+ key: 'dates',
+ type: 'group', // Nested group inside array
+ label: 'Employment Dates',
+ gridSize: 12,
+ children: [
+ {
+ key: 'startDate',
+ type: 'date',
+ label: 'Start Date',
+ gridSize: 6
+ },
+ {
+ key: 'endDate',
+ type: 'date',
+ label: 'End Date',
+ gridSize: 6
+ }
+ ]
+ },
+ {
+ key: 'description',
+ type: 'textarea',
+ label: 'Description',
+ gridSize: 12
+ }
+ ]
+}
+```
+
+## API Reference
+
+### FormFieldConfig (Extended)
+
+```typescript
+interface FormFieldConfig {
+ // ... existing properties
+
+ // NEW: Nested form properties
+ children?: FormFieldConfig[]; // Child fields for group/array types
+ minItems?: number; // Minimum items for array (default: 0)
+ maxItems?: number; // Maximum items for array (default: unlimited)
+}
+```
+
+### New Components
+
+#### DynamicFormGroupComponent
+```typescript
+@Input() groupConfig: FormFieldConfig;
+@Input() formGroup: FormGroup;
+@Input() visible: boolean = true;
+```
+
+#### DynamicFormArrayComponent
+```typescript
+@Input() arrayConfig: FormFieldConfig;
+@Input() formGroup: FormGroup;
+@Input() visible: boolean = true;
+
+addItem(): void; // Add new item to array
+removeItem(index): void; // Remove item from array
+```
+
+## Styling
+
+### Group Styling
+
+```scss
+.form-group-container {
+ border-left: 3px solid var(--bs-primary);
+ padding: 1rem;
+ background-color: var(--bs-light);
+}
+```
+
+### Array Styling
+
+```scss
+.array-item {
+ border: 1px solid var(--bs-border-color);
+ padding: 1rem;
+ margin-bottom: 1rem;
+ background: white;
+
+ &:hover {
+ box-shadow: 0 0.125rem 0.25rem rgba(0, 0, 0, 0.075);
+ }
+}
+```
+
+## Accessibility
+
+All nested forms include:
+- ✅ **ARIA roles** (`role="group"`, `role="list"`, `role="listitem"`)
+- ✅ **ARIA labels** (`aria-label`, `aria-labelledby`)
+- ✅ **Live regions** (`aria-live="polite"` for item count)
+- ✅ **Semantic HTML** (``, ``)
+- ✅ **Keyboard navigation** (Tab, Enter, Space)
+- ✅ **Screen reader announcements**
+
+## Migration Guide
+
+### From Simple to Nested
+
+**Before:**
+```typescript
+{
+ key: 'street',
+ type: 'text',
+ label: 'Street'
+},
+{
+ key: 'city',
+ type: 'text',
+ label: 'City'
+}
+```
+
+**After:**
+```typescript
+{
+ key: 'address',
+ type: 'group',
+ label: 'Address',
+ children: [
+ { key: 'street', type: 'text', label: 'Street' },
+ { key: 'city', type: 'text', label: 'City' }
+ ]
+}
+```
+
+### Data Structure Change
+
+**Before:**
+```json
+{
+ "street": "123 Main St",
+ "city": "New York"
+}
+```
+
+**After:**
+```json
+{
+ "address": {
+ "street": "123 Main St",
+ "city": "New York"
+ }
+}
+```
+
+## Best Practices
+
+1. **Use Groups** for logical field grouping (address, contact info)
+2. **Use Arrays** for dynamic lists (phone numbers, work history)
+3. **Set minItems/maxItems** to prevent empty or excessive arrays
+4. **Use gridSize** for responsive layouts within nested forms
+5. **Keep nesting shallow** (max 2-3 levels for UX)
+6. **Add validation** to required nested fields
+7. **Use meaningful labels** for array items
+
+## Examples
+
+See `apps/dev-app/src/app/dynamic-form-page` for complete examples:
+- Phone Numbers (simple array)
+- Work Experience (complex array)
+- Address (group)
+
+## Troubleshooting
+
+### Array items not showing
+- Check `minItems` - may need to be > 0
+- Verify `children` array is not empty
+
+### Can't add items
+- Check `maxItems` limit
+- Verify button is not disabled
+
+### Form data not nested
+- Confirm `type: 'group'` or `type: 'array'`
+- Check FormGroup structure in component
+
+## Performance
+
+- ✅ **OnPush** change detection
+- ✅ **TrackBy** functions for arrays
+- ✅ **Lazy rendering** for conditional fields
+- ✅ **Minimal re-renders** on add/remove
+
+---
+
+**Version:** 1.0.0
+**Added:** 2026-01-20
+**Status:** Production Ready ✅
diff --git a/npm/ng-packs/packages/components/dynamic-form/src/dynamic-form-array/dynamic-form-array.component.html b/npm/ng-packs/packages/components/dynamic-form/src/dynamic-form-array/dynamic-form-array.component.html
new file mode 100644
index 0000000000..2608180d15
--- /dev/null
+++ b/npm/ng-packs/packages/components/dynamic-form/src/dynamic-form-array/dynamic-form-array.component.html
@@ -0,0 +1,93 @@
+@if (visible()) {
+
+}
diff --git a/npm/ng-packs/packages/components/dynamic-form/src/dynamic-form-array/dynamic-form-array.component.scss b/npm/ng-packs/packages/components/dynamic-form/src/dynamic-form-array/dynamic-form-array.component.scss
new file mode 100644
index 0000000000..71707783e8
--- /dev/null
+++ b/npm/ng-packs/packages/components/dynamic-form/src/dynamic-form-array/dynamic-form-array.component.scss
@@ -0,0 +1,75 @@
+.form-array-container {
+ margin-bottom: 1.5rem;
+}
+
+.array-header {
+ border-bottom: 2px solid var(--bs-primary, #007bff);
+ padding-bottom: 0.5rem;
+}
+
+.form-array-label {
+ font-size: 1.1rem;
+ font-weight: 600;
+ color: var(--bs-dark, #212529);
+ margin-bottom: 0;
+}
+
+.array-items {
+ margin-top: 1rem;
+}
+
+.array-item {
+ background-color: var(--bs-white, #fff);
+ transition: all 0.2s ease;
+ position: relative;
+
+ &:hover {
+ box-shadow: 0 0.125rem 0.25rem rgba(0, 0, 0, 0.075);
+ transform: translateY(-1px);
+ }
+
+ // Nested arrays get lighter background
+ .array-item {
+ background-color: var(--bs-light, #f8f9fa);
+ }
+}
+
+.item-header {
+ border-bottom: 1px solid var(--bs-border-color, #dee2e6);
+ padding-bottom: 0.75rem;
+}
+
+.item-title {
+ color: var(--bs-primary, #007bff);
+ font-size: 0.95rem;
+}
+
+.array-footer {
+ margin-top: 0.5rem;
+ padding-top: 0.5rem;
+ border-top: 1px solid var(--bs-border-color, #dee2e6);
+}
+
+// Accessibility: Focus styles for buttons
+button {
+ &:focus-visible {
+ outline: 2px solid var(--bs-primary, #007bff);
+ outline-offset: 2px;
+ }
+}
+
+// Animation for add/remove
+@keyframes slideIn {
+ from {
+ opacity: 0;
+ transform: translateY(-10px);
+ }
+ to {
+ opacity: 1;
+ transform: translateY(0);
+ }
+}
+
+.array-item {
+ animation: slideIn 0.3s ease;
+}
diff --git a/npm/ng-packs/packages/components/dynamic-form/src/dynamic-form-array/dynamic-form-array.component.ts b/npm/ng-packs/packages/components/dynamic-form/src/dynamic-form-array/dynamic-form-array.component.ts
new file mode 100644
index 0000000000..eb86a9da7f
--- /dev/null
+++ b/npm/ng-packs/packages/components/dynamic-form/src/dynamic-form-array/dynamic-form-array.component.ts
@@ -0,0 +1,88 @@
+import {
+ ChangeDetectionStrategy,
+ Component,
+ input,
+ inject,
+ ChangeDetectorRef,
+ forwardRef,
+} from '@angular/core';
+import { FormGroup, FormArray, FormBuilder, ReactiveFormsModule } from '@angular/forms';
+import { CommonModule } from '@angular/common';
+import { FormFieldConfig } from '../dynamic-form.models';
+import { DynamicFormService } from '../dynamic-form.service';
+import { LocalizationPipe } from '@abp/ng.core';
+import { DynamicFormFieldComponent } from '../dynamic-form-field';
+import { DynamicFormGroupComponent } from '../dynamic-form-group';
+
+@Component({
+ selector: 'abp-dynamic-form-array',
+ templateUrl: './dynamic-form-array.component.html',
+ styleUrls: ['./dynamic-form-array.component.scss'],
+ changeDetection: ChangeDetectionStrategy.OnPush,
+ imports: [
+ CommonModule,
+ ReactiveFormsModule,
+ LocalizationPipe,
+ DynamicFormFieldComponent,
+ DynamicFormGroupComponent,
+ forwardRef(() => DynamicFormArrayComponent), // Self reference for recursion
+ ],
+})
+export class DynamicFormArrayComponent {
+ arrayConfig = input.required();
+ formGroup = input.required();
+ visible = input(true);
+
+ private fb = inject(FormBuilder);
+ private dynamicFormService = inject(DynamicFormService);
+ private cdr = inject(ChangeDetectorRef);
+
+ get formArray(): FormArray {
+ return this.formGroup().get(this.arrayConfig().key) as FormArray;
+ }
+
+ get sortedChildren(): FormFieldConfig[] {
+ const children = this.arrayConfig().children || [];
+ return children.sort((a, b) => (a.order || 0) - (b.order || 0));
+ }
+
+ get canAddItem(): boolean {
+ const maxItems = this.arrayConfig().maxItems;
+ return maxItems ? this.formArray.length < maxItems : true;
+ }
+
+ get canRemoveItem(): boolean {
+ const minItems = this.arrayConfig().minItems || 0;
+ return this.formArray.length > minItems;
+ }
+
+ addItem() {
+ if (!this.canAddItem) return;
+
+ const itemGroup = this.dynamicFormService.createFormGroup(
+ this.arrayConfig().children || []
+ );
+
+ this.formArray.push(itemGroup);
+ this.cdr.markForCheck();
+ }
+
+ removeItem(index: number) {
+ if (!this.canRemoveItem) return;
+
+ this.formArray.removeAt(index);
+ this.cdr.markForCheck();
+ }
+
+ getItemFormGroup(index: number): FormGroup {
+ return this.formArray.at(index) as FormGroup;
+ }
+
+ getNestedFormGroup(index: number, key: string): FormGroup {
+ return this.getItemFormGroup(index).get(key) as FormGroup;
+ }
+
+ trackByIndex(index: number): number {
+ return index;
+ }
+}
diff --git a/npm/ng-packs/packages/components/dynamic-form/src/dynamic-form-array/index.ts b/npm/ng-packs/packages/components/dynamic-form/src/dynamic-form-array/index.ts
new file mode 100644
index 0000000000..2ea9bc1460
--- /dev/null
+++ b/npm/ng-packs/packages/components/dynamic-form/src/dynamic-form-array/index.ts
@@ -0,0 +1 @@
+export * from './dynamic-form-array.component';
diff --git a/npm/ng-packs/packages/components/dynamic-form/src/dynamic-form-field/dynamic-form-field.component.html b/npm/ng-packs/packages/components/dynamic-form/src/dynamic-form-field/dynamic-form-field.component.html
index 962f0f7c90..ed33dca227 100644
--- a/npm/ng-packs/packages/components/dynamic-form/src/dynamic-form-field/dynamic-form-field.component.html
+++ b/npm/ng-packs/packages/components/dynamic-form/src/dynamic-form-field/dynamic-form-field.component.html
@@ -1,5 +1,9 @@
@if (visible()) {
+
+
+
+
@if (field().type === 'text') {