mellis481
mellis481

Reputation: 4158

Angular.js paginate filtered data and display total items without duplicating code in view/controller

Let's say I have a filter implemented in a view like this:

<input data-ng-model="statementFilter" />
<ul>
    <li data-ng-repeat="statement in statements | filter: statementFilter">
        {{ statement.Name }}
    </li>
</ul>

This will do a case insensitive partial match for statements with any properties that contain statementFilter.

I need to implement this in my controller instead of in my view. I understand you can create custom filters in Angular, but I want my filter to do the generic case insensitive partial matching on any complex object that the built-in Angular filter in the view does. If I create a custom filter, I have to do the actually filtering using javascript which will require another library AFAIK.

How can I leverage the generic case insensitive partial matching that comes with Angular's "view" filter in code?

Thanks

UPDATE

Here is a Plunker of what I'm trying to do.

Upvotes: 1

Views: 4197

Answers (2)

Joel Davey
Joel Davey

Reputation: 2603

Here's another approach achieving the same thing with multiple filters and pagination. There's not really any advantages over Cory's answer except this version does not depend on angular-ui for the pagination controls.

app.js

var app = angular.module('statementApp', ['ui.bootstrap']);

app.controller('statementController', function($scope, $interpolate) {

$scope.currentPage = 0;
$scope.pageSize = 10;

$scope.data = [
{name:"John Smith", price:"1.20"},
{name:"John Smith", price:"1.20"},
{name:"Sam Smith", price:"1.20"},
{name:"Sam Smith", price:"1.20"},
{name:"Sam Smith", price:"1.20"},
{name:"Sam Smith", price:"1.20"},
{name:"Sarah Smith", price:"1.20"},
{name:"Sarah Smith", price:"1.20"},
{name:"Sarah Smith", price:"1.20"},
{name:"Sarah Smith", price:"1.20"},
{name:"Sarah Smith", price:"1.20"},
{name:"Sarah Smith", price:"1.20"}
];

var init = function() {
  $scope.initPaging();
}


$scope.searchFilter = function(item) {
  if(!$scope.searchVal) return true;
  var qsRegex = new RegExp($scope.searchVal, 'gi');
  return qsRegex ? item.name.match(qsRegex) : true;

}

    $scope.initPaging = function() {
                $scope.currentPage = 0;
                $scope.numberOfPages=function(){

                        if($scope.filteredItems.length > $scope.pageSize) {
                        return Math.ceil($scope.filteredItems.length / $scope.pageSize);
                    }
                    return 1;
                }

            }

$scope.$watch(function () {
                $scope.filteredItems = $scope.$eval("data | filter:searchFilter | orderBy: 'price'");
            });

  init();

});

index.html

<!DOCTYPE html>
<html ng-app="statementApp">

  <head>
    <script src="//ajax.googleapis.com/ajax/libs/angularjs/1.2.16/angular.js"></script>
<script src="//angular-ui.github.io/bootstrap/ui-bootstrap-tpls-0.12.0.js"></script>
<link href="//netdna.bootstrapcdn.com/bootstrap/3.1.1/css/bootstrap.min.css" rel="stylesheet">
<script src="script.js"></script>
  </head>

  <body ng-controller="statementController">
<input id="txtStatementFilter" class="form-control" ng-model="searchVal" placeholder="Search records" />
   <div>Total records: {{ filteredItems.length }}</div>
<table class="table table-hover table-bordered table-striped table-condensed">
    <thead>
        <tr>
            <th>Name</th>
            <th>Price</th>
        </tr>
    </thead>
    <tbody>
        <tr ng-repeat="item in (filteredItems).slice(currentPage*pageSize,(currentPage*pageSize) + pageSize)">
                <td>{{item.name}}</td>
                <td>{{item.price | currency:"£":0}}</td>
            </tr>
    </tbody>
</table>
   <button ng-disabled="currentPage == 0" ng-click="currentPage=currentPage-1">
            Previous
        </button>
        {{currentPage+1}}/{{numberOfPages()}}
        <button ng-disabled="currentPage >= filteredItems.length/pageSize - 1" ng-click="currentPage=currentPage+1">
            Next
        </button>
  </body>

</html>

Plunker link http://plnkr.co/edit/OWotCiVYBed77F50wdMf?p=preview

Upvotes: 0

Cory Danielson
Cory Danielson

