Skip to content
This repository was archived by the owner on Oct 7, 2020. It is now read-only.

Commit 18d3e37

Browse files
authored
fix(checkbox): Throws exception with Ivy (#2115)
closes #2109
1 parent abfd276 commit 18d3e37

File tree

4 files changed

+118
-32
lines changed

4 files changed

+118
-32
lines changed

packages/checkbox/checkbox.ts

Lines changed: 43 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ import {
1616
} from '@angular/core';
1717
import {NG_VALUE_ACCESSOR, ControlValueAccessor} from '@angular/forms';
1818
import {coerceBooleanProperty} from '@angular/cdk/coercion';
19-
import {Platform} from '@angular/cdk/platform';
19+
import {Platform, supportsPassiveEventListeners} from '@angular/cdk/platform';
2020
import {fromEvent, Subject} from 'rxjs';
2121
import {takeUntil, filter} from 'rxjs/operators';
2222

@@ -102,14 +102,14 @@ export class MdcCheckbox extends MDCComponent<MDCCheckboxFoundation> implements
102102

103103
_root!: Element;
104104

105+
private _initialized: boolean = false;
105106
private _uniqueId: string = `mdc-checkbox-${++nextUniqueId}`;
106107

107108
@Input() id: string = this._uniqueId;
108109

109110
/** Returns the unique id for the visual hidden input. */
110111
get inputId(): string {
111-
return `${this.id || this._uniqueId}-input`
112-
;
112+
return `${this.id || this._uniqueId}-input`;
113113
}
114114

115115
@Input() name: string | null = null;
@@ -170,7 +170,7 @@ export class MdcCheckbox extends MDCComponent<MDCCheckboxFoundation> implements
170170
}
171171
this.indeterminateChange.emit({source: this, indeterminate: this._indeterminate});
172172
this._changeDetectorRef.markForCheck();
173-
this._foundation.handleChange();
173+
this._foundation?.handleChange();
174174
}
175175
}
176176
private _indeterminate: boolean = false;
@@ -224,20 +224,23 @@ export class MdcCheckbox extends MDCComponent<MDCCheckboxFoundation> implements
224224
/** View to model callback called when component has been touched */
225225
_onTouched: () => any = () => {};
226226

227-
getDefaultFoundation() {
227+
getDefaultFoundation(): any {
228+
// Do not initialize foundation until ngAfterViewInit runs
229+
if (!this._initialized) {
230+
return undefined;
231+
}
232+
228233
const adapter: MDCCheckboxAdapter = {
229-
addClass: (className: string) => this._getHostElement().classList.add(className),
230-
removeClass: (className: string) => this._getHostElement().classList.remove(className),
234+
addClass: (className: string) => this._root.classList.add(className),
235+
removeClass: (className: string) => this._root.classList.remove(className),
231236
setNativeControlAttr: (attr: string, value: string) =>
232237
this._inputElement.nativeElement.setAttribute(attr, value),
233-
removeNativeControlAttr: (attr: string) =>
234-
this._inputElement.nativeElement.removeAttribute(attr),
238+
removeNativeControlAttr: (attr: string) => this._inputElement.nativeElement.removeAttribute(attr),
235239
isIndeterminate: () => this.indeterminate,
236240
isChecked: () => this.checked,
237241
hasNativeControl: () => true,
238-
setNativeControlDisabled: (disabled: boolean) =>
239-
this._inputElement.nativeElement.disabled = disabled,
240-
forceLayout: () => this._getHostElement().offsetWidth,
242+
setNativeControlDisabled: (disabled: boolean) => this._inputElement.nativeElement.disabled = disabled,
243+
forceLayout: () => (<HTMLElement>this._root).offsetWidth,
241244
isAttachedToDOM: () => true
242245
};
243246
return new MDCCheckboxFoundation(adapter);
@@ -253,16 +256,27 @@ export class MdcCheckbox extends MDCComponent<MDCCheckboxFoundation> implements
253256
super(elementRef);
254257

255258
this._root = this.elementRef.nativeElement;
256-
this.ripple = this._createRipple();
257-
this.ripple.init();
258259

259260
if (this._parentFormField) {
260261
_parentFormField.elementRef.nativeElement.classList.add('mdc-form-field');
261262
}
262263
}
263264

265+
async _asyncBuildFoundation(): Promise<void> {
266+
this._foundation = this.getDefaultFoundation();
267+
}
268+
264269
ngAfterViewInit(): void {
265-
this._foundation.init();
270+
this._initialized = true;
271+
272+
this._asyncBuildFoundation()
273+
.then(() => {
274+
this._foundation.init();
275+
this.setDisabledState(this._inputElement.nativeElement.disabled);
276+
277+
this.ripple = this._createRipple();
278+
this.ripple.init();
279+
});
266280
this._loadListeners();
267281
}
268282

@@ -314,9 +328,13 @@ export class MdcCheckbox extends MDCComponent<MDCCheckboxFoundation> implements
314328
}
315329

