0

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?

Nick
  • 10,904
  • 10
  • 49
  • 78

3 Answers3

2

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();
Bergi
  • 630,263
  • 148
  • 957
  • 1,375
  • Yep . . . typed it up faster than I could. :) – talemyn Mar 26 '14 at 17:55
  • Fields is an Object. I'm aware that I can do the appends in the first loop, but since appending multiple times in a loop is slower than building text in a loop and appending it at once, the question is really about the performance implications of one vs the other, or if there is a third alternative. – Nick Mar 26 '14 at 17:57
  • Are you sure you can active the plugin on an element BEFORE it's inserted in the DOM? Some plugins would not work that way. – jfriend00 Mar 26 '14 at 18:00
  • 1
    @Nick: Who said that it was slower? The `.appendTo($TableBody);` is done in the end once, so there are no unnecessary updates. Also, for using the elements further in the script (`.initSomePlugin()`) this is just the way to go, markup strings are simply impractical there. – Bergi Mar 26 '14 at 18:02
  • No, that's the problem- it must exist in the DOM first. – Nick Mar 26 '14 at 18:02
  • @jfriend00: That's what I meant with "*You might need to do the appends before calling `.InitSomePlugin()`*" - moving them to the top is easy. – Bergi Mar 26 '14 at 18:03
  • @Bergi: Doesn't creating Jquery Nodes, ie. `$('')` cause more overhead than simple string concatenation? – Nick Mar 26 '14 at 18:03
  • It requires extra code to keep track of all the input objects you create because they aren't actually in the DOM until your final line of code so you'd have to activate them after that. It's not rocket science to change it, but it's not just moving something in your code either. – jfriend00 Mar 26 '14 at 18:16
  • @Nick: Yes, it does cause more overhead of course, but it does save you the whole `$('#'+UID).children('td').children('.ActivateMe').each(function(){ $(this)` selector, traverse, function call, wrapping… The nodes will need to get created and individually referenced anyway, so no reason to build and parse HTML strings. – Bergi Mar 26 '14 at 18:19
  • @jfriend00: Moving the final line before the loop? – Bergi Mar 26 '14 at 18:20
  • It is generally better to build up your DOM sub-structure BEFORE you add the root to the DOM because you aren't incurring layout or repaint processing when it isn't in the DOM yet so, no I wouldn't recommend inserting the row object first. That's why my second code option keeps an array of the inserted objects that need to be activated so I can reference them all directly after everything is in the DOM. – jfriend00 Mar 26 '14 at 18:25
  • Yes, it is better micro-performancewise so if you require this you will have to resort to the array. Yet, not every DOM insertion does trigger a reflow/repaint; browsers optimize this very well and I would expect the difference to be negligible. – Bergi Mar 26 '14 at 18:30
  • @jfriend00: As I understand the [docs of `appendTo`](http://api.jquery.com/appendTo/), it does return a collection with the inserted elements (the input, the row) and not the target containers to which they were inserted. – Bergi Mar 26 '14 at 18:36
  • 1
    @jfriend00: Check my nesting: `var x = $("
    Fee
    ").appendTo($("
    Foo
    ").appendTo("#test"));` works. You were appending the `fee` first to `foo` and then to `test` were it ended up.
    – Bergi Mar 26 '14 at 18:45
  • OK, your nesting is there. Easy to be confused by it though. – jfriend00 Mar 26 '14 at 18:56
  • Since you are discussing it, `$(document.createElement("*TAG*"))` is the fastest way to create a new, basic element in jQuery (4-5 time faster than using `$("<*TAG* />")` or `$("<*TAG*>*TAG*>")`). See here for more information: http://stackoverflow.com/questions/268490/jquery-document-createelement-equivalent/268520#268520 – talemyn Mar 26 '14 at 21:21
  • @talemyn: Yes it is faster, but readability wins. If you were that much into performance, you would not use jQuery at all. – Bergi Mar 26 '14 at 23:16
  • @Bergi - you'd be hard pressed to convince me that a piece of code that says "create element" in it is less readable than one that doesn't, when that code is creating an element. LOL – talemyn Mar 27 '14 at 15:21
  • @talemyn: I fear to most jQuery developers it is :-/ At least, the "jQuery style" is shorter and more consistent with other html strings passed to `$()` – Bergi Mar 27 '14 at 15:24
  • @Bergi . . . okay, "more terse" I can buy. :) That being said, I've switched over to using that method exclusively . . . particularly on highly dynamic pages (e.g., like a situation I was working on a while ago where the main content of the page constructed based on a dynamic, XML-based configuration file), 4-5 times faster can actually make a noticeable difference, if you are creating a lot of elements. – talemyn Mar 27 '14 at 15:31
-1

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:

  1. Create each cell in one statement rather than three
  2. When you create the new row object, keep a reference to it so we don't have to find it again.
  3. 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
  4. 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.

jfriend00
  • 683,504
  • 96
  • 985
  • 979
  • You're right that it hardly matters performancewise. Yet I like the coding simplicity and purity of the second better :-) – Bergi Mar 26 '14 at 18:22
  • 1
    @Bergi - and I like the simplicity of a string of HTML, certainly the coding logic is simpler in the first option. As I recommended, the OP should select the option that they feel is the cleanest and simplest code which we have shown is personal opinion unless you want to actually care about performance and measure performance and choose based on performance rather than simplicity. – jfriend00 Mar 26 '14 at 18:26
-1
$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);
breakdown1986
  • 439
  • 1
  • 6
  • 12
  • Note that I have quotes around object keys to prevent against IE issues. As well as '' and '' where IE expects the closing tags when creating DOM objects through jquery – breakdown1986 Mar 26 '14 at 18:11
  • 1
    I don't think you can call [`.html()`](http://api.jquery.com/html/) with a jQuery collection – Bergi Mar 26 '14 at 18:15
  • 1
    `$('')` is totally valid as it doesn't parse it, but translates to `$(document.createElement("tr"))` – Bergi Mar 26 '14 at 18:16