55

I have a view where there can be a large number of items for the user to scroll through and I'd like to implement infinite scrolling to enable progressive loading of the content.

It looks like some folks have done pagination but Google doesn't bring up anyone discussing how they've done infinite lists with Ember/Ember Data. Anyone already worked through this and have a blog post/example code to share?

Community
  • 1
  • 1
outside2344
  • 1,959
  • 1
  • 26
  • 50
  • 1
    Very good question, I hope you will have answers, as here I have absolutely no idea how to do this, and I'm pretty sure I will need it. – sly7_7 Aug 10 '12 at 19:18
  • The concept of infinite scroll seems rather simple, you're just paging your data and instead of dropping the existing content from the UI and replace it with whatever you brought from the store (usually tabular data views), you'll append it to the container (say and `ul` with several `li` elements appended to it), but there might be something else involved (like caching and things like that). I'd like to see an example of that since I don't have the time to get down to try coding it at the moment – MilkyWayJoe Aug 10 '12 at 19:46
  • Interested in this question as well -- especially in regards to how much data should be in the store and not yet shown (the caching referred to by @MilkyWayJoe). On a related note, if the top results have changed (e.g. tweets have been tweeted), how would the shift in pagination best be handled? – dechov Aug 11 '12 at 05:09
  • if an event that touches the "infinite scroll" happens on the server side - as you mentioned a new tweet's been added - the app should use one of these always connected frameworks (e.g. node.js, signalr.js) to fire something on the client to load more results. Additionally, the client must have something watching the scrolling of the page - obviously - to fire the function that consumes data from the server. – MilkyWayJoe Aug 11 '12 at 18:44

4 Answers4

61

I've implemented an infinite scroll mechanism at the GitHub Dashboard project, I'm currently developing. The feature is added in commit 68d1728.

The basic idea is to have a LoadMoreView which invokes the loadMore method on the controller every time the view is visible on the current viewport. I'm using the jQuery plugin inview for this. It allows you to register for an inview event, which is fired when the element of the specified selector is visible on screen and when it disappears.

The controller also has properties which indicate whether there are more items to load and if there are currently items fetched. These properties are called canLoadMore and isLoading.

The LoadMoreView basically looks like this:

App.LoadMoreView = Ember.View.extend({
  templateName: 'loadMore',
  didInsertElement: function() {
    var view = this;
    this.$().bind('inview', function(event, isInView, visiblePartX, visiblePartY) {
      if (isInView) Ember.tryInvoke(view.get('controller'), 'loadMore');
    });
  }
});

where the loadMore template is defined as follows:

