AngularJS orderby with empty field - javascript

I am ordering a my data and its working all correcty except some fields are empty or have no value. When ordered these empty field come up first. For example when ordering numbers we would get a huge empty list before getting the "0"-values.
I am doing it like thise:
ng-click="predicate = 'name'; reverse=!reverse"
and
ng-repeat="name in names | orderBy:predicate:reverse"
JSFiddle: http://jsfiddle.net/JZuCX/1/
Is there an easy elegant way to fix this? I want the empty fields to come last, no matter what.

How about this for sorting strings:
item in (items|orderBy:['!name', 'name'])
The advantage (apart from being more concise) is it sorts null & undefined with the blank strings.
In my case I wanted the blanks & nulls & undefineds together at the top (nulls and undefineds by default sort to the bottom), so I used:
item in (items|orderBy:['!!name', 'name'])

I'd write a filter that takes items with empty name from ordered array and places them at the end:
<li ng-repeat="item in (items|orderBy:'name'|emptyToEnd:'name')">{{item.name}}</li>
Code might look like this:
.filter("emptyToEnd", function () {
return function (array, key) {
if(!angular.isArray(array)) return;
var present = array.filter(function (item) {
return item[key];
});
var empty = array.filter(function (item) {
return !item[key]
});
return present.concat(empty);
};
});
Working example.
By the way, your fiddle doesn't contain any relevant code. Did you use the wrong link?
Update 2:
Your fiddle with my filter.

Down here! :D
This solution extends the normal functionality of the angularJs orderBy filter to take a third argument specifying whether or not to invert the normal sorting of null and undefined values. It observes the property names it is passed (not just one), and doesn't iterate over items a second as some of the other solutions do. It's used like this:
<li ng-repeat="item in (items|orderBy:'name':false:true)">{{item.name}}</li>
I found a bunch of threads, some not directly about orderBy, and compiled their techniques plus a couple bits of my own into this:
angular.module('lib')
.config(['$provide', function ($provide) {
$provide.decorator('orderByFilter', ['$delegate', '$parse', function ($delegate, $parse) {
return function () {
var predicates = arguments[1];
var invertEmpties = arguments[3];
if (angular.isDefined(invertEmpties)) {
if (!angular.isArray(predicates)) {
predicates = [predicates];
}
var newPredicates = [];
angular.forEach(predicates, function (predicate) {
if (angular.isString(predicate)) {
var trimmed = predicate;
if (trimmed.charAt(0) == '-') {
trimmed = trimmed.slice(1);
}
var keyFn = $parse(trimmed);
newPredicates.push(function (item) {
var value = keyFn(item);
return (angular.isDefined(value) && value != null) == invertEmpties;
})
}
newPredicates.push(predicate);
});
predicates = newPredicates;
}
return $delegate(arguments[0], predicates, arguments[2]);
}
}])
}]);
To use this code verbatim, be to specify 'lib' as a dependency for your app.
Credits to:
$parse
[nullSorter].concat(originalPredicates)
decorator pattern

I don't believe there's an "out of the box" solution for this. I could easily be wrong.
Here's my attempt at a solution using a function as the predicate:
ng-repeat="name in names | orderBy:predicate"
Inside your controller:
$scope.predicate = function(name) {
return name === '' ? 'zzzzzzz' : !name;
/* The 'zzzzzz' forces the empty names to the end,
I can't think of a simpler way at the moment. */
}

In addition to the solution of Klaster_1, add an extra parameter to make the filter more generic:
http://jsfiddle.net/Zukzuk/JZuCX/27/
Implementation
<tr ng-repeat="name in (names | orderBy:predicate:reverse | orderEmpty:'name':'toBottom')">
Filter
.filter('orderEmpty', function () {
return function (array, key, type) {
var present, empty, result;
if(!angular.isArray(array)) return;
present = array.filter(function (item) {
return item[key];
});
empty = array.filter(function (item) {
return !item[key]
});
switch(type) {
case 'toBottom':
result = present.concat(empty);
break;
case 'toTop':
result = empty.concat(present);
break;
// ... etc, etc ...
default:
result = array;
break;
}
return result;
};
});
Thnx Klaster_1!

