fix(virtual-list): works with infinite-scroll

fixes #9350
fixes #9722
fixes #9247
fixes #10778
This commit is contained in:
Manu Mtz.-Almeida
2017-03-15 17:15:33 +01:00
parent 1222c56d23
commit 999efaca9a
7 changed files with 277 additions and 100 deletions

View File

@@ -248,7 +248,7 @@ export class Img implements OnDestroy {
const imgEle = this._img;
const renderer = this._renderer;
if (imgEle.src !== srcAttr) {
if (imgEle && imgEle.src !== srcAttr) {
renderer.setElementAttribute(this._img, 'src', srcAttr);
renderer.setElementAttribute(this._img, 'alt', this.alt);
}

View File

@@ -253,7 +253,6 @@ export class E2EPage {
}
presentLoadingOpenDismiss() {
// debugger;
const loading = this.loadingCtrl.create({
content: 'Loading 1'
});

View File

@@ -1,22 +1,20 @@
import { Component, NgModule } from '@angular/core';
import { Component, NgModule, enableProdMode } from '@angular/core';
import { IonicApp, IonicModule, NavController, Platform } from '../../../../../ionic-angular';
enableProdMode();
@Component({
templateUrl: 'main.html'
})
export class E2EPage {
items: any[] = [];
webview: string = '';
counter: number = 0;
constructor(plt: Platform, public navCtrl: NavController) {
for (var i = 0; i < 200; i++) {
this.items.push({
value: i,
someMethod: function() {
return '!!';
}
});
this.addItem();
}
if (plt.is('ios')) {
@@ -43,6 +41,16 @@ export class E2EPage {
this.navCtrl.push(E2EPage);
}
addItem() {
this.items.push({
value: this.counter,
someMethod: function() {
return '!!';
}
});
this.counter++;
}
reload() {
window.location.reload(true);
}

View File

@@ -5,6 +5,9 @@
<button ion-button (click)="reload()">
Reload
</button>
<button ion-button icon-only (click)="addItem()">
<ion-icon name="add"></ion-icon>
</button>
</ion-buttons>
</ion-navbar>
</ion-header>

View File

@@ -0,0 +1,82 @@
import { Component, NgModule } from '@angular/core';
import { IonicApp, IonicModule } from '../../../../../ionic-angular';
@Component({
templateUrl: 'main.html'
})
export class E2EPage {
counter = 1;
items: any[] = [];
enabled = true;
constructor() {
for (let i = 0; i < 100; i++) {
this.addItem();
}
}
addItem() {
this.items.push(this.counter);
this.counter++;
}
doInfinite(): Promise<any> {
console.log('Begin async operation');
return getAsyncData().then(newData => {
for (var i = 0; i < newData.length; i++) {
this.items.push( this.items.length );
}
console.log('Finished receiving data, async operation complete');
if (this.items.length > 900) {
this.enabled = false;
}
});
}
}
function getAsyncData(): Promise<any[]> {
// async return mock data
return new Promise(resolve => {
setTimeout(() => {
let data: number[] = [];
for (var i = 0; i < 30; i++) {
data.push(i);
}
resolve(data);
}, 500);
});
}
@Component({
template: '<ion-nav [root]="root"></ion-nav>'
})
export class E2EApp {
root = E2EPage;
}
@NgModule({
declarations: [
E2EApp,
E2EPage
],
imports: [
IonicModule.forRoot(E2EApp)
],
bootstrap: [IonicApp],
entryComponents: [
E2EApp,
E2EPage
]
})
export class AppModule {}

View File

@@ -0,0 +1,32 @@
<ion-header>
<ion-navbar>
<ion-title>Virtual Scroll</ion-title>
<ion-buttons end>
<button ion-button icon-only (click)="addItem()">
<ion-icon name="add"></ion-icon>
</button>
</ion-buttons>
</ion-navbar>
</ion-header>
<ion-content padding>
<ion-list [virtualScroll]="items">
<ion-item *virtualItem="let item">
Item: {{item}}
</ion-item>
</ion-list>
<ion-infinite-scroll (ionInfinite)="$event.waitFor(doInfinite())" [enabled]="enabled" threshold="100px">
<ion-infinite-scroll-content
loadingSpinner="bubbles"
loadingText="Loading more data...">
</ion-infinite-scroll-content>
</ion-infinite-scroll>
</ion-content>

View File

@@ -216,7 +216,8 @@ export class VirtualScroll implements DoCheck, AfterContentInit, OnDestroy {
_differ: any;
_scrollSub: any;
_scrollEndSub: any;
_init: boolean;
_resizeSub: any;
_init: boolean = false;
_lastEle: boolean;
_hdrFn: Function;
_ftrFn: Function;
@@ -229,6 +230,7 @@ export class VirtualScroll implements DoCheck, AfterContentInit, OnDestroy {
scrollTop: 0,
};
_queue: number;
_recordSize: number = 0;
@ContentChild(VirtualItem) _itmTmp: VirtualItem;
@ContentChild(VirtualHeader) _hdrTmp: VirtualHeader;
@@ -376,24 +378,23 @@ export class VirtualScroll implements DoCheck, AfterContentInit, OnDestroy {
private _plt: Platform,
private _ctrl: ViewController,
private _config: Config,
private _dom: DomController) {
private _dom: DomController
) {
// hide the virtual scroll element with opacity so we don't
// see jank as it loads up, but we're still able to read
// dimensions because it's still rendered and only opacity hidden
this._renderer.setElementClass(_elementRef.nativeElement, 'virtual-loading', true);
this.setElementClass('virtual-loading', true);
// wait for the content to be rendered and has readable dimensions
_ctrl.readReady.subscribe(() => {
this._init = true;
if (this._hasChanges()) {
this.readUpdate();
if (isPresent(this._changes())) {
this.readUpdate(true);
// wait for the content to be writable
var subscription = _ctrl.writeReady.subscribe(() => {
subscription.unsubscribe();
this.writeUpdate();
this.writeUpdate(true);
});
}
@@ -405,34 +406,54 @@ export class VirtualScroll implements DoCheck, AfterContentInit, OnDestroy {
* @private
*/
ngDoCheck() {
if (this._init && this._hasChanges()) {
// only continue if we've already initialized
// and if there actually are changes
this.readUpdate();
this.writeUpdate();
// only continue if we've already initialized
if (!this._init) {
return;
}
// and if there actually are changes
const changes = this._changes();
if (!isPresent(changes)) {
return;
}
let needClean = false;
if (changes) {
changes.forEachOperation((item: any, _: number, cindex: number) => {
if (item.previousIndex != null || (cindex < this._recordSize)) {
needClean = true;
}
});
} else {
needClean = true;
}
this._recordSize = this._records.length;
this.readUpdate(needClean);
this.writeUpdate(needClean);
}
readUpdate(needClean: boolean) {
if (needClean) {
// reset everything
console.debug(`virtual-scroll, readUpdate: slow path`);
this._cells.length = 0;
this._nodes.length = 0;
this._itmTmp.viewContainer.clear();
// ******** DOM READ ****************
this.calcDimensions();
} else {
console.debug(`virtual-scroll, readUpdate: fast path`);
}
}
readUpdate() {
console.debug(`virtual-scroll, readUpdate`);
// reset everything
this._cells.length = 0;
this._nodes.length = 0;
this._itmTmp.viewContainer.clear();
// ******** DOM READ ****************
calcDimensions(this._data, this._elementRef.nativeElement,
this.approxItemWidth, this.approxItemHeight,
this.approxHeaderWidth, this.approxHeaderHeight,
this.approxFooterWidth, this.approxFooterHeight,
this.bufferRatio);
}
writeUpdate() {
writeUpdate(needClean: boolean) {
console.debug(`virtual-scroll, writeUpdate`);
const data = this._data;
const stopAtHeight = (data.scrollTop + data.renderHeight);
data.scrollDiff = SCROLL_DIFFERENCE_MINIMUM + 1;
const stopAtHeight = ((this._data.scrollTop || 0) + this._data.renderHeight);
processRecords(stopAtHeight,
this._records,
this._cells,
@@ -441,86 +462,116 @@ export class VirtualScroll implements DoCheck, AfterContentInit, OnDestroy {
this._data);
// ******** DOM WRITE ****************
this.renderVirtual();
this.renderVirtual(needClean);
}
private _hasChanges() {
return (isPresent(this._records) && isPresent(this._differ) && isPresent(this._differ.diff(this._records)));
private calcDimensions() {
calcDimensions(this._data, this._elementRef.nativeElement,
this.approxItemWidth, this.approxItemHeight,
this.approxHeaderWidth, this.approxHeaderHeight,
this.approxFooterWidth, this.approxFooterHeight,
this.bufferRatio);
}
private _changes() {
if (isPresent(this._records) && isPresent(this._differ)) {
return this._differ.diff(this._records);
}
return null;
}
/**
* @private
* DOM WRITE
*/
renderVirtual() {
renderVirtual(needClean: boolean) {
const nodes = this._nodes;
const cells = this._cells;
const data = this._data;
const records = this._records;
// initialize nodes with the correct cell data
if (needClean) {
// ******** DOM WRITE ****************
updateDimensions(this._plt, nodes, cells, data, true);
data.topCell = 0;
data.bottomCell = (cells.length - 1);
}
adjustRendered(cells, data);
data.bottomCell = (cells.length - 1);
populateNodeData(data.topCell || 0,
data.bottomCell,
data.viewWidth, true,
cells, records, nodes,
this._itmTmp.viewContainer,
this._itmTmp.templateRef,
this._hdrTmp && this._hdrTmp.templateRef,
this._ftrTmp && this._ftrTmp.templateRef, true);
populateNodeData(data.topCell, data.bottomCell,
data.viewWidth, true,
cells, records, nodes,
this._itmTmp.viewContainer,
this._itmTmp.templateRef,
this._hdrTmp && this._hdrTmp.templateRef,
this._ftrTmp && this._ftrTmp.templateRef, needClean);
// ******** DOM WRITE ****************
this._cd.detectChanges();
if (needClean) {
this._cd.detectChanges();
}
this._plt.raf(() => {
// at this point, this fn was called from within another
// requestAnimationFrame, so the next dom reads/writes within the next frame
// wait a frame before trying to read and calculate the dimensions
this._dom.read(() => {
// ******** DOM READ ****************
initReadNodes(this._plt, nodes, cells, data);
});
// at this point, this fn was called from within another
// requestAnimationFrame, so the next dom reads/writes within the next frame
// wait a frame before trying to read and calculate the dimensions
this._dom.read(() => {
// ******** DOM READ ****************
initReadNodes(this._plt, nodes, cells, data);
});
this._dom.write(() => {
const ele = this._elementRef.nativeElement;
const recordsLength = records.length;
const renderer = this._renderer;
this._dom.write(() => {
const ele = this._elementRef.nativeElement;
const recordsLength = records.length;
const renderer = this._renderer;
// update the bound context for each node
updateNodeContext(nodes, cells, data);
// update the bound context for each node
updateNodeContext(nodes, cells, data);
// ******** DOM WRITE ****************
for (var i = 0; i < nodes.length; i++) {
(<any>nodes[i].view).detectChanges();
}
if (!this._lastEle) {
// add an element at the end so :last-child css doesn't get messed up
// ******** DOM WRITE ****************
var lastEle: HTMLElement = renderer.createElement(ele, 'div');
lastEle.className = 'virtual-last';
this._lastEle = true;
}
for (var i = 0; i < nodes.length; i++) {
(<any>nodes[i].view).detectChanges();
}
// ******** DOM WRITE ****************
renderer.setElementClass(ele, 'virtual-scroll', true);
if (!this._lastEle) {
// add an element at the end so :last-child css doesn't get messed up
// ******** DOM WRITE ****************
var lastEle: HTMLElement = renderer.createElement(ele, 'div');
lastEle.className = 'virtual-last';
this._lastEle = true;
}
// ******** DOM WRITE ****************
renderer.setElementClass(ele, 'virtual-loading', false);
// ******** DOM WRITE ****************
this.setElementClass('virtual-scroll', true);
// ******** DOM WRITE ****************
writeToNodes(this._plt, nodes, cells, recordsLength);
// ******** DOM WRITE ****************
this.setElementClass('virtual-loading', false);
// ******** DOM WRITE ****************
this._setHeight(
estimateHeight(recordsLength, cells[cells.length - 1], this._vHeight, 0.25)
);
// ******** DOM WRITE ****************
writeToNodes(this._plt, nodes, cells, recordsLength);
this._content.imgsUpdate();
// ******** DOM WRITE ****************
this._setHeight(
estimateHeight(recordsLength, cells[cells.length - 1], this._vHeight, 0.25)
);
this._content.imgsUpdate();
});
});
}
/**
* @private
*/
resize() {
// only continue if we've already initialized
if (!this._init) {
return;
}
console.debug('virtual-list: resized window');
this.calcDimensions();
this.writeUpdate(false);
}
/**
@@ -665,13 +716,9 @@ export class VirtualScroll implements DoCheck, AfterContentInit, OnDestroy {
this._content.enableJsScroll();
}
this._scrollSub = this._content.ionScroll.subscribe((ev: ScrollEvent) => {
this.scrollUpdate(ev);
});
this._scrollEndSub = this._content.ionScrollEnd.subscribe((ev: ScrollEvent) => {
this.scrollEnd(ev);
});
this._resizeSub = this._plt.resize.subscribe(this.resize.bind(this));
this._scrollSub = this._content.ionScroll.subscribe(this.scrollUpdate.bind(this));
this._scrollEndSub = this._content.ionScrollEnd.subscribe(this.scrollEnd.bind(this));
}
}
@@ -702,14 +749,20 @@ export class VirtualScroll implements DoCheck, AfterContentInit, OnDestroy {
}
}
setElementClass(className: string, add: boolean) {
this._renderer.setElementClass(this._elementRef.nativeElement, className, add);
}
/**
* @private
*/
ngOnDestroy() {
this._resizeSub && this._resizeSub.unsubscribe();
this._scrollSub && this._scrollSub.unsubscribe();
this._scrollEndSub && this._scrollEndSub.unsubscribe();
this._scrollEndSub = this._scrollSub = null;
this._hdrFn = this._ftrFn = this._records = this._cells = this._nodes = this._data = null;
}
}
const SCROLL_DIFFERENCE_MINIMUM = 40;