Reputation: 14501

Solution: http://plnkr.co/edit/AcAP437OJGMgMuGCtxT3?p=preview


Basically, the problem was that the controller was doing what you knew could be accomplished inside of the view... what made this more difficult than most other cases was that you're trying to paginate the same data that you're filtering and displaying the length... so this means that your data must be manipulated in this order:

  1. filtered data based on search item
  2. capture the length of the filtered items
  3. paginate the filtered items and display

The first thing that I know had to be done was rework the ng-repeat to do the filtering. The goal was to use that build in angular filtering.

Originally, it looked like this. Which did the filtering AND the paging, but used custom code in the controller.

data-ng-repeat="statement in pagedStatementData()"

Step 1: Use the angular filter

The filtering that you posted in your question was an easier way to do this without writing custom filtering code... so that was my first step. Easy enough.

data-ng-repeat="statement in statements | filter:statementFilter"

Step 2: Get pagination back

At this point, the list is filtered correctly, but displays all of the filtered items and does not break them into pages. The pagination buttons work as they should and the total records update accordingly. So now the next step is to insert that pagination into this filtered list.

In the script, I added begin and end to the scope. These variables were previously created inside of the pagedStatementData(). Then using those values, I can slice the filtered array to get the pagination going.

Note: This $scope.begin $scope.end code was eventually removed in Step 5, because it's only calculated on the initial render and didn't update after then. It was a bug I didn't notice until Step 5.

$scope.begin = ($scope.currentPage-1)*$scope.numPerPage;
$scope.end = ($scope.begin + $scope.numPerPage);
data-ng-repeat="statement in (statements | filter:statementFilter).slice(begin, end)"

Step 3: Remove controller code that is not wanted/needed

At this point, everything works... but the goal is to remove the custom filtering code... so I removed the $scope.filteredStatementData method and the $scope.totalFilteredStatementItems method that called it. $scope.pagedStatementData can get deleted also.. that was called in the ng-repeat that was modified in Step 1.

Removed:

  • $scope.filteredStatementData // custom filter code.. removed
  • $scope.totalFilteredStatementItems // called filteredStatementData... removed
  • $scope.pagedStatementData // this was called by the original ng-repeat... removed

Step 4: Fix total items # and pagination buttons. Both depend on the same .length

At this point... the view is broken, because it's still making a few calls to the methods we just removed. (totalFilteredStatementItems) So now the goal is to replace that functionality with what we have in the view. totalFilteredStatementItems used to run that custom filtering logic and then got the length without paginating the data.

We already have the items being filtered, so we just need to save them to the scope (before they're paginated) so that they can be accessed elsewhere. We can save that filtered array inside of the ng-repeat, actually. As long as the syntax remains item in items... but items can be assigned to a scope variable... like item in (items = (/*filter*/)).slice(x,y)

data-ng-repeat="statement in (filteredItems = (statements | filter:statementFilter)).slice(being, end)"    
<div>Total records: {{ filteredItems.length }}</div>
<pagination data-ng-model="currentPage" total-items="filteredItems.length"

Okay. That ng-repeat is starting to get crazy, but it's still working. The parens are the real magic here. This code is executed in the desired order.

// filtered data based on search item
$scope.filteredItems = $scope.statements.filter(/*statementFilter magic*/);
// paginate the filtered items
var _temp = filteredItems.slice($scope.begin, $scope.end),
    _i, statement;
// display page of filtered items
for (var _i in _temp) {
    statement = _temp[_i];
    // Render each row w/ statement
}

Also, I'm sure there's some Angular $scope magic going on to update the filteredItems.length since it's used in the Total records: div before the list is filtered... thanks Angular! Or maybe it prioritizes ng-repeat and executes that block first. Idk. It works.


Step 5: Pagination is broken. Get the pagination component to update begin and end variables that the list depends on.

Deleted $scope.begin and $scope.end code in controller. Create them inside of ng-init when the component is first created, and then on the data-ng-change event, recalculate those values.

<pagination data-ng-model="currentPage" total-items="filteredItems.length"
    items-per-page="numPerPage" data-max-size="maxSize" data-boundary-links="true"
    ng-init="begin = (currentPage-1)*numPerPage; end = begin + numPerPage"
    data-ng-change="begin = (currentPage-1)*numPerPage; end = begin + numPerPage">

Upvotes: 4

Related Questions