Angular: Reinclude null values when filter parameter is empty - angularjs

I have a pretty simple textbox filtering an ng-repeat on some unordered lis. When I add a value to the textbox the items with the null values are removed and do not return even when the textbox is cleared. I have an idea of why this is happening (the search object now has an empty property which doesn't match the nulls), but I cannot figure out how to solve the problem. I've tried to pop() the property off of the search object with no luck.
HTML:
<div ng-controller="ListCtrl">
<input type="text" ng-model="search.age" placeholder="Age"></input>
<ul>
<li ng-repeat="item in items | filter:search">
{{item.name}} - {{item.age}}
</li>
</ul>
</div>
JS:
function ListCtrl($scope) {
$scope.items = [
{'name':'Carl', 'age':69},
{'name':'Neil', 'age':54},
{'name':'Richard'},
{'name':'Chris', 'age':58}
];
}
Please checkout the JSfiddle to better illustrate the issue.

I figured it out with the help of this answer. If I just add an ng-change to the textbox I can watch for an empty value and delete the property.
HTML:
<input type="text" ng-model="search.age" ng-change="clear()" placeholder="Age"></input>
JS:
$scope.clear = function(){
if($scope.search.age.length == 0){
delete $scope.search.age;
}
}
Updated fiddle. I am aware the current if prevents a user from filtering on a single space, but so far this does not seem to cause a problem for me.
BONUS: ! will return all null values and !! will return all not null values.

The cleanest solution I have found is writing a custom directive to modify the input field behaviour like this:
app.directive('deleteIfEmpty', function () {
return {
restrict: 'A',
scope: {
ngModel: '='
},
link: function (scope, element, attrs) {
scope.$watch("ngModel", function (newValue, oldValue) {
if (typeof scope.ngModel !== 'undefined' && scope.ngModel.length === 0) {
delete scope.ngModel;
}
});
}
};
});
And use it as follows:
<input type="text" ng-model="filter" delete-if-empty>

Modify the input ng-model:
<input type="text" ng-model="searchObj.age" placeholder="Age"></input>
Add this to your controller:
$scope.searchObj = {
}
And either of these will work in your html repeat:
ng-repeat="item in items | filter: searchObj.age"
Or
ng-repeat="item in items | filter: {age: searchObj.age || undefined}"
jsfiddle

You won't be able to use filter:search. Looking at the Angular code, if your obj with an undefined age gets filtered (even when the input is empty) it will fall through this switch statement and always return false. This switch doesn't get called the first time your ng-repeat is run because $scope.search.age is undefined. After your first entry into the input and clearing it out, now $scope.search.age is an empty string...so the filter will always be run.
switch (typeof obj) { ***<-- obj is undefined when you have a missing age***
case "boolean":
case "number":
case "string":
return comparator(obj, text);
case "object":
switch (typeof text) {
case "object":
return comparator(obj, text);
default:
for ( var objKey in obj) {
if (objKey.charAt(0) !== '$' && search(obj[objKey], text)) {
return true;
}
}
break;
}
return false;
case "array":
for ( var i = 0; i < obj.length; i++) {
if (search(obj[i], text)) {
return true;
}
}
return false;
default:
return false; ***<--falls through and just returns false***
}
You can try writing your own filter function, something like this.
http://jsfiddle.net/wuqu2/
<div ng-controller="ListCtrl">
<input type="text" ng-model="search.age" placeholder="Age"></input>
<ul>
<li ng-repeat="item in items | filter:checkAge">
{{item.name}} - {{item.age}}
</li>
</ul>
</div>
$scope.checkAge = function(item)
{
if($scope.search && $scope.search.age && $scope.search.age.length > 0)
{
return item.age && item.age.toString().indexOf($scope.search.age) > -1;
}
return true;
}

Related

AngularJS filter (object and multiple values) not working

So I got a project which is using AngularJS with UI-Router, I iterate through a json which has a property called "state" which is what I want to use as a filter, however, I will be using a "selector" where I can pick "All" or a specific one.
I'm new to AngularJS So not sure exactly what is not working, this is the code I have so far:
function filterState (installment){
switch(selectedValue) {
case 1:
if(installment.state === 'PAID'){
return installment;
}
break;
case 2:
if(installment.state === 'LATE'){
return installment;
}
break;
case 3:
if(installment.state === 'PENDING'){
return installment;
}
break;
default:
return installment;
}
The filter however is not working as it should
Here is how I'm calling it:
<div class="row" ng-repeat="installment in vm.clients.installments | filter: filterState">
Not sure exactly what is the error, when I filter by a value manually like filter: {state:'PAID'}
It works
If you need to filter with a select option then you can do something like this:
<select name="select" id="select" ng-model="clients.status">
<option value="PAID">PAID</option>
<option value="LATE">LATE</option>
<option value="PENDING">PENDING</option>
<option value="ALL">ALL</option>
</select>
<div class="row" ng-repeat="installment in clients.installments | filter: filterState">
{{installment.id}}
</div>
And in controller the function to filter:
$scope.filterState = function(installment) {
if ($scope.clients.status === 'ALL') return true;
return $scope.clients.status === installment.status;
};
When option ALL is selected, it shows all the items of the list
Check a working demo here: DEMO
Apparently I needed to return a boolean value instead of the object, I did try that before, but my other issue was that I was calling the function directly instead of doing vm.function (once passed to the context), after doing that it worked.
Filter function returns an iterator function. like callback. Please check below example.
var app = angular.module('plunker', []);
app.controller('MainCtrl', function($scope) {
$scope.criteria = { name: 'Bob'};
$scope.criteriaMatch = function( criteria ) {
return function( item ) {
return item.name === criteria.name;
};
};
$scope.items =[
{ name: 'Bob'},
{ name: 'Alice'}
];
});
<div ng-repeat="item in items | filter:criteriaMatch(criteria)">
{{ item }}
</div>
http://plnkr.co/edit/vtNjEgmpItqxX5fdwtPi?p=preview
Credit: Anonymous

Angular: custom filter for recursive search in nested object

I have some problem with filtering nested object. In select I choose the filter parameter(object value) and in input I type some text that searches it in object key.
Tried to write custom filter with recursion for deep search but it doesn't work.
input is parameter for object in ng-repeat, param1 is for select's ng-model and param2 is for input's ng-model.
JS
.filter('personFilter', function($filter) {
return function(input, param1, param2) {
var output = {};
for (var prop in input) {
if (typeof input[prop] == 'object' || prop != param1 && input[prop] != param2) {
$filter('personFilter')(input[prop]);
} else {
output[key] = input[key];
}
}
return output;
}
Here's the plunker: http://plnkr.co/edit/83lPNRWFy6wa9U2FkMfH?p=preview
I hope someone give me some advice
sorry, just dirty solution, have no time to refactor. Mb it save time to you
.filter('personFilter', function($filter) {
return function(input, recursive, search) {
return input.filter(filterFn);
function filterFn(obj){
var val, res;
for (var prop in obj) {
val = obj[prop];
if (typeof val == 'object' && recursive) {
recursive = false;
res = res || val.filter(filterFn).length;
recursive = true;
} else if(!recursive){
res = res || val == search;
}
}
console.log(res, obj, recursive, search);
return res;
}
}
with following markup
<body ng-controller="appCtrl">
<select ng-model="selectParameter" ng-options="item.value as item.key for item in parameter track by item.value">
</select>
<input ng-model="query" />
<div ng-repeat="person in object.node1.node2.persons | personFilter:selectParameter:query track by $index">
<!-- -->
<p>Person: {{person.name}}, Children: <span ng-repeat="child in person.children track by $index">{{child.name}}, </span></p>
</div>
</body>
and controller init like:
$scope.parameter = [
{value: false, key: 'Person Name'},
{value: true, key: 'Child Name'}
];
$scope.selectParameter = true;

AngularJS - ng-if checking for true values when key is unknown

I want to apply a ng-if in AngularJS depending if any of the values in a JSON is true.
The first level keys are always the same, but then the second level keys are always different. (so I cannot do ng-if="known_stuff.unpredictable_thing", as the name of "unpredictable_thing" will be different each time. Here is the JSON.
{
"known_stuff":
{
"unpredictable_thing":false
},
"known_stuff_2":
{
"non_predictable_stuff":true
},
"known_stuff_3":
{
"something_unknown":false
}
}
Thanks in advance!
controller:
$scope.check = function(someObject) {
// return true if some value is true inside the object
for (var key in someObject) {
if (someObject[key] === true) {
return true;
}
}
return false;
};
template:
ng-if="check(known_stuff)"
or
ng-show="check(known_stuff)"
if your data is an array then the function has to look like that:
$scope.checkData = function(data) {
for (var i = 0; i < data.length; i++) {
for (var key1 in data[i]) {
// return true if some value is true inside the object
for (var key in data[i][key1]) {
if (data[i][key1][key] === true) {
return true;
}
}
}
}
return false;
};
template:
ng-if="checkData(data)"
If you want to check any of the value in your provided json is true then
ng-if="known_stuff.unpredictable_thing == ture ||
known_stuff_2.non_predictable_stuff == true ||
known_stuff_3.something_unknown == true"
If I got your question right, your json will have the first level key same (known_stuff) but inside know_stuff there can be multiple key with different names (like unpredictable_thing here).
The easiest solution is to iterate the first level key, gaining key value pair like below.
<div ng-repeat = "(key, val) in known_stuff" ng-if="known_stuff[key]">
//do something --> {{key}} - {{val}}
</div>
Supporting Plunk -> http://plnkr.co/edit/6hQQAtqRseb1gWvueFKr
-----------------------------------------------UPDATE---------------------------------------------
Assign jsonData with your data.
<div ng-repeat= "stuff in jsonData">
<div ng-repeat = "(key, val) in stuff" ng-if="stuff[key]">
//do something --> {{key}} - {{val}}
</div>
</div>
Also, updated the same plunk. I hope this answers your question.

$filter with OR [duplicate]

I want to use the filter in angular and want to filter for multiple values, if it has either one of the values then it should be displayed.
I have for example this structure:
An object movie which has the property genres and I want to filter for Action and Comedy.
I know I can do filter:({genres: 'Action'} || {genres: 'Comedy'}), but what to do if I want to filter it dynamically. E.g. filter: variableX
How do I set variableX in the $scope, when I have an array of the genres I have to filter?
I could construct it as a string and then do an eval() but I don't want to use eval()...
I would just create a custom filter. They are not that hard.
angular.module('myFilters', []).
filter('bygenre', function() {
return function(movies,genres) {
var out = [];
// Filter logic here, adding matches to the out var.
return out;
}
});
template:
<h1>Movies</h1>
<div ng-init="movies = [
{title:'Man on the Moon', genre:'action'},
{title:'Meet the Robinsons', genre:'family'},
{title:'Sphere', genre:'action'}
];" />
<input type="checkbox" ng-model="genrefilters.action" />Action
<br />
<input type="checkbox" ng-model="genrefilters.family" />Family
<br />{{genrefilters.action}}::{{genrefilters.family}}
<ul>
<li ng-repeat="movie in movies | bygenre:genrefilters">{{movie.title}}: {{movie.genre}}</li>
</ul>
Edit here is the link: Creating Angular Filters
UPDATE: Here is a fiddle that has an exact demo of my suggestion.
You can use a controller function to filter.
function MoviesCtrl($scope) {
$scope.movies = [{name:'Shrek', genre:'Comedy'},
{name:'Die Hard', genre:'Action'},
{name:'The Godfather', genre:'Drama'}];
$scope.selectedGenres = ['Action','Drama'];
$scope.filterByGenres = function(movie) {
return ($scope.selectedGenres.indexOf(movie.genre) !== -1);
};
}
HTML:
<div ng-controller="MoviesCtrl">
<ul>
<li ng-repeat="movie in movies | filter:filterByGenres">
{{ movie.name }} {{ movie.genre }}
</li>
</ul>
</div>
Creating a custom filter might be overkill here, you can just pass in a custom comparator, if you have the multiples values like:
$scope.selectedGenres = "Action, Drama";
$scope.containsComparator = function(expected, actual){
return actual.indexOf(expected) > -1;
};
then in the filter:
filter:{name:selectedGenres}:containsComparator
Here is the implementation of custom filter, which will filter the data using array of values.It will support multiple key object with both array and single value of keys. As mentioned inangularJS API AngularJS filter Doc supports multiple key filter with single value, but below custom filter will support same feature as angularJS and also supports array of values and combination of both array and single value of keys.Please find the code snippet below,
myApp.filter('filterMultiple',['$filter',function ($filter) {
return function (items, keyObj) {
var filterObj = {
data:items,
filteredData:[],
applyFilter : function(obj,key){
var fData = [];
if (this.filteredData.length == 0)
this.filteredData = this.data;
if (obj){
var fObj = {};
if (!angular.isArray(obj)){
fObj[key] = obj;
fData = fData.concat($filter('filter')(this.filteredData,fObj));
} else if (angular.isArray(obj)){
if (obj.length > 0){
for (var i=0;i<obj.length;i++){
if (angular.isDefined(obj[i])){
fObj[key] = obj[i];
fData = fData.concat($filter('filter')(this.filteredData,fObj));
}
}
}
}
if (fData.length > 0){
this.filteredData = fData;
}
}
}
};
if (keyObj){
angular.forEach(keyObj,function(obj,key){
filterObj.applyFilter(obj,key);
});
}
return filterObj.filteredData;
}
}]);
Usage:
arrayOfObjectswithKeys | filterMultiple:{key1:['value1','value2','value3',...etc],key2:'value4',key3:[value5,value6,...etc]}
Here is a fiddle example with implementation of above "filterMutiple" custom filter.
:::Fiddle Example:::
If you want to filter on Array of Objects then you can give
filter:({genres: 'Action', key :value }.
Individual property will be filtered by particular filter given for that property.
But if you wanted to something like filter by individual Property and filter globally for all properties then you can do something like this.
<tr ng-repeat="supp in $data | filter : filterObject | filter : search">
Where "filterObject" is an object for searching an individual property and "Search" will search in every property globally.
~Atul
I've spent some time on it and thanks to #chrismarx, I saw that angular's default filterFilter allows you to pass your own comparator. Here's the edited comparator for multiple values:
function hasCustomToString(obj) {
return angular.isFunction(obj.toString) && obj.toString !== Object.prototype.toString;
}
var comparator = function (actual, expected) {
if (angular.isUndefined(actual)) {
// No substring matching against `undefined`
return false;
}
if ((actual === null) || (expected === null)) {
// No substring matching against `null`; only match against `null`
return actual === expected;
}
// I edited this to check if not array
if ((angular.isObject(expected) && !angular.isArray(expected)) || (angular.isObject(actual) && !hasCustomToString(actual))) {
// Should not compare primitives against objects, unless they have custom `toString` method
return false;
}
// This is where magic happens
actual = angular.lowercase('' + actual);
if (angular.isArray(expected)) {
var match = false;
expected.forEach(function (e) {
e = angular.lowercase('' + e);
if (actual.indexOf(e) !== -1) {
match = true;
}
});
return match;
} else {
expected = angular.lowercase('' + expected);
return actual.indexOf(expected) !== -1;
}
};
And if we want to make a custom filter for DRY:
angular.module('myApp')
.filter('filterWithOr', function ($filter) {
var comparator = function (actual, expected) {
if (angular.isUndefined(actual)) {
// No substring matching against `undefined`
return false;
}
if ((actual === null) || (expected === null)) {
// No substring matching against `null`; only match against `null`
return actual === expected;
}
if ((angular.isObject(expected) && !angular.isArray(expected)) || (angular.isObject(actual) && !hasCustomToString(actual))) {
// Should not compare primitives against objects, unless they have custom `toString` method
return false;
}
console.log('ACTUAL EXPECTED')
console.log(actual)
console.log(expected)
actual = angular.lowercase('' + actual);
if (angular.isArray(expected)) {
var match = false;
expected.forEach(function (e) {
console.log('forEach')
console.log(e)
e = angular.lowercase('' + e);
if (actual.indexOf(e) !== -1) {
match = true;
}
});
return match;
} else {
expected = angular.lowercase('' + expected);
return actual.indexOf(expected) !== -1;
}
};
return function (array, expression) {
return $filter('filter')(array, expression, comparator);
};
});
And then we can use it anywhere we want:
$scope.list=[
{name:'Jack Bauer'},
{name:'Chuck Norris'},
{name:'Superman'},
{name:'Batman'},
{name:'Spiderman'},
{name:'Hulk'}
];
<ul>
<li ng-repeat="item in list | filterWithOr:{name:['Jack','Chuck']}">
{{item.name}}
</li>
</ul>
Finally here's a plunkr.
Note: Expected array should only contain simple objects like String, Number etc.
you can use searchField filter of angular.filter
JS:
$scope.users = [
{ first_name: 'Sharon', last_name: 'Melendez' },
{ first_name: 'Edmundo', last_name: 'Hepler' },
{ first_name: 'Marsha', last_name: 'Letourneau' }
];
HTML:
<input ng-model="search" placeholder="search by full name"/>
<th ng-repeat="user in users | searchField: 'first_name': 'last_name' | filter: search">
{{ user.first_name }} {{ user.last_name }}
</th>
<!-- so now you can search by full name -->
You can also use ngIf if the situation permits:
<div ng-repeat="p in [
{ name: 'Justin' },
{ name: 'Jimi' },
{ name: 'Bob' }
]" ng-if="['Jimi', 'Bob'].indexOf(e.name) > -1">
{{ p.name }} is cool
</div>
The quickest solution that I've found is to use the filterBy filter from angular-filter, for example:
<input type="text" placeholder="Search by name or genre" ng-model="ctrl.search"/>
<ul>
<li ng-repeat="movie in ctrl.movies | filterBy: ['name', 'genre']: ctrl.search">
{{movie.name}} ({{movie.genre}}) - {{movie.rating}}
</li>
</ul>
The upside is that angular-filter is a fairly popular library (~2.6k stars on GitHub) which is still actively developed and maintained, so it should be fine to add it to your project as a dependency.
I believe this is what you're looking for:
<div>{{ (collection | fitler1:args) + (collection | filter2:args) }}</div>
Please try this
var m = angular.module('yourModuleName');
m.filter('advancefilter', ['$filter', function($filter){
return function(data, text){
var textArr = text.split(' ');
angular.forEach(textArr, function(test){
if(test){
data = $filter('filter')(data, test);
}
});
return data;
}
}]);
Lets assume you have two array, one for movie and one for genre
Just use the filter as: filter:{genres: genres.type}
Here genres being the array and type has value for genre
I wrote this for strings AND functionality (I know it's not the question but I searched for it and got here), maybe it can be expanded.
String.prototype.contains = function(str) {
return this.indexOf(str) != -1;
};
String.prototype.containsAll = function(strArray) {
for (var i = 0; i < strArray.length; i++) {
if (!this.contains(strArray[i])) {
return false;
}
}
return true;
}
app.filter('filterMultiple', function() {
return function(items, filterDict) {
return items.filter(function(item) {
for (filterKey in filterDict) {
if (filterDict[filterKey] instanceof Array) {
if (!item[filterKey].containsAll(filterDict[filterKey])) {
return false;
}
} else {
if (!item[filterKey].contains(filterDict[filterKey])) {
return false;
}
}
}
return true;
});
};
});
Usage:
<li ng-repeat="x in array | filterMultiple:{key1: value1, key2:[value21, value22]}">{{x.name}}</li>
Angular Or Filter Module
$filter('orFilter')([{..}, {..} ...], {arg1, arg2, ...}, false)
here is the link: https://github.com/webyonet/angular-or-filter
I had similar situation. Writing custom filter worked for me. Hope this helps!
JS:
App.filter('searchMovies', function() {
return function (items, letter) {
var resulsts = [];
var itemMatch = new RegExp(letter, 'i');
for (var i = 0; i < items.length; i++) {
var item = items[i];
if ( itemMatch.test(item.name) || itemMatch.test(item.genre)) {
results.push(item);
}
}
return results;
};
});
HTML:
<div ng-controller="MoviesCtrl">
<ul>
<li ng-repeat="movie in movies | searchMovies:filterByGenres">
{{ movie.name }} {{ movie.genre }}
</li>
</ul>
</div>
Here is my example how create filter and directive for table jsfiddle
directive get list (datas) and create table with filters
<div ng-app="autoDrops" ng-controller="HomeController">
<div class="row">
<div class="col-md-12">
<h1>{{title}}</h1>
<ng-Multiselect array-List="datas"></ng-Multiselect>
</div>
</div>
</div>
my pleasure if i help you
Too late to join the party but may be it can help someone:
We can do it in two step, first filter by first property and then concatenate by second filter:
$scope.filterd = $filter('filter')($scope.empList, { dept: "account" });
$scope.filterd = $scope.filterd.concat($filter('filter')($scope.empList, { dept: "sales" }));
See the working fiddle with multiple property filter
OPTION 1:
Using Angular providered filter comparator parameter
// declaring a comparator method
$scope.filterBy = function(actual, expected) {
return _.contains(expected, actual); // uses underscore library contains method
};
var employees = [{name: 'a'}, {name: 'b'}, {name: 'c'}, {name: 'd'}];
// filter employees with name matching with either 'a' or 'c'
var filteredEmployees = $filter('filter')(employees, {name: ['a','c']}, $scope.filterBy);
OPTION 2:
Using Angular providered filter negation
var employees = [{name: 'a'}, {name: 'b'}, {name: 'c'}, {name: 'd'}];
// filter employees with name matching with either 'a' or 'c'
var filteredEmployees = $filter('filter')($filter('filter')(employees, {name: '!d'}), {name: '!b'});
My solution
ng-repeat="movie in movies | filter: {'Action'} + filter: {'Comedy}"
the best answer is :
filter:({genres: 'Action', genres: 'Comedy'}

Issue with view updation in AngularJS directive

I am using the following directive for 'add tag' functionality in my application:
directives.addTag = function ($http) {
return {
link: function (scope, element, attrs) {
element.bind('keypress', function (event) {
if (event.keyCode == 13) { /*If enter key pressed*/
if (!scope.$parent.post) { //For KShare
var newTagId = "tagToNote";
}
else { //For KB
var newTagId = "tagToAddFor" + scope.post.meta.id;
}
var tagValue = element[0].value;
if (tagValue == "")
return;
if (!scope.$parent.post) {
scope.$parent.tags.push(tagValue);
scope.addTagButtonClicked = false;
}
else {
scope.post.tags.push(tagValue);
scope.addTagButtonClicked = false;
}
scope.$apply();
element[0].value = "";
}
});
}
}
}
This is the HTML code for rendering the tags:
<div class="tagAdditionSpan" ng-repeat="tag in post.tags" ng-mouseenter="hover = true" ng-mouseleave="hover = false">
<span>{{tag}}</span>
<span class="deleteIconSpan" ng-class="{deleteTagIcon: hover}" ng-click="$parent.deleteTag($index,$parent.$index);"></span>
</div>
I have a textbox to add tags when a user types the name of the tag in it and presses 'Enter' key. On page load, I am statically populating 1 tag into the 'tags' array.
I am even able to add tags using the tags and it is reflected in the view. However after adding 2 or 3 tags, it starts misbehaving and the view is no longer updated with the added tags.
I tried debugging this and found that it is being updated in the 'scope.post.tags' array but is not reflected in the view.
What am I doing wrong?
Based on the comments received, I was able to solve the issue. 'ng-repeat' used to break the loop on addition of duplicate tags and hence the view was not updated accordingly.
This fixed the issue(added 'track by' in ng-repeat):
<div class="tagAdditionSpan" ng-repeat="tag in post.tags track by $index" ng-mouseenter="hover = true" ng-mouseleave="hover = false">
<span>{{tag}}</span>
<span class="deleteIconSpan" ng-class="{deleteTagIcon: hover}" ng-click="$parent.deleteTag($index,$parent.$index);"></span>
</div>

Resources