Skip to content

Commit 2b02e14

Browse files
feat: on push strategy for tile components (#3192)
Co-authored-by: Akshat Patel <[email protected]>
1 parent 0b61296 commit 2b02e14

File tree

7 files changed

+187
-13
lines changed

7 files changed

+187
-13
lines changed

src/tiles/clickable-tile.component.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,8 @@ import {
33
Input,
44
Output,
55
EventEmitter,
6-
Optional
6+
Optional,
7+
ChangeDetectionStrategy
78
} from "@angular/core";
89
import { Router } from "@angular/router";
910

@@ -37,7 +38,8 @@ import { Router } from "@angular/router";
3738
[attr.rel]="rel ? rel : null"
3839
[attr.aria-disabled]="disabled">
3940
<ng-content />
40-
</a>`
41+
</a>`,
42+
changeDetection: ChangeDetectionStrategy.OnPush
4143
})
4244
export class ClickableTile {
4345
/**

src/tiles/expandable-tile.component.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,8 @@ import {
33
Input,
44
ElementRef,
55
AfterViewInit,
6-
ViewChild
6+
ViewChild,
7+
ChangeDetectionStrategy
78
} from "@angular/core";
89
import { I18n } from "carbon-components-angular/i18n";
910
import { merge } from "carbon-components-angular/utils";
@@ -80,7 +81,8 @@ export interface ExpandableTileTranslations {
8081
</div>
8182
</div>
8283
</ng-template>
83-
`
84+
`,
85+
changeDetection: ChangeDetectionStrategy.OnPush
8486
})
8587
export class ExpandableTile implements AfterViewInit {
8688
/**
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
import { ChangeDetectorRef } from "@angular/core";
2+
import { fakeAsync, tick } from "@angular/core/testing";
3+
import { I18n } from "carbon-components-angular/i18n";
4+
5+
import { SelectionTile } from "./selection-tile.component";
6+
7+
describe("SelectionTile", () => {
8+
let changeDetectorRef: jasmine.SpyObj<ChangeDetectorRef>;
9+
let component: SelectionTile;
10+
11+
beforeEach(() => {
12+
changeDetectorRef = jasmine.createSpyObj<ChangeDetectorRef>("ChangeDetectorRef", ["markForCheck"]);
13+
component = new SelectionTile(new I18n(), changeDetectorRef);
14+
component["input"] = {
15+
nativeElement: {
16+
checked: false
17+
}
18+
};
19+
});
20+
21+
it("marks for check when selected is changed programmatically", () => {
22+
component.selected = true;
23+
24+
expect(changeDetectorRef.markForCheck).toHaveBeenCalledTimes(1);
25+
expect(component.selected).toBeTrue();
26+
});
27+
28+
it("marks for check after the deferred sync in ngAfterViewInit", fakeAsync(() => {
29+
component.selected = true;
30+
changeDetectorRef.markForCheck.calls.reset();
31+
32+
component.ngAfterViewInit();
33+
tick();
34+
35+
expect(component["input"].nativeElement.checked).toBeTrue();
36+
expect(changeDetectorRef.markForCheck).toHaveBeenCalledTimes(1);
37+
}));
38+
});

src/tiles/selection-tile.component.ts

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,9 @@ import {
55
EventEmitter,
66
ViewChild,
77
HostListener,
8-
AfterViewInit
8+
AfterViewInit,
9+
ChangeDetectorRef,
10+
ChangeDetectionStrategy
911
} from "@angular/core";
1012
import { I18n } from "carbon-components-angular/i18n";
1113

@@ -40,7 +42,8 @@ import { I18n } from "carbon-components-angular/i18n";
4042
<div class="cds--tile-content">
4143
<ng-content />
4244
</div>
43-
</label>`
45+
</label>`,
46+
changeDetection: ChangeDetectionStrategy.OnPush
4447
})
4548
export class SelectionTile implements AfterViewInit {
4649
static tileCount = 0;
@@ -67,6 +70,7 @@ export class SelectionTile implements AfterViewInit {
6770
if (this.input) {
6871
this.input.nativeElement.checked = this._selected;
6972
}
73+
this.changeDetectorRef.markForCheck();
7074
}
7175

7276
get selected() {
@@ -105,14 +109,15 @@ export class SelectionTile implements AfterViewInit {
105109
// the value and check again when input exists in `AfterViewInit`.
106110
protected _selected = null;
107111

108-
constructor(public i18n: I18n) {
112+
constructor(public i18n: I18n, protected changeDetectorRef: ChangeDetectorRef) {
109113
SelectionTile.tileCount++;
110114
}
111115

112116
ngAfterViewInit() {
113117
if (this.input) {
114118
setTimeout(() => {
115119
this.input.nativeElement.checked = this._selected;
120+
this.changeDetectorRef.markForCheck();
116121
});
117122
}
118123
}
@@ -129,5 +134,3 @@ export class SelectionTile implements AfterViewInit {
129134
this.change.emit(event);
130135
}
131136
}
132-
133-
Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
import { Component, ViewChild, ViewChildren, QueryList } from "@angular/core";
2+
import { ComponentFixture, TestBed, fakeAsync, tick, waitForAsync } from "@angular/core/testing";
3+
import { By } from "@angular/platform-browser";
4+
5+
import { TilesModule, TileGroup, SelectionTile } from "./";
6+
7+
@Component({
8+
template: `
9+
<cds-tile-group [multiple]="multiple">
10+
<cds-selection-tile value="tile1" [selected]="true">First</cds-selection-tile>
11+
<cds-selection-tile value="tile2">Second</cds-selection-tile>
12+
<cds-selection-tile value="tile3">Third</cds-selection-tile>
13+
</cds-tile-group>
14+
`
15+
})
16+
class SingleSelectHostComponent {
17+
multiple = false;
18+
@ViewChild(TileGroup, { static: true }) group!: TileGroup;
19+
@ViewChildren(SelectionTile) tiles!: QueryList<SelectionTile>;
20+
}
21+
22+
describe("TileGroup", () => {
23+
describe("single select behaviour", () => {
24+
let fixture: ComponentFixture<SingleSelectHostComponent>;
25+
26+
beforeEach(waitForAsync(() => {
27+
TestBed.configureTestingModule({
28+
imports: [TilesModule],
29+
declarations: [SingleSelectHostComponent]
30+
}).compileComponents();
31+
}));
32+
33+
it("clears previous selection when another tile is chosen", fakeAsync(() => {
34+
fixture = TestBed.createComponent(SingleSelectHostComponent);
35+
fixture.detectChanges();
36+
tick();
37+
fixture.detectChanges();
38+
39+
let labels = fixture.debugElement.queryAll(By.css("label.cds--tile--selectable"));
40+
41+
expect(labels[0].nativeElement.classList).toContain("cds--tile--is-selected");
42+
expect(labels[1].nativeElement.classList).not.toContain("cds--tile--is-selected");
43+
expect(labels[2].nativeElement.classList).not.toContain("cds--tile--is-selected");
44+
45+
labels[1].nativeElement.click();
46+
fixture.detectChanges();
47+
tick();
48+
fixture.detectChanges();
49+
50+
labels = fixture.debugElement.queryAll(By.css("label.cds--tile--selectable"));
51+
52+
expect(labels[0].nativeElement.classList).not.toContain("cds--tile--is-selected");
53+
expect(labels[1].nativeElement.classList).toContain("cds--tile--is-selected");
54+
expect(labels[2].nativeElement.classList).not.toContain("cds--tile--is-selected");
55+
56+
const tiles = fixture.componentInstance.tiles.toArray();
57+
expect(tiles[0].selected).toBeFalse();
58+
expect(tiles[1].selected).toBeTrue();
59+
expect(tiles[2].selected).toBeFalse();
60+
61+
labels[2].nativeElement.click();
62+
fixture.detectChanges();
63+
tick();
64+
fixture.detectChanges();
65+
66+
labels = fixture.debugElement.queryAll(By.css("label.cds--tile--selectable"));
67+
68+
expect(labels[0].nativeElement.classList).not.toContain("cds--tile--is-selected");
69+
expect(labels[1].nativeElement.classList).not.toContain("cds--tile--is-selected");
70+
expect(labels[2].nativeElement.classList).toContain("cds--tile--is-selected");
71+
72+
expect(tiles[0].selected).toBeFalse();
73+
expect(tiles[1].selected).toBeFalse();
74+
expect(tiles[2].selected).toBeTrue();
75+
}));
76+
77+
it("keeps multiple selections when enabled", fakeAsync(() => {
78+
fixture = TestBed.createComponent(SingleSelectHostComponent);
79+
fixture.componentInstance.multiple = true;
80+
81+
fixture.detectChanges();
82+
tick();
83+
fixture.detectChanges();
84+
85+
const labels = fixture.debugElement.queryAll(By.css("label.cds--tile--selectable"));
86+
87+
labels[1].nativeElement.click();
88+
labels[2].nativeElement.click();
89+
fixture.detectChanges();
90+
tick();
91+
fixture.detectChanges();
92+
93+
const tiles = fixture.componentInstance.tiles.toArray();
94+
expect(tiles[0].selected).toBeTrue();
95+
expect(tiles[1].selected).toBeTrue();
96+
expect(tiles[2].selected).toBeTrue();
97+
98+
expect(labels[0].nativeElement.classList).toContain("cds--tile--is-selected");
99+
expect(labels[1].nativeElement.classList).toContain("cds--tile--is-selected");
100+
expect(labels[2].nativeElement.classList).toContain("cds--tile--is-selected");
101+
102+
// deselect the middle tile should not affect the others
103+
labels[1].nativeElement.click();
104+
fixture.detectChanges();
105+
tick();
106+
fixture.detectChanges();
107+
108+
expect(tiles[0].selected).toBeTrue();
109+
expect(tiles[1].selected).toBeFalse();
110+
expect(tiles[2].selected).toBeTrue();
111+
112+
expect(labels[0].nativeElement.classList).toContain("cds--tile--is-selected");
113+
expect(labels[1].nativeElement.classList).not.toContain("cds--tile--is-selected");
114+
expect(labels[2].nativeElement.classList).toContain("cds--tile--is-selected");
115+
}));
116+
});
117+
});

src/tiles/tile-group.component.ts

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,8 @@ import {
88
ContentChildren,
99
QueryList,
1010
OnDestroy,
11-
TemplateRef
11+
TemplateRef,
12+
ChangeDetectionStrategy
1213
} from "@angular/core";
1314
import { SelectionTile } from "./selection-tile.component";
1415
import { NG_VALUE_ACCESSOR } from "@angular/forms";
@@ -46,7 +47,8 @@ import { takeUntil } from "rxjs/operators";
4647
useExisting: TileGroup,
4748
multi: true
4849
}
49-
]
50+
],
51+
changeDetection: ChangeDetectionStrategy.OnPush
5052
})
5153
export class TileGroup implements AfterContentInit, OnDestroy {
5254
static tileGroupCount = 0;
@@ -103,6 +105,14 @@ export class TileGroup implements AfterContentInit, OnDestroy {
103105
tile.change
104106
.pipe(takeUntil(this.unsubscribeTiles$))
105107
.subscribe(() => {
108+
if (!this.multiple) {
109+
this.selectionTiles.forEach(otherTile => {
110+
if (otherTile !== tile) {
111+
otherTile.selected = false;
112+
}
113+
});
114+
tile.selected = true;
115+
}
106116
this.selected.emit({
107117
value: tile.value,
108118
selected: tile.selected,

src/tiles/tile.component.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
import {
22
Component,
33
HostBinding,
4-
Input
4+
Input,
5+
ChangeDetectionStrategy
56
} from "@angular/core";
67

78
/**
@@ -21,7 +22,8 @@ import {
2122
*/
2223
@Component({
2324
selector: "cds-tile, ibm-tile",
24-
template: `<ng-content />`
25+
template: `<ng-content />`,
26+
changeDetection: ChangeDetectionStrategy.OnPush
2527
})
2628
export class Tile {
2729
@HostBinding("class.cds--tile") tileClass = true;

0 commit comments

Comments
 (0)