Reputation: 4158
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
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
Reputation: 14501
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:
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()"
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"
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)"
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:
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.
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