Skip to content

Commit d83f062

Browse files
robertmesserlejelbourn
authored andcommitted
Fix/403/radio without name (#478)
* fix(radio): resolves change detection issues Using *ngFor to iterate over options was causing the error "Expression has changed after it was checked. Previous value: undefined." closes #403 * fix(radio): name property should be inherited from radio group
1 parent fcc4066 commit d83f062

File tree

2 files changed

+54
-34
lines changed

2 files changed

+54
-34
lines changed

src/components/radio/radio.spec.ts

Lines changed: 36 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -197,12 +197,11 @@ describe('MdRadio', () => {
197197

198198
it('should deselect all of the checkboxes when the group value is cleared', () => {
199199
radioInstances[0].checked = true;
200-
fixture.detectChanges();
201200

202201
expect(groupInstance.value).toBeTruthy();
203202

204203
groupInstance.value = null;
205-
fixture.detectChanges();
204+
206205
expect(radioInstances.every(radio => !radio.checked)).toBe(true);
207206
});
208207
});
@@ -236,6 +235,31 @@ describe('MdRadio', () => {
236235
});
237236
}));
238237

238+
it('should set individual radio names based on the group name', () => {
239+
expect(groupInstance.name).toBeTruthy();
240+
for (let radio of radioInstances) {
241+
expect(radio.name).toBe(groupInstance.name);
242+
}
243+
244+
groupInstance.name = 'new name';
245+
for (let radio of radioInstances) {
246+
expect(radio.name).toBe(groupInstance.name);
247+
}
248+
});
249+
250+
it('should check the corresponding radio button on group value change', () => {
251+
expect(groupInstance.value).toBeFalsy();
252+
for (let radio of radioInstances) {
253+
expect(radio.checked).toBeFalsy();
254+
}
255+
256+
groupInstance.value = 'vanilla';
257+
for (let radio of radioInstances) {
258+
expect(radio.checked).toBe(groupInstance.value === radio.value);
259+
}
260+
expect(groupInstance.selected.value).toBe(groupInstance.value);
261+
});
262+
239263
it('should have the correct ngControl state initially and after interaction', fakeAsync(() => {
240264
// The control should start off valid, pristine, and untouched.
241265
expect(groupNgControl.valid).toBe(true);
@@ -333,7 +357,7 @@ describe('MdRadio', () => {
333357
@Component({
334358
directives: [MD_RADIO_DIRECTIVES],
335359
template: `
336-
<md-radio-group [disabled]="isGroupDisabled" [value]="groupValue">
360+
<md-radio-group [disabled]="isGroupDisabled" [value]="groupValue" name="test-name">
337361
<md-radio-button value="fire">Charmander</md-radio-button>
338362
<md-radio-button value="water">Squirtle</md-radio-button>
339363
<md-radio-button value="leaf">Bulbasaur</md-radio-button>
@@ -365,21 +389,26 @@ class StandaloneRadioButtons { }
365389
directives: [MD_RADIO_DIRECTIVES, FORM_DIRECTIVES],
366390
template: `
367391
<md-radio-group [(ngModel)]="modelValue">
368-
<md-radio-button value="vanilla">Vanilla</md-radio-button>
369-
<md-radio-button value="chocolate">Chocolate</md-radio-button>
370-
<md-radio-button value="strawberry">Strawberry</md-radio-button>
392+
<md-radio-button *ngFor="let option of options" [value]="option.value">
393+
{{option.label}}
394+
</md-radio-button>
371395
</md-radio-group>
372396
`
373397
})
374398
class RadioGroupWithNgModel {
375399
modelValue: string;
400+
options = [
401+
{label: 'Vanilla', value: 'vanilla'},
402+
{label: 'Chocolate', value: 'chocolate'},
403+
{label: 'Strawberry', value: 'strawberry'},
404+
];
376405
}
377406

378407
// TODO(jelbourn): remove eveything below when Angular supports faking events.
379408

380409

381410
/**
382-
* Dispatches a focus change event from an element.
411+
* Dispatches a focus change event from an element.
383412
* @param eventName Name of the event, either 'focus' or 'blur'.
384413
* @param element The element from which the event will be dispatched.
385414
*/

src/components/radio/radio.ts

Lines changed: 18 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,7 @@ export class MdRadioGroup implements AfterContentInit, ControlValueAccessor {
6969
private _value: any = null;
7070

7171
/** The HTML name attribute applied to radio buttons in this group. */
72-
private _name: string = null;
72+
private _name: string = `md-radio-group-${_uniqueIdCounter++}`;
7373

7474
/** Disables all individual radio buttons assigned to this group. */
7575
private _disabled: boolean = false;
@@ -101,14 +101,7 @@ export class MdRadioGroup implements AfterContentInit, ControlValueAccessor {
101101

102102
set name(value: string) {
103103
this._name = value;
104-
this._updateChildRadioNames();
105-
}
106-
107-
/** Propagate name attribute to radio buttons. */
108-
private _updateChildRadioNames(): void {
109-
(this._radios || []).forEach(radio => {
110-
radio.name = this._name;
111-
});
104+
this._updateRadioButtonNames();
112105
}
113106

114107
@Input()
@@ -160,10 +153,6 @@ export class MdRadioGroup implements AfterContentInit, ControlValueAccessor {
160153
* @internal
161154
*/
162155
ngAfterContentInit() {
163-
if (!this._name) {
164-
this.name = `md-radio-group-${_uniqueIdCounter++}`;
165-
}
166-
167156
// Mark this component as initialized in AfterContentInit because the initial value can
168157
// possibly be set by NgModel on MdRadioGroup, and it is possible that the OnInit of the
169158
// NgModel occurs *after* the OnInit of the MdRadioGroup.
@@ -181,9 +170,15 @@ export class MdRadioGroup implements AfterContentInit, ControlValueAccessor {
181170
}
182171
}
183172

173+
private _updateRadioButtonNames(): void {
174+
(this._radios || []).forEach(radio => {
175+
radio.name = this.name;
176+
});
177+
}
178+
184179
/** Updates the `selected` radio button from the internal _value state. */
185180
private _updateSelectedRadioFromValue(): void {
186-
// If the value already matches the selected radio, no dothing.
181+
// If the value already matches the selected radio, do nothing.
187182
let isAlreadySelected = this._selected != null && this._selected.value == this._value;
188183

189184
if (this._radios != null && !isAlreadySelected) {
@@ -192,8 +187,8 @@ export class MdRadioGroup implements AfterContentInit, ControlValueAccessor {
192187
if (matchingRadio) {
193188
this.selected = matchingRadio;
194189
} else if (this.value == null) {
195-
this.selected = null;
196-
this._radios.forEach(radio => { radio.checked = false; });
190+
this.selected = null;
191+
this._radios.forEach(radio => { radio.checked = false; });
197192
}
198193
}
199194
}
@@ -206,7 +201,7 @@ export class MdRadioGroup implements AfterContentInit, ControlValueAccessor {
206201
this.change.emit(event);
207202
}
208203

209-
/**
204+
/**
210205
* Implemented as part of ControlValueAccessor.
211206
* @internal
212207
*/
@@ -258,7 +253,7 @@ export class MdRadioButton implements OnInit {
258253
/** The unique ID for the radio button. */
259254
@HostBinding('id')
260255
@Input()
261-
id: string;
256+
id: string = `md-radio-${_uniqueIdCounter++}`;
262257

263258
/** Analog to HTML 'name' attribute used to group radios for unique selection. */
264259
@Input()
@@ -345,15 +340,11 @@ export class MdRadioButton implements OnInit {
345340

346341
/** @internal */
347342
ngOnInit() {
348-
// All radio buttons must have a unique id.
349-
if (!this.id) {
350-
this.id = `md-radio-${_uniqueIdCounter++}`;
351-
}
352-
353-
// If the radio is inside of a radio group and it matches that group's value upon
354-
// initialization, start off as checked.
355-
if (this.radioGroup && this.radioGroup.value === this._value) {
356-
this.checked = true;
343+
if (this.radioGroup) {
344+
// If the radio is inside a radio group, determine if it should be checked
345+
this.checked = this.radioGroup.value === this._value;
346+
// Copy name from parent radio group
347+
this.name = this.radioGroup.name;
357348
}
358349
}
359350

0 commit comments

Comments
 (0)