Skip to content

Commit

Permalink
Add infinite scroll loading to sermon-list
Browse files Browse the repository at this point in the history
  • Loading branch information
clarkmalmgren committed Dec 27, 2017
1 parent e173a8c commit 49dcbff
Show file tree
Hide file tree
Showing 14 changed files with 163 additions and 32 deletions.
2 changes: 2 additions & 0 deletions src/app/components/atoms/index.ts
@@ -1,7 +1,9 @@
import { HamburgerComponent } from './hamburger';
import { InfiniteScrollDirective } from './infinite-scroll.directive';
import { ObservableButtonComponent } from './observable-button';

export const ATOMS = [
HamburgerComponent,
InfiniteScrollDirective,
ObservableButtonComponent
];
55 changes: 55 additions & 0 deletions src/app/components/atoms/infinite-scroll.directive.ts
@@ -0,0 +1,55 @@
import {
AfterViewInit,
Directive,
ElementRef,
EventEmitter,
Input,
Output
} from '@angular/core';
import { Autoclean } from '../templates/autoclean';
import { Aperture, Observable } from '../../services';

class Snapshot {
scrollHeight: number;
scrollTop: number;
clientHeight: number;

constructor(target: Element) {
this.scrollHeight = target.scrollHeight;
this.scrollTop = target.scrollTop;
this.clientHeight = target.clientHeight;
}
};


@Directive({
selector: '[bcInfiniteScroll]'
})
export class InfiniteScrollDirective extends Autoclean implements AfterViewInit {

@Input()
scrollPercent: number = 70;

@Output()
onScroll = new EventEmitter<any>();

constructor(
private aperture: Aperture,
private el: ElementRef
) {
super();
}

ngAfterViewInit() {
this.autoclean(
this.aperture
.observableWindowEvent('scroll')
.map((e: Event) => new Snapshot((e.target as any).scrollingElement as Element))
.pairwise()
.filter((pair: Snapshot[]) => pair[0].scrollTop < pair[1].scrollTop)
.map((pair: Snapshot[]) => pair[1])
.filter((s: Snapshot) => 100 * (s.scrollTop + s.clientHeight) / s.scrollHeight > this.scrollPercent)
.subscribe(e => { this.onScroll.emit(''); })
);
}
}
5 changes: 4 additions & 1 deletion src/app/components/organisms/sermon-list.html
@@ -1,3 +1,6 @@
<div class="sermon-list">
<div
class="sermon-list"
bcInfiniteScroll
(onScroll)="addSermons()">
<bc-sermon-card *ngFor="let s of sermons" [sermon]="s"></bc-sermon-card>
</div>
24 changes: 20 additions & 4 deletions src/app/components/organisms/sermon-list.ts
@@ -1,5 +1,9 @@
import { Component, Input, OnInit } from '@angular/core';
import { SermonService, Sermon } from '../../services';
import {
LinearPager,
SermonService,
Sermon
} from '../../services';

