Speed up selectors and method - javascript

Before I dive in to the jQuery/Sizzle source I thought i'd ask here about ways to speed the below method up.
This is a standard "Check All" checkbox scenario. A header cell (<thead><tr><th>) has a checkbox that when checked checks all other checkboxes in it's table's tbody that are in the same column.
This works:
// We want to check/uncheck all columns in a table when the "select all"
// header checkbox is changed (even if the table/rows/checkboxes were
// added after page load).
(function () {
// use this dummy div so we can reattach our table later.
var dummy = $("<div style=display:none />");
// hook it all up!
body.delegate(".js_checkAll", "change", function () {
// cache selectors as much as possible...
var self = $(this),
// use closest() instead of parent() because
// the checkbox may be in a containing element(s)
cell = self.closest("th"),
// Use "cell" here to make the "closest()" call 1 iteration
// shorter. I wonder if "parent().parent()" would be faster
// (and still safe for use if "thead" is omitted)?
table = cell.closest("table"),
isChecked,
index;
// detaching speeds up the checkbox loop below.
// we have to insert the "dummy" div so we know
// where to put the table back after the loop.
table.before(dummy).detach();
index = cell.index();
isChecked = this.checked;
// I'm sure this chain is slow as molasses
table
// get only _this_ table's tbody
.children("tbody")
// get _this_ table's trs
.children()
// get _this_ table's checkboxes in the specified column
.children(":eq(" + index + ") :checkbox")
// finally...
.each(function () {
this.checked = isChecked;
});
// put the table back and detach the dummy for
// later use
dummy.before(table).detach();
});
} ());
However, for 250+ rows, it starts to become slow (at least on my machine). Users may need to have up to 500 rows of data so paging the data isn't the solution (items are already paged # 500/page).
Any ideas how to speed it up some more?

I wouldn't use all those calls to .children() like that. You'd be much better off just using .find() to find the checkboxes, and then check the parents:
table.find('input:checkbox').each(function(_, cb) {
var $cb = $(cb);
if ($cb.parent().index() === index) cb.checked = isChecked;
});
By just calling .find() like that with a tag name ('input'), Sizzle will just use the native getElementsByTagName (if not querySelectorAll) to get the inputs, then filter through those for the checkboxes. I really suspect that'd be faster.
If finding the parent's index gets expensive, you could always precompute that and store it in a .data() element on the parent (or right on the checkbox for that matter).

// I wonder if "parent().parent()" would be faster
// (and still safe for use if "thead" is omitted)?
No. If <thead> is omitted then in HTML a <tbody> element will be automatically added, because in HTML4 both the start-tag and the end-tag are ‘optional’. So in HTML, it would be parent().parent().parent(), but in XHTML-served-as-XML, which doesn't have the nonsense that is optional tags, it would be parent().parent().
It's probably best to stick with closest(). It's clearer, it's not particularly slow and you're only using it once, so it's not critical anyway.
index = cell.index();
Although, again, this is only once per table so not critical, there is a standard DOM property to get the index of a table cell directly, which will be faster than asking jQuery to search and count previous siblings: index= cell[0].cellIndex.
// we have to insert the "dummy" div so we know
// where to put the table back after the loop.
That's a bit ugly. Standard DOM has a better answer to this: remember the element's parentNode and nextSibling (which may be null if this is the last sibling) and when you're done you can parent.insertBefore(table, sibling).
.children("tbody")
.children()
.children(":eq(" + index + ") :checkbox")
.each(function () {
this.checked = isChecked;
});
You should consider using .children().eq(index) rather than hiding that away in a selector. Won't make a big difference, but it's a bit clearer.
In any case, you can save jQuery's selector engine a bunch of work by using some more standard DOM to traverse the table:
$.each(table[0].tBodies[0].rows, function() {
$(this.cells[index]).find('input[type=checkbox]').each(function() {
this.checked = isChecked;
});
});
Selector queries can be fast, when they're operating against the document and using only standard CSS selectors. In this case jQuery can pass the work onto the browser's fast document.querySelectorAll method. But scoped selectors (find and the second argument to $()) can't be optimised due to a disagreement between jQuery and Selectors-API over what they mean, and non-standard Sizzle selectors like :eq and :checkbox will just get rejected. So this:
$('#tableid>tbody>tr>td:nth-child('+(index+1)+') input[type=checkbox]')
could actually be faster, on modern browsers with querySelectorAll!