316330
setDisabledState(disabled: boolean): void {
317-
this._disabled = coerceBooleanProperty(disabled);
318-
this._foundation.setDisabled(this._disabled);
319-
this._changeDetectorRef.markForCheck();
331+
const newValue = coerceBooleanProperty(disabled);
332+
333+
if (newValue !== this._disabled) {
334+
this._disabled = newValue;
335+
this._foundation?.setDisabled(newValue);
336+
this._changeDetectorRef.markForCheck();
337+
}
320338
}
321339

322340
private _setState(checked?: boolean): void {
@@ -344,7 +362,11 @@ export class MdcCheckbox extends MDCComponent<MDCCheckboxFoundation> implements
344362
...MdcRipple.createAdapter(this),
345363
isSurfaceActive: () => matches(this._inputElement.nativeElement, ':active'),
346364
isUnbounded: () => true,
347-
isSurfaceDisabled: () => !this._disableRipple
365+
isSurfaceDisabled: () => this.disableRipple,
366+
deregisterInteractionHandler: (evtType: any, handler: any) =>
367+
this._inputElement.nativeElement.removeEventListener(evtType, handler, supportsPassiveEventListeners()),
368+
registerInteractionHandler: (evtType: any, handler: any) =>
369+
this._inputElement.nativeElement.addEventListener(evtType, handler, supportsPassiveEventListeners()),
348370
};
349371
return new MdcRipple(this.elementRef, new MDCRippleFoundation(adapter));
350372
}
@@ -355,15 +377,10 @@ export class MdcCheckbox extends MDCComponent<MDCCheckboxFoundation> implements
355377
}
356378

357379
this._ngZone.runOutsideAngular(() =>
358-
fromEvent<AnimationEvent>(this._getHostElement(), 'animationend')
380+
fromEvent<AnimationEvent>(this._root, 'animationend')
359381
.pipe(takeUntil(this._destroy), filter((e: AnimationEvent) =>
360-
e.target === this._getHostElement()))
382+
e.target === this._root))
361383
.subscribe(() =>
362384
this._ngZone.run(() => this._foundation.handleAnimationEnd())));
363385
}
364-
365-
/** Retrieves the DOM element of the component host. */
366-
private _getHostElement(): HTMLElement {
367-
return this.elementRef.nativeElement;
368-
}
369386
}

test/checkbox/checkbox.test.ts

Lines changed: 61 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,21 @@
11
import {Component, DebugElement} from '@angular/core';
2-
import {ComponentFixture, fakeAsync, flushMicrotasks, TestBed} from '@angular/core/testing';
2+
import {async, ComponentFixture, fakeAsync, flushMicrotasks, TestBed} from '@angular/core/testing';
33
import {FormsModule, NgModel, ReactiveFormsModule} from '@angular/forms';
44
import {By} from '@angular/platform-browser';
5+
import {Platform} from '@angular/cdk/platform';
56

67
import {dispatchFakeEvent, dispatchMouseEvent} from '../testing/dispatch-events';
78

89
import {MdcCheckbox, MdcCheckboxModule} from '@angular-mdc/web';
910

