Skip to content

Commit d680d4a

Browse files
authored
fix(radio-group): dynamically added radio buttons do not initialize (#16021)
1 parent e289b14 commit d680d4a

File tree

8 files changed

+303
-20
lines changed

8 files changed

+303
-20
lines changed

projects/igniteui-angular/src/lib/directives/radio/radio-group.directive.spec.ts

Lines changed: 106 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { ChangeDetectionStrategy, Component, ViewChild } from '@angular/core';
1+
import { ChangeDetectionStrategy, Component, ComponentRef, OnInit, ViewChild, ViewContainerRef } from '@angular/core';
22
import { TestBed, fakeAsync, tick, waitForAsync } from '@angular/core/testing';
33
import { IgxRadioGroupDirective } from './radio-group.directive';
44
import { FormsModule, ReactiveFormsModule, UntypedFormGroup, UntypedFormBuilder, FormGroup, FormControl } from '@angular/forms';
@@ -20,7 +20,9 @@ describe('IgxRadioGroupDirective', () => {
2020
RadioGroupWithModelComponent,
2121
RadioGroupRequiredComponent,
2222
RadioGroupReactiveFormsComponent,
23-
RadioGroupDeepProjectionComponent
23+
RadioGroupDeepProjectionComponent,
24+
RadioGroupTestComponent,
25+
DynamicRadioGroupComponent
2426
]
2527
})
2628
.compileComponents();
@@ -69,13 +71,15 @@ describe('IgxRadioGroupDirective', () => {
6971
// name
7072
radioInstance.name = 'newGroupName';
7173
fixture.detectChanges();
74+
tick();
7275

7376
const allButtonsWithNewName = radioInstance.radioButtons.filter((btn) => btn.name === 'newGroupName');
7477
expect(allButtonsWithNewName.length).toEqual(radioInstance.radioButtons.length);
7578

7679
// required
7780
radioInstance.required = true;
7881
fixture.detectChanges();
82+
tick();
7983

8084
const allRequiredButtons = radioInstance.radioButtons.filter((btn) => btn.required);
8185
expect(allRequiredButtons.length).toEqual(radioInstance.radioButtons.length);
@@ -261,6 +265,38 @@ describe('IgxRadioGroupDirective', () => {
261265
expect(radioGroup.radioButtons.first.checked).toEqual(true);
262266
expect(domRadio.classList.contains('igx-radio--invalid')).toBe(false);
263267
}));
268+
269+
it('Should select radio button when added programmatically after group value is set', (() => {
270+
const fixture = TestBed.createComponent(DynamicRadioGroupComponent);
271+
const component = fixture.componentInstance;
272+
const radioGroup = component.radioGroup;
273+
274+
// Simulate AppBuilder configurator setting value before radio buttons exist
275+
radioGroup.value = 'option2';
276+
277+
// Verify no radio buttons exist yet
278+
expect(radioGroup.radioButtons.length).toBe(0);
279+
expect(radioGroup.selected).toBeNull();
280+
281+
fixture.detectChanges();
282+
283+
component.addRadioButton('option1', 'Option 1');
284+
component.addRadioButton('option2', 'Option 2');
285+
component.addRadioButton('option3', 'Option 3');
286+
287+
fixture.detectChanges();
288+
289+
// Radio button with value 'option2' should be selected
290+
expect(radioGroup.value).toBe('option2');
291+
expect(radioGroup.selected).toBeDefined();
292+
expect(radioGroup.selected.value).toBe('option2');
293+
expect(radioGroup.selected.checked).toBe(true);
294+
295+
// Verify only one radio button is selected
296+
const checkedButtons = radioGroup.radioButtons.filter(btn => btn.checked);
297+
expect(checkedButtons.length).toBe(1);
298+
expect(checkedButtons[0].value).toBe('option2');
299+
}));
264300
});
265301

