feat(data): show skeleton screens before data is loaded

Closes #4
This commit is contained in:
Jovan Krunić
2019-04-24 09:11:24 +02:00
committed by Sebastian Lange
parent 88f87a2ce1
commit e1039aa226
10 changed files with 102 additions and 39 deletions

View File

@@ -17,7 +17,7 @@ import {HTTP_INTERCEPTORS, HttpClient,
import {Injectable} from '@angular/core'; import {Injectable} from '@angular/core';
import {SCIndexResponse, SCThingType} from '@openstapps/core'; import {SCIndexResponse, SCThingType} from '@openstapps/core';
import {Observable, of} from 'rxjs'; import {Observable, of} from 'rxjs';
import {map} from 'rxjs/operators'; import {map, delay} from 'rxjs/operators';
import {SampleThings} from './data/sample-things'; import {SampleThings} from './data/sample-things';
const sampleIndexResponse: SCIndexResponse = { const sampleIndexResponse: SCIndexResponse = {
@@ -150,12 +150,12 @@ export class FakeBackendInterceptor implements HttpInterceptor {
return this.sampleFetcher.getSampleThing(request.body.filter.arguments.value) return this.sampleFetcher.getSampleThing(request.body.filter.arguments.value)
.pipe(map((sampleData: any) => { .pipe(map((sampleData: any) => {
return new HttpResponse({status: 200, body: {data: sampleData}}); return new HttpResponse({status: 200, body: {data: sampleData}});
})); }), delay(1000)); // add delay for skeleton screens to be seen (see !16)
} }
} }
return this.sampleFetcher.getSampleThings().pipe(map((sampleData: any) => { return this.sampleFetcher.getSampleThings().pipe(map((sampleData: any) => {
return new HttpResponse({status: 200, body: {data: sampleData}}); return new HttpResponse({status: 200, body: {data: sampleData}});
})); }), delay(1000)); // add delay for skeleton screens to be seen (see !16)
} }
} }
return next.handle(request); return next.handle(request);

View File

