Skip to content

Commit 9e56f70

Browse files
authored
fix(radio-group): dynamically added radio buttons do not initialize (#16042)
1 parent 9da28d7 commit 9e56f70

File tree

9 files changed

+302
-20
lines changed

9 files changed

+302
-20
lines changed

projects/igniteui-angular/src/lib/combo/combo.component.spec.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@ const CSS_CLASS_ITME_CHECKBOX_CHECKED = 'igx-checkbox--checked';
5858
const defaultDropdownItemHeight = 40;
5959
const defaultDropdownItemMaxHeight = 400;
6060

61-
fdescribe('igxCombo', () => {
61+
describe('igxCombo', () => {
6262
let fixture: ComponentFixture<any>;
6363
let combo: IgxComboComponent;
6464
let input: DebugElement;

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: 34 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

@@ -493,10 +491,7 @@ export class IgxRadioGroupDirective implements ControlValueAccessor, OnDestroy,
493491

494492
effect(() => {
495493
this.initialize();
496-
497-
Promise.resolve().then(() => {
498-
this.setRadioButtons();
499-
});
494+
this.setRadioButtons();
500495
});
501496
}
502497

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

540537
if (button.value === this._value) {
541538
button.checked = true;
@@ -647,6 +644,33 @@ export class IgxRadioGroupDirective implements ControlValueAccessor, OnDestroy,
647644
}
648645
}
649646

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+
650674
/**
651675
* @hidden
652676
* @internal

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

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

1619
/**
1720
* **Ignite UI for Angular Radio Button** -
@@ -41,11 +44,13 @@ import { CheckboxBaseDirective } from '../checkbox/checkbox-base.directive';
4144

4245
export class IgxRadioComponent
4346
extends CheckboxBaseDirective
44-
implements AfterViewInit, ControlValueAccessor, EditorProvider {
47+
implements AfterViewInit, OnDestroy, ControlValueAccessor, EditorProvider {
4548

4649
/** @hidden @internal */
4750
public blurRadio = new EventEmitter();
4851

52+
private radioGroup = inject(IgxRadioGroupDirective, { optional: true, skipSelf: true });
53+
4954
/**
5055
* Returns the class of the radio component.
5156
* ```typescript
@@ -205,4 +210,28 @@ export class IgxRadioComponent
205210
super.onBlur();
206211
this.blurRadio.emit();
207212
}
213+
214+
/**
215+
* @hidden
216+
* @internal
217+
*/
218+
public override ngAfterViewInit(): void {
219+
super.ngAfterViewInit();
220+
221+
// Register with parent radio group if it exists
222+
if (this.radioGroup) {
223+
this.radioGroup._addRadioButton(this);
224+
}
225+
}
226+
227+
/**
228+
* @hidden
229+
* @internal
230+
*/
231+
public ngOnDestroy(): void {
232+
// Unregister from parent radio group if it exists
233+
if (this.radioGroup) {
234+
this.radioGroup._removeRadioButton(this);
235+
}
236+
}
208237
}
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: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,3 +100,12 @@ <h4 class="sample-title">Radio group in reactive form</h4>
100100
<p>Updated model: {{ newPerson | json }}</p>
101101
</article>
102102
</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>
110+
</article>
111+
</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)