{{#if isLoading}}
    fetching some more stuff <img width="10" src="img/ajax-loader.gif" >
{{else}}
    {{#if canLoadMore}}
        <a {{action "loadMore" target="controller" }}>click to load more items</a>
    {{else}}
        <i>no more items</i>
    {{/if}}
{{/if}}

The controller which handles the fetching of more items is then implemented as follows. Note that in the loadMore method a query on the store is performed, which loads a specific page of of entries for a model.

App.EventsController = Ember.ArrayController.extend({
  currentPage: 1,

  canLoadMore: function() {
    // can we load more entries? In this example only 10 pages are possible to fetch ...
    return this.get('currentPage') < 10;
  }.property('currentPage'),

  loadMore: function() {
    if (this.get('canLoadMore')) {
      this.set('isLoading', true);
      var page = this.incrementProperty('currentPage');

      // findQuery triggers somehing like /events?page=6 and this
      // will load more models of type App.Event into the store
      this.get('store').findQuery(App.Event, { page: page });
    } else {
      this.set('isLoading', false);
    }
  }
});

The only thing left is to initially set the content of the controller to the result of a filter function, so the content is updated when new models are loaded into the store (which happens due to the findQuery method in the loadMore of the controller). Also, a query hash is added when the filter is invoked. This ensures that an initial query to the server is made.

App.eventsController = App.EventsController.create({
    content: []
});

var events = App.store.filter(App.Event, { page: 1 }, function(data) {
    // show all events; return false if a specific model - for example a specific
    // type of event - shall not be included
    return true;
});
pangratz
  • 15,255
  • 6
  • 46
  • 73
  • 3
    Very helpful, but why are you using filter? You're not actually filtering anything (i.e. `return true`) so is there some other benefit to using filter? – Alexander Wallace Matchneer Sep 24 '12 at 03:07
  • Why are you using `Ember.tryInvoke`? – Jakub Arnold Feb 19 '13 at 16:51
  • @JakubArnold just to be sure the call doesn't error for the case where `controller` doesn't implement `loadMore` ... – pangratz Feb 19 '13 at 23:22
  • 3
    I don't really get the last step, `var event = ...`. Why is this saved in a variable? Where is the variable used and what is the use of the filter? – polyclick Feb 20 '13 at 14:41
  • I noticed that the first call to the service still gets all items instead of the first page content. Any thoughts on this? – polyclick Mar 05 '13 at 11:17
  • 1
    Amazing answer. Thank you!! I've made a version based on your work at in case it's helpful to others. https://github.com/iHiD/meducation_mobile_app/commit/8bd955df461f2813de643cc47b9d8e032b1cec9c#commitcomment-2917505. – iHiD Mar 31 '13 at 23:45
  • 1
    Instead of Ember.tryInvoke one can include Ember.ViewTargetActionSupport mixin and use view.triggerAction {action: 'loadMore'} to trigger action from controller. check it you here: http://emberjs.com/api/classes/Ember.ViewTargetActionSupport.html – Damian Walczak Oct 29 '13 at 09:30
15

Were you aware of the newly released Ember.ListView component?

https://github.com/emberjs/list-view

It was announced at the February San Francisco Ember Meetup. Here's a slidedeck from Erik Bryn, one of the Ember Core developers about using it:

http://talks.erikbryn.com/ember-list-view/

commadelimited
  • 5,100
  • 5
  • 38
  • 69
  • can this be used with ember-data and sequential page queries? – Christopher Manning Mar 18 '13 at 18:59
  • Can def be used with Ember.Data. I believe it does the pagination for you. Check out the second link above as it has a video of a presentation that Erik Bryn did. – commadelimited Mar 18 '13 at 20:25
  • The control is aware of an update to its underlying contents and if it changes the list will update. There doesn't look to be an infinite scroll behavior. – dmarr Mar 21 '13 at 03:51
  • There's a few issues that need sorting with that control as far as I can see. Firstly, it is its own frame with its own scroller -- doesn't fit into anything like bootstrap. Secondly, ember-data doesn't give out mutable datasets so there's a bit more involved in hooking everything up. – dineth Mar 22 '13 at 02:42
5

I'm writing an infinite pagination plugin for Ember based on @pangratz's work.

Please fire any issues on there if you have questions or improvements that you'd like.

iHiD
  • 2,421
  • 17
  • 32
  • I have written an infinite scroll mixin for ember js https://github.com/jeswinjose/Ember-Plugins/blob/master/infinite-scrolling-view.js – jsHero Mar 25 '15 at 06:43
1

I would recommend using Ember Infinity addon. It supports Ember 1.10 through to 2.0+. It's relatively easy to setup. You only need to modify your route and template.

Route (Product is example model):

import InfinityRoute from 'ember-infinity/mixins/route';

export default Ember.Route.extend(InfinityRoute, {
  model() {
    /* Load pages of the Product Model, starting from page 1, in groups of 12. */
    return this.infinityModel('product', { perPage: 12, startingPage: 1 });
  }
});

Template:

{{#each model as |product|}}
  ...
{{/each}}

{{infinity-loader infinityModel=model}}

When {{infinity-loader}} component becomes visible it sends an action to your route, so it knows to update model array with new (fetched) records.

First request will be sent to:

/products?per_page=12&page=1

So you also need to prepare your backend API to handle these query params. It's obviously customizable, take a look at Advanced Usage section of Readme.

Note:

Both using ListView (@commadelimited's answer) and views with ArrayController (@pangratz's answer) is deprecated/removed as of Ember 2.0 being stable version.

Daniel Kmak
  • 16,209
  • 7
  • 65
  • 83