headerPart2.jpg

Angular Directive: Sortable Table Pt. II, Pagination

UPDATE (May 2016): This is written for Angular 1.x. While Angular 1.x is not close to being deprecated, we have an updated series written specifically for Angular 2. Check out part 1 of the new series to learn how to migrate Angular 1.x filters to Angular 2 pipes, and part 2 for how to migrate your Angular 1.x directives to Angular 2 components.

Continuing off of the last Angular Directive blog about a sortable table directive there were still some features missing that could be added. The example was shown with only a small amount of data, but what about large amounts of data and the performance degradation? How would that data look? We’re going to take this directive another step by adding in pagination.

Angular Directive: Sortable Table with Pagination

For those that are not sure what pagination is, to simply put it, it is a way to numerically order pages of any kind of document or material. For websites, pagination is a visual representation and navigation to go from page to page of data.

There are 3 major features we need our pagination to have:

  • Change the amount of data shown per page
  • Going to the next and previous page
  • Jumping to a certain page number

Specify Amount of Data Per Page

How are we going to have our table to show data per page rather than all at once? Angular provides us with a few ng-repeat filters that can be used with what we need. The filter that’s the most beneficial to us is limitTo. What limitTo does is tell the ng-repeat to only loop up to the number specified. So no matter how much data is being looped, if limitTo is set to 5, the ng-repeat will only be looped to 5.

That helps with the amount of data to be shown per page, but are we going to make pages work for any amount of data? The fix I came up with is to specify where in the array of data the counting begins, and to do that we need to make another directive:

.filter('startFrom', function () {
  return function (input, start) {
    return input.slice(parseInt(start));
  }
})

What this filter does is take in the input, which is whatever the filter is put on, in this case it is our data. Then the start parameter is the number we are going to pass through in the filter. From there we slice the data array at the point we specify. So slicing an array of [‘a’, ‘b’, ‘c’, ‘d’, ‘e’] at step 2 will return an array of [‘c’, ‘d’, ‘e’]. Exactly what we need!

Now let’s add some functions and variables to the scope for the page size:

scope.pageSizes = [1, 5, 10, 25, 50];
scope.pageSize = scope.pageSize ? parseInt(scope.pageSize) : 5;
scope.changePageSize = function(newSize) {
    scope.pageSize = parseInt(newSize);
};

The array of numbers in pageSizes is what we will use to display the different options the user has for the amount of data per page. The pageSize variable is here to default to 5 the user hasn’t specified a page size yet. And then I also added a function to change the page size based on a parameter.

Change Page That User is Viewing

Now that we have the table display functionality, we need to create functions to control those numbers.

scope.currentPage = 0;
scope.numberOfPages = Math.ceil(scope.data.length / scope.pageSize);
scope.changePage = function(newPage) {
    scope.currentPage = newPage;
};

scope.$watch('pageSize', function() {
    //Recalculate number of pages
    scope.numberOfPages = Math.ceil(scope.data.length / scope.pageSize);

    //If current page doesn't exist anymore set to last page
    if (scope.currentPage >= scope.numberOfPages) {
        scope.changePage(scope.numberOfPages - 1);
    }
});

In here we just set a hard coded value of current page at 0 to always start the user on the first page on load. We set current page to 0 because arrays start counting at 0, not 1. The value is what will be used for our startFrom filter. Then we do some math to find the total amount of pages based on the amount of data and the page size. There’s a function to change the current page to whatever is passed as a parameter. And finally there is a $watch function, which watches a specified variable of the scope and when that variable changes at all, the function runs. The great thing about this watch is that the scope is reapplied to the DOM and allows for directive to re-render with the necessary variables.

The Layout

Let’s plan out our directive’s new layout using Bootstrap. Here is the basic HTML layout that we want to be interactive:


 

1 / 2

 

Jump to:

Now we have to make this static HTML interactive, so let’s break it down per section.

Pagination Template

This is the barebones html for this part of the template:


 

{{currentPage + 1}} / {{numberOfPages}}

 

Jump to:

