mirror of https://github.com/abpframework/abp.git
9 changed files with 438 additions and 15 deletions
@ -1,9 +0,0 @@ |
|||
import { ChangeDetectionStrategy, Component } from '@angular/core'; |
|||
|
|||
@Component({ |
|||
selector: 'abp-dynamic-form-field', |
|||
template: ``, |
|||
changeDetection: ChangeDetectionStrategy.OnPush |
|||
}) |
|||
|
|||
export class DynamicFormFieldComponent {} |
|||
@ -0,0 +1,75 @@ |
|||
|
|||
import {Observable} from 'rxjs'; |
|||
import {AbstractControlDirective, NgControl} from '@angular/forms'; |
|||
import {Directive} from '@angular/core'; |
|||
|
|||
@Directive() |
|||
export abstract class MatFormFieldControl<T> { |
|||
/** The value of the control. */ |
|||
value: T | null; |
|||
|
|||
/** |
|||
* Stream that emits whenever the state of the control changes such that the parent `MatFormField` |
|||
* needs to run change detection. |
|||
*/ |
|||
readonly stateChanges: Observable<void>; |
|||
|
|||
/** The element ID for this control. */ |
|||
readonly id: string; |
|||
|
|||
/** The placeholder for this control. */ |
|||
readonly placeholder: string; |
|||
|
|||
/** Gets the AbstractControlDirective for this control. */ |
|||
readonly ngControl: NgControl | AbstractControlDirective | null; |
|||
|
|||
/** Whether the control is focused. */ |
|||
readonly focused: boolean; |
|||
|
|||
/** Whether the control is empty. */ |
|||
readonly empty: boolean; |
|||
|
|||
/** Whether the control is required. */ |
|||
readonly required: boolean; |
|||
|
|||
/** Whether the control is disabled. */ |
|||
readonly disabled: boolean; |
|||
|
|||
/** Whether the control is in an error state. */ |
|||
readonly errorState: boolean; |
|||
|
|||
/** |
|||
* An optional name for the control type that can be used to distinguish `mat-form-field` elements |
|||
* based on their control type. The form field will add a class, |
|||
* `mat-form-field-type-{{controlType}}` to its root element. |
|||
*/ |
|||
readonly controlType?: string; |
|||
|
|||
/** |
|||
* Whether the input is currently in an autofilled state. If property is not present on the |
|||
* control it is assumed to be false. |
|||
*/ |
|||
readonly autofilled?: boolean; |
|||
|
|||
/** |
|||
* Value of `aria-describedby` that should be merged with the described-by ids |
|||
* which are set by the form-field. |
|||
*/ |
|||
readonly userAriaDescribedBy?: string; |
|||
|
|||
/** |
|||
* Whether to automatically assign the ID of the form field as the `for` attribute |
|||
* on the `<label>` inside the form field. Set this to true to prevent the form |
|||
* field from associating the label with non-native elements. |
|||
*/ |
|||
readonly disableAutomaticLabeling?: boolean; |
|||
|
|||
/** Gets the list of element IDs that currently describe this control. */ |
|||
readonly describedByIds?: string[]; |
|||
|
|||
/** Sets the list of element IDs that currently describe this control. */ |
|||
abstract setDescribedByIds(ids: string[]): void; |
|||
|
|||
/** Handles a click on the control's container. */ |
|||
abstract onContainerClick(event: MouseEvent): void; |
|||
} |
|||
@ -0,0 +1,99 @@ |
|||
@if (isVisible) { |
|||
<div class="field-container"> |
|||
|
|||
@if (field.type === 'text') { |
|||
<!-- Text Input --> |
|||
<div class="form-group"> |
|||
<label [for]="field.key">{{ field.label }}</label> |
|||
<input |
|||
[id]="field.key" |
|||
[formControlName]="field.key" |
|||
[placeholder]="field.placeholder || ''" |
|||
class="form-control" |
|||
[class.is-invalid]="isFieldInvalid()"> |
|||
@if (isFieldInvalid()) { |
|||
<div class="invalid-feedback"> |
|||
{{ getErrorMessage() }} |
|||
</div> |
|||
} |
|||
</div> |
|||
} @else if (field.type === 'select') { |
|||
<!-- Select Dropdown --> |
|||
<div class="form-group"> |
|||
<label [for]="field.key">{{ field.label }}</label> |
|||
<select |
|||
[id]="field.key" |
|||
[formControlName]="field.key" |
|||
class="form-control" |
|||
[class.is-invalid]="isFieldInvalid()"> |
|||
<option value="">Please select...</option> |
|||
@for (option of field.options; track option.key) { |
|||
<option |
|||
[value]="option.key"> |
|||
{{ option.value }} |
|||
</option> |
|||
} |
|||
</select> |
|||
@if (isFieldInvalid()) { |
|||
<div class="invalid-feedback"> |
|||
{{ getErrorMessage() }} |
|||
</div> |
|||
} |
|||
</div> |
|||
} @else if (field.type === 'checkbox') { |
|||
<!-- Checkbox --> |
|||
<div class="form-group form-check"> |
|||
<input |
|||
type="checkbox" |
|||
[id]="field.key" |
|||
[formControlName]="field.key" |
|||
class="form-check-input" |
|||
[class.is-invalid]="isFieldInvalid()"> |
|||
<label class="form-check-label" [for]="field.key"> |
|||
{{ field.label }} |
|||
</label> |
|||
@if (isFieldInvalid()) { |
|||
<div class="invalid-feedback"> |
|||
{{ getErrorMessage() }} |
|||
</div> |
|||
} |
|||
</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]="field.key" |
|||
[placeholder]="field.placeholder || ''" |
|||
class="form-control" |
|||
[class.is-invalid]="isFieldInvalid()"> |
|||
@if (isFieldInvalid()) { |
|||
<div class="invalid-feedback"> |
|||
{{ getErrorMessage() }} |
|||
</div> |
|||
} |
|||
</div> |
|||
} @else if (field.type === 'textarea') { |
|||
<!-- Textarea --> |
|||
<div class="form-group"> |
|||
<label [for]="field.key">{{ field.label }}</label> |
|||
<textarea |
|||
[id]="field.key" |
|||
[formControlName]="field.key" |
|||
[placeholder]="field.placeholder || ''" |
|||
rows="4" |
|||
class="form-control" |
|||
[class.is-invalid]="isFieldInvalid()"> |
|||
</textarea> |
|||
@if (isFieldInvalid()) { |
|||
<div class="invalid-feedback"> |
|||
{{ getErrorMessage() }} |
|||
</div> |
|||
} |
|||
</div> |
|||
} |
|||
</div> |
|||
<!-- Add more field types as needed--> |
|||
} |
|||
@ -0,0 +1,56 @@ |
|||
import { ChangeDetectionStrategy, Component, InjectionToken } from '@angular/core'; |
|||
import { NgTemplateOutlet } from '@angular/common'; |
|||
|
|||
export const ABP_DYNAMIC_FORM_FIELD = new InjectionToken<DynamicFormFieldComponent>('AbpDynamicFormField'); |
|||
|
|||
@Component({ |
|||
selector: 'abp-dynamic-form-field', |
|||
templateUrl: './dynamic-form-field.component.html', |
|||
providers: [{ provide: ABP_DYNAMIC_FORM_FIELD, useExisting: DynamicFormFieldComponent }], |
|||
host: { 'class': 'abp-dynamic-form-field' }, |
|||
exportAs: 'abpDynamicFormField', |
|||
changeDetection: ChangeDetectionStrategy.OnPush, |
|||
imports: [ |
|||
NgTemplateOutlet |
|||
] |
|||
}) |
|||
|
|||
export class DynamicFormFieldComponent { |
|||
field = input<FormFieldConfig>(); |
|||
@Input() isVisible: boolean = true; |
|||
|
|||
ngOnInit() { |
|||
const control = this.form.get(this.field.key); |
|||
if (control) { |
|||
control.valueChanges.subscribe(value => { |
|||
this.fieldChange.emit({ fieldKey: this.field.key, value }); |
|||
}); |
|||
} |
|||
} |
|||
|
|||
isFieldInvalid(): boolean { |
|||
const control = this.form.get(this.field.key); |
|||
return !!(control && control.invalid && (control.dirty || control.touched)); |
|||
} |
|||
|
|||
getErrorMessage(): string { |
|||
const control = this.form.get(this.field.key); |
|||
if (!control || !control.errors) return ''; |
|||
|
|||
const validators = this.field.validators || []; |
|||
|
|||
for (const validator of validators) { |
|||
if (control.errors[validator.type]) { |
|||
return validator.message; |
|||
} |
|||
} |
|||
|
|||
// Fallback error messages
|
|||
if (control.errors['required']) return `${this.field.label} is required`; |
|||
if (control.errors['email']) return 'Please enter a valid email address'; |
|||
if (control.errors['minlength']) return `Minimum length is ${control.errors['minlength'].requiredLength}`; |
|||
if (control.errors['maxlength']) return `Maximum length is ${control.errors['maxlength'].requiredLength}`; |
|||
|
|||
return 'Invalid input'; |
|||
} |
|||
} |
|||
@ -0,0 +1 @@ |
|||
export * from './dynamic-form-field.component'; |
|||
@ -0,0 +1,27 @@ |
|||
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; |
|||
} |
|||
|
|||
export interface ValidatorConfig { |
|||
type: 'required' | 'email' | 'minLength' | 'maxLength' | 'pattern' | 'custom'; |
|||
value?: any; |
|||
message: string; |
|||
} |
|||
|
|||
export interface ConditionalRule { |
|||
dependsOn: string; |
|||
condition: 'equals' | 'notEquals' | 'contains' | 'greaterThan' | 'lessThan'; |
|||
value: any; |
|||
action: 'show' | 'hide' | 'enable' | 'disable'; |
|||
} |
|||
@ -1,8 +1,56 @@ |
|||
import { Injectable } from '@angular/core'; |
|||
import {Injectable} from '@angular/core'; |
|||
import {FormControl, FormGroup, ValidatorFn, Validators} from '@angular/forms'; |
|||
import {FormFieldConfig, ValidatorConfig} from './dynamic-form.models'; |
|||
|
|||
@Injectable({ |
|||
providedIn: 'root', |
|||
providedIn: 'root' |
|||
}) |
|||
export class DynamicFormFieldService { |
|||
// Add any shared logic for dynamic form fields here if needed in the future
|
|||
|
|||
export class DynamicFormService { |
|||
|
|||
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 new FormGroup(group); |
|||
} |
|||
|
|||
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); |
|||
default: |
|||
return Validators.nullValidator; |
|||
} |
|||
}); |
|||
} |
|||
|
|||
private getInitialValue(field: FormFieldConfig): any { |
|||
switch (field.type) { |
|||
case 'checkbox': |
|||
return false; |
|||
case 'number': |
|||
return 0; |
|||
default: |
|||
return ''; |
|||
} |
|||
} |
|||
} |
|||
|
|||
@ -1 +1,2 @@ |
|||
export * from './dynamic-form.component'; |
|||
export * from './dynamic-form-field'; |
|||
|
|||
Loading…
Reference in new issue