AngularJS smart-table strict search - angularjs

Smart-table has a built in functionality to search through columns (st-search). Is it possible to perform a strict search for words, so with an exact match? For example: if I have table like this: id, name, status:
1, John, address somewhere
2, Johnny, another address
3, Jane, address
and do a search for 'John' only the first row should show. Is this possible?

I had the same problem. My solution was to use the undocumented stSetFilter feature to pass in my own custom filter.
<table st-set-filter="myFilter" st-table="myData">
Then you just create an angular filter.
angular.module('myApp').filter('myFilter',
function($filter) {
return function(input, predicate) {
return $filter('filter')(input, predicate, true);
};
}
);
In my case I needed to only do strict searches some of the time so I put some additional logic in the filter, but this will do you for doing strict searches all the time.
Mine looks more like this:
angular.module('myApp').filter('myFilter',
function($filter) {
return function(input, predicate) {
var strict = false;
if (predicate) { // some conditional if I want strict
strict = true;
}
return $filter('filter')(input, predicate, strict);
};
}
);

Old, but still relevant.
If you have multiple input fields, but want strict only on some.
#elthrasher gave me the idea.
angular.module('app').filter('myStrictFilter', function($filter) {
return function(input, predicate) {
var response = input,
strict = false,
custom_predicate = {};
// some input fields must be strict
angular.forEach(predicate, function(value, key) {
custom_predicate = {};
custom_predicate[key] = value;
strict = false;
if (key === 'completion_status') { // strict it is.
strict = true;
}
response = $filter('filter')(response, custom_predicate, strict);
});
return response;
};
});

Yo should use strict comparator:
<input type="text" ng-model="query" />
<ul>
<li data-ng-repeat="user in users | filter:{ name:query }:true">
{{ user.id }}, {{ user.name }}, {{ user.address }}
</li>
</ul>
Comparator which is used in determining if the expected value (from
the filter expression) and actual value (from the object in the array)
should be considered a match.
Can be one of:
function(actual, expected): The function will be given the object
value and the predicate value to compare and should return true if
both values should be considered equal.
true: A shorthand for function(actual, expected) { return
angular.equals(actual, expected)}. This is essentially strict
comparison of expected and actual.
false|undefined: A short hand for a function which will look for a
substring match in case insensitive way.
Primitive values are converted to strings. Objects are not compared
against primitives, unless they have a custom toString method (e.g.
Date objects).

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.

AngularJS smart-table search within multiple columns

