angularjs filter with devider functionality - angularjs

I tried to implement a filter that returns a list as well as entries for divider. TextSearch should also be available with this filter.
Template:
<input type="search" placeholder="Search" ng-model="searchText" autocorrect="off" >
<li class="item item-checkbox"
ng-repeat="destination in destinations | (a)
destinationFilter:searchText:true" (b)
ng-class="destination.letter? 'item-divider':''"> (c)
a iterate over the array destinations
b use the custom filter
c if the item comming from the filter has a letter-field, it's marked as divider
Filter:
app.filter('myFilter', function() {
return function(input, key, startsWith) {
if(input){
var filteredInput = []; (1)
var lastChar = ''; (2)
var re = /.*/i; (3)
if(key){
if(startsWith) (4)
key = "^"+key;
re = new RegExp(key, "i");
}
for(var i=0; i<input.length; i++){
var item = input[i];
if(item.name.match(re)){ (5)
if(item.name.charAt(0) !== lastChar) { (6)
filteredInput.push({name:item.name.charAt(0),letter:true});
lastChar = item.name.charAt(0);
}
filteredInput.push(item); (7)
}
};
return filteredInput;
}
return input;
};
});
1 array to contain the filtered list-elements from input
2 stores last divider character
3 regular expression to match everything
4 if statsWith -> only check reg expression from the beginning of the word
5 if the regular expression-match is successful -> push the item to the filteredInput
6 if last character matches actual item-firstChar dont generate a new list-divider-entry
7 push the item to the filteredInput array
Unfortunately this filter is called very often by a list of 30 elements. It also causes this error:
Error: [$rootScope:infdig] http://errors.angularjs.org/1.2.25/$rootScope/infdig?
Divider are added for each element in the input I guess. It looks very odd.
FIDDLE: HERE

The problem is that your filter produces a new array each call, which leads angular to an infinite loop.
You better provide ng-repeat a constant array instead of a function (filter).
In your example it can be done simply by applying the filter in the controller instead of a view (your real world case might be different though):
$scope.filtered = $filter('destinationFilter')($scope.destinations, undefined, true);
In your html:
<li class="item item-checkbox" ng-repeat="destination in filtered" ng-class="destination.letter? 'item-divider':''" >
See this fiddle.

Related

Count repeated items in an observableArray in knockout js

i have an observableArray with about 540 records
in the records it has an advert property and a date property i would like to do a function that finds all the adverts with on a particular date and counts the number of records eg
Date = 23/02/2015
Advert 1 (16)
Advert 2 (5)
Advert 3 (10)
Total (31)
This should help you get along, but basically you can have a function that iterates through the object and creates an array of counts:
self.counts = ko.observable();
function updateCounts() {
var leads = self.leads(),
counts = {};
for (var i = 0, j = leads.length; i < j; i++) {
var prop = leads[i].date_enquired();
if (counts[prop] == null) {
counts[prop] = {
name : prop,
count : 1
};
}
else {
counts[prop].count++;
}
}
// set it as an array instead of an object
self.counts(Object.keys(counts).map(function (key) { return counts[key]; }));
}
Run this function at the start and whenever the list updates:
updateCounts();
self.leads.subscribe(updateCounts);
Then display the object in the view in whatever way you'd like:
<div data-bind="foreach: counts">
<div data-bind="text: name + ': ' + count"></div>
</div>
JSFiddle
You could also create your own object that handles pushing to the array and updating the list of counts. That way additions are O(1) instead of O(n) each time an item is added or removed. Overall, it's not too big of a deal because iterating over 500 items is really fast.

jQuery 2.1.0 | Reorder li based on separate delimited string