Sorting, and reverse sorting, using a variable sort column, and keeping the undefined at the bottom, even below the negative values
I love the elegance of Sean's answer above! I needed to give my users the ability to choose the column to sort on, and choice of sort direction, but still require the undefined's to fall to the bottom, even if there are negative numbers.
The key insight from Sean that fixes negative numbers is !!. Use '!'+predicate if you are doing forward sorting and '!!'+predicate if you are doing reverse sorting.
The snippet below demonstrates this. By the way, I have put the variables that set the predicate (choice of propery to sort on) and reverse inside an object ("d") just so that we don't get weird scope issues. You may not need the "d."s in your environment.
Moreover you would probably want to use something better than my crappy buttons at the bottom of the page to control your sort predicate and direction. However this keeps the key parts of the code easy to read.
function mainController($scope) {
$scope.userArray = [
{ name: "Don", age: 20 },
{ name: "Bob", age: 30, height: 170 },
{ name: "Abe", age: 40, height: 160 },
{ name: "Zoe", age: 70 },
{ age: 70, height: 155 },
{ name: "Shorty",age:45,height: -200},
{ name: "TwinkleInEye", age: -1, height: 152 }
]
$scope.d = {}; // Create an object into which info can be stored and not trashed by Angular's tendency to add scopes
$scope.d.predicate = "name"; // This string is the name of the property on which to sort
$scope.d.reverse = false; // True means reverse the sort order
}
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.2.23/angular.min.js"></script>
<body ng-app="" ng-controller="mainController">
<div ng-repeat="user in (userArray | orderBy: (d.reverse ?['!!'+d.predicate,d.predicate]:['!'+d.predicate,d.predicate]) : d.reverse)">
Name {{ user.name }} : Age {{ user.age }} : Height {{ user.height }}
</div>
<br/>
<button ng-click="d.predicate='name';">Name</button>
<button ng-click="d.predicate='age';">Age</button>
<button ng-click="d.predicate='height';">Height</button> Currently: {{d.predicate}}
<br/> Leave undefined at bottom, but otherwise:
<button ng-click="d.reverse= !d.reverse;">Reverse</button> Currently: {{d.reverse}}
</body>

#Klaster_1 was really on to something but as soon as I needed a nested value the filter stopped working. Also, if I was reverse ordering I still wanted my null values to show up before 0. I added $parse to take care of the nested keys and added a reverse parameter to I knew when to put the null values at the top.
.filter("emptyToEnd", function ($parse) {
return function (array, key, reverse) {
if(!angular.isArray(array)) return;
var keyFn = $parse(key);
var present = [];
var empty = [];
angular.forEach(array, function(item){
var val = keyFn(item);
if(angular.isUndefined(val) || val === null) {
empty.push(item);
} else {
present.push(item);
}
});
if (reverse) {
return present.concat(empty);
} else {
return empty.concat(present);
}
};
});

I don't know why other answer suggest to put the null value records at the bottom, If I want to sort normally, means in ASC order all the null on top and in DESC order all the nulls go to bottom, I tried other answers here but could not helped me so change the code to convert the null to '' in my array and it works now smooth like this:
$scope.convertNullToBlank = function (array) {
for (var i = 0; i < array.length; i++) {
if (array[i].col1 === null)
array[i].col1 = '';
if (array[i].col2 === null)
array[i].col2 = '';
}
return array;
}

I created a gist with an alternative filter based on the previous solutions:
https://gist.github.com/360disrupt/1432ee1cd1685a0baf8967dc70ae14b1
The filter extends the existing angular filter:
angular.module 'tsd.orderByEmptyLast', []
.filter 'orderByEmptyLast', ($filter) ->
return (list, predicate, reverse)->
orderedList = $filter('orderBy')(list, if reverse then ['!' + predicate, '-' + predicate] else ['!' + predicate, predicate] )
return orderedList
On newer angular versions you might need to include orderByFilter instead of using $filter
angular.module 'tsd.orderByEmptyLast', ['orderByFilter']
.filter 'orderByEmptyLast', () ->
return (list, predicate, reverse)->
orderedList = orderByFilter(list, if reverse then ['!' + predicate, '-' + predicate] else ['!' + predicate, predicate] )
return orderedList

Related

How to sort list start with first and then contains string search in md-autocomplete AngularJS

