Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(infinite): add scroll in opposite direction #8099

Merged
merged 9 commits into from
Mar 15, 2017
22 changes: 21 additions & 1 deletion src/components/content/content.ts
Original file line number Diff line number Diff line change
Expand Up @@ -174,6 +174,8 @@ export class Content extends Ion implements OnDestroy, OnInit {
_viewCtrlReadSub: any;
/** @internal */
_viewCtrlWriteSub: any;
/** @internal */
_scrollDownOnLoad: boolean = false;

private _imgReqBfr: number;
private _imgRndBfr: number;
Expand Down Expand Up @@ -472,13 +474,25 @@ export class Content extends Ion implements OnDestroy, OnInit {
*/
@Input()
get fullscreen(): boolean {
return !!this._fullscreen;
return this._fullscreen;
}

set fullscreen(val: boolean) {
this._fullscreen = isTrueProperty(val);
}

/**
* @input {boolean} If true, the content will scroll down on load.
*/
@Input()
get scrollDownOnLoad(): boolean {
return this._scrollDownOnLoad;
}

set scrollDownOnLoad(val: boolean) {
this._scrollDownOnLoad = isTrueProperty(val);
}

/**
* @private
*/
Expand Down Expand Up @@ -827,6 +841,12 @@ export class Content extends Ion implements OnDestroy, OnInit {
this._tabs.setTabbarPosition(-1, 0);
}
}

// Scroll the page all the way down after setting dimensions
if (this._scrollDownOnLoad) {
this.scrollToBottom(0);
this._scrollDownOnLoad = false;
}
}

