From a9486e92c45a23ce548717459d5856114cda510a Mon Sep 17 00:00:00 2001 From: erdemcaygor Date: Mon, 13 Oct 2025 16:26:27 +0300 Subject: [PATCH] dynamic form field host component added to custom form input --- .../dynamic-form-field-host.component.ts | 134 ++++++++++++++++++ .../src/dynamic-form-field/index.ts | 1 + .../src/dynamic-form.component.html | 8 ++ .../src/dynamic-form.component.ts | 24 +++- .../dynamic-form/src/dynamic-form.models.ts | 2 + 5 files changed, 165 insertions(+), 4 deletions(-) create mode 100644 npm/ng-packs/packages/components/dynamic-form/src/dynamic-form-field/dynamic-form-field-host.component.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 0000000000..6a3209a98e --- /dev/null +++ b/npm/ng-packs/packages/components/dynamic-form/src/dynamic-form-field/dynamic-form-field-host.component.ts @@ -0,0 +1,134 @@ +import { + Component, Input, ViewChild, ViewContainerRef, OnChanges, SimpleChanges, + ChangeDetectionStrategy, forwardRef, Type, Injector, effect, DestroyRef, inject +} from '@angular/core'; +import { + ControlValueAccessor, NG_VALUE_ACCESSOR, FormControl, ReactiveFormsModule +} from '@angular/forms'; +import { CommonModule } from '@angular/common'; + +type MaybeCVA = Partial & { setDisabledState?(d: boolean): void }; +type WithFormControlInput = { formControl?: FormControl }; + +@Component({ + selector: 'abp-dynamic-form-field-host', + standalone: true, + imports: [CommonModule, ReactiveFormsModule], + template: ``, + changeDetection: ChangeDetectionStrategy.OnPush, + providers: [{ + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => DynamicFieldHostComponent), + multi: true + }] +}) +export class DynamicFieldHostComponent implements ControlValueAccessor, OnChanges { + @Input({ required: true }) component!: Type; + @Input() inputs: Record = {}; // field, visible, vs. + + @ViewChild('vc', { read: ViewContainerRef, static: true }) vc!: ViewContainerRef; + + private compRef?: any; + private onChange: (v: any) => void = () => {}; + private onTouched: () => void = () => {}; + private lastValue: any; + private disabled = false; + + // Eğer child CVA değilse kullanmak üzere bir iç kontrol (opsiyonel) + private innerControl = new FormControl(null); + + ngOnChanges(changes: SimpleChanges): void { + if (changes['component']) { + this.createChild(); + } else if (this.compRef && changes['inputs']) { + this.applyInputs(); + } + } + + private createChild() { + this.vc.clear(); + if (!this.component) return; + + this.compRef = this.vc.createComponent(this.component); + + // inputları ata + this.applyInputs(); + + // ÇOCUK TIPINI KONTROL ET: CVA mı? + const instance: any = this.compRef.instance as MaybeCVA & WithFormControlInput; + + 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); + } + // İlk değeri ilet + if (this.lastValue !== undefined) { + instance.writeValue?.(this.lastValue); + } + } else { + // Child CVA değilse, formControl input’u varsa köprüle + if ('formControl' in instance) { + instance.formControl = this.innerControl; + // son değeri uygula + if (this.lastValue !== undefined) { + this.innerControl.setValue(this.lastValue, { emitEvent: false }); + } + this.innerControl.valueChanges.subscribe(v => this.onChange(v)); + this.innerControl.disabled ? null : (this.disabled && this.innerControl.disable({ emitEvent: false })); + } else { + // Son çare: valueChange EventEmitter’ı varsa bağla (konvansiyonel) + if ('valueChange' in instance && instance['valueChange']?.subscribe) { + instance['valueChange'].subscribe((v: any) => this.onChange(v)); + } + } + } + } + + private applyInputs() { + if (!this.compRef) return; + const inst = this.compRef.instance; + for (const [k, v] of Object.entries(this.inputs ?? {})) { + inst[k] = v; + } + // change detection tetiklenebilir: + this.compRef.changeDetectorRef?.markForCheck?.(); + } + + private isCVA(obj: any): obj is MaybeCVA { + return obj && typeof obj.writeValue === 'function' && typeof obj.registerOnChange === 'function'; + } + + /* --------- CVA (wrapper) --------- */ + + writeValue(obj: any): void { + this.lastValue = obj; + if (!this.compRef) return; + + const inst: any = this.compRef.instance as MaybeCVA & WithFormControlInput; + + if (this.isCVA(inst)) { + inst.writeValue?.(obj); + } else if ('formControl' in inst && inst.formControl instanceof FormControl) { + inst.formControl.setValue(obj, { emitEvent: false }); + } + } + + registerOnChange(fn: any): void { this.onChange = fn; } + registerOnTouched(fn: any): void { this.onTouched = fn; } + + setDisabledState(isDisabled: boolean): void { + this.disabled = isDisabled; + if (!this.compRef) return; + + const inst = this.compRef.instance as MaybeCVA & WithFormControlInput; + + 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/index.ts b/npm/ng-packs/packages/components/dynamic-form/src/dynamic-form-field/index.ts index ce487ae2a5..826f7c70d2 100644 --- 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 @@ -1 +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 index 303365d960..006029afe8 100644 --- 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 @@ -3,11 +3,19 @@
@for (field of sortedFields; track field.key) {
+ @if (field.component) { + + + } @else { + }
}
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 index 294043fc47..9761f492c7 100644 --- 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 @@ -8,13 +8,18 @@ import { DestroyRef, ChangeDetectorRef, effect, + contentChild, + ContentChildren, + QueryList, + AfterContentInit, } 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 { FormFieldConfig } from './dynamic-form.models'; -import { DynamicFormFieldComponent } from './dynamic-form-field'; +import { DynamicFormFieldComponent, DynamicInputDirective } from './dynamic-form-field'; +import { DynamicFieldHostComponent } from './dynamic-form-field/dynamic-form-field-host.component'; @Component({ selector: 'abp-dynamic-form', @@ -23,9 +28,15 @@ import { DynamicFormFieldComponent } from './dynamic-form-field'; host: { class: 'abp-dynamic-form' }, changeDetection: ChangeDetectionStrategy.OnPush, exportAs: 'abpDynamicForm', - imports: [CommonModule, DynamicFormFieldComponent, ReactiveFormsModule], + imports: [ + CommonModule, + DynamicFormFieldComponent, + ReactiveFormsModule, + DynamicInputDirective, + DynamicFieldHostComponent, + ], }) -export class DynamicFormComponent implements OnInit { +export class DynamicFormComponent implements OnInit, AfterContentInit { fields = input([]); values = input>(); submitButtonText = input('Submit'); @@ -36,6 +47,7 @@ export class DynamicFormComponent implements OnInit { private dynamicFormService = inject(DynamicFormService); readonly destroyRef = inject(DestroyRef); readonly changeDetectorRef = inject(ChangeDetectorRef); + @ContentChildren(DynamicInputDirective) dynamicInputs: QueryList; dynamicForm!: FormGroup; fieldVisibility: { [key: string]: boolean } = {}; @@ -48,6 +60,10 @@ export class DynamicFormComponent implements OnInit { }); } + ngAfterContentInit() { + console.log(this.dynamicInputs.toArray()); + } + get sortedFields(): FormFieldConfig[] { return this.fields().sort((a, b) => (a.order || 0) - (b.order || 0)); } @@ -77,7 +93,7 @@ export class DynamicFormComponent implements OnInit { const initialValues: { [key: string]: any } = this.dynamicFormService.getInitialValues( this.fields(), ); - this.dynamicForm.reset({...initialValues}); + this.dynamicForm.reset({ ...initialValues }); this.dynamicForm.markAsUntouched(); this.dynamicForm.markAsPristine(); this.changeDetectorRef.markForCheck(); 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 index 8323a84ade..7a32868957 100644 --- 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 @@ -1,3 +1,4 @@ + export interface FormFieldConfig { key: string; value?: any; @@ -11,6 +12,7 @@ export interface FormFieldConfig { conditionalLogic?: ConditionalRule[]; order?: number; gridSize?: number; + component?: any; } export interface ValidatorConfig {