fix: performance degradation when scrolling

This commit is contained in:
Rainer Killinger
2021-05-04 11:33:48 +02:00
parent add690c842
commit f0a45d1b8e
15 changed files with 607 additions and 392 deletions

View File

@@ -12,6 +12,7 @@
* You should have received a copy of the GNU General Public License along with
* this program. If not, see <https://www.gnu.org/licenses/>.
*/
import {ScrollingModule} from '@angular/cdk/scrolling';
import {CommonModule} from '@angular/common';
import {HttpClientModule} from '@angular/common/http';
import {NgModule} from '@angular/core';
@@ -135,6 +136,7 @@ import {VideoListItem} from './types/video/video-list-item.component';
'm': 59,
},
}),
ScrollingModule,
StorageModule,
TranslateModule.forChild(),
ThingTranslateModule.forChild(),

View File

@@ -20,6 +20,7 @@ import {Component} from '@angular/core';
@Component({
selector: 'stapps-skeleton-list-item',
templateUrl: 'skeleton-list-item.html',
styleUrls: ['skeleton-list-item.scss'],
})
export class SkeletonListItem {
}

View File

@@ -1,7 +1,7 @@
<ion-item>
<ion-thumbnail slot="start">
<ion-skeleton-text animated></ion-skeleton-text>
</ion-thumbnail>
<ion-thumbnail slot="start" class="ion-margin-end">
<ion-skeleton-text animated></ion-skeleton-text>
</ion-thumbnail>
<ion-grid>
<ion-row>
<ion-col>

View File

@@ -0,0 +1,7 @@
::ng-deep {
ion-grid,ion-col {
padding-inline-start: 0px!important;
padding-top: 0px!important;
padding-bottom: 0px!important;
}
}

View File

