From 999efaca9a1ba4eec705ad975457c646697167a0 Mon Sep 17 00:00:00 2001 From: "Manu Mtz.-Almeida" Date: Wed, 15 Mar 2017 17:15:33 +0100 Subject: [PATCH] fix(virtual-list): works with infinite-scroll fixes #9350 fixes #9722 fixes #9247 fixes #10778 --- src/components/img/img.ts | 2 +- .../loading/test/basic/app.module.ts | 1 - .../virtual-scroll/test/basic/app.module.ts | 22 +- .../virtual-scroll/test/basic/main.html | 3 + .../test/infinite-scroll/app.module.ts | 82 ++++++ .../test/infinite-scroll/main.html | 32 +++ .../virtual-scroll/virtual-scroll.ts | 235 +++++++++++------- 7 files changed, 277 insertions(+), 100 deletions(-) create mode 100644 src/components/virtual-scroll/test/infinite-scroll/app.module.ts create mode 100644 src/components/virtual-scroll/test/infinite-scroll/main.html diff --git a/src/components/img/img.ts b/src/components/img/img.ts index c6eeb33259..2fac369e08 100644 --- a/src/components/img/img.ts +++ b/src/components/img/img.ts @@ -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); } diff --git a/src/components/loading/test/basic/app.module.ts b/src/components/loading/test/basic/app.module.ts index 38f319bec3..61c045359f 100644 --- a/src/components/loading/test/basic/app.module.ts +++ b/src/components/loading/test/basic/app.module.ts @@ -253,7 +253,6 @@ export class E2EPage { } presentLoadingOpenDismiss() { - // debugger; const loading = this.loadingCtrl.create({ content: 'Loading 1' }); diff --git a/src/components/virtual-scroll/test/basic/app.module.ts b/src/components/virtual-scroll/test/basic/app.module.ts index daebae3783..2f954426a5 100644 --- a/src/components/virtual-scroll/test/basic/app.module.ts +++ b/src/components/virtual-scroll/test/basic/app.module.ts @@ -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); } diff --git a/src/components/virtual-scroll/test/basic/main.html b/src/components/virtual-scroll/test/basic/main.html index 6116fe018a..c023dc093c 100644 --- a/src/components/virtual-scroll/test/basic/main.html +++ b/src/components/virtual-scroll/test/basic/main.html @@ -5,6 +5,9 @@ + diff --git a/src/components/virtual-scroll/test/infinite-scroll/app.module.ts b/src/components/virtual-scroll/test/infinite-scroll/app.module.ts new file mode 100644 index 0000000000..06f8d90d38 --- /dev/null +++ b/src/components/virtual-scroll/test/infinite-scroll/app.module.ts @@ -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 { + 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 { + // 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: '' +}) +export class E2EApp { + root = E2EPage; +} + + +@NgModule({ + declarations: [ + E2EApp, + E2EPage + ], + imports: [ + IonicModule.forRoot(E2EApp) + ], + bootstrap: [IonicApp], + entryComponents: [ + E2EApp, + E2EPage + ] +}) +export class AppModule {} diff --git a/src/components/virtual-scroll/test/infinite-scroll/main.html b/src/components/virtual-scroll/test/infinite-scroll/main.html new file mode 100644 index 0000000000..7c86cdf921 --- /dev/null +++ b/src/components/virtual-scroll/test/infinite-scroll/main.html @@ -0,0 +1,32 @@ + + + Virtual Scroll + + + + + + + + + + + + + + Item: {{item}} + + + + + + + + + + + diff --git a/src/components/virtual-scroll/virtual-scroll.ts b/src/components/virtual-scroll/virtual-scroll.ts index f629844363..3ac9169606 100644 --- a/src/components/virtual-scroll/virtual-scroll.ts +++ b/src/components/virtual-scroll/virtual-scroll.ts @@ -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++) { - (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++) { + (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;