I'm new to angularJs and want to filter the search result.
Here is the example :
https://codepen.io/anon/pen/mpJyKm
I want search result based on query input filtered such as string starts with come first in the result list and then contains. I'm using md-autocomplete example and want to filter list in controller only.
e.g. If search input is : A
then result should be like this :
Alabama
Alaska
Arizona
Arkansas
California
Colorado
...
tried this for filtering result but returning Starts with or contains in any order without filtering as I need:
function createFilterFor(query) {
var lowercaseQuery = angular.lowercase(query);
return function filterFn(state) {
return (state.value.indexOf(lowercaseQuery) != -1);
};
}
You can use querySearch() function like this:
function querySearch (query) {
var q = angular.lowercase(query);
var results = self.states
.reduce(function(res, state) {
var idx = state.value.indexOf(q);
if(idx === 0) {
res[0].push(state)
} else if(idx > 0) {
res[1].push(state)
}
return res;
},[[],[]])
return results[0].concat(results[1]);
}
You can go through all states and put those that match query into one of two arrays: the first one is where the state starts from query, and the second if the match is in the middle. After forming these arrays, you can concat them. Since your allStates are already sorted alphabetically, you don't need to apply extra sorting afterwards.
Also, if you want to highlight the matching pattern in case-insensitive way, you should add md-highlight-flags="i" to your item template
<md-item-template>
<span md-highlight-text="ctrl.searchText"
md-highlight-flags="i">{{item.display}}</span>
</md-item-template>
CodePen: https://codepen.io/anon/pen/QajOqj/
You may use
md-items="item in querySearch(searchText) | orderBy : 'fieldName'"
Vadim's is an excellent solution. Here's a generic TS version:
interface QuerySearchProps {
query: string;
data: any;
propToMatch: string;
}
export const querySearch = ({query, data, propToMatch}: QuerySearchProps) => {
const q = query.toLowerCase();
const results = data.reduce(
function (res: any, item: any) {
const idx = item[propToMatch].toLowerCase().indexOf(q);
if (idx === 0) {
res[0].push(item);
} else if (idx > 0) {
res[1].push(item);
}
return res;
},
[[], []],
);
return results[0].concat(results[1]);
};

Angular JS Filter or statement

