Skip to content

Commit

Permalink
[amp-list-load-more] documentation, demo, bugfix, and ui tweaks (#19399)
Browse files Browse the repository at this point in the history
* Tweak infinite scroll ratio to 3 viewports down

* Add infinite scroll examples

* Minor code cleanup with bugfixes

* Implement reload after load failed

* Add documentation draft

* Update example

* Remove TODO because it's already implemented

* Undo unintentional change

* Add latency option to infinite scroll endpoints

* Fix typo

* Fix unlisten issue

* Fix nextUrl for no pages left

* Handle undefined next in infinite scroll

* Don't show the load more button if there's nothing left to load

* Refactor load and reload callbacks

* Test various falsy options for next
  • Loading branch information
cathyxz authored Nov 28, 2018
1 parent 35a0680 commit a6686fc
Show file tree
Hide file tree
Showing 5 changed files with 330 additions and 33 deletions.
48 changes: 48 additions & 0 deletions build-system/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -1164,6 +1164,54 @@ app.get('/dist/ww(.max)?.js', (req, res) => {
});
});

app.get('/infinite-scroll', function(req, res) {
const {query} = req;
const items = [];
const numberOfItems = query['items'] || 10;
const pagesLeft = query['left'] || 1;
const latency = query['latency'] || 0;

if (pagesLeft == 0) {
res.json({items: []});
}

for (let i = 0; i < numberOfItems; i++) {
const imageUrl = 'http://picsum.photos/200?' +
Math.floor(Math.random() * Math.floor(50));
const r = {
'title': 'Item ' + i,
imageUrl,
'price': i + 0.99,
};
items.push(r);
}

const nextUrl = '/infinite-scroll?items=' +
numberOfItems + '&left=' + JSON.stringify(pagesLeft - 1);

const randomFalsy = () => {
const rand = Math.floor(Math.random() * Math.floor(3));
switch (rand) {
case 1: return null;
case 2: return undefined;
case 3: return '';
default: return false;
}
};

const next = pagesLeft == 0 ? randomFalsy() : nextUrl;
const results = next === false ? {items} : {items, next};

if (latency) {
setTimeout(() => res.json(results), latency);
} else {
res.json(results);
}
});




/**
* Autosuggest endpoint
*/
Expand Down
111 changes: 78 additions & 33 deletions extensions/amp-list/0.1/amp-list.js
Original file line number Diff line number Diff line change
Expand Up @@ -112,15 +112,16 @@ export class AmpList extends AMP.BaseElement {
/** @private {?string} */
this.loadMoreSrc_ = null;
/** @private {?Element} */
this.loadMoreOverflow_ = null;
this.loadMoreButton_ = null;
/** @private {?Element} */
this.loadMoreLoadingOverlay_ = null;
/** @private {?Element} */
this.loadMoreLoadingElement_ = null;
/** @private {?Element} */
this.loadMoreFailedElement_ = null;
/** @private {?Element} */
this.loadMoreOverflowElement_ = null;
/**@private {?UnlistenDef} */
this.unlistenLoadMoreButton_ = null;

/** @private {?../../../src/service/position-observer/position-observer-impl.PositionObserver} */
this.positionObserver_ = null;

Expand Down Expand Up @@ -178,7 +179,7 @@ export class AmpList extends AMP.BaseElement {
});

