-
Notifications
You must be signed in to change notification settings - Fork 3.6k
Dynamic form component #23917
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Draft
erdemcaygor
wants to merge
20
commits into
dev
Choose a base branch
from
feat/#23891
base: dev
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Draft
Dynamic form component #23917
Changes from all commits
Commits
Show all changes
20 commits
Select commit
Hold shift + click to select a range
1adc27f
dynamic form component init
erdemcaygor fe26f71
refactoring
erdemcaygor e8ced70
refactoring
erdemcaygor 753350d
refactoring
erdemcaygor 5ca3db5
refactoring
erdemcaygor d19134b
refactoring
erdemcaygor fb0dd44
refactoring
erdemcaygor bc3f38f
form field updated
erdemcaygor 391c51e
dynamic form field validation logic updated
erdemcaygor ffa3b85
removed unnecessary form field control interface
erdemcaygor 6715aae
refactoring
erdemcaygor b094d5a
dynamic form field validator func updated
erdemcaygor 32f4047
localization added for error messages
erdemcaygor 6ba1165
refactoring
erdemcaygor 6e4179f
reset form function added
erdemcaygor d66c8d6
refactoring
erdemcaygor 1963745
reset form function added
erdemcaygor a9486e9
dynamic form field host component added to custom form input
erdemcaygor fbd7aa5
refactoring
erdemcaygor 3f2f25e
custom form field support added
erdemcaygor File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
6 changes: 6 additions & 0 deletions
6
npm/ng-packs/packages/components/dynamic-form/ng-package.json
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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" | ||
| } | ||
| } |
137 changes: 137 additions & 0 deletions
137
...kages/components/dynamic-form/src/dynamic-form-field/dynamic-form-field-host.component.ts
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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 }); | ||
| } | ||
| } | ||
| } |
91 changes: 91 additions & 0 deletions
91
...packages/components/dynamic-form/src/dynamic-form-field/dynamic-form-field.component.html
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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> |
4 changes: 4 additions & 0 deletions
4
...packages/components/dynamic-form/src/dynamic-form-field/dynamic-form-field.component.scss
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,4 @@ | ||
| .form-group { | ||
| display: flex; | ||
| flex-direction: column; | ||
| } |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
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.