Syntax Highlighter

Wednesday, May 22, 2013

Superheroic dynamic pagination with AngularJs

Data binding, custom directives, reusable components, dependency injection, you name it! AngularJs has all you need in order to boost front-end productivity. I've been using it for quite a while now and recently I came by an interesting problem: dynamic pagination. I'm not sure if you understand what I mean by "dynamic". I wanted to implement an admin page containing a paginated table holding all the site's signed-up users There would be a searchbar as well, and the number of pages and filtering should occur dynamically as the user types in the query.
Using data binders and clever event handling this can be easily accomplished by angular and the result is incredibly satisfying. Let me show you the code. First add these stylesheets:
<link href="http://netdna.bootstrapcdn.com/twitter-bootstrap/2.1.1/css/bootstrap.no-icons.min.css" rel="stylesheet">
<link href="http://netdna.bootstrapcdn.com/font-awesome/2.0/css/font-awesome.css" rel="stylesheet">
Now the table view. It binds any change to the text input to the function $scope.search(), adds all the pagedItems[currentPage] array to the table's body and sets up the pagination logic in the table's foot:
        <div ng-controller="ctrlRead">
            <div class="input-append">
                <input type="text" ng-model="query" ng-change="search()" class="input-large search-query" placeholder="Search">
             <span class="add-on"><i class="icon-search"></i></span>
            </div>
            <table class="table table-striped table-condensed table-hover">
                <thead>
                    <tr>
                        <th class="id">Id&nbsp;</th>
                        <th class="name">Name&nbsp;</th>
                        <th class="description">Description&nbsp;</th>
                        <th class="field3">Field 3&nbsp;</th>
                        <th class="field4">Field 4&nbsp;</th>
                        <th class="field5">Field 5&nbsp;</th>
                    </tr>
                </thead>
                <tfoot>
                    <td colspan="6">
                        <div class="pagination pull-right">
                            <ul>
                                <li ng-class="{disabled: currentPage == 0}">
                                    <a href ng-click="prevPage()">« Prev</a>
                                </li>
                                <li ng-repeat="n in range(pagedItems.length)"
                                    ng-class="{active: n == currentPage}"
                                ng-click="setPage()">
                                    <a href ng-bind="n + 1">1</a>
                                </li>
                                <li ng-class="{disabled: currentPage == pagedItems.length - 1}">
                                    <a href ng-click="nextPage()">Next »</a>
                                </li>
                            </ul>
                        </div>
                    </td>
                </tfoot>
                <tbody>
                    <tr ng-repeat="item in pagedItems[currentPage] | orderBy:sortingOrder:reverse">
                        <td>{{item.id}}</td>
                        <td>{{item.name}}</td>
                        <td>{{item.description}}</td>
                        <td>{{item.field3}}</td>
                        <td>{{item.field4}}</td>
                        <td>{{item.field5}}</td>
                    </tr>
                </tbody>
            </table>
        </div>
We need only build the filteredItems array after each call to $scope.search() accordingly. This can be done by passing the classic needle/haystack function as $filter's matching function and then split the result based on the number of elements per page. But talk is cheap, I know:
//classical way of finding a substring (needle) in a given string (haystack)
    var searchMatch = function (haystack, needle) {
        if (!needle) {
            return true;
        }
        return haystack.toLowerCase().indexOf(needle.toLowerCase()) !== -1;
    };

    // filter the items following the search string
    $scope.search = function () {
        $scope.filteredItems = $filter('filter')($scope.items, function (item) {
            for(var attr in item) {
                if(item.hasOwnProperty(attr))
                    if (searchMatch(item[attr], $scope.query))
                        return true;
            }
            return false;
        });
        // take care of the sorting order
        if ($scope.sortingOrder !== '') {
            $scope.filteredItems = $filter('orderBy')($scope.filteredItems, $scope.sortingOrder, $scope.reverse);
        }
        $scope.currentPage = 0;
        // now group by pages
        $scope.groupToPages();
    };
    
    // divide elements by page
    $scope.groupToPages = function () {
        $scope.pagedItems = [];
        
        for (var i = 0; i < $scope.filteredItems.length; i++) {
            if (i % $scope.itemsPerPage === 0) {
                $scope.pagedItems[Math.floor(i / $scope.itemsPerPage)] = [ $scope.filteredItems[i] ];
            } else {
                $scope.pagedItems[Math.floor(i / $scope.itemsPerPage)].push($scope.filteredItems[i]);
            }
        }
    };
Finally, just add the paging logic for the table's foot and call $scope.search() so we show everything after the page loads.
    $scope.range = function (end) {
        var ret = [];
        for(var i = 0; i < end; i++) {
            ret.push(i);
        }
        return ret;
    };
    
    $scope.prevPage = function () {
        if ($scope.currentPage > 0) {
            $scope.currentPage--;
        }
    };
    
    $scope.nextPage = function () {
        if ($scope.currentPage < $scope.pagedItems.length - 1) {
            $scope.currentPage++;
        }
    };
    
    $scope.setPage = function () {
        $scope.currentPage = this.n;
    };

    // create filtered items for the first time
    $scope.search();
Just add this code to our view's controller and we're done! Check out this fiddle with our working code and some mock data! Now, of course this is only useful when all the necessary data can be fetched from the server quickly, so that the search is consistent. If you cannot provide Angular with all the necessary data, perhaps you should consider infinite scrolling instead. If that's the case, check this out. Have fun!
ftw.

6 comments:

  1. I am extremely impressed with your writing skills and also with the layout on your blog thanks for sharing information,nice article
    Angularjs Training In Hyderabad

    ReplyDelete
  2. You’d outstanding guidelines there. I did a search about the field and identified that very likely the majority will agree with your web page.
    Agra BCom Time Table 2020
    Allahabad BCom Time Table 2020
    Brij BCOM TimeTable 2020

    ReplyDelete
  3. Thanks for sharing this valuable information to our vision. You have posted a trust worthy blog keep sharing.Nice article i was really impressed by seeing this article, it was very interesting and it is very useful for me.
    Salesforce Training in Chennai

    Salesforce Online Training in Chennai

    Salesforce Training in Bangalore

    Salesforce Training in Hyderabad

    Salesforce training in ameerpet

    Salesforce Training in Pune

    Salesforce Online Training

    Salesforce Training

    ReplyDelete
  4. Good info I gained more information about that. Thanks for such a helpful information.Generative AI Training in Hyderabad

    ReplyDelete