I have a ng-repeat statement for projects. I then have a dropdown that selects the region the project was in. I have it working if its a single region but i need to check for projects in multiple regions. Example they selected Africa and North America.
<div class="column project-thumbnail" data-ng-repeat="project in projects | orderBy:livePredicate:'title' | filter:liveRegion:'region'>
I need it to be like this:
filter:'Africa' OR 'North America' OR 'etc':'region'>
I have tried to pass it an object, and I have also tried what I saw in another post about a function that passes like this:
$scope.showMovie = function(movie){
return movie.genre === $scope.Filter.Comedy ||
movie.genre === $scope.Filter.Drama ||
movie.genre === $scope.Filter.Action;
Any suggestions or help is super appreciated. The object has a project.region that it is comparing to and can have any number of values. So any selected region I would want to show.
I haven't tried that yet, But I believe you should be able to pass an array to your custom filters and apply the filter logic in there. Something like below:
angular.module('app', []).
filter('regionFilter', function () {
return function (projects, regions) {
var filteredProjects=[]
angular.forEach(projects, function (project) {
if (regions.indexOf(project.region)>=0) {
filteredProjects.push(value);
}
});
return filteredProjects;
};
});
And
<div class="column project-thumbnail" data-ng-repeat="project in projects | regionFilter: regions">
Where regions is an array of the selected regions you want as your filter criteria.
On a side note, your syntax to orderBy seems wrong. It should be like {{ orderBy_expression | orderBy : expression : reverse}}
Your solution will probably be somthing like building a custom filter.
So, if you have a list of possible criteria , separated by ',' (AKA csv)...
.filter('csvFilter', function() {
return function(data, csv, field) {
return data.filter(function(d) {
if (!csv) return true;
var csvArr = csv.split(',');
for (var i = 0; i < csvArr.length; i++) {
if (d[field] === csvArr[i])
return true;
}
return false;
});
}
})
this filter is usin javascript avnilla filter() function.
see in JSFiddle: https://jsfiddle.net/ronapelbaum/svxL486n/
Better Solution
using complex filters for arrays in angular might cost you in performance. in that case, i'de recommand using a simple condition with ng-hide:
html
<ul ng-repeat="project in ctrl.projects">
<li ng-show="ctrl.condition(project.region)">{{project.name}}</li>
</ul>
javascript
this.condition = function(field) {
if (!this.filter) return true;
var csvArr = this.filter.split(',');
for (var i = 0; i < csvArr.length; i++) {
if (field === csvArr[i])
return true;
}
return false;
}
https://jsfiddle.net/ronapelbaum/svxL486n/7/

returning and calling an object in a list (ANGULAR.js)

in this example I'm returning a string depending on whether a text field is a value for "beast" and/or "color". but this is only returning a string. I would return in two conditions that have an object:
http://plnkr.co/edit/fSut1gqIKFAEej8UdbmE?p=preview
in the first conditional I need create an object and return:
{name: item.beast, type: "animal"}
in the second conditional, I need create an object and :
{name: item.color, type: "color of animal"}
and then in the HTML file, would that I could put the list:
{{ item.name }} and {{item.type}}
so if I type "r" should appear in the list:
"rat and animal"
"red and color of animal "
I am not sure what you are trying to get. Have a look at this plunk. Is this what you wanted ?
It works if you push the whole item to the results list like:
results.push(item);
http://plnkr.co/edit/xW9hqC1z1rKddRWaP2g6?p=preview
Look at this
app.service('myFactoryService', function(){
this.createObj = function(name,type){
var obj = {};
obj.name = name;
obj.type = type;
return obj;
}
});
results.push(myFactoryService.createObj(item.beast,"animal"));
I don't know somehow why two-way binding doesn't permit to show two variables differently, only object, but there is defined service to create an object, then it is created in filter according to both options.
You can return objects inside the filter, exactly as you have written them on your question (also, I have cleaned up a bit the ifs... don't use document.getElementById inside the filter, you have the searchText in there already as second parameter of the filter!):
app.filter('searchData', function() {
return function(items, searchText) {
var results = [];
if(searchText) {
angular.forEach(items, function(item) {
if(item.beast.indexOf(searchText) === 0) {
results.push({ name: item.beast, type: 'animal' });
}
if(item.color.indexOf(searchText) === 0) {
results.push({ name: item.color, type: 'color of animal' });
}
});
}
return results;
};
});
And then, you can use this on the P object of the html:
<p ng-repeat = "item in data | searchData : search">{{item.name + ' and ' + item.type}}</p>
I have forked the plunkr here with the changed code:
http://plnkr.co/edit/EDd578?p=preview
Hope it helps,
Best regards,
Rafa.
ps. Also, remember always to use the equality operators !== or === in Javascript, and not the != and == forms.

Creating a binding list projection from a list of search terms

I am trying to create a filtered list projection from a collection of search terms. For instance, if I have one search term, I can do something like this:
if (options.groupKey == "filtered") {
this._items = Data.getItemsFromGroup(this._group);
var query = Windows.Storage.ApplicationData.current.localSettings.values["filters"];
this._items = this._items.createFiltered(function (item) {
if (item.content.search(query) > -1) {
return true
} else {
return false
}
})
}
But what if the 'filters' local setting is a CRLF delimited list, like this:
Cisco
Microsoft
Dell
Currently, the search will compare each term to 'Cisco/nMicrosoft/nDell' which obviously won't work. content.search doesn't accept an array. Should I just do a loop in the createFiltered function somehow? That doesn't seem to be in the spirit of the projection. What is the generally accepted way to do this?
What about storing and object in the "filters" settings where every filter is a property? will that work for you?
if (options.groupKey == "filtered") {
this._items = Data.getItemsFromGroup(this._group);
var query = Windows.Storage.ApplicationData.current.localSettings.values["filters"];
this._items = this._items.createFiltered(function (item) {
return Object.keys(query).indexOf(item) > -1;
})
}
The query object would be something as follows:
{
Cisco: "",
Microsoft: "",
Dell: ""
}
Does that make sense?
edit: made a little change in the code since I believe if (query[item]) would always return false because of javascript type-casting

Filtering on object map rather than array in AngularJS

Given a controller with a $scope property that is an object with other properties rather than an array like below, how should I filter the ng-repeat set?
Here is a JSFiddle: http://jsfiddle.net/ZfGx4/110/
Controller:
function HelloCntl($scope, $filter) {
$scope.friends = {
john: {
name: 'John',
phone: '555-1276'
},
mary: {
name: 'Mary',
phone: '800-BIG-MARY'
},
mike: {
name: 'Mike',
phone: '555-4321'
},
adam: {
name: 'Adam',
phone: '555-5678'
},
julie: {
name: 'Julie',
phone: '555-8765'
}
};
}​
Template:
<div ng:app>
<div ng-controller="HelloCntl">
<input placeholder="Type to filter" ng-model="query">
<ul>
<li ng-repeat="(id, friend) in friends | filter:query">
<span>{{friend.name}} # {{friend.phone}}</span>
</li>
</ul>
</div>
</div>
I would change my data structure to an array. Anyway, here's another implementation to filter your friends object.
angular.module('filters',['utils'])
.filter('friendFilter', function(utils){
return function(input, query){
if(!query) return input;
var result = [];
angular.forEach(input, function(friend){
if(utils.compareStr(friend.name, query) ||
utils.compareStr(friend.phone, query))
result.push(friend);
});
return result;
};
});
This iterates over the object only once, compares by name and phone and can be called like this.
<li ng-repeat="friend in friends | friendFilter:query">
I defined the compareStr in another module, but you don't really need to do it.
angular.module('utils', [])
.factory('utils', function(){
return{
compareStr: function(stra, strb){
stra = ("" + stra).toLowerCase();
strb = ("" + strb).toLowerCase();
return stra.indexOf(strb) !== -1;
}
};
});
Don't forget to inject the filters module into your app
angular.module('app',['filters'])
Here's the full example: http://jsbin.com/acagag/5/edit
I guess you can't do it directly with 'filter'.
Looking at the code in angular.js, these are the first lines of the filter function:
function filterFilter() {
return function(array, expression) {
if (!(array instanceof Array)) return array;
So if it receives something different from an array, it does nothing.
Here is one way to do it, not sure if I would recommend it, but it's is an idea:
In the controller, just convert to an array before passing it to the filter:
$scope.filteredFriends = function() {
var array = [];
for(key in $scope.friends) {
array.push($scope.friends[key]);
}
return $filter('filter')(array, $scope.query);
}
And the ng-repeat:
<li ng-repeat="friend in filteredFriends()">
Example: http://jsbin.com/acagag/2/edit
Maybe a better solution is to write a custom filter.
I had the same problem, and succeeded by creating my own filter that accepts a map, while still using the built-in filter for doing the actual matching.
My custom filter traverses the map, and for each element calls the built-in filter, but given that that filter only accepts an array, I wrap each element in an array of length 1 (the [data] below). So the match succeeds if the output-array's length is still 1.
.filter('mapFilter', function($filter) {
var filter = $filter('filter');
return function(map, expression, comparator) {
if (! expression) return map;
var result = {};
angular.forEach(map, function(data, index) {
if (filter([data], expression, comparator).length)
result[index] = data;
});
return result;
}
})
This setup loses efficiency of course because the built-in filter is required to determine what it needs to do for each element in the map, instead of only once if you give it an array with all elements, but in my case, with a map of 500 elements the filtering is still instanteanous.
I made an example on how to not change your object to an Array. I think this answers the question more correct.
I had the same problem that i could not search in a Object.
http://jsbin.com/acagag/223/edit
angular.module('filters',['utils'])
.filter('friendFilter', function(utils){
return function(input, query){
if(!query) return input;
var result = {};
angular.forEach(input, function(friendData, friend){
if(utils.compareStr(friend, query) ||
utils.compareStr(friendData.phone, query))
result[friend] = friendData;
});
return result;
};
});
So just instead of returning a array you return a object.
Hope this helps someone!
Not an optimal solution but if you want something quick and dirty :
<li ng-repeat="(id, friend) in friends | filter:query" ng-hide="id !== query.id">
<span>{{friend.name}} # {{friend.phone}}</span>
</li>
Or ng-if instead of ng-hide
Rather than using a filter you can also simply :
$http.get('api/users').success(function(data){
angular.forEach(data, function(value, key){
users.push(value);
});
$scope.users = users;
});
And in the template
<input type="text" ng-model="query" placeholder="Search user"/>
<li ng-repeat="user in users | filter:query">
{{ user.name }}
</li>

Categories

Resources