We only want the pagination to show if there are more than 2 pages, otherwise we just want to allow them to change the amount per page. There are 2 buttons that basically move the page forward and backward, then are disabled if they are on the first or last page respectively. In between the buttons are a simple output of the current page and total number of pages. You’ll notice the current page is displayed as currentPage + 1 because remember, arrays start counting at 0! So page 0 is really the first page.

The next form group is the select for the user to jump to a certain page. We set a model to the select of currentPage so that the actual currentPage scope variable is bound to the select. So if the variable changes so will the select. Unfortunately, you’ll see I left a comment here saying we need a for loop. This is because Angular doesn’t have an ng-repeat where you can specify the number of loops by number. So let’s create our own!

.filter('range', function () {
  return function (input, min, max) {
    min = parseInt(min); //Make string input int
    max = parseInt(max);
    for (var i = min; i < max; i++)
        input.push(i);
    return input;
  };
})

This filter creates an array based on a number range in 2 parameters. Now let’s add the options to our select!


You’ll see that we added ng-options to the select which creates options based on our range from 0 to the numberOfPages. Again, you’ll see n+1 which is what we want the user to see. So an example option that is created from this is:

n+1

where n equals whatever number of the loop it’s on.

Results Per Page Template

Below is the HTML layout we want our results per page to display as:


 

 

You’ll notice here that we have a simple list of different page sizes to select from. To start we want to be able to select page sizes from the scope variable scope.pageSizes = [1, 5, 10, 25, 50]. You’ll also notice that we have an option to view all rows at once, no matter the size. Below is how we accomplished this in a clean manner using our scope from above:


 

    • Results per page:

 

 

 

You’ll notice that we are looping over our scope.pageSizes variable and checking to see if we should show each page size based on the size of our scope.data array. If the current page size in the loop is less than the length of the data array display it. Then always display the “All” page size option. Adding an ng-click attribute to each of the page sizes and the “All” option, we can run our changePageSize() function. We pass through the wanted size per page, and our DOM is immediately updated with the change!

Add More Data!

Since we have this super useful pagination ability implemented, lets add some more data to our table so we can actually use it. In our controller, we’re going to change up the make up of our table a little so that we can better understand how great this really is.

If you recall from part 1 of this blog, our directive takes in 3 parameters: columns, data, and sort. Let’s update each of these parameters in our controller:

$scope.columns = [
    {
        display: 'Loop Number', //The text to display
        variable: 'Id', //The name of the key that's apart of the data array
        filter: 'number : 0' //The type data type of the column (number, text, date, etc.)
    },
    {
        display: 'Name',
        variable: 'Name',
        filter: 'text'
    },
    {
        display: 'Amount',
        variable: 'Amount',
        filter: 'number : 0'
    },
    {
        display: 'DateTime',
        variable: 'Date',
        filter: 'dateTime'
    }
];

Our data array needs to be made up of objects that match the variables of each of the columns. We also want to add way more rows than just 3, like our last blog’s example, so we’re going to push a large number of objects into our new data array:

$scope.rows = []; //make a new array to push to

for(var i = 1; i <= 52; i++){
  $scope.rows.push({
    'Id': i,
    'Name': Math.random().toString(36).substring(7),
    'Amount': Math.floor((Math.random() * 12345)),
    'Date': new Date(new Date() - (Math.floor((Math.random() * 12345)) * 100000000))
  })
}

What this code is doing is creating a new array in the controller’s $scope and then looping 52 times. Each time a new object is pushed to the array with properties that match the variable of each of the columns above. Here is a breakdown of what each column is doing:

  • Id is the loop number
  • Name is a randomized string
  • Amount is a random number between 0 and 12345
  • Date is a random date in between the current time and about 40 years ago.

And finally our new sort sorts by the Id column ascending:

$scope.sort = {
        column: 'Id', //to match the variable of one of the columns
        descending: false
};

Finished Product

And that’s it! You should now have a fully functional sortable table directive with pagination!

Error: Embedded data could not be displayed.

We recently updated our Fuel Gauge Dashboard to use this new feature, and it really helps us display more data in a pleasing manner!

Leave a comment below!

comments

Cory ShawAngular Directive: Sortable Table Pt. II, Pagination
Share this post