Skip to content

Instantly share code, notes, and snippets.

@stalniy
Created September 13, 2017 19:22
Show Gist options
  • Save stalniy/bea9a080387ddaef65d6e04628f744cb to your computer and use it in GitHub Desktop.
Save stalniy/bea9a080387ddaef65d6e04628f744cb to your computer and use it in GitHub Desktop.
Ionic infinite scroll
import { Directive, ElementRef, EventEmitter, Input, NgZone, Output, forwardRef, Renderer } from '@angular/core';
import { Content, ScrollEvent, DomController, InfiniteScroll as IonicInfiniteScroll } from 'ionic-angular';
// TODO: remove this when https://github.com/ionic-team/ionic/pull/12599 is merged
@Directive({
selector: 'my-infinite-scroll',
providers: [
{ provide: IonicInfiniteScroll, useExisting: forwardRef(() => InfiniteScroll) }
]
})
export class InfiniteScroll {
_lastCheck: number = 0;
_highestY: number = 0;
_scLsn: any;
_thr: string = '15%';
_thrPx: number = 0;
_thrPc: number = 0.15;
_position: string = POSITION_BOTTOM;
_init: boolean = false;
_prevDim: any;
/**
* @internal
*/
state: string = STATE_ENABLED;
/**
* @input {string} The threshold distance from the bottom
* of the content to call the `infinite` output event when scrolled.
* The threshold value can be either a percent, or
* in pixels. For example, use the value of `10%` for the `infinite`
* output event to get called when the user has scrolled 10%
* from the bottom of the page. Use the value `100px` when the
* scroll is within 100 pixels from the bottom of the page.
* Default is `15%`.
*/
@Input()
get threshold(): string {
return this._thr;
}
set threshold(val: string) {
this._thr = val;
if (val.indexOf('%') > -1) {
this._thrPx = 0;
this._thrPc = (parseFloat(val) / 100);
} else {
this._thrPx = parseFloat(val);
this._thrPc = 0;
}
}
/**
* @input {boolean} If true, Whether or not the infinite scroll should be
* enabled or not. Setting to `false` will remove scroll event listeners
* and hide the display.
*/
@Input()
set enabled(shouldEnable: boolean) {
this.enable(shouldEnable);
}
/**
* @input {string} The position of the infinite scroll element.
* The value can be either `top` or `bottom`.
* Default is `bottom`.
*/
@Input()
get position(): string {
return this._position;
}
set position(val: string) {
if (val === POSITION_TOP || val === POSITION_BOTTOM) {
this._position = val;
} else {
console.error(`Invalid value for ion-infinite-scroll's position input. Its value should be '${POSITION_BOTTOM}' or '${POSITION_TOP}'.`);
}
}
/**
* @output {event} Emitted when the scroll reaches
* the threshold distance. From within your infinite handler,
* you must call the infinite scroll's `complete()` method when
* your async operation has completed.
*/
@Output() ionInfinite: EventEmitter<InfiniteScroll> = new EventEmitter<InfiniteScroll>();
constructor(
private _content: Content,
private _zone: NgZone,
private _elementRef: ElementRef,
private _dom: DomController,
private _renderer: Renderer
) {
_content.setElementClass('has-infinite-scroll', true);
}
_onScroll(ev: ScrollEvent) {
if (this.state === STATE_LOADING || this.state === STATE_DISABLED) {
return 1;
}
if (!ev || this._lastCheck + 32 > ev.timeStamp) {
// no need to check less than every XXms
return 2;
}
this._lastCheck = ev.timeStamp;
// ******** DOM READ ****************
const infiniteHeight = this._elementRef.nativeElement.scrollHeight;
if (!infiniteHeight) {
// if there is no height of this element then do nothing
return 3;
}
const d = this._content.getContentDimensions()
const height = d.contentHeight;
const threshold = this._thrPc ? (height * this._thrPc) : this._thrPx;
// ******** DOM READS ABOVE / DOM WRITES BELOW ****************
let distanceFromInfinite: number;
if (this._position === POSITION_BOTTOM) {
distanceFromInfinite = d.scrollHeight - infiniteHeight - d.scrollTop - height - threshold;
} else {
// assert(this._position === POSITION_TOP, '_position should be top');
distanceFromInfinite = d.scrollTop - infiniteHeight - threshold;
}
if (distanceFromInfinite < 0) {
// ******** DOM WRITE ****************
this._dom.write(() => {
this._zone.run(() => {
if (this.state !== STATE_LOADING && this.state !== STATE_DISABLED) {
this._prevDim = d;
this.state = STATE_LOADING;
this.ionInfinite.emit(this);
}
});
});
return 5;
}
return 6;
}
/**
* Call `complete()` within the `infinite` output event handler when
* your async operation has completed. For example, the `loading`
* state is while the app is performing an asynchronous operation,
* such as receiving more data from an AJAX request to add more items
* to a data list. Once the data has been received and UI updated, you
* then call this method to signify that the loading has completed.
* This method will change the infinite scroll's state from `loading`
* to `enabled`.
*/
complete() {
if (this.state !== STATE_LOADING) {
return;
}
if (this._position === POSITION_BOTTOM) {
this.state = STATE_ENABLED;
return;
}
// assert(this._position === POSITION_TOP, 'position should be top');
this._updateScrollTopPosition();
}
/**
* @hidden
*
* Updates `scrollTop` position of `Content with respect to the last received `ScrollEvent`
*/
_updateScrollTopPosition() {
// ******** DOM READ ****************
this._dom.read(() => {
const scrollEl = this._content.getScrollElement();
const newDim = this._content.getContentDimensions();
const newScrollTop = (this._prevDim.scrollTop < 0 ? 0 : this._prevDim.scrollTop) + newDim.scrollHeight - this._prevDim.scrollHeight;
// ******** DOM WRITE ****************
this._dom.write(() => {
this._renderer.setElementStyle(scrollEl, '-webkit-overflow-scrolling', 'auto');
this._content.scrollTop = newScrollTop;
});
this._dom.write(() => {
this._renderer.setElementStyle(scrollEl, '-webkit-overflow-scrolling', '');
this.state = STATE_ENABLED;
});
})
}
/**
* Pass a promise inside `waitFor()` within the `infinite` output event handler in order to
* change state of infiniteScroll to "complete"
*/
waitFor(action: Promise<any>) {
const enable = this.complete.bind(this);
action.then(enable, enable);
}
/**
* Call `enable(false)` to disable the infinite scroll from actively
* trying to receive new data while scrolling. This method is useful
* when it is known that there is no more data that can be added, and
* the infinite scroll is no longer needed.
* @param {boolean} shouldEnable If the infinite scroll should be
* enabled or not. Setting to `false` will remove scroll event listeners
* and hide the display.
*/
enable(shouldEnable: boolean) {
this.state = (shouldEnable ? STATE_ENABLED : STATE_DISABLED);
this._setListeners(shouldEnable);
}
/**
* @hidden
*/
_setListeners(shouldListen: boolean) {
if (this._init) {
if (shouldListen) {
if (!this._scLsn) {
this._scLsn = this._content.ionScroll.subscribe(this._onScroll.bind(this));
}
} else {
this._scLsn && this._scLsn.unsubscribe();
this._scLsn = null;
}
}
}
/**
* @hidden
*/
ngAfterContentInit() {
this._init = true;
this._setListeners(this.state !== STATE_DISABLED);
if (this._position === POSITION_TOP) {
this._content.scrollDownOnLoad = true;
}
}
/**
* @hidden
*/
ngOnDestroy() {
this._setListeners(false);
}
}
const STATE_ENABLED = 'enabled';
const STATE_DISABLED = 'disabled';
const STATE_LOADING = 'loading';
const POSITION_TOP = 'top';
const POSITION_BOTTOM = 'bottom';
import { NgModule } from '@angular/core'
import { IonicModule } from 'ionic-angular'
import { InfiniteScroll } from '../components/infinite-scroll/infinite-scroll'
@NgModule({
imports: [IonicModule],
declarations: [
InfiniteScroll
],
exports: [
InfiniteScroll
]
})
export class InfiniteScrollModule {}
<my-infinite-scroll position="top" *ngIf="canLoadMore()" (ionInfinite)="$event.waitFor(loadMore())">
<ion-infinite-scroll-content></ion-infinite-scroll-content>
</my-infinite-scroll>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment