|
| 1 | +# Building Dynamic Forms in Angular for Enterprise Applications |
| 2 | + |
| 3 | +## Introduction |
| 4 | + |
| 5 | +Dynamic forms are useful for enterprise applications where form structures need to be flexible, configurable, and generated at runtime based on business requirements. This approach allows developers to create forms from configuration objects rather than hardcoding them, enabling greater flexibility and maintainability. |
| 6 | + |
| 7 | +## Benefits |
| 8 | + |
| 9 | +1. **Flexibility**: Forms can be easily modified without changing the code. |
| 10 | +2. **Reusability**: Form components can be shared across components. |
| 11 | +3. **Maintainability**: Changes to form structures can be managed through configuration files or databases. |
| 12 | +4. **Scalability**: New form fields and types can be added without significant code changes. |
| 13 | +4. **User Experience**: Dynamic forms can adapt to user roles and permissions, providing a tailored experience. |
| 14 | + |
| 15 | +## Architecture |
| 16 | + |
| 17 | +### 1. Form Configuration Model |
| 18 | + |
| 19 | +We define a model to represent the form configuration. This model includes field types, labels, validation rules, and other metadata. |
| 20 | + |
| 21 | +```typescript |
| 22 | +export interface FormFieldConfig { |
| 23 | + key: string; |
| 24 | + value?: any; |
| 25 | + type: 'text' | 'email' | 'number' | 'select' | 'checkbox' | 'date' | 'textarea'; |
| 26 | + label: string; |
| 27 | + placeholder?: string; |
| 28 | + required?: boolean; |
| 29 | + disabled?: boolean; |
| 30 | + options?: { key: string; value: any }[]; |
| 31 | + validators?: ValidatorConfig[]; |
| 32 | + conditionalLogic?: ConditionalRule[]; |
| 33 | + order?: number; |
| 34 | + gridSize?: number; // For layout purposes, e.g., Bootstrap grid size (1-12) |
| 35 | +} |
| 36 | + |
| 37 | +export interface ValidatorConfig { |
| 38 | + type: 'required' | 'email' | 'minLength' | 'maxLength' | 'pattern' | 'custom'; |
| 39 | + value?: any; |
| 40 | + message: string; |
| 41 | +} |
| 42 | + |
| 43 | +// Conditional logic to show/hide or enable/disable fields based on other field values |
| 44 | +export interface ConditionalRule { |
| 45 | + dependsOn: string; |
| 46 | + condition: 'equals' | 'notEquals' | 'contains' | 'greaterThan' | 'lessThan'; |
| 47 | + value: any; |
| 48 | + action: 'show' | 'hide' | 'enable' | 'disable'; |
| 49 | +} |
| 50 | + |
| 51 | +``` |
| 52 | +### 2. Dynamic Form Service |
| 53 | + |
| 54 | +A service to handle form creation and validation processes. |
| 55 | + |
| 56 | +```typescript |
| 57 | +@Injectable({ |
| 58 | + providedIn: 'root' |
| 59 | +}) |
| 60 | +export class DynamicFormService { |
| 61 | + |
| 62 | + createFormGroup(fields: FormFieldConfig[]): FormGroup { |
| 63 | + const group: any = {}; |
| 64 | + |
| 65 | + fields.forEach(field => { |
| 66 | + const validators = this.buildValidators(field.validators || []); |
| 67 | + const initialValue = this.getInitialValue(field); |
| 68 | + |
| 69 | + group[field.key] = new FormControl({ |
| 70 | + value: initialValue, |
| 71 | + disabled: field.disabled || false |
| 72 | + }, validators); |
| 73 | + }); |
| 74 | + |
| 75 | + return new FormGroup(group); |
| 76 | + } |
| 77 | + |
| 78 | + private buildValidators(validatorConfigs: ValidatorConfig[]): ValidatorFn[] { |
| 79 | + return validatorConfigs.map(config => { |
| 80 | + switch (config.type) { |
| 81 | + case 'required': |
| 82 | + return Validators.required; |
| 83 | + case 'email': |
| 84 | + return Validators.email; |
| 85 | + case 'minLength': |
| 86 | + return Validators.minLength(config.value); |
| 87 | + case 'maxLength': |
| 88 | + return Validators.maxLength(config.value); |
| 89 | + case 'pattern': |
| 90 | + return Validators.pattern(config.value); |
| 91 | + default: |
| 92 | + return Validators.nullValidator; |
| 93 | + } |
| 94 | + }); |
| 95 | + } |
| 96 | + |
| 97 | + private getInitialValue(field: FormFieldConfig): any { |
| 98 | + switch (field.type) { |
| 99 | + case 'checkbox': |
| 100 | + return false; |
| 101 | + case 'number': |
| 102 | + return 0; |
| 103 | + default: |
| 104 | + return ''; |
| 105 | + } |
| 106 | + } |
| 107 | +} |
| 108 | + |
| 109 | +``` |
| 110 | + |
| 111 | +### 3. Dynamic Form Component |
| 112 | + |
| 113 | +```typescript |
| 114 | +@Component({ |
| 115 | + selector: 'app-dynamic-form', |
| 116 | + template: ` |
| 117 | + <form [formGroup]="dynamicForm" (ngSubmit)="onSubmit()" class="dynamic-form"> |
| 118 | + @for (field of sortedFields; track field.key) { |
| 119 | + <div class="row"> |
| 120 | + <div [ngClass]="'col-md-' + (field.gridSize || 12)"> |
| 121 | + <app-dynamic-form-field |
| 122 | + [field]="field" |
| 123 | + [form]="dynamicForm" |
| 124 | + [isVisible]="isFieldVisible(field)" |
| 125 | + (fieldChange)="onFieldChange($event)"> |
| 126 | + </app-dynamic-form-field> |
| 127 | + </div> |
| 128 | + </div> |
| 129 | + } |
| 130 | + <div class="form-actions"> |
| 131 | + <button |
| 132 | + type="button" |
| 133 | + class="btn btn-secondary" |
| 134 | + (click)="onCancel()"> |
| 135 | + Cancel |
| 136 | + </button> |
| 137 | + <button |
| 138 | + type="submit" |
| 139 | + class="btn btn-primary" |
| 140 | + [disabled]="!dynamicForm.valid || isSubmitting"> |
| 141 | + {{ submitButtonText() }} |
| 142 | + </button> |
| 143 | + </div> |
| 144 | + </form> |
| 145 | + `, |
| 146 | + styles: [` |
| 147 | + .dynamic-form { |
| 148 | + display: flex; |
| 149 | + gap: 0.5rem; |
| 150 | + flex-direction: column; |
| 151 | + } |
| 152 | + .form-actions { |
| 153 | + display: flex; |
| 154 | + justify-content: flex-end; |
| 155 | + gap: 0.5rem; |
| 156 | + } |
| 157 | + `], |
| 158 | + imports: [ReactiveFormsModule, CommonModule, DynamicFormFieldComponent], |
| 159 | +}) |
| 160 | +export class DynamicFormComponent implements OnInit { |
| 161 | + fields = input<FormFieldConfig[]>([]); |
| 162 | + submitButtonText = input<string>('Submit'); |
| 163 | + formSubmit = output<any>(); |
| 164 | + formCancel = output<void>(); |
| 165 | + private dynamicFormService = inject(DynamicFormService); |
| 166 | + |
| 167 | + dynamicForm!: FormGroup; |
| 168 | + isSubmitting = false; |
| 169 | + fieldVisibility: { [key: string]: boolean } = {}; |
| 170 | + |
| 171 | + ngOnInit() { |
| 172 | + this.dynamicForm = this.dynamicFormService.createFormGroup(this.fields()); |
| 173 | + this.initializeFieldVisibility(); |
| 174 | + this.setupConditionalLogic(); |
| 175 | + } |
| 176 | + |
| 177 | + get sortedFields(): FormFieldConfig[] { |
| 178 | + return this.fields().sort((a, b) => (a.order || 0) - (b.order || 0)); |
| 179 | + } |
| 180 | + |
| 181 | + onSubmit() { |
| 182 | + if (this.dynamicForm.valid) { |
| 183 | + this.isSubmitting = true; |
| 184 | + this.formSubmit.emit(this.dynamicForm.value); |
| 185 | + } else { |
| 186 | + this.markAllFieldsAsTouched(); |
| 187 | + } |
| 188 | + } |
| 189 | + |
| 190 | + onCancel() { |
| 191 | + this.formCancel.emit(); |
| 192 | + } |
| 193 | + |
| 194 | + onFieldChange(event: { fieldKey: string; value: any }) { |
| 195 | + this.evaluateConditionalLogic(event.fieldKey); |
| 196 | + } |
| 197 | + |
| 198 | + isFieldVisible(field: FormFieldConfig): boolean { |
| 199 | + return this.fieldVisibility[field.key] !== false; |
| 200 | + } |
| 201 | + |
| 202 | + private initializeFieldVisibility() { |
| 203 | + this.fields().forEach(field => { |
| 204 | + this.fieldVisibility[field.key] = !field.conditionalLogic?.length; |
| 205 | + }); |
| 206 | + } |
| 207 | + |
| 208 | + private setupConditionalLogic() { |
| 209 | + this.fields().forEach(field => { |
| 210 | + if (field.conditionalLogic) { |
| 211 | + field.conditionalLogic.forEach(rule => { |
| 212 | + const dependentControl = this.dynamicForm.get(rule.dependsOn); |
| 213 | + if (dependentControl) { |
| 214 | + dependentControl.valueChanges.subscribe(() => { |
| 215 | + this.evaluateConditionalLogic(field.key); |
| 216 | + }); |
| 217 | + } |
| 218 | + }); |
| 219 | + } |
| 220 | + }); |
| 221 | + } |
| 222 | + |
| 223 | + private evaluateConditionalLogic(fieldKey: string) { |
| 224 | + const field = this.fields().find(f => f.key === fieldKey); |
| 225 | + if (!field?.conditionalLogic) return; |
| 226 | + |
| 227 | + field.conditionalLogic.forEach(rule => { |
| 228 | + const dependentValue = this.dynamicForm.get(rule.dependsOn)?.value; |
| 229 | + const conditionMet = this.evaluateCondition(dependentValue, rule.condition, rule.value); |
| 230 | + |
| 231 | + this.applyConditionalAction(fieldKey, rule.action, conditionMet); |
| 232 | + }); |
| 233 | + } |
| 234 | + |
| 235 | + private evaluateCondition(fieldValue: any, condition: string, ruleValue: any): boolean { |
| 236 | + switch (condition) { |
| 237 | + case 'equals': |
| 238 | + return fieldValue === ruleValue; |
| 239 | + case 'notEquals': |
| 240 | + return fieldValue !== ruleValue; |
| 241 | + case 'contains': |
| 242 | + return fieldValue && fieldValue.includes && fieldValue.includes(ruleValue); |
| 243 | + case 'greaterThan': |
| 244 | + return Number(fieldValue) > Number(ruleValue); |
| 245 | + case 'lessThan': |
| 246 | + return Number(fieldValue) < Number(ruleValue); |
| 247 | + default: |
| 248 | + return false; |
| 249 | + } |
| 250 | + } |
| 251 | + |
| 252 | + private applyConditionalAction(fieldKey: string, action: string, shouldApply: boolean) { |
| 253 | + const control = this.dynamicForm.get(fieldKey); |
| 254 | + |
| 255 | + switch (action) { |
| 256 | + case 'show': |
| 257 | + this.fieldVisibility[fieldKey] = shouldApply; |
| 258 | + break; |
| 259 | + case 'hide': |
| 260 | + this.fieldVisibility[fieldKey] = !shouldApply; |
| 261 | + break; |
| 262 | + case 'enable': |
| 263 | + if (control) { |
| 264 | + shouldApply ? control.enable() : control.disable(); |
| 265 | + } |
| 266 | + break; |
| 267 | + case 'disable': |
| 268 | + if (control) { |
| 269 | + shouldApply ? control.disable() : control.enable(); |
| 270 | + } |
| 271 | + break; |
| 272 | + } |
| 273 | + } |
| 274 | + |
| 275 | + private markAllFieldsAsTouched() { |
| 276 | + Object.keys(this.dynamicForm.controls).forEach(key => { |
| 277 | + this.dynamicForm.get(key)?.markAsTouched(); |
| 278 | + }); |
| 279 | + } |
| 280 | +} |
| 281 | +``` |
0 commit comments