'Why does *ngIf seem to break my reactive form? How do I handle conditional Inputs and validation in Angular?

What I want to accomplish is for the form to change as the user changes the form type from the radio.

Standard basically has 2 selects (one classic and a fancier one, made with ng-select) and custom has a simple classic text input.

I am trying to change the form's functionality dynamically as the form type changes using the radio.

Besides trying to use formBuilder.group, I also tried using .setValidators on the individual inputs, but the result is the same: when I change the radio and the custom_channel_name input is shown i get this console error "Error: Cannot find control with name: 'custom_channel_name'"

What am I doing wrong and how do I properly handle reactive forms in this fashion?

What I have so far looks like this: https://i.imgur.com/n24mKs7.png , https://i.imgur.com/FfCgXFX.png

[ component.html ]

<div>
  <form [formGroup]="organizationChannelForm" (ngSubmit)="submitOrganizationChannelsForm()">
    <div *ngIf="isChannelTypeStandard" class="grid gap-4 grid-cols-2">
      <!-- <form-picker label="Countries" [values]="selectCountriesSources" labelField="name" valueField="warehouse_id"
        formControlName="warehouse_id"></form-picker> -->
      <div>
        <label for="channel_id" class="text-gray-700 dark:text-gray-400 block w-full pb-1 text-sm">Channel</label>
        <select [(ngModel)]="selectedChannel" id="channel_id" formControlName="channel_id"
          class="block w-full dark:bg-gray-700 dark:text-gray-300 form-input">
          <option *ngFor="let channel of selectChannelsSources" [value]="channel">{{ channel }}</option>
        </select>
      </div>
      <div>
        <label for="countries_ids" class="text-gray-700 dark:text-gray-400 block w-full pb-1 text-sm">Countries</label>
        <ng-select [items]="selectCountriesSources" [(ngModel)]="selectedCountries"
          [ngModelOptions]="{ standalone: true }" id="countries_ids" [multiple]="true" bindLabel="name"
          name="countries_ids"></ng-select>
      </div>
    </div>
    <div *ngIf="!isChannelTypeStandard">
      <label for="custom_channel_name" class="text-gray-700 dark:text-gray-400 block w-full pb-1 text-sm">Custom Channel</label>
        <input class="form-input block w-full dark:bg-gray-700 dark:text-gray-300"
        type="text" id="custom_channel_name" formControlName="custom_channel_name">
    </div>
    <div class="flex mt-5">
      <div class="flex ml-auto items-center">
        <span class="dark:text-gray-400">Channel Type</span>
        <div class="ml-6 flex">
          <label class="flex items-center cursor-pointer">
            <input [(ngModel)]="isChannelTypeStandard" [ngModelOptions]="{ standalone: true }" (change)="updateOrganizationChannelForm($event)"
              type="radio" class="form-radio text-purple-600 h-4 w-4" name="channelType" [value]="true">
            <span class="dark:text-gray-300 font-medium ml-2">Standard</span>
          </label>
          <label class="flex items-center cursor-pointer ml-4">
            <input [(ngModel)]="isChannelTypeStandard" [ngModelOptions]="{ standalone: true }" (change)="updateOrganizationChannelForm($event)"
              type="radio" class="form-radio text-purple-600 h-4 w-4" name="channelType" [value]="false">
            <span class="dark:text-gray-300 font-medium ml-2">Custom</span>
          </label>
        </div>
      </div>
      <div class="ml-8 min-w-0 text-white flex flex-col items-end rounded-lg shadow-xs">
        <button type="submit" aria-label="add" [disabled]="organizationChannelForm.errors || organizationChannelForm.pristine"
          class="flex items-end justify-between px-4 py-2 text-sm font-medium leading-5 text-white transition-colors duration-150 bg-purple-600 border border-transparent rounded-lg active:bg-purple-600 hover:bg-purple-700 focus:outline-none focus:shadow-outline-purple disabled:bg-grey-600">
          Assign Channel
          <fa-icon class="ml-2" [icon]="icons.plus"></fa-icon>
        </button>
      </div>
    </div>
  </form>
</div>

[ component.ts ]

export class OrganizationChannelsComponent implements OnInit {

  selectChannelsSources: Array<string> = ["eMag Marketplace", "Vtex", "Shopify", "Magento1", "Magento2", "WooCommerce", "Prestashop", "TeamShare", "Gomag", "Opencart", "MerchantPro", "Cscart", "Allegro", "Idosell", "ChannelAdvisor", "Shoprenter", "Transfer", "Defecte/Defects", "Manual Order"];
  selectCountriesSources: Array<Country> = [];
  icons = {
    close: faTimes,
    plus: faPlus
  }

  organizationChannelForm!: FormGroup;

  selectedCountries: Array<Country> = [];
  selectedChannel: Channel | undefined;
  isChannelTypeStandard: boolean = true;

  @Input() organizationId!: ID;

  organizationChannels$: Observable<OrganizationChannel[]> = new BehaviorSubject<OrganizationChannel[]>([]);
  channels$: Observable<Channel[]> = new BehaviorSubject<Channel[]>([]);

  constructor(
    private organizationChannelsService: OrganizationsChannelsService,
    private organizationChannelsQuery: OrganizationChannelsQuery,
    private countriesService: CountriesService,
    private toasterService: ToasterService,
    private formBuilder: FormBuilder,
  ) { }

  ngOnInit(): void {
    this.organizationChannelForm = this.formBuilder.group({
      channel_id: ['', Validators.required],
    });

    this.organizationChannelsService.getOrganizationChannels(this.organizationId).subscribe();
    this.organizationChannels$ = this.organizationChannelsQuery.selectOrganizationChannels(this.organizationId as number);
    this.countriesService.get().subscribe(countries => this.selectCountriesSources = countries);
  }

  updateOrganizationChannelForm() {
    if (this.isChannelTypeStandard) {
      this.organizationChannelForm = this.formBuilder.group({
        channel_id: ['', Validators.required],
      });
    }
    else {
      this.organizationChannelForm = this.formBuilder.group({
        custom_channel_name: [Validators.required, Validators.minLength(8)]
      });
    }    
  }
}


Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source