Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
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