Table filter by predicate - angularjs

I made a jsfiddle to show what is my problem.
The fisrt part is working in a partial way. See line number 15. I put the predicate in the filter (predicate is l_name) by hand and is working. The table is filtered by Last Name column.
<tr ng-repeat="item in items | filter:{l_name:myInput}">
The second part of the sample is not working when I use the select (model named mySelect2) to choose the predicate where I'm going to filter (see line number 36).
What I'm trying to do is use the select to choose the column by predicate and the input to filter in that column.
<tr ng-repeat="item in items | filter:{mySelect2:myInput2}">
Am I missing something or the binding of the select (mySelect2) must update the filter on the table?
Thanks for the help!
PS: type jo in the input.

Here's a fiddle with some options: http://jsfiddle.net/jgoemat/tgKkD/1/
Option 1 - Search on multiple fields
You can use an object on your model ('search' here) as your filter and separate input boxes for l_name and f_name. This allows you not only to filter on either, but filter on both:
any: <input ng-model="search.$"/><br/>
l_name: <input ng-model="search.l_name"/><br/>
f_name: <input ng-model="search.f_name"/><br/>
<!-- skipping code -->
<tr ng-repeat="item in items|filter:search">
Option 2 - Use a function on your controller
The built-in filter can take a function as an argument that should return true if the object should be included. This function takes the object to be filtered as its only argument and returns true if it should be included. Html:
<tr ng-repeat="item in items|filter:filterFunc">
controller function:
$scope.filterFunc = function(obj) {
// property not specified do we want to filter all instead of skipping filter?
if (!$scope.mySelect)
return obj;
if (obj[$scope.mySelect].toLowerCase().indexOf($scope.myInput.toLowerCase()) >= 0)
return obj;
return false;
};
Option 3 - Create a custom filter
This filter function will take the whole list as an argument and return the filtered list. This does require you to create an angular module and specify it in the ng-app tag like ng-app="MyApp"Html:
<tr ng-repeat="item in items|MyFilter:mySelect:myInput">
Code:
var app = angular.module('MyApp', []);
app.filter('MyFilter', function() {
return function(list, propertyName, value) {
console.log('MyFilter(list, ', propertyName, ', ', value, ')');
// property not specified do we want to filter all instead of skipping filter?
if (!propertyName)
return list;
var newList = [];
var lower = value.toLowerCase();
angular.forEach(list, function(v) {
if (v[propertyName].toLowerCase().indexOf(lower) >= 0)
newList.push(v);
});
return newList;
}
});
Option 4: ng-show
The built-in filter filter expressions don't let you use any expression, but ng-show does so you can just limit visible items like so:
<tr ng-show="item[mySelect].toLowerCase().indexOf(myInput.toLowerCase()) >= 0 || !mySelect" ng-repeat="item in items">
I think option 1 is easy and flexible. If you prefer your drop-down + field UI then I think option 3 is the most useful, and you can re-use it as a dependency in other apps like this:
var app = angular.module("NewApp", ["MyApp"]);
I would just name it something better like 'filterByNamedProperty'. Option 2 is easy but it is tied to your controller. Option 4 is messy and I wouldn't use it.

What about using a custom filter? Users concatenate the property with the criteria (e.g. last:jo). In the filter, split on the colon, and use the first part as the property name and the second part as the criteria.

You may pass scope variables to your filters:
<tr ng-repeat="item in items | filter:myScopeVariable">
This means that you may define your filter object in controller and it will be used by the filter:
$scope.$watch('mySelect2', function(val){
$scope.myScopeVariable = {};
$scope.myScopeVariable[val] = $scope.myInput2;
});
$scope.$watch('myInput2', function(val){
$scope.myScopeVariable = {};
$scope.myScopeVariable[$scope.mySelect2] = $scope.myInput2;
});
Demo Fiddle

Related

AngularJS, accessing ngRepeat values in filter predicate

