Tom G
Tom G

Reputation: 85

Why is my radio input element inside a custom component not working properly with reactive forms?

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

  1. Expect the following initial screen:

    Expected Result

  2. When I click on the No radio after initial screen, the Yes radio gets unchecked and the No radio gets checked.
  3. When I click on the No radio after initial screen, the form data shows that the question property has a value of "No".
  4. Subsequent option clicks unchecks the other option and checks the clicked option.
  5. Subsequent option clicks change the form value for question field.

Actual Behavior

  1. Success
  2. Failed. Both options are checked.
  3. Success
  4. Failed. Nothing seems to happen when I click on either option.
  5. Failed. Nothing seems to happen when I click on either option.

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:

enter image description here

I would be so grateful if someone could lend me some help. Thank you.

Upvotes: 2

Views: 2492

Answers (3)

Tom G
Tom G

Reputation: 85

Why my Stackblitz sample did not work?

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.

Solution:

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

OSH
OSH

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

gerstams
gerstams

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

Related Questions