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;