My best attempts at finding a solution to this have come up empty. Basically I want to do something like this in my html:
<div data-ng-repeat="tag in allTags">
<h3>{{tag}}</h3>
<uib-accordion>
<div uib-accordion-group data-ng-repeat="item in displayItems | filter: tagFilter: tag">
tagFilter looks like this:
$scope.tagFilter = function(item, tag) {
if(item.tags.indexOf(tag) === -1) {
return false;
}
return true;
}
Each item in displayItems is an object that has an array of tags, so display items looks something like this:
[
{ title: "Item 1 Title", body: "Some escaped HTML Content", tags: ["tag1", "tag2", "tag3"]},
{ title: "Item 2 Title", body: "Some escaped HTML Content", tags: ["tag2", "tag4"] }
]
and I want it to appear under all headings to which it belongs. The problem is I can't figure out how to properly pass the value of 'tag' to tagFilter. In the code above the parameter tag in codeFilter is just equal to 0 no matter what.
The problem here is actually in the semantics of the Filter syntax. More specifically, the syntax you're using above is for when you're defining an Angular Filter using the ngApp.filter(...) syntax... i.e., a filter that's registered for the entire application and can be used anywhere. In that scenario the 3rd parameter in your filter statement is the value you want to pass to the registered filter.
In your case, you're defining a filter function inside your controller which changes how the filter works. Specifically, you cannot pass dynamic values to a filter function inside a controller. When you use a function as the filter expression, it has the following signature:
function(value, index, array) {}
and then gets called in the filter statement just by the function name, so:
array|filter:filterfunction - with no params or parenthesis.
value is the value of the current item (in the array) being filtered, index is the index of that item in the array, and array is the whole array being filtered. You cannot "pass" a value to this expression, but you can use a controller or scope variable if it applies. In your case it doesn't because the value you want to filter on is inside a repeater.
To achieve what you want, you need to make your $scope.tagFilter into an actual Angular Filter, like so:
ngApp.filter('tagFilter', function($filter)
{
return function(items, searchValue)
{
// initialize array to return
var filtered = [];
angular.forEach(items, function(obj)
{
// use filter to find matching tags (3rd param means EXACT search - set to False for wildcard match)
var objFilter = ($filter("filter")(obj.tags, searchValue, true))[0];
// If a matching tag was found, add it to the filtered array
if (objFilter) filtered.push(obj);
});
return filtered;
};
});
The above assumes you've saved your angular.module(...) bootstrap to a variable named ngApp. Once this filter is registered, your current filter syntax should work as expected!
Assuming displayItems is an array,
<div uib-accordion-group data-ng-repeat="item in displayItems.filter(tagFilter(tag))" >
should do the trick.
Figured out a way to do this based on this blog post: https://toddmotto.com/everything-about-custom-filters-in-angular-js/
Basically I had to create my own custom filter rather than using angulars predicate filter
The Javascript:
ng.module('faq').filter(
'tagFilter', function() {
return function(items, tag) {
return items.filter(function(item) {
if(item.tags.indexOf(tag) === -1) {
return false;
}
return true;
});
}
}
)
The HTML:
<div uib-accordion-group data-ng-repeat="item in displayItems | tagFilter: tag">
Still don't know why the original version was not working, so if anyone can answer that 10 points to them.

How does filter work in AngularJS?

