I am using angular 18. I am trying to build a component that can render a form, based on a usertype and add any data I set, if any. But i am having some serious issues with what appears to be timing issues. Basically I am trying to make a dynamic form, where I can show fields, based on the selected user type. Then it should look in a configuration, where all the settings are, and if any fields should be dependent on another and its value before being shown. But When I try to forexample make a toggle switch that has a username and password that is dependent on it, it just breaks! FYI be warned, wall of code inc. This is my config, export const USER_TYPE_FIELDS_EDIT: UserTypeFields[] = [ { type: 'dealer', fields: { AXDealerId: { type: 'text', defaultValue: '', validators: [Validators.required], allowedForProviders: [123, 345], isDisabled: false, label: 'Internt Forhandler nummer' }, showServiceCalculator: { type: 'toggle', defaultValue: false, validators: [Validators.required], isDisabled: false, label: 'Vis service' }, serviceCalculatorUsername: { type: 'text', defaultValue: '', validators: [], isDisabled: false, label: 'Brugernavn til serviceberegner', dependsOn: { field: 'showServiceCalculator', value: true } }, serviceCalculatorPassword: { type: 'text', defaultValue: '', validators: [], isDisabled: false, label: 'Adgangskode til serviceberegner', dependsOn: { field: 'showServiceCalculator', value: true } }, showLeasingGuide: { type: 'toggle', defaultValue: false, validators: [Validators.required], isDisabled: false, label: 'Vis leasingguide' }, connectedProducts: { type: 'select', defaultValue: [], validators: [Validators.required], isDisabled: false, label: 'Tilknyt produkter', placeholder: 'Vælg et eller flere produkter', isMultiple: true, } }, availableForTypes: ['provideradmin', 'provideruser'], }, { type: 'validator', fields: {}, availableForTypes: ['provideradmin', 'provideruser'], }, ]; With this it generates among others, the toggle ShowServiceCalculator Then there are the two fields username / password. Those are dependent on the toggle above. And if the toggle is true, it should show them, if not then remove them. My issue here is that when ever i try to hide or show them, it just breaks, and can't find the formcontrols (ERROR Error: There is no FormControl instance attached to form control element with name: 'showServiceCalculator') This is my dynamic form component to build it: import { ChangeDetectorRef, Component, EventEmitter, Input, OnChanges, OnInit, Output, SimpleChanges } from '@angular/core'; import {FormControl, ReactiveFormsModule, Validators} from '@angular/forms'; import {SelectOption} from '../../../models/select-option.model'; // adjust import import {UserTypeFields} from "../../../models/user-type-fields.model"; import {DealerService} from "../../../services/dealer.service"; import {ProviderService} from "../../../services/provider.service"; import {ProductService} from "../../../services/product.service"; import {ToggleSwitchComponent} from "../../form-elements/toggle-switch/toggle-switch.component"; import {TextInputComponent} from "../../form-elements/text-input/text-input.component"; import {SelectSearchComponent} from "../../form-elements/select-search/select-search.component"; import {LabelComponent} from "../../form-elements/label/label.component"; import {EmailInputComponent} from "../../form-elements/email-input/email-input.component"; import {ExtendedFormGroup} from "../../../../core/extensions/extended-form-group.utility"; import {UserService} from "../../../../core/services/user.service"; import {Subscription} from "rxjs"; @Component({ selector: 'app-user-dynamic-form', standalone: true, templateUrl: './user-dynamic-form.component.html', imports: [ ToggleSwitchComponent, TextInputComponent, SelectSearchComponent, LabelComponent, EmailInputComponent, ReactiveFormsModule, ] }) export class UserDynamicFormComponent implements OnInit, OnChanges { @Input() mode: 'create' | 'edit' = 'create'; @Input() config!: UserTypeFields[]; @Input() availableRoles: SelectOption[] = []; @Input() user: any = null; // for edit mode @Input() currentUserType: string = ''; @Output() formSubmit = new EventEmitter<any>(); dynamicFieldsConfig: any[] = []; role: string = ''; public form: ExtendedFormGroup = new ExtendedFormGroup({}); private dependencyListeners: Set<string> = new Set(); private dependencySubscriptions: { [key: string]: Subscription } = {}; constructor( private dealerService: DealerService, private providerService: ProviderService, private productService: ProductService, private userService: UserService, private cdRef: ChangeDetectorRef ) { } ngOnInit() { if (!this.config || this.config.length === 0) { throw new Error('User fields configuration is required!'); } if (this.mode === 'edit' && this.user) { this.role = this.user.role.toLowerCase().replace(/\s+/g, ''); // Give the role control, the label value of availableRoles as a translated value. let translatedRole = this.availableRoles.find(role => role.value === this.role)?.label || this.role; this.form.addControl('role', new FormControl({ value: translatedRole, disabled: true }, Validators.required)); this.form.addControl('email', new FormControl({ value: this.user?.email || '', disabled: true }, [Validators.required, Validators.email])); // this.generateFormFields(this.role, this.user); } else { this.form.addControl('role', new FormControl('', Validators.required)); this.form.get('role')?.valueChanges.subscribe(role => { this.role = role; this.generateFormFields(role); }); this.form.addControl('email', new FormControl({ value: this.user?.email || '', disabled: false }, [Validators.required, Validators.email])); } } ngOnChanges(changes: SimpleChanges) { if (changes['user'] && this.mode === 'edit' && this.user) { this.role = this.user.role.toLowerCase().replace(/\s+/g, ''); this.generateFormFields(this.role, this.user); this.form.get('email')?.setValue(this.user.email); } } getCurrentProviderId(): number | undefined { return this.userService?.getProvider()?.id; } generateFormFields(role: string, initialData: any = {}) { // 1. Unsubscribe from previous dependency subscriptions Object.values(this.dependencySubscriptions).forEach(sub => sub.unsubscribe()); this.dependencySubscriptions = {}; const previousValues = { ...this.form.getRawValue() }; // 2. Remove all controls except 'role' and 'email' (batch) const keep = new Set(['role', 'email']); Object.keys(this.form.controls).forEach(key => { if (!keep.has(key)) { this.form.removeControl(key); } }); const currentUserType = (this.currentUserType || '').toLowerCase().replace(/\s+/g, ''); const currentProviderId = this.getCurrentProviderId(); // 3. Get config for this role const userTypeConfig = this.config.find(type => type.type === role); if (!userTypeConfig) { this.dynamicFieldsConfig = []; this.cdRef.detectChanges(); return; } const fieldEntries = Object.entries(userTypeConfig.fields); // 4. Build orderedFields and dependents arrays const orderedFields: any[] = []; const dependents: Array<[string, any]> = []; fieldEntries.forEach(([fieldName, fieldConfig]) => { if ( this.isAllowedForProvider(fieldConfig, currentProviderId) && this.isAvailableForType(fieldConfig, currentUserType) ) { if (!fieldConfig.dependsOn) { const freshConfig = { ...fieldConfig, name: fieldName, options: fieldConfig.options ? [...fieldConfig.options] : [] }; orderedFields.push(freshConfig); } else { dependents.push([fieldName, fieldConfig]); } } }); // 5. Add all non-dependent controls (batch) orderedFields.forEach(fieldConfig => { const fieldName = fieldConfig.name; if (!this.form.contains(fieldName)) { this.form.addControl( fieldName, new FormControl( { value: initialData[fieldName] !== undefined ? initialData[fieldName] : (previousValues[fieldName] !== undefined ? previousValues[fieldName] : (fieldConfig.isMultiple ? [] : fieldConfig.defaultValue)), disabled: fieldConfig.isDisabled }, fieldConfig.validators ) ); } }); // 6. Add dependents if their dependencies are satisfied (batch) dependents.forEach(([fieldName, fieldConfig]) => { const depField = fieldConfig.dependsOn.field; const depValue = fieldConfig.dependsOn.value; const currentDepValue = this.form.get(depField)?.value; if (currentDepValue === depValue) { const freshConfig = { ...fieldConfig, name: fieldName, options: fieldConfig.options ? [...fieldConfig.options] : [] }; // Insert after controller const controllerIndex = orderedFields.findIndex(f => f.name === depField); if (controllerIndex !== -1) { orderedFields.splice(controllerIndex + 1, 0, freshConfig); } else { orderedFields.push(freshConfig); } if (!this.form.contains(fieldName)) { this.form.addControl( fieldName, new FormControl( { value: initialData[fieldName] !== undefined ? initialData[fieldName] : (previousValues[fieldName] !== undefined ? previousValues[fieldName] : (fieldConfig.isMultiple ? [] : fieldConfig.defaultValue)), disabled: fieldConfig.isDisabled }, fieldConfig.validators ) ); } } else { if (this.form.contains(fieldName)) { this.form.removeControl(fieldName); } } }); // 7. Update disabled state for all controls (batch) orderedFields.forEach(fieldConfig => { const fieldName = fieldConfig.name; const control = this.form.get(fieldName); if (control) { if (fieldConfig.isDisabled !== control.disabled) { if (fieldConfig.isDisabled) { control.disable({ emitEvent: false }); } else { control.enable({ emitEvent: false }); } } } }); // 8. Set up listeners for dependency fields (batch) orderedFields.forEach(fieldConfig => { const fieldName = fieldConfig.name; dependents.forEach(([depName, depConfig]) => { if (depConfig.dependsOn.field === fieldName) { const control = this.form.get(fieldName); if (control) { this.dependencySubscriptions[fieldName] = control.valueChanges.subscribe(() => { this.generateFormFields(role, this.form.getRawValue()); }); } } }); }); // 9. Update dynamicFieldsConfig (batch) this.dynamicFieldsConfig = [...orderedFields]; // 10. Only now, call detectChanges (once, at the end) this.cdRef.detectChanges(); // 11. Async select options (do NOT call detectChanges here) orderedFields.forEach(fieldConfig => { const fieldName = fieldConfig.name; if (fieldConfig.type === 'select') { let obs$; switch (fieldName) { case 'connectedDealers': case 'primaryDealer': case 'dealer': obs$ = this.dealerService.getByProvider(); break; case 'connectedProducts': case 'productAccess': obs$ = this.currentUserType === 'administrator' ? this.productService.getAll() : this.productService.getByProvider(); break; case 'connectedProvider': obs$ = this.providerService.getAll(); break; } if (obs$) { obs$.subscribe((response: any) => { let options: SelectOption[]; if (fieldName === 'connectedProvider') { options = response.data.map((provider: any) => ({ label: provider.name, value: provider.id })); } else if (fieldName === 'connectedProducts' || fieldName === 'productAccess') { options = response.data.map((product: any) => ({ label: product.name, value: product.product_id })); } else { options = response.data.map((dealer: any) => ({ label: dealer.name, value: dealer.id })); } // Update the field in orderedFields and dynamicFieldsConfig const idx = orderedFields.findIndex(f => f.name === fieldName); if (idx !== -1) { orderedFields[idx].options = options; this.dynamicFieldsConfig = [...orderedFields]; // No detectChanges here! } // Remap and set the value after options are loaded const selectControl = this.form.get(fieldName); if (selectControl && selectControl.value !== undefined) { if (fieldConfig.isMultiple) { const optionValues = options.map((o: any) => o.value); const mapped = (Array.isArray(selectControl.value) ? selectControl.value : [selectControl.value]) .map((val: any) => { const opt = options.find((o: any) => o.value == val); return opt ? opt.value : val; }) .filter((val: any) => optionValues.includes(val)); selectControl.setValue(mapped, { emitEvent: false }); } else { const opt = options.find((o: any) => o.value == selectControl.value); if (opt) { selectControl.setValue(opt.value, { emitEvent: false }); } } } }); } } }); } isAllowedForProvider(fieldConfig: any, currentProviderId: number | undefined): boolean { if (!fieldConfig.allowedForProviders) return true; if (!currentProviderId) return true; return fieldConfig.allowedForProviders.includes(currentProviderId ?? 0); } isAvailableForType(fieldConfig: any, currentUserType: string): boolean { if (!fieldConfig.availableForTypes || fieldConfig.availableForTypes.length === 0) return true; return fieldConfig.availableForTypes.map((t: string) => t.toLowerCase()).includes(currentUserType); } private isDependencySatisfied(fieldConfig: any): boolean { if (!fieldConfig.dependsOn) return true; const depField = fieldConfig.dependsOn.field; const depValue = fieldConfig.dependsOn.value; const currentDepValue = this.form.get(depField)?.value; return currentDepValue === depValue; } onSubmit() { if (this.form.valid) { this.formSubmit.emit(this.form.getRawValue()); } else { this.form.markAllAsTouched(); } } } And its html: <form [formGroup]="form"> @if (form.get('role')) { <div class="my-4"> <app-label forId="role">Brugertype</app-label> @if (mode === 'edit') { <app-text-input formControlName="role" id="role" [placeholder]="'Brugertype'" [messages]="{ required: 'Brugertype er påkrævet.' }" ></app-text-input> } @else { <app-select-search class="flex-1" formControlName="role" [options]="availableRoles" placeholder="Vælg brugertype" ></app-select-search> } </div> } @if (form.get('email')) { <div class="my-4"> <app-label forId="email">Email</app-label> <app-email-input formControlName="email" id="email" [disableAutoComplete]="true" [messages]="{ required: 'Email er påkrævet.', strictEmail: 'Email er ikke gyldig.' }" placeholder="example@email.dk" ></app-email-input> </div> } @for (field of dynamicFieldsConfig; track field.name) { @if (form.get(field.name)) { <div class="my-4"> <app-label [forId]="field.name">{{ field.label }}</app-label> @switch (field.type) { @case ('select') { <app-select-search class="flex-1" [formControlName]="field.name" [options]="field.options" [placeholder]="field.placeholder" [multiple]="field.isMultiple" [messages]="{ required: field.required ? 'Dette felt er påkrævet.' : '' }" ></app-select-search> } @case ('text') { <app-text-input [formControlName]="field.name" [placeholder]="field.placeholder ?? ''" [messages]="{ required: field.required ? 'Dette felt er påkrævet.' : '' }" ></app-text-input> } @case ('toggle') { <app-toggle-switch [id]="field.name" [formControlName]="field.name" [messages]="{ required: field.required ? 'Dette felt er påkrævet.' : '' }"> </app-toggle-switch> } } </div> } } </form> I have tried using chatGPT 4.1 to help debug it for almost 2 days now, and have not had any success with it. I am hoping someone can help me make this work! If there is any code or information missing, please let me know, and I will add it. Continue reading...