Skip to content

Commit 15d2fe4

Browse files
committed
fix(checkbox, radio): setting id to null causes invalid id for input
* In some situations, developers will change the `id` of a component dynamically. At some point if they set the `[id]` to `null` the input id will look like the following: `null-input`. * If developers are setting the `id` using a binding then the DOM Id for the host component won't be set at all (because the @input is triggered instead of the DOM property) Fixes #5394
1 parent 8817b4d commit 15d2fe4

File tree

6 files changed

+42
-33
lines changed

6 files changed

+42
-33
lines changed

e2e/components/checkbox-e2e.spec.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ describe('checkbox', () => {
99

1010
it('should be checked when clicked, and unchecked when clicked again', async () => {
1111
let checkboxEl = element(by.id('test-checkbox'));
12-
let inputEl = element(by.css('input[id=input-test-checkbox]'));
12+
let inputEl = element(by.css('input[id=test-checkbox-input]'));
1313

1414
screenshot('start');
1515
checkboxEl.click();
@@ -32,7 +32,7 @@ describe('checkbox', () => {
3232
});
3333

3434
it('should toggle the checkbox when pressing space', () => {
35-
let inputEl = element(by.css('input[id=input-test-checkbox]'));
35+
let inputEl = element(by.css('input[id=test-checkbox-input]'));
3636

3737
expect(inputEl.getAttribute('checked'))
3838
.toBeFalsy('Expect checkbox "checked" property to be false');

src/lib/checkbox/checkbox.spec.ts

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -266,6 +266,15 @@ describe('MdCheckbox', () => {
266266

267267
it('should preserve the user-provided id', () => {
268268
expect(checkboxNativeElement.id).toBe('simple-check');
269+
expect(inputElement.id).toBe('simple-check-input');
270+
});
271+
272+
it('should generate a unique id for the checkbox input if no id is set', () => {
273+
testComponent.checkboxId = null;
274+
fixture.detectChanges();
275+
276+
expect(checkboxInstance.inputId).toMatch(/md-checkbox-\d+/);
277+
expect(inputElement.id).toBe(checkboxInstance.inputId);
269278
});
270279

271280
it('should project the checkbox content into the label element', () => {
@@ -675,8 +684,8 @@ describe('MdCheckbox', () => {
675684
fixture.debugElement.queryAll(By.directive(MdCheckbox))
676685
.map(debugElement => debugElement.nativeElement.querySelector('input').id);
677686

678-
expect(firstId).toBeTruthy();
679-
expect(secondId).toBeTruthy();
687+
expect(firstId).toMatch(/md-checkbox-\d+-input/);
688+
expect(secondId).toMatch(/md-checkbox-\d+-input/);
680689
expect(firstId).not.toEqual(secondId);
681690
});
682691
});
@@ -833,7 +842,7 @@ describe('MdCheckbox', () => {
833842
template: `
834843
<div (click)="parentElementClicked = true" (keyup)="parentElementKeyedUp = true">
835844
<md-checkbox
836-
id="simple-check"
845+
[id]="checkboxId"
837846
[required]="isRequired"
838847
[labelPosition]="labelPos"
839848
[checked]="isChecked"
@@ -857,6 +866,7 @@ class SingleCheckbox {
857866
disableRipple: boolean = false;
858867
parentElementClicked: boolean = false;
859868
parentElementKeyedUp: boolean = false;
869+
checkboxId: string | null = 'simple-check';
860870
checkboxColor: string = 'primary';
861871
checkboxValue: string = 'single_checkbox';
862872

src/lib/checkbox/checkbox.ts

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -27,9 +27,8 @@ import {FocusOrigin, FocusOriginMonitor, MdRipple, RippleRef} from '../core';
2727
import {mixinDisabled, CanDisable} from '../core/common-behaviors/disabled';
2828
import {CanColor, mixinColor} from '../core/common-behaviors/color';
2929

30-
31-
/** Monotonically increasing integer used to auto-generate unique ids for checkbox components. */
32-
let nextId = 0;
30+
// Increasing integer for generating unique ids for checkbox components.
31+
let nextUniqueId = 0;
3332

3433
/**
3534
* Provider Expression that allows md-checkbox to register as a ControlValueAccessor.
@@ -88,6 +87,7 @@ export const _MdCheckboxMixinBase = mixinColor(mixinDisabled(MdCheckboxBase), 'a
8887
styleUrls: ['checkbox.css'],
8988
host: {
9089
'class': 'mat-checkbox',
90+
'[id]': 'id',
9191
'[class.mat-checkbox-indeterminate]': 'indeterminate',
9292
'[class.mat-checkbox-checked]': 'checked',
9393
'[class.mat-checkbox-disabled]': 'disabled',
@@ -111,8 +111,13 @@ export class MdCheckbox extends _MdCheckboxMixinBase
111111
*/
112112
@Input('aria-labelledby') ariaLabelledby: string | null = null;
113113

114-
/** A unique id for the checkbox. If one is not supplied, it is auto-generated. */
115-
@Input() id: string = `md-checkbox-${++nextId}`;
114+
private _uniqueId: string = `md-checkbox-${++nextUniqueId}`;
115+
116+
/** A unique id for the checkbox input. If none is supplied, it will be auto-generated. */
117+
@Input() id: string = this._uniqueId;
118+
119+
/** Returns the unique id for the visual hidden input. */
120+
get inputId(): string { return `${this.id || this._uniqueId}-input`; }
116121

117122
/** Whether the ripple effect on click should be disabled. */
118123
private _disableRipple: boolean;
@@ -122,11 +127,6 @@ export class MdCheckbox extends _MdCheckboxMixinBase
122127
get disableRipple(): boolean { return this._disableRipple; }
123128
set disableRipple(value) { this._disableRipple = coerceBooleanProperty(value); }
124129

125-
/** ID of the native input element inside `<md-checkbox>` */
126-
get inputId(): string {
127-
return `input-${this.id}`;
128-
}
129-
130130
private _required: boolean;
131131

132132
/** Whether the checkbox is required. */

src/lib/radio/radio.ts

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,8 @@ import {coerceBooleanProperty} from '@angular/cdk';
3939
import {mixinDisabled, CanDisable} from '../core/common-behaviors/disabled';
4040
import {CanColor, mixinColor} from '../core/common-behaviors/color';
4141

42+
// Increasing integer for generating unique ids for radio components.
43+
let nextUniqueId = 0;
4244

4345
/**
4446
* Provider Expression that allows md-radio-group to register as a ControlValueAccessor. This
@@ -51,8 +53,6 @@ export const MD_RADIO_GROUP_CONTROL_VALUE_ACCESSOR: any = {
5153
multi: true
5254
};
5355

54-
let _uniqueIdCounter = 0;
55-
5656
/** Change event object emitted by MdRadio and MdRadioGroup. */
5757
export class MdRadioChange {
5858
/** The MdRadioButton that emits the change event. */
@@ -90,7 +90,7 @@ export class MdRadioGroup extends _MdRadioGroupMixinBase
9090
private _value: any = null;
9191

9292
/** The HTML name attribute applied to radio buttons in this group. */
93-
private _name: string = `md-radio-group-${_uniqueIdCounter++}`;
93+
private _name: string = `md-radio-group-${nextUniqueId++}`;
9494

9595
/** The currently selected radio button. Should match value. */
9696
private _selected: MdRadioButton | null = null;
@@ -326,8 +326,10 @@ export const _MdRadioButtonMixinBase = mixinColor(MdRadioButtonBase, 'accent');
326326
export class MdRadioButton extends _MdRadioButtonMixinBase
327327
implements OnInit, AfterViewInit, OnDestroy, CanColor {
328328

329+
private _uniqueId: string = `md-radio-${++nextUniqueId}`;
330+
329331
/** The unique ID for the radio button. */
330-
@Input() id: string = `md-radio-${_uniqueIdCounter++}`;
332+
@Input() id: string = this._uniqueId;
331333

332334
/** Analog to HTML 'name' attribute used to group radios for unique selection. */
333335
@Input() name: string;
@@ -437,9 +439,7 @@ export class MdRadioButton extends _MdRadioButtonMixinBase
437439
radioGroup: MdRadioGroup;
438440

439441
/** ID of the native input element inside `<md-radio-button>` */
440-
get inputId(): string {
441-
return `${this.id}-input`;
442-
}
442+
get inputId(): string { return `${this.id || this._uniqueId}-input`; }
443443

444444
/** Whether this radio is checked. */
445445
private _checked: boolean = false;

src/lib/slide-toggle/slide-toggle.spec.ts

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -180,18 +180,20 @@ describe('MdSlideToggle', () => {
180180
testComponent.slideId = 'myId';
181181
fixture.detectChanges();
182182

183-
expect(inputElement.id).toBe('myId-input');
183+
expect(slideToggleElement.id).toBe('myId');
184+
expect(inputElement.id).toBe(`${slideToggleElement.id}-input`);
184185

185186
testComponent.slideId = 'nextId';
186187
fixture.detectChanges();
187188

188-
expect(inputElement.id).toBe('nextId-input');
189+
expect(slideToggleElement.id).toBe('nextId');
190+
expect(inputElement.id).toBe(`${slideToggleElement.id}-input`);
189191

190192
testComponent.slideId = null;
191193
fixture.detectChanges();
192194

193-
// Once the id input is falsy, we use a default prefix with a incrementing unique number.
194-
expect(inputElement.id).toMatch(/md-slide-toggle-[0-9]+-input/g);
195+
// Once the id binding is set to null, the id property should auto-generate a unique id.
196+
expect(inputElement.id).toMatch(/md-slide-toggle-\d+-input/);
195197
});
196198

197199
it('should forward the tabIndex to the underlying input', () => {

src/lib/slide-toggle/slide-toggle.ts

Lines changed: 4 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,8 @@ import {ControlValueAccessor, NG_VALUE_ACCESSOR} from '@angular/forms';
3535
import {mixinDisabled, CanDisable} from '../core/common-behaviors/disabled';
3636
import {CanColor, mixinColor} from '../core/common-behaviors/color';
3737

38+
// Increasing integer for generating unique ids for slide-toggle components.
39+
let nextUniqueId = 0;
3840

3941
export const MD_SLIDE_TOGGLE_VALUE_ACCESSOR: any = {
4042
provide: NG_VALUE_ACCESSOR,
@@ -48,11 +50,6 @@ export class MdSlideToggleChange {
4850
checked: boolean;
4951
}
5052

51-
// Increasing integer for generating unique ids for slide-toggle components.
52-
let nextId = 0;
53-
54-
55-
5653
// Boilerplate for applying mixins to MdSlideToggle.
5754
/** @docs-private */
5855
export class MdSlideToggleBase {
@@ -66,6 +63,7 @@ export const _MdSlideToggleMixinBase = mixinColor(mixinDisabled(MdSlideToggleBas
6663
selector: 'md-slide-toggle, mat-slide-toggle',
6764
host: {
6865
'class': 'mat-slide-toggle',
66+
'[id]': 'id',
6967
'[class.mat-checked]': 'checked',
7068
'[class.mat-disabled]': 'disabled',
7169
'[class.mat-slide-toggle-label-before]': 'labelPosition == "before"',
@@ -82,8 +80,7 @@ export class MdSlideToggle extends _MdSlideToggleMixinBase
8280
private onChange = (_: any) => {};
8381
private onTouched = () => {};
8482

85-
// A unique id for the slide-toggle. By default the id is auto-generated.
86-
private _uniqueId = `md-slide-toggle-${++nextId}`;
83+
private _uniqueId: string = `md-slide-toggle-${++nextUniqueId}`;
8784
private _checked: boolean = false;
8885
private _slideRenderer: SlideToggleRenderer;
8986
private _required: boolean = false;

0 commit comments

Comments
 (0)