I have a table generated with ng-repeat (from an objects' array).
I would like to filter it with a search text field.
Objects contained in my array has got deep properties.
I don't know why and how, but the filter is only working on email field, which is as deep as other properties.
I'm using this search form :
<input type="text" name="search" ng-model="searchText" />
...
<tr ng-repeat="x in obj | filter:searchText track by $index">
...
</tr>
plunker
EDIT :
This answer helps me to understand why it's not working.
Someone knows how I can bypass the $ verification in filter ?
I'm using $ because I'm following the Google Contact API format.
You can check the source code of ngFilter here
It is set to ignore keys starting with $ as it's a prefix used by AngularJS for public ($) and private ($$) properties.
$ is a prefix used by Angular internal properties. For technical reasons, Angular prevents you to use it. Here is a workaround to deal with $ properties names without changing your JSON object:
You can iterate in ng-repeat over Object.keys($scope.object) instead $scope.object.
Demo on JSFiddle
Since it is clear that we can change neither third party API nor AngularJS library code, we could go for modifying the object keys to not have $ in the beginning. But, since the data has so many of them at multiple level, let's do it recursively! :)
Here's how. I would remap each object in $scope.obj array to call a function:
$scope.obj = $scope.obj.map(function(cur) {
return renameKey(cur)
})
Now, inside renameKey, it would check whether it's an Array or Object using helper functions and call itself recursively while replacing the keys prepending x for the strings starting with $
function renameKey(cur) {
if(isArray(cur)) {
cur.forEach(function(obj) {
obj = renameKey(obj)
})
} else if (isObject(cur)) {
for (let key in cur) {
if(key.charAt(0) === '$') {
cur['x'+key] = cur[key];
delete cur[key];
}
cur[key] = renameKey(cur[key])
}
}
return cur
}
function isObject(obj) {
return obj && (typeof obj === "object");
}
function isArray(obj) {
return isObject(obj) && (obj instanceof Array);
}
Looks little tedius but it does work! Now, all we need to do is have x$t instead of $t in the HTML, and boom!
working plunker
email works because nested property address doesn't contain any $ char.
Unfortunately, I don't think there is a way to bypass this behavior, however you can make your own filter and use it in ng-repeat.
This is simple example that should work for you:
JS
app.filter('customFilter', function() {
return function(items, keyword) {
if (!keyword || keyword.length === 0) return items;
return items.filter(function(item){
var phrase = keyword.$.toLowerCase();
return item.gd$name.gd$fullName.$t.toLowerCase().includes(phrase) ||
item.gd$name.gd$familyName.$t.toLowerCase().includes(phrase) ||
item.gd$name.gd$givenName.$t.toLowerCase().includes(phrase) ||
item.gd$email[0].address.toLowerCase().includes(phrase) ||
item.gd$phoneNumber[0].$t.toLowerCase().includes(phrase) ||
(!!item.gd$organization[0].gd$orgTitle && item.gd$organization[0].gd$orgTitle.$t.toLowerCase().includes(phrase)) ||
(!!item.gd$organization[0].gd$orgName && item.gd$organization[0].gd$orgName.$t.toLowerCase().includes(phrase));
});
}
});
HTML
<tr ng-repeat="x in obj | customFilter:searchText">
Of course, you will have to add more checks for possible null values. I've just wanted to make it work on the data you've provided.
Hope, you'll find it useful.
Here's plunk
I can't comment because my reputation is less than 50 but as far as i can tell it's any property that has a $ in it's name is not used in the filter.. I tried changing the property names and this fixed the issue. Realise you may or may not have control over this.
Suppose your obj is as below:
$scope.obj=[{firstName:'Jeet',lastName:'kumar'},{firstName:'test1',lastName:'dev'},{firstName:'test2',lastName:'other'}];
Search input box
<input type="text" name="search" ng-model="searchText" />
Datatable filter by index 'firstName'
<tr ng-repeat="x in obj | filter:{firstName:searchText}">
<td>{{x.firstName}}</td>
<td>{{x.lastName}}</td>
</tr>
Datatable filter over all index
<tr ng-repeat="x in obj | filter:searchText">
<td>{{x.firstName}}</td>
<td>{{x.lastName}}</td>
</tr>
Desc:
filter:{name:searchText}
filter on the basis of 'firstName' index from your $socpe.obj array

Angular 1.5.x - passing a variable to a built-in filter

I'm trying to filter data based on the select and input fields. Here is part of my code:
<select ng-change="students.changeValue(changeFilter)" ng-model="changeFilter">
<option ng-selected="true" value="name">Name</option>
<option value="age">Age</option>
</select>
<input ng-model="searchPhrase" />
name and age are example keys that I have. Here is my data generator structure:
<div ng-class="{breakLine: $index % 3 === 0}"
class="student-item col-md-4"
ng-repeat="s in students.studentsList | filter:{searchFilter: searchPhrase}">
The searchFilter is supposed to be a key that is set from a variable but it doesn't work. If I make there something like: filter:{name: searchPhrase} then it works because I have such keys in my data structures.
Here is a part of the controller:
.controller('StudentsListCtrl', ['$scope', function($scope) {
$scope.searchFilter = '';
this.changeValue = function(val) {
console.log(val); ---> gives key or age on change
$scope.searchFilter = val;
}
So when I manually write e.g.: | filter:{name: searchPhrase} then it works. But when I pass 'name' (i.e. the key) in a variable like: | filter:{searchFilter: searchPhrase} then it's broken...
How can I fix this?
You should use the last option described in filter documentation for the expression:
function(value, index, array): A predicate function can be used to write arbitrary filters. The function is called for each element of the array, with the element, its index, and the entire array itself as arguments.
In the controller, you define the predicate, e.g.
$scope.personFilter = function(person) {
// return true if this person should be displayed
// according to the defined filter
}
and use it in the ng-repeat filter, e.g.
<ul>
<li ng-repeat="p in persons | filter:personFilter">{{p.name}} ({{p.age}})</li>
</ul>
See the plunker for a demo. Note I choose age as a string to simplify the predicate function. You can refine it according to your needs.
Write a function in your $scope to generate the object that is passed to the filter:
$scope.getFilterObject = function() {
var filterObject = {};
filterObject[$scope.searchFilter] = $scope.searchPhrase;
return filterObject;
}
Use it as the argument in the filter:
ng-repeat="s in students.studentsList | filter:getFilterObject()"

using ng-click to update every cell in a table column

I'm new to Angular and I was trying to figure out how to have the data in one of the columns in a table I made convert from one number format to another when the user clicks on the top of the table cell. I've create a filter already, but i don't know how to call it so it effects all the cells in the table.
<tr ng-repeat="x in data">
<td>{{x.id}}</td>
<td>{{x.name}} </td>
<td>{{x.desc}}></td>
<td>{{x.number}}</td> <--- THIS IS WHAT I WANT TO CONVERT
</tr>
I'm not even sure where to start with this. I basically have a ng-click directive call "convert" which I've defined in the controller. I know that if I define a variable in the $scope (such as $scope.foo = "1") and then call the convert() function I can replace the value like this:
$scope.convert = function(){
$scope.foo = 2;
}
And then my table updates with that value. But what if every table cell in that column has a different value, and I basically want to run that value through a filter I've made.
Any suggestions on how to approach this?
You said you already have a filter?
Then just give your filter an argument 'numberFormat':
angular.
module('yourModule').
filter('yourFilter', function() {
return function(input, numberFormat) {
// convert the input according to the numberFormat
return filteredValue;
};
});
Then you can update the format in your convert() scope-method:
$scope.convert = function(){
$scope.numberFormat = 'long';
}
and pass it to your filter:
<td>{{x.number | yourFilter:numberFormat}}</td>
BTW:
Read about controllerAs - IMHO it is a better practice to store values at the controller rrather than directly on the scope.
Your function simply needs to update number property of the each object in the data array. You could do it like this:
$scope.convert = function() {
$scope.data.forEach(function(item) {
item.number = item.number + 2; // Convert number somehow
});
};
I assume click handler on table header of column as below
<th ng-click="convert()">column of number</th>
in the controller write the function as below
$scope.convert = function() {
$scope.data.forEach(function(obj) {
obj.number += 1;
})
//$scope.$apply() use this is template is not refreshed
}

angular filter name as variable

I'm designing universal table that reads data and columns from ajax.
In columns description is also filter name which angular should use for a specific column.
But in HTML templates I can't use variables for filter names:/
Is there a solution for that? Or should I code javascript loop with data source?
Here is code example:
<tr ng-repeat="item in data">
<td ng-repeat="col in cols">
{{item[col.source]}}
<span ng-if="col.ngFilter">
{{col.ngFilter}} // ex. "state" filter
{{item[col.source]|col.ngFilter}} //is not working - is looking for "col.ngFilter" not "state" filter.
</span>
</td>
</tr>
You cannot do it in your HTML. First, you need to apply the filter in your controller.
function MyCtrl($scope, $filter) {
$scope.applyFilter = function(model, filter) {
return $filter(filter)(model);
};
}
Then, in your HTML:
Instead of
{{item[col.source]|col.ngFilter}}
use
{{applyFilter(item[col.source], col.ngFilter)}}
For anyone looking to do something like
{{applyFliter(item[col.source], col.ngFilter)}}
where ngFilter might contains some colon separated parameters such as
currency:"USD$":0
I ended up writing this little helper
function applyFilter (model, filter){
if(filter){
var pieces = filter.split(':');
var filterName = pieces[0];
var params = [model];
if(pieces.length>1){
params = params.concat(pieces.slice(1));
}
return $filter(filterName).apply(this,params);
}else{
return model;
}
}

Resources