if (this.loadMoreEnabled_) {
this.getLoadMoreOverflowElement_();
this.getloadMoreButton_();
this.getLoadMoreLoadingElement_();
if (!this.loadMoreLoadingElement_) {
this.getLoadMoreLoadingOverlay_();
Expand All @@ -191,12 +192,12 @@ export class AmpList extends AMP.BaseElement {
* @private
* @return {!Element|null}
*/
getLoadMoreOverflowElement_() {
if (!this.loadMoreOverflow_) {
this.loadMoreOverflow_ = childElementByAttr(
getloadMoreButton_() {
if (!this.loadMoreButton_) {
this.loadMoreButton_ = childElementByAttr(
this.element, 'load-more-button');
}
return this.loadMoreOverflowElement_;
return this.loadMoreButton_;
}

/** @override */
Expand Down Expand Up @@ -371,13 +372,31 @@ export class AmpList extends AMP.BaseElement {
items = items.slice(0, maxLen);
}
return this.scheduleRender_(/** @type {!Array} */(items), !!opt_append);
}, error => {
throw user().createError('Error fetching amp-list', error);
});
}
return fetch.catch(error => this.showFallback_(error));

return fetch.catch(error => {
if (opt_append) {
this.handleLoadMoreFailed_();
} else {
this.showFallback_(error);
}
});
}

/**
* When the fetch fails, we should show the load-more-failed element if
* one exists, otherwise show the load-more-button element that triggers
* a new fetch on click.
* @private
*/
handleLoadMoreFailed_() {
if (this.loadMoreFailedElement_) {
this.setLoadMoreFailed_();
} else {
this.setLoadMoreReload_();
}
}
/**
* Proxies the template rendering to the viewer.
* @param {boolean} refresh
Expand Down Expand Up @@ -670,42 +689,64 @@ export class AmpList extends AMP.BaseElement {
* @private
*/
setLoadMore_() {
if (!this.loadMoreSrc_ && !this.loadMoreOverflow_) {
// Done loading, nothing more to load.
if (!this.loadMoreSrc_) {
this.loadMoreButton_.classList.toggle('amp-visible', false);
return;
}
const triggerOnScroll = this.element.getAttribute('load-more') === 'auto';
if (triggerOnScroll) {
this.maybeSetupLoadMoreAuto_();
}
if (this.loadMoreOverflow_) {
if (this.loadMoreButton_) {
this.mutateElement(() => {
this.loadMoreOverflow_.classList.toggle('amp-visible', true);
listen(this.loadMoreOverflow_, 'click', () => this.loadMoreCallback_());
this.loadMoreButton_.classList.toggle('amp-visible', true);
this.unlistenLoadMoreButton_ = listen(this.loadMoreButton_, 'click',
() => this.loadMoreCallback_());
});
}
if (!this.loadMoreOverflow_ && !triggerOnScroll) {
if (!this.loadMoreButton_ && !triggerOnScroll) {
user().error(TAG,
'load-more is specified but no means of paging (overflow or ' +
'load-more=auto) is available', this);
}
}

/**
* Called when a fetch fails under load-more. Shows the load-more-button
* element and triggers a reloading of the failed src on click.
* @private
*/
loadMoreCallback_() {
if (!this.loadMoreSrc_) {
return;
setLoadMoreReload_() {
if (this.loadMoreButton_) {
this.mutateElement(() => {
this.loadMoreButton_.classList.toggle('amp-visible', true);
this.unlistenLoadMoreButton_ = listen(this.loadMoreButton_, 'click',
() => this.loadMoreCallback_());
});
}
if (this.loadMoreOverflow_) {
this.loadMoreOverflow_.onclick = null;
}

/**
* Called when 3 viewports above bottom of automatic load-more list, or
* manually on clicking the load-more-button element. Sets the amp-list
* src to the bookmarked src and fetches data from it.
* @private
*/
loadMoreCallback_() {
if (this.loadMoreSrc_) {
this.element.setAttribute('src', this.loadMoreSrc_);
this.loadMoreSrc_ = null;
}
this.element.setAttribute('src', this.loadMoreSrc_);
this.loadMoreSrc_ = null;
this.toggleLoadMoreLoading_(true);
return this.fetchList_(/* opt_append */ true)
.catch(() => this.setLoadMoreFailed_())
.then(() => this.toggleLoadMoreLoading_(false));
.then(() => {
this.toggleLoadMoreLoading_(false);
if (this.unlistenLoadMoreButton_) {
this.unlistenLoadMoreButton_();
this.unlistenLoadMoreButton_ = null;
}
});
}

/**
Expand All @@ -727,44 +768,48 @@ export class AmpList extends AMP.BaseElement {
this.loadMoreLoadingOverlay_ = createLoaderElement(
this.win.document, 'load-more-loading');
this.loadMoreLoadingOverlay_.setAttribute('load-more-loading', '');
this.loadMoreOverflow_.appendChild(this.loadMoreLoadingOverlay_);
this.loadMoreButton_.appendChild(this.loadMoreLoadingOverlay_);
}
return this.loadMoreLoadingOverlay_;
}

/**
* Toggles the visibility of the load-more-loading element, the
* amp-load-more-loading CSS class, and the active state of the loader.
* @param {boolean} state
* @private
*/
toggleLoadMoreLoading_(state) {
if (this.loadMoreLoadingElement_) {
this.mutateElement(() => {
if (state) {
this.loadMoreOverflow_.classList.toggle('amp-visible', false);
this.loadMoreButton_.classList.toggle('amp-visible', false);
}
this.loadMoreLoadingElement_.classList.toggle('amp-visible', state);
});
} else if (this.loadMoreOverflow_) {
} else if (this.loadMoreButton_) {
this.mutateElement(() => {
this.loadMoreOverflow_.classList.toggle('amp-load-more-loading', state);
this.loadMoreButton_.classList.toggle('amp-load-more-loading', state);
this.loadMoreLoadingOverlay_.classList.toggle('amp-active', !state);
});
}
}

/**
* Shows the load-more-failed element and hides the load-more-button
* element.
* @private
*/
setLoadMoreFailed_() {
if (!this.loadMoreFailedElement_ && !this.loadMoreOverflow_) {
if (!this.loadMoreFailedElement_ && !this.loadMoreButton_) {
return;
}
this.mutateElement(() => {
if (this.loadMoreFailedElement_) {
this.loadMoreFailedElement_.classList.toggle('amp-visible', true);
}
if (this.loadMoreOverflow_) {
this.loadMoreOverflow_.classList.toggle('amp-visible', false);
if (this.loadMoreButton_) {
this.loadMoreButton_.classList.toggle('amp-visible', false);
}
});
}
Expand Down Expand Up @@ -803,7 +848,7 @@ export class AmpList extends AMP.BaseElement {
this.positionObserver_.observe(this.container_,
PositionObserverFidelity.LOW,
({positionRect, viewportRect}) => {
const ratio = 1.5;
const ratio = 3;
if (this.loadMoreSrc_ &&
positionRect.bottom < ratio * viewportRect.bottom) {
this.loadMoreCallback_();
Expand Down
29 changes: 29 additions & 0 deletions extensions/amp-list/amp-list.md
Original file line number Diff line number Diff line change
Expand Up @@ -281,6 +281,35 @@ We recommend using `binding="no"` or `binding="refresh"` for faster performance.

If `binding` attribute is not provided, default is `always`.

## Experimental: Infinite Scroll (amp-list-load-more)
We've introduced an experiment called `amp-list-load-more` as an implementation for pagination and infinite scroll in amp-list. This is an experimental feature, and final APIs may change.

### Attributes
#### load-more (mandatory)
Adding this attribute will allow amp-list (with no value) to show a “load-more" button at the end of the amp-list. The value of this attribute can be set to “auto" to trigger automatic loading more elements three viewports down for an infinite scroll effect.

#### load-more-bookmark (mandatory)
This attribute specifies an attribute in the returned data that will give the url of the next items to load. E.g. In the following sample payload, we would specify `load-more-bookmark="next"`.

```
{ "items": [], "next": "https://url.to.load" }
```

### Additional children of `<amp-list>`
`<amp-list>` with the `load-more` attribute expects the following additional child elements:

#### load-more-button (mandatory)
An element containing the `load-more-button` attribute. Clicking on this button will trigger a fetch to load more elements from the url contained in the field of the data returned corresponding to the `load-more-bookmark` attribute.

In the case of `load-more="auto"`, or infinite scroll, this button will show up if the user has reached the end of the list but the contents are still loading.

#### load-more-failed (optional)
An element containing the `load-more-failed` attribute. This element will be displayed at the bottom of the `<amp-list>` if loading failed. If this element is not provided, the `load-more-button` element will be displayed and clicking on it will result in an attempt to re-fetch data from the last (failed) url.

#### .amp-load-more-loading (css class)
This class is applied to the element with the `load-more-button` attribute while the data is loading. This can be used to tweak the visual appearance of the load-more-button (e.g. show a loader) when the `<amp-list>` is in the middle of loading data.


##### common attributes

This element includes [common attributes](https://www.ampproject.org/docs/reference/common_attributes) extended to AMP components.
Expand Down
Loading

0 comments on commit a6686fc

Please sign in to comment.