Angular - Sorting


In this video we are going to add in Sorting into our Angular CRUD implementation. This will function (almost) identically to the Twig implementation, giving our end-user the ability to click on the id and / or title table column headings, and this will order the data into ascending, or descending order. Clicking each column multiple times will toggle between the ascending, or descending orders.

The way in which this differs from the Twig implementation is in that we no longer need to do a full page refresh whenever we click the table column headings. I know, I know, this is some futuristic web dev :)

To begin implementing this functionality, we will wrap both the title and id table row headings inside a span, and add an ng-click directive to this span:

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

<table class="table table-hover table-responsive">
    <thead>
    <tr>
        <th><span ng-click="sortBy('bp.id')">id</span></th>
        <th><span ng-click="sortBy('bp.title')">Title</span></th>
        <th>Options</th>
    </tr>
    </thead>
    <tbody>

<!-- * snip * ->

We will make use of the same function, but change the argument depending on what we clicked.

The fact that we need to pass in the prefix of bp. is a little icky for my tastes, but that's how it has to be. We could try and be clever about this, but honestly, for the amount of extra work that would cause, it really isn't worthwhile for this demonstration.

Anyway, we've used this function yet it doesn't currently exist. So let's add it:

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

// * snip *

    $scope.sortBy = function (propertyName) {
        console.log('sort by property name', propertyName);
    };

// * snip *

And at this stage, you should be able to click either span in the table row heading, and see the console.log output as appropriate.

As we progress through the next few videos, we are going to need to use this propertyName variable again at various points, so for the moment, let's ensure we update the variable on our scope so it isn't lost:

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

    $scope.propertyName = null;

// * snip *

    $scope.sortBy = function (propertyName) {
        console.log('sort by property name', propertyName);
        $scope.propertyName = propertyName;
    };

// * snip *

Next, we have the requirement that the first time the span is clicked then the sorting should be in the ascending order. If you click the same span again, it should be descending, and then click again, ascending... and so on.

Anything For An Easy Life? true

To make this as easy as possible for us, I would suggest using a boolean value (a true or false) and essentially go ascending when true, and descending when false. By using a boolean, we can easily 'flip' the value by negating / taking the opposite of what we already have.

To begin with, we will start with a true:

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

    $scope.propertyName = null;
    $scope.reverse = true;

// * snip *

    $scope.sortBy = function (propertyName) {
        console.log('sort by property name', propertyName);
        $scope.propertyName = propertyName;
        $scope.reverse = !$scope.reverse;
        console.log('sort by - $scope.reverse', $scope.reverse);
    };

// * snip *

And so any time the sortBy function is invoked, whatever the value of $scope.reverse was, will be inverted - true becomes false, false becomes true.

However, the boolean value is not directly useful too us. Instead, we need to convert the truthy / falsy representation to a string. We can write a quick function for this:

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

// * snip *

    var reversedAsString = function (bool) {
        return bool === true ? 'asc' : 'desc';
    };

// * snip *

All this function does is evaluate the given argument (bool), and if it is true, then return asc, or it will return desc. If this syntax looks funky to you, then this documentation on the Ternary Operator will see you right.

Pass Through

We now have the property we want to sort by (propertyName), and the direction we want to sort by (reversedAsString($scope.reverse)). We need to send this info to the API, which means updating the getAll function in our Api.js file:

// /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, sortBy, direction) {
                return $http({
                    url: ROOT_URL,
                    method: 'GET',
                    params: {
                        page: page || 1,
                        sort: sortBy || '',
                        direction: direction || ''
                    }
                });
            }

Again, much like [the previous video][3] we pass in the given values, or default back - in this case - to empty strings. This means it will work either with or without a value.

And because we have provided those defaults, nothing should have broken at this point. But we do need to update our getBlogPosts function calls to properly pass through the expected data, should it be provided:

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

    var getBlogPosts = function (page, sortBy, direction) {
        Api.getAll(page, 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);
            });
    };

With that in place, we need to update the sortBy function to actually call getBlogPosts when invoked:

    $scope.sortBy = function (propertyName) {
        $scope.propertyName = propertyName;
        $scope.reverse = !$scope.reverse;
        getBlogPosts($scope.currentPage, $scope.propertyName, reversedAsString($scope.reverse));
    };

And notice there as the last parameter we are passing through the outcome of reversedAsString. You could extract that to a variable first:

    $scope.sortBy = function (propertyName) {
        $scope.propertyName = propertyName;
        $scope.reverse = !$scope.reverse;
        var ascOrDesc = reversedAsString($scope.reverse);
        getBlogPosts($scope.currentPage, $scope.propertyName, ascOrDesc);
    };

Both are identical in terms of functionality.

Fixing A Regression

This will have introduced an unwanted behaviour. Now, everything appears to work, but if you sort by id descending, then browse to page 2, the sort and direction are lost.

To fix this, we must update the call to getBlogPosts inside the pageChanged function:

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

Becomes:

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

If we call the pageChanged function before we have ever sorted our data, the default values will be used.

If we have sorted our data then we will have stored our propertyName and reverse values in the associated scope variables, so we can make use of them again here.

And that just about wraps up Sorting in our Angular implementation.

Code For This Video

Get the code for this video.

Episodes