Smart-table has a built in functionality to search through all columns (st-search) or through one desired column (st-search="'firstName'). Is it possible to do a search within several columns (not all)?
Example: if I have table like this: name, nickname, address with such data:
John, JJ, Some address
Steve, John, also address
Jane, Jane, John-village
and do a search for 'John' only first two columns should be as result.
Is it possible?
I have a similar problem and I solved using the hints in this post.
Smart Table doc says:
The stSetFilter replaces the filter used when searching through Smart
Table. When the default behavior for stSearch does not meet your
demands, like in a select where one entry is a substring of another,
use a custom filter to achieve your goals.
and:
Note that st-safe-src is required for the select to properly display
all distinct elements in the collection. Should this be omitted, the
select would only contain values from elements visible in table, also
affected by paging.
You can declare your own filter inside table element in HTML:
<table ... st-set-filter="myCustomFilter" class="table table-striped">
...and your can customize your filter (in your app) through a predicate function. It could work in this way:
// filter method, creating `myCustomFilter` a globally
// available filter in our module
.filter('myCustomFilter', ['$filter', function($filter) {
// function that's invoked each time Angular runs $digest()
return function(input, predicate) {
searchValue = predicate['$'];
//console.log(searchValue);
var customPredicate = function(value, index, array) {
console.log(value);
// if filter has no value, return true for each element of the input array
if (typeof searchValue === 'undefined') {
return true;
}
var p0 = value['name'].toLowerCase().indexOf(searchValue.toLowerCase());
var p1 = value['nickname'].toLowerCase().indexOf(searchValue.toLowerCase());
if (p0 > -1 || p1 > -1) {
// return true to include the row in filtered resultset
return true;
} else {
// return false to exclude the row from filtered resultset
return false;
}
}
//console.log(customPredicate);
return $filter('filter')(input, customPredicate, false);
}
}])
I made this little plnkr to see the filter in action
Nope, a workaround is to create you own directive which require the table controller and call its api twice (as the search get added)
directive('stSearch', ['$timeout', function ($timeout) {
return {
require: '^stTable',
scope: {
predicate: '=?stSearch'
},
link: function (scope, element, attr, ctrl) {
var tableCtrl = ctrl;
// view -> table state
element.bind('input', function (evt) {
evt = evt.originalEvent || evt;
tableCtrl.search(evt.target.value, 'column1');
tableCtrl.search(evt.target.value, 'column2');
});
}
};
}]);
you'll find more details in the stSearch direcitve
You can solve the issue by creating the new search enabled table and combining the fields that you might be showing in one column.
example:if you have IdNo1 and IdNo2 as view fileds in One column, you can combine them to add the new element in the table array.
View :
table injection:
table st-table="displayedCollection" st-safe-src="rowSearchCollection"
Search injection:
input type="search" ng-model="idSearch" st-search="idSearch"
Controller:
$scope.rowSearchCollection = [];
vm.searchEnabledTable = function(tableDetails) {
//$scope.rowSearchCollection = angular.copy(tableDetails);
var deferred = $q.defer();
if(_.isArray(tableDetails)) {
_.each(tableDetails, function(element, index, list) {
$scope.rowSearchCollection[index] = angular.copy(element);
$scope.rowSearchCollection[index].idSearch = element.IdNo1+','+element.IdNo2;
});
deferred.resolve("DATA_PROCESSED");
} else {
deferred.reject("NOT_ARRAY");
}
return deferred.promise;
}
the problem with stSetFilter is that the new filter will be to all the
searchs (st-search) that you will use in the table.
Another idea:
If your data rowCollection is not too big,
in the javascript in the init() of the page do something like:
self.rowCollection.forEach(function (element) {
element.NameForFilter = element.name+ ' ' + element.nickName;
});
and then in the input of the th:
st-search=NameForFilter
try st-search="{{firstName + nickname}}". I tried with smart table v2.1.6 and seems working.

Always display a Key First with Angular

I have an ng-repeat like:
<div ng-repeat="(k, v) in query_p[$index].tags">
I would like it so that if the key (k) is a certain string, say "foo", that string always appears first in the list. It seems the getter option or orderBy only works with arrays. Anyone have an example of how they might achieve this?
Basically you have an unordered object, and you want it to have some kind of order.
To do that you need to create a function that returns some ordered object.
myApp.filter('promote_foo', function() {
return function(object, comp) {
console.log(object);
console.log(comp);
var ordered = [];
for (var key in object) {
var obj = {
key: key,
value: object[key]
};
if (key === comp)
ordered.splice(0,0,obj);
else
ordered.push(obj);
}
console.log(ordered);
return ordered;
};
});
the function takes a parameter which will promote and object if the key matches it. Now I only call it in the controller directly, but you could use it just like any angular filter.
$scope.order = $filter('promote_foo')($scope.data, 'foo');
Also, you can play with the fiddle here.
Hope this helped!

Can I use filter in ng-repeat to filter multiple items for a specific field

I'm working on a filtering solution for a grid. The filter contains multiple checkboxes in a panel. I currently have a working solution that uses the filter built into the ng-repeat directive. However, I haven't found a way to make it filter a single field with multiple parameters. This is what I have:
HTML:
<tr data-ng-repeat="rule in rules | orderBy:sortOrder | filter:filterOrder">
Contoller:
var defaultFilter = {critical:'', score:'', category:'', pass:'false'};
$scope.filterOrder = defaultFilter;
What I would like is to be able to do something like this:
var defaultFilter = {critical:'', score:'', category:'', pass:'false && notRun'};
I'm not sure if this is possible. I've looked for the syntax on it, but I have yet to find it. I know there are ways to put it in the controller, but due to our current project this implementation would be considerably easier.
Assuming you are using the 1.0.x branch, the relevant part of the source code for the filtering on objects is shown in the footnote below. This shows that the standard filter finds the key to test, and then gets var text = (''+expression[key]).toLowerCase(); from your search criteria before calling search. search then does a text search using (indexOf(text) > -1.
Therefore you can't set one of the items in your search criteria to an expression that requires evaluation in the way that you are trying.
Your only other options are to:
use a predicate function as the parameter for the filter (this just needs to return true/false); or
to write your own filter (this just needs to return a new array from the original one).
Predicate function
$scope.myFilterTest(item) { return ... true or false test result ...; }
and:
<tr data-ng-repeat="rule in rules | orderBy:sortOrder | filter:myFilterTest">
custom filter
var someApp=angular.module('myApp', []);
someApp.filter('complexFilter', function() {
return function(input, criteria) {
var result = [];
for (var i = 0; i < input.length; i++) {
if(... multiple tests ...){
result.push(input[i]);
}
}
return result;
}
});
And then:
<tr data-ng-repeat="rule in rules | orderBy:sortOrder | complexFilter:filterOrder">
Footnote:
relevant part of src/ng/filter/filter.js
case "object":
for (var key in expression) {
if (key == '$') {
(function() {
var text = (''+expression[key]).toLowerCase();
if (!text) return;
predicates.push(function(value) {
return search(value, text);
});
})();
} else {
(function() {
var path = key;
var text = (''+expression[key]).toLowerCase();
if (!text) return;
predicates.push(function(value) {
return search(getter(value, path), text);
});
})();
}
}
break;
yes you can achieve this using custom filter, here is the answer for you question.
And fiddle link here
filterMultiple

Underscore - Filter against an array of values

I have a collection self.models. I also have an array of an object which contains the fields and filters I wish to apply to my collection called filterArr. An example of this would be:
[{field: "Account", filter: "123"}, {field: "Owner", filter: "Bob"}]
The question is, I'm not sure quite how I'd iterate through each of my models to return only those models to which this filterArr applies too, I know it has to be something like this, but this is hard-coded:
self.models = _.filter(self.models, function (model) {
model = model.toJSON();
return model.Account === "123" && model.Owner === "Bob";
});
First of all, underscore's filter returns an Array, so what you're doing effectively here is substituting your collection with a filtered array. Something like this would be more appropriate:
this.filtered = _.filter(this.models, ...);
Backbone Collection implements most of underscore's useful functions. So the solution above is far from optimal (in fact it doesn't work at all the way you want it to), instead do something like this:
this.filtered = this.models.filter(function() {...});
The best way to get and set model fields by name are by far the get and set functions of Backbone Model, so why not use them? Model.toJSON() works, but you're just copying the attributes-hash unnecessarily around.
this.filterObj = { // Why not make it an object instead of array of objects
"Account": "123",
"Owner": "Bob"
};
this.filtered = this.models.filter(function(model) {
// use the for in construct to loop the object
for (filter in filterObj) {
// if the model doesn't pass a filter check, then return false
if (model.get(filter) !== filterObj[filter]) return false;
}
// the model passed all checks, return true
return true;
});
Hope this helps!
Basically you need to iterate over model's attributes and compare their keys and values to filter's attributes.
self.models = _.filter(self.models, function (model) {
var fits = true; // does this model "fit" the filter?
model = model.toJSON();
_.each(model, function(modelVal, modelKey) {
_.each(filterArr, function(filter) {
if (modelKey === filter.field && modelVal !== filter.filter) {
fits = false
}
}
})
return fits
})
However, with a bit of underscore magic there's a trickier way. I'm not sure if it's better in terms of performance, but it surely looks better to my eye.
// change a bit the way filter is described:
var filter = {Account: '123', Owner: 'Bob'},
// save an array of filter keys (Account, Owner)
filterKeys = _.keys(filter),
// and an array of its values (123, Bob)
filterVals = _.values(filter)
self.models = _.filter(self.models, function (model) {
// pick a subset of model which has the same keys as filter
var filteredSubset = _.pick(model.attributes, filterKeys),
// pick values of this subset
subsetValues = _.values(filteredSubset)
// this values have to be equal to filter's values
// (use .join() to turn array to string before comparison due to references)
return filteredVals.join() === subsetValues.join()
})
Notice that in the latter case all models have to have all the keys declared in filter.
If I were you and I were looking for a most robust way, I would rewrite the first example, but would have changed _.each to standard for loops and return false as soon as first 'non-fit' value is met.

Resources