Skip to content

Commit 150d5eb

Browse files
Christian862Christian Quinn
andauthored
feat: Skeleton loader for LoadAsync directive (#1373)
* feat: added skeleton component * fix: integrating skeleton component with loader * style: adding loader types and fixing display * feat: added skeleton shapes and styling * style: minor style adjustments to skeletons * feat: added donut skeleton shape * fix: default isOldLoaderFlag to true * test: updated for loader component changes * test: skeleton component testing Co-authored-by: Christian Quinn <christian@FewGoodTaters.local>
1 parent f9af505 commit 150d5eb

File tree

9 files changed

+489
-8
lines changed

9 files changed

+489
-8
lines changed

projects/components/src/load-async/load-async.module.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,14 @@ import { CommonModule } from '@angular/common';
22
import { NgModule } from '@angular/core';
33
import { IconModule } from '../icon/icon.module';
44
import { MessageDisplayModule } from '../message-display/message-display.module';
5+
import { SkeletonModule } from '../skeleton/skeleton.module';
56
import { LoadAsyncDirective } from './load-async.directive';
67
import { LoaderComponent } from './loader/loader.component';
78
import { LoadAsyncWrapperComponent } from './wrapper/load-async-wrapper.component';
89

910
@NgModule({
1011
declarations: [LoadAsyncDirective, LoadAsyncWrapperComponent, LoaderComponent],
11-
imports: [CommonModule, IconModule, MessageDisplayModule],
12+
imports: [CommonModule, IconModule, MessageDisplayModule, SkeletonModule],
1213
exports: [LoadAsyncDirective]
1314
})
1415
export class LoadAsyncModule {}

projects/components/src/load-async/load-async.service.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,14 @@ export type AsyncState = LoadingAsyncState | SuccessAsyncState | NoDataOrErrorAs
6262
export const enum LoaderType {
6363
Spinner = 'spinner',
6464
ExpandableRow = 'expandable-row',
65-
Page = 'page'
65+
Page = 'page',
66+
Rectangle = 'rectangle',
67+
Text = 'text',
68+
Square = 'square',
69+
Circle = 'circle',
70+
TableRow = 'table-row',
71+
ListItem = 'list-item',
72+
Donut = 'donut'
6673
}
6774

6875
interface LoadingAsyncState {

projects/components/src/load-async/loader/loader.component.scss

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,6 @@
33
.ht-loader {
44
width: 100%;
55
height: 100%;
6-
display: flex;
7-
flex-direction: column;
8-
justify-content: center;
9-
align-items: center;
106

117
.page {
128
height: 50px;
@@ -23,3 +19,10 @@
2319
width: auto;
2420
}
2521
}
22+
23+
.flex-centered {
24+
display: flex;
25+
flex-direction: column;
26+
justify-content: center;
27+
align-items: center;
28+
}

projects/components/src/load-async/loader/loader.component.test.ts

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
import { CommonModule } from '@angular/common';
22
import { ImagesAssetPath } from '@hypertrace/assets-library';
33
import { createHostFactory, SpectatorHost } from '@ngneat/spectator/jest';
4+
import { MockComponent } from 'ng-mocks';
5+
import { SkeletonComponent, SkeletonType } from '../../skeleton/skeleton.component';
46
import { LoaderType } from '../load-async.service';
57
import { LoaderComponent } from './loader.component';
68

@@ -9,13 +11,15 @@ describe('Loader component', () => {
911

1012
const createHost = createHostFactory({
1113
component: LoaderComponent,
14+
declarations: [MockComponent(SkeletonComponent)],
1215
imports: [CommonModule]
1316
});
1417

1518
test('Loader component when loader type is page', () => {
1619
spectator = createHost(`<ht-loader [loaderType]="'${LoaderType.Page}'"></ht-loader>`);
1720

1821
expect(spectator.query('.ht-loader')).toExist();
22+
expect(spectator.query('.ht-loader')).toHaveClass('flex-centered');
1923
expect(spectator.query('.ht-loader img')).toExist();
2024
expect(spectator.query('.ht-loader img')).toHaveClass(LoaderType.Page);
2125
expect(spectator.query('.ht-loader img')).toHaveAttribute('src', ImagesAssetPath.LoaderPage);
@@ -25,6 +29,7 @@ describe('Loader component', () => {
2529
spectator = createHost(`<ht-loader></ht-loader>`);
2630

2731
expect(spectator.query('.ht-loader')).toExist();
32+
expect(spectator.query('.ht-loader')).toHaveClass('flex-centered');
2833
expect(spectator.query('.ht-loader img')).toExist();
2934
expect(spectator.query('.ht-loader img')).toHaveClass(LoaderType.Spinner);
3035
expect(spectator.query('.ht-loader img')).toHaveAttribute('src', ImagesAssetPath.LoaderSpinner);
@@ -34,6 +39,7 @@ describe('Loader component', () => {
3439
spectator = createHost(`<ht-loader [loaderType]="'${LoaderType.Spinner}'"></ht-loader>`);
3540

3641
expect(spectator.query('.ht-loader')).toExist();
42+
expect(spectator.query('.ht-loader')).toHaveClass('flex-centered');
3743
expect(spectator.query('.ht-loader img')).toExist();
3844
expect(spectator.query('.ht-loader img')).toHaveClass(LoaderType.Spinner);
3945
expect(spectator.query('.ht-loader img')).toHaveAttribute('src', ImagesAssetPath.LoaderSpinner);
@@ -43,8 +49,93 @@ describe('Loader component', () => {
4349
spectator = createHost(`<ht-loader [loaderType]="'${LoaderType.ExpandableRow}'"></ht-loader>`);
4450

4551
expect(spectator.query('.ht-loader')).toExist();
52+
expect(spectator.query('.ht-loader')).toHaveClass('flex-centered');
4653
expect(spectator.query('.ht-loader img')).toExist();
4754
expect(spectator.query('.ht-loader img')).toHaveClass(LoaderType.ExpandableRow);
4855
expect(spectator.query('.ht-loader img')).toHaveAttribute('src', ImagesAssetPath.LoaderExpandableRow);
4956
});
57+
58+
test('Should use old loader type by default', () => {
59+
spectator = createHost(`<ht-loader></ht-loader>`);
60+
61+
expect(spectator.component.isOldLoaderType).toBe(true);
62+
expect(spectator.query(SkeletonComponent)).not.toExist();
63+
});
64+
65+
test('Should use corresponding skeleton component for loader type rectangle', () => {
66+
spectator = createHost(`<ht-loader [loaderType]="'${LoaderType.Rectangle}'" ></ht-loader>`);
67+
68+
expect(spectator.query('.ht-loader')).toExist();
69+
expect(spectator.query('.ht-loader')).not.toHaveClass('flex-centered');
70+
71+
const skeletonComponent = spectator.query(SkeletonComponent);
72+
expect(skeletonComponent).toExist();
73+
expect(skeletonComponent).toHaveAttribute('skeletonType', SkeletonType.Rectangle);
74+
});
75+
76+
test('Should use corresponding skeleton component for loader type rectangle text', () => {
77+
spectator = createHost(`<ht-loader [loaderType]="'${LoaderType.Text}'" ></ht-loader>`);
78+
79+
expect(spectator.query('.ht-loader')).toExist();
80+
expect(spectator.query('.ht-loader')).not.toHaveClass('flex-centered');
81+
82+
const skeletonComponent = spectator.query(SkeletonComponent);
83+
expect(skeletonComponent).toExist();
84+
expect(skeletonComponent).toHaveAttribute('skeletonType', SkeletonType.Text);
85+
});
86+
87+
test('Should use corresponding skeleton component for loader type circle', () => {
88+
spectator = createHost(`<ht-loader [loaderType]="'${LoaderType.Circle}'" ></ht-loader>`);
89+
90+
expect(spectator.query('.ht-loader')).toExist();
91+
expect(spectator.query('.ht-loader')).not.toHaveClass('flex-centered');
92+
93+
const skeletonComponent = spectator.query(SkeletonComponent);
94+
expect(skeletonComponent).toExist();
95+
expect(skeletonComponent).toHaveAttribute('skeletonType', SkeletonType.Circle);
96+
});
97+
98+
test('Should use corresponding skeleton component for loader type square', () => {
99+
spectator = createHost(`<ht-loader [loaderType]="'${LoaderType.Square}'" ></ht-loader>`);
100+
101+
expect(spectator.query('.ht-loader')).toExist();
102+
expect(spectator.query('.ht-loader')).not.toHaveClass('flex-centered');
103+
104+
const skeletonComponent = spectator.query(SkeletonComponent);
105+
expect(skeletonComponent).toExist();
106+
expect(skeletonComponent).toHaveAttribute('skeletonType', SkeletonType.Square);
107+
});
108+
109+
test('Should use corresponding skeleton component for loader type table row', () => {
110+
spectator = createHost(`<ht-loader [loaderType]="'${LoaderType.TableRow}'" ></ht-loader>`);
111+
112+
expect(spectator.query('.ht-loader')).toExist();
113+
expect(spectator.query('.ht-loader')).not.toHaveClass('flex-centered');
114+
115+
const skeletonComponent = spectator.query(SkeletonComponent);
116+
expect(skeletonComponent).toExist();
117+
expect(skeletonComponent).toHaveAttribute('skeletonType', SkeletonType.TableRow);
118+
});
119+
120+
test('Should use corresponding skeleton component for loader type donut', () => {
121+
spectator = createHost(`<ht-loader [loaderType]="'${LoaderType.Donut}'" ></ht-loader>`);
122+
123+
expect(spectator.query('.ht-loader')).toExist();
124+
expect(spectator.query('.ht-loader')).not.toHaveClass('flex-centered');
125+
126+
const skeletonComponent = spectator.query(SkeletonComponent);
127+
expect(skeletonComponent).toExist();
128+
expect(skeletonComponent).toHaveAttribute('skeletonType', SkeletonType.Donut);
129+
});
130+
131+
test('Should use corresponding skeleton component for loader type list item', () => {
132+
spectator = createHost(`<ht-loader [loaderType]="'${LoaderType.ListItem}'" ></ht-loader>`);
133+
134+
expect(spectator.query('.ht-loader')).toExist();
135+
expect(spectator.query('.ht-loader')).not.toHaveClass('flex-centered');
136+
137+
const skeletonComponent = spectator.query(SkeletonComponent);
138+
expect(skeletonComponent).toExist();
139+
expect(skeletonComponent).toHaveAttribute('skeletonType', SkeletonType.ListItem);
140+
});
50141
});

projects/components/src/load-async/loader/loader.component.ts

Lines changed: 62 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,66 @@
11
import { ChangeDetectionStrategy, Component, Input, OnChanges } from '@angular/core';
22
import { ImagesAssetPath } from '@hypertrace/assets-library';
3+
import { assertUnreachable } from '@hypertrace/common';
4+
import { SkeletonType } from '../../skeleton/skeleton.component';
35
import { LoaderType } from '../load-async.service';
46

57
@Component({
68
selector: 'ht-loader',
79
styleUrls: ['./loader.component.scss'],
810
changeDetection: ChangeDetectionStrategy.OnPush,
911
template: `
10-
<div class="ht-loader">
11-
<img [ngClass]="[this.currentLoaderType]" [src]="this.getImagePathFromType(this.currentLoaderType)" />
12+
<div class="ht-loader" [ngClass]="{ 'flex-centered': this.isOldLoaderType }">
13+
<ng-container *ngIf="!this.isOldLoaderType; else oldLoaderTemplate">
14+
<ht-skeleton [skeletonType]="this.skeletonType"></ht-skeleton>
15+
</ng-container>
16+
17+
<ng-template #oldLoaderTemplate>
18+
<img [ngClass]="[this.currentLoaderType]" [src]="this.imagePath" />
19+
</ng-template>
1220
</div>
1321
`
1422
})
1523
export class LoaderComponent implements OnChanges {
1624
@Input()
1725
public loaderType?: LoaderType;
1826

27+
public skeletonType: SkeletonType = SkeletonType.Rectangle;
28+
1929
public currentLoaderType: LoaderType = LoaderType.Spinner;
2030

31+
public imagePath: ImagesAssetPath = ImagesAssetPath.LoaderSpinner;
32+
33+
public isOldLoaderType: boolean = true;
34+
2135
public ngOnChanges(): void {
2236
this.currentLoaderType = this.loaderType ?? LoaderType.Spinner;
37+
38+
if (this.determineIfOldLoaderType(this.currentLoaderType)) {
39+
this.isOldLoaderType = true;
40+
this.imagePath = this.getImagePathFromType(this.currentLoaderType);
41+
} else {
42+
this.isOldLoaderType = false;
43+
this.skeletonType = this.getSkeletonTypeForLoader(this.currentLoaderType);
44+
}
45+
}
46+
47+
public determineIfOldLoaderType(loaderType: LoaderType): boolean {
48+
switch (loaderType) {
49+
case LoaderType.Spinner:
50+
case LoaderType.ExpandableRow:
51+
case LoaderType.Page:
52+
return true;
53+
case LoaderType.Circle:
54+
case LoaderType.Text:
55+
case LoaderType.ListItem:
56+
case LoaderType.Rectangle:
57+
case LoaderType.Square:
58+
case LoaderType.TableRow:
59+
case LoaderType.Donut:
60+
return false;
61+
default:
62+
return assertUnreachable(loaderType);
63+
}
2364
}
2465

2566
public getImagePathFromType(loaderType: LoaderType): ImagesAssetPath {
@@ -33,4 +74,23 @@ export class LoaderComponent implements OnChanges {
3374
return ImagesAssetPath.LoaderSpinner;
3475
}
3576
}
77+
78+
public getSkeletonTypeForLoader(curLoaderType: LoaderType): SkeletonType {
79+
switch (curLoaderType) {
80+
case LoaderType.Text:
81+
return SkeletonType.Text;
82+
case LoaderType.Circle:
83+
return SkeletonType.Circle;
84+
case LoaderType.Square:
85+
return SkeletonType.Square;
86+
case LoaderType.TableRow:
87+
return SkeletonType.TableRow;
88+
case LoaderType.ListItem:
89+
return SkeletonType.ListItem;
90+
case LoaderType.Donut:
91+
return SkeletonType.Donut;
92+
default:
93+
return SkeletonType.Rectangle;
94+
}
95+
}
3696
}

0 commit comments

Comments
 (0)