266302
@Component({
@@ -444,8 +480,75 @@ class RadioGroupDeepProjectionComponent {
444480
}
445481
}
446482

483+
@Component({
484+
template: `
485+
<igx-radio-group
486+
[alignment]="alignment"
487+
[required]="required"
488+
[value]="value"
489+
(change)="handleChange($event)"
490+
>
491+
<ng-container #radioContainer></ng-container>
492+
</igx-radio-group>
493+
`,
494+
imports: [IgxRadioComponent, IgxRadioGroupDirective]
495+
})
496+
497+
class RadioGroupTestComponent implements OnInit {
498+
@ViewChild('radioContainer', { read: ViewContainerRef, static: true })
499+
public container!: ViewContainerRef;
500+
501+
public alignment = 'horizontal';
502+
public required = false;
503+
public value: any;
504+
505+
public radios: { label: string; value: any }[] = [];
506+
507+
public handleChange(args: any) {
508+
this.value = args.value;
509+
}
510+
511+
public ngOnInit(): void {
512+
this.container.clear();
513+
this.radios.forEach((option) => {
514+
const componentRef: ComponentRef<IgxRadioComponent> =
515+
this.container.createComponent(IgxRadioComponent);
516+
517+
componentRef.instance.placeholderLabel.nativeElement.textContent =
518+
option.label;
519+
componentRef.instance.value = option.value;
520+
});
521+
}
522+
}
523+
524+
@Component({
525+
template: `
526+
<igx-radio-group #radioGroup>
527+
<ng-container #radioContainer></ng-container>
528+
</igx-radio-group>
529+
`,
530+
imports: [IgxRadioGroupDirective, IgxRadioComponent]
531+
})
532+
class DynamicRadioGroupComponent {
533+
@ViewChild('radioGroup', { read: IgxRadioGroupDirective, static: true })
534+
public radioGroup: IgxRadioGroupDirective;
535+
536+
@ViewChild('radioContainer', { read: ViewContainerRef, static: true })
537+
public radioContainer: ViewContainerRef;
538+
539+
/**
540+
* Simulates how AppBuilder adds radio buttons programmatically
541+
* via ViewContainerRef.createComponent()
542+
*/
543+
public addRadioButton(value: string, label: string): void {
544+
const componentRef = this.radioContainer.createComponent(IgxRadioComponent);
545+
componentRef.instance.value = value;
546+
componentRef.instance.placeholderLabel.nativeElement.textContent = label;
547+
componentRef.changeDetectorRef.detectChanges();
548+
}
549+
}
550+
447551
const dispatchRadioEvent = (eventName, radioNativeElement, fixture) => {
448552
radioNativeElement.dispatchEvent(new Event(eventName));
449553
fixture.detectChanges();
450554
};
451-

projects/igniteui-angular/src/lib/directives/radio/radio-group.directive.ts

Lines changed: 35 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,6 @@ import {
1212
QueryList,
1313
Self,
1414
booleanAttribute,
15-
contentChildren,
1615
effect,
1716
signal
1817
} from '@angular/core';
@@ -62,7 +61,7 @@ let nextId = 0;
6261
standalone: true
6362
})
6463
export class IgxRadioGroupDirective implements ControlValueAccessor, OnDestroy, DoCheck {
65-
private _radioButtons = contentChildren(IgxRadioComponent, { descendants: true });
64+
private _radioButtons = signal<IgxRadioComponent[]>([]);
6665
private _radioButtonsList = new QueryList<IgxRadioComponent>();
6766

6867
/**
@@ -74,8 +73,7 @@ export class IgxRadioGroupDirective implements ControlValueAccessor, OnDestroy,
7473
* ```
7574
*/
7675
public get radioButtons(): QueryList<IgxRadioComponent> {
77-
const buttons = Array.from(this._radioButtons());
78-
this._radioButtonsList.reset(buttons);
76+
this._radioButtonsList.reset(this._radioButtons());
7977
return this._radioButtonsList;
8078
}
8179

@@ -492,10 +490,7 @@ export class IgxRadioGroupDirective implements ControlValueAccessor, OnDestroy,
492490

493491
effect(() => {
494492
this.initialize();
495-
496-
Promise.resolve().then(() => {
497-
this.setRadioButtons();
498-
});
493+
this.setRadioButtons();
499494
});
500495
}
501496

