Avoid double looping to insert nodes and activate them? - javascript

A common problem I have is the need to create multiple DOM nodes in a loop, and then activate those nodes in some way, either by applying a plugin, an event handler or similar. The activation step requires that the element actually exist first.
So you end up doing something like:
// Loop 1: Create the nodes
var HTML = '<tr id="UID">';
for(var k in Fields){ // Fields is an object!
HTML += '<td>';
HTML += '<input class="ActivateMe"/>';
HTML += '</td>';
}
var HTML += '</tr>';
$TableBody.children('tr').first().before(HTML);
// Loop 2: Activate the new nodes
$('#'+UID).children('td').children('.ActivateMe').each(function(index){
$(this).InitSomePlugin();
});
The code above is simplified for the question, but assume that each element inside a given cell can be different (maybe an input, may be a div), and might also require a different plugin (Maybe it's a color picker, maybe it's a combo box).
Is it possible to avoid looping over the data set twice and doing the insert and activate in one go? I think it may be possible by appending the nodes within the first loop, which would also allow activation in the first loop. But it is generally considered bad practice to use append in a loop rather than store all your HTML it in a var and append all the HTML at once. At the same time, looping over the same set of data two times seems inefficient too. What is the best way to handle this scenario with minimal performance impact?

Yes. Don't build a lengthy HTML string, but create the elements programmatically in the first loop so that you can direclty instantiate your plugin on them:
var $TableBody = …,
var $row = $('<tr>', {id:UID});
for(var k in Fields) { // sure that Fields is an object?
// For an array, use a normal for loop
var $cell = $('<td>');
var $input = $('<input class="ActivateMe"/>');
$input.InitSomePlugin();
$input.appendTo($cell);
$cell.appendTo($row);
}
$row.prependTo($TableBody);
You might need to do the appends before calling .InitSomePlugin(). You also might want to nest the calls and use chaining for shortening the code:
var $row = $('<tr>', {id:UID}).prependTo(…);
for(var k in Fields)
$('<input class="ActivateMe"/>')
.appendTo($('<td>').appendTo($row))
.InitSomePlugin();

$tableBody = $('table');
// Prepare an DOM object but don't append it to DOM yet.
$tr = $('<tr></tr>',{
id: "UID"
});
// Loop over any condition you would see fit.
for(var i = 0; i < 2; ++i) {
$td = $('<td></td>',{
// Prepare your input element with your event bound to it
"html": $('<input/>', {"class": "ActivateMe"}).InitSomePlugin();
});
// append your td element
$tr.append($td);
}
// Add your tr element to the beginning of your table with prepand method.
$tableBody.prepend($tr);

