Live search in html table using cache - javascript

I used this code to live search in table:
$search.keyup(function() {
var value = this.value.toLowerCase().trim();
$.each($table.find('tbody'), function() {
if($(this).text().toLowerCase().indexOf(value) == -1)
$(this).hide();
else
$(this).show();
});
});
It worked great, but now I have 1000+ rows in table and searching is really slow on older machines so I created array from table:
function getTableData() {
var data = [];
$table.find('tbody').each(function (rowIndex, r) {
var cols = [];
$(this).find('td').each(function (colIndex, c) {
cols.push(c.textContent);
});
data.push(cols);
});
return data;
};
And I don't know how to search same way in this array to get tbody index.

3 Update uses cache and a hidden results box;
Here we hide the table, and create a results list on the fly, please note the html markup is very simple and incorrect but it demonstrates the speed improvements of not touching the dom when we cycle our memory cache, the next improvement would be to cache the html of each row and have it in our cache object, thus no need to get the row from the dom when it matches.
http://jsfiddle.net/hh6aed45/5/
var $search = $('input[type="text"]');
var $table = $('table');
var $result = document.getElementById("search");
var i=1;
function uuid(){
i++;
return "A"+i;
};
$search.keyup(function() {
var value = this.value.toLowerCase().trim(), html="";
for (row in cache) {
// the getElementById can be speeded up by caching the html in our memory cache!
if (cache[row].indexOf(value) !== -1) html+=document.getElementById(row).innerHTML;
}
$result.innerHTML=html;
});
function getTableData() {
var cache = {};
$table.find('tbody').each(function (rowIndex, r) {
$(this).find("tr").each(function(rowIndex,r){
var cols = [], id = uuid();
r.id=id;
$(this).find('td').each(function (colIndex, c) {
cols.push(c.textContent);
});
cache[id]=cols.join(" ").toLowerCase();
});
});
return cache;
};
var cache = getTableData();
2 Update uses just a memory cache!
http://jsfiddle.net/hh6aed45/3/
This caches the dom as you requested, I add a uuid as an id so that the search can find each row, i could of cached an object and included a live reference to the dom. in fairness as I said I would only switch on what filters in and switch in the minority, as that would speed the loop as no Dom changes would occur for all rows that are not matching, which out of 10,000 would be many. I would also toggle a css class for "in" and "out", that would allow a live list to be cached of all in items, which then could be hidden before the search starts. So in basic, hide all 10,000 rows, turn on what matches, thus the loop does not touch the Dom unless it has! That is about as fast as you will get it.
var $search = $('input[type="text"]');
var $table = $('table');
var i=1;
function uuid(){
i++;
return "A"+i;
};
$search.keyup(function() {
var value = this.value.toLowerCase().trim();
for (row in cache) {
document.getElementById(row).style.display = (cache[row].indexOf(value) === -1) ? "none" : "table-row";
}
});
function getTableData() {
var cache = {};
$table.find('tbody').each(function (rowIndex, r) {
$(this).find("tr").each(function(rowIndex,r){
var cols = [], id = uuid();
r.id=id;
$(this).find('td').each(function (colIndex, c) {
cols.push(c.textContent);
});
cache[id]=cols.join(" ").toLowerCase();
});
});
return cache;
};
var cache = getTableData();
1 uses native raw javascript with optimizations;
For this kind of thing plane java-script is a must, if your talking about performance first things first, throw jquery out in your loop. Cache the dom first, dont keep loading it.
Speed is an illusion when your building programs.
For example you could do the lookup after two or three letters, not one. You could also change the code so that if a row does not match it is skipped, not switched into view.
so step one turn all rows on, (find all rows that are off, cache the view as views are live)
step two remove rows, not turn rows on and off as you go, that will speed the loop. here is
a sped up version walking the dom, if we had 10,000 rows two test we could improve and improve.
Here is a starting point, from here we use the steps as suggested above:
var $search = $('input[type="text"]'),
$table = $('table'),
$body = $table.find('tbody')
$rows = $body.find('tr'); // this view of the dom is live!
$search.keyup(function() {
var value = this.value.toLowerCase().trim();
//loops are costly, native is ultra fast
for (var i = 0; l=$rows.length, i < l; i++) {
$rows[i].style.display= ($rows[i].innerHTML.toLowerCase().indexOf(value) === -1) ? "none" : "table-row";
}
});
http://jsfiddle.net/hh6aed45/

