@@ -6,6 +8,7 @@
MyProjectName is successfully running!
+
{{ '::Welcome' | abpLocalization }}
{{ '::LongWelcomeMessage' | abpLocalization }}
diff --git a/npm/ng-packs/apps/dev-app/src/app/home/home.component.ts b/npm/ng-packs/apps/dev-app/src/app/home/home.component.ts
index 44bf9f269ec..6afd17a90c3 100644
--- a/npm/ng-packs/apps/dev-app/src/app/home/home.component.ts
+++ b/npm/ng-packs/apps/dev-app/src/app/home/home.component.ts
@@ -1,21 +1,99 @@
import { AuthService, LocalizationPipe } from '@abp/ng.core';
-import { Component, inject } from '@angular/core';
+import { Component, inject, ViewChild } from '@angular/core';
import { NgTemplateOutlet } from '@angular/common';
import { ButtonComponent, CardBodyComponent, CardComponent } from '@abp/ng.theme.shared';
+import { DynamicFormComponent, FormFieldConfig } from '@abp/ng.components/dynamic-form';
@Component({
selector: 'app-home',
templateUrl: './home.component.html',
- imports: [NgTemplateOutlet, LocalizationPipe, CardComponent, CardBodyComponent, ButtonComponent],
+ imports: [NgTemplateOutlet, LocalizationPipe, CardComponent, CardBodyComponent, ButtonComponent, DynamicFormComponent],
})
export class HomeComponent {
+ @ViewChild(DynamicFormComponent, { static: true }) dynamicFormComponent: DynamicFormComponent;
protected readonly authService = inject(AuthService);
-
+ formFields: FormFieldConfig[] = [
+ {
+ key: 'firstName',
+ type: 'text',
+ label: 'First Name',
+ placeholder: 'Enter first name',
+ value: 'erdemc',
+ required: true,
+ validators: [
+ { type: 'required', message: 'First name is required' },
+ { type: 'minLength', value: 2, message: 'Minimum 2 characters required' }
+ ],
+ gridSize: 6,
+ order: 1
+ },
+ {
+ key: 'lastName',
+ type: 'text',
+ label: 'Last Name',
+ placeholder: 'Enter last name',
+ required: true,
+ validators: [
+ { type: 'required', message: 'Last name is required' }
+ ],
+ gridSize: 12,
+ order: 3
+ },
+ {
+ key: 'email',
+ type: 'email',
+ label: 'Email Address',
+ placeholder: 'Enter email',
+ required: true,
+ validators: [
+ { type: 'required', message: 'Email is required' },
+ { type: 'email', message: 'Please enter a valid email' }
+ ],
+ gridSize: 6,
+ order: 2
+ },
+ {
+ key: 'userType',
+ type: 'select',
+ label: 'User Type',
+ required: true,
+ options: [
+ { key: 'admin', value: 'Administrator' },
+ { key: 'user', value: 'Regular User' },
+ { key: 'guest', value: 'Guest User' }
+ ],
+ validators: [
+ { type: 'required', message: 'Please select user type' }
+ ],
+ order: 4
+ },
+ {
+ key: 'adminNotes',
+ type: 'textarea',
+ label: 'Admin Notes',
+ placeholder: 'Enter admin-specific notes',
+ conditionalLogic: [
+ {
+ dependsOn: 'userType',
+ condition: 'equals',
+ value: 'admin',
+ action: 'show'
+ }
+ ],
+ order: 5
+ }
+ ];
loading = false;
+
get hasLoggedIn(): boolean {
return this.authService.isAuthenticated;
}
+ submit(val) {
+ console.log('submit', val);
+ this.dynamicFormComponent.resetForm();
+ }
+
login() {
this.loading = true;
this.authService.navigateToLogin();
diff --git a/npm/ng-packs/apps/dev-app/src/server.ts b/npm/ng-packs/apps/dev-app/src/server.ts
index a8d7558341c..fb45c1dec0a 100644
--- a/npm/ng-packs/apps/dev-app/src/server.ts
+++ b/npm/ng-packs/apps/dev-app/src/server.ts
@@ -11,9 +11,7 @@ import {environment} from './environments/environment';
import * as oidc from 'openid-client';
import { ServerCookieParser } from '@abp/ng.core';
-if (environment.production === false) {
- process.env["NODE_TLS_REJECT_UNAUTHORIZED"] = "0";
-}
+process.env["NODE_TLS_REJECT_UNAUTHORIZED"] = "0";
const serverDistFolder = dirname(fileURLToPath(import.meta.url));
const browserDistFolder = resolve(serverDistFolder, '../browser');
diff --git a/npm/ng-packs/packages/components/dynamic-form/ng-package.json b/npm/ng-packs/packages/components/dynamic-form/ng-package.json
new file mode 100644
index 00000000000..e09fb3fd037
--- /dev/null
+++ b/npm/ng-packs/packages/components/dynamic-form/ng-package.json
@@ -0,0 +1,6 @@
+{
+ "$schema": "../../../node_modules/ng-packagr/ng-entrypoint.schema.json",
+ "lib": {
+ "entryFile": "src/public-api.ts"
+ }
+}
diff --git a/npm/ng-packs/packages/components/dynamic-form/src/dynamic-form-field/dynamic-form-field-host.component.ts b/npm/ng-packs/packages/components/dynamic-form/src/dynamic-form-field/dynamic-form-field-host.component.ts
new file mode 100644
index 00000000000..54600b4e7a1
--- /dev/null
+++ b/npm/ng-packs/packages/components/dynamic-form/src/dynamic-form-field/dynamic-form-field-host.component.ts
@@ -0,0 +1,137 @@
+import {
+ Component,
+ ViewChild,
+ ViewContainerRef,
+ ChangeDetectionStrategy,
+ forwardRef,
+ Type,
+ Injector,
+ effect,
+ DestroyRef,
+ inject,
+ input,
+ ChangeDetectorRef,
+} from '@angular/core';
+import {
+ ControlValueAccessor, NG_VALUE_ACCESSOR, FormControl, ReactiveFormsModule
+} from '@angular/forms';
+import { CommonModule } from '@angular/common';
+import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
+
+type controlValueAccessorLike = Partial
& { setDisabledState?(d: boolean): void };
+type acceptsFormControl = { formControl?: FormControl };
+
+@Component({
+ selector: 'abp-dynamic-form-field-host',
+ imports: [CommonModule, ReactiveFormsModule],
+ template: ``,
+ changeDetection: ChangeDetectionStrategy.OnPush,
+ providers: [{
+ provide: NG_VALUE_ACCESSOR,
+ useExisting: forwardRef(() => DynamicFieldHostComponent),
+ multi: true
+ }]
+})
+export class DynamicFieldHostComponent implements ControlValueAccessor {
+ component = input>();
+ inputs = input>({});
+
+ @ViewChild('vcRef', { read: ViewContainerRef, static: true }) viewContainerRef!: ViewContainerRef;
+ private componentRef?: any;
+
+ private value: any;
+ private disabled = false;
+
+ // if child has not implemented ControlValueAccessor. Create form control
+ private innerControl = new FormControl(null);
+ readonly destroyRef = inject(DestroyRef);
+
+ constructor() {
+ effect(() => {
+ if (this.component()) {
+ this.createChild();
+ } else if (this.componentRef && this.inputs()) {
+ this.applyInputs();
+ }
+ });
+ }
+
+ private createChild() {
+ this.viewContainerRef.clear();
+ if (!this.component()) return;
+
+ this.componentRef = this.viewContainerRef.createComponent(this.component());
+ this.applyInputs();
+
+ const instance: any = this.componentRef.instance as controlValueAccessorLike & acceptsFormControl;
+
+ if (this.isCVA(instance)) {
+ // Child CVA ise wrapper -> child delege
+ instance.registerOnChange?.((v: any) => this.onChange(v));
+ instance.registerOnTouched?.(() => this.onTouched());
+ if (this.disabled && instance.setDisabledState) {
+ instance.setDisabledState(true);
+ }
+ // set initial value
+ if (this.value !== undefined) {
+ instance.writeValue?.(this.value);
+ }
+ } else {
+ // No CVA -> use form control
+ if ('formControl' in instance) {
+ instance.formControl = this.innerControl;
+ // apply initial value/disabled state
+ if (this.value !== undefined) {
+ this.innerControl.setValue(this.value, { emitEvent: false });
+ }
+ this.innerControl.valueChanges.pipe(takeUntilDestroyed(this.destroyRef)).subscribe(v => this.onChange(v));
+ this.innerControl.disabled ? null : (this.disabled && this.innerControl.disable({ emitEvent: false }));
+ }
+ }
+ }
+
+ private applyInputs() {
+ if (!this.componentRef) return;
+ const inst = this.componentRef.instance;
+ for (const [k, v] of Object.entries(this.inputs ?? {})) {
+ inst[k] = v;
+ }
+ this.componentRef.changeDetectorRef?.markForCheck?.();
+ }
+
+ private isCVA(obj: any): obj is controlValueAccessorLike {
+ return obj && typeof obj.writeValue === 'function' && typeof obj.registerOnChange === 'function';
+ }
+
+ writeValue(obj: any): void {
+ this.value = obj;
+ if (!this.componentRef) return;
+
+ const inst: any = this.componentRef.instance as controlValueAccessorLike & acceptsFormControl;
+
+ if (this.isCVA(inst)) {
+ inst.writeValue?.(obj);
+ } else if ('formControl' in inst && inst.formControl instanceof FormControl) {
+ inst.formControl.setValue(obj, { emitEvent: false });
+ }
+ }
+
+ private onChange: (v: any) => void = () => {};
+ private onTouched: () => void = () => {};
+
+ registerOnChange(fn: any): void { this.onChange = fn; }
+ registerOnTouched(fn: any): void { this.onTouched = fn; }
+
+ setDisabledState(isDisabled: boolean): void {
+ this.disabled = isDisabled;
+ if (!this.componentRef) return;
+
+ const inst = this.componentRef.instance as controlValueAccessorLike & acceptsFormControl;
+
+ if (this.isCVA(inst) && inst.setDisabledState) {
+ inst.setDisabledState(isDisabled);
+ } else if ('formControl' in inst && inst.formControl instanceof FormControl) {
+ isDisabled ? inst.formControl.disable({ emitEvent: false }) : inst.formControl.enable({ emitEvent: false });
+ }
+ }
+}
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
new file mode 100644
index 00000000000..8bd9ad1fbfe
--- /dev/null
+++ b/npm/ng-packs/packages/components/dynamic-form/src/dynamic-form-field/dynamic-form-field.component.html
@@ -0,0 +1,91 @@
+@if (visible()) {
+
+ @if (field().type === 'text') {
+
+
+
+
+ @if (isInvalid) {
+
+ }
+
+ } @else if (field().type === 'select') {
+
+
+
+
+ @if (isInvalid) {
+
+ }
+
+ } @else if (field().type === 'checkbox') {
+
+
+ } @else if (field().type === 'email') {
+
+
+
+
+ @if (isInvalid) {
+
+ }
+
+ } @else if (field().type === 'textarea') {
+
+
+
+
+ @if (isInvalid) {
+
+ }
+
+ }
+
+}
+
+
+
+
+
+
+
+ @for (error of errors; track error) {
+
{{ error | abpLocalization }}
+ }
+
+
diff --git a/npm/ng-packs/packages/components/dynamic-form/src/dynamic-form-field/dynamic-form-field.component.scss b/npm/ng-packs/packages/components/dynamic-form/src/dynamic-form-field/dynamic-form-field.component.scss
new file mode 100644
index 00000000000..a1b8a5c16fa
--- /dev/null
+++ b/npm/ng-packs/packages/components/dynamic-form/src/dynamic-form-field/dynamic-form-field.component.scss
@@ -0,0 +1,4 @@
+.form-group {
+ display: flex;
+ flex-direction: column;
+}
diff --git a/npm/ng-packs/packages/components/dynamic-form/src/dynamic-form-field/dynamic-form-field.component.ts b/npm/ng-packs/packages/components/dynamic-form/src/dynamic-form-field/dynamic-form-field.component.ts
new file mode 100644
index 00000000000..f7bd4493523
--- /dev/null
+++ b/npm/ng-packs/packages/components/dynamic-form/src/dynamic-form-field/dynamic-form-field.component.ts
@@ -0,0 +1,132 @@
+import {
+ ChangeDetectionStrategy,
+ ChangeDetectorRef,
+ Component,
+ DestroyRef,
+ forwardRef,
+ inject,
+ InjectionToken, Injector,
+ input,
+ OnInit,
+} from '@angular/core';
+import { FormFieldConfig } from '../dynamic-form.models';
+import {
+ ControlValueAccessor,
+ FormBuilder,
+ FormControl,
+ FormControlName,
+ FormGroupDirective,
+ NG_VALUE_ACCESSOR,
+ NgControl,
+ FormGroup,
+ ReactiveFormsModule,
+} from '@angular/forms';
+import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
+import { NgTemplateOutlet } from '@angular/common';
+import { LocalizationPipe } from '@abp/ng.core';
+import { FormCheckboxComponent } from '@abp/ng.theme.shared';
+
+export const ABP_DYNAMIC_FORM_FIELD = new InjectionToken('AbpDynamicFormField');
+
+const DYNAMIC_FORM_FIELD_CONTROL_VALUE_ACCESSOR = {
+ provide: NG_VALUE_ACCESSOR,
+ useExisting: forwardRef(() => DynamicFormFieldComponent),
+ multi: true,
+};
+
+@Component({
+ selector: 'abp-dynamic-form-field',
+ templateUrl: './dynamic-form-field.component.html',
+ styleUrls: ['./dynamic-form-field.component.scss'],
+ providers: [
+ { provide: ABP_DYNAMIC_FORM_FIELD, useExisting: DynamicFormFieldComponent },
+ DYNAMIC_FORM_FIELD_CONTROL_VALUE_ACCESSOR,
+ ],
+ host: { class: 'abp-dynamic-form-field' },
+ exportAs: 'abpDynamicFormField',
+ changeDetection: ChangeDetectionStrategy.OnPush,
+ imports: [NgTemplateOutlet, LocalizationPipe, ReactiveFormsModule, FormCheckboxComponent],
+})
+export class DynamicFormFieldComponent implements OnInit, ControlValueAccessor {
+ field = input.required();
+ visible = input(true);
+ control!: FormControl;
+ fieldFormGroup: FormGroup;
+ readonly changeDetectorRef = inject(ChangeDetectorRef);
+ readonly destroyRef = inject(DestroyRef);
+ private injector = inject(Injector);
+ private formBuilder = inject(FormBuilder);
+
+ constructor() {
+ this.fieldFormGroup = this.formBuilder.group({
+ value: [{ value: '' }],
+ });
+ }
+
+ ngOnInit() {
+ const ngControl = this.injector.get(NgControl, null);
+ if (ngControl) {
+ this.control = this.injector.get(FormGroupDirective).getControl(ngControl as FormControlName);
+ }
+ this.value.valueChanges.pipe(takeUntilDestroyed(this.destroyRef)).subscribe(value => {
+ this.onChange(value);
+ });
+ }
+
+ writeValue(value: any[]): void {
+ this.value.setValue(value || '');
+ this.changeDetectorRef.markForCheck();
+ }
+
+ registerOnChange(fn: any): void {
+ this.onChange = fn;
+ }
+
+ registerOnTouched(fn: any): void {
+ this.onTouched = fn;
+ }
+
+ setDisabledState(isDisabled: boolean): void {
+ if (isDisabled) {
+ this.value.disable();
+ } else {
+ this.value.enable();
+ }
+ this.changeDetectorRef.markForCheck();
+ }
+
+ get isInvalid(): boolean {
+ if (this.control) {
+ return this.control.invalid && (this.control.dirty || this.control.touched);
+ }
+ return false;
+ }
+
+ get errors(): string[] {
+ if (this.control && this.control.errors) {
+ const errorKeys = Object.keys(this.control.errors);
+ return errorKeys.map(key => {
+ const validator = this.field().validators.find(
+ v => v.type.toLowerCase() === key.toLowerCase(),
+ );
+ console.log(this.field().validators, key);
+ if (validator && validator.message) {
+ return validator.message;
+ }
+ // Fallback error messages
+ if (key === 'required') return `${this.field().label} is required`;
+ if (key === 'email') return 'Please enter a valid email address';
+ if (key === 'minlength')
+ return `Minimum length is ${this.control.errors[key].requiredLength}`;
+ if (key === 'maxlength')
+ return `Maximum length is ${this.control.errors[key].requiredLength}`;
+ return `${this.field().label} is invalid due to ${key} validation.`;
+ });
+ }
+ }
+ get value() {
+ return this.fieldFormGroup.get('value');
+ }
+ private onChange: (value: any) => void = () => {};
+ private onTouched: () => void = () => {};
+}
diff --git a/npm/ng-packs/packages/components/dynamic-form/src/dynamic-form-field/index.ts b/npm/ng-packs/packages/components/dynamic-form/src/dynamic-form-field/index.ts
new file mode 100644
index 00000000000..826f7c70d20
--- /dev/null
+++ b/npm/ng-packs/packages/components/dynamic-form/src/dynamic-form-field/index.ts
@@ -0,0 +1,2 @@
+export * from './dynamic-form-field.component';
+export * from './dynamic-form-field-host.component';
diff --git a/npm/ng-packs/packages/components/dynamic-form/src/dynamic-form.component.html b/npm/ng-packs/packages/components/dynamic-form/src/dynamic-form.component.html
new file mode 100644
index 00000000000..9c9709bea7a
--- /dev/null
+++ b/npm/ng-packs/packages/components/dynamic-form/src/dynamic-form.component.html
@@ -0,0 +1,46 @@
+
diff --git a/npm/ng-packs/packages/components/dynamic-form/src/dynamic-form.component.scss b/npm/ng-packs/packages/components/dynamic-form/src/dynamic-form.component.scss
new file mode 100644
index 00000000000..038d8eed943
--- /dev/null
+++ b/npm/ng-packs/packages/components/dynamic-form/src/dynamic-form.component.scss
@@ -0,0 +1,15 @@
+:host(.abp-dynamic-form) {
+ form {
+ display: flex;
+ flex-direction: column;
+ gap: 0.5rem;
+ }
+ .form-wrapper {
+ text-align: left;
+ }
+}
+.form-actions {
+ display: flex;
+ justify-content: flex-end;
+ gap: 0.5rem;
+}
diff --git a/npm/ng-packs/packages/components/dynamic-form/src/dynamic-form.component.ts b/npm/ng-packs/packages/components/dynamic-form/src/dynamic-form.component.ts
new file mode 100644
index 00000000000..2fb9a73372e
--- /dev/null
+++ b/npm/ng-packs/packages/components/dynamic-form/src/dynamic-form.component.ts
@@ -0,0 +1,178 @@
+import {
+ ChangeDetectionStrategy,
+ Component,
+ input,
+ output,
+ inject,
+ OnInit,
+ DestroyRef,
+ ChangeDetectorRef,
+ effect
+} from '@angular/core';
+import { FormGroup, ReactiveFormsModule } from '@angular/forms';
+import { CommonModule } from '@angular/common';
+import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
+import { DynamicFormService } from './dynamic-form.service';
+import { ConditionalAction, FormFieldConfig } from './dynamic-form.models';
+import { DynamicFormFieldComponent, DynamicFieldHostComponent } from './dynamic-form-field';
+
+@Component({
+ selector: 'abp-dynamic-form',
+ templateUrl: './dynamic-form.component.html',
+ styleUrls: ['./dynamic-form.component.scss'],
+ host: { class: 'abp-dynamic-form' },
+ changeDetection: ChangeDetectionStrategy.OnPush,
+ exportAs: 'abpDynamicForm',
+ imports: [
+ CommonModule,
+ DynamicFormFieldComponent,
+ ReactiveFormsModule,
+ DynamicFieldHostComponent,
+ ],
+})
+export class DynamicFormComponent implements OnInit {
+ fields = input([]);
+ values = input>();
+ submitButtonText = input('Submit');
+ submitInProgress = input(false);
+ showCancelButton = input(false);
+ onSubmit = output();
+ formCancel = output();
+ private dynamicFormService = inject(DynamicFormService);
+ readonly destroyRef = inject(DestroyRef);
+ readonly changeDetectorRef = inject(ChangeDetectorRef);
+
+ dynamicForm!: FormGroup;
+ fieldVisibility: { [key: string]: boolean } = {};
+
+ ngOnInit() {
+ this.setupFormAndLogic();
+ }
+
+ get sortedFields(): FormFieldConfig[] {
+ return this.fields().sort((a, b) => (a.order || 0) - (b.order || 0));
+ }
+
+ submit() {
+ console.log(this.dynamicForm.valid, this.dynamicForm.value);
+ if (this.dynamicForm.valid) {
+ this.onSubmit.emit(this.dynamicForm.getRawValue());
+ } else {
+ this.markAllFieldsAsTouched();
+ }
+ }
+
+ onCancel() {
+ this.formCancel.emit();
+ }
+
+ onFieldChange(event: { fieldKey: string; value: any }) {
+ this.evaluateConditionalLogic(event.fieldKey);
+ }
+
+ isFieldVisible(field: FormFieldConfig): boolean {
+ return this.fieldVisibility[field.key] !== false;
+ }
+
+ resetForm() {
+ const initialValues: { [key: string]: any } = this.dynamicFormService.getInitialValues(
+ this.fields(),
+ );
+ this.dynamicForm.reset({ ...initialValues });
+ this.dynamicForm.markAsUntouched();
+ this.dynamicForm.markAsPristine();
+ this.changeDetectorRef.markForCheck();
+ }
+
+ private initializeFieldVisibility() {
+ this.fields().forEach(field => {
+ this.fieldVisibility = {
+ ...this.fieldVisibility,
+ [field.key]: !field.conditionalLogic?.length,
+ };
+ });
+ }
+
+ private setupConditionalLogic() {
+ this.fields().forEach(field => {
+ if (field.conditionalLogic) {
+ field.conditionalLogic.forEach(rule => {
+ const dependentControl = this.dynamicForm.get(rule.dependsOn);
+ if (dependentControl) {
+ this.evaluateConditionalLogic(field.key);
+ dependentControl.valueChanges
+ .pipe(takeUntilDestroyed(this.destroyRef))
+ .subscribe(() => {
+ this.evaluateConditionalLogic(field.key);
+ });
+ }
+ });
+ }
+ });
+ }
+
+ private evaluateConditionalLogic(fieldKey: string) {
+ const field = this.fields().find(f => f.key === fieldKey);
+ if (!field?.conditionalLogic) return;
+
+ field.conditionalLogic.forEach(rule => {
+ const dependentValue = this.dynamicForm.get(rule.dependsOn)?.value;
+ const conditionMet = this.evaluateCondition(dependentValue, rule.condition, rule.value);
+
+ this.applyConditionalAction(fieldKey, rule.action, conditionMet);
+ });
+ }
+
+ private evaluateCondition(fieldValue: any, condition: string, ruleValue: any): boolean {
+ switch (condition) {
+ case 'equals':
+ return fieldValue === ruleValue;
+ case 'notEquals':
+ return fieldValue !== ruleValue;
+ case 'contains':
+ return fieldValue && fieldValue.includes && fieldValue.includes(ruleValue);
+ case 'greaterThan':
+ return Number(fieldValue) > Number(ruleValue);
+ case 'lessThan':
+ return Number(fieldValue) < Number(ruleValue);
+ default:
+ return false;
+ }
+ }
+
+ private applyConditionalAction(fieldKey: string, action: string, shouldApply: boolean) {
+ const control = this.dynamicForm.get(fieldKey);
+
+ switch (action) {
+ case ConditionalAction.SHOW:
+ this.fieldVisibility = { ...this.fieldVisibility, [fieldKey]: shouldApply };
+ break;
+ case ConditionalAction.HIDE:
+ this.fieldVisibility = { ...this.fieldVisibility, [fieldKey]: !shouldApply };
+ break;
+ case ConditionalAction.ENABLE:
+ if (control) {
+ shouldApply ? control.enable() : control.disable();
+ }
+ break;
+ case ConditionalAction.DISABLE:
+ if (control) {
+ shouldApply ? control.disable() : control.enable();
+ }
+ break;
+ }
+ }
+
+ private setupFormAndLogic() {
+ this.dynamicForm = this.dynamicFormService.createFormGroup(this.fields());
+ this.initializeFieldVisibility();
+ this.setupConditionalLogic();
+ this.changeDetectorRef.markForCheck();
+ }
+
+ private markAllFieldsAsTouched() {
+ Object.keys(this.dynamicForm.controls).forEach(key => {
+ this.dynamicForm.get(key)?.markAsTouched();
+ });
+ }
+}
diff --git a/npm/ng-packs/packages/components/dynamic-form/src/dynamic-form.models.ts b/npm/ng-packs/packages/components/dynamic-form/src/dynamic-form.models.ts
new file mode 100644
index 00000000000..716c252d7b9
--- /dev/null
+++ b/npm/ng-packs/packages/components/dynamic-form/src/dynamic-form.models.ts
@@ -0,0 +1,38 @@
+import { Type } from '@angular/core';
+import { ControlValueAccessor } from '@angular/forms';
+
+export interface FormFieldConfig {
+ key: string;
+ value?: any;
+ type: 'text' | 'email' | 'number' | 'select' | 'checkbox' | 'date' | 'textarea';
+ label: string;
+ placeholder?: string;
+ required?: boolean;
+ disabled?: boolean;
+ options?: { key: string; value: any }[];
+ validators?: ValidatorConfig[];
+ conditionalLogic?: ConditionalRule[];
+ order?: number;
+ gridSize?: number;
+ component?: Type;
+}
+
+export interface ValidatorConfig {
+ type: 'required' | 'email' | 'minLength' | 'maxLength' | 'pattern' | 'custom' | 'min' | 'max' | 'requiredTrue';
+ value?: any;
+ message: string;
+}
+
+export interface ConditionalRule {
+ dependsOn: string;
+ condition: 'equals' | 'notEquals' | 'contains' | 'greaterThan' | 'lessThan';
+ value: any;
+ action: 'show' | 'hide' | 'enable' | 'disable';
+}
+
+export enum ConditionalAction {
+ SHOW = 'show',
+ HIDE = 'hide',
+ ENABLE = 'enable',
+ DISABLE = 'disable'
+}
diff --git a/npm/ng-packs/packages/components/dynamic-form/src/dynamic-form.service.ts b/npm/ng-packs/packages/components/dynamic-form/src/dynamic-form.service.ts
new file mode 100644
index 00000000000..d5c90bc70af
--- /dev/null
+++ b/npm/ng-packs/packages/components/dynamic-form/src/dynamic-form.service.ts
@@ -0,0 +1,75 @@
+import {Injectable, inject} from '@angular/core';
+import {FormControl, FormGroup, ValidatorFn, Validators, FormBuilder} from '@angular/forms';
+import {FormFieldConfig, ValidatorConfig} from './dynamic-form.models';
+
+@Injectable({
+ providedIn: 'root'
+})
+
+export class DynamicFormService {
+
+ private formBuilder = inject(FormBuilder);
+
+ createFormGroup(fields: FormFieldConfig[]): FormGroup {
+ const group: any = {};
+
+ fields.forEach(field => {
+ const validators = this.buildValidators(field.validators || []);
+ const initialValue = this.getInitialValue(field);
+
+ group[field.key] = new FormControl({
+ value: initialValue,
+ disabled: field.disabled || false
+ }, validators);
+ });
+
+ return this.formBuilder.group(group);
+ }
+
+ getInitialValues(fields: FormFieldConfig[]): any {
+ const initialValues: any = {};
+ fields.forEach(field => {
+ initialValues[field.key] = this.getInitialValue(field);
+ });
+ return initialValues;
+ }
+
+ private buildValidators(validatorConfigs: ValidatorConfig[]): ValidatorFn[] {
+ return validatorConfigs.map(config => {
+ switch (config.type) {
+ case 'required':
+ return Validators.required;
+ case 'email':
+ return Validators.email;
+ case 'minLength':
+ return Validators.minLength(config.value);
+ case 'maxLength':
+ return Validators.maxLength(config.value);
+ case 'pattern':
+ return Validators.pattern(config.value);
+ case 'min':
+ return Validators.min(config.value);
+ case 'max':
+ return Validators.max(config.value);
+ case 'requiredTrue':
+ return Validators.requiredTrue;
+ default:
+ return Validators.nullValidator;
+ }
+ });
+ }
+
+ private getInitialValue(field: FormFieldConfig): any {
+ if (field.value) {
+ return field.value;
+ }
+ switch (field.type) {
+ case 'checkbox':
+ return false;
+ case 'number':
+ return 0;
+ default:
+ return '';
+ }
+ }
+}
diff --git a/npm/ng-packs/packages/components/dynamic-form/src/public-api.ts b/npm/ng-packs/packages/components/dynamic-form/src/public-api.ts
new file mode 100644
index 00000000000..aae289278c6
--- /dev/null
+++ b/npm/ng-packs/packages/components/dynamic-form/src/public-api.ts
@@ -0,0 +1,3 @@
+export * from './dynamic-form.component';
+export * from './dynamic-form-field';
+export * from './dynamic-form.models';
diff --git a/npm/ng-packs/tsconfig.base.json b/npm/ng-packs/tsconfig.base.json
index f863f18520d..0adc8db800a 100644
--- a/npm/ng-packs/tsconfig.base.json
+++ b/npm/ng-packs/tsconfig.base.json
@@ -21,6 +21,7 @@
"@abp/ng.account/config": ["packages/account/config/src/public-api.ts"],
"@abp/ng.components": ["packages/components/src/public-api.ts"],
"@abp/ng.components/chart.js": ["packages/components/chart.js/src/public-api.ts"],
+ "@abp/ng.components/dynamic-form": ["packages/components/dynamic-form/src/public-api.ts"],
"@abp/ng.components/extensible": ["packages/components/extensible/src/public-api.ts"],
"@abp/ng.components/page": ["packages/components/page/src/public-api.ts"],
"@abp/ng.components/tree": ["packages/components/tree/src/public-api.ts"],