Related

Performant append to array of existing DOM elements

I have the following piece of code, that loops over an Array of DOM objects, runs a test and appends some text to the node if returns true.
$.each( selections, function ( i, e ) {
var test = some_test(e);
if(test) {
$(e).append('passed');
}
});
Whilst this code works fine, on a large set it is obviously going to perform a lot of appends to the DOM. This article on reasons to use append correctly demonstrates how appending to the DOM is far more performant :
var arr = reallyLongArray;
var textToInsert = [];
var i = 0;
$.each(arr, function(count, item) {
textToInsert[i++] = '<tr><td name="pieTD">';
textToInsert[i++] = item;
textToInsert[i++] = '</td></tr>';
});
$('table').append(textToInsert.join(''));
My question is what is the most performant way to make changes to a set of existing DOM elements without having to call .append on each element ? The above example demonstrates the creation of a new element. Is this the only way ?
What makes live DOM manipulations slow is mainly the fact that it's causing DOM reflows for most manipulations that are made. Therefore, you should strive to reduce the amount of DOM reflows by reducing the number of live DOM manipulations.
If you want to manipulate multiple elements that are already part of the DOM, one strategy that can be used is to temporarly remove the parent of those nodes from the DOM, manipulate the elements and then re-attach the parent node where it was.
In the exemple below, I detach the table before manipulating it's rows and then reattach it to the DOM. That's 2 reflows rather than n reflows.
var data = [1, 2, 3, 4, 5, 6, 7];
populateCells(document.querySelector('table'), data);
function populateCells(table, data) {
var rows = table.rows,
reAttachTable = temporarilyDetachEl(table);
data.forEach(function (num, i) {
rows[i].cells[0].innerHTML = num;
});
reAttachTable();
}
function temporarilyDetachEl(el) {
var parent = el.parentNode,
nextSibling = el.nextSibling;
parent.removeChild(el);
return function () {
if (nextSibling) parent.insertBefore(el, nextSibling);
else parent.appendChild(el);
};
}
<table>
<tr><td></td></tr>
<tr><td></td></tr>
<tr><td></td></tr>
<tr><td></td></tr>
<tr><td></td></tr>
<tr><td></td></tr>
<tr><td></td></tr>
</table>
You could completely rebuild the set of DOM elements using one of the techniques from the linked article, then clear the existing set and .append() the new one. But given that you already have the set and just want to adjust a subset of its items, there's nothing wrong with your existing code.
When talking about performance it is a good idea to profile your code so that you don't have to guess.
Here is a simple test case around this example:
http://jsperf.com/most-performant-way-to-append-to-dom-elements
(and related fiddle to demo the visual side of this: http://jsfiddle.net/f62ptjbf/)
This test compares four possible methods for doing this: (does not by any means cover all solutions)
Append "passed" as text node (like your example code)
Append "passed" as a SPAN node (slight variation to your example)
Build a DOM fragment that renders the nodes and records the "passed", then add to DOM as a single append operation from HTML string
Remove selection elements from DOM, manipulate them, then re-add to DOM.
It shows that [at least in my copy of Chrome browser] the fastest method is removal of elements before processing, then re-add to DOM after processing. (#4)
Here is another test that shows [at least in my copy of Chrome] it is faster to append SPAN elements with the text "passed" than it is to append text nodes.
http://jsperf.com/append-variations
Based on these findings, I could recommend two potential performance improvements to your code:
Remove DOM elements in selection before manipulating them, then re-add when finished.
Append a SPAN with the text "passed" instead of appending the text directly.
However, #1 works best if the elements have a shared parent. The performance will be a function of the number of append operations.

Is it possible to use a variable to highlight a row in jQuery?

I'm trying to highlight rows in a table using jQuery, but I'm wondering if it's possible to use a variable for the row I want highlighted. This is the code I have now, which is not working.
var rowNumber = 3 //I want to use a loop, but for testing purposes I have it set to 3
$('tr:eq(rowNumber)').addClass('highlight');
Sure, why not. You may pass a variable in :eq() selector:
$("tr:eq(" + rowNumber + ")").addClass("highlight");
or use eq() method instead:
$("tr").eq(rowNumber).addClass("highlight");
$('tr').eq(rowNumber).addClass('highlight');
Should work for you.
Let me first address isolated access (i.e. not taking into consideration optimisation for loops)
Best solution: Use .eq() (fast, nice and short)
You could try something like
$('tr').eq(rowNumber).addClass('highlight');
Explanation: .eq(index) Reduces the set of matched elements to the one at the specified index.
Source: http://api.jquery.com/eq/
Alternative solution: Use the ":eq(index)" selector (unnecessarily slower, more verbose and convoluted)
$("tr:eq("+rowNumber+")").addClass('highlight');
A third solution: (fast, but more verbose than the proposed solution)
$($('tr').get(rowNumber)).addClass('highlight');
How this one works: $('tr').get(rowNumber) gets the (rowNumber+1)th DOM element matching the query selector and then this is wrapped in jQuery goodness using the surrounding $( ).
More info at: http://api.jquery.com/get/
Feel free to experiment with the accompanying jsFiddle:
http://jsfiddle.net/FuLJE/
If you are particularly performance conscious and are indeed are going to iterate through an array you can do this instead:
var trs = $('tr').get(); //get without arguments return the entire array of matched DOM elements
var rowNumber, len = trs.length;
for(rowNumber = 0; rowNumber < len; rowNumber++) {
var $tr = $(trs[rowNumber]);
//various stuff here
$tr.addClass('highlight');
//more stuff here
}
Of course you could also use .each()
$("tr").each(function (rowNumber, tr) {
var $tr = $(tr);
//various stuff here
$tr.addClass('highlight');
//more stuff here
})
Documentation can be found here: http://api.jquery.com/each/
Just to point out the obvious: $("tr").addClass('highlight') would work if adding the highlight class to all tr was all that the OP wanted to do :-)

jQuery bind click event to id that has increasing number on every page refresh

Here's what I have. A SharePoint 2010 custom view in a list web part. I have 6 categories and 4 sub-categories. Items do not have to have sub-category but do have to have a category.
The view shows the a blank sub-category witha number next to it. I'm trying to bind a click event to all of them but the ID increases on every page refresh. The base ID is titl[0-9]*[0-9]. Then there is another ID underneath that I want to check as well, it is titl[0-9]*_[0-9]1.
So I've tried using the regex selector for jQuery and it doesn't bind correctly. It finds the object but doesn't bind correctly.
I need it to bind to the id and then be able to trigger the onclick event of the next tbody which is the 1_. Then check if the text of it is " " and if so hide the tbody.
My code:
$(":regex(id,titl[0-9]*-[0-9]_) td a").bind('click', function(){
var parent = $(this);
var child = $(this).next("tbody");
var grandchild = $(this).next("tbody td a");
//alert(parent + " | " + child + " | " + grandchild ); //always return undefined??
// Everything below works if I can get the IDs correct for child and grandchild
if($(grandchild).css('display')!='none'){
$(grandchild).click();
if($(grandchild).text()==" "){
$(child).hide();
};
};
});
I'd strongly suggest you need to re-think your IDs - they should be consistent, really.
If you absolutely must work with a variable ID, you can use the "id" attribute in a selector as with any other attribute:
// Any element, ID starts with "titl"
$('[id^="titl"]')
To capture that and re-use it, I'd really suggest you're doing something wrong with your IDs. However, for completeness (although I can't stress enough how much you should try to avoid having to use this), something based on this should be a good (haha, yeah right) starting point
// Long-winded messy hideous foul code
var $title = $('[id^="titl"]'),
title = $title.length && $title.eq(0).attr('id');
if (title !== 0)
{
$('#' + title + ' #' + title + '1 td a').html('Ow.');
}
I'm not sure I get this, but you can target any ID starting with titl, and then filter based on the ID in many other ways inside the function:
$('[id^="titl["]').on('click', function() {
var check = this.id.charAt(10) == '_', //tenth character is underscore ?
parent = $(this),
child = $(this).next("tbody"),
grandchild = $(this).next("tbody td a");
if (check) {
//element matching second selectortype clicked
}else{
if (grandchild.is(':visible')){
grandchild.trigger('click');
if (grandchild.text()==" ") child.hide();
}
}
});
I agree about rethinking your ids if you have control over them. Even if you don't, the StartsWith selector will get you all of the higher level elements, and you can traverse to the lower level ones. Remember that chaining the selectors means that you can match on similar patterns in ids, not paying any attention to the actual values.
One other note: I've never needed to resort to a regex match with jQuery. The CSS3-like selectors are just far too powerful for that.

c.replace is not a function

Hi everyone,
Actually, i got "c.replace is not a function" while i was trying to delete some DOM elements and..i don't understand.
i'd like to delete some tags from the DOM and so, i did it :
var liste=document.getElementById("tabs").getElementsByTagName("li");
for(i=0;i<liste.length;i++)
{
if(liste[i].id==2)
{
$("#tabs").detach(liste[i]);
}
}
I tried .detach and .remove but it's the same. My version of jQuery is 1.7.1.min.js.
Thanks for help.
order of iteration on a NodeLIst
Doing forward iteration of a NodeList that is being modified when you remove an element can be an issue. Iterate in reverse when removing elements from the DOM.
misuse of detach()
Also, the arguments to .detach() do not perform a nested find, but rather act as a filter on the existing element(s) in the jQuery object, and should be passed a string. It seems that you actually want to detach the li, which would mean that you'd need to call .detach() on the li itself...
var liste=document.getElementById("tabs").getElementsByTagName("li");
var i = liste.length
while(i--) {
if(liste[i].id==2) {
$(liste[i]).detach();
}
}
remove() may be preferred
Keep in mind that if you use .detach(), any jQuery data is retained. If you have no further use for the element, you should be using .remove() instead.
// ...
$(liste[i]).remove(); // clean up all data
code reduction
Finally, since you're using jQuery, you could just do all this in the selector...
$('#tabs li[id=2]').remove(); // or .detach() if needed
valid id attributes
Keep these items in mind with respect to IDs...
It's invalid to have duplicate IDs on a page
It's invalid in HTML4 to have an ID that starts with a number
In the selector above, I used the attribute-equals filter, so it'll work, but you should really be using valid HTML to avoid problems elsewhere.
liste is not (yet) a jQuery object. use $(liste[i])
or use
var liste= $('#tabs li');
Maybe I'm missing something, but is the id suppose to match the number 2.
var liste=document.getElementById("tabs").getElementsByTagName("li");
for(i=0;i<liste.length;i++) {
if(liste[i].id==2) {
$(liste[i]).detach();
}
}
Since you are already using jQuery, why not just do:
$("li", "#tabs").filter("#2").detach();
var two = document.getElementById('2');
two.parentNode.removeChild(two);

Using Jquery: Comparing Two Arrays for ANY Match

I'm looking for a concise way to compare two arrays for any match.
I am using this comparison to apply a particular style to any table cell that has matching content. One array is a static list of content that should be contained in at least one table cell on the page. The other array is being generated by JQuery, and is the text of all table cells.
The reason why I have to compare content to apply style is that the HTML document will semantically change over time, is being generated by different versions of excel (pretty awful looking code), and this script needs to accommodate that. I know that the content I'm looking to apply the style to in this document will never change, so I need to detect all matches for this content to apply styles to them.
So, the code should be something like (in english):
for each table cell, compare cell text to contents of array. If there is any match, apply this css to the table cell.
This is what I have so far (and I know it's wrong):
$(document).ready(function(){
$("a.loader").click(function(event){
event.preventDefault();
var fileToLoad = $(this).attr("href");
var fileType = $(this).text();
var makes = new Array("ACURA","ALFA ROMEO","AMC","ASTON MARTIN","ASUNA","AUDI","BENTLEY","BMW","BRITISH LEYLAND","BUICK","CADILLAC","CHEVROLET","CHRYSLER","CITROEN","COLT","DACIA","DAEWOO","DELOREAN","DODGE","EAGLE","FERRARI","FIAT","FORD","GEO","GMC","HONDA","HUMMER","HYUNDAI","INFINITI","INNOCENTI","ISUZU","JAGUAR","JEEP","KIA","LADA","LAMBORGHINI","LANCIA","LAND ROVER","LEXUS","LINCOLN","LOTUS","M.G.B.","MASERATI","MAYBACH","MAZDA","MERCEDES BENZ","MERCURY","MG","MINI","MITSUBISHI","MORGAN","NISSAN (Datsun)","OLDSMOBILE","PASSPORT","PEUGEOT","PLYMOUTH","PONTIAC","PORSCHE","RANGE ROVER","RENAULT","ROLLS-ROYCE / BENTLEY","SAAB","SATURN","SCION","SHELBY","SKODA","SMART","SUBARU","SUZUKI","TOYOTA","TRIUMPH","VOLKSWAGEN","VOLVO","YUGO","Acura","Alfa Romeo","Amc","Aston Martin","Asuna","Audi","Bentley","Bmw","British Leyland","Buick","Cadillac","Chevrolet","Chrysler","Citroen","Colt","Dacia","Daewoo","Delorean","Dodge","Eagle","Ferrari","Fiat","Ford","Geo","Gmc","Honda","Hummer","Hyundai","Infiniti","Innocenti","Isuzu","Jaguar","Jeep","Kia","Lada","Lamborghini","Lancia","Land Rover","Lexus","Lincoln","Lotus","M.G.B.","Maserati","Maybach","Mazda","Mercedes Benz","Mercury","Mg","Mini","Mitsubishi","Morgan","Nissan (Datsun)","Oldsmobile","Passport","Peugeot","Plymouth","Pontiac","Porsche","Range Rover","Renault","Rolls-Royce / Bentley","Saab","Saturn","Scion","Shelby","Skoda","Smart","Subaru","Suzuki","Toyota","Triumph","Volkswagen","Volvo","Yugo");
$("div#carApp").html("<img src='images/loadingAnimation.gif' alt='LOADING...' />");
$("div#carApp").load(fileToLoad, function(){
$("#carApp style").children().remove();
$('#carApp td').removeAttr('style');
$('#carApp td').removeAttr('class');
$('#carApp table').removeAttr('class');
$('#carApp table').removeAttr('style');
$('#carApp table').removeAttr('width');
$('#carApp tr').removeAttr('style');
$('#carApp tr').removeAttr('class');
$('#carApp col').remove();
$('#carApp table').width('90%');
var content = $("#carApp table td");
jQuery.each(content, function() {
var textValue = $(this).text();
if (jQuery.inArray(textValue, makes)==true)
$(this).css("color","red");
});
});
});
});
Any ideas?
You're checking $.inArray(...) == true. inArray actually returns an integer with the index of the item in the array (otherwise -1.) So you want to check if it is greater than or equal to 0.
Here's how you can change your each loop.
$('#carApp td').each(function () {
var cell = $(this);
if ($.inArray(cell.text(), makes) >= 0) {
cell.addClass('selected-make');
}
});
I use a CSS class instead of the style attribute, because it's better practice to put styling in a CSS file rather than in your JavaScript code. Easier to update that way (especially when you want to apply the same style in multiple places in your code.)
Other points worth noting:
jQuery selections have the each(...) function as well. So you can do $(...).each(...) instead of jQuery.each($(...), ...)
jQuery and $ are the same object as long as you don't have other frameworks that redefine the $ variable. Therefore you can do $.inArray(...) instead of jQuery.inArray(...). It's a matter of taste, though.
Have you had a look at $.grep() ?
Finds the elements of an array which
satisfy a filter function. The
original array is not affected. The
filter function will be passed two
arguments: the current array item and
its index. The filter function must
return 'true' to include the item in
the result array.
An optimization would be to make makes a hash (aka dictionary):
var makes = { "ACURA": 1,"ALFA ROMEO": 1,"AMC": 1, ...};
Then you don't have to iterate makes every time with inArray.
...
var textValue = $(this).text();
if (makes[textValue] == 1)
$(this).css("color","red");
}
...

Categories