@@ -533,8 +528,10 @@ export class IgxRadioGroupDirective implements ControlValueAccessor, OnDestroy,
533528
*/
534529
private setRadioButtons() {
535530
this._radioButtons().forEach((button) => {
536-
button.name = this._name;
537-
button.required = this._required;
531+
Promise.resolve().then(() => {
532+
button.name = this._name;
533+
button.required = this._required;
534+
});
538535

539536
if (button.value === this._value) {
540537
button.checked = true;
@@ -646,6 +643,34 @@ export class IgxRadioGroupDirective implements ControlValueAccessor, OnDestroy,
646643
}
647644
}
648645

646+
647+
/**
648+
* Registers a radio button with this radio group.
649+
* This method is called by radio button components when they are created.
650+
* @hidden @internal
651+
*/
652+
public _addRadioButton(radioButton: IgxRadioComponent): void {
653+
this._radioButtons.update(buttons => {
654+
if (!buttons.includes(radioButton)) {
655+
this._setRadioButtonEvents(radioButton);
656+
657+
return [...buttons, radioButton];
658+
}
659+
return buttons;
660+
});
661+
}
662+
663+
/**
664+
* Unregisters a radio button from this radio group.
665+
* This method is called by radio button components when they are destroyed.
666+
* @hidden @internal
667+
*/
668+
public _removeRadioButton(radioButton: IgxRadioComponent): void {
669+
this._radioButtons.update(buttons =>
670+
buttons.filter(btn => btn !== radioButton)
671+
);
672+
}
673+
649674
/**
650675
* @hidden
651676
* @internal

projects/igniteui-angular/src/lib/radio/radio.component.ts

Lines changed: 31 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,15 @@ import {
55
HostBinding,
66
HostListener,
77
Input,
8-
booleanAttribute
8+
booleanAttribute,
9+
OnDestroy,
10+
inject
911
} from '@angular/core';
1012
import { ControlValueAccessor } from '@angular/forms';
1113
import { EditorProvider, EDITOR_PROVIDER } from '../core/edit-provider';
1214
import { IgxRippleDirective } from '../directives/ripple/ripple.directive';
1315
import { CheckboxBaseDirective } from '../checkbox/checkbox-base.directive';
16+
import { IgxRadioGroupDirective } from '../directives/radio/radio-group.directive';
1417

1518
/**
1619
* **Ignite UI for Angular Radio Button** -
@@ -37,10 +40,12 @@ import { CheckboxBaseDirective } from '../checkbox/checkbox-base.directive';
3740
})
3841
export class IgxRadioComponent
3942
extends CheckboxBaseDirective
40-
implements AfterViewInit, ControlValueAccessor, EditorProvider {
43+
implements AfterViewInit, OnDestroy, ControlValueAccessor, EditorProvider {
4144
/** @hidden @internal */
4245
public blurRadio = new EventEmitter();
4346

47+
private radioGroup = inject(IgxRadioGroupDirective, { optional: true, skipSelf: true });
48+
4449
/**
4550
* Returns the class of the radio component.
4651
* ```typescript
@@ -200,4 +205,28 @@ export class IgxRadioComponent
200205
super.onBlur();
201206
this.blurRadio.emit();
202207
}
208+
209+
/**
210+
* @hidden
211+
* @internal
212+
*/
213+
public override ngAfterViewInit(): void {
214+
super.ngAfterViewInit();
215+
216+
// Register with parent radio group if it exists
217+
if (this.radioGroup) {
218+
this.radioGroup._addRadioButton(this);
219+
}
220+
}
221+
222+
/**
223+
* @hidden
224+
* @internal
225+
*/
226+
public ngOnDestroy(): void {
227+
// Unregister from parent radio group if it exists
228+
if (this.radioGroup) {
229+
this.radioGroup._removeRadioButton(this);
230+
}
231+
}
203232
}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
<igx-radio-group
2+
[alignment]="alignment"
3+
[required]="required"
4+
[value]="value"
5+
(change)="handleChange($event)"
6+
>
7+
<ng-container #radioContainer></ng-container>
8+
</igx-radio-group>
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
import {
2+
Component,
3+
Input,
4+
ViewChild,
5+
ComponentRef,
6+
ViewContainerRef,
7+
OnInit,
8+
} from '@angular/core';
9+
import {
10+
IChangeCheckboxEventArgs,
11+
IgxRadioComponent,
12+
IgxRadioGroupDirective,
13+
RadioGroupAlignment,
14+
} from 'igniteui-angular';
15+
16+
@Component({
17+
selector: 'app-radio-group',
18+
templateUrl: './radio-group.component.html',
19+
imports: [IgxRadioGroupDirective],
20+
})
21+
export class RadioGroupComponent implements OnInit {
22+
@Input() public alignment!: RadioGroupAlignment;
23+
@Input() public required!: boolean;
24+
@Input() public value!: unknown;
25+
26+
public handleChange(args: IChangeCheckboxEventArgs) {
27+
this.value = args.value;
28+
}
29+
30+
@ViewChild('radioContainer', { read: ViewContainerRef, static: true })
31+
public container!: ViewContainerRef;
32+
33+
@Input() public radios: { label: string; value: any }[] = [];
34+
35+
public ngOnInit(): void {
36+
this.container.clear();
37+
this.radios.forEach((option) => {
38+
const componentRef: ComponentRef<IgxRadioComponent> =
39+
this.container.createComponent(IgxRadioComponent);
40+
41+
componentRef.instance.placeholderLabel.nativeElement.textContent =
42+
option.label;
43+
componentRef.instance.value = option.value;
44+
});
45+
}
46+
}

src/app/radio/radio.sample.html

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,15 @@ <h4 class="sample-title">Radio group in reactive form</h4>
9797
</form>
9898
<p>Form value: {{ personKirkForm.value | json }}</p>
9999
<p>Model value: {{ personKirk | json }}</p>
100-
<p>Updated model: {{ newPerson | json }}</p>
100+
<p>Updated model:r{{ newPerson | json }}</p>
101+
</article>
102+
</section>
103+
<section class="sample-content">
104+
<article class="sample-column">
105+
<h4 class="sample-title">Dynamically Create Radio Group</h4>
106+
<button igxButton="contained" (buttonClick)="createRadioGroupComponent()" style="width: 200px">
107+
Create Radio Group
108+
</button>
109+
<ng-template #container></ng-template>
101110
</article>
102111
</section>

src/app/radio/radio.sample.scss

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
1+
igx-radio-group {
2+
width: initial;
3+
}
4+
15
.sample-content {
26
flex-flow: column nowrap;
37
}

0 commit comments

Comments
 (0)