jQuery 2.1.0 | Reorder li based on separate delimited string - arrays

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.

Related

reorder list randomly in ng-repeat

I have an ng-repeat as following :
<ul>
<li ng-repeat="o in villes | limitTo:5">{{o.nomVille}}</li>
</ul>
I want to reorder the villes list randomly before I limit it to 5, so every time I open my page I get 5 different villes each time.
is there a filter in angularjs who can do that for me ?
edit :
I created a costum filter to randomize that list as following :
.filter('random', function() {
return function(val) {
let shuffle = (a) => {
let r = [];
while (arr.length)
r.push(
arr.splice( (Math.floor(Math.random() * arr.length)) , 1)[0]
);
return shuffle(val);
}
};
});
and in ng-repeat I did this :
<li ng-repeat="o in villes | random | limitTo:5">{{o.nomVille}}</li>
but I cant no longer see anything in my page.
this is the example on jsfiddle : https://jsfiddle.net/z10wwhcv/
You'd have to build a custom filter function that randomizes the order and then apply the filters in the correct order (random before limit).
Have a look at this for details:
Using AngularJS how could I randomize the order of a collection?
If you want the order to change each time you load the page you can't do it as a filter as that will presumably change on each digest cycle. You need to store villes on the scope somewhere and generate it in a random order when the page loads, e.g. in your controller function for that page.
use the filter in the controller (which is also a best practice performance boost):
$scope.shuffled = $filter('random',$scope.villes)
you'll need to inject the service in the controller
this is what your filter should look like ( not tested but should work ):
.filter('random', function() {
return function(a) {
var r = [];
while (a.length)
r.push(
a.splice((Math.floor(Math.random() * a.length)), 1)[0]
);
return r;
}
}
This is the solution I created :
shufflemodule.filter('shuffle', function() {
var shuffledArr = [],
shuffledLength = 0;
return function(arr) {
var o = arr.slice(0, arr.length);
if (shuffledLength == arr.length) return shuffledArr;
for(var j, x, i = o.length; i; j = parseInt(Math.random() * i), x = o[--i], o[i] = o[j], o[j] = x);
shuffledArr = o;
shuffledLength = o.length;
return o;
};
});
http://jsfiddle.net/aimad_majdou/6mngvo38/

angularjs filter with devider functionality

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.

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.

AngularJS Filter-- two filters with an 'or' relationship

Just wondering: in AngularJS, is there a native way to filter such that it has an 'or' relationship instead of the 'and' ?
for example:
<tr ng-repeat="account in accounts | filter1 *OR* filter2 *OR* filter3" >
so if any of the filters match, it returns that object. As of right now, all three have to pass in order for it to show up.
Thanks a lot,
Y
You could write a custom filter. It could accept the names of other filters and check each one of them to see if each item in the accounts array is a valid match against any of the filters. The following example shows kind of the idea, thought I haven't actually run the code to see if it works, so forgive me for any typos:
app.filter('anyOf', function($filter) {
return function(){
var array = arguments[0];
var result = [];
angular.forEach(array, function(item){
for(var i=1; i<arguments.length; i++){
var filter = $filter(arguments[i]);
if(filter([item]).length){
result.push(item);
break;
}
}
});
return result;
}
});
And then you would use it like so:
<tr ng-repeat="account in accounts | anyOf:'filter1':'filter2':'filter3'" >
I think it should work, though its not very efficient because its taking every item in the input array and checking it against any of the filters. But that might work if your accounts array wasn't super long.
You can use $filter to call filters in your own code, so you can pass the filter names as arguments to a new filter to merge the results (JSFIDDLE).
app.filter('merge', function($filter) {
return function(input, extra) {
var result = [];
for (var i = 1; i < arguments.length; i++) {
angular.forEach($filter(arguments[i])(input), function(item) {
if (result.indexOf(item) < 0) {
result.push(item);
}
});
}
return result;
};
});
And you can pass arguments to your filter like this:
<ul>
<li ng-repeat="item in items|merge:'color':'rating'">
{{item.rating}}: {{item.color}}
</li>
</ul>
If you wanted to pass arguments to the filter you're merging, you could parse them yourself in the merge filter:
<h3>Merge brown and rating >= 5</h3>
<ul>
<li ng-repeat="item in items|mergeWithArgs:['color','brown']:['rating',5]">
{{item.rating}}: {{item.color}}
</li>
</ul>
Code:
app.filter('mergeWithArgs', function($filter) {
return function(input) {
console.dir(arguments);
var result = [];
for (var i = 1; i < arguments.length; i++) {
var params = arguments[i];
var filter = $filter(params[0]);
// remove filter name, prepend input
params.splice(0, 1, input);
angular.forEach(filter.apply(this, params), function(item) {
if (result.indexOf(item) < 0) {
result.push(item);
}
});
}
return result;
};
});

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.

Resources