Linking to the next/previous item in list of items

I’ve been laboring for a while now trying to figure out the best way to link to the ‘next’ item in a list of items. For example, let’s say we’re making a simple blog with nested routes:

this.resource('posts', { path: '/' }, function() {
  this.resource('post', { path: '/:post_id' })
});

In the post template I’d like to link to the next post. I’ve thought of a few ways to handle the next/previous links, but they all feel a bit hacky to me. Any thoughts on the best way to achieve this?

2 Likes

I would also love to know the solution for this.

If you can modify the backend I would include the data for the next & previous posts in the post json. Like so:

{
    id: 12,
    title: 'My second post',
    content: '<p>All the html</p>',
    previousPost: {
        id: 10,
        title: 'My first post'
    },
    nextPost: null // No posts left
}
1 Like

@rytis it feels as though the backend shouldn’t have to know which is next.

The situation I see this becoming important in is when you use ember to sort the list of posts, and you want the ‘next’ link to go to the next post as its ordered by Ember.

Any thoughts?

If you had a sorted array of posts in your PostsController you could do:

App.PostsRoute = Ember.Route({
  model: function() {
    return App.Post.find();
  }
});
App.PostsController = Ember.ArrayController();

App.PostController = Ember.ObjectController({
  needs: ['posts'], //This way this controller can access the data from the `App.PostsController` through the property `controllers.posts`
  nextPost: function() {
    this.advancePost(1);
  },
  previousPost: function() {
    this.advancePost(-1);
  },
  advancePost: function(delta) {
    var posts = this.get('controllers.posts'),
        index = posts.indexOf(this.get('content')) + delta;
    if (index >= 0 && index <= posts.get('length')-1) {
      this.transitionToRoute('posts.post', posts.objectAt(index));
    }
  }
});

nextPost and previousPost should be set up as {{action}} handlers in your template.

3 Likes

Awesome - that works and feels pretty organized. I ended up wanting a functionality that looped back to the beginning if you hit ‘next’ and there are no more, so I’m pasting my final code:

previousPost: function() {
  return this.advancePost(-1);
},
nextPost: function() {
  return this.advancePost(1);
},
advancePost: function(delta) {
  var index, length, posts;
  posts = this.get('controllers.posts');
  length = posts.get('length');
  index = (posts.indexOf(this.get('content')) + delta + length) % length;
  return this.transitionToRoute('post', posts.objectAt(index));
}
1 Like

Is there a way to do this without nesting the routes?

.i.e. this.resource(‘posts’, { path: ‘/’ }); this.resource(‘post’, { path: ‘posts/:post_id’});

Reason being, I’m showing an individual post without showing the list of all posts. When I do this with nested routes, the back button doesn’t work as expected (once you’re on a post page, you have to click the back button twice to get back to the index).

Also: The next and previous work fine w/out nested routes when navigating from index to post and back, but not when you land on a post first.

The only 2 ways I can think of knowing which post is next/previous, is either nesting the routes so that Ember has access to the entire list of posts (in which case the next post is just the next post in the arrayController), or you can use the suggestion from @rytis and return the next/previous post id in the json response of each post.

It seems that the better way to structure this is using nested routes and trying to debug your back button issue you’re having.

Thanks Matt. The back button takes me from /post_id to /posts (my index) but without refreshing the view, so it seems like an Ember issue related to the nested routes.

@bugsinamber : shot in the dark, but if you’re using Chrome and you’re initially loading your /posts route into the browser, you’re hitting a Chrome bug that’s already patched in the master branch.

You could use EmberArray - 4.6 - Ember API Documentation

(BTW, the Chrome bug workaround is in RC3)

There seems to be a third solution after all, accessing the posts without nesting routes:

App.StoryRoute = Ember.Route.extend({
  setupController: function(controller, model) {
    controller.setProperties({
      model: model,
      posts: App.Post.find()
    });
  },
  model: function(params) {
    return App.Story.find(params.story_id);
  }
});

and then, in the story controller:

advancePost: function(delta) {
  var index, length, posts;
  posts = this.get('posts');
  length = posts.get('length');
  index = (posts.indexOf(this.get('content')) + delta + length) % length;
  return this.transitionToRoute('post', posts.objectAt(index));
}

Credit: Teddy Zeenny

Thanks @stusalsbury. I was running into the issue on all browsers, but have fixed it by not nesting the routes in this case.

Hi All,

I’m new to Ember, but trying to get next/prev link working. I’m trying to cycle through images, kind of like a slideshow.

Here is what my router map looks like:

App.Router.map(function() {
  this.resource('images', function() {
    this.resource('image', { path: ':image_id' });
  });
});

I’m using handlebars to iterate over fixture data:

{{#each model}}
  {{#linkTo image this}}<div {{bindAttr class="imgclass"}}><img {{bindAttr src="href"}}></div>{{/linkTo}}   
{{/each}}

This results in urls that look like “index.html#/images/1”, “index.html#/images/2”, etc.

I’m trying to get a “next” link working to cycle through the images, 1 → 2 → 3 → 4 → etc.

I’ve tried all the solutions presented in this thread, and just can’t get it hooked up right. Help?

edit: Also, apologies if this is something better suited for SO. I felt like it was a little borderline, and since this thread already existed, I figured I’d post here.

Well… tell us what went wrong, then we can help you :slight_smile:

Doh! Sorry. Here is a jsbin. Some of the styling has been altered for the bin.

The images cascade and default to have an opacity of .5. When one is active it has an opacity of 1. I’m hoping to get the “next” action, and a “prev” action, to allow the user to cycle through the images.

You have to click on one of the images to make it active, which brings up another question: how do I redirect the route so that it starts with “/1” active?

Thanks so much!

This post helped me a lot but it did not work out of the box. Since it was posted it seems Ember.js must heavily changed. For the functionality the controller has to look like this know:

export default Ember.ObjectController.extend({
  needs: ['services'],
  actions: {
    prevItem: function() {
      return this.advanceItem(-1);
    },
    nextItem: function() {
      alert(this.index);
      return this.advanceItem(1);
    }
  },
  advanceItem: function(delta) {
    // alert(delta);
    var index, length, items;
    items = this.get('controllers.services');
    length = items.get('length');
    index = (items.indexOf(this.get('content')) + delta + length) % length;
    return this.transitionToRoute('service', items.objectAt(index));
  }
});

But I have the following problem.When I reload my browser and then click the next or previous button, they do not work and the browser‘s address bar says “undefined” instead of the right number.

Any suggestions?

ps I am new to Ember, so please have mercy!

Update: When I reload this controller does not receive anything from ‘controllers.services’. How can I change that?