@@ -60,6 +60,8 @@ import {SemesterDetailContentComponent} from './types/semester/semester-detail-c
import {SemesterListItem} from './types/semester/semester-list-item.component'; import {SemesterListItem} from './types/semester/semester-list-item.component';
import {VideoDetailContentComponent} from './types/video/video-detail-content.component'; import {VideoDetailContentComponent} from './types/video/video-detail-content.component';
import {VideoListItem} from './types/video/video-list-item.component'; import {VideoListItem} from './types/video/video-list-item.component';
import {SkeletonListItem} from './elements/skeleton-list-item.component';
import {SkeletonSimpleCard} from './elements/skeleton-simple-card.component';
@NgModule({ @NgModule({
declarations: [ declarations: [
@@ -69,6 +71,7 @@ import {VideoListItem} from './types/video/video-list-item.component';
ArticleDetailContentComponent, ArticleDetailContentComponent,
ArticleListItem, ArticleListItem,
SimpleCardComponent, SimpleCardComponent,
SkeletonSimpleCard,
CatalogDetailContentComponent, CatalogDetailContentComponent,
CatalogListItem, CatalogListItem,
DataDetailComponent, DataDetailComponent,
@@ -96,6 +99,7 @@ import {VideoListItem} from './types/video/video-list-item.component';
PlaceListItem, PlaceListItem,
SemesterDetailContentComponent, SemesterDetailContentComponent,
SemesterListItem, SemesterListItem,
SkeletonListItem,
VideoDetailContentComponent, VideoDetailContentComponent,
VideoListItem, VideoListItem,
], ],

View File

@@ -28,36 +28,37 @@ export class DataDetailComponent {
dataProvider: DataProvider; dataProvider: DataProvider;
item: SCThing; item: SCThing;
language: SCLanguageCode; language: SCLanguageCode;
constructor(private route: ActivatedRoute, dataProvider: DataProvider, translateService: TranslateService) { constructor(private route: ActivatedRoute, dataProvider: DataProvider, translateService: TranslateService) {
this.dataProvider = dataProvider; this.dataProvider = dataProvider;
this.language = translateService.currentLang as SCLanguageCode; this.language = translateService.currentLang as SCLanguageCode;
// alert(translateService.currentLang);
translateService.onLangChange.subscribe((event: LangChangeEvent) => { translateService.onLangChange.subscribe((event: LangChangeEvent) => {
this.language = event.lang as SCLanguageCode; this.language = event.lang as SCLanguageCode;
}); });
} }
/** /**
* Provides data item with given UID * Provides data item with given UID
* *
* @param uid Unique identifier of a thing * @param uid Unique identifier of a thing
*/ */
async getItem(uid: SCUuid): Promise<SCThing> { async getItem(uid: SCUuid): Promise<void> {
return (await this.dataProvider.get(uid, DataScope.Remote)); this.dataProvider.get(uid, DataScope.Remote).then((data) => {
this.item = data;
});
} }
async ngOnInit() { ngOnInit() {
this.item = await this.getItem(this.route.snapshot.paramMap.get('uid') || ''); this.getItem(this.route.snapshot.paramMap.get('uid') || '');
} }
/** /**
* Updates the shown thing * Updates the shown thing
* *
* @param refresher Refresher component the triggers the update * @param refresher Refresher component that triggers the update
*/ */
async refresh(refresher: IonRefresher) { async refresh(refresher: IonRefresher) {
this.item = await this.getItem(this.item.uid); await this.getItem(this.item.uid);
refresher.complete(); refresher.complete();
} }
} }

View File

@@ -7,12 +7,18 @@
<ion-title text-center>{{'data.detail.TITLE' | translate}}</ion-title> <ion-title text-center>{{'data.detail.TITLE' | translate}}</ion-title>
</ion-toolbar> </ion-toolbar>
</ion-header> </ion-header>
<ion-content padding *ngIf="item"> <ion-content padding>
<ion-refresher slot="fixed" (ionRefresh)="refresh($event.target)"> <ion-refresher slot="fixed" (ionRefresh)="refresh($event.target)">
<ion-refresher-content pullingIcon="arrow-dropdown" pullingText="{{'data.REFRESH_ACTION' | translate}}" <ion-refresher-content pullingIcon="arrow-dropdown" pullingText="{{'data.REFRESH_ACTION' | translate}}"
refreshingText="{{'data.REFRESHING' | translate}}"> refreshingText="{{'data.REFRESHING' | translate}}">
</ion-refresher-content> </ion-refresher-content>
</ion-refresher> </ion-refresher>
<stapps-data-list-item [item]="item"></stapps-data-list-item> <ng-container *ngIf="!item">
<stapps-data-detail-content [item]="item"></stapps-data-detail-content> <stapps-skeleton-list-item></stapps-skeleton-list-item>
<stapps-skeleton-simple-card></stapps-skeleton-simple-card>
</ng-container>
<ng-container *ngIf="item">
<stapps-data-list-item [item]="item"></stapps-data-list-item>
<stapps-data-detail-content [item]="item"></stapps-data-detail-content>
</ng-container>
</ion-content> </ion-content>

View File

@@ -0,0 +1,22 @@
/*
* Copyright (C) 2019 StApps
* This program is free software: you can redistribute it and/or modify it
* under the terms of the GNU General Public License as published by the Free
* Software Foundation, version 3.
*
* This program is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* 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} from '@angular/core';
@Component({
selector: 'stapps-skeleton-list-item',
templateUrl: 'skeleton-list-item.html',
})
export class SkeletonListItem {
}

View File

@@ -0,0 +1,14 @@
<ion-item>
<ion-thumbnail slot="start">
<ion-skeleton-text animated></ion-skeleton-text>
</ion-thumbnail>
<ion-grid>
<ion-row>
<ion-col>
<h2 class="name"><ion-skeleton-text animated style="width: 80%"></ion-skeleton-text></h2>
<p><ion-skeleton-text animated style="width: 80%;"></ion-skeleton-text></p>
<ion-note><ion-skeleton-text animated style="width: 20%"></ion-skeleton-text></ion-note>
</ion-col>
</ion-row>
</ion-grid>
</ion-item>

View File

@@ -0,0 +1,22 @@
/*
* Copyright (C) 2019 StApps
* This program is free software: you can redistribute it and/or modify it
* under the terms of the GNU General Public License as published by the Free
* Software Foundation, version 3.
*
* This program is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* 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} from '@angular/core';
@Component({
selector: 'stapps-skeleton-simple-card',
templateUrl: 'skeleton-simple-card.html',
})
export class SkeletonSimpleCard {
}

View File

@@ -0,0 +1,8 @@
<ion-card>
<ion-card-header>
<ion-skeleton-text animated style="width: 15%"></ion-skeleton-text>
</ion-card-header>
<ion-card-content>
<p><ion-skeleton-text animated style="width: 85%;"></ion-skeleton-text></p>
</ion-card-content>
</ion-card>

View File

@@ -13,7 +13,7 @@
* this program. If not, see <https://www.gnu.org/licenses/>. * this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
import {Component} from '@angular/core'; import {Component} from '@angular/core';
import {AlertController, LoadingController} from '@ionic/angular'; import {AlertController} from '@ionic/angular';
import {SCThing} from '@openstapps/core'; import {SCThing} from '@openstapps/core';
import {Subject} from 'rxjs'; import {Subject} from 'rxjs';
import {debounceTime, distinctUntilChanged} from 'rxjs/operators'; import {debounceTime, distinctUntilChanged} from 'rxjs/operators';
@@ -25,8 +25,9 @@ import {DataProvider} from '../data.provider';
}) })
export class DataListComponent { export class DataListComponent {
dataProvider: DataProvider; dataProvider: DataProvider;
items: SCThing[] = []; items: SCThing[];
selectedItem: any; selectedItem: any;
loaded: boolean = false;
size: number = 30; size: number = 30;
from: number = 0; from: number = 0;
@@ -34,10 +35,7 @@ export class DataListComponent {
query: string; query: string;
queryChanged: Subject<string> = new Subject<string>(); queryChanged: Subject<string> = new Subject<string>();
loading: HTMLIonLoadingElement;
constructor( constructor(
private loadingController: LoadingController,
private alertController: AlertController, private alertController: AlertController,
dataProvider: DataProvider, dataProvider: DataProvider,
) { ) {
@@ -49,31 +47,19 @@ export class DataListComponent {
.subscribe((model) => { .subscribe((model) => {
this.from = 0; this.from = 0;
this.query = model; this.query = model;
this.items = [];
this.fetchItems(); this.fetchItems();
}); });
this.fetchItems(); this.fetchItems();
} }
private async fetchItems(): Promise<any> { private async fetchItems(): Promise<any> {
if (this.from === 0) {
this.loading = await this.loadingController.create();
await this.loading.present();
}
return this.dataProvider.search({ return this.dataProvider.search({
from: this.from, from: this.from,
query: this.query, query: this.query,
size: this.size, size: this.size,
} as any).then((result) => { } as any).then((result) => {
result.data.forEach((item) => { this.items = result.data;
this.items.push(item); this.loaded = true;
});
if (this.from === 0) {
this.loading.dismiss();
}
}, async (err) => { }, async (err) => {
const alert: HTMLIonAlertElement = await this.alertController.create({ const alert: HTMLIonAlertElement = await this.alertController.create({
buttons: ['Dismiss'], buttons: ['Dismiss'],
@@ -82,8 +68,6 @@ export class DataListComponent {
}); });
await alert.present(); await alert.present();
await this.loading.dismiss();
}); });
} }

View File

@@ -12,10 +12,12 @@
</ion-header> </ion-header>
<ion-content> <ion-content>
<ion-list *ngFor="let item of items"> <ion-list *ngIf="items">
<stapps-data-list-item [item]="item"></stapps-data-list-item> <stapps-data-list-item [item]="item" *ngFor="let item of items"></stapps-data-list-item>
</ion-list>
<ion-list *ngIf="!items">
<stapps-skeleton-list-item *ngFor="let skeleton of [1, 2, 3, 4, 5]"></stapps-skeleton-list-item>
</ion-list> </ion-list>
<ion-infinite-scroll (ionInfinite)="loadMore($event)"> <ion-infinite-scroll (ionInfinite)="loadMore($event)">
<ion-infinite-scroll-content></ion-infinite-scroll-content> <ion-infinite-scroll-content></ion-infinite-scroll-content>
</ion-infinite-scroll> </ion-infinite-scroll>