@@ -25,12 +25,17 @@ import {DataRoutingService} from '../data-routing.service';
templateUrl: 'data-list-item.html',
})
export class DataListItem {
/**
* Whether or not the list item should show a thumbnail
*/
@Input() hideThumbnail = false;
/**
* An item to show
*/
@Input() item: SCThings;
constructor(private dataRoutingService: DataRoutingService) {}
constructor(private readonly dataRoutingService: DataRoutingService) {}
/**
* Emit event that an item was selected

View File

@@ -1,37 +1,30 @@
<ion-item class="ion-text-wrap" button="true" lines="inset" (click)="notifySelect()">
<ion-thumbnail slot="start">
<ion-item button="true" lines="inset" (click)="notifySelect()">
<ion-thumbnail slot="start" *ngIf="!hideThumbnail" class="ion-margin-end">
<ion-icon color="medium" [attr.name]="item.type | dataIcon"></ion-icon>
</ion-thumbnail>
<ion-label [ngSwitch]="true">
<div>
<stapps-catalog-list-item [item]="item" *ngSwitchCase="item.type === 'catalog'"></stapps-catalog-list-item>
<stapps-date-series-list-item [item]="item" *ngSwitchCase="item.type === 'date series'"></stapps-date-series-list-item>
<stapps-dish-list-item [item]="item" *ngSwitchCase="item.type === 'dish'"></stapps-dish-list-item>
<stapps-event-list-item [item]="item" *ngSwitchCase="item.type === 'academic event'"></stapps-event-list-item>
<stapps-event-list-item [item]="item" *ngSwitchCase="item.type === 'sport course'"></stapps-event-list-item>
<stapps-favorite-list-item [item]="item" *ngSwitchCase="item.type === 'favorite'"></stapps-favorite-list-item>
<stapps-message-list-item [item]="item" *ngSwitchCase="item.type === 'message'"></stapps-message-list-item>
<stapps-organization-list-item [item]="item" *ngSwitchCase="item.type === 'organization'"></stapps-organization-list-item>
<stapps-person-list-item [item]="item" *ngSwitchCase="item.type === 'person'"></stapps-person-list-item>
<stapps-place-list-item [item]="item" *ngSwitchCase="item.type === 'building'"></stapps-place-list-item>
<stapps-place-list-item [item]="item" *ngSwitchCase="item.type === 'floor'"></stapps-place-list-item>
<stapps-place-list-item [item]="item" *ngSwitchCase="item.type === 'point of interest'"></stapps-place-list-item>
<stapps-place-list-item [item]="item" *ngSwitchCase="item.type === 'room'"></stapps-place-list-item>
<stapps-semester-list-item [item]="item" *ngSwitchCase="item.type === 'semester'"></stapps-semester-list-item>
<stapps-video-list-item [item]="item" *ngSwitchCase="item.type === 'video'"></stapps-video-list-item>
<ion-grid *ngSwitchDefault>
<ion-row>
<ion-col>
<div class="ion-text-wrap">
<h2 class="name">{{'name' | thingTranslate: item}}</h2>
<p *ngIf="item.description">
<stapps-long-inline-text [text]="'description' | thingTranslate: item" [size]="80"></stapps-long-inline-text>
</p>
<ion-note>{{'type' | thingTranslate: item}}</ion-note>
</div>
</ion-col>
</ion-row>
</ion-grid>
<ion-label class="ion-text-wrap" [ngSwitch]="true">
<stapps-catalog-list-item [item]="item" *ngSwitchCase="item.type === 'catalog'"></stapps-catalog-list-item>
<stapps-date-series-list-item [item]="item" *ngSwitchCase="item.type === 'date series'"></stapps-date-series-list-item>
<stapps-dish-list-item [item]="item" *ngSwitchCase="item.type === 'dish'"></stapps-dish-list-item>
<stapps-event-list-item [item]="item" *ngSwitchCase="item.type === 'academic event'"></stapps-event-list-item>
<stapps-event-list-item [item]="item" *ngSwitchCase="item.type === 'sport course'"></stapps-event-list-item>
<stapps-favorite-list-item [item]="item" *ngSwitchCase="item.type === 'favorite'"></stapps-favorite-list-item>
<stapps-message-list-item [item]="item" *ngSwitchCase="item.type === 'message'"></stapps-message-list-item>
<stapps-organization-list-item [item]="item" *ngSwitchCase="item.type === 'organization'"></stapps-organization-list-item>
<stapps-person-list-item [item]="item" *ngSwitchCase="item.type === 'person'"></stapps-person-list-item>
<stapps-place-list-item [item]="item" *ngSwitchCase="item.type === 'building'"></stapps-place-list-item>
<stapps-place-list-item [item]="item" *ngSwitchCase="item.type === 'floor'"></stapps-place-list-item>
<stapps-place-list-item [item]="item" *ngSwitchCase="item.type === 'point of interest'"></stapps-place-list-item>
<stapps-place-list-item [item]="item" *ngSwitchCase="item.type === 'room'"></stapps-place-list-item>
<stapps-semester-list-item [item]="item" *ngSwitchCase="item.type === 'semester'"></stapps-semester-list-item>
<stapps-video-list-item [item]="item" *ngSwitchCase="item.type === 'video'"></stapps-video-list-item>
<div *ngSwitchDefault>
<h2>
{{'name' | thingTranslate: item}}
</h2>
<p *ngIf="item.description">
<stapps-long-inline-text [text]="'description' | thingTranslate: item" [size]="80"></stapps-long-inline-text>
</p>
</div>
</ion-label>
</ion-item>

View File

@@ -0,0 +1,8 @@
::ng-deep {
ion-grid,ion-col {
padding-inline-start: 0px!important;
padding-top: 0px!important;
padding-bottom: 0px!important;
}
}

View File

@@ -1,6 +1,7 @@
import {async, ComponentFixture, TestBed} from '@angular/core/testing';
import {DataListComponent} from './data-list.component';
import {TranslateModule} from '@ngx-translate/core';
describe('DataListComponent', () => {
let component: DataListComponent;
@@ -8,7 +9,10 @@ describe('DataListComponent', () => {
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [ DataListComponent ]
declarations: [ DataListComponent ],
imports: [
TranslateModule.forRoot(),
]
})
.compileComponents();
}));

View File

@@ -12,8 +12,11 @@
* You should have received a copy of the GNU General Public License along with
* this program. If not, see <https://www.gnu.org/licenses/>.
*/
import {Component, EventEmitter, Input, OnInit, Output} from '@angular/core';
import {CdkVirtualScrollViewport} from '@angular/cdk/scrolling';
import {Component, EventEmitter, Input, OnChanges, OnInit, Output, SimpleChanges, ViewChild} from '@angular/core';
import {SCThings} from '@openstapps/core';
import {BehaviorSubject, Observable, Subscription} from 'rxjs';
/**
* Shows the list of items
*/
@@ -22,16 +25,84 @@ import {SCThings} from '@openstapps/core';
templateUrl: 'data-list.html',
styleUrls: ['data-list.scss'],
})
export class DataListComponent implements OnInit {
// tslint:disable-next-line:completed-docs
@Input() items: SCThings[];
// tslint:disable-next-line:completed-docs
@Output('loadmore') loadMore = new EventEmitter<Event>();
ngOnInit(): void {
export class DataListComponent implements OnChanges, OnInit {
/**
* Amount of list items left to show (in percent) that should trigger a data reload
*/
private readonly reloadThreshold = 0.2;
/**
* All SCThings to display
*/
@Input() items?: SCThings[];
/**
* Stream of SCThings for virtual scroll to consume
*/
itemStream = new BehaviorSubject<SCThings[]>([]);
/**
* Output binding to trigger pagination fetch
*/
@Output('loadmore') loadMore = new EventEmitter<DataSourceRefreshed>();
/**
* Indicates whether or not the list is to display SCThings of a single type
*/
@Input() singleType = false;
// tslint:disable-next-line: completed-docs
@ViewChild(CdkVirtualScrollViewport) viewPort: CdkVirtualScrollViewport;
/**
* Uniquely identifies item at a certain list index
*/
identifyItem(_index: number, item: SCThings) {
return item.uid;
}
notifyLoadMore(e: Event) {
this.loadMore.emit(e);
// tslint:disable-next-line: completed-docs
ngOnChanges(_changes: SimpleChanges): void {
if (Array.isArray(this.items)) {
if (this.itemStream.getValue().length === 0) {
this.itemStream = new BehaviorSubject<SCThings[]>(this.items);
} else {
if ((this.items[0].uid ?? '') !== this.itemStream.getValue()[0].uid) {
this.itemStream = new BehaviorSubject<SCThings[]>(this.items);
}
}
}
}
// tslint:disable-next-line: completed-docs
ngOnInit(): void {
if (typeof this.queryChanged !== 'undefined') {
this.subscriptions.push(this.queryChanged.subscribe(() => {
this.viewPort.scrollToIndex(0);
}));
}
}
// tslint:disable-next-line: completed-docs
ngOnDestroy(): void {
for (const subscription of this.subscriptions) {
subscription.unsubscribe();
}
}
/**
* Component proxy for dataSource.finishedLoadMore
*/
notifyLoadMore() {
this.loadMore.emit((items) => {
this.itemStream.next(items);
});
}
/**
* Function to call whenever scroll view visible range changed
*/
scrolled(_event: Event) {
if ((this.items?.length ?? 0) - this.viewPort.getRenderedRange().end <= (this.items?.length ?? 0) * this.reloadThreshold) {
this.notifyLoadMore();
}
}
}
export type DataSourceRefreshed = (items: SCThings[]) => unknown;

View File

@@ -1,20 +1,15 @@
<ion-content>
<div *ngIf="items; else loading">
<ion-list *ngIf='items.length > 0; else empty'>
<stapps-data-list-item [item]="item" *ngFor="let item of items"></stapps-data-list-item>
</ion-list>
<ng-template #empty>
<div>
<ion-label class='notFoundContainer'>
{{'search.nothing_found' | translate}}
</ion-label>
</div>
</ng-template>
</div>
<ng-template #loading>
<stapps-skeleton-list-item *ngFor="let skeleton of [1, 2, 3, 4, 5]"></stapps-skeleton-list-item>
</ng-template>
<ion-infinite-scroll (ionInfinite)="notifyLoadMore($event)">
<ion-infinite-scroll-content></ion-infinite-scroll-content>
</ion-infinite-scroll>
</ion-content>
<cdk-virtual-scroll-viewport itemSize="80" minBufferPx="1500" maxBufferPx="2000" (scrolledIndexChange)="scrolled($event)"
[style.display]="items && items.length ? 'block': 'none'">
<ion-list>
<stapps-data-list-item *cdkVirtualFor="let item of itemStream;trackBy: identifyItem" [item]="item"
[hideThumbnail]="singleType"></stapps-data-list-item>
</ion-list>
</cdk-virtual-scroll-viewport>
<div [style.display]="items && items.length === 0 ? 'block': 'none'">
<ion-label class='notFoundContainer'>
{{'search.nothing_found' | translate | titlecase}}
</ion-label>
</div>
<ion-list [style.display]="items ? 'none': 'block'">
<stapps-skeleton-list-item *ngFor="let skeleton of [1, 2, 3, 4, 5]"></stapps-skeleton-list-item>
</ion-list>

View File

@@ -0,0 +1,10 @@
cdk-virtual-scroll-viewport {
min-height: 100%;
width: 100%;
}
::ng-deep {
.cdk-virtual-scroll-content-wrapper {
width: 100%;
}
}

View File

@@ -29,6 +29,7 @@ import {ContextMenuService} from '../../menu/context/context-menu.service';
import {SettingsProvider} from '../../settings/settings.provider';
import {DataRoutingService} from '../data-routing.service';
import {DataProvider} from '../data.provider';
import {DataSourceRefreshed} from './data-list.component';
/**
* SearchPageComponent queries things and shows list of things as search results and filter as context menu
@@ -71,6 +72,10 @@ export class SearchPageComponent implements OnInit {
* Time to wait for search query if search text is changing
*/
searchQueryDueTime = 1000;
/**
* Search response only ever contains a single SCThingType
*/
singleTypeResponse = false;
/**
* Api query sorting
*/
@@ -183,6 +188,7 @@ export class SearchPageComponent implements OnInit {
return this.dataProvider.search(searchOptions)
.then(async (result) => {
this.singleTypeResponse = result.facets.find(facet => facet.field === 'type')?.buckets.length === 1;
if (append) {
let items = await this.items;
// append results
@@ -219,10 +225,10 @@ export class SearchPageComponent implements OnInit {
* Loads next page of things
*/
// tslint:disable-next-line:no-any
async loadMore(event: any): Promise<void> {
async loadMore(finished: DataSourceRefreshed): Promise<void> {
this.from += this.pageSize;
await this.fetchAndUpdateItems(true);
event.target.complete();
finished(await this.items);
}
/**

View File

@@ -1,4 +1,4 @@
<stapps-context contentId="data-list"></stapps-context>
<stapps-context></stapps-context>
<ion-header>
<ion-toolbar>
<ion-buttons slot="start">
@@ -15,5 +15,5 @@
</ion-header>
<ion-content>
<stapps-data-list id="data-list" [items]="items | async" (loadmore)="loadMore($event)"></stapps-data-list>
<stapps-data-list id="data-list" [items]="items | async" [singleType]="singleTypeResponse" (loadmore)="loadMore($event)"></stapps-data-list>
</ion-content>