Since the activation step requires that the element already exists and (probably) is in the DOM, then you only have two choices here:
1) You can create each DOM element individually (with things like document.creatElement() or jQuery's $(html)) such that you have saved DOM object references that you can later use for intializing the plugin.
or
2) You can build up a string of HTML as you are doing. Insert that string, letting the browser create all the elements for you and then you will have to find the appropriate DOM elements in order to initialize the plugins.
Tests have shown that it is often the case that browsers will create lots of HTML objects faster when given a string of HTML rather than manually creating and inserting individual DOM objects so there is no particular issue with using the string of HTML.
There is no 100% right or wrong answer here. Performance is probably not the primary issue unless you have hundreds to thousands of these DOM elements. I tend to go with whichever path leads to the cleanest and simplest code.
In your case, you have to iterate over the Fields object so you can't avoid that. You have to find the first row of your table so you can't avoid that.
If you've decided that building the string of HTML is the most expedient approach to writing the code (which it probably is here), then you can't avoid refinding the objects you need to activate in the DOM.
You can be as efficient about things as possible.
Here's a little bit of streamlining that stays with the basic philosophy:
// create the new rows
var HTML = '<tr id="UID">';
for(var k in Fields) {
HTML += '<td>' + '<input class="ActivateMe"/>'+ '</td>';
}
HTML += '</tr>';
// create an insert new content, save reference to new content
var newObj = $(HTML);
$TableBody.prepend(newObj);
// now activate the plugin on the appropriate objects in the new content
newObj.find(".ActivateMe").InitSomePlugin();
Streamlining steps:
Create each cell in one statement rather than three
When you create the new row object, keep a reference to it so we don't have to find it again.
When you add the new content, use .prepend() to make it the first row rather than finding all the rows and selecting the first one
With you initalize the plugin, there's no need for a .each() loop if you're running the same jQuery method on every object. You can so it like this: newObj.find(".ActivateMe").InitSomePlugin(); without .each().
You could also create the DOM objects yourself and not use the HTML string and keep track of the objects that need to be activate as you go so they don't have to be found again:
// create the new rows
var row = $('<tr id="UID"></tr>'), input, item, activates = [];
for(var k in Fields) {
item = $('<td>');
input = $('<input class="ActivateMe"/>')
activates.push(input);
item.append(input);
row.append(item);
}
// insert row into table
$TableBody.prepend(row);
// now activate the plugin on the appropriate objects that are now inserted
$(activates).InitSomePlugin();
Purists might "like" the second option better than the first option because it's not using an HTML string, but unless you're doing this hundreds to thousands of times such that performance is paramount (in which case you'd have to test which method actually performs better and diagnose why), I can't honestly say that the second is better than the first. I like the coding simplicity of the first and finding a few class objects in a specific table just isn't an expensive operation.

Related

JQuery & Javascript: Intermittant mis-matching of clicked div and returned div?

Thanks for giving this a look. I'll start with a quick image. Clicking on any of the red-boxed search results seems to return the <div> for the item directly above it.
Here I clicked on 1613 CAROUSEL CIR, but the event returned the id/content for the item representing 1612..
Sometimes it's even weirder, for example, every item following 1420 might point back to 1420. So it's not always a conflict with a <div> and it's immediate neighbor, although that's usually the case.
I've been unable to find any definite pattern in this behavior. Sometimes it's just one or two items in the list; sometimes most of the list is affected, with "a good portion" of results pointing to one particular div.
There's only one true consistency--typically the first several items work as expected, and short lists will be 100% correct. But really long lists (50+) are not necessarily worse than semi-long lists (20+).. :/
The code building the search results iterates over JSON data retrieved by a JQuery $.ajax() call, and this is the relevant code building the visible search results:
if( result.d.length > 0 )
{
var i=0;
for(i; i<result.d.length; i++)
{
// ..there's a bunch of irrelevant code here to set the map bounds..
// ..then I build the HTML using JQuery like this
//
var date = new Date();
var divID = "searchItemDiv" + date.getTime().toString();
var $searchItemDiv = $( "<div id='" + divID + "' class='searchItemDiv'>"+result.d[i].Description+"</div>" );
$searchItemDiv.data('itemData', result.d[i]);
$searchItemDiv.bind('click', onSearchItemClick);
$( "#searchResults" ).append($searchItemDiv);
}
}
While I don't suspect the event handler is the issue, the relevant code there looks like this:
function onSearchItemClick(event)
{
if( event.target.id.toString() !== '' )
{
// I clicked 1613, but event returned DIV with text of "1612"??
//
var item = $('#'+event.target.id.toString()).data('itemData');
alert( event.target.id.toString()+"\n"+
$('#'+event.target.id.toString()).text() );
// ..more irrelevant stuff to show a popup of property data..
}
}
FireFox, Chrome, and IE all demonstrate the same behavior, so it's not browser-specific.
I'm relatively sure this is not the product of a race condition during the render phase, but I'm not comfortable-enough with JavaScript to know that for certain.
I'm pretty baffled by this. FWIW, I'm a former Flex & C# developer and relatively new to JavaScript/JQuery development, so there may be a gotcha related JavaScript contexts and/or JQuery that I'm stepping into.
I would say, instead of binding the click function within a for-loop, just select all of the searchItemDiv's after the for-loop binds the data to them, and register a click function on all of them at once. You don't need a separate line to define variable i, just do it in the for statement. I also wouldn't try to generate random IDs with new Dates, that just seems unnecessary. Registering all click functionality at once will also make your click handler much simpler:
if( result.d.length > 0 )
{
for(var i = 0; i<result.d.length; i++)
{
// ..there's a bunch of irrelevant code here to set the map bounds..
// ..then I build the HTML using JQuery like this
// select the i'th searchItemDiv
$searchItemDiv = $($('.searchItemDiv')[i])
// give it the data
$searchItemDiv.data('itemData', result.d[i]);
$( "#searchResults" ).append($searchItemDiv);
}
// then register all the click handlers at once, very simple
$('.searchItemDiv').bind('click', function() {
var item = $(this);
alert(item.text());
});
}
--EDIT--
also, do the searchItemDivs already exist or are you trying to create them?
if you're trying to create them, you might want this in the for-loop instead:
for(var i = 0; i<result.d.length; i++)
{
// ..there's a bunch of irrelevant code here to set the map bounds..
// ..then I build the HTML using JQuery like this
// create a searchItemDiv
$searchItemDiv = $('<div class="searchItemDiv"></div>')
// give it the data
$searchItemDiv.data('itemData', result.d[i]);
$( "#searchResults" ).append($searchItemDiv);
}
I'm guessing that is what you want to do.
I think your problem depends on your searchItemDiv id.
Using the date doesn't ensure ids are unique so when you retrieve the object by id it will return an element (probably the first) with the same id.
Make sure to assign unique id on your elements.

Does jQuery use create document fragment inside each loops?

So I've read that jQuery uses document fragments internally to make rendering faster. But I am wondering if anyone knows if jQuery would use createDocumentFragment in this situation where I'm appending img elements to the DOM using the each loop?
var displayArray = []; // Lots of img elements
$.each(displayArray, function()
{
$('#imgSection').append(this);
});
Or would I need to use this code in order to reduce the number of browser reflows?
var displayArray = []; // Lots of img elements
var imgHolder = $('<div/>');
$.each(displayArray, function()
{
imgHolder.append(this);
});
$('#imgSection').append(imgHolder);
Also, the displayArray is populated by other code, not shown here, that creates img elements based off of paths in a JSON file.
Thank you for any advice.
Why all the looping to add elements?
$('#imgSection').append("<div>" + displayArray .join("") + "</div>");
Okay so it is elements.
The quickest way is going to be using append with the array itself.
$("#out").append(elems);
other option using one div to append is
var div = $("<div/>").append(elems);
$("#out").append(div);
BUT appending a lot of images at once is going to be bad unless they are preloaded. That will be a bunch of http requests being queued up.
jsPerf test cases
No, if you use $.each() then jQuery won't use a DocumentFragment - jQuery has no way of knowing what you're going to do inside the loop and each iteration is independent.
The point of the document fragment is that you don't have to wrap all your new elements up in a wrapper element as you've done in your second example to limit the reflows.
jQuery apparently will use a document fragment if you pass an array of elements directly to .append() instead of iterating over them yourself.
If you really care about reflows (and have noticed the displaying to be slow), you can hide and show the image-holding element:
var displayArray = […]; // Lots of img elements
var holder = $('#imgSection').hide();
for (var i=0; i<displayArray.length; i++)
holder.append(displayArray[i]);
holder.show();

In jQuery, how to efficiently add lots of elements?

I currently have a sketch for a truthtable generator. While it works fine, it is rather slow. Each combination of boolean values I have added to a <table> using jQuery. For each value, a <td> element is created by jQuery and then added to the <table>. Moreover, I'm using jQuery UI for a nice looking buttonset instead of radio buttons.
In my posted code extract, table is an array containing each boolean combination. Perhaps my code is a little inscrutable but what it basically comes down to is that with 4 boolean variables (16 possibilities), 96 <td> elements are created with classes added and data attributes set. In the second part, three groups of three radio buttons are created and converted into a jQuery UI buttonset.
Using a timer I figured out that it takes approximately 0.4 seconds before everything is filled up. Not that big of a deal, but it is certainly noticeable and does not have a positive effect on the user as each time he enters a different boolean formula it takes half a second to load.
$table = $('#table');
$.each(table, function(k, v) {
$tr = $('<tr>').addClass('res').data('number', k);
$.each(v[0], function(k2, v2) {
$td = $('<td>').text(v2).addClass(v2 ? 'green notresult' : 'red notresult');
for(var i = 0; i < 4; i++) {
$td.data(i, i === k2);
}
$tr.append($td);
});
$tr.append($('<td>').addClass('spacing'));
$table.append(
$tr.append(
$('<td>').text(v[1]).addClass(v[1] ? 'green result' : 'red result')
)
);
});
// ... here is some code that's not slowing down
$radiobuttonsdiv = $('#radiobuttonsdiv');
for(var i = 0; i < 4; i++) {
var $radiobase = $('<input>').attr('type', 'radio')
.attr('name', 'a'+i)
.click(handleChange);
// .label() is a custom function of mine which adds a label to a radio button
var $radioboth = $radiobase.clone().val('both').label();
var $radiotrue = $radiobase.clone().val('true').label();
var $radiofalse = $radiobase.clone().val('false').label();
var $td1 = $('<td>').addClass('varname').html(i);
var $td2 = $('<td>').attr('id', i);
$td2.append($radioboth, $radiotrue, $radiofalse).buttonset();
var $tr = $('<tr>').append($td1, $td2);
$radiobuttonsdiv.append($tr);
}
My questions are:
How could table-filling using jQuery be optimized? Or is a table perhaps not the best solution in this scenario?
Is it perhaps possible to suspend drawing, since that might be slowing everything down?
Try to avoid using .append in a loop, especially if you're adding a lot of elements. This is always a performance killer.
A better option is to build up a string with the markup and do a single (or as few as possible) .append when your loop is finished.
I see that you're using .data, which makes things a bit more complicated, but another option is to use the metadata plugin to attach structured markup to existing tags.
To defer rendering, you could try creating a new table without adding it to the DOM like:
var myDisconnectedTable = $('<table></table>')
Then adding your rows to that table:
myDisconnectedTable.append(...)
Then append your table to the div you want it in:
$('#idOfMyDiv').append(myDisconnectedTable)
I don't know that it will help, but it's worth a shot.

Most efficient way to create form with Jquery based on server data

I have a database webmethod that I call via Jquery ajax. This returns an array of data objects from the server. For each data object I want to populate a form with maybe a couple dozen fields.
What is the most efficient way to generate and populate these forms.
Right now, I create a string of the html in my javascript code for each record and then add it to the page. Then I add some events to the new elements.
This works OK in firefox, about 700 ms total for an array of 6 elements, and 4500 ms for an array of 30 elements. However, this is an internal app for my company, and the clients can only use IE8 on their machines. In IE8, this takes 2-10 SECONDS! for and array of length 6 and 47 seconds the one time it was actually able to complete for an array of length 30. Not sure what the ##$% IE8 is doing, but anyways... I need something more efficient it seems...
Thanks!
MORE INFO:
Edit: first thing I do is:
$("#approvalContainer").empty();
Web method signature:
[WebMethod]
public List<User> ContractorApprovals()
So I call the method and then do this with the data:
for (var i = 0; i < data.length; i++) {
displayUserResult("#approvalContainer", data[i], false);
}
formEvents("#approvalContainer");
$("#approvalContainer").show("slow");
displayUserResult looks like this:
var div = "<div style=\"width:695px\" class=..........."
fillForm(div,data)
$("#approvalContainer").append(div)
formEvents does things like add click events, create styles buttons and add watermarks.
fillForm does stuff like this:
$(userForm).find(".form-web-userName").text(userObject._web._userName);
where userForm is the div created in the displayUserResult function, and userObject is one of the objects in the array returned by the ajax call.
Please let me know if you need more information.
You are doing too much DOM manipulation. Every .append and .find and .text inside your loop makes it slower.
The most efficient way is to build an entire HTML string and append that once. I'm not going to bother with the displayUserResult function, and just modify your function that handles the data:
var div = "<div style=\"width:695px\" class=...........",
html = "";
for (var i = 0; i < data.length; i++) {
// Keep adding on to the same html string
html += div + data[i]._web._userName + "</div>";
}
// After the loop, replace the entire HTML contents of the container in one go:
$("#approvalContainer").html(html);
However, while this is fast, note that this is only appropriate if _userName doesn't contain special characters, or is already HTML escaped.

How to insert a a set of table rows after a row in pure JS

I have the following problem:
I need to insert N rows after row X. The set of rows I need to insert is passed to my function as chunk of HTML consisting of TR elements. I also have access to the TR after which I need to insert.
This is slightly different then what I have done before where I was replacing TBODY with another TBODY.
The problem I am having is that appendChild requires a single element, but I have to insert a set.
Edit:
Here is the solution :
function appendRows(node, html){
var temp = document.createElement("div");
var tbody = node.parentNode;
var nextSib = node.nextSibling;
temp.innerHTML = "<table><tbody>"+html;
var rows = temp.firstChild.firstChild.childNodes;
while(rows.length){
tbody.insertBefore(rows[i], nextSib);
}
}
see this in action:
http://programmingdrunk.com/test.htm
if(node.nextSibling && node.nextSibling.nodeName.toLowerCase() === "tr")
What's this for? I don't think you need it. If node.nextSibling is null, it doesn't matter. You can pass that to insertBefore and it will act the same as appendChild.
And there's no other element allowed inside a tbody than ‘tr’ anyway.
for(var i = 0; i < rows.length; i++){
tbody.insertBefore(rows[i], node.nextSibling);
This won't work for multiple rows. Once you've done one insertBefore, node.nextSibling will now point to the row you just inserted; you'll end up inserting all your rows in reverse order. You'll need to remember the original nextSibling.
ETA: plus, if ‘rows’ is a live DOM NodeList, every time you insert one of the rows into the new body, it removes it from the old body! Thus, you are destroying the list as you iterate over it. This is a common cause of ‘every other one’ errors: you process item 0 of the list, and in doing so remove it from the list, moving item 1 down into where item 0 was. Next you access the new item 1, which is the original item 2, and the original item 1 never gets seen.
Either make a copy of the list in a normal non-live ‘Array’, or, if you know it's going to be a live list, you can actually use a simpler form of loop:
var parent= node.parentNode;
var next= node.nextSibling;
while (rows.length!=0)
parent.insertBefore(rows[0], next);
i have to insert a set.
Usually when you think about inserting a set of elements at once, you want to be using a DocumentFragment. However, unfortunately, you can't set ‘innerHTML’ on a DocumentFragment, so you'd have to set the rows on a table like above, then move them one by one into a DocumentFragment, then insertBefore the documentFragment. Whilst this could theoretically be faster than appending into the final target table (due to less childNodes list-bashing), in practice by my testing it isn't actually reliably significantly faster.
Another approach is insertAdjacentHTML, an IE only extension method. You can't call insertAdjacentHTML on the child of a tbody, unfortunately, in the same way as you can't set tbody.innerHTML due to the IE bug. You can set it inside a DocumentFragment:
var frag= document.createDocumentFragment();
var div= document.createElement(div);
frag.appendChild(div);
div.insertAdjacentHTML('afterEnd', html);
frag.removeChild(div);
node.parentNode.insertBefore(frag, node.nextSibling);
Unfortunately, somehow insertAdjacentHTML works very slowly in this context for some mysterious reason. The above method is about half the speed of the one-by-one insertBefore for me. :-(
What altCognito said, just be aware of the big innerHTML tbody bug in case you want to replace everything at once using innerHTML because you're doing a lot of this and the DOM operations turn out to be too slow.
There is an error with IE using innerHTML to insert the rows, so that won't work. Straight from the horse's mouth: http://www.ericvasilik.com/2006/07/code-karma.html
I would recommend just updating the tbody, but appending your code where it belongs in the new structure.
With your table object you can insert a new row into it.
var tbl = document.getElementById(tableBodyId);
var lastRow = tbl.rows.length;
// if there's no header row in the table, then iteration = lastRow + 1
var iteration = lastRow;
var row = tbl.insertRow(lastRow);
it will insert a row at the end of your table.
This worked a lot better for me when inserting a row anywhere other than in the last position,
var rowNode = refTable.insertRow(0);
var cellNode = rowNode.insertCell();
You'll need to append them one by one.
you need to determine if row X is the last row...
//determine if lastRow...
//if not, determine row_after_row_x
for(i in n_rows){
if(lastRow){
tbodyObj.appendChild(row_i_of_n);
} else {
tbodyObj.insertBefore(row_i_of_n, row_after_row_x);
}
}

Categories

Resources