Build an infinitely scrolling list using only templates
Learn how to rely on the power of declarative templates to load and render paginated data from an API.
Summary
These days, there are many different ways to load and paginate data in an Ember app. In this video we'll constrain ourselves to writing template-only code in order to paginate a list of videos. We prefer the "template only" approach over others because it results in a declarative template that automatically re-renders whenever any piece of state changes, whether that's due to new network data or a user interaction.
To make this task easier, we'll use our existing data loader component as well as ember composable helpers.
First, the data loader component makes it easy for us to load and paginate videos.
<LoadRecords
@modelName="clip"
@params={{hash
sort="-position"
filter=(hash series_id=this.refactoringsSeries.id)
page=(hash limit=4 offset=4)
}}
@backgroundReload={{false}} as |isLoading isError videos|
>
{{#if isLoading}}
Loading
{{else}}
There are {{videos.length}} videos.
{{/if}}
</LoadRecords>
With this component we can change the limit
and offset
params and our backend will take care of the pagination for us.
However, there's a problem. Whenever we change the limit or offset, our component goes into a loading state as it fetches and renders the new data. This UI is jumpy, all of the existing video posters exit the page and very quickly new videos are rendered. This is not only unsettling, but it's hard to keep track of our scroll position on the page with so many elements moving in and out of the document.
Instead of using limit and offset to let the backend paginate the data, we'll render multiple LoadRecords
components, one for each page.
{{#each (range 1 this.page true) as |pageToLoad|}}
<LoadRecords
@modelName="clip"
@params={{hash
sort="-position"
filter=(hash series_id=this.refactoringsSeries.id)
page=(hash limit=4 offset=(mult (dec 1 pageToLoad) 4))
}}
@backgroundReload={{false}} as |isLoading isError videos|
>
{{#if isLoading}}
Loading
{{else}}
There are {{videos.length}} videos on page {{pageToLoad}}.
{{/if}}
</LoadRecords>
{{/each}}
<button
{{on "click" (action (mut this.page) (inc this.page 1)}}
>
Load next page
</button>
The range
helper lets us take a number, which is what our page
variable is, and convert it to an array that counts up to the number. This is needed in order to generate something we can iterate over to render multiple instances of load records.
Based on the pageToLoad
number we can calculate the limit
and offset
. The limit is always 4
, and the offset is (pageToLoad - 1) * 4
.
Finally we have a button at the very bottom of this template that increments this.page
. Whenever this.page
changes our {{#each range}}
loop will render the next page using a new <LoadRecords>
block.
Infinite scroll
Next, we'll add infinite scrolling after the user has clicked the "Load more" button at least once.
We'll use our <LoadMore />
component, which is a component that takes @shouldInfiniteScroll
and @load
properties.
{{! load-more.hbs }}
{{#if shouldInfiniteScroll}}
{{ui-hooks on-enter=(action load)}}
{{else}}
<Button
@on-click={{action load}}
@style="blue"
data-test-id="load-more"
>
Load more
</Button>
{{/if}}
And we'll render this component in our template.
<LoadMore
@load={{action (mut this.page) (inc 1 this.page)}}
@shouldInfiniteScroll={{gt this.page 1}}
/>
The @load
action will increment the this.page
count by one.
And since we want the user to have to click the load more button once, the @shouldInfiniteScroll
will only be true when this.page
is greater than one.