Angular - Pagination


In this video we are going to add Pagination to our Angular application, making use of the pagination facilities we added to our Symfony 3 REST API in the previous video.

Often in Angular / JavaScript tutorials you will see how to paginate on the client side. JavaScript is undeniably pretty awesome at stuff like this, with map, reduce, and all those powerful, more functional constructs at our disposal.

The thing is, if pagination is already a feature of our API, we don't need to re-implement it in the same way on the client side. Which is kinda sad, because that's a cool exercise, but also, we did it already in PHP, so why re-invent the wheel?

Ok, so with that in mind we are going to add pagination by way of a Bootstrap pagination widget to our Angular frontend. This widget allows us to slot the functions we wish to call whenever interesting things happen on that widget.

For example, when a user clicks one of the page numbers, we want to "hear" that click, get told what page number that represents, and then compose a URL which we can call on our API to get back just the data they requested.

Getting Started

To make life easy for ourselves, we are going to continue on with the Angular / JavaScript code we created during this series. If you haven't completed this series yet then grab it from GitHub and follow along. Note there are two branches on there, and we are going to start from master.

And in that same spirit of making lives easier for ourselves, we will make use of the [UI-Bootstrap Pagination directive][4. As they put it:

A lightweight pagination directive that is focused on ... providing pagination & will take care of visualising a pagination bar and enable / disable buttons correctly!

One thing to note here is that because we are re-using an existing project, we have made an assumption in the way that our data will be arriving from the API. This is because we were making requests to the API before pagination was implemented. Now that pagination is implemented, the data shape will have changed, and we need to fix that to restore our Angular app back to a working state.

I'd advise watching the video (around the 1:40 mark onwards) for a better, more visual representation of this problem.

Thankfully, this is very easy to fix.

We just need to change the 'thing' we are interested in on our API response, from:

// app/blogPost/list/listController.js

Api.getAll()
  .then(function (result) {
    $scope.blogPosts = result.data;
  }, function (error) {
    console.log('error', error);
  });

to:

Api.getAll()
  .then(function (result) {
    $scope.blogPosts = result.data.data;
  }, function (error) {
    console.log('error', error);
  });

The high level reasoning for this is because before we had pagination on our API, the only thing being return was our list of blog posts.

After we have added pagination, we get back a bunch of pagination related info, such as current page number, amount of entries per page, and so on. One of those fields is 'data', which contains our blog post data. That's why we end up with result.data.data. You can rename this, again, see the video for more.

With that in place, our Angular implementation should be working once again.

Adding Angular UI Bootstrap

To use the Pagination directive we must bring the code into our project. You can do this in a few different ways.

Likely the most preferable way would be to use :

npm install angular-ui-bootstrap --save

But you could also do :

bower install angular-bootstrap --save

Both of which will go ahead and pull in the latest version to your project directory. If you are following along with the video code, you will need to update the index.html file to reference this new JS code:

<!-- /app/index.html -->

  <script src="bower_components/angular/angular.js"></script>
  <script src="bower_components/angular-route/angular-route.js"></script>
  <script src="bower_components/angular-bootstrap/ui-bootstrap.js"></script>

  <!-- etc -->

  </body>
</html>

And also add it as a dependency to your Angular app:

// /app/app.js

'use strict';

// Declare app level module which depends on views, and components
angular.module('myApp', [
  'ngRoute',
  'ui.bootstrap',
  'myApp.blogPost'
]).

With that in place, we can now use the Pagination (or any other UI Bootstrap) directive in our project

Using The Pagination Directive

Thankfully, the documentation provides plenty of examples of how to use the Pagination directive.

I'm going to go ahead and add the Pagination directive just above the Create button:

<!-- /app/blogPost/list/list.html -->

    <!-- * snip * -->
    </tbody>
</table>

<uib-pagination total-items="totalItems"
                items-per-page="itemsPerPage"
                ng-model="currentPage"
                ng-change="pageChanged()"></uib-pagination>

<a href="#!create" class="btn btn-lg btn-success">Create</a>

And what this has done is hooked up the Pagination directive to our Scope. This will mean the Pagination component is expecting certain variables to be available on the listController's scope - totalItems, itemsPerPage, currentPage, and also a function, pageChanged which will be invoked whenever the paginator is clicked on.

Let's add these variables to our scope:

// /app/blogPost/list/listController.js

'use strict';

angular.module('myApp.blogPost')

.controller('listController', ['$scope', 'Api', '$filter', function($scope, Api, $filter) {

  $scope.blogPosts = [];
  $scope.totalItems = 1;
  $scope.currentPage = 1;
  $scope.itemsPerPage = 10;

  // * snip *

Easy enough.

Now, adding the function is no different:

  $scope.pageChanged = function () {
    console.log('called page changed', $scope.currentPage);
  };

We've now setup some variables with default values, so when the page loads, the paginator should have 1 page, which is also the current page, and we would anticipate their being 10 items per page.

We've also added a function - pageChanged - which will be invoked whenever the Pagination directive has a button clicked, and it should simply log out the currentPage.

This should work, and it should allow you to load the page, but it's really not very interesting so far.

What we really need to do is populate these variables with their proper values. And to do that we can grab them from the API response, which we already get whenever we load the page:

// /app/blogPost/list/listController.js

    Api.getAll()
        .then(function (result) {

            console.log('result', result);

            $scope.blogPosts = result.data.data;
            $scope.totalItems = result.data.totalCount;
            $scope.currentPage = result.data.currentPageNumber;

        }, function (error) {
            console.log('error', error);
        });

All this is cool and you should see now that the Pagination directive shows the correct number of pages to choose from. But still, it doesn't quite work as expected. All we see is a console log statement with the page number that we wanted, rather than any real API request to actually GET that data.

To fix this, we need to implement the pageChanged function.

What we want to happen here is that whenever the page is changed, a new API request is sent in, and the list data should be updated from the response.

To do this, we need to call Api.getAll() again, only this time, we must pass in some parameters to ensure we get back the correct page.

GET All On Command

Currently this Api.getAll() function is only triggered on page load.

We need a way to not only call this function on page load, but whenever we click the pagination buttons.

To fix this, we can place our Api.getAll() function call inside a variable. This is such a cool feature of JavaScript - being able to treat functions as though they were any other kind of object. I love this.

    var getBlogPosts = function () {
        Api.getAll()
            .then(function (result) {

                console.log('result', result);
                $scope.blogPosts = result.data.data;
                $scope.totalItems = result.data.totalCount;
                $scope.currentPage = result.data.currentPageNumber;

            }, function (error) {
                console.log('error', error);
            });
    };

Careful though, this will have now broken our implementation. Remember, prior to this change, our function was invoked on page load. To continue doing this, we must explicitly call the function stored inside our getBlogPosts variable:

    var getBlogPosts = function () {
        Api.getAll()
            .then(function (result) {

                console.log('result', result);
                $scope.blogPosts = result.data.data;
                $scope.totalItems = result.data.totalCount;
                $scope.currentPage = result.data.currentPageNumber;

            }, function (error) {
                console.log('error', error);
            });
    };

    getBlogPosts();

Ok cool. That's that problem solved, and our page should be loading properly again.

But we still haven't fixed the paginator.

We can do this in a basic way by updating the pageChanged function to call getBlogPosts():

  $scope.pageChanged = function () {
    console.log('called page changed', $scope.currentPage);
    getBlogPosts();
  };

So now, whenever the paginator is clicked, we should see both the console.log output, and in the network tab, also a request to our API.

Only, it's always the same API call. Well, that's because we aren't passing in the page number that we want, and even if we did, it wouldn't do anything currently. Let's fix that also:

  $scope.pageChanged = function () {
    console.log('called page changed', $scope.currentPage);
    getBlogPosts($scope.currentPage);
  };

Ok, now we pass the page we want to the getBlogPosts function. But that doesn't yet expect a parameter, so we need to update that also:

    var getBlogPosts = function (page) {
        Api.getAll(page)
            .then(function (result) {

                console.log('result', result);
                $scope.blogPosts = result.data.data;
                $scope.totalItems = result.data.totalCount;
                $scope.currentPage = result.data.currentPageNumber;

            }, function (error) {
                console.log('error', error);
            });
    };

    getBlogPosts(1);

There's three important changes here.

Firstly, we have added the page parameter to our getBlogPosts function arguments.

Then, we immediately pass this through to Api.getAll(page) - so we will need to update the Api code also.

Then, we have set the call to getBlogPosts(1); which triggers when the page is first loaded to pass through 1 as its parameter. That way, on initial page load, we always get the first page of results. Nice.

Updating the Api code is our next task:

// app/blogPost/Api.js

'use strict';

angular.module('myApp.blogPost')

    .factory('Api', [
        '$http',
        function ($http) {

            var ROOT_URL = 'http://api.symfony-3.dev/app_dev.php/posts';

            // snip

            function getAll(page) {
                return $http({
                    url: ROOT_URL,
                    method: 'GET',
                    params: {
                        page: page || 1
                    }
                });
            }

            // snip

All we are doing here is taking the passed in page and ensuring that the page parameter ends up on the URL we use to make the API request. If for some reason we do not pass in a page parameter, then it will default to the number 1.

And with that done, we should now be paginating :)

Code For This Video

Get the code for this video.

Episodes