diff --git a/npm/ng-packs/apps/dev-app/src/app/home/home.component.html b/npm/ng-packs/apps/dev-app/src/app/home/home.component.html index 1a0d46c2daf..466c08d8247 100644 --- a/npm/ng-packs/apps/dev-app/src/app/home/home.component.html +++ b/npm/ng-packs/apps/dev-app/src/app/home/home.component.html @@ -1,4 +1,6 @@
+ +
+

{{ '::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') { + +
+ + @if (isInvalid) { + + } +
+ } @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 @@ +
+
+
+ @for (field of sortedFields; track field.key) { +
+ @if (field.component) { + + + } @else { + + + } +
+ } +
+ + + + +
+ + +
+ @if (showCancelButton()) { + + } + +
+
+
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"],