javascripthtmlangularangular-reactive-formsangular-forms

Dynamic custom component radio buttons do not have unique name when duplicated


I have the following form with a set of radio buttons in each section I can only select one of the buttons at any given time.

It appears that they do not have unique names.

enter image description here

The form is dynamically generated using the following code:

  get anatomicalComplaints(): FormArray {
    return this.childForm.get('dcrr_pc')?.get('pc_anat_loc_compl') as FormArray
  }

  addAnatomicalComplaints() {
    this.anatomicalComplaints.push(this.createNewAnatomicalComplaint())
  }

  removeAnatomicalComplaints(index: number) {
    this.anatomicalComplaints.removeAt(index)
  }

  createNewAnatomicalComplaint() {
    return new FormGroup({
      spinal: new FormControl(''),
      vas: new FormControl(''),
      laterality: new FormControl(''),
      radicular_component: new FormControl(''),
      pc_cas_hist_doc_compl: new FormControl('')
    })
  }

Each input in the form is a custom component that I have built to make development easier. The code follows here:

import { CommonModule } from '@angular/common';
import { Component, forwardRef, Host, Input, OnDestroy, OnInit, Optional, ViewEncapsulation } from '@angular/core';
import { toSignal } from '@angular/core/rxjs-interop';
import { ControlValueAccessor, FormControl, NG_VALUE_ACCESSOR, ReactiveFormsModule, FormsModule, AbstractControl, FormGroupDirective, ControlContainer, FormGroup, FormArray } from '@angular/forms';
import { Subject } from 'rxjs';

@Component({
  selector: 'app-form-input',
  standalone: true,
  imports: [
    CommonModule
  ],
  templateUrl: './form-input.component.html',
  styleUrl: './form-input.component.scss',
  providers: [
    {
      provide: NG_VALUE_ACCESSOR,
      useExisting: forwardRef(() => FormInputComponent),
      multi: true
    },
    
  ],
  viewProviders: [
    {provide: ControlContainer, useExisting: FormGroupDirective}
  ]
})
export class FormInputComponent implements OnInit, ControlValueAccessor, OnDestroy {
  
  @Input() placeholder: string = ''
  @Input() type: string = 'text'
  @Input() label: string = ''
  @Input() required: boolean = false
  @Input() minLength: number = 0
  @Input() labelClassList: string = ''
  @Input() classList: string= ''
  @Input() size: string = 'sm'
  @Input() options: string[] = [];
  @Input() selectOptions: any;
  @Input() controlName : string | null = null
  @Input() indexAt : number = 0
  @Input() formType : string = 'horizontal'
  @Input() inputGroup : boolean = false

  protected onTouched: any = () => {}

  private onChange: any = () => {}
  private destroy$ = new Subject<void>()

  control !: AbstractControl

  value: any;

  constructor(public parentForm: FormGroupDirective) {

  }

  ngOnInit() {

  }

  setRadioId = (index: number) => `${this.controlName}${index}`


  writeValue(value: any): void {
    this.value = value
  }

  registerOnChange(fn: any): void {
      this.onChange = fn
  }

  registerOnTouched(fn: any): void {
    this.onTouched = fn    
  }

  onInputChange(event: any): void {
    this.value = event.target.value
    this.onChange(this.value)
  }

  ngOnDestroy(): void {
    this.destroy$.next()
    this.destroy$.complete()
  }
}
@if(!inputGroup) {
    <div [class]="classList">
        @if(label != '' && formType == 'default') {
            <label>{{label}}</label>
        }
        @if(type == 'text' || type == 'date') {
            <input 
                [type]="type"
                [placeholder]="placeholder"
                [value]="value"
                (input)="onInputChange($event)"
                (blur)="onTouched()"
                class="form-control"
                [class.form-control-sm]="size == 'sm'"
            />
        }
        @if(type == 'radio') {
            @for(option of options; track option; let idx = $index) {
                <div class="form-check form-check-inline">
                    <input class="form-check-input" [type]="type" [name]="controlName" [id]="setRadioId(idx)" [value]="option">
                    <label class="form-check-label" [for]="setRadioId(idx)">
                        {{option}}
                    </label>
                </div>
            }
        }

        @if(type == 'checkbox') {
            @for(option of options; track option; let idx = $index) {
                <div class="form-check">
                    <input [type]="type" class="form-check-input" [name]="controlName">
                    <label class="form-check-label">
                    {{option}}
                    </label>
                </div>
            }
        }

        @if(type == 'select') {
            <select class="form-control" [class.form-control-sm]="size == 'sm'" (input)="onInputChange($event)"
            (blur)="onTouched()">
                <option value="">--Payer--</option>
                <option *ngFor="let o of selectOptions" [value]="o">{{o}}</option>
            </select>
        }

        @if(type == 'textarea') {
            <textarea class="form-control" [class.form-control-sm]="size == 'sm'" rows="5" width="100%"></textarea>
        }
    </div>
}

@if(inputGroup) {
    <div [class]="classList">
        <div class="input-group">
            <span class="input-group-text border-0 bg-transparent">{{label}}</span>
                @if(type == 'text' || type == 'date') {
                    <input 
                        [type]="type"
                        [placeholder]="placeholder"
                        [value]="value"
                        (input)="onInputChange($event)"
                        (blur)="onTouched()"
                        class="form-control"
                        [class.form-control-sm]="size == 'sm'"
                    />
                }
                @if(type == 'select') {
                    <select class="form-control" [class.form-control-sm]="size == 'sm'" (input)="onInputChange($event)"
                    (blur)="onTouched()">
                        <option value="">--Payer--</option>
                        <option *ngFor="let o of selectOptions" [value]="o">{{o}}</option>
                    </select>
                }
        </div>
    </div>
}

I have tried passing in the unique indexes, setting the formControlName inside the custom component, and setting unique ids. None of these solutions have worked.


I have a working stackblitz application here

https://stackblitz.com/edit/stackblitz-starters-pr7ac5dg?file=src%2Ftemplate.html

Steps to Replicate

  1. Load the application
  2. Click the blue plus button any number of times
  3. Try to select any radio buttons

Expected Outcomes

You will only be able to select one radio button in the whole group

Desired Outcome

Select any number of radio buttons in the array


Solution

  • The radio button all have the same name attribute, that is why the selection jumps between rows. The name property decides the radio button groupings and toggling action behaviour.

    Create the unique name based on the row index indexAt:

      <div class="form-check form-check-inline">
        <input
          class="form-check-input"
          [type]="type"
          [name]="controlName + '_' + indexAt"
          [id]="setRadioId(idx)"
          [value]="option"
        />
    

    Stackblitz Demo