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.

👋 Hey there, Ember dev!

We hope you enjoyed this free video 🙂

If you like it and want to keep learning with us, we've written a free 6-lesson email course about the fundamental patterns of modern component design in Ember.

To get the first lesson now, enter your best email address below:

You can also check out more details about the course by clicking here.

Questions?

us, or ask in #media on Discord