Skip to content
Merged
Show file tree
Hide file tree
Changes from 20 commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
1adc27f
dynamic form component init
erdemcaygor Oct 6, 2025
fe26f71
refactoring
erdemcaygor Oct 6, 2025
e8ced70
refactoring
erdemcaygor Oct 6, 2025
753350d
refactoring
erdemcaygor Oct 7, 2025
5ca3db5
refactoring
erdemcaygor Oct 8, 2025
d19134b
refactoring
erdemcaygor Oct 8, 2025
fb0dd44
refactoring
erdemcaygor Oct 8, 2025
bc3f38f
form field updated
erdemcaygor Oct 8, 2025
391c51e
dynamic form field validation logic updated
erdemcaygor Oct 8, 2025
ffa3b85
removed unnecessary form field control interface
erdemcaygor Oct 8, 2025
6715aae
refactoring
erdemcaygor Oct 8, 2025
b094d5a
dynamic form field validator func updated
erdemcaygor Oct 9, 2025
32f4047
localization added for error messages
erdemcaygor Oct 9, 2025
6ba1165
refactoring
erdemcaygor Oct 9, 2025
6e4179f
reset form function added
erdemcaygor Oct 9, 2025
d66c8d6
refactoring
erdemcaygor Oct 9, 2025
1963745
reset form function added
erdemcaygor Oct 10, 2025
a9486e9
dynamic form field host component added to custom form input
erdemcaygor Oct 13, 2025
fbd7aa5
refactoring
erdemcaygor Oct 13, 2025
3f2f25e
custom form field support added
erdemcaygor Oct 13, 2025
990f989
example
erdemcaygor Dec 2, 2025
b943365
Merge branch 'dev' into feat/#23891
erdemcaygor Dec 2, 2025
da26bfc
example
erdemcaygor Dec 2, 2025
fc2fcaf
example
erdemcaygor Dec 2, 2025
fc34eac
Merge branch 'dev' into feat/#23891
erdemcaygor Dec 9, 2025
77f16cb
dynamic options support added
erdemcaygor Dec 9, 2025
2db1b40
Merge branch 'rel-10.1' into feat/#23891
erdemcaygor Jan 5, 2026
3831000
log removed
erdemcaygor Jan 5, 2026
ec35e77
update: remove unused imports
sumeyyeKurtulus Jan 16, 2026
b4a44a4
new types added. A11y updates
erdemcaygor Jan 20, 2026
90b2be5
nested form support added
erdemcaygor Jan 20, 2026
b8d1170
refactor
erdemcaygor Jan 21, 2026
cb866e6
refactor
erdemcaygor Jan 21, 2026
b5d6a9c
unused imports removed
erdemcaygor Jan 21, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions npm/ng-packs/apps/dev-app/src/app/home/home.component.html
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
<div class="container">
<abp-dynamic-form [fields]="formFields" (onSubmit)="submit($event)">
</abp-dynamic-form>
<div class="p-5 text-center">
<div class="d-inline-block bg-success text-white p-1 h5 rounded mb-4" role="alert">
<h5 class="m-1">
<i class="fas fa-rocket" aria-hidden="true"></i> Congratulations,
<strong>MyProjectName</strong> is successfully running!
</h5>
</div>

<h1>{{ '::Welcome' | abpLocalization }}</h1>

<p class="lead px-lg-5 mx-lg-5">{{ '::LongWelcomeMessage' | abpLocalization }}</p>
Expand Down
84 changes: 81 additions & 3 deletions npm/ng-packs/apps/dev-app/src/app/home/home.component.ts
Original file line number Diff line number Diff line change
@@ -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();
Expand Down
4 changes: 1 addition & 3 deletions npm/ng-packs/apps/dev-app/src/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Copy link

Copilot AI Oct 14, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Setting NODE_TLS_REJECT_UNAUTHORIZED to '0' disables TLS certificate validation globally, which is a security risk. This should be conditional for development environments only or use a more targeted approach.