@Component({
selector: 'bc-sermon-list',
Expand All @@ -8,13 +12,25 @@ import { SermonService, Sermon } from '../../services';
})
export class SermonListComponent implements OnInit {

@Input() sermons: Sermon[];
sermons: Sermon[] = [];
pager: LinearPager<Sermon>;

constructor(private service: SermonService) {}

ngOnInit() {
this.service.complete()
.subscribe(sermons => { this.sermons = sermons; });
this.pager = new LinearPager(this.service.complete());
this.pager.itemsPerPage = 20;

this.pager
.observe()
.subscribe(sermons => {
this.sermons = this.sermons.concat(sermons);
})
}

addSermons(): void {
console.log('Add more sermons!');
this.pager.next();
}

}
6 changes: 3 additions & 3 deletions src/app/components/pages/admin/sermons.ts
Expand Up @@ -5,7 +5,7 @@ import { Secured } from './secured';
import {
FirebaseService,
Observable,
Pager,
PaginatedPager,
SeriesImageForm,
SeriesImageService,
Sermon,
Expand All @@ -20,7 +20,7 @@ export class SermonsComponent extends Secured implements OnInit {

sermons: Sermon[];
images: SeriesImageForm[];
pager: Pager<Sermon>;
pager: PaginatedPager<Sermon>;

constructor(
router: Router,
Expand All @@ -41,7 +41,7 @@ export class SermonsComponent extends Secured implements OnInit {
}

update(): void {
this.pager = this.service.page();
this.pager = this.service.paginated();
this.pager
.observe()
.subscribe(sermons => {
Expand Down
8 changes: 6 additions & 2 deletions src/app/services/analytics.spec.ts
@@ -1,7 +1,7 @@
import { expect, sinon, async, spyOf } from 'testing';
import { Location } from '@angular/common';
import {
Event,
Event as RouterEvent,
NavigationCancel,
NavigationEnd,
NavigationStart,
Expand All @@ -10,6 +10,7 @@ import {
import { Analytics } from './analytics';
import { Aperture } from './aperture';
import { Env } from './env';
import { Observable } from './observable';

class MockAperture extends Aperture {
scrollTo(options?: ScrollToOptions): void {
Expand All @@ -27,6 +28,9 @@ class MockAperture extends Aperture {
create(target: any): Aperture {
throw new Error('Method not implemented.');
}
observableWindowEvent(eventName: string): Observable<Event> {
throw new Error('Method not implemented.');
}
constructor(private wrapped: Function) {
super();
}
Expand Down Expand Up @@ -63,7 +67,7 @@ describe('Analytics', () => {
});

it('event callbacks should work correctly', () => {
let subscription: (event: Event) => void;
let subscription: (event: RouterEvent) => void;
const subscribe = sinon.stub().callsFake((fn) => { subscription = fn; });

const router = <Router><any> { events: { subscribe: subscribe } };
Expand Down
8 changes: 6 additions & 2 deletions src/app/services/aperture.browser.ts
@@ -1,5 +1,5 @@
import { Aperture } from './aperture';

import { Aperture } from './aperture';
import { Observable } from './observable';

/* Make ga typesafe, sortof */
declare global {
Expand Down Expand Up @@ -49,4 +49,8 @@ export class BrowserAperture extends Aperture {
create(target: any): Aperture {
return new BrowserAperture().with(target);
}

observableWindowEvent(eventName: string): Observable<Event> {
return Observable.fromEvent(window, eventName);
}
}
7 changes: 6 additions & 1 deletion src/app/services/aperture.server.ts
@@ -1,4 +1,5 @@
import { Aperture } from './aperture';
import { Aperture } from './aperture';
import { Observable } from './observable';

export class ServerAperture extends Aperture {

Expand Down Expand Up @@ -31,4 +32,8 @@ export class ServerAperture extends Aperture {
create(target: any): Aperture {
return this;
}

observableWindowEvent(eventName: string): Observable<Event> {
return Observable.empty();
}
}
4 changes: 2 additions & 2 deletions src/app/services/aperture.ts
@@ -1,3 +1,4 @@
import { Observable } from './observable';

export abstract class Aperture {
innerHeight: number;
Expand All @@ -9,6 +10,5 @@ export abstract class Aperture {
abstract set(key: string, value: any): void;
abstract now(): number;
abstract create(target: any): Aperture;
abstract observableWindowEvent(eventName: string): Observable<Event>;
}


4 changes: 3 additions & 1 deletion src/app/services/index.ts
Expand Up @@ -6,7 +6,7 @@ import { Env } from './env';
import { GlobalErrorHandler } from './error.handler';
import { FirebaseService, FirebaseStorage, FirebaseDatabase } from './firebase.service';
import { Observable, Subscription } from './observable';
import { Pager } from './pager';
import { Pager, PaginatedPager, LinearPager } from './pager';
import { SeriesImageForm, SeriesImageService } from './series.service';
import { SermonService, Sermon } from './sermon.service';
import { FeatureToggles, TogglesService } from './toggles.service';
Expand Down Expand Up @@ -36,8 +36,10 @@ export {
FirebaseService,
FirebaseStorage,
GlobalErrorHandler,
LinearPager,
Observable,
Pager,
PaginatedPager,
SeriesImageForm,
SeriesImageService,
Sermon,
Expand Down
3 changes: 3 additions & 0 deletions src/app/services/observable.ts
Expand Up @@ -5,6 +5,7 @@ export { Subscription } from 'rxjs/Subscription';
/* Explicitly include the ways we might generate an observable */
import 'rxjs/add/observable/empty';
import 'rxjs/add/observable/from';
import 'rxjs/add/observable/fromEvent';
import 'rxjs/add/observable/fromPromise';
import 'rxjs/add/observable/interval';
import 'rxjs/add/observable/merge';
Expand All @@ -14,7 +15,9 @@ import 'rxjs/add/observable/of';

/* Include operators */
import 'rxjs/add/operator/catch';
import 'rxjs/add/operator/filter';
import 'rxjs/add/operator/map';
import 'rxjs/add/operator/mergeMap';
import 'rxjs/add/operator/pairwise';
import 'rxjs/add/operator/shareReplay';
import 'rxjs/add/operator/toArray';
61 changes: 49 additions & 12 deletions src/app/services/pager.ts
Expand Up @@ -2,36 +2,54 @@ import { PageEvent } from '@angular/material'
import { FirebaseDatabase } from './firebase.service';
import { Observable, Observer } from './observable';

export class Pager<T> {
export abstract class Pager<T> {

itemsPerPage: number = 25;
currentPage: number = 0;

private items: T[];
private observer: Observer<T[]>
protected items: T[];
protected observer: Observer<T[]>;

constructor(private source: Observable<T[]>) {
constructor(protected source: Observable<T[]>) {
source.subscribe((items) => {
this.items = items;
this.emit();
})
}

get length(): number {
return this.items.length;
}

observe(): Observable<T[]> {
return Observable.create((observer: Observer<T[]>) => {
this.observer = observer;
this.emit();
});
}

private emit(): void {
get length(): number {
return this.items.length;
}

get pages(): number {
return Math.ceil(this.length / this.itemsPerPage);
}

page(index: number): T[] {
const min = index * this.itemsPerPage;
return this.items.slice(min, min + this.itemsPerPage);
}

protected abstract emit(): void;
}

export class PaginatedPager<T> extends Pager<T> {

currentPage: number = 0;

constructor(source: Observable<T[]>) {
super(source);
}

protected emit(): void {
if (this.observer && this.items) {
const min = this.currentPage * this.itemsPerPage;
this.observer.next(this.items.slice(min, this.itemsPerPage));
this.observer.next(this.page(this.currentPage));
}
}

Expand All @@ -40,5 +58,24 @@ export class Pager<T> {
this.itemsPerPage = event.pageSize;
this.emit();
}
}

export class LinearPager<T> extends Pager<T> {

currentPage: number = 0;

constructor(source: Observable<T[]>) {
super(source);
}

protected emit(): void {
if (this.observer && this.items && this.currentPage <= this.pages) {
this.observer.next(this.page(this.currentPage));
this.currentPage++;
}
}

next(): void {
this.emit();
}
}
6 changes: 3 additions & 3 deletions src/app/services/sermon.service.ts
@@ -1,7 +1,7 @@
import { Injectable } from '@angular/core';
import { FirebaseService, FirebaseUtils } from './firebase.service';
import { Observable } from './observable';
import { Pager } from './pager';
import { PaginatedPager, LinearPager } from './pager';
import * as moment from 'moment';
import 'moment-timezone';

Expand Down Expand Up @@ -96,8 +96,8 @@ export class SermonService {
});
}

page(): Pager<Sermon> {
return new Pager(this.all());
paginated(): PaginatedPager<Sermon> {
return new PaginatedPager(this.all());
}

all(): Observable<Sermon[]> {
Expand Down
2 changes: 1 addition & 1 deletion tslint.json
Expand Up @@ -89,7 +89,7 @@
"check-type"
],

"directive-selector": [true, "attribute", "app", "camelCase"],
"directive-selector": [true, "attribute", "bc", "camelCase"],
"component-selector": [true, "element", "bc", "kebab-case"],
"use-input-property-decorator": true,
"use-output-property-decorator": true,
Expand Down

0 comments on commit 49dcbff

Please sign in to comment.