1011
describe('MdcCheckbox', () => {
1112
let fixture: ComponentFixture<any>;
13+
let platform: {isBrowser: boolean};
1214

1315
beforeEach(fakeAsync(() => {
16+
// Set the default Platform override that can be updated before component creation.
17+
platform = {isBrowser: true};
18+
1419
TestBed.configureTestingModule({
1520
imports: [MdcCheckboxModule, FormsModule, ReactiveFormsModule],
1621
declarations: [
@@ -19,11 +24,60 @@ describe('MdcCheckbox', () => {
1924
CheckboxWithAriaLabel,
2025
CheckboxWithTabIndex,
2126
CheckboxWithFormDirectives,
27+
DisabledCheckbox,
28+
],
29+
providers: [
30+
{provide: Platform, useFactory: () => platform}
2231
]
2332
});
2433
TestBed.compileComponents();
2534
}));
2635

36+
describe('Tests for SSR', () => {
37+
let testDebugElement: DebugElement;
38+
let testInstance: MdcCheckbox;
39+
let testComponent: SingleCheckbox;
40+
41+
beforeEach(() => {
42+
// Set the default Platform override that can be updated before component creation.
43+
platform = {isBrowser: false};
44+
45+
fixture = TestBed.createComponent(SingleCheckbox);
46+
fixture.detectChanges();
47+
48+
testDebugElement = fixture.debugElement.query(By.directive(MdcCheckbox));
49+
testInstance = testDebugElement.componentInstance;
50+
testComponent = fixture.debugElement.componentInstance;
51+
});
52+
53+
it('#should have mdc-checkbox by default', () => {
54+
expect(testDebugElement.nativeElement.classList)
55+
.toContain('mdc-checkbox', 'Expected to have mdc-checkbox');
56+
});
57+
});
58+
59+
describe('disabled checkbox', () => {
60+
let testDebugElement: DebugElement;
61+
let testNativeElement: HTMLElement;
62+
let testInstance: MdcCheckbox;
63+
let testComponent: DisabledCheckbox;
64+
65+
beforeEach(async(() => {
66+
fixture = TestBed.createComponent(DisabledCheckbox);
67+
fixture.detectChanges();
68+
69+
testDebugElement = fixture.debugElement.query(By.directive(MdcCheckbox));
70+
testNativeElement = testDebugElement.nativeElement;
71+
testInstance = testDebugElement.componentInstance;
72+
testComponent = fixture.debugElement.componentInstance;
73+
}));
74+
75+
it('#should be disabled', () => {
76+
fixture.detectChanges();
77+
expect(testInstance._inputElement.nativeElement.disabled).toBe(true);
78+
});
79+
});
80+
2781
describe('basic behaviors', () => {
2882
let checkboxDebugElement: DebugElement;
2983
let checkboxNativeElement: HTMLElement;
@@ -194,10 +248,6 @@ describe('MdcCheckbox', () => {
194248
expect(checkboxInstance.indeterminate).toBe(true);
195249
});
196250

197-
it('expect disableRipple to be false', () => {
198-
expect(testComponent.disableRipple).toBe(false);
199-
});
200-
201251
it('expect disableRipple to be true', () => {
202252
testComponent.disableRipple = true;
203253
fixture.detectChanges();
@@ -348,7 +398,7 @@ class SingleCheckbox {
348398
indeterminate: boolean;
349399
checkboxValue: boolean = false;
350400
indeterminateToChecked: boolean = true;
351-
disableRipple: boolean = false;
401+
disableRipple: boolean;
352402
touch: boolean = false;
353403
}
354404

@@ -388,3 +438,8 @@ class CheckboxWithTabIndex {
388438
class CheckboxWithFormDirectives {
389439
isGood: boolean = false;
390440
}
441+
442+
@Component({
443+
template: `<mdc-checkbox disabled></mdc-checkbox>`,
444+
})
445+
class DisabledCheckbox {}

test/select/select.test.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import {
99
import {async, fakeAsync, ComponentFixture, TestBed, flush} from '@angular/core/testing';
1010
import {By} from '@angular/platform-browser';
1111
import {DOWN_ARROW} from '@angular/cdk/keycodes';
12+
import {Platform} from '@angular/cdk/platform';
1213

1314
import {dispatchKeyboardEvent, dispatchMouseEvent} from '../testing/dispatch-events';
1415

@@ -19,6 +20,11 @@ import {
1920
} from '@angular-mdc/web';
2021

2122
function configureMdcTestingModule(declarations: any[], providers: Provider[] = []) {
23+
let platform: {isBrowser: boolean};
24+
25+
// Set the default Platform override that can be updated before component creation.
26+
platform = {isBrowser: true};
27+
2228
TestBed.configureTestingModule({
2329
imports: [
2430
MdcSelectModule,
@@ -27,6 +33,7 @@ function configureMdcTestingModule(declarations: any[], providers: Provider[] =
2733
],
2834
declarations: declarations,
2935
providers: [
36+
{provide: Platform, useFactory: () => platform},
3037
...providers
3138
],
3239
}).compileComponents();

test/textfield/textfield.test.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import {Component, DebugElement, Provider, ViewChild, Type} from '@angular/core'
22
import {async, ComponentFixture, fakeAsync, TestBed, flush, tick} from '@angular/core/testing';
33
import {FormsModule} from '@angular/forms';
44
import {By} from '@angular/platform-browser';
5+
import {Platform} from '@angular/cdk/platform';
56

67
import {
78
dispatchFakeEvent,
@@ -17,6 +18,11 @@ import {
1718
} from '@angular-mdc/web';
1819

1920
function configureMdcTestingModule(declarations: any[], providers: Provider[] = []) {
21+
let platform: {isBrowser: boolean};
22+
23+
// Set the default Platform override that can be updated before component creation.
24+
platform = {isBrowser: true};
25+
2026
TestBed.configureTestingModule({
2127
imports: [
2228
MdcTextFieldModule,
@@ -25,6 +31,7 @@ function configureMdcTestingModule(declarations: any[], providers: Provider[] =
2531
],
2632
declarations: declarations,
2733
providers: [
34+
{provide: Platform, useFactory: () => platform},
2835
...providers
2936
],
3037
}).compileComponents();

0 commit comments

Comments
 (0)