I need to reorder li containing unique same-length text based on position of identical unique same-length semicolon-delimited text found in a separate string, as in following example:
li elements ...
<ul>
<li><span>28dg4</span></li>
<li><span>jjk63</span></li>
<li><span>HN3gE</span></li>
<li><span>k213C</span></li>
</ul>
... sorted by order of semicolon delimited substrings (left to right),
<div id="string">HN3gE;28dg4;k213C;jjk63</div>
... must reorder li like this:
<ul>
<li><span>HN3gE</span></li>
<li><span>28dg4</span></li>
<li><span>k213C</span></li>
<li><span>jjk63</span></li>
</ul>
I've set up a fiddle but can't formulate the approrpiate comparison function (new to me):
http://jsfiddle.net/ysec0ydp/16/
Maybe I cannot use sort and should use something else? I haven't found anything on here or elsewhere that does specifically what I want ;-(
Thx for pointers.
UPDATE
The following code outputs the proper order of string, but doesn't display as appended li:
<div id="string">tree$1234567890;boat$4567321890;9876512340;1736925408</div>
<ul>
<li><span>1234567890</span></li>
<li><span>1736925408</span></li>
<li><span>4567321890</span></li>
<li><span>9876512340</span></li>
</ul>
var ul = $('ul');
var li = ul.children("li");
var my_list = li.map(function() {
return $(this).find('span').text();
}).get();
var my_string = $.map($("#string").text().split(/;/), function (t) {
return t.replace(/^.*\$/, "");
});
li.detach();
temp = [];
for (i = 0; i < my_string.length; i++) {
for (j = 0; j < my_list.length; j++) {
if ( my_string[i] == my_list[j] ) {
temp.push(my_list[j]);
}
}
}
$("ul").append( temp );
// RESULT is one li: 1234567890456732189098765123401736925408
// instead of the 4 reordered li: 1234567890
4567321890
9876512340
1736925408
How can this be fixed? See fiddle: http://jsfiddle.net/ysec0ydp/19/
Here you go:
var orderBy = $('#string').text().split(';');
var $newUl = $('<ul></ul>');
var $myList = $('#my-list'); // gave id="my-list" to your list to identify it.
orderBy.forEach(function(key){
$newUl.append($myList.find('li').filter(function(idx, li){ return $(li).text() === key; }).detach());
});
$myList.append($newUl.html());
I took the liberty of giving an id to your list to identify it.
Fiddle here.

Angular JS using ng-repeat to get in 3's then create a new row