Related

Setting innerHTML/html() on DOM-elements within nested loop - IE performance

I am working on some horrible legacy code where a JSON-object is fetched from the server. The object is a list of IDs that correspond to DOM-Nodes. Each list-item contains new HTML-Code-Snippets to be inserted for that node. Basically it looks like this:
var $table = $("#theTable");
var tableData = getData();
for (var row in tableData) {
// find the TR
var $row = $table.find('tr[data-id=' + row + ']');
// update many sub-elements of the TR (~100-1000 nodes)
for (var el in row) {
var $el = $row.find("td[data-element='" + el + "']");
if ($el.length) {
$el[0].innerHTML = row[el];
}
}
}
Obviously it is bad practice to select nodes or call .innerHTML/.html() within a loop, but at this point there is no other choice. This results in very bad performance especially in IE. I tried to hide/detach the node, before doing the updates, reattaching/showing it again when everything is done, but it did not improve performance what so ever - it just got worse.
Is there any way to easily speed this up?

How to Get Filtered data from paged ui-grid

I'd like to get filtered data from a ui-grid when the paging feature is enabled. In general case I used
$scope.gridApi.core.on.filterChanged($scope, function () {
if ($scope.gridApi.grid.columns[1].filter.term != "" && $scope.gridApi.grid.columns[1].filter.term != undefined) {
var dd =$scope.gridApi.core.getVisibleRows($scope.gridApi.grid);
console.log(dd);
});
but the code doesn't work well when the paging is enabled, it return only rows of the first page. but I need all the filtered data.
the easiest solution is filter data source based on the filter term but it decreases the performance dramatically.
any advice?
Note: I didn't try it with pagination, just grouping, but hope it gives you a hint.
Try using the rowsVisibleChanged event together with the filterChanged event. You have to use both because if you use the filterChanged event alone it won't work since it's launched before the rows are actually filtered. I use a flag variable (filterChanged) to know if a filter was modified.
Then, use something like lodash to filter the $scope.gridApi.grid.rows that have the visible property set to true:
// UI-Grid v.3.0.7
var filterChanged = false;
$scope.gridApi.core.on.filterChanged($scope, function() {
filterChanged = true;
});
$scope.gridApi.core.on.rowsVisibleChanged($scope, function() {
if(!filterChanged){
return;
}
filterChanged = false;
// The following line extracts the filtered rows
var filtered = _.filter($scope.gridApi.grid.rows, function(o) { return o.visible; });
var entities = _.map(filtered, 'entity'); // Entities extracted from the GridRow array
});
I was able to export filtered data across all pagination via uiGridExporterService service. Thanks to #Patricio's above answer for the hint.
//you can set it to ALL or VISIBLE or SELECTED
var columnsDownloadType = uiGridExporterConstants.ALL;
//get all the visible rows across all paginations
var filtered = _.filter(grid.api.grid.rows, function (o) {
return o.visible;
});
//get the entities of each of the filtered rows
var entities = _.map(filtered, 'entity');
//get all or visible column headers of this grid depending on the columnsDownloadType
var exportColumnHeaders = grid.options.showHeader ? uiGridExporterService.getColumnHeaders(grid, columnsDownloadType) : [];
var exportData = [];
/**this lodash for-each loop will covert the grid data into below array of array format
* [[{value:'row1col1value'},{value:'row1col2value'}],[{value:'row2col1value'},{value:'row2col2value'}].....]
* uiGridExporterService.formatAsCsv expects it in this format
**/
_.each(entities, function (row) {
var values = [];
_.each(exportColumnHeaders, function (column) {
var value = row[column.name];
values.push({value: value});
});
exportData.push(values);
});
//format the header,content in csv format
var csvContent = uiGridExporterService.formatAsCsv(exportColumnHeaders, exportData, ',');
//export as csv file
uiGridExporterService.downloadFile(grid.options.exporterCsvFilename, csvContent, grid.options.exporterOlderExcelCompatibility);
I tried the custom exporter and it worked!
Prerequisites :
enableSelectAll:true,
multiSelect:true,
your controller needs :
uiGridExporterService,uiGridExporterConstants
app module needs :
"ui.grid.selection" ,"ui.grid.exporter"
$scope.exportCSV = function(){
var exportService=uiGridExporterService;
var grid=$scope.gridApi.grid;
var fileName="myfile.csv";
exportService.loadAllDataIfNeeded(grid, uiGridExporterConstants.ALL, uiGridExporterConstants.VISIBLE).then(function() {
var exportColumnHeaders = exportService.getColumnHeaders(grid, uiGridExporterConstants.VISIBLE);
$scope.gridApi.selection.selectAllVisibleRows();
var exportData = exportService.getData(grid, uiGridExporterConstants.SELECTED,uiGridExporterConstants.VISIBLE);
var csvContent = exportService.formatAsCsv(exportColumnHeaders, exportData, grid.options.exporterCsvColumnSeparator);
exportService.downloadFile(fileName, csvContent, grid.options.exporterOlderExcelCompatibility);
$scope.gridApi.selection.clearSelectedRows();
});
}

Loop, get unique values and update

I am doing the below to get certain nodes from a treeview followed by getting text from those nodes, filtering text to remove unique and then appending custom image to the duplicate nodes.
For this I am having to loop 4 times. Is there is a simpler way of doing this? I am worried about it's performance for large amount of data.
//Append duplicate item nodes with custom icon
function addRemoveForDuplicateItems() {
var treeView = $('#MyTree').data('t-TreeView li.t-item');
var myNodes = $("span.my-node", treeView);
var myNames = [];
$(myNodes).each(function () {
myNames.push($(this).text());
});
var duplicateItems = getDuplicateItems(myNames);
$(myNodes).each(function () {
if (duplicateItems.indexOf($(this).text()) > -1) {
$(this).parent().append(("<span class='remove'></span>"));
}
});
}
//Get all duplicate items removing unique ones
//Input [1,2,3,3,2,2,4,5,6,7,7,7,7] output [2,3,3,2,2,7,7,7,7]
function getDuplicateItems(myNames) {
var duplicateItems = [], itemOccurance = {};
for (var i = 0; i < myNames.length; i++) {
var dept = myNames[i];
itemOccurance[dept] = itemOccurance[dept] >= 1 ? itemOccurance[dept] + 1 : 1;
}
for (var item in itemOccurance) {
if (itemOccurance[item] > 1)
duplicateItems.push(item);
}
return duplicateItems;
}
If I understand correctly, the whole point here is simply to mark duplicates, right? You ought to be able to do this in two simpler passes:
var seen = {};
var SEEN_ONCE = 1;
var SEEN_DUPE = 2;
// First pass, build object
myNodes.each(function () {
var name = $(this).text();
var seen = seen[name];
seen[name] = seen ? SEEN_DUPE : SEEN_ONCE;
});
// Second pass, append node
myNodes.each(function () {
var name = $(this).text();
if (seen[name] === SEEN_DUPE) {
$(this).parent().append("<span class='remove'></span>");
}
});
If you're actually concerned about performance, note that iterating over DOM elements is much more of a performance concern than iterating over an in-memory array. The $(myNodes).each(...) calls are likely significantly more expensive than iteration over a comparable array of the same length. You can gain some efficiencies from this, by running the second pass over an array and only accessing DOM nodes as necessary:
var names = [];
var seen = {};
var SEEN_ONCE = 1;
var SEEN_DUPE = 2;
// First pass, build object
myNodes.each(function () {
var name = $(this).text();
var seen = seen[name];
names.push(name);
seen[name] = seen ? SEEN_DUPE : SEEN_ONCE;
});
// Second pass, append node only for dupes
names.forEach(function(name, index) {
if (seen[name] === SEEN_DUPE) {
myNodes.eq(index).parent()
.append("<span class='remove'></span>");
}
});
The approach of this code is to go through the list, using the property name to indicate whether the value is in the array. After execution, itemOccurance will have a list of all the names, no duplicates.
var i, dept, itemOccurance = {};
for (i = 0; i < myNames.length; i++) {
dept = myNames[i];
if (typeof itemOccurance[dept] == undefined) {
itemOccurance[dept] = true;
}
}
If you must keep getDuplicateItems() as a separate, generic function, then the first loop (from myNodes to myNames) and last loop (iterate myNodes again to add the span) would be unavoidable. But I am curious. According to your code, duplicateItems can just be a set! This would help simplify the 2 loops inside getDuplicateItems(). #user2182349's answer just needs one modification: add a return, e.g. return Object.keys(itemOccurance).
If you're only concerned with ascertaining duplication and not particularly concerned about the exact number of occurrences then you could consider refactoring your getDuplicateItems() function like so:
function getDuplicateItems(myNames) {
var duplicateItems = [], clonedArray = myNames.concat(), i, dept;
for(i=0;i<clonedArray.length;i+=1){
dept = clonedArray[i];
if(clonedArray.indexOf(dept) !== clonedArray.lastIndexOf(dept)){
if(duplicateItems.indexOf(dept) === -1){
duplicateItems.push(dept);
}
/* Remove duplicate found by lastIndexOf, since we've already established that it's a duplicate */
clonedArray.splice(clonedArray.lastIndexOf(dept), 1);
}
}
return duplicateItems;
}

Set HTML table rows all at once

I currently have code that runs through every row of a html table and updates it with a different row.
Here is the code
function sort(index) {
var rows = $table.find('tbody tr');
var a = $table.find('tbody tr');
//Only sort if it has not been sorted yet and the index not the same
if(sortedIndex === index){
for(var i = 0; i < rows.length; i++){
a[i].outerHTML = rows[(rows.length - i)-1].outerHTML;
}
toggleSorted();
}else{
sortedIndex = index;
rows.sort(naturalSort);
for (var i = 0; i < rows.length; i++) {
a[i].outerHTML = rows[i].outerHTML;
}
sortedDown = true;
}
$('#${tableId}').trigger('repaginate');
};
What I am trying to do is to instead of going through every single row in the for loop and setting a[i].outterHTML = rows[i].outterHTML; I would like to just set all of the rows at once. Currently it takes about 1.5 seconds to set them and that is very slow.... Only issue is I cannot seem to find a way to do this. Is this actually possible? (It takes 1.5 seconds on large data sets which is what I am working with).
Since the rows are the same just reordered, you can .append them with the new order:
var $tBody = $table.find('tbody');
var $rows = $tBody.find('tr');
if(sortedIndex === index){
toggleSorted();
$tBody.append($rows.get().reverse());
}
else {
sortedIndex = index;
sortedDown = true;
$tBody.append($rows.get().sort(naturalSort));
}
Here's a fiddle that demonstrates the above: http://jsfiddle.net/k4u45Lnn/1/
Unfortunately the only way to "set all of your rows at once" is to loop through all of your rows and perform an operation on each row. There may be some libraries which have methods and functions that make it look like you're doing the operation on all of your rows at one shot, but ultimately if you want to edit every element in a set you need to iterate through the set and carry out the action on each element, seeing as HTML doesn't really provide any way to logically "link" the attributes of your elements.

search for element within element by tagname

After being stuck for a few hours on this problem, i think it is time for call for help on this.
Situation
I have a XML file which i need to filter and group. I've managed to filter it with the :Contains part. I've also determined the nodes on which i need to group (the getGroups function gives those back to me). Now i want to create a new XML with the filtered values and grouped by the returned keys.
Code
var XMLElement = document.createElement("DataElementsCalc");
jQuery(xml).find("DataElements " + topNodes + filter).each( function() {
var dataSetTemp = this.parentNode;
if(calculation1 != "")
{
var groupKeys = getGroups(dataSetTemp,calculation1);
var tempXML = XMLElement;
jQuery(groupKeys).each(function (key,value) {
var tempValue = 'a' + value.toLowerCase().replace(/\W/g, '');
if(tempXML.getElementsByTagName(tempValue).length > 0)
{
tempXML = tempXML.getElementsByTagName(tempValue);
}
else
{
var Node = document.createElement(tempValue);
tempXML.appendChild(Node);
tempXML = Node;
}
});
var Node = document.createElement("InfoSet");
var x = dataSetTemp.childNodes;
for (i=0; i < x.length; i++)
{
if(x[i].nodeType == 1)
{
var tempElement = document.createElement(x[i].nodeName);
tempElement.innerHTML = x[i].childNodes[0].nodeValue;
Node.appendChild(tempElement);
}
}
tempXML.appendChild(Node);
}
});
Explanation
As said in the situation, i already filtered the XML and have the groupNames from the getGroups function. There are a few other things i need to explain for this code:
tempValue is being build as a a + value.toLowerCase().replace(/\W/g, '');. This is being done because i possible get dates into the groupKeys. This way the node name is getting a working name (i received errors on other ways).
I want to create a new XML which is leveled by the groups. If a group already exists, i want to create a new element in it, not get a new group with the same name. (my problem at the moment).
Problem
As mentioned above, the groups aren't checked properly. Firstly: tempXML.getElementsByTagName(tempValue).length returns the error that the function tempXML.getElementsByTagName does not exists. Secondly: If i change this to document.getElemetsByTagName I get a lot of the same nodes in my XML file.
Effect
The grouping effect doesn't work as it should. I get OR an error, OR a lot of the same nodes in my DataElementsCalc.
Questions
How can i solve this? How do create nodes beneath specific nodes (for if there is a group A beneath group 1 as well as beneath group 2)?
Tried
Change tempXML to document on different places (all getElementsByTagName, at the same time or not). Looked for another way to create a XML which is easier to handle (haven't found one, yet)
As mentioned by myself in the comments of the question:
I also don't see anything in the source code for this (maybe this is the reason why it doesn't work??)
I tried to place the XMLElement into an existing element on my webpage (like this:
var XMLElement = document.createElement("DataElementsCalc");
jQuery('.basicData').append(XMLElement);
in which basicData is a class of an existing element).
Now i do get a list of all elements ordered on the groups i wanted.
Final version
var XMLElement = jQuery("<DataElementsCalc/>");
jQuery('.basicData').append(XMLElement);
jQuery(xml).find("DataElements " + topNodes + filter).each( function()
{
aantalElementsTotal++;
var dataSetTemp = this.parentNode;
if(calculation1 != "")
{
var groupKeys = getGroups(dataSetTemp,calculation1);
var tempXML = XMLElement;
var groupId = '';
jQuery(groupKeys).each(function (key,value) {
var tempValue = 'a' + value.toLowerCase().replace(/\W/g, '');
groupId += 'a' + value.toLowerCase().replace(/\W/g, '');
if(jQuery("#" + groupId).length > 0)
{
tempXML = jQuery("#" + groupId);
}
else
{
var Node = jQuery("<"+tempValue+"/>");
jQuery(Node).attr('id', groupId);
jQuery(tempXML).append(Node);
tempXML = Node;
}
});
var Node = jQuery("<InfoSet/>");
var x = dataSetTemp.childNodes;
for (i=0; i < x.length; i++)
{
if(x[i].nodeType == 1)
{
var tempElement = jQuery("<"+x[i].nodeName+"/>");
jQuery(tempElement).text(x[i].childNodes[0].nodeValue);
jQuery(Node).append(tempElement);
}
}
jQuery(tempXML).append(Node);
}
});

Categories

Resources