Suggested change
process.env["NODE_TLS_REJECT_UNAUTHORIZED"] = "0";
if (!environment.production) {
process.env["NODE_TLS_REJECT_UNAUTHORIZED"] = "0";
}

Copilot uses AI. Check for mistakes.

const serverDistFolder = dirname(fileURLToPath(import.meta.url));
const browserDistFolder = resolve(serverDistFolder, '../browser');
Expand Down
6 changes: 6 additions & 0 deletions npm/ng-packs/packages/components/dynamic-form/ng-package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"$schema": "../../../node_modules/ng-packagr/ng-entrypoint.schema.json",
"lib": {
"entryFile": "src/public-api.ts"
}
}
Original file line number Diff line number Diff line change
@@ -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<ControlValueAccessor> & { setDisabledState?(d: boolean): void };
type acceptsFormControl = { formControl?: FormControl };

@Component({
selector: 'abp-dynamic-form-field-host',
imports: [CommonModule, ReactiveFormsModule],
template: `<ng-template #vcRef></ng-template>`,
changeDetection: ChangeDetectionStrategy.OnPush,
providers: [{
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => DynamicFieldHostComponent),
multi: true
}]
})
export class DynamicFieldHostComponent implements ControlValueAccessor {
component = input<Type<ControlValueAccessor>>();
inputs = input<Record<string, any>>({});

@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<any>(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 });
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
@if (visible()) {
<div [formGroup]="fieldFormGroup">
@if (field().type === 'text') {
<!-- Text Input -->
<div class="form-group">
<ng-container [ngTemplateOutlet]="labelTemplate" />
<input
[id]="field().key"
[placeholder]="field().placeholder || ''"
formControlName="value"
[class.is-invalid]="isInvalid"
class="form-control">
@if (isInvalid) {
<ng-container [ngTemplateOutlet]="errorTemplate"/>
}
</div>
} @else if (field().type === 'select') {
<!-- Select Dropdown -->
<div class="form-group">
<ng-container [ngTemplateOutlet]="labelTemplate" />
<select
[id]="field().key"
formControlName="value"
[class.is-invalid]="isInvalid"
class="form-control">
<option value="">Please select...</option>
@for (option of field().options; track option.key) {
<option
[value]="option.key">
{{ option.value }}
</option>
}
</select>
@if (isInvalid) {
<ng-container [ngTemplateOutlet]="errorTemplate"/>
}
</div>
} @else if (field().type === 'checkbox') {
<!-- Checkbox -->
<div class="form-group form-check">
<abp-checkbox [label]="field().label" formControlName="value" [id]="field().key" />
@if (isInvalid) {
<ng-container [ngTemplateOutlet]="errorTemplate"/>
}
</div>
} @else if (field().type === 'email') {
<!-- Email Input -->
<div class="form-group">
<label [for]="field().key">{{ field().label }}</label>
<input
type="email"
[id]="field().key"
formControlName="value"
[placeholder]="field().placeholder || ''"
[class.is-invalid]="isInvalid"
class="form-control">
@if (isInvalid) {
<ng-container [ngTemplateOutlet]="errorTemplate"/>
}
</div>
} @else if (field().type === 'textarea') {
<!-- Textarea -->
<div class="form-group">
<label [for]="field().key">{{ field().label }}</label>
<textarea
[id]="field().key"
formControlName="value"
[placeholder]="field().placeholder || ''"
[class.is-invalid]="isInvalid"
rows="4"
class="form-control">
</textarea>
@if (isInvalid) {
<ng-container [ngTemplateOutlet]="errorTemplate"/>
}
</div>
}
</div>
}

<ng-template #labelTemplate>
<label [for]="field().key">{{ field().label | abpLocalization }}</label>
</ng-template>

<ng-template #errorTemplate>
<div class="invalid-feedback">
@for (error of errors; track error) {
<div>{{ error | abpLocalization }}</div>
}
</div>
</ng-template>
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
.form-group {
display: flex;
flex-direction: column;
}
Loading