I am trying to create a table using ng-repeat, to 3 items in each row, then create a new row after, etc.
I am using a div span3, but it is not wrapping my bootstrap badges.
Any help would be great.
Thanks
Using a range filter you can do nested repeats- the inner counting off the 3 items in your row and the outer counting off your rows (i.e. groups of three):
<div class="row" ng-repeat="n in [] | range:(data.length/3)+1">
<span ng-repeat='item in data.slice($index*3,($index*3+3))'>
{{item}}
</span>
</div>
and the associated range filter which returns an array that goes from 0 up to the specified parameter:
app.filter('range', function () {
return function (input, total) {
total = parseInt(total);
for (var i = 0; i < total; i++) {
input.push(i);
}
return input;
};

Prepare array of objects before passing it to Angularjs ng-repeat to avoid '10 $digest() iterations reached' error

I have a following ng-repeat in my view:
<div ng-repeat="field in fields">
{{field.someValue}}
</div>
The content of fields needs to be preprocessed before it is given to the view. So in my controller I have a function that loops through the fields object and adds some keys and removes some keys. A simplified pseudocode would look like that
myApp.controller('FieldsController', function($scope) {
$scope.fields = loadFieldsFromResource();
var i=0;
for(i = 0; i < $scope.fields.length; i++) {
if ($scope.fields[i].someProperty > maxValue) {
// Remove an item from the array
$scope.fields.splice(i,1);
}
else if ($scope.fields[i].someProperty < minValue) {
// Add an item to the array
$scope.fields.splice(i,0,createNewField());
}
}
})
Now this produces correct output but gives me the 10 $digest() iterations reached. error.
How can I make it work? (I only need the preprocessing done on init).
I've tried to copy the fields with angular.copy() to a temporary variable. Do the preprocessing on it and then assigning it to the fields variable but still I get the same error.
Is there a way of doing this sort of preprocessing outside of the Angular watch before I give it to the view?
Could you use ng-init?
<div ng-init="processFields()" ng-repeat="field in fields">
{{field.someValue}}
</div>
And then in your controller you could do the following:
myApp.controller('FieldsController', function ($scope) {
$scope.fields = [];
$scope.processFields = function () {
var fields = loadFieldsFromResource();
var i = 0;
for (i = 0; i < fields.length; i++) {
if (fields[i].someProperty > maxValue) {
// Remove an item from the array
fields.splice(i, 1);
}
else if (fields[i].someProperty < minValue) {
// Add an item to the array
fields.splice(i, 0, createNewField());
}
}
$scope.fields = fields;
}
})
I don't know your entire situation but I have few places where I need things loaded and sorted before I display.

angular grouping filter

Following angular.js conditional markup in ng-repeat, I tried to author a custom filter that does grouping. I hit problems regarding object identity and the model being watched for changes, but thought I finally nailed it, as no errors popped in the console anymore.
Turns out I was wrong, because now when I try to combine it with other filters (for pagination) like so
<div ng-repeat="r in blueprints | orderBy:sortPty | startFrom:currentPage*pageSize | limitTo:pageSize | group:3">
<div ng-repeat="b in r">
I get the dreaded "10 $digest() iterations reached. Aborting!" error message again.
Here is my group filter:
filter('group', function() {
return function(input, size) {
if (input.grouped === true) {
return input;
}
var result=[];
var temp = [];
for (var i = 0 ; i < input.length ; i++) {
temp.push(input[i]);
if (i % size === 2) {
result.push(temp);
temp = [];
}
}
if (temp.length > 0) {
result.push(temp);
}
angular.copy(result, input);
input.grouped = true;
return input;
};
}).
Note both the use of angular.copy and the .grouped marker on input, but to no avail :(
I am aware of e.g. "10 $digest() iterations reached. Aborting!" due to filter using angularjs but obviously I did not get it.
Moreover, I guess the grouping logic is a bit naive, but that's another story. Any help would be greatly appreciated, as this is driving me crazy.
It looks like the real problem here is you're altering your input, rather than creating a new variable and outputing that from your filter. This will trigger watches on anything that is watching the variable you've input.
There's really no reason to add a "grouped == true" check in there, because you should have total control over your own filters. But if that's a must for your application, then you'd want to add "grouped == true" to the result of your filter, not the input.
The way filters work is they alter the input and return something different, then the next filter deals with the previous filters result... so your "filtered" check would be mostly irrelavant item in items | filter1 | filter2 | filter3 where filter1 filters items, filter2 filters the result of filter1, and filter3 filters the result of filter 2... if that makes sense.
Here is something I just whipped up. I'm not sure (yet) if it works, but it gives you the basic idea. You'd take an array on one side, and you spit out an array of arrays on the other.
app.filter('group', function(){
return function(items, groupSize) {
var groups = [],
inner;
for(var i = 0; i < items.length; i++) {
if(i % groupSize === 0) {
inner = [];
groups.push(inner);
}
inner.push(items[i]);
}
return groups;
};
});
HTML
<ul ng-repeat="grouping in items | group:3">
<li ng-repeat="item in grouping">{{item}}</li>
</ul>
EDIT
Perhaps it's nicer to see all of those filters in your code, but it looks like it's causing issues because it constantly needs to be re-evaluated on $digest. So I propose you do something like this:
app.controller('MyCtrl', function($scope, $filter) {
$scope.blueprints = [ /* your data */ ];
$scope.currentPage = 0;
$scope.pageSize = 30;
$scope.groupSize = 3;
$scope.sortPty = 'stuff';
//load our filters
var orderBy = $filter('orderBy'),
startFrom = $filter('startFrom'),
limitTo = $filter('limitTo'),
group = $filter('group'); //from the filter above
//a method to apply the filters.
function updateBlueprintDisplay(blueprints) {
var result = orderBy(blueprints, $scope.sortPty);
result = startForm(result, $scope.currentPage * $scope.pageSize);
result = limitTo(result, $scope.pageSize);
result = group(result, 3);
$scope.blueprintDisplay = result;
}
//apply them to the initial value.
updateBlueprintDisplay();
//watch for changes.
$scope.$watch('blueprints', updateBlueprintDisplay);
});
then in your markup:
<ul ng-repeat="grouping in blueprintDisplay">
<li ng-repeat="item in grouping">{{item}}</li>
</ul>
... I'm sure there are typos in there, but that's the basic idea.
EDIT AGAIN: I know you've already accepted this answer, but there is one more way to do this I learned recently that you might like better:
<div ng-repeat="item in groupedItems = (items | group:3 | filter1 | filter2)">
<div ng-repeat="subitem in items.subitems">
{{subitem}}
</div>
</div>
This will create a new property on your $scope called $scope.groupedItems on the fly, which should effectively cache your filtered and grouped results.
Give it a whirl and let me know if it works out for you. If not, I guess the other answer might be better.
Regardless, I'm still seeing the $digest error, which is puzzling: plnkr.co/edit/tHm8uYfjn8EJk3cG31DP – blesh Jan 22 at 17:21
Here is the plunker forked with the fix to the $digest error, using underscore's memoize function: http://underscorejs.org/#memoize.
The issue was that Angular tries to process the filtered collection as a different collection during each iteration. To make sure the return of the filter always returns the same objects, use memoize.
http://en.wikipedia.org/wiki/Memoization
Another example of grouping with underscore: Angular filter works but causes "10 $digest iterations reached"
You can use groupBy filter of angular.filter module,
and do something like this:
usage: (key, value) in collection | groupBy: 'property'or 'propperty.nested'
JS:
$scope.players = [
{name: 'Gene', team: 'alpha'},
{name: 'George', team: 'beta'},
{name: 'Steve', team: 'gamma'},
{name: 'Paula', team: 'beta'},
{name: 'Scruath', team: 'gamma'}
];
HTML:
<ul ng-repeat="(key, value) in players | groupBy: 'team'" >
Group name: {{ key }}
<li ng-repeat="player in value">
player: {{ player.name }}
</li>
</ul>
<!-- result:
Group name: alpha
* player: Gene
Group name: beta
* player: George
* player: Paula
Group name: gamma
* player: Steve
* player: Scruath

Resources