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.
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.