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({ |
@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.component'; |
||||
|
export * from './dynamic-form-field'; |
||||
|
|||||
Loading…
Reference in new issue