Angular - Limiting


In this video we are going to add "limiting" functionality to our Angular CRUD implementation. What does this mean?

Put simply it means we will add a numerical input field which will allow the user to easily choose how many record (blog posts) to show on any given page of the list view.

By default we will set this to 10, and we will ensure that whenever the user changes the value, the list will update - without a page refresh - to show that amount of records.

Now, as with the previous two videos, we will not need to do a great deal on the front end to make this work. This is because all the hard work is done by the KNP Pagination Bundle on the API / back-end.

We do need to ensure that our UI Bootstrap Pagination Directive is kept in sync with our changes, however, otherwise it will introduce bugs into our list view whenever we do anything with the limit input.

Where Does The Limit Come From?

Much like how we add in a field to sort on, a direction to sort in, or the page of results to display, the limit is applied via the URL:

http://api.symfony-3.dev/app_dev.php/posts?limit=10

All we need to do is to make sure we pass through a limit parameter via the URL, and KNP Pagination Bundle will handle the rest for us.

Some variants of this that would also work:

  • http://api.symfony-3.dev/app_dev.php/posts?limit=7500
  • http://api.symfony-3.dev/app_dev.php/posts?limit=50
  • http://api.symfony-3.dev/app_dev.php/posts?limit=
  • http://api.symfony-3.dev/app_dev.php/posts

There's a couple of confusing entries there, but let's address them in order.

http://api.symfony-3.dev/app_dev.php/posts?limit=7500

Be careful of this one. We don't currently have any 'protection' in place to stop a user pulling every record out of the DB by simply using a massive limit value.

If you are going to add some "protection" here, be sure to add this to the API side. Adding it in the Angular code won't properly protect you from savvy users, and worse, you will have to remember to add in the protective code to every front end implementation you create. You could add in some guarding code around this line.

http://api.symfony-3.dev/app_dev.php/posts?limit=50

This one is fairly straightforward, and is only here to show that other numbers than 10 are viable options :)

  • http://api.symfony-3.dev/app_dev.php/posts?limit=
  • http://api.symfony-3.dev/app_dev.php/posts

These two are more interesting.

They behave identically. That is to say, the both give you the default limit.

Remember, from the PHP code we provide a default limit value of 10 unless one is provided:

// /src/AppBundle/Controller/BlogPostsController.php

    public function cgetAction(Request $request)
    {
        // * snip *

        return $this->get('knp_paginator')->paginate(
            $queryBuilder->getQuery(), /* query NOT result */
            $request->query->getInt('page', 1), /*page number*/
            $request->query->getInt('limit', 10)/*limit per page*/
        );
    }

So we always have a limit, even if one is not provided. However, as mentioned, this does not protect from very high numbers, so do consider that if using this implementation.

Adding In The Limit input

Adding the limit functionality to our Angular project is fairly straightforward.

To begin with, we will declare a limit variable on our $scope. However, limit is such a computer science term. Really we care about the number of items per page. Let's just call this $scope.itemsPerPage:

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

'use strict';

angular.module('myApp.blogPost')

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

    // * snip *
    $scope.itemsPerPage = 5;

I've set the itemsPerPage to 5 to better demonstrate that we can override the default value of 10.

This itemsPerPage variable will become the model for our limiter input:

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

<label for="limiter">Limit</label>
<input ng-model="itemsPerPage" type="number" id="limiter">

<table class="table table-hover table-responsive">
    <thead>

That's enough to get a numeric input field on our page which will allow a user to indirectly change the limit.

Updating The API Request

As all the real work takes place on the back-end, and the only way to talk to the back-end is via modification of the URL used to make requests, we must first ensure that our API function is updated to handle a limit value:

// /app/blogPost/Api.js

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

Much like in the previous video we give ourselves the option to either pass in a value, or set a default (of 10 in this instance) if one is not provided.

We will be initially overriding this value with 5 though.

As we have changed the getAll function, be sure to go back through the listController and update any calls to the getAll method, e.g.:

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

    var getBlogPosts = function (page, itemsPerPage, sortBy, direction) {
        Api.getAll(page, itemsPerPage, sortBy, direction)
            .then(function (result) {

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

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

and

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

  getBlogPosts(1, $scope.itemsPerPage);

There are others, so be sure to update each in turn.

At this stage, when you first load the list view, you should now see only 5 results in the list.

However, this has broken the UI Bootstrap Pagination Directive, so let's quickly fix that.

Fixing The Pagination Directive

The first problem is that the Pagination Directive still displays only 5 pages - it still believes we have 10 items per page.

Changing this is really easy to do. We just need to use the items-page-page setting:

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

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

And we should now be displaying 10 pages in the paginator when the page first loads.

The second problem is not immediately obvious.

This one will only show up when we use the limiter beyond a trivial test.

As soon as we change the value in the limiter, e.g. adding a zero to our initial value of 5 to make it 50, we can see that straightaway, our list view fills with 50 results. Nice.

However, should we change this to 25, it doesn't work. Boo. Well, it's not that it doesn't work completely, it just ends up in a very unexpected state (see the video around 2:30).

Fixing this involves adding a $watch function to the limiter input to ensure it properly updates.

Running A Function When The Limit Changes

We are going to make use of Angular's $watch functionality to listen for changes to a scope variable - $scope.itemsPerPage in this instance - which allows us to run a function (a callback) when this value changes.

This may sound confusing, but is thankfully quite simple to do:

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

    $scope.$watch('itemsPerPage', function(newValue, oldValue) {
        console.log('itemsPerPage changes', newValue, oldValue); // not used, just for demonstration
        getBlogPosts($scope.currentPage, $scope.itemsPerPage, $scope.propertyName, reversedAsString($scope.reverse));
    });

And that should be good enough to fix our problem.

With that we have a working - if rather basic - implementation of a simple limiter input which allows our users to determine how many records they would like to see per page.

Code For This Video

Get the code for this video.

Episodes