Reputation: 85
I am trying to write a component that provides a reusable custom radio input field. I have created a real simple sample in StackBlitz that I hope communicates what I am trying to do. This simple app does not work as expected and I have not yet figured out why. Would love someone else's eyes on this.
The custom radio component's html
<div [formGroup]="frm">
<label [for]="id">
<input [id]="id" type="radio" [value]="value" [formControlName]="cn">{{label}}
</label>
</div>
Custom radio component's typescript
import { Component, Input } from "@angular/core";
import { FormGroup } from "@angular/forms";
@Component({
selector: 'app-radio',
templateUrl: './radio.component.html',
styleUrls: ['./radio.component.css']
})
export class RadioComponent {
@Input() frm: FormGroup;
@Input() label: string;
@Input() cn: string;
@Input() value: string;
@Input() id: string;
}
HTML where custom radio component is being used
<form [formGroup]="frm">
<app-radio id="rc1" [frm]="frm" label="Yes" value="Yes" cn="question" ></app-radio>
<app-radio id="rc2" [frm]="frm" label="No" value="No" cn="question"></app-radio>
</form>
<hr>
<h3>Form Data</h3>
<pre>{{frm.value | json}}
Typescript for html using the custom radio component
import { Component, OnInit } from '@angular/core';
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
@Component({
selector: 'my-app',
templateUrl: './app.component.html',
styleUrls: [ './app.component.css' ]
})
export class AppComponent implements OnInit {
frm: FormGroup;
constructor(private fb: FormBuilder){}
ngOnInit() {
this.frm = this.fb.group({
question: ['Yes', Validators.required]
});
}
}
Expected Behavior
Expect the following initial screen:
Actual Behavior
Other Observations I was surprised to not find value and name attributes on the resulting input elements as seen in a screenshot I took from Chrome Dev Tool:
I would be so grateful if someone could lend me some help. Thank you.
Upvotes: 2
Views: 2492
Reputation: 85
The answer to why my Stackblitz sample does not work as expected is found in the source code for RadioControlRegistry (packages/forms/src/directives/radio_control_value_accessor.ts).
select(accessor: RadioControlValueAccessor) {
this._accessors.forEach((c) => {
if (this._isSameGroup(c, accessor) && c[1] !== accessor) {
c[1].fireUncheck(accessor.value);
}
});
}
private _isSameGroup(
controlPair: [NgControl, RadioControlValueAccessor],
accessor: RadioControlValueAccessor): boolean {
if (!controlPair[0].control) return false;
return controlPair[0]._parent === accessor._control._parent &&
controlPair[1].name === accessor.name;
}
When you click on a unchecked radio control, this select function is called. It will call fireUncheck for other radio controls with the same name property AND the same parent.
That last part is the problem. Each custom radio control has a different parent. Parent here refers to an instance of a FormGroupDirective. Each custom radio control is wrapped with a div that gets a new instance of FormGroupDirective. The fireUncheck never gets called.
If somehow I could make the parents be equal in this scenario, it would work.
I found that I could by using formControl directive instead of the formControlName directive in the radio component.
I have created a modified version of the Stackblitz sample. I replaced formGroup and formControlName directives with the formControl directive. Works like a charm.
Here is the modified code:
The custom radio component's html
<div>
<label [for]="id">
<input [id]="id" type="radio" [value]="value" [formControl]="ctrl">{{label}}
</label>
</div>
Custom radio component's typescript
import { Component, Input } from "@angular/core";
import { FormControl } from "@angular/forms";
@Component({
selector: 'app-radio',
templateUrl: './radio.component.html',
styleUrls: ['./radio.component.css']
})
export class RadioComponent {
@Input() ctrl: FormControl;
@Input() label: string;
@Input() value: string;
@Input() id: string;
}
HTML where custom radio component is being used
<form [formGroup]="frm">
<app-radio id="rc1" label="Yes" value="Yes" [ctrl]="frm.get('question')" ></app-radio>
<app-radio id="rc2" label="No" value="No" [ctrl]="frm.get('question')"></app-radio>
</form>
<hr>
<h3>Form Data</h3>
<pre>{{frm.value | json}}
Typescript for html using the custom radio component
import { Component, OnInit } from '@angular/core';
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
@Component({
selector: 'my-app',
templateUrl: './app.component.html',
styleUrls: [ './app.component.css' ]
})
export class AppComponent implements OnInit {
frm: FormGroup;
constructor(private fb: FormBuilder){}
ngOnInit() {
this.frm = this.fb.group({
question: ['Yes', Validators.required]
});
}
}
Upvotes: 3
Reputation: 821
Here's an updated StackBlitz that I believe fulfills your behavior expectations.
I've added [checked]="frm.get(cn).value === value"
.
radio.component.html
<div [formGroup]="frm">
<label [for]="id">
<input [id]="id" type="radio" [value]="value" [formControlName]="cn" [checked]="frm.get(cn).value === value">{{label}}
</label>
</div>
I'm still doing a bit of digging myself, but I suspect the issue is that the outer formGroup
directive in app.component.html
can't locate the nested radio inputs created by RadioComponent
.
Upvotes: 2
Reputation: 445
I think what's happening here is that your two radio buttons are not connected. I just tried your Stackblitz example and for me the initial value is Yes and when I click No, both are checked.
As far as I know, radio buttons need to share the same name property to be connected as also documented by W3 schools. You could try the following, which uses the name question:
<form [formGroup]="frm">
<app-radio id="rc1" name="question" [frm]="frm" label="Yes" value="Yes" cn="question" ></app-radio>
<app-radio id="rc2" name="question" [frm]="frm" label="No" value="No" cn="question"></app-radio>
</form>
To be honest: I do not quite get what you are trying to achieve with your custom component here. You could also go with native radio buttons:
<form [formGroup]="frm">
<input type="radio" id="rc1" name="question" label="Yes" value="Yes" cn="question" >
<input type="radio" id="rc2" name="question" label="No" value="No" cn="question">
</form>
Note: I've removed [frm]="frm" here.
And if you need custom CSS, you could overwrite that as well:
input {
color: red;
}
EDIT: Using the RadioControlValueAccessor
The official Angular documentation shows an example of using radioButtons inside a reactive form similar to what you do (except for the custom component wrapping the radioButton input field).
<form [formGroup]="form">
<input type="radio" formControlName="food" value="beef" > Beef
<input type="radio" formControlName="food" value="lamb"> Lamb
<input type="radio" formControlName="food" value="fish"> Fish
</form>
Here they all use the same formControlName food to link the radioButtons. In your question, this will be question to match your formControl.
Upvotes: 1