Home > database >  Angular: Input radios not maintaining selection in custom component implemented with ControlValueAcc
Angular: Input radios not maintaining selection in custom component implemented with ControlValueAcc

Time:07-19

I have been trying to create a custom radio component using ControlValueAccessor in Angular.

In the main component I have a form and it has two form controls which is tied to the custom radio component I created. In the initial load, both the custom radio components are selected, but as soon as I change value, only one of the custom radio maintains the selected state.

initial load

After changing the value

Stackblitz minimal repro

There seems to be an issue which is causing this unusual behavior and I am unable to figure it out. I tried with both ngModel (with standalone option) & FormControl within the custom component implementing the ControlValueAccessor, but to no avail.

radio.html

<div>
  <ng-container *ngFor="let o of options; index as i; trackBy: trackByCode">
    <input
      type="radio"
      id="{{ inputName   '_'   o.code }}"
      [value]="o.code"
      [attr.name]="inputName"
      [formControl]="control"
      (blur)="onBlur($event)"
      (change)="onChange($event)"
    />
    <label for="{{ inputName   '_'   o.code }}"> {{ o.name }} </label>
  </ng-container>
</div>

radio.component.ts

import { Component, forwardRef, Input } from '@angular/core';

import {
  ControlValueAccessor,
  FormControl,
  NG_VALUE_ACCESSOR,
} from '@angular/forms';

const MY_RADIO_VALUE_ACCESSOR: any = {
  provide: NG_VALUE_ACCESSOR,
  useExisting: forwardRef(() => RadioComponent),
  multi: true,
};

@Component({
  selector: 'radio',
  templateUrl: './radio.html',
  providers: [MY_RADIO_VALUE_ACCESSOR],
})
export class RadioComponent implements ControlValueAccessor {
  @Input() inputName;
  @Input() options = [];

  control = new FormControl();
  private _onChange!: (_: any) => void;
  private _onTouched!: (_: any) => void;

  trackByCode(index: number, option: any) {
    return option.code;
  }

  writeValue(value: string): void {
    this.control.setValue(value);
  }

  registerOnChange(fn: (_: any) => void): void {
    this._onChange = fn;
  }

  registerOnTouched(fn: (_: any) => void): void {
    this._onTouched = fn;
  }

  setDisabledState(isDisabled: boolean): void {
    isDisabled ? this.control.disable() : this.control.enable();
  }

  onChange($event: any) {
    this._onChange(this.control.value);
  }

  onBlur($event: any) {
    this._onTouched($event);
  }
}

app.component.ts

fruitOptions = [
    { name: 'Tomato', code: 'tomato' },
    { name: 'Apple', code: 'apple' },
    { name: 'Banana', code: 'banana' },
  ];
  vegOptions = [
    { name: 'Cabbage', code: 'cabbage' },
    { name: 'Potato', code: 'potato' },
    { name: 'Beans', code: 'beans' },
  ];

  form = this.fb.group({
    fruit: ['tomato', Validators.required],
    veg: ['cabbage', Validators.required],
  });

app.component.html

<div [formGroup]="form">
  <radio inputName="fruit" [options]="fruitOptions" formControlName="fruit"></radio>
  <radio inputName="veg" [options]="vegOptions" formControlName="veg"></radio>
</div>
<br />
<div>{{ form.value | json }}</div>

CodePudding user response:

In radio.html don't use [attr.name]="inputName" but use [name]="inputName" instead.

Essentially, right now is like you are not setting the name property, so from an HTML point of view all radio belongs to the same group.

This seems to be a known issue: angular github issue #24871

CodePudding user response:

TLDR: Angular needed both [name] and [attr.name]

Initially I had [name], but in the markup generated by angular, there was no name attribute. It was changing to ng-reflect-name="veg" and since there was no name attrib, html was not considering it as a group.

<input type="radio" ng-reflect-name="veg" ng-reflect-value="cabbage" ng-reflect-form="[object Object]" id="veg_cabbage" >

Later, I found we need to [attr.name] to tell Angular to differentiate this from native attribute. This time the generated markup had name and looked good, but it still didn't work.

<input type="radio" ng-reflect-value="cabbage" ng-reflect-form="[object Object]" id="veg_cabbage" name="veg" >

I tried few other methods and made it work by getting the list of radio inputs using ViewChildren and setting the checked property through nativeElement.

But the actual solution turned to be simpler, setting both [name] and [attr.name] on the input, one for angular and the other for the native html, finally did it.

  • Related