/**
Expand Down
32 changes: 32 additions & 0 deletions src/components/content/test/scroll-down-on-load/app.module.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { Component, NgModule } from '@angular/core';
import { IonicApp, IonicModule } from '../../../../../ionic-angular';


@Component({
templateUrl: 'main.html'
})
export class E2EPage {}


@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 {}
40 changes: 40 additions & 0 deletions src/components/content/test/scroll-down-on-load/main.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
<ion-content scrollDownOnLoad="true">
<b>This page should scroll down on load</b>
<p>
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Morbi scelerisque dolor lacus, ut vehicula arcu dapibus id. Morbi iaculis fermentum blandit. Curabitur tempus, ante et vehicula tempor, urna velit rutrum massa, quis suscipit purus lacus eget est. Sed nisi nulla, tempus id dictum a, cursus ut felis. Aliquam orci magna, rutrum nec tempor ac, fermentum quis eros. Sed ullamcorper felis sit amet tristique sagittis. Nullam sed tempus mi. Morbi sit amet lacinia leo. Nunc facilisis orci id consectetur dignissim. Integer dictum consectetur enim. Vivamus auctor, turpis ut eleifend pharetra, purus magna mattis arcu, vel pharetra tellus orci eget ex. Integer blandit posuere vehicula. Ut ipsum lorem, efficitur vitae eleifend tincidunt, fermentum nec lacus. Ut nec fermentum dui.
</p>
<p>
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Morbi scelerisque dolor lacus, ut vehicula arcu dapibus id. Morbi iaculis fermentum blandit. Curabitur tempus, ante et vehicula tempor, urna velit rutrum massa, quis suscipit purus lacus eget est. Sed nisi nulla, tempus id dictum a, cursus ut felis. Aliquam orci magna, rutrum nec tempor ac, fermentum quis eros. Sed ullamcorper felis sit amet tristique sagittis. Nullam sed tempus mi. Morbi sit amet lacinia leo. Nunc facilisis orci id consectetur dignissim. Integer dictum consectetur enim. Vivamus auctor, turpis ut eleifend pharetra, purus magna mattis arcu, vel pharetra tellus orci eget ex. Integer blandit posuere vehicula. Ut ipsum lorem, efficitur vitae eleifend tincidunt, fermentum nec lacus. Ut nec fermentum dui.
</p>
<p>
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Morbi scelerisque dolor lacus, ut vehicula arcu dapibus id. Morbi iaculis fermentum blandit. Curabitur tempus, ante et vehicula tempor, urna velit rutrum massa, quis suscipit purus lacus eget est. Sed nisi nulla, tempus id dictum a, cursus ut felis. Aliquam orci magna, rutrum nec tempor ac, fermentum quis eros. Sed ullamcorper felis sit amet tristique sagittis. Nullam sed tempus mi. Morbi sit amet lacinia leo. Nunc facilisis orci id consectetur dignissim. Integer dictum consectetur enim. Vivamus auctor, turpis ut eleifend pharetra, purus magna mattis arcu, vel pharetra tellus orci eget ex. Integer blandit posuere vehicula. Ut ipsum lorem, efficitur vitae eleifend tincidunt, fermentum nec lacus. Ut nec fermentum dui.
</p>
<p>
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Morbi scelerisque dolor lacus, ut vehicula arcu dapibus id. Morbi iaculis fermentum blandit. Curabitur tempus, ante et vehicula tempor, urna velit rutrum massa, quis suscipit purus lacus eget est. Sed nisi nulla, tempus id dictum a, cursus ut felis. Aliquam orci magna, rutrum nec tempor ac, fermentum quis eros. Sed ullamcorper felis sit amet tristique sagittis. Nullam sed tempus mi. Morbi sit amet lacinia leo. Nunc facilisis orci id consectetur dignissim. Integer dictum consectetur enim. Vivamus auctor, turpis ut eleifend pharetra, purus magna mattis arcu, vel pharetra tellus orci eget ex. Integer blandit posuere vehicula. Ut ipsum lorem, efficitur vitae eleifend tincidunt, fermentum nec lacus. Ut nec fermentum dui.
</p>
<p>
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Morbi scelerisque dolor lacus, ut vehicula arcu dapibus id. Morbi iaculis fermentum blandit. Curabitur tempus, ante et vehicula tempor, urna velit rutrum massa, quis suscipit purus lacus eget est. Sed nisi nulla, tempus id dictum a, cursus ut felis. Aliquam orci magna, rutrum nec tempor ac, fermentum quis eros. Sed ullamcorper felis sit amet tristique sagittis. Nullam sed tempus mi. Morbi sit amet lacinia leo. Nunc facilisis orci id consectetur dignissim. Integer dictum consectetur enim. Vivamus auctor, turpis ut eleifend pharetra, purus magna mattis arcu, vel pharetra tellus orci eget ex. Integer blandit posuere vehicula. Ut ipsum lorem, efficitur vitae eleifend tincidunt, fermentum nec lacus. Ut nec fermentum dui.
</p>
<p>
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Morbi scelerisque dolor lacus, ut vehicula arcu dapibus id. Morbi iaculis fermentum blandit. Curabitur tempus, ante et vehicula tempor, urna velit rutrum massa, quis suscipit purus lacus eget est. Sed nisi nulla, tempus id dictum a, cursus ut felis. Aliquam orci magna, rutrum nec tempor ac, fermentum quis eros. Sed ullamcorper felis sit amet tristique sagittis. Nullam sed tempus mi. Morbi sit amet lacinia leo. Nunc facilisis orci id consectetur dignissim. Integer dictum consectetur enim. Vivamus auctor, turpis ut eleifend pharetra, purus magna mattis arcu, vel pharetra tellus orci eget ex. Integer blandit posuere vehicula. Ut ipsum lorem, efficitur vitae eleifend tincidunt, fermentum nec lacus. Ut nec fermentum dui.
</p>
<p>
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Morbi scelerisque dolor lacus, ut vehicula arcu dapibus id. Morbi iaculis fermentum blandit. Curabitur tempus, ante et vehicula tempor, urna velit rutrum massa, quis suscipit purus lacus eget est. Sed nisi nulla, tempus id dictum a, cursus ut felis. Aliquam orci magna, rutrum nec tempor ac, fermentum quis eros. Sed ullamcorper felis sit amet tristique sagittis. Nullam sed tempus mi. Morbi sit amet lacinia leo. Nunc facilisis orci id consectetur dignissim. Integer dictum consectetur enim. Vivamus auctor, turpis ut eleifend pharetra, purus magna mattis arcu, vel pharetra tellus orci eget ex. Integer blandit posuere vehicula. Ut ipsum lorem, efficitur vitae eleifend tincidunt, fermentum nec lacus. Ut nec fermentum dui.
</p>
<p>
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Morbi scelerisque dolor lacus, ut vehicula arcu dapibus id. Morbi iaculis fermentum blandit. Curabitur tempus, ante et vehicula tempor, urna velit rutrum massa, quis suscipit purus lacus eget est. Sed nisi nulla, tempus id dictum a, cursus ut felis. Aliquam orci magna, rutrum nec tempor ac, fermentum quis eros. Sed ullamcorper felis sit amet tristique sagittis. Nullam sed tempus mi. Morbi sit amet lacinia leo. Nunc facilisis orci id consectetur dignissim. Integer dictum consectetur enim. Vivamus auctor, turpis ut eleifend pharetra, purus magna mattis arcu, vel pharetra tellus orci eget ex. Integer blandit posuere vehicula. Ut ipsum lorem, efficitur vitae eleifend tincidunt, fermentum nec lacus. Ut nec fermentum dui.
</p>
<p>
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Morbi scelerisque dolor lacus, ut vehicula arcu dapibus id. Morbi iaculis fermentum blandit. Curabitur tempus, ante et vehicula tempor, urna velit rutrum massa, quis suscipit purus lacus eget est. Sed nisi nulla, tempus id dictum a, cursus ut felis. Aliquam orci magna, rutrum nec tempor ac, fermentum quis eros. Sed ullamcorper felis sit amet tristique sagittis. Nullam sed tempus mi. Morbi sit amet lacinia leo. Nunc facilisis orci id consectetur dignissim. Integer dictum consectetur enim. Vivamus auctor, turpis ut eleifend pharetra, purus magna mattis arcu, vel pharetra tellus orci eget ex. Integer blandit posuere vehicula. Ut ipsum lorem, efficitur vitae eleifend tincidunt, fermentum nec lacus. Ut nec fermentum dui.
</p>
<p>
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Morbi scelerisque dolor lacus, ut vehicula arcu dapibus id. Morbi iaculis fermentum blandit. Curabitur tempus, ante et vehicula tempor, urna velit rutrum massa, quis suscipit purus lacus eget est. Sed nisi nulla, tempus id dictum a, cursus ut felis. Aliquam orci magna, rutrum nec tempor ac, fermentum quis eros. Sed ullamcorper felis sit amet tristique sagittis. Nullam sed tempus mi. Morbi sit amet lacinia leo. Nunc facilisis orci id consectetur dignissim. Integer dictum consectetur enim. Vivamus auctor, turpis ut eleifend pharetra, purus magna mattis arcu, vel pharetra tellus orci eget ex. Integer blandit posuere vehicula. Ut ipsum lorem, efficitur vitae eleifend tincidunt, fermentum nec lacus. Ut nec fermentum dui.
</p>
<p>
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Morbi scelerisque dolor lacus, ut vehicula arcu dapibus id. Morbi iaculis fermentum blandit. Curabitur tempus, ante et vehicula tempor, urna velit rutrum massa, quis suscipit purus lacus eget est. Sed nisi nulla, tempus id dictum a, cursus ut felis. Aliquam orci magna, rutrum nec tempor ac, fermentum quis eros. Sed ullamcorper felis sit amet tristique sagittis. Nullam sed tempus mi. Morbi sit amet lacinia leo. Nunc facilisis orci id consectetur dignissim. Integer dictum consectetur enim. Vivamus auctor, turpis ut eleifend pharetra, purus magna mattis arcu, vel pharetra tellus orci eget ex. Integer blandit posuere vehicula. Ut ipsum lorem, efficitur vitae eleifend tincidunt, fermentum nec lacus. Ut nec fermentum dui.
</p>
<p>
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Morbi scelerisque dolor lacus, ut vehicula arcu dapibus id. Morbi iaculis fermentum blandit. Curabitur tempus, ante et vehicula tempor, urna velit rutrum massa, quis suscipit purus lacus eget est. Sed nisi nulla, tempus id dictum a, cursus ut felis. Aliquam orci magna, rutrum nec tempor ac, fermentum quis eros. Sed ullamcorper felis sit amet tristique sagittis. Nullam sed tempus mi. Morbi sit amet lacinia leo. Nunc facilisis orci id consectetur dignissim. Integer dictum consectetur enim. Vivamus auctor, turpis ut eleifend pharetra, purus magna mattis arcu, vel pharetra tellus orci eget ex. Integer blandit posuere vehicula. Ut ipsum lorem, efficitur vitae eleifend tincidunt, fermentum nec lacus. Ut nec fermentum dui.
</p>
<b>It worked!</b>
</ion-content>
65 changes: 56 additions & 9 deletions src/components/infinite-scroll/infinite-scroll.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import { DomController } from '../../platform/dom-controller';
* @name InfiniteScroll
* @description
* The Infinite Scroll allows you to perform an action when the user
* scrolls a specified distance from the bottom of the page.
* scrolls a specified distance from the bottom or top of the page.
*
* The expression assigned to the `infinite` event is called when
* the user scrolls to the specified distance. When this expression
Expand Down Expand Up @@ -148,6 +148,7 @@ export class InfiniteScroll {
_thr: string = '15%';
_thrPx: number = 0;
_thrPc: number = 0.15;
_position: string = POSITION_BOTTOM;
_init: boolean = false;


Expand Down Expand Up @@ -192,6 +193,23 @@ export class InfiniteScroll {
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,
Expand Down Expand Up @@ -229,17 +247,20 @@ export class InfiniteScroll {

// ******** DOM READ ****************
const d = this._content.getContentDimensions();
const height = d.contentHeight;

let reloadY = d.contentHeight;
if (this._thrPc) {
reloadY += (reloadY * this._thrPc);
} else {
reloadY += this._thrPx;
}
const threshold = this._thrPc ? (height * this._thrPc) : this._thrPx;

// ******** DOM READS ABOVE / DOM WRITES BELOW ****************

const distanceFromInfinite = ((d.scrollHeight - infiniteHeight) - d.scrollTop) - reloadY;
let distanceFromInfinite: number;

if (this._position === POSITION_BOTTOM) {
distanceFromInfinite = ((d.scrollHeight - infiniteHeight) - d.scrollTop) - height - threshold;
} else if (this._position === POSITION_TOP) {
distanceFromInfinite = d.scrollTop - infiniteHeight - threshold;
}

if (distanceFromInfinite < 0) {
// ******** DOM WRITE ****************
this._dom.write(() => {
Expand Down Expand Up @@ -267,7 +288,26 @@ export class InfiniteScroll {
* to `enabled`.
*/
complete() {
if (this.state === STATE_LOADING) {
if (this._position === POSITION_TOP) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can you explain a little bit the algorithm used here?
how many frames does it take? and why?

I think I get it, but still... you probably know better

Copy link
Contributor Author

@Manduro Manduro Mar 15, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sure! I'll add a few more comments as well.

New content is being added at the top, but the scrollTop position stays the same, which causes a scroll jump visually. This algorithm makes sure to prevent this.

(Frame 1)

  1. complete() is called, but the UI hasn't had time to update yet.
  2. Save the current content dimensions.
  3. Wait for the next frame using _dom.read, so the UI will be updated.

(Frame 2)

  1. Read the new content dimensions.
  2. Calculate the height difference and the new scroll position.
  3. Delay the scroll position change until other possible dom reads are done using _dom.write to be performant.

(Still frame 2, if I'm correct)

  1. Change the scroll position (= visually maintain the scroll position).
  2. Change the state to re-enable the InfiniteScroll. This should be after changing the scroll position, or it could cause the InfiniteScroll to be triggered again immediately.

(Frame 3)

// ******** DOM READ ****************
// Save the current content dimensions before the UI updates
const prevDim = this._content.getContentDimensions();

// ******** DOM READ ****************
this._dom.read(() => {
// UI has updated, save the new content dimensions
const newDim = this._content.getContentDimensions();

// New content was added on top, so the scroll position should be changed immediately to prevent it from jumping around
const newScrollTop = newDim.scrollHeight - (prevDim.scrollHeight - prevDim.scrollTop);

// ******** DOM WRITE ****************
this._dom.write(() => {
this._content.scrollTop = newScrollTop;
this.state = STATE_ENABLED;
});
});
} else {
this.state = STATE_ENABLED;
}
}
Expand Down Expand Up @@ -319,6 +359,10 @@ export class InfiniteScroll {
ngAfterContentInit() {
this._init = true;
this._setListeners(this.state !== STATE_DISABLED);

if (this._position === POSITION_TOP) {
this._content.scrollDownOnLoad = true;
}
}

/**
Expand All @@ -333,3 +377,6 @@ export class InfiniteScroll {
const STATE_ENABLED = 'enabled';
const STATE_DISABLED = 'disabled';
const STATE_LOADING = 'loading';

const POSITION_TOP = 'top';
const POSITION_BOTTOM = 'bottom';
23 changes: 23 additions & 0 deletions src/components/infinite-scroll/test/infinite-scroll.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,29 @@ describe('Infinite Scroll', () => {

});

describe('position', () => {

it('should default to bottom', () => {
expect(inf._position).toEqual('bottom');
});

it('should set to top', () => {
inf.position = 'top';
expect(inf._position).toEqual('top');
});

it('should set to bottom', () => {
inf.position = 'bottom';
expect(inf._position).toEqual('bottom');
});

it('should not set to anything else', () => {
inf.position = 'derp';
expect(inf._position).toEqual('bottom');
});

});


let config = mockConfig();
let inf: InfiniteScroll;
Expand Down
95 changes: 95 additions & 0 deletions src/components/infinite-scroll/test/position-top/app.module.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
import { Component, ViewChild, NgModule } from '@angular/core';
import { Content, IonicApp, IonicModule, InfiniteScroll, NavController } from '../../../../../ionic-angular';


@Component({
templateUrl: 'main.html'
})
export class E2EPage1 {
@ViewChild(InfiniteScroll) infiniteScroll: InfiniteScroll;
@ViewChild(Content) content: Content;
items: number[] = [];
enabled: boolean = true;

constructor(public navCtrl: NavController) {
for (var i = 0; i < 30; i++) {
this.items.unshift( this.items.length );
}
}

doInfinite(infiniteScroll: InfiniteScroll) {
console.log('Begin async operation');

getAsyncData().then(newData => {
for (var i = 0; i < newData.length; i++) {
this.items.unshift( this.items.length );
}

console.log('Finished receiving data, async operation complete');
infiniteScroll.complete();

if (this.items.length > 90) {
this.enabled = false;
}
});
}

goToPage2() {
this.navCtrl.push(E2EPage2);
}

toggleInfiniteScroll() {
this.enabled = !this.enabled;
}
}


@Component({
template: '<ion-content><button ion-button (click)="navCtrl.pop()">Pop</button></ion-content>'
})
export class E2EPage2 {
constructor(public navCtrl: NavController) {}
}


@Component({
template: '<ion-nav [root]="root"></ion-nav>'
})
export class E2EApp {
root = E2EPage1;
}

@NgModule({
declarations: [
E2EApp,
E2EPage1,
E2EPage2
],
imports: [
IonicModule.forRoot(E2EApp)
],
bootstrap: [IonicApp],
entryComponents: [
E2EApp,
E2EPage1,
E2EPage2
]
})
export class AppModule {}


function getAsyncData(): Promise<any[]> {
// async return mock data
return new Promise(resolve => {

setTimeout(() => {
let data: number[] = [];
for (var i = 0; i < 30; i++) {
data.unshift(i);
}

resolve(data);
}, 2000);

});
}
31 changes: 31 additions & 0 deletions src/components/infinite-scroll/test/position-top/main.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
<ion-header>

<ion-toolbar>
<ion-title>Infinite Scroll</ion-title>
</ion-toolbar>

</ion-header>


<ion-content>

<ion-infinite-scroll (ionInfinite)="doInfinite($event)" position="top" [enabled]="enabled">
<ion-infinite-scroll-content>
</ion-infinite-scroll-content>
</ion-infinite-scroll>

<ion-list>
<button ion-item (click)="goToPage2()" *ngFor="let item of items">
{{ item }}
</button>
</ion-list>

<p>
InfiniteScroll is enabled: {{enabled}}
</p>

<button ion-button (click)="toggleInfiniteScroll()" block>
Toggle InfiniteScroll Enabled
</button>

</ion-content>