Skip to content
Brett Cassette edited this page Aug 23, 2014 · 16 revisions

Pagination is easy with ngActiveResource. First, let's go over the concepts:

1) When you want to paginate a collection, call paginate:
$scope.posts = Post.findAll();
$scope.posts.paginate();

paginate will preload the next & previous page in the collection, so your users will immediately see the next page when they hit the "next" button. One paginate has been called, there's no need to call it again; it will automatically preload again whenever the user navigates to a new page.

2) In your template, you have access to some useful helper methods:
<div ng-repeat="post in posts">
  {{post.title}}
</div>

<button ng-click="posts.next_page()" ng-disabled="!posts.next_page_exists()">
  Next
</button>

<input ng-model="page" ng-change="posts.page($scope.page)">

<button ng-click="posts.previous_page()" ng-disabled="!posts.previous_page_exists()">
  Previous
</button>
Next / Previous Page

Switch to the next or previous page.

$scope.posts.next_page();

Just like with normal queries, you can use appendQueryString to get the pagination attribute tacked onto the URL to preserve application state:

$scope.posts.previous_page({appendQueryString: true});
Next / Previous Page Exists

True / False: There is another page to move to

$scope.posts.next_page_exists();
Page

Load an exact page number.

$scope.posts.page(2);
3) Setting up pagination with your API

How do next_page_exists and previous_page_exists know how to do what they do? They receive hypermedia from your API.

Specifically, APIs tend to use the Link header to indicate the next and previous pages in a collection:

Link: '<https://api.edmodo.com/posts.json?author_id=1&page=1&per_page=5>; rel="previous", <https://api.edmodo.com/posts.json?author_id=1&page=3&per_page=5; rel="next"'

Link headers:

  • Indicate the relationship (rel of the linked resource to the current resource). The accepted relations for pagination are prev (or previous) and next. These are Internet standards, not suggestions. Check out the IANA Link Relations Registry for a list of accepted link relations.
  • May include as many related links as you want. The parser will specifically look for next, prev, and previous.
  • Are comma-separated

As long as your API sends the appropriate Link headers, pagination will work. Otherwise, ngActiveResource will read that there are no additional pages for the current collection.

Other common pagination hypermedia parsers are welcome via Pull Request.

4) Configuration Options

Pagination params can be defined globally or on a per-model basis, like other configuration. Here are the defaults for reference:

ngActiveResource.api.configure(function(config) {
  config.paginationAttribute = "page";
  config.perPageAttribute    = "per_page"; 
});

That means that the entire application will use page to request a specific page, and per_page to change the number of items requested per page.

5) Preloading Pages for Lightning Speed

If you're here because you want to turn off the preloader, just:

$scope.posts.paginate({preload: false});

If you're here because you want to understand what the preloader's doing under the hood:

Consider requesting a collection of posts:

  1. In your controller, you request all posts with an author_id of 1:
// posts_ctrl.js
$scope.posts = Post.where({author_id: 1});
$scope.posts.paginate();

Since no page is requested, ngActiveResource will automatically append page=1 as the default.

  1. We receive a response. It also lists a next (but not previous page; remember, we're on page 1) via the Link header.
Link: '<https://api.edmodo.com/posts.json?author_id=1&page=2&per_page=5>; rel="next"'
  1. Perfect! Let's load that URL to see what it contains:
[{post ...}, {post ...}, {post ...}]

Just as we suspected! More posts.

  1. Cache the preloaded posts. When the user clicks the "Next" button, they won't have to wait for the backend to reply, since we already have the request cached!

  2. Our preloaded URL returned us a Link header too. It has a next relation (page 3) and a prev relation (page 1).

Link: '<https://api.edmodo.com/posts.json?author_id=1&page=1&per_page=5>; rel="prev", <https://api.edmodo.com/posts.json?author_id=1&page=3&per_page=5>; rel="next"'
  • We've already made the request for page 1, so we do nothing with that.
  • Since the user hasn't actually navigated to page 2 yet, let's not preload page 3 until they do. Otherwise we could preload tons of pages the user never even comes close to visiting!
  • When the user visits page 2, then we'll preload page 3.
  1. If we have hypermedia for a next page, then we know next_page_exists; same with previous pages.
6) But What About Other Parameters?

Here's a common scenario: You have a page where you can filter, search, and paginate a collection. You can cut it any which way you want! How...to...do?

Simple! Since paginate is just a wrapper around where, and where clauses are infinitely chainable, your collection doesn't miss a beat:

<!-- Use an input to search your collection -->
<input ng-model="query" ng-change="posts.where({query: query})">

<!-- And a select -->
<select ng-model="author" ng-options="author.value as author.name for author in authors" ng-change="posts.where({author_id: author.id})">
</select>

<!-- And pagination -->
<button ng-click="posts.previous_page()" ng-disabled="!posts.previous_page_exists()">
  Previous
</button>

<button ng-click="posts.next_page()" ng-disabled="!posts.next_page_exists()">
  Next
</button>

Q: But doesn't that affect preloading the next and previous page?

A: Duh. We've got it covered though. Remember, these components are a part of the same library.

Q: But what about...?

A: If you have a need that's not currently addressed, please submit a PR :)

What now? How about Events?