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.
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 :-)
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.
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);
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");
}
...