"Load More" data using ember-data and find queries


#1

Hi everyone! I have only been working with ember and ember-data for about a week now, so please bear with me if this question is really silly.

I’ve been trying to implement a feature where the user can click a button to load more images. So, when the page first loads, only the first 10 images are displayed. The user clicks a “Load More” button, and 10 more are displayed, etc. But I am really stuck right now. I’ve spent the evening searching for solutions, but I can’t figure out what to do for the life of me.

So, my route’s initial model is fairly straightforward. I have just put random dummy data into the parameters list for brevity:

App.IndexRoute = Ember.Route.extend({
   model: function() {
      return this.store.find('event', {page: 1, perPage: 10, lat: 2, lng: 2});
   }
})

(Just to note: the query is dependent on the user’s location. I’m just pointing this out because I recall reading that sending parameters to the store’s find query affects the way the results are processed.)

I am able to get this data to load on the page no problem. The issue is when the user wants to load more images. In my controller, I have a “loadMore” action where I simply just increment the page by one and call find on the store again.

 loadMore: function() {
     // Increment page
     // 
     this.store.find('image', {page: page + 1, perPage: 10, lat: 2, lng: 2})
 }

Now, from my understanding, since these queries are using extra parameters, the model won’t automatically be updated with the new records, whereas if they were omitted, the model would reflect the changes. But since I can’t remove those parameters, how would I then add these additional records to my model? I tried doing it manually after the promise was resolved… so something like this:

var self = this;
var more = this.store.find('image', {page: page + 1, perPage: 10, lat: 2, lng: 2});
more.then(function() {
   self.pushObjects(more);
});

But this didn’t work. It threw an error because the model’s array is immutable.

I really hope this is making sense. I’m still learning everything, so it’s entirely possible that I am way off base here. Any help on how to resolve this would be greatly appreciated. I don’t even know if this is the best/correct approach, so any alternative suggestions are also happily welcome.

Thank you for your time.

Mike


#2

After looking around more, I discovered that the syntax of my attempt was incorrect. I tried the following:

var self = this;
this.store.find('image', {params}).then(function(results) {
   self.pushObjects(results);
});

This also didn’t work (same reason: the model is immutable). What would be the correct approach to loading more records to an immutable model?


#3

I think you should be using this.store.findAll(‘image’, params), because this will generate a call to your API passing the params as query strings in the URL.


#4

I’ve run into the same issue. There are two solutions:

  1. Make the model a standard array and use the route hooks and “load more” actions to fill it with records from ED query results
  2. Set the model to a store.filter and have the other methods populate the store

I’ve resorted to using method 1 for my infinite scrolling/load more needs as method 2 can give you a sparsely populated list if you have been loading records in other parts of the app which makes pagination awkward.

There are some other topics worth reading:


#5

Hmm, I kind of like that store.filter approach. I might look into that later.

I came up with a solution… I think. I mean, everything is working on my page, but I don’t know if it’s the “correct” way or if there are any potential consequences.

What I did was define the model in the route like:

App.IndexRoute = Ember.Route.extend({
  model: function() {
    return this.store.find('image', params).then(function (results) {
       return results.content;
    }
 }
})

and then in the controller, the load more looks like:

loadMore: function() {
  var self = this;
  this.store.find('image', {params}).then(function(results) {
     self.pushObjects(results.content);
  }
});

Works like a charm so far, although I imagine I will lose flexibility later on.

Thank you for the responses!


#6

I had a lot of pages that required implementing infinite scrolling, so I ended up defining a mixin that I would add to any route that needed pagination / infinite scroll:

App.Paginated = Ember.Mixin.create({
  // Return an empty array immediately from the model hook.
  model: function() {
    this.set('cursor', null);
    return [];
  },

  // Wrapper around fetchPage which needs to be implemented by the route.
  // Keeps track of whether we are currently fetching a page, and saves the cursor
  // returned by the server.
  fetchPageProxy: function(cursor) {
    var self = this;
    this.set('currentlyFetchingPage', true);

    return this.fetchPage(cursor).then(function(objects) {
      self.set('cursor', objects.get('meta.cursor'));

      Ember.run.next(function() { self.set('currentlyFetchingPage', false); });

      if (objects.get('length') == 0) { self.setCanLoadMore(false); }
      else { self.setCanLoadMore(true); }

      return objects;
    });
  },

  // Set canLoadMore on the controller while setting it up.
  setupController: function(controller, model) {
    this.setCanLoadMore(true);
    controller.set('canLoadMore', this.get('canLoadMore'));
    controller.set('model', model);
  },

  // Set `canLoadMore` on the route and, if possible, on the controller.
  setCanLoadMore: function(canLoadMore) {
    this.set('canLoadMore', canLoadMore);
    if (this.controller) { this.controller.set('canLoadMore', canLoadMore); }
  },

  actions: {
    loadNextPage: function() {
      var that = this;
      if (this.get('canLoadMore') && !this.get('currentlyFetchingPage')) {
        this.fetchPageProxy(this.get('cursor')).then(function(objects) {
          that.controller.get('content').addObjects(objects);
        });
      }
    }
  }
});

Then in the route I only need to implement a fetchPage function:

App.PaginatedRoute = Ember.Route.extend App.Paginated,
  fetchPage: (page) ->
    @store.find 'model', by_model: @modelFor('model').get('id'), page: page

#7

I was able to get basic pagination to work with the following. Not sure if it’s the best approach.

Route:

App.IndexRoute = Ember.Route.extend({
  model: function() {
    return this.store.find('image');
  }
}

In Controller:

loadMore: function() {
  var metaData = this.store.metadataFor('image');
  var params = { page: metaData.page+1, per_page: metaData.per_page };

  this.store.find('image', params).then(function(images) {
    self.get('model').addObjects(images);
  });
}

#8

Thank you, this worked for me with some modifications, also i added this snippet of code to my route instead of controller, I don’t know where is the best place to add it.

    import Ember from 'ember';

    export default Ember.Route.extend({
      lastThreadsPage: false,
      limit: 25,
      offset: 0,

      model: function(){
        var self = this;
        return Ember.RSVP.hash({
          threads: this.store.find('thread', {
            limit: self.get('limit'),
            offset: self.get('offset')
          }),
          groups: this.store.findAll('group')
        });
      },

      actions: {
        loadMoreThreads: function() {
          var self = this;
          var params = {
            limit: this.get('limit'),
            offset: this.get('offset')+this.get('limit')
          };

          this.set('offset', this.get('offset') + this.get('limit'));

          this.store.find('thread', params).then(function(threads) {
            if(threads.content.length===0){
              self.set('lastThreadsPage', true);
            }
            self.currentModel.threads.